fix(gc): decode lsl #12 frame adjustments in the aarch64 prologue walker
#194
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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: | |
| push: | |
| branches: [main] | |
| concurrency: | |
| group: eh-transport-${{ github.event_name }}-${{ github.event_name == 'push' && github.sha || github.ref }} | |
| # 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: | |
| 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: | | |
| if [ "${{ github.event_name }}" = "push" ]; 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@v4 | |
| - 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 |