Skip to content

perf(runtime): give the relational operators the numeric fast path + and === already have #2431

perf(runtime): give the relational operators the numeric fast path + and === already have

perf(runtime): give the relational operators the numeric fast path + and === already have #2431

Workflow file for this run

# CI arm for the exception transport (#7302) and its owned unwinder.
#
# Two things need exercising, and neither is visible from behavior alone:
#
# 1. The owned single-phase walker must actually CARRY throws. If it
# silently starts declining every throw, every test still passes — the
# system unwinder is the fallback and semantics are identical — and the
# only symptom is that the optimization quietly stopped happening. So
# this job asserts `fallback=0` with a nonzero `fast=` count, not merely
# that the program produced the right answer.
# 2. The walker's agreement with the system unwinder must be re-proven, not
# assumed from the bring-up run. `PERRY_EH_WALKER=diff` predicts the
# landing for every throw and asserts it inside the personality against
# the system unwinder's answer; this job requires a nonzero verified
# count and zero declines.
#
# Both are the "assert the subject was LIVE" rule from CLAUDE.md's "Four
# ways a gate can be unable to fail" — a green run here means the fast path
# ran and agreed, not that nothing threw.
#
# NON-REQUIRED until it has run green once on main; promote afterwards (a
# new gate has never been green, so promoting first blocks every PR).
name: eh-transport
# No trigger-level `paths:` — a required check whose workflow is
# path-filtered at the trigger never creates a check run for unrelated PRs,
# so the context waits forever and blocks the merge. Filtering lives in the
# `changes` job: a job skipped by `if:` still reports a check run.
on:
pull_request:
# PR arm is OPT-IN via the `run-extended-tests` label (see the header of
# test.yml and docs/src/testing/ci-tiers.md): an unlabelled PR still gets
# a run, but every job in it is skipped, which costs no runner slot. The
# main-line arm (schedule / tags) is unchanged. `labeled` re-fires the run
# when the label lands.
types: [opened, synchronize, reopened, labeled]
# POST-MERGE ARM: staggered six-hourly sweep of `main`, NOT one run per merge.
# `push: branches: [main]` starved this gate and nine others for two days
# (#7856): ~29 jobs enqueued per merge at 58 merges/day against a ~9-job
# concurrency ceiling. The pull-request arm is unchanged -- every PR is still
# measured. ***DO NOT RESTORE `push: branches: [main]`.***
# Rationale, measurement and cost: docs/src/testing/ci-gate-scheduling.md
schedule:
- cron: "32 */6 * * *"
push:
tags: ["v*"]
workflow_dispatch:
concurrency:
# ***#7966: KEY EVERY MAIN-LINE RUN ON `github.run_id`, NOT `github.sha`.***
# The previous expression read `github.event_name == 'push' && github.sha ||
# github.ref`. That was #7205's fix and it keyed on the event being `push` --
# correct while the main-line arm WAS `push: branches: [main]`. #7856 moved the
# main-line arm to `schedule:`, which falls through to `github.ref` (constant
# `refs/heads/main`), so every scheduled run shared one group again and #7205
# came straight back. Measured 2026-08-12 on all ten scheduled gates, the same
# shape every time: oldest run `queued` holding the group, the two after it
# `cancelled` with `jobs: 0`, newest `pending`. `github.run_id` is unique per
# run, so schedule / tag-push / workflow_dispatch each get a group of their own
# and none can supersede another. PR runs keep the shared per-ref group and
# keep superseding themselves, which is still what we want.
group: eh-transport-${{ github.event_name }}-${{ github.event_name == 'pull_request' && github.ref || github.run_id }}
# One group per main COMMIT, cancelling PR runs only. `cancel-in-progress:
# false` alone does not protect a `main` run: GitHub allows at most one
# PENDING run per group and cancels the previously pending one when a new run
# enters, regardless of that setting. That is #7205 — measured here as EIGHT
# consecutive `main` runs cancelled, zero executions. Keying push runs on the
# SHA gives every merged commit a group of its own.
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
changes:
# PR arm is opt-in (label `run-extended-tests`); see the `on:` block.
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-extended-tests')
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
pull-requests: read
outputs:
relevant: ${{ steps.filter.outputs.relevant }}
steps:
- id: filter
env:
GH_TOKEN: ${{ github.token }}
run: |
# Anything that is not a pull request (the scheduled `main` sweep, a
# release tag, a manual dispatch) measures unconditionally: there is no
# PR file list to filter on, and #7856's starvation is precisely what
# happens when a post-merge arm quietly declines to run. Testing for
# `= "push"` here was correct only while `push: branches: [main]` was
# the post-merge trigger; under the schedule it would fall through to
# the PR branch, dereference an empty PR number and fail the step.
if [ "${{ github.event_name }}" != "pull_request" ]; then
echo "relevant=true" >> "$GITHUB_OUTPUT"
exit 0
fi
files=$(gh api "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files" \
--paginate --jq '.[].filename')
if echo "$files" | grep -qE '^(crates/perry-runtime/src/(eh|eh_walker|eh_windows|exception)\.rs$|crates/perry-codegen/src/stmt/|test-files/test_(gap|issue)_7302_|\.cargo/config\.toml$|Cargo\.toml$|\.github/workflows/eh-transport\.yml$)'; then
echo "relevant=true" >> "$GITHUB_OUTPUT"
else
echo "relevant=false" >> "$GITHUB_OUTPUT"
fi
eh-transport:
needs: changes
if: needs.changes.outputs.relevant == 'true'
# arm64 macOS: the owned walker's current platform. Other targets fall
# back to the system unwinder by design, so there is nothing to assert
# there beyond the behavior the parity suites already cover.
runs-on: macos-15
timeout-minutes: 60
steps:
- uses: actions/checkout@v7
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- uses: ./.github/actions/setup-llvm22
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version-file: .node-version
- uses: Swatinem/rust-cache@v2
with:
shared-key: "${{ runner.os }}-perry"
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Build compiler + runtime archives
run: |
cargo build --profile perry-dev -p perry -p perry-runtime-static \
-p perry-stdlib-static
- name: Owned transport — liveness, agreement, fallback, parity
run: |
set -euo pipefail
export PERRY_RUNTIME_DIR="$PWD/target/perry-dev"
export PERRY_NO_AUTO_OPTIMIZE=1
BIN=target/perry-dev/perry
# Two subjects: the structural try/catch matrix (nesting, finally
# on both edges, return-in-try, rethrow) and the GC probe that
# throws across a collection point.
for SRC in test-files/test_gap_7302_invoke_eh_paths.ts \
test-files/test_gap_7302_gc_throw_across_collection.ts; do
NAME=$(basename "$SRC" .ts)
"$BIN" "$SRC" -o "/tmp/$NAME"
# Oracle parity first: a fast path that is wrong is worse than
# no fast path, so nothing below is meaningful without this.
node --experimental-strip-types --no-warnings "$SRC" > "/tmp/$NAME.node"
"/tmp/$NAME" > "/tmp/$NAME.perry"
diff "/tmp/$NAME.node" "/tmp/$NAME.perry"
# LIVENESS: the walker carried every throw. `fallback=0` alone
# would pass vacuously on a program that never threw, so require
# a nonzero fast count too.
stats=$(PERRY_EH_WALKER=stats "/tmp/$NAME" 2>&1 >/dev/null | grep "eh-walker: fast=")
echo "$NAME: $stats"
fast=$(echo "$stats" | sed -E 's/.*fast=([0-9]+).*/\1/')
fb=$(echo "$stats" | sed -E 's/.*fallback=([0-9]+).*/\1/')
[ "$fast" -gt 0 ] || { echo "walker never took the fast path"; exit 1; }
[ "$fb" -eq 0 ] || { echo "walker fell back $fb time(s)"; exit 1; }
# AGREEMENT: re-prove the prediction against the system
# unwinder on this build, rather than trusting the bring-up run.
# The personality aborts on mismatch; the tally must show real
# verifications and no declines.
diffout=$(PERRY_EH_WALKER=diff "/tmp/$NAME" 2>&1 >/dev/null | grep "eh-walker diff:")
echo "$NAME: $diffout"
ver=$(echo "$diffout" | sed -E 's/.*verified=([0-9]+).*/\1/')
dec=$(echo "$diffout" | sed -E 's/.*declined=([0-9]+).*/\1/')
[ "$ver" -gt 0 ] || { echo "diff mode verified nothing"; exit 1; }
[ "$dec" -eq 0 ] || { echo "walk declined $dec throw(s)"; exit 1; }
# FALLBACK: the escape hatch must still produce correct output —
# it is the bisection knob and the every-other-platform path.
PERRY_EH_WALKER=off "/tmp/$NAME" > "/tmp/$NAME.off"
diff "/tmp/$NAME.node" "/tmp/$NAME.off"
done
- name: Throws on perry/thread workers
run: |
set -euo pipefail
export PERRY_RUNTIME_DIR="$PWD/target/perry-dev"
export PERRY_NO_AUTO_OPTIMIZE=1
# Each worker walks ITS OWN stack while the row cache and image
# index are process-global behind a mutex. node cannot run
# perry/thread, so the oracle here is the system unwinder itself:
# walker-on must equal walker-off, and diff mode must agree on
# every worker throw.
SRC=test-files/test_issue_7302_thread_throws.ts
target/perry-dev/perry "$SRC" -o /tmp/eh_threads
/tmp/eh_threads > /tmp/eh_threads.on
PERRY_EH_WALKER=off /tmp/eh_threads > /tmp/eh_threads.off
diff /tmp/eh_threads.on /tmp/eh_threads.off
stats=$(PERRY_EH_WALKER=stats /tmp/eh_threads 2>&1 >/dev/null | grep "eh-walker: fast=")
echo "threads: $stats"
fast=$(echo "$stats" | sed -E 's/.*fast=([0-9]+).*/\1/')
fb=$(echo "$stats" | sed -E 's/.*fallback=([0-9]+).*/\1/')
[ "$fast" -gt 0 ] || { echo "walker never took the fast path on workers"; exit 1; }
[ "$fb" -eq 0 ] || { echo "walker fell back $fb time(s) on workers"; exit 1; }
diffout=$(PERRY_EH_WALKER=diff /tmp/eh_threads 2>&1 >/dev/null | grep "eh-walker diff:")
echo "threads: $diffout"
dec=$(echo "$diffout" | sed -E 's/.*declined=([0-9]+).*/\1/')
ver=$(echo "$diffout" | sed -E 's/.*verified=([0-9]+).*/\1/')
[ "$ver" -gt 0 ] || { echo "diff mode verified nothing on workers"; exit 1; }
[ "$dec" -eq 0 ] || { echo "walk declined $dec worker throw(s)"; exit 1; }
- name: Cross-helper throws + unwind-table self-check
run: |
set -euo pipefail
export PERRY_RUNTIME_DIR="$PWD/target/perry-dev"
export PERRY_NO_AUTO_OPTIMIZE=1
# Throws crossing runtime Rust frames (throwing getter, throwing
# toString, JSON.parse, map callback, deep recursion). These are
# the frames the panic=abort + force-unwind-tables contract exists
# for; a runtime built without the tables aborts here with the
# self-check's message instead of stranding the throw silently.
SRC=test-files/test_gap_7302_throw_across_helper_frames.ts
target/perry-dev/perry "$SRC" -o /tmp/eh_helpers
node --experimental-strip-types --no-warnings "$SRC" > /tmp/eh_helpers.node
/tmp/eh_helpers > /tmp/eh_helpers.perry
diff /tmp/eh_helpers.node /tmp/eh_helpers.perry