From 67fe3c523e4c4011d33046a307877f9c4ce898d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 24 Aug 2026 06:39:57 +0200 Subject: [PATCH] fix(codegen): retry oversized RS4GC functions with shadow roots, and CI hardening Lands #8687, #8686 and #8684. #8687 (closes #8679) replaces the post-RS4GC instruction-budget hard refusal with a typed spill-retry: an already-lowered LlFunction switches from native statepoint roots to a complete precise shadow frame and the unit is rebuilt at the originally requested optimization level. This is the durable handling for the estimator misses #8678 could only make more accurate. Retry termination is guaranteed rather than argued. `request_shadow_frame_spill()` latches on `force_shadow_frame` and returns false if already set; `apply_budget_spill_retry` records only the functions where it returned true, and any violation not recorded becomes a hard error naming it. A function therefore cannot be retried twice. #8686 builds the gap suite's fast-mode archives once in a dedicated `gap-suite-build` job and shares them across the six shards. The skipped-vs-failed distinction is handled explicitly: `always()` keeps a SKIPPED build (full mode, where the job never runs) from cascading into skipped shards, while the guard still requires `success` or `skipped`, so a genuine build FAILURE stops the shards. The shard also verifies the downloaded binary is runnable and exports PERRY_BIN / PERRY_RUNTIME_DIR. #8684 inlines dtolnay/rust-toolchain and SHA-pins every other third-party action. Verified no job and no gate is dropped: the single removed step is the fast-mode archive build, which moved into gap-suite-build. No version bump. --- .github/workflows/auto-opt-app-patterns.yml | 10 +- .github/workflows/benchmark.yml | 36 +- .github/workflows/container-tests.yml | 38 +- .github/workflows/coverage.yml | 15 +- .github/workflows/docs.yml | 8 +- .github/workflows/eh-transport.yml | 10 +- .github/workflows/ext-link.yml | 12 +- .github/workflows/feature-matrix.yml | 12 +- .github/workflows/gate-freshness.yml | 2 +- .github/workflows/gc-moving-witnesses.yml | 12 +- .github/workflows/gc-native-roots.yml | 11 +- .github/workflows/gc-parse-churn-gate.yml | 10 +- .../workflows/gc-ptr-shape-off-witness.yml | 12 +- .github/workflows/gc-ratchet.yml | 12 +- .github/workflows/gc-root-dominance.yml | 24 +- .github/workflows/llvm-inprocess.yml | 2 +- .github/workflows/next-app-route.yml | 13 +- .github/workflows/node-compat-matrix.yml | 12 +- .github/workflows/node-core-subset.yml | 18 +- .github/workflows/node-suite-guard.yml | 12 +- .github/workflows/npm-launcher.yml | 8 +- .github/workflows/npm-package-sweep.yml | 12 +- .github/workflows/npm-publish-freshness.yml | 4 +- .github/workflows/npm-stage-publish.yml | 8 +- .github/workflows/release-hono-server.yml | 4 +- .github/workflows/release-packages.yml | 48 +- .github/workflows/security-audit.yml | 14 +- .github/workflows/simctl-tests.yml | 10 +- .github/workflows/test.yml | 334 +++++++------- .github/workflows/tls-budget.yml | 12 +- changelog.d/8679-rs4gc-budget-spill-retry.md | 4 + crates/perry-codegen/src/codegen/helpers.rs | 7 +- crates/perry-codegen/src/codegen/mod.rs | 111 +++-- crates/perry-codegen/src/function.rs | 80 +++- crates/perry-codegen/src/inprocess.rs | 246 ++++++++-- crates/perry-codegen/src/linker.rs | 27 +- crates/perry-codegen/src/native_emit.rs | 420 ++++++++++++++---- .../perry/src/commands/compile/build_cache.rs | 5 +- .../src/commands/compile/object_cache.rs | 4 +- 39 files changed, 1066 insertions(+), 573 deletions(-) create mode 100644 changelog.d/8679-rs4gc-budget-spill-retry.md diff --git a/.github/workflows/auto-opt-app-patterns.yml b/.github/workflows/auto-opt-app-patterns.yml index 3deeebf15a..bfc85ef82d 100644 --- a/.github/workflows/auto-opt-app-patterns.yml +++ b/.github/workflows/auto-opt-app-patterns.yml @@ -100,7 +100,7 @@ jobs: contents: read pull-requests: read steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false @@ -137,12 +137,10 @@ jobs: - name: Install Rust toolchain if: steps.relevance.outputs.run == 'true' - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + run: rustup toolchain install nightly-2026-08-20 --profile minimal - uses: ./.github/actions/setup-llvm22 - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 if: steps.relevance.outputs.run == 'true' with: shared-key: "${{ runner.os }}-perry" @@ -156,7 +154,7 @@ jobs: - name: Setup Node oracle if: steps.relevance.outputs.run == 'true' - uses: actions/setup-node@v7 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: # Single source of truth: .node-version. Every kernel's stdout is # diffed against this node, so the pin is a correctness input. diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index cdef58b02c..bd9a113654 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -41,19 +41,17 @@ jobs: outputs: status: ${{ steps.compare.outputs.status }} steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: # Need history to compare against previous commits on main fetch-depth: 2 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + run: rustup toolchain install nightly-2026-08-20 --profile minimal - uses: ./.github/actions/setup-llvm22 - name: Cache cargo - uses: actions/cache@v6 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: path: | ~/.cargo/registry @@ -63,7 +61,7 @@ jobs: restore-keys: ${{ runner.os }}-cargo- - name: Setup Node.js (for perf comparison) - uses: actions/setup-node@v7 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: # Single source of truth: .node-version at the repo root. Node here is # an informational peer (the hard regression gate is perry-vs-perry @@ -73,7 +71,7 @@ jobs: node-version-file: .node-version - name: Setup Bun (for perf comparison) - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 with: bun-version: '1.3.14' @@ -250,7 +248,7 @@ jobs: - name: Upload benchmark results if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: benchmark-results-${{ github.sha }} path: | @@ -268,16 +266,14 @@ jobs: binary-size: runs-on: macos-14 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + run: rustup toolchain install nightly-2026-08-20 --profile minimal - uses: ./.github/actions/setup-llvm22 - name: Cache cargo - uses: actions/cache@v6 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: path: | ~/.cargo/registry @@ -356,7 +352,7 @@ jobs: - name: Upload binary size results if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: binary-sizes-${{ github.sha }} path: .bench-results/binary-sizes.json @@ -368,12 +364,10 @@ jobs: compile-time: runs-on: macos-14 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + run: rustup toolchain install nightly-2026-08-20 --profile minimal - uses: ./.github/actions/setup-llvm22 - name: Measure clean-build time @@ -409,7 +403,7 @@ jobs: - name: Upload compile-time results if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: compile-time-${{ github.sha }} path: .bench-results/compile-time.json @@ -426,12 +420,12 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: token: ${{ secrets.GITHUB_TOKEN }} - name: Download benchmark results - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: benchmark-results-${{ github.sha }} path: .bench-results diff --git a/.github/workflows/container-tests.yml b/.github/workflows/container-tests.yml index f413a731ca..c7bbd8a98f 100644 --- a/.github/workflows/container-tests.yml +++ b/.github/workflows/container-tests.yml @@ -86,7 +86,7 @@ jobs: os: [macos-14, ubuntu-24.04] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Free up disk space (macOS) if: runner.os == 'macOS' @@ -96,13 +96,11 @@ jobs: df -h / | tail -1 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + run: rustup toolchain install nightly-2026-08-20 --profile minimal - uses: ./.github/actions/setup-llvm22 - name: Cache cargo registry - uses: actions/cache@v6 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: path: | ~/.cargo/registry @@ -164,7 +162,7 @@ jobs: runs-on: macos-14 if: (github.event_name != 'pull_request' || github.base_ref == 'main') && (github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-extended-tests')) steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Free up disk space (macOS) run: | @@ -172,13 +170,11 @@ jobs: df -h / | tail -1 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + run: rustup toolchain install nightly-2026-08-20 --profile minimal - uses: ./.github/actions/setup-llvm22 - name: Cache cargo registry - uses: actions/cache@v6 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: path: | ~/.cargo/registry @@ -213,16 +209,14 @@ jobs: runs-on: ubuntu-24.04 if: (github.event_name != 'pull_request' || github.base_ref == 'main') && (github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-extended-tests')) steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + run: rustup toolchain install nightly-2026-08-20 --profile minimal - uses: ./.github/actions/setup-llvm22 - name: Cache cargo registry - uses: actions/cache@v6 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: path: | ~/.cargo/registry @@ -259,16 +253,14 @@ jobs: startsWith(github.ref, 'refs/tags/v') || github.event.inputs.run_e2e == 'true' steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + run: rustup toolchain install nightly-2026-08-20 --profile minimal - uses: ./.github/actions/setup-llvm22 - name: Cache cargo registry - uses: actions/cache@v6 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: path: | ~/.cargo/registry @@ -306,7 +298,7 @@ jobs: - name: Upload e2e binaries (debugging) if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: e2e-bins-linux path: target/e2e-bin/ @@ -326,7 +318,7 @@ jobs: matrix: target: [compose_yaml_parse, env_interpolation, compose_spec_json_round_trip] steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Install nightly Rust + cargo-fuzz run: | @@ -340,7 +332,7 @@ jobs: - name: Upload crash artifacts if: failure() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: fuzz-crashes-${{ matrix.target }} path: crates/perry-container-compose/fuzz/artifacts/${{ matrix.target }}/ diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index a5946af769..83510864df 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -32,16 +32,13 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 90 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' - components: llvm-tools-preview + run: rustup toolchain install nightly-2026-08-20 --profile minimal --component llvm-tools-preview - uses: ./.github/actions/setup-llvm22 - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 with: # Separate key from the main test cache — llvm-cov rebuilds # the workspace with instrumentation, so its objects can't @@ -50,7 +47,7 @@ jobs: save-if: ${{ github.ref == 'refs/heads/main' }} - name: Install cargo-llvm-cov - uses: taiki-e/install-action@v3 + uses: taiki-e/install-action@6cd13508893c0e7eab5f273c2575d3859bd7229a # v2.86.6 with: tool: cargo-llvm-cov @@ -93,7 +90,7 @@ jobs: - name: Upload HTML report if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: coverage-html path: target/llvm-cov-html @@ -102,7 +99,7 @@ jobs: - name: Upload lcov.info if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: coverage-lcov path: target/lcov.info diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c5b8e25d12..05de5b3027 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -20,7 +20,7 @@ jobs: name: github-pages url: ${{ steps.deployment.outputs.page_url }} steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Install mdBook env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -34,9 +34,9 @@ jobs: run: | ./docs/i18n.sh build-all cp docs/CNAME docs/book/CNAME - - uses: actions/configure-pages@v6 - - uses: actions/upload-pages-artifact@v5 + - uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6 + - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5 with: path: docs/book - id: deployment - uses: actions/deploy-pages@v5 + uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5 diff --git a/.github/workflows/eh-transport.yml b/.github/workflows/eh-transport.yml index d22a4f704a..aa234df6e4 100644 --- a/.github/workflows/eh-transport.yml +++ b/.github/workflows/eh-transport.yml @@ -112,20 +112,18 @@ jobs: runs-on: macos-15 timeout-minutes: 60 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + run: rustup toolchain install nightly-2026-08-20 --profile minimal - uses: ./.github/actions/setup-llvm22 - name: Setup Node.js - uses: actions/setup-node@v7 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: node-version-file: .node-version - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 with: shared-key: "${{ runner.os }}-perry" save-if: ${{ github.ref == 'refs/heads/main' }} diff --git a/.github/workflows/ext-link.yml b/.github/workflows/ext-link.yml index e1e4dc7f65..e577f661f4 100644 --- a/.github/workflows/ext-link.yml +++ b/.github/workflows/ext-link.yml @@ -78,7 +78,7 @@ jobs: steps: # This job compiles PR-controlled build scripts; don't leave the workflow # token in .git/config for them to read. - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false @@ -113,26 +113,24 @@ jobs: - name: Install Rust toolchain if: steps.scope.outputs.pkgs != '' - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + run: rustup toolchain install nightly-2026-08-20 --profile minimal - if: steps.scope.outputs.pkgs != '' uses: ./.github/actions/setup-llvm22 - name: Install sccache if: steps.scope.outputs.pkgs != '' - uses: mozilla-actions/sccache-action@v0.0.11 + uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11 - name: Cache sccache objects if: steps.scope.outputs.pkgs != '' - uses: actions/cache@v6 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: path: ${{ github.workspace }}/.sccache key: sccache-${{ runner.os }}-perry-${{ github.job }}-${{ github.run_id }} restore-keys: | sccache-${{ runner.os }}-perry- - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 if: steps.scope.outputs.pkgs != '' with: shared-key: "${{ runner.os }}-perry" diff --git a/.github/workflows/feature-matrix.yml b/.github/workflows/feature-matrix.yml index 3d2d718a9b..5b9decc1fd 100644 --- a/.github/workflows/feature-matrix.yml +++ b/.github/workflows/feature-matrix.yml @@ -35,21 +35,19 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 60 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + run: rustup toolchain install nightly-2026-08-20 --profile minimal - uses: ./.github/actions/setup-llvm22 - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 with: shared-key: "${{ runner.os }}-perry-feature-matrix" save-if: ${{ github.ref == 'refs/heads/main' }} - name: Setup Node.js - uses: actions/setup-node@v7 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: # Single source of truth: .node-version at the repo root. node-version-file: .node-version @@ -79,7 +77,7 @@ jobs: - name: Upload feature matrix artifacts if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: feature-matrix-${{ github.sha }} path: | diff --git a/.github/workflows/gate-freshness.yml b/.github/workflows/gate-freshness.yml index 8337050489..d700691f48 100644 --- a/.github/workflows/gate-freshness.yml +++ b/.github/workflows/gate-freshness.yml @@ -68,7 +68,7 @@ jobs: actions: read issues: write steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false diff --git a/.github/workflows/gc-moving-witnesses.yml b/.github/workflows/gc-moving-witnesses.yml index c65a715886..dd7f2899b8 100644 --- a/.github/workflows/gc-moving-witnesses.yml +++ b/.github/workflows/gc-moving-witnesses.yml @@ -189,7 +189,7 @@ jobs: # avoids a `fetch-depth: 0` clone of a large history. pull-requests: read steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false @@ -224,12 +224,10 @@ jobs: - name: Install Rust toolchain if: steps.relevance.outputs.run == 'true' - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + run: rustup toolchain install nightly-2026-08-20 --profile minimal - uses: ./.github/actions/setup-llvm22 - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 if: steps.relevance.outputs.run == 'true' with: # Same key as `gc-stress`, which builds the identical package set, so @@ -246,7 +244,7 @@ jobs: - name: Setup Node oracle if: steps.relevance.outputs.run == 'true' - uses: actions/setup-node@v7 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: # Single source of truth: .node-version. Node is a correctness input, # not a peer benchmark — every witness's stdout is byte-diffed against @@ -413,7 +411,7 @@ jobs: - name: Upload the witness report if: always() && steps.relevance.outputs.run == 'true' - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: gc-moving-witnesses-${{ github.sha }} path: gc-moving-witnesses.json diff --git a/.github/workflows/gc-native-roots.yml b/.github/workflows/gc-native-roots.yml index 2c6206e20f..d6f7993cd8 100644 --- a/.github/workflows/gc-native-roots.yml +++ b/.github/workflows/gc-native-roots.yml @@ -273,15 +273,14 @@ jobs: # llvm-inprocess feature, which cargo cannot share with the build above. timeout-minutes: 120 steps: - - uses: actions/checkout@v7 - - uses: actions/setup-node@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: node-version-file: .node-version - - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + - name: Install Rust toolchain + run: rustup toolchain install nightly-2026-08-20 --profile minimal - uses: ./.github/actions/setup-llvm22 - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 with: shared-key: gc-native-roots - name: Build compiler and static runtime (perry-dev profile) diff --git a/.github/workflows/gc-parse-churn-gate.yml b/.github/workflows/gc-parse-churn-gate.yml index b51afea87f..1706fbd070 100644 --- a/.github/workflows/gc-parse-churn-gate.yml +++ b/.github/workflows/gc-parse-churn-gate.yml @@ -130,7 +130,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false - name: Self-test the verdict logic @@ -148,7 +148,7 @@ jobs: contents: read pull-requests: read steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false @@ -181,13 +181,11 @@ jobs: - name: Install Rust toolchain if: steps.relevance.outputs.run == 'true' - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + run: rustup toolchain install nightly-2026-08-20 --profile minimal - uses: ./.github/actions/setup-llvm22 if: steps.relevance.outputs.run == 'true' - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 if: steps.relevance.outputs.run == 'true' with: # Shared with gc-stress/gc-moving-witnesses (same package set), so diff --git a/.github/workflows/gc-ptr-shape-off-witness.yml b/.github/workflows/gc-ptr-shape-off-witness.yml index 36430bd26c..5993892fa1 100644 --- a/.github/workflows/gc-ptr-shape-off-witness.yml +++ b/.github/workflows/gc-ptr-shape-off-witness.yml @@ -134,7 +134,7 @@ jobs: contents: read pull-requests: read steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false @@ -168,13 +168,11 @@ jobs: - name: Install Rust toolchain if: steps.relevance.outputs.run == 'true' - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + run: rustup toolchain install nightly-2026-08-20 --profile minimal - uses: ./.github/actions/setup-llvm22 if: steps.relevance.outputs.run == 'true' - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 if: steps.relevance.outputs.run == 'true' with: # Same key as gc-stress, which builds the identical package set, so @@ -190,7 +188,7 @@ jobs: - name: Setup Node oracle if: steps.relevance.outputs.run == 'true' - uses: actions/setup-node@v7 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: # Single source of truth: .node-version. The matrix refuses to run # when the running node disagrees with the pin, so a version drift @@ -237,7 +235,7 @@ jobs: - name: Upload the witness report if: always() && steps.relevance.outputs.run == 'true' - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: gc-ptr-shape-off-witness-${{ github.sha }} path: gc-ptr-shape-off-witness.json diff --git a/.github/workflows/gc-ratchet.yml b/.github/workflows/gc-ratchet.yml index 51d3610f59..1c7ee38640 100644 --- a/.github/workflows/gc-ratchet.yml +++ b/.github/workflows/gc-ratchet.yml @@ -119,7 +119,7 @@ jobs: # it avoids a fetch-depth: 0 clone of a large history. pull-requests: read steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false @@ -178,14 +178,12 @@ jobs: - name: Install Rust toolchain if: steps.relevance.outputs.run == 'true' - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + run: rustup toolchain install nightly-2026-08-20 --profile minimal - uses: ./.github/actions/setup-llvm22 - name: Cache cargo if: steps.relevance.outputs.run == 'true' - uses: actions/cache@v6 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: path: | ~/.cargo/registry @@ -196,7 +194,7 @@ jobs: - name: Setup Node oracle if: steps.relevance.outputs.run == 'true' - uses: actions/setup-node@v7 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: # Single source of truth: .node-version at the repo root. Node is a # correctness input here, not a peer benchmark: every probe's stdout @@ -265,7 +263,7 @@ jobs: - name: Upload measurement if: always() && steps.relevance.outputs.run == 'true' - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: gc-ratchet-${{ github.sha }} path: | diff --git a/.github/workflows/gc-root-dominance.yml b/.github/workflows/gc-root-dominance.yml index 0255c8a2df..9f99738a70 100644 --- a/.github/workflows/gc-root-dominance.yml +++ b/.github/workflows/gc-root-dominance.yml @@ -120,7 +120,7 @@ jobs: runs-on: macos-14 timeout-minutes: 90 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false @@ -216,9 +216,7 @@ jobs: run: python3 scripts/gc_root_dominance_check.py --audit-immovable-sources - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + run: rustup toolchain install nightly-2026-08-20 --profile minimal - uses: ./.github/actions/setup-llvm22 # ★ The dependency-scale corpus needs the dependency. @@ -228,14 +226,14 @@ jobs: # else in that file -- not a fixture invented for this job. # `--ignore-scripts` because nothing here needs a lifecycle script to # run, and a corpus generator is a bad place to execute one. - - uses: actions/setup-node@v7 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: node-version-file: .node-version - name: Install the npm dependencies the dep corpus compiles run: npm ci --ignore-scripts --no-audit --no-fund - name: Cache cargo - uses: actions/cache@v6 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: path: | ~/.cargo/registry @@ -408,7 +406,7 @@ jobs: - name: Upload the IR corpus on failure if: failure() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: gc-root-dominance-ir path: | @@ -450,7 +448,7 @@ jobs: runs-on: macos-14 timeout-minutes: 90 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false @@ -493,16 +491,14 @@ jobs: # (statepoints are the default on aarch64 and x86-64) reported nothing # at all, while its shadow-frame sibling stayed green and made the gate # look healthy. - - uses: actions/setup-node@v7 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: node-version-file: .node-version - name: Install the npm dependencies the dep corpus compiles run: npm ci --ignore-scripts --no-audit --no-fund - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + run: rustup toolchain install nightly-2026-08-20 --profile minimal # This also puts a matched LLVM 22 `opt` on disk and exports # LLVM_SYS_221_PREFIX, which is how the corpus script finds it. The # native corpus is `--trace llvm` output PLUS the production statepoint @@ -511,7 +507,7 @@ jobs: - uses: ./.github/actions/setup-llvm22 - name: Cache cargo - uses: actions/cache@v6 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: path: | ~/.cargo/registry @@ -768,7 +764,7 @@ jobs: - name: Upload the IR corpus on failure if: failure() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: gc-root-dominance-statepoint-ir path: ir-corpus-native diff --git a/.github/workflows/llvm-inprocess.yml b/.github/workflows/llvm-inprocess.yml index 2c11677fef..e1ebd77026 100644 --- a/.github/workflows/llvm-inprocess.yml +++ b/.github/workflows/llvm-inprocess.yml @@ -107,7 +107,7 @@ jobs: # files were. A diagnostic that cannot report the problem it exists to # report is worse than no diagnostic; it was reassuring three readers # while the corpora sat 151 commits behind. - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: fetch-depth: 0 diff --git a/.github/workflows/next-app-route.yml b/.github/workflows/next-app-route.yml index d9d9a57044..00650ad210 100644 --- a/.github/workflows/next-app-route.yml +++ b/.github/workflows/next-app-route.yml @@ -35,22 +35,21 @@ jobs: SCCACHE_GHA_ENABLED: "true" SCCACHE_CACHE_SIZE: "4G" steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false - - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + - name: Install Rust toolchain + run: rustup toolchain install nightly-2026-08-20 --profile minimal - uses: ./.github/actions/setup-llvm22 - - uses: mozilla-actions/sccache-action@v0.0.11 - - uses: Swatinem/rust-cache@v2 + - uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 with: shared-key: "${{ runner.os }}-perry" save-if: ${{ github.ref == 'refs/heads/main' }} - - uses: actions/setup-node@v7 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: node-version-file: .node-version cache: npm diff --git a/.github/workflows/node-compat-matrix.yml b/.github/workflows/node-compat-matrix.yml index 8f04a9e0b8..2eabf302ee 100644 --- a/.github/workflows/node-compat-matrix.yml +++ b/.github/workflows/node-compat-matrix.yml @@ -54,28 +54,26 @@ jobs: runs-on: macos-14 timeout-minutes: 120 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: # Read-only job (build + verify); don't leave the GITHUB_TOKEN in # the local git config (least privilege — OWASP / CodeRabbit). persist-credentials: false - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + run: rustup toolchain install nightly-2026-08-20 --profile minimal - uses: ./.github/actions/setup-llvm22 - name: Start sccache - uses: mozilla-actions/sccache-action@v0.0.11 + uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11 - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 with: shared-key: "${{ runner.os }}-perry" save-if: ${{ github.ref == 'refs/heads/main' }} - name: Setup Node.js (executes the runner; oracle is the pinned dist) - uses: actions/setup-node@v7 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: # Single source of truth: .node-version at the repo root. This node # only RUNS node_compat_matrix.mjs; the compat oracle is the pinned diff --git a/.github/workflows/node-core-subset.yml b/.github/workflows/node-core-subset.yml index df37ec2b77..ee15cf36e4 100644 --- a/.github/workflows/node-core-subset.yml +++ b/.github/workflows/node-core-subset.yml @@ -70,15 +70,13 @@ jobs: # from the job log for six weeks. PYTHONUNBUFFERED: "1" steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + run: rustup toolchain install nightly-2026-08-20 --profile minimal - uses: ./.github/actions/setup-llvm22 - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 with: shared-key: "${{ runner.os }}-perry" save-if: ${{ github.ref == 'refs/heads/main' }} @@ -104,7 +102,7 @@ jobs: echo "version=$node_version" >> "$GITHUB_OUTPUT" - name: Setup Node.js (matches the pinned test corpus, NOT .node-version) - uses: actions/setup-node@v7 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: node-version: ${{ steps.node_core_version.outputs.version }} @@ -146,7 +144,7 @@ jobs: - name: Upload shard report if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: node-core-subset-report-shard-${{ matrix.shard }} path: test-compat/node-core/report-shard-${{ matrix.shard }}.json @@ -161,10 +159,10 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Download shard reports - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: pattern: node-core-subset-report-shard-* merge-multiple: true @@ -228,7 +226,7 @@ jobs: - name: Upload radar report if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: node-core-subset-report path: test-compat/node-core/report.json diff --git a/.github/workflows/node-suite-guard.yml b/.github/workflows/node-suite-guard.yml index 26728a8ef1..32e2024d5c 100644 --- a/.github/workflows/node-suite-guard.yml +++ b/.github/workflows/node-suite-guard.yml @@ -42,22 +42,20 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 120 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: # Read-only job (build + test); don't leave the GITHUB_TOKEN in the # local git config (least privilege — OWASP / CodeRabbit). persist-credentials: false - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + run: rustup toolchain install nightly-2026-08-20 --profile minimal - uses: ./.github/actions/setup-llvm22 - name: Start sccache - uses: mozilla-actions/sccache-action@v0.0.11 + uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11 - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 with: shared-key: "${{ runner.os }}-perry" save-if: ${{ github.ref == 'refs/heads/main' }} @@ -66,7 +64,7 @@ jobs: # Node 26 is the oracle the node_suite_baseline.json floors were # captured on; running any other major would compare against the # wrong reference and produce spurious regressions. - uses: actions/setup-node@v7 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: # Single source of truth: .node-version at the repo root. node-version-file: .node-version diff --git a/.github/workflows/npm-launcher.yml b/.github/workflows/npm-launcher.yml index 757cf1aeaf..1ebbc2e761 100644 --- a/.github/workflows/npm-launcher.yml +++ b/.github/workflows/npm-launcher.yml @@ -28,7 +28,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false # Single source of truth: .node-version at the repo root. detect.cjs is @@ -37,7 +37,7 @@ jobs: # file was created by #6350 on the same day #6367 converted every other # workflow to the shared pin, so it kept that day's ambient "22.23.1" # literal by omission rather than by decision. - - uses: actions/setup-node@v7 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: node-version-file: .node-version - run: node npm/perry/test/detect.test.cjs @@ -55,14 +55,14 @@ jobs: runs-on: ubuntu-22.04 timeout-minutes: 20 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false # Single source of truth: .node-version. Safe on this image — Node 26 # requires glibc >= 2.28 (nodejs/node v26.x BUILDING.md, Tier 1 linux # x64/arm64) and jammy ships 2.35. The glibc under test here is the one # Perry's own prebuilt binary needs, not Node's. - - uses: actions/setup-node@v7 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: node-version-file: .node-version diff --git a/.github/workflows/npm-package-sweep.yml b/.github/workflows/npm-package-sweep.yml index 07b2b75183..a74662c767 100644 --- a/.github/workflows/npm-package-sweep.yml +++ b/.github/workflows/npm-package-sweep.yml @@ -41,21 +41,19 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 90 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + run: rustup toolchain install nightly-2026-08-20 --profile minimal - uses: ./.github/actions/setup-llvm22 - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 with: shared-key: "${{ runner.os }}-perry-npm-sweep" save-if: ${{ github.ref == 'refs/heads/main' }} - name: Setup Node.js - uses: actions/setup-node@v7 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: # Single source of truth: .node-version at the repo root. node-version-file: .node-version @@ -95,7 +93,7 @@ jobs: - name: Upload sweep artifacts if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: npm-package-sweep-${{ github.sha }} path: .npm-sweep-results/ diff --git a/.github/workflows/npm-publish-freshness.yml b/.github/workflows/npm-publish-freshness.yml index 005a4fb5e9..12e30fdf8a 100644 --- a/.github/workflows/npm-publish-freshness.yml +++ b/.github/workflows/npm-publish-freshness.yml @@ -33,7 +33,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false - name: Self-test — prove the checker can fail @@ -63,7 +63,7 @@ jobs: contents: read issues: write steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: ref: main persist-credentials: false diff --git a/.github/workflows/npm-stage-publish.yml b/.github/workflows/npm-stage-publish.yml index 167eb13b33..d8fcf804cb 100644 --- a/.github/workflows/npm-stage-publish.yml +++ b/.github/workflows/npm-stage-publish.yml @@ -69,7 +69,7 @@ jobs: outputs: build-run-id: ${{ steps.resolve.outputs.build-run-id }} steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Resolve or dispatch the stage-mode build id: resolve env: @@ -174,12 +174,12 @@ jobs: PUBLISH: ${{ inputs.publish }} SOCKET_API_TOKEN: ${{ secrets.SOCKET_API_TOKEN }} steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 # DELIBERATE EXEMPTION from the repo-wide .node-version pin: this Node is # a publishing toolchain (npm registry auth), not a test oracle. # Registered in scripts/check_node_version_consistency.py. - - uses: actions/setup-node@v7 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: node-version: "26" registry-url: "https://registry.npmjs.org" @@ -201,7 +201,7 @@ jobs: echo "npm $cur satisfies the publish-flow floor (>= $floor)." - name: Download build artifacts from the stage-mode build run - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: path: release-artifacts/ github-token: ${{ github.token }} diff --git a/.github/workflows/release-hono-server.yml b/.github/workflows/release-hono-server.yml index e8276d89d4..739524043e 100644 --- a/.github/workflows/release-hono-server.yml +++ b/.github/workflows/release-hono-server.yml @@ -24,7 +24,7 @@ jobs: run: working-directory: packages/hono-perry-server steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 # DELIBERATE EXEMPTION from the repo-wide .node-version pin: this Node is a # *publishing* toolchain, not a test oracle — it never runs a parity diff. @@ -40,7 +40,7 @@ jobs: # that publishes releases. Registered in # scripts/check_node_version_consistency.py, which keeps this major in # step with .node-version's major. Was "24" until the Node-26 sweep. - - uses: actions/setup-node@v7 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: node-version: "26" diff --git a/.github/workflows/release-packages.yml b/.github/workflows/release-packages.yml index 382e3297eb..33baa6cd0c 100644 --- a/.github/workflows/release-packages.yml +++ b/.github/workflows/release-packages.yml @@ -72,7 +72,7 @@ jobs: # label (#confirmed republish version-drift finding). checkout-ref: ${{ steps.resolve.outputs.checkout-ref }} steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: fetch-depth: 0 # republish mode needs `git show :Cargo.toml` below @@ -389,15 +389,12 @@ jobs: # version (15.x) than being linked (14.x)` across every stdlib .o. MACOSX_DEPLOYMENT_TARGET: "13.0" steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: ref: ${{ needs.preflight.outputs.checkout-ref }} - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' - targets: ${{ matrix.target }} + run: rustup toolchain install nightly-2026-08-20 --profile minimal --target ${{ matrix.target }} # The old-sysroot image carries LLVM 22 under /usr/lib/llvm-22. # Its host-side GTK4 build does not depend on perry-codegen, so avoid # installing a second LLVM copy on the noble runner for these two legs. @@ -409,7 +406,7 @@ jobs: # release-tag on the same Cargo.lock reuses the compiled crates — # webkit6/gstreamer/swc/etc. dominate cold cargo and recompile takes # 10-15 min per matrix entry. Cache hit drops that to ~30s. - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 with: shared-key: "release-${{ matrix.target }}" workspaces: | @@ -968,7 +965,7 @@ jobs: # artifact instead. - name: Upload packaged archive (Unix, for publish-assets job) if: runner.os != 'Windows' - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: release-archive-${{ matrix.artifact }} # update.json only exists when a tag was resolved (not on stage-only @@ -980,7 +977,7 @@ jobs: - name: Upload packaged archive (Windows, for publish-assets job) if: runner.os == 'Windows' - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: release-archive-${{ matrix.artifact }} path: | @@ -990,14 +987,14 @@ jobs: - name: Upload build artifact (Unix, for deb + npm-publish jobs) if: runner.os != 'Windows' - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: ${{ matrix.artifact }} path: staging/ - name: Upload build artifact (Windows, for npm-publish job) if: runner.os == 'Windows' - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: ${{ matrix.artifact }} path: staging/ @@ -1089,22 +1086,19 @@ jobs: # users on macOS 13. MACOSX_DEPLOYMENT_TARGET: "13.0" steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: ref: ${{ needs.preflight.outputs.checkout-ref }} - name: Install Rust toolchain + cross target - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' - targets: ${{ matrix.target }} + run: rustup toolchain install nightly-2026-08-20 --profile minimal --target ${{ matrix.target }} - uses: ./.github/actions/setup-llvm22 # Same cache shape as `build:` so the cross legs share crate compiles # across release cycles. Keyed on (target, Cargo.lock) — first run on a # new target is cold, but every subsequent release-tag reuses the # SWC/llvm-sys/webkit etc. precompile from the prior cycle. - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 with: shared-key: "release-cross-${{ matrix.target }}" @@ -1343,7 +1337,7 @@ jobs: # dispatches onto the PREVIOUS release. The artifact also lets a # maintainer pull the bundle off a stage-only run directly. - name: Upload cross build artifact (for publish-assets job + inspection) - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: perry-cross-${{ matrix.target }} path: | @@ -1370,7 +1364,7 @@ jobs: needs.build-cross.result == 'success' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Create tag + GitHub Release (notes from changelog.d) env: @@ -1434,7 +1428,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Download packaged host archives - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: pattern: release-archive-* merge-multiple: true @@ -1444,7 +1438,7 @@ jobs: # continue-on-error: a fully-red build-cross (legacy modes) must not # stop the host archives from shipping. continue-on-error: true - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: pattern: perry-cross-* merge-multiple: true @@ -1613,7 +1607,7 @@ jobs: echo "tag=${TAG}" >> "$GITHUB_OUTPUT" - name: Download build artifact - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: ${{ matrix.artifact }} path: staging/ @@ -1786,7 +1780,7 @@ jobs: done - name: Upload .deb artifacts (for apt-repo job) - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: deb-${{ matrix.arch }} path: "*.deb" @@ -1808,7 +1802,7 @@ jobs: echo "tag=${TAG}" >> "$GITHUB_OUTPUT" - name: Download .deb packages - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: pattern: deb-* merge-multiple: true @@ -2005,7 +1999,7 @@ jobs: contents: read id-token: write # REQUIRED for OIDC + npm --provenance steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: # republish mode pins this to the tag's exact commit (see # preflight's checkout-ref) — without it, npm publishes whatever @@ -2025,7 +2019,7 @@ jobs: # Was "20" until the Node-26 sweep. Node 20 reached end-of-life on # 2026-04-30, and this is the repo's most privileged job (`id-token: write`, # OIDC-publishes every platform package) — an EOL runtime is a defect there. - - uses: actions/setup-node@v7 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: node-version: "26" registry-url: "https://registry.npmjs.org" @@ -2039,7 +2033,7 @@ jobs: run: npm install -g npm@latest - name: Download all build artifacts - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: path: release-artifacts/ diff --git a/.github/workflows/security-audit.yml b/.github/workflows/security-audit.yml index 8c97262a3e..d05f4cf39f 100644 --- a/.github/workflows/security-audit.yml +++ b/.github/workflows/security-audit.yml @@ -48,11 +48,10 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + - name: Install Rust toolchain + run: rustup toolchain install nightly-2026-08-20 --profile minimal - uses: ./.github/actions/setup-llvm22 - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 with: shared-key: security-audit - name: Install cargo-audit @@ -200,11 +199,10 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + - name: Install Rust toolchain + run: rustup toolchain install nightly-2026-08-20 --profile minimal - uses: ./.github/actions/setup-llvm22 - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 with: shared-key: security-audit - name: Install cargo-deny diff --git a/.github/workflows/simctl-tests.yml b/.github/workflows/simctl-tests.yml index 6e586e64ee..9c3d021cea 100644 --- a/.github/workflows/simctl-tests.yml +++ b/.github/workflows/simctl-tests.yml @@ -29,19 +29,17 @@ jobs: runs-on: macos-14 timeout-minutes: 60 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + run: rustup toolchain install nightly-2026-08-20 --profile minimal - uses: ./.github/actions/setup-llvm22 - name: Install iOS simulator Rust target run: rustup target add aarch64-apple-ios-sim aarch64-apple-ios - name: Cache cargo - uses: actions/cache@v6 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: path: | ~/.cargo/registry @@ -78,7 +76,7 @@ jobs: - name: Upload simctl logs if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: simctl-logs path: target/perry-simctl-tests/*.log diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6ecbefed46..e276323e52 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -105,7 +105,7 @@ jobs: plan: ${{ steps.plan.outputs.plan }} tier: ${{ steps.plan.outputs.tier }} steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false @@ -170,7 +170,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 20 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 # Changeset gate (was its own job): PRs touching crates/ must ship # changelog.d/-.md (see changelog.d/README.md; fragments fold @@ -205,7 +205,7 @@ jobs: exit 1 - name: Setup Node.js for benchmark harness tests - uses: actions/setup-node@v7 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: # Kept in lockstep with benchmark.yml's peer-comparison Node via the # shared .node-version pin — these harness tests exercise the same @@ -225,13 +225,10 @@ jobs: run: ./tests/test_stage_npm_windows_llvm.sh - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' - components: rustfmt, clippy + run: rustup toolchain install nightly-2026-08-20 --profile minimal --component rustfmt --component clippy - uses: ./.github/actions/setup-llvm22 - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 with: shared-key: "${{ runner.os }}-perry" # PRs read from cache; only main writes new entries. @@ -738,16 +735,13 @@ jobs: # a perf budget — margin costs nothing on healthy runs. timeout-minutes: 90 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' - components: clippy + run: rustup toolchain install nightly-2026-08-20 --profile minimal --component clippy - uses: ./.github/actions/setup-llvm22 - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 with: shared-key: "${{ runner.os }}-perry" save-if: ${{ github.ref == 'refs/heads/main' }} @@ -827,26 +821,24 @@ jobs: SCCACHE_CACHE_SIZE: "12G" CARGO_INCREMENTAL: "0" steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + run: rustup toolchain install nightly-2026-08-20 --profile minimal - uses: ./.github/actions/setup-llvm22 - name: Install sccache - uses: mozilla-actions/sccache-action@v0.0.11 + uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11 - name: Restore sccache objects - uses: actions/cache/restore@v6 + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: path: ${{ github.workspace }}/.sccache key: sccache-${{ runner.os }}-perry-${{ github.job }}-${{ github.run_id }} restore-keys: | sccache-${{ runner.os }}-perry- - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 with: shared-key: "${{ runner.os }}-perry" save-if: ${{ github.ref == 'refs/heads/main' }} @@ -875,7 +867,7 @@ jobs: # branched from. - name: Save sccache objects (main-line runs only) if: always() && github.event_name != 'pull_request' - uses: actions/cache/save@v6 + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: path: ${{ github.workspace }}/.sccache key: sccache-${{ runner.os }}-perry-${{ github.job }}-${{ github.run_id }} @@ -941,16 +933,14 @@ jobs: SCCACHE_CACHE_SIZE: "12G" CARGO_INCREMENTAL: "0" steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + run: rustup toolchain install nightly-2026-08-20 --profile minimal - uses: ./.github/actions/setup-llvm22 - name: Install sccache - uses: mozilla-actions/sccache-action@v0.0.11 + uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11 # Persist the sccache disk cache across runs. The github.job + # github.run_id key makes every run (including PRs) save its own fresh @@ -959,14 +949,14 @@ jobs: # cache from ANY of them. The object cache therefore warms continuously # and cross-pollinates instead of starting cold each run. - name: Restore sccache objects - uses: actions/cache/restore@v6 + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: path: ${{ github.workspace }}/.sccache key: sccache-${{ runner.os }}-perry-${{ github.job }}-${{ github.run_id }} restore-keys: | sccache-${{ runner.os }}-perry- - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 with: shared-key: "${{ runner.os }}-perry" save-if: ${{ github.ref == 'refs/heads/main' }} @@ -1168,7 +1158,7 @@ jobs: # branched from. - name: Save sccache objects (main-line runs only) if: always() && github.event_name != 'pull_request' - uses: actions/cache/save@v6 + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: path: ${{ github.workspace }}/.sccache key: sccache-${{ runner.os }}-perry-${{ github.job }}-${{ github.run_id }} @@ -1222,7 +1212,7 @@ jobs: steps: # Nothing here pushes back to the repo, and the job compiles third-party # crates (build scripts run), so don't leave a git credential on disk. - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false @@ -1267,26 +1257,24 @@ jobs: - name: Install Rust toolchain if: steps.scope.outputs.rust_work == 'true' - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + run: rustup toolchain install nightly-2026-08-20 --profile minimal - uses: ./.github/actions/setup-llvm22 if: steps.scope.outputs.rust_work == 'true' - name: Install sccache if: steps.scope.outputs.rust_work == 'true' - uses: mozilla-actions/sccache-action@v0.0.11 + uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11 - name: Restore sccache objects if: steps.scope.outputs.rust_work == 'true' - uses: actions/cache/restore@v6 + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: path: ${{ github.workspace }}/.sccache key: sccache-${{ runner.os }}-perry-${{ github.job }}-${{ github.run_id }} restore-keys: | sccache-${{ runner.os }}-perry- - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 if: steps.scope.outputs.rust_work == 'true' with: shared-key: "${{ runner.os }}-perry" @@ -1391,7 +1379,7 @@ jobs: # branched from. - name: Save sccache objects (main-line runs only) if: always() && github.event_name != 'pull_request' - uses: actions/cache/save@v6 + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: path: ${{ github.workspace }}/.sccache key: sccache-${{ runner.os }}-perry-${{ github.job }}-${{ github.run_id }} @@ -1425,7 +1413,7 @@ jobs: # timeout on a required-path job is a deterministic PR blocker). timeout-minutes: 75 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: # This job compiles PR-controlled build scripts / proc macros; # don't leave the workflow token in .git/config for them to read @@ -1455,17 +1443,15 @@ jobs: python benchmarks/gc_ratchet/gc_ratchet.py validate --scope structural - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + run: rustup toolchain install nightly-2026-08-20 --profile minimal - uses: ./.github/actions/setup-llvm22 - name: Setup Node.js - uses: actions/setup-node@v7 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: node-version-file: .node-version - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 with: shared-key: "${{ runner.os }}-perry" save-if: ${{ github.ref == 'refs/heads/main' }} @@ -1555,14 +1541,12 @@ jobs: runs-on: windows-11-arm timeout-minutes: 75 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + run: rustup toolchain install nightly-2026-08-20 --profile minimal - uses: ./.github/actions/setup-llvm22 - name: Set up ARM64 MSVC environment @@ -1570,11 +1554,11 @@ jobs: # PowerShell environment does not include the SDK's ARM64 library # directories. Populate LIB / INCLUDE / PATH so the final PE link can # resolve system import libraries such as user32.lib. - uses: ilammy/msvc-dev-cmd@v1 + uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1 with: arch: amd64_arm64 - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 with: shared-key: "${{ runner.os }}-${{ runner.arch }}-perry" save-if: ${{ github.ref == 'refs/heads/main' }} @@ -1669,15 +1653,13 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 90 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + run: rustup toolchain install nightly-2026-08-20 --profile minimal - uses: ./.github/actions/setup-llvm22 - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 with: shared-key: "${{ runner.os }}-perry" save-if: ${{ github.ref == 'refs/heads/main' }} @@ -1688,7 +1670,7 @@ jobs: sudo apt-get install -y clang - name: Setup Node.js - uses: actions/setup-node@v7 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: # Single source of truth: .node-version at the repo root. Node is the # matrix oracle (we byte-diff against it), so the version is a @@ -1750,7 +1732,7 @@ jobs: - name: Upload matrix report if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: gc-repsel-matrix path: gc-repsel-matrix.json @@ -1790,16 +1772,14 @@ jobs: SCCACHE_CACHE_SIZE: "12G" CARGO_INCREMENTAL: "0" steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + run: rustup toolchain install nightly-2026-08-20 --profile minimal - uses: ./.github/actions/setup-llvm22 - name: Install sccache - uses: mozilla-actions/sccache-action@v0.0.11 + uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11 # Persist the sccache disk cache across runs. The github.job + # github.run_id key makes every run (including PRs) save its own fresh @@ -1808,14 +1788,14 @@ jobs: # cache from ANY of them. The object cache therefore warms continuously # and cross-pollinates instead of starting cold each run. - name: Restore sccache objects - uses: actions/cache/restore@v6 + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: path: ${{ github.workspace }}/.sccache key: sccache-${{ runner.os }}-perry-${{ github.job }}-${{ github.run_id }} restore-keys: | sccache-${{ runner.os }}-perry- - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 with: shared-key: "${{ runner.os }}-perry" save-if: ${{ github.ref == 'refs/heads/main' }} @@ -1911,7 +1891,7 @@ jobs: - name: Upload compiler-output artifacts if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: compiler-output-regression path: target/compiler-output-regression/ @@ -1925,7 +1905,7 @@ jobs: # branched from. - name: Save sccache objects (main-line runs only) if: always() && github.event_name != 'pull_request' - uses: actions/cache/save@v6 + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: path: ${{ github.workspace }}/.sccache key: sccache-${{ runner.os }}-perry-${{ github.job }}-${{ github.run_id }} @@ -1988,26 +1968,24 @@ jobs: # because it rebuilds runtime libs the census never links. PERRY_NO_AUTO_OPTIMIZE: "1" steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + run: rustup toolchain install nightly-2026-08-20 --profile minimal - uses: ./.github/actions/setup-llvm22 - name: Install sccache - uses: mozilla-actions/sccache-action@v0.0.11 + uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11 - name: Restore sccache objects - uses: actions/cache/restore@v6 + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: path: ${{ github.workspace }}/.sccache key: sccache-${{ runner.os }}-perry-${{ github.job }}-${{ github.run_id }} restore-keys: | sccache-${{ runner.os }}-perry- - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 with: shared-key: "${{ runner.os }}-perry" save-if: ${{ github.ref == 'refs/heads/main' }} @@ -2119,7 +2097,7 @@ jobs: - name: Upload census reports if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: repsel-census path: target/repsel-census/ @@ -2133,7 +2111,7 @@ jobs: # branched from. - name: Save sccache objects (main-line runs only) if: always() && github.event_name != 'pull_request' - uses: actions/cache/save@v6 + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: path: ${{ github.workspace }}/.sccache key: sccache-${{ runner.os }}-perry-${{ github.job }}-${{ github.run_id }} @@ -2162,26 +2140,24 @@ jobs: SCCACHE_CACHE_SIZE: "12G" CARGO_INCREMENTAL: "0" steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + run: rustup toolchain install nightly-2026-08-20 --profile minimal - uses: ./.github/actions/setup-llvm22 - name: Install sccache - uses: mozilla-actions/sccache-action@v0.0.11 + uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11 - name: Restore sccache objects - uses: actions/cache/restore@v6 + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: path: ${{ github.workspace }}/.sccache key: sccache-${{ runner.os }}-perry-native-abi-evidence-${{ github.run_id }} restore-keys: | sccache-${{ runner.os }}-perry- - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 with: shared-key: "${{ runner.os }}-perry" save-if: ${{ github.ref == 'refs/heads/main' }} @@ -2201,7 +2177,7 @@ jobs: - name: Upload native ABI evidence packet if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: native-abi-evidence-packet path: target/native-abi-evidence-packet/ @@ -2215,7 +2191,7 @@ jobs: # branched from. - name: Save sccache objects (main-line runs only) if: always() && github.event_name != 'pull_request' - uses: actions/cache/save@v6 + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: path: ${{ github.workspace }}/.sccache key: sccache-${{ runner.os }}-perry-native-abi-evidence-${{ github.run_id }} @@ -2226,9 +2202,57 @@ jobs: # in the full tier (scripts/ci_plan.py GAP_SUITE). The `gate` fan-in below is # what branch protection requires; a single shard's red bubbles up through it. # --------------------------------------------------------------------------- - gap-suite: + # Builds the fast-mode gap-suite's shared release archives ONCE instead of + # redundantly rebuilding the identical archives in every shard (measured + # ~10 min per shard, repeated across all 6 PR-tier shards -- ~50 min of + # pure redundant compute per run). Full mode is unaffected: its shards + # build their own feature-stripped runtime per distinct test through + # run_gap_tests.sh's own auto-optimize path, which this job does not + # attempt to share. + gap-suite-build: needs: plan - if: fromJSON(needs.plan.outputs.plan).jobs.gap_suite + if: fromJSON(needs.plan.outputs.plan).jobs.gap_suite && fromJSON(needs.plan.outputs.plan).gap.mode == 'fast' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + + - name: Install Rust toolchain + run: rustup toolchain install nightly-2026-08-20 --profile minimal + - uses: ./.github/actions/setup-llvm22 + + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 + with: + shared-key: "${{ runner.os }}-perry" + save-if: ${{ github.ref == 'refs/heads/main' }} + + - name: Build compiler + runtime archives (fast mode, shared across shards) + # Release profile, codegen-units=1: the parity harness's own build + # command. cu=16 miscompiles the release runtime (see CLAUDE.md). + run: cargo build --release -p perry -p perry-runtime -p perry-stdlib -p perry-runtime-static -p perry-stdlib-static + + # Only the three files the fast-mode harness actually links against + # (PERRY_BIN / PERRY_RUNTIME_DIR below) -- not the rest of target/release, + # which is Rust-internal build state the downloading shard doesn't need. + - name: Upload the shared fast-mode build + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: gap-suite-fast-build + path: | + target/release/perry + target/release/libperry_runtime.a + target/release/libperry_stdlib.a + if-no-files-found: error + retention-days: 1 + + gap-suite: + needs: [plan, gap-suite-build] + # `always()` so a SKIPPED gap-suite-build (full mode, where it never + # runs) doesn't cascade into skipping gap-suite too -- only a genuine + # build FAILURE should stop the shards from starting. + if: >- + always() && + fromJSON(needs.plan.outputs.plan).jobs.gap_suite && + (needs.gap-suite-build.result == 'success' || needs.gap-suite-build.result == 'skipped') # 2026-07-02 audit §11: nothing on the default PR path exercised # TypeScript SEMANTICS — lint/cargo-test/api-docs-drift build and unit- # test the compiler but never diff a compiled program against node, so @@ -2280,21 +2304,19 @@ jobs: # the #6456 reshard lands. timeout-minutes: 110 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + run: rustup toolchain install nightly-2026-08-20 --profile minimal - uses: ./.github/actions/setup-llvm22 - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 with: shared-key: "${{ runner.os }}-perry" save-if: ${{ github.ref == 'refs/heads/main' }} - name: Setup Node.js - uses: actions/setup-node@v7 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: # Single source of truth: .node-version at the repo root. Node is the # gap/parity oracle (we byte-diff against it), so the version is a @@ -2310,23 +2332,31 @@ jobs: run: npm ci --ignore-scripts --no-audit --no-fund # Two harness modes, chosen by the plan (see ci_plan.py GAP_SUITE): - # fast: build the release compiler + runtime archives ONCE here, then - # PERRY_SKIP_BUILD=1 so every test links the prebuilt archives - # (~1.5 s/test). Only ext-routed tests (http/net/ws/zlib/events) - # still take the per-test auto-optimize path, because no single + # fast: PERRY_SKIP_BUILD=1 so every test links the prebuilt archives + # (~1.5 s/test) that gap-suite-build compiled ONCE and shared + # below. Only ext-routed tests (http/net/ws/zlib/events) still + # take the per-test auto-optimize path, because no single # prebuilt stdlib can serve them (#7629). PR + sweep tiers. # full: the harness's default -- every test compiles through # auto-optimize, which rebuilds a feature-stripped runtime per # distinct feature set (~200 s each; measured 96% of a shard's # wall time). It is the arm that sees auto-optimize-only bugs, - # so it stays in the nightly/release tier at 8 shards. + # so it stays in the nightly/release tier at 8 shards, and never + # downloads a shared build. # Both compare against the SAME committed Linux snapshot; a divergence # between them is a real auto-optimize-specific finding, not noise. - - name: Build compiler + runtime archives (fast mode) + - name: Download the shared fast-mode build if: fromJSON(needs.plan.outputs.plan).gap.mode == 'fast' - # Release profile, codegen-units=1: the parity harness's own build - # command. cu=16 miscompiles the release runtime (see CLAUDE.md). - run: cargo build --release -p perry -p perry-runtime -p perry-stdlib -p perry-runtime-static -p perry-stdlib-static + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: gap-suite-fast-build + path: target/release + + # Belt-and-suspenders: make sure the downloaded binary is runnable + # regardless of how the artifact round-trip handled its permission bits. + - name: Ensure the downloaded perry binary is executable + if: fromJSON(needs.plan.outputs.plan).gap.mode == 'fast' + run: chmod +x target/release/perry - name: Run gap suite (shard ${{ matrix.shard }}/${{ fromJSON(needs.plan.outputs.plan).gap.total }}, ${{ fromJSON(needs.plan.outputs.plan).gap.mode }} mode) env: @@ -2352,14 +2382,14 @@ jobs: # gap_snapshot.json -- see docs/src/testing/ci-tiers.md "Re-baselining". - name: Upload re-baselined gap snapshot if: fromJSON(needs.plan.outputs.plan).gap.update_snapshot - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: gap-snapshot-update path: test-parity/gap_snapshot.json - name: Upload gap report if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: gap-suite-report-${{ fromJSON(needs.plan.outputs.plan).gap.mode }}-shard-${{ matrix.shard }} path: test-parity/reports/ @@ -2398,18 +2428,16 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 150 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + run: rustup toolchain install nightly-2026-08-20 --profile minimal - uses: ./.github/actions/setup-llvm22 - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 with: shared-key: "${{ runner.os }}-perry" save-if: ${{ github.ref == 'refs/heads/main' }} - name: Setup Node.js - uses: actions/setup-node@v7 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: # Single source of truth: .node-version at the repo root. Node is the # gap/parity oracle (we byte-diff against it), so the version is a @@ -2434,7 +2462,7 @@ jobs: --known test-parity/known_failures.json - name: Upload shard report if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: parity-shard-${{ matrix.shard }} path: | @@ -2449,7 +2477,7 @@ jobs: # error message rather than inferring from the test family. - name: Upload compile-error logs if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: parity-compile-errors-${{ runner.os }}-shard-${{ matrix.shard }} path: test-parity/output/*.compile_error.log @@ -2474,9 +2502,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Download shard reports - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: pattern: parity-shard-* path: parity-shards @@ -2519,7 +2547,7 @@ jobs: cat test-parity/reports/parity_matrix_latest.md >> "$GITHUB_STEP_SUMMARY" - name: Upload merged parity report if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: parity-report path: | @@ -2560,15 +2588,13 @@ jobs: # `run_extended_tests=true` runs this job on demand. runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + run: rustup toolchain install nightly-2026-08-20 --profile minimal - uses: ./.github/actions/setup-llvm22 - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 with: shared-key: "${{ runner.os }}-perry" save-if: ${{ github.ref == 'refs/heads/main' }} @@ -2840,7 +2866,7 @@ jobs: - name: Upload compile-smoke error logs if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: compile-smoke-error-logs path: /tmp/perry_smoke_logs/*.compile_error.log @@ -2903,7 +2929,7 @@ jobs: - name: Upload GC evidence artifacts if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: gc-evidence-${{ runner.os }} path: ${{ runner.temp }}/gc-evidence @@ -2952,15 +2978,13 @@ jobs: # (cargo-test/lint/api-docs-drift/compiler-output-regression) still gate. runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + run: rustup toolchain install nightly-2026-08-20 --profile minimal - uses: ./.github/actions/setup-llvm22 - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 with: shared-key: "${{ runner.os }}-perry" save-if: ${{ github.ref == 'refs/heads/main' }} @@ -3016,21 +3040,19 @@ jobs: --health-timeout=3s --health-retries=20 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + run: rustup toolchain install nightly-2026-08-20 --profile minimal - uses: ./.github/actions/setup-llvm22 - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 with: shared-key: "${{ runner.os }}-perry" save-if: ${{ github.ref == 'refs/heads/main' }} - name: Setup Node.js - uses: actions/setup-node@v7 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: # Single source of truth: .node-version at the repo root. Node is the # gap/parity oracle (we byte-diff against it), so the version is a @@ -3085,21 +3107,19 @@ jobs: # Aspirational smoke (informational) — see harmonyos-smoke. Does not block publish. runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + run: rustup toolchain install nightly-2026-08-20 --profile minimal - uses: ./.github/actions/setup-llvm22 - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 with: shared-key: "${{ runner.os }}-perry" save-if: ${{ github.ref == 'refs/heads/main' }} - name: Setup Node.js - uses: actions/setup-node@v7 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: # Single source of truth: .node-version at the repo root. Node is the # gap/parity oracle (we byte-diff against it), so the version is a @@ -3117,7 +3137,7 @@ jobs: - name: Upload Ink fixture logs if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: ink-link-smoke-logs path: | @@ -3141,21 +3161,19 @@ jobs: if: fromJSON(needs.plan.outputs.plan).jobs.effect_basic_smoke runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + run: rustup toolchain install nightly-2026-08-20 --profile minimal - uses: ./.github/actions/setup-llvm22 - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 with: shared-key: "${{ runner.os }}-perry" save-if: ${{ github.ref == 'refs/heads/main' }} - name: Setup Node.js - uses: actions/setup-node@v7 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: # Single source of truth: .node-version at the repo root. Node is the # gap/parity oracle (we byte-diff against it), so the version is a @@ -3173,7 +3191,7 @@ jobs: - name: Upload Effect fixture logs if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: effect-basic-smoke-logs path: | @@ -3295,7 +3313,7 @@ jobs: run: shell: ${{ matrix.shell }} steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 # macos-14 ships with ~14 GB free disk after the preinstalled Xcode + # iOS/tvOS/watchOS simulator runtime images. Several `cargo build @@ -3315,9 +3333,7 @@ jobs: echo "Disk free: ${BEFORE} -> ${AFTER}" - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + run: rustup toolchain install nightly-2026-08-20 --profile minimal - uses: ./.github/actions/setup-llvm22 - name: Set up MSVC environment (Windows) @@ -3326,7 +3342,7 @@ jobs: # shell session has no clue where, and perry's LNK1181 fatal # errors looking for Windows SDK libs. if: matrix.os == 'windows-2022' - uses: ilammy/msvc-dev-cmd@v1 + uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1 - name: Install GTK4 + GStreamer + Xvfb + PulseAudio headers (Linux) if: matrix.os == 'ubuntu-24.04' @@ -3402,7 +3418,7 @@ jobs: if: matrix.os == 'macos-14' || matrix.os == 'ubuntu-24.04' run: rustup target add aarch64-linux-android - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 with: shared-key: "${{ runner.os }}-perry" save-if: ${{ github.ref == 'refs/heads/main' }} @@ -3444,14 +3460,14 @@ jobs: - name: Upload doc-tests report if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: doc-tests-report-${{ matrix.os }} path: docs/examples/_reports/latest.json - name: Upload gallery screenshot + diff artifacts if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: gallery-screenshots-${{ matrix.os }} path: | @@ -3469,15 +3485,13 @@ jobs: if: fromJSON(needs.plan.outputs.plan).jobs.binary_size runs-on: macos-14 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + run: rustup toolchain install nightly-2026-08-20 --profile minimal - uses: ./.github/actions/setup-llvm22 - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 with: shared-key: "${{ runner.os }}-perry" save-if: ${{ github.ref == 'refs/heads/main' }} @@ -3494,7 +3508,7 @@ jobs: cat /tmp/sizes.md - name: Upload size report - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: binary-sizes path: /tmp/sizes.md diff --git a/.github/workflows/tls-budget.yml b/.github/workflows/tls-budget.yml index f52a9bc2c7..ca3ed060bc 100644 --- a/.github/workflows/tls-budget.yml +++ b/.github/workflows/tls-budget.yml @@ -105,7 +105,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false - name: Self-test the budget verdict logic @@ -130,7 +130,7 @@ jobs: contents: read pull-requests: read steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false @@ -163,13 +163,11 @@ jobs: - name: Install Rust toolchain if: steps.relevance.outputs.run == 'true' - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 'nightly-2026-08-20' + run: rustup toolchain install nightly-2026-08-20 --profile minimal - uses: ./.github/actions/setup-llvm22 if: steps.relevance.outputs.run == 'true' - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 if: steps.relevance.outputs.run == 'true' with: shared-key: "${{ runner.os }}-perry" @@ -198,7 +196,7 @@ jobs: - name: Attach the profiles if: always() && steps.relevance.outputs.run == 'true' - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: tls-budget-profiles path: ${{ runner.temp }}/tls-budget/*.sample diff --git a/changelog.d/8679-rs4gc-budget-spill-retry.md b/changelog.d/8679-rs4gc-budget-spill-retry.md new file mode 100644 index 0000000000..d2dbba098d --- /dev/null +++ b/changelog.d/8679-rs4gc-budget-spill-retry.md @@ -0,0 +1,4 @@ +Fixed large native functions whose statepoint rewrite crossed the LLVM +instruction budget but fell below the root-spill estimate threshold. Perry now +re-lowers only those functions with precise shadow-frame roots and retries the +same optimization pipeline instead of refusing the whole codegen unit. diff --git a/crates/perry-codegen/src/codegen/helpers.rs b/crates/perry-codegen/src/codegen/helpers.rs index 12b8c43d6b..b825dbf7d5 100644 --- a/crates/perry-codegen/src/codegen/helpers.rs +++ b/crates/perry-codegen/src/codegen/helpers.rs @@ -391,9 +391,10 @@ pub(crate) fn inline_hot_small_max_call_sites() -> u32 { /// fan-out it avoids (an ~8M function spilled in 303 s vs 180 s fanned out, /// #8620) — and above it fan-out risks not finishing and the shadow frame wins. /// The former 4M default fired on ~8M functions that fan out fine in minutes. -/// The post-RS4GC instruction-budget assertion (#8586, inprocess.rs) backstops -/// any function this estimate misses: it fails loudly rather than hanging, so -/// raising the threshold is safe. +/// The post-RS4GC instruction budget (#8586/#8679, inprocess.rs) backstops any +/// function this estimate misses: it re-lowers that function onto a precise +/// shadow frame and retries before LLVM's optimizer can hang, so raising the +/// estimate threshold is safe. /// /// `PERRY_ROOT_SPILL_RELOCATIONS=` overrides it; `0` disables spilling /// (every function stays on native statepoints, the pre-#8583 behavior). diff --git a/crates/perry-codegen/src/codegen/mod.rs b/crates/perry-codegen/src/codegen/mod.rs index ea5f58e119..d18666bbd4 100644 --- a/crates/perry-codegen/src/codegen/mod.rs +++ b/crates/perry-codegen/src/codegen/mod.rs @@ -3354,28 +3354,30 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result> progress.phase(3, "object ready; releasing generated IR"); return result; } - let units = llmod.render_codegen_units(n_units); - log::debug!( - "perry-codegen: split '{}' into {} codegen units", - hir.name, - units.len() - ); - // #7154: dump the units. The comment above used to claim `PERRY_SAVE_LL` - // took the single-text path — it never did; this `return` fires before - // the `PERRY_SAVE_LL` write below. So `--trace llvm` silently emitted - // NOTHING for any module past `MIN_CALLABLES_TO_SPLIT`, i.e. exactly the - // largest modules, which is where a static IR audit - // (`scripts/gc_root_dominance_check.py`) most needs to look — a corpus - // that quietly omits its biggest members makes a clean verdict - // meaningless. One file per unit, not one concatenation: the units are - // already materialized here, so this adds no peak. - if let Ok(save_dir) = std::env::var("PERRY_SAVE_LL") { - for (i, unit) in units.iter().enumerate() { - let filename = format!("{}/{}.unit{}.ll", save_dir, module_prefix, i); - let _ = std::fs::write(&filename, unit); + loop { + let units = llmod.render_codegen_units(n_units); + log::debug!( + "perry-codegen: split '{}' into {} codegen units", + hir.name, + units.len() + ); + // #7154: dump the units. The comment above used to claim + // `PERRY_SAVE_LL` took the single-text path — it never did; this + // return fires before the write below. One file per unit, not one + // concatenation: the units are already materialized here, so this + // adds no peak. + if let Ok(save_dir) = std::env::var("PERRY_SAVE_LL") { + for (i, unit) in units.iter().enumerate() { + let filename = format!("{}/{}.unit{}.ll", save_dir, module_prefix, i); + let _ = std::fs::write(&filename, unit); + } + } + match crate::linker::compile_units_to_object(&units, opts.target.as_deref()) { + Ok(object) => return Ok(object), + Err(error) if apply_rs4gc_budget_retry(&mut llmod, &error)? => continue, + Err(error) => return Err(error), } } - return crate::linker::compile_units_to_object(&units, opts.target.as_deref()); } // exp/llvm-inprocess Phase 2: `PERRY_LLVM_INPROCESS=native` constructs @@ -3383,29 +3385,60 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result> // textual); `=diff` builds both arms and diffs them. Unit-split and // emit_ir_only paths above stay textual (they fall into the in-process // *transport* under these values, so no clang subprocess either way). - if let Some(result) = try_native_construction(&llmod, opts.target.as_deref(), &module_prefix) { + if let Some(result) = + try_native_construction(&mut llmod, opts.target.as_deref(), &module_prefix) + { return result; } - let ll_text = llmod.to_ir(); - log::debug!( - "perry-codegen: emitted {} bytes of LLVM IR for '{}' ({} interned strings)", - ll_text.len(), - hir.name, - strings.len() - ); - // Save .ll files when PERRY_SAVE_LL= is set - if let Ok(save_dir) = std::env::var("PERRY_SAVE_LL") { - let filename = format!("{}/{}.ll", save_dir, module_prefix); - let _ = std::fs::write(&filename, &ll_text); - } - if opts.emit_ir_only { - Ok(ll_text.into_bytes()) - } else { - crate::linker::compile_ll_to_object(&ll_text, opts.target.as_deref()) + loop { + let ll_text = llmod.to_ir(); + log::debug!( + "perry-codegen: emitted {} bytes of LLVM IR for '{}' ({} interned strings)", + ll_text.len(), + hir.name, + strings.len() + ); + // Save .ll files when PERRY_SAVE_LL= is set + if let Ok(save_dir) = std::env::var("PERRY_SAVE_LL") { + let filename = format!("{}/{}.ll", save_dir, module_prefix); + let _ = std::fs::write(&filename, &ll_text); + } + if opts.emit_ir_only { + return Ok(ll_text.into_bytes()); + } + match crate::linker::compile_ll_to_object(&ll_text, opts.target.as_deref()) { + Ok(object) => return Ok(object), + Err(error) if apply_rs4gc_budget_retry(&mut llmod, &error)? => continue, + Err(error) => return Err(error), + } } } +/// Consume the typed post-RS4GC budget signal on text-transport paths. The +/// native constructors have the same loop closer to their LLVM modules; text +/// compilation returns through `linker`, so its retry belongs at the last +/// point where the lowering-owned `LlModule` is still available. +#[cfg(feature = "llvm-inprocess")] +fn apply_rs4gc_budget_retry( + llmod: &mut crate::module::LlModule, + error: &anyhow::Error, +) -> Result { + let Some(violations) = crate::inprocess::rs4gc_budget_retry(error) else { + return Ok(false); + }; + crate::native_emit::apply_budget_spill_retry(llmod.functions_mut(), &violations)?; + Ok(true) +} + +#[cfg(not(feature = "llvm-inprocess"))] +fn apply_rs4gc_budget_retry( + _llmod: &mut crate::module::LlModule, + _error: &anyhow::Error, +) -> Result { + Ok(false) +} + /// exp/llvm-inprocess: unit-split twin of [`try_native_construction`]. #[cfg(feature = "llvm-inprocess")] fn try_native_units( @@ -3445,7 +3478,7 @@ fn try_native_units( /// in-process mode is requested, so the flag can never silently no-op. #[cfg(feature = "llvm-inprocess")] fn try_native_construction( - llmod: &crate::module::LlModule, + llmod: &mut crate::module::LlModule, target: Option<&str>, module_prefix: &str, ) -> Option>> { @@ -3472,7 +3505,7 @@ fn try_native_construction( #[cfg(not(feature = "llvm-inprocess"))] fn try_native_construction( - _llmod: &crate::module::LlModule, + _llmod: &mut crate::module::LlModule, _target: Option<&str>, _module_prefix: &str, ) -> Option>> { diff --git a/crates/perry-codegen/src/function.rs b/crates/perry-codegen/src/function.rs index 3807c36395..39d2f54186 100644 --- a/crates/perry-codegen/src/function.rs +++ b/crates/perry-codegen/src/function.rs @@ -302,12 +302,35 @@ impl LlFunction { /// into every caller's hot loop. Skip the frame entirely; the /// to_ir() rewrite pass keys off `shadow_frame_slot.is_some()`, /// so no matching pop is emitted either. - /// #8583: route this function's precise roots through the heap shadow - /// frame instead of native statepoints. Must be called BEFORE - /// `enable_shadow_frame` / `enable_post_init_shadow_frame` so the frame is - /// built in shadow form. No effect once a frame has been emitted. - pub fn request_shadow_frame_spill(&mut self) { + /// #8583/#8679: route this function's precise roots through the heap + /// shadow frame instead of native statepoints. + /// + /// The estimate-driven path calls this before `enable_shadow_frame`, while + /// the post-RS4GC budget retry calls it after lowering is complete. In the + /// latter case the native-root path deliberately retained the original + /// `js_shadow_slot_bind` calls until final rendering, so converting the + /// recorded stack-map request back into a shadow-frame push is a complete + /// re-lowering: final rendering keeps those binds, adds the matching pops, + /// and drops the GC strategy so RS4GC skips the function on retry. + /// + /// Returns `true` only when this call changed the lowering. A retry driver + /// uses that to reject an impossible second retry instead of looping. + pub fn request_shadow_frame_spill(&mut self) -> bool { + if self.force_shadow_frame { + return false; + } self.force_shadow_frame = true; + self.stack_map_requested = false; + if self.shadow_frame_requested + && self.shadow_frame_slot.is_none() + && self.stack_map_slot_count != 0 + { + self.emit_shadow_frame_push( + self.stack_map_slot_count, + self.shadow_frame_post_init_region, + ); + } + true } /// Whether this function spills its roots to the shadow frame (#8583). @@ -1363,6 +1386,53 @@ mod define_header_tests { ); } + /// #8679's budget is learned only after RS4GC, so the durable fallback + /// necessarily asks an already-lowered function to change root lowering. + /// This pins that late request to the same complete shadow-frame shape as + /// the estimate-driven early request, including balanced return pops. + #[test] + fn a_post_lowering_spill_request_rebuilds_the_shadow_frame() { + use crate::codegen::helpers::NativeRootsPin; + use crate::types::{I64, PTR}; + const STRATEGY: &str = "gc \"statepoint-example\""; + + let _native = NativeRootsPin::native(); + let mut function = LlFunction::new("late_spill", crate::types::VOID, vec![]); + function.enable_post_init_shadow_frame(0); + let idx = function + .reserve_shadow_slot() + .expect("native lowering reserves a precise-root slot"); + let root = function.alloca_entry(I64); + function.entry_allocas_push_store(I64, "0", &root); + function.entry_setup_call_void( + "js_shadow_slot_bind", + &[(crate::types::I32, &idx.to_string()), (PTR, &root)], + ); + function.mark_entry_init_boundary(); + let entry = function.create_block("entry"); + let _ = entry.call(I64, "may_collect", &[]); + entry.ret_void(); + + let native_ir = function.to_ir(); + assert!(native_ir.contains(STRATEGY)); + assert!(!native_ir.contains("@js_shadow_frame_enter")); + assert!(!native_ir.contains("@js_shadow_slot_bind")); + + assert!( + function.request_shadow_frame_spill(), + "the first late request must change the lowering" + ); + assert!( + !function.request_shadow_frame_spill(), + "a repeated request must report that no retry progress is possible" + ); + let shadow_ir = function.to_ir(); + assert!(!shadow_ir.contains(STRATEGY), "{shadow_ir}"); + assert!(shadow_ir.contains("call ptr @js_shadow_frame_enter(i32 1)")); + assert!(shadow_ir.contains("call void @js_shadow_slot_bind(i32 0")); + assert!(shadow_ir.contains("call void @js_shadow_frame_pop(i64")); + } + /// `force_external` drops only the linkage keyword. The codegen-unit path /// depends on that and on nothing else changing. #[test] diff --git a/crates/perry-codegen/src/inprocess.rs b/crates/perry-codegen/src/inprocess.rs index 513dd8c21b..aa1ffbc654 100644 --- a/crates/perry-codegen/src/inprocess.rs +++ b/crates/perry-codegen/src/inprocess.rs @@ -27,6 +27,7 @@ use inkwell::passes::PassBuilderOptions; use inkwell::targets::{ CodeModel, FileType, InitializationConfig, RelocMode, Target, TargetMachine, TargetTriple, }; +use inkwell::values::AsValueRef; use inkwell::OptimizationLevel; use crate::linker::STATEPOINT_REWRITE_PASSES; @@ -383,12 +384,10 @@ fn module_instruction_census( /// Instruction budget for ONE function after `rewrite-statepoints-for-gc`. /// -/// This is an assertion about the estimate that keeps relocation fan-out out -/// of LLVM's input (#8583), not an optimization policy: a function past it is -/// refused loudly, never demoted. The #8421 contract — every function is -/// optimized at the level the plan asked for — stays intact; what this adds -/// is that an estimator miss fails in seconds with the function's name and -/// sizes instead of hanging the build for hours. +/// This is the measured backstop for the estimate that keeps relocation +/// fan-out out of LLVM's optimizer input (#8583). A function past it is sent +/// back to codegen for a shadow-frame spill and then compiled again at the +/// requested optimization level (#8679); it is never demoted or refused. /// /// Calibrated between the two measured points of #8128 on the Next 16.3.0 /// production bundle: the largest post-rewrite function that finished @@ -405,6 +404,52 @@ enum RewriteBudget { Warn(usize), } +/// One function that must be re-lowered onto a shadow frame before LLVM can +/// safely optimize its codegen unit. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct Rs4gcBudgetViolation { + /// LLVM symbol of the function to spill. + pub name: String, + /// Instruction count before RS4GC, when the caller requested a census. + pub pre_instructions: Option, + /// Instruction count after RS4GC and before the optimizer. + pub post_instructions: usize, + /// Active per-function instruction limit. + pub cap: usize, +} + +/// Typed backend signal consumed by the codegen retry loops. Keeping this as +/// an error lets every existing LLVM API stop before the super-linear +/// optimizer, while the type (preserved through `anyhow` contexts) prevents +/// callers from scraping a diagnostic string for function names. +#[derive(Debug)] +struct Rs4gcBudgetExceeded { + violations: Vec, +} + +impl std::fmt::Display for Rs4gcBudgetExceeded { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for (index, violation) in self.violations.iter().enumerate() { + if index != 0 { + writeln!(f)?; + } + write!(f, "{}", rewrite_budget_message(violation, true))?; + } + Ok(()) + } +} + +impl std::error::Error for Rs4gcBudgetExceeded {} + +/// Recover an RS4GC spill request through any diagnostic contexts added by +/// the native or text transport layers. +pub(crate) fn rs4gc_budget_retry(error: &anyhow::Error) -> Option> { + error + .chain() + .find_map(|cause| cause.downcast_ref::()) + .map(|request| request.violations.clone()) +} + fn parse_rewrite_budget(value: Option<&str>) -> RewriteBudget { match value.map(str::trim) { None | Some("") => RewriteBudget::Error(DEFAULT_RS4GC_MAX_INSTRS), @@ -427,21 +472,92 @@ fn parse_rewrite_budget(value: Option<&str>) -> RewriteBudget { } fn rs4gc_instruction_budget() -> RewriteBudget { + #[cfg(test)] + if let Some(budget) = TEST_RS4GC_BUDGET.with(std::cell::Cell::get) { + return budget; + } parse_rewrite_budget(std::env::var("PERRY_LL_RS4GC_MAX_INSTRS").ok().as_deref()) } -/// Every defined function whose post-rewrite body exceeds `cap`. +#[cfg(test)] +thread_local! { + static TEST_RS4GC_BUDGET: std::cell::Cell> = const { + std::cell::Cell::new(None) + }; +} + +/// Thread-local budget seam for native-construction tests. Unlike mutating +/// `PERRY_LL_RS4GC_MAX_INSTRS`, this cannot make concurrently-running LLVM +/// tests spuriously spill or fail. +#[cfg(test)] +pub(crate) fn with_test_rs4gc_budget(cap: usize, run: impl FnOnce() -> T) -> T { + struct Restore(Option); + impl Drop for Restore { + fn drop(&mut self) { + TEST_RS4GC_BUDGET.set(self.0); + } + } + let old = TEST_RS4GC_BUDGET.replace(Some(RewriteBudget::Error(cap))); + let _restore = Restore(old); + run() +} + +#[cfg(test)] +/// Return the producer thread's test-only error budget for worker inheritance. +pub(crate) fn test_rs4gc_budget_cap() -> Option { + TEST_RS4GC_BUDGET.with(|budget| match budget.get() { + Some(RewriteBudget::Error(cap)) => Some(cap), + _ => None, + }) +} + +#[cfg(test)] +/// Install the producer's test budget around one worker-thread backend call. +pub(crate) fn with_inherited_test_rs4gc_budget( + cap: Option, + run: impl FnOnce() -> T, +) -> T { + match cap { + Some(cap) => with_test_rs4gc_budget(cap, run), + None => run(), + } +} + +/// Names of functions that actually entered RS4GC. A shadow-spilled function +/// still lives in a native-roots module, but carries no GC strategy and must +/// not trip the retry budget a second time merely because its ordinary body +/// is large. +fn rs4gc_functions(module: &inkwell::module::Module<'_>) -> std::collections::HashSet { + let mut names = std::collections::HashSet::new(); + let mut function = module.get_first_function(); + while let Some(f) = function { + if f.count_basic_blocks() > 0 { + let gc = unsafe { llvm_sys::core::LLVMGetGC(f.as_value_ref()) }; + if !gc.is_null() + && unsafe { std::ffi::CStr::from_ptr(gc) }.to_bytes() == b"statepoint-example" + { + names.insert(f.get_name().to_string_lossy().into_owned()); + } + } + function = f.get_next_function(); + } + names +} + +/// Every RS4GC-participating function whose post-rewrite body exceeds `cap`. fn rs4gc_budget_violations( module: &inkwell::module::Module<'_>, cap: usize, + rewritten_functions: &std::collections::HashSet, ) -> Vec<(String, usize)> { let mut over = Vec::new(); let mut function = module.get_first_function(); while let Some(f) = function { if f.count_basic_blocks() > 0 { + let name = f.get_name().to_string_lossy().into_owned(); let n = function_instruction_count(f); - if n > cap { - over.push((f.get_name().to_string_lossy().into_owned(), n)); + if n > cap && rewritten_functions.contains(&name) { + over.push((name, n)); } } function = f.get_next_function(); @@ -449,18 +565,23 @@ fn rs4gc_budget_violations( over } -fn rewrite_budget_message(name: &str, post: usize, cap: usize, pre: Option) -> String { - let before = pre +fn rewrite_budget_message(violation: &Rs4gcBudgetViolation, retry: bool) -> String { + let before = violation + .pre_instructions .map(|n| format!(" (it was {n} before the rewrite)")) .unwrap_or_default(); + let outcome = if retry { + "Perry will re-lower this function with precise roots in a shadow frame, then retry the \ + unit at the requested optimization level" + } else { + "the warning-only budget override leaves the function for LLVM to optimize" + }; format!( - "rewrite-statepoints-for-gc grew `{name}` to {post} instructions{before}; the \ - per-function budget is {cap}. LLVM's optimizer is super-linear on statepoint \ - relocation fan-out of this size and the compile would not finish in practical \ - time, so the unit is refused instead of being left to hang. Perry does not lower \ - the optimization level for it: the fix is to keep this function's GC roots out \ - of the relocation set or to split it (#8583). Override with \ - PERRY_LL_RS4GC_MAX_INSTRS= (raise), =warn: (warn only) or =0 (disable)." + "rewrite-statepoints-for-gc grew `{}` to {} instructions{before}; the \ + per-function budget is {}. LLVM's optimizer is super-linear on statepoint \ + relocation fan-out of this size; {outcome} (#8679). Override with \ + PERRY_LL_RS4GC_MAX_INSTRS= (raise), =warn: (warn only) or =0 (disable).", + violation.name, violation.post_instructions, violation.cap ) } @@ -470,25 +591,34 @@ fn enforce_rs4gc_instruction_budget( module: &inkwell::module::Module<'_>, budget: RewriteBudget, pre: &std::collections::HashMap, + rewritten_functions: &std::collections::HashSet, ) -> Result<()> { let (cap, fatal) = match budget { RewriteBudget::Off => return Ok(()), RewriteBudget::Error(cap) => (cap, true), RewriteBudget::Warn(cap) => (cap, false), }; - let over = rs4gc_budget_violations(module, cap); + let over = rs4gc_budget_violations(module, cap, rewritten_functions); if over.is_empty() { return Ok(()); } - let messages: Vec = over - .iter() - .map(|(name, post)| rewrite_budget_message(name, *post, cap, pre.get(name).copied())) + let violations: Vec = over + .into_iter() + .map(|(name, post_instructions)| Rs4gcBudgetViolation { + pre_instructions: pre.get(&name).copied(), + name, + post_instructions, + cap, + }) .collect(); if fatal { - return Err(anyhow!("{}", messages.join("\n"))); + return Err(anyhow::Error::new(Rs4gcBudgetExceeded { violations })); } - for m in messages { - eprintln!("perry: warning: {m}"); + for violation in &violations { + eprintln!( + "perry: warning: {}", + rewrite_budget_message(violation, false) + ); } Ok(()) } @@ -593,6 +723,7 @@ fn optimize_and_emit( // Sizes before the rewrite: the budget message below names them, and // the per-unit report compares them with the post-rewrite census. let budget = rs4gc_instruction_budget(); + let rewritten_functions = rs4gc_functions(module); let pre_sizes = if budget == RewriteBudget::Off && stats.is_none() { std::collections::HashMap::new() } else { @@ -635,8 +766,11 @@ fn optimize_and_emit( stats.post_rewrite_instructions = total; stats.post_rewrite_widest = widest; } - // The relocation-fan-out assertion (#8583): refuse, never demote. - enforce_rs4gc_instruction_budget(module, budget, &pre_sizes)?; + // The relocation-fan-out backstop (#8583/#8679): stop before the + // super-linear optimizer and ask codegen to retry the named functions + // with precise shadow-frame roots. The retry keeps this same pipeline + // and optimization level; only the GC-root representation changes. + enforce_rs4gc_instruction_budget(module, budget, &pre_sizes, &rewritten_functions)?; } let pipeline = match opt { @@ -820,6 +954,7 @@ mod tests { let before = parse_ir_text(&context, &fixture, "fanout_before").expect("fixture parses"); let after = parse_ir_text(&context, &rewritten, "fanout_after").expect("rewritten parses"); let pre = pre_rewrite_sizes(&before); + let rewritten_functions = rs4gc_functions(&before); let pre_f = pre["f"]; let (_, post_total, post_widest) = module_instruction_census(&after); let post_f = post_widest.as_ref().map(|(_, n)| *n).unwrap_or(0); @@ -831,10 +966,10 @@ mod tests { let cap = pre_f + (post_f - pre_f) / 2; assert!( - rs4gc_budget_violations(&before, cap).is_empty(), + rs4gc_budget_violations(&before, cap, &rewritten_functions).is_empty(), "the pre-rewrite module is under the budget by construction" ); - let over = rs4gc_budget_violations(&after, cap); + let over = rs4gc_budget_violations(&after, cap, &rewritten_functions); assert_eq!( over.len(), 1, @@ -843,8 +978,19 @@ mod tests { assert_eq!(over[0].0, "f"); assert_eq!(over[0].1, post_f); - let err = enforce_rs4gc_instruction_budget(&after, RewriteBudget::Error(cap), &pre) - .expect_err("the default spelling refuses the unit"); + let err = enforce_rs4gc_instruction_budget( + &after, + RewriteBudget::Error(cap), + &pre, + &rewritten_functions, + ) + .expect_err("the default spelling requests a spill retry"); + let retry = rs4gc_budget_retry(&err).expect("the request stays typed"); + assert_eq!(retry.len(), 1); + assert_eq!(retry[0].name, "f"); + assert_eq!(retry[0].pre_instructions, Some(pre_f)); + assert_eq!(retry[0].post_instructions, post_f); + assert_eq!(retry[0].cap, cap); let msg = format!("{err:#}"); for needle in [ "`f`", @@ -852,7 +998,8 @@ mod tests { &format!("it was {pre_f} before"), &format!("budget is {cap}"), "PERRY_LL_RS4GC_MAX_INSTRS", - "#8583", + "re-lower", + "#8679", ] { assert!( msg.contains(needle), @@ -863,12 +1010,35 @@ mod tests { !msg.contains("optnone"), "the budget is an assertion, never a demotion:\n{msg}" ); - enforce_rs4gc_instruction_budget(&after, RewriteBudget::Warn(cap), &pre) - .expect("warn spelling does not refuse"); - enforce_rs4gc_instruction_budget(&after, RewriteBudget::Off, &pre) - .expect("off spelling does not refuse"); - enforce_rs4gc_instruction_budget(&after, RewriteBudget::Error(post_f), &pre) - .expect("a budget at the exact size is not exceeded"); + enforce_rs4gc_instruction_budget( + &after, + RewriteBudget::Warn(cap), + &pre, + &rewritten_functions, + ) + .expect("warn spelling does not retry"); + enforce_rs4gc_instruction_budget(&after, RewriteBudget::Off, &pre, &rewritten_functions) + .expect("off spelling does not retry"); + enforce_rs4gc_instruction_budget( + &after, + RewriteBudget::Error(post_f), + &pre, + &rewritten_functions, + ) + .expect("a budget at the exact size is not exceeded"); + + // The retry removes the function's GC strategy. Its ordinary shadow + // body may itself exceed a deliberately tiny test cap, but it must not + // request the same spill forever: only functions that entered RS4GC + // are governed by this relocation-fan-out budget. + let no_rewritten_functions = std::collections::HashSet::new(); + enforce_rs4gc_instruction_budget( + &after, + RewriteBudget::Error(cap), + &pre, + &no_rewritten_functions, + ) + .expect("a shadow-spilled function is outside the RS4GC budget"); } fn constant_fold_order_fixture(folded: bool) -> String { diff --git a/crates/perry-codegen/src/linker.rs b/crates/perry-codegen/src/linker.rs index a90ba9ff46..ff6804db34 100644 --- a/crates/perry-codegen/src/linker.rs +++ b/crates/perry-codegen/src/linker.rs @@ -841,14 +841,27 @@ fn compile_ll_inprocess_in( Ok(bytes) } Err(e) => { - let error = anyhow!( + // Preserve typed backend errors through this diagnostic layer. + // In particular, #8679's codegen caller must be able to recover + // an `Rs4gcBudgetExceeded` and re-lower the named functions; a + // freshly formatted anyhow string would turn that retry request + // back into the old hard refusal. + let error = e.context(format!( "in-process LLVM compile failed (PERRY_LLVM_INPROCESS).\n\ - requested -target: {}\n\ - \n\ - {}", - plan.effective_target, - e - ); + requested -target: {}", + plan.effective_target + )); + if crate::inprocess::rs4gc_budget_retry(&error).is_some() { + // This is expected control flow, not a failed compile: the + // lowering owner will rebuild the named functions. Do not + // consume the process-wide "retain the first LLVM failure" + // slot or leave an intermediate behind unless the user + // explicitly requested all IR via PERRY_LLVM_KEEP_IR. + if !policy.keep { + let _ = fs::remove_dir_all(&paths.scratch_dir); + } + return Err(error); + } Err(failed_scratch.finish_with_ir(error, ll_text)) } } diff --git a/crates/perry-codegen/src/native_emit.rs b/crates/perry-codegen/src/native_emit.rs index 2010ee7090..9258884027 100644 --- a/crates/perry-codegen/src/native_emit.rs +++ b/crates/perry-codegen/src/native_emit.rs @@ -34,7 +34,7 @@ //! the remaining per-LINE formatting; the `instructions=` counter logged per //! module is that migration's ratchet. -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context as _, Result}; use inkwell::context::Context; use inkwell::module::Module; @@ -166,8 +166,55 @@ struct FrozenUnit { function_count: usize, } +/// Apply a typed post-RS4GC budget request to the lowering-owned functions +/// that produced a module/unit. The request is expected to make progress for +/// every named function; otherwise retrying would either preserve the refusal +/// or loop forever, so fail with the original names and counts instead. +pub(crate) fn apply_budget_spill_retry<'a>( + funcs: impl IntoIterator, + violations: &[crate::inprocess::Rs4gcBudgetViolation], +) -> Result<()> { + let mut changed = std::collections::HashSet::new(); + for function in funcs { + let Some(violation) = violations + .iter() + .find(|violation| function.name == violation.name) + else { + continue; + }; + if function.request_shadow_frame_spill() { + changed.insert(violation.name.clone()); + eprintln!( + "perry: `{}` exceeded the post-RS4GC instruction budget ({} -> {} \ + instructions; cap {}); retrying it with precise GC roots in a shadow \ + frame at the requested optimization level (#8679)", + violation.name, + violation + .pre_instructions + .map_or_else(|| "unknown".to_string(), |n| n.to_string()), + violation.post_instructions, + violation.cap, + ); + } + } + let missing: Vec<&str> = violations + .iter() + .filter(|violation| !changed.contains(&violation.name)) + .map(|violation| violation.name.as_str()) + .collect(); + if missing.is_empty() { + Ok(()) + } else { + Err(anyhow!( + "post-RS4GC budget requested a shadow-frame retry for {}, but those \ + functions were not available for a new lowering (or were already retried)", + missing.join(", ") + )) + } +} + fn freeze_unit( - part: crate::module::OwnedCodegenUnitPart, + part: &crate::module::OwnedCodegenUnitPart, external_declarations: &[(String, String)], ) -> Result { let crate::module::OwnedCodegenUnitPart { pre, post, funcs } = part; @@ -201,11 +248,11 @@ fn freeze_unit( // no inkwell builders. Let LLVM's in-process assembly parser build // only these exceptional functions; all ordinary bodies remain on // the typed C-API path and never become text. - skeleton.push_str(&crate::module::render_fn_external(&f)); + skeleton.push_str(&crate::module::render_fn_external(f)); skeleton.push('\n'); continue; } - skeleton.push_str(&crate::module::declare_line_for(&f)); + skeleton.push_str(&crate::module::declare_line_for(f)); skeleton.push('\n'); let mut items = Vec::new(); if f.stack_map_requested() { @@ -233,7 +280,7 @@ fn freeze_unit( } functions.push(FrozenFunction { name: f.name.clone(), - header: synth_define_header(&f, true), + header: synth_define_header(f, true), items, }); } @@ -362,7 +409,15 @@ pub fn compile_module_units_native( .collect(); let target_triple = llmod.target_triple.clone(); let owned_module = std::mem::replace(llmod, LlModule::new(target_triple)); - let parts = owned_module.into_codegen_unit_parts(n); + // Keep at most a bounded window of lowering-owned units alive after they + // are frozen. A post-RS4GC budget miss needs that source graph exactly + // once so the named functions can switch root lowering and be frozen + // again; successful units are still dropped immediately (#8679). + let mut parts: Vec> = owned_module + .into_codegen_unit_parts(n) + .into_iter() + .map(Some) + .collect(); let unit_timings = std::env::var("PERRY_CODEGEN_UNIT_TIMINGS").is_ok(); let show_progress = matches!( std::env::var("PERRY_CODEGEN_PROGRESS").as_deref(), @@ -385,25 +440,34 @@ pub fn compile_module_units_native( // `LlModule::skeleton_ir`; cross-unit declarations with their actual // signatures already live in each part's filtered `pre`. let llvm_started = std::time::Instant::now(); + #[cfg(test)] + let test_budget = crate::inprocess::test_rs4gc_budget_cap(); let compile_one = |i: usize, unit: &FrozenUnit| -> Result> { let started = std::time::Instant::now(); let context = Context::create(); let module = crate::inprocess::parse_ir_text(&context, &unit.skeleton, "perry_native_module") - .map_err(|e| anyhow!("unit {i} skeleton: {e:#}"))?; + .with_context(|| format!("unit {i} skeleton"))?; let (t, r) = stream_frozen_functions(&context, &module, &unit.functions) - .map_err(|e| anyhow!("unit {i}: {e:#}"))?; + .with_context(|| format!("unit {i}"))?; debug_dump(&module, &format!("{module_prefix}.unit{i}")); let (effective_target, args) = crate::linker::native_plan_args(target, native_roots); let mut stats = crate::inprocess::UnitCodegenStats::default(); - let unit_bytes = crate::inprocess::optimize_and_emit_module_with_stats( - &module, - &effective_target, - &args, - native_roots, - unit_timings.then_some(&mut stats), - ) - .map_err(|e| anyhow!("unit {i}: {e:#}"))?; + let stats_out = unit_timings.then_some(&mut stats); + let optimize = || { + crate::inprocess::optimize_and_emit_module_with_stats( + &module, + &effective_target, + &args, + native_roots, + stats_out, + ) + }; + #[cfg(test)] + let optimized = crate::inprocess::with_inherited_test_rs4gc_budget(test_budget, optimize); + #[cfg(not(test))] + let optimized = optimize(); + let unit_bytes = optimized.with_context(|| format!("unit {i}"))?; if unit_timings { let widest = |w: &Option<(String, usize)>| { w.as_ref() @@ -429,7 +493,7 @@ pub fn compile_module_units_native( ); } let obj = crate::linker::finish_native_emission(unit_bytes, &effective_target, &args) - .map_err(|e| anyhow!("unit {i}: {e:#}"))?; + .with_context(|| format!("unit {i}"))?; log::debug!( "perry-codegen: native unit {i}: {} fns, {t} typed + {r} raw insts, {:.3}s", unit.function_count, @@ -447,6 +511,7 @@ pub fn compile_module_units_native( if show_progress { let estimated_mib: f64 = parts .iter() + .flatten() .map(|part| { (part.pre.len() + part.post.len() @@ -462,21 +527,23 @@ pub fn compile_module_units_native( "[perry] codegen: {module_prefix}: freeze/LLVM pipeline started: {unit_total} units, {jobs} workers, ~{estimated_mib:.1} MiB estimated IR" ); } - let completed = std::sync::atomic::AtomicUsize::new(0); let frozen = std::sync::atomic::AtomicUsize::new(0); - let slots: Vec>>>> = (0..parts.len()) - .map(|_| std::sync::Mutex::new(None)) - .collect(); - // The producer alone touches lowering-owned LlFunction/Rc state. Each - // completed owned payload immediately enters a bounded queue, letting LLVM - // consume it while the producer freezes later units. Previously all units - // were frozen into a Vec first: full Claude waited ~5 minutes before LLVM - // started and retained both graphs at peak RSS. + let mut slots: Vec>>> = (0..parts.len()).map(|_| None).collect(); + // The producer alone touches lowering-owned LlFunction/Rc state. Workers + // return their result through a second channel; on a typed budget request + // the producer can mutate that still-local graph, freeze it again, and + // resubmit it. The in-flight window stays bounded so this retry ability + // does not restore the old whole-bundle retention peak. let (sender, receiver) = std::sync::mpsc::sync_channel::<(usize, Result)>(jobs.max(1)); + let (result_sender, result_receiver) = + std::sync::mpsc::channel::<(usize, std::time::Duration, Result>)>(); let receiver = std::sync::Mutex::new(receiver); std::thread::scope(|scope| { for worker_index in 0..jobs { + let result_sender = result_sender.clone(); + let receiver = &receiver; + let compile_one = &compile_one; // LLVM recursion depth scales with function size, and a post-RS4GC // relocation-fan-out function reaches millions of instructions // (#8082) — Rust's default 2 MiB worker stack SIGBUSes on the @@ -485,40 +552,29 @@ pub fn compile_module_units_native( std::thread::Builder::new() .name(format!("perry-llvm-unit-{worker_index}")) .stack_size(64 * 1024 * 1024) - .spawn_scoped(scope, || loop { - let received = receiver - .lock() - .expect("native freeze queue poisoned") - .recv(); - let Ok((i, frozen_unit)) = received else { break }; - let unit_started = std::time::Instant::now(); - let out = frozen_unit.and_then(|unit| compile_one(i, &unit)); - let done = completed.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1; - if show_progress { - let elapsed = llvm_started.elapsed().as_secs_f64(); - let eta = if done < unit_total { - elapsed / done as f64 * (unit_total - done) as f64 - } else { - 0.0 + .spawn_scoped(scope, move || loop { + let received = receiver + .lock() + .expect("native freeze queue poisoned") + .recv(); + let Ok((i, frozen_unit)) = received else { + break; }; - eprintln!( - "[perry] codegen: {module_prefix}: LLVM unit {}/{} finished ({:.1}s; {} complete; elapsed {:.1} min; ETA ~{:.1} min)", - i + 1, unit_total, unit_started.elapsed().as_secs_f64(), done, - elapsed / 60.0, eta / 60.0 - ); - } - *slots[i].lock().expect("native codegen-unit slot poisoned") = Some(out); + let unit_started = std::time::Instant::now(); + let out = frozen_unit.and_then(|unit| compile_one(i, &unit)); + if result_sender + .send((i, unit_started.elapsed(), out)) + .is_err() + { + break; + } }) .expect("spawn LLVM unit worker"); } + drop(result_sender); let freeze_started = std::time::Instant::now(); let report_step = (unit_total / 20).max(1); - // Consume each part as soon as its owned worker payload has been - // produced. Keeping `parts` alive through the scoped worker join held - // every unit's large pre/post strings until all LLVM work completed; - // dropping that multi-gigabyte graph afterwards added a several-minute - // single-threaded destructor tail on the full Claude Code bundle. - for (i, part) in parts.into_iter().enumerate() { + let enqueue = |i: usize, part: &crate::module::OwnedCodegenUnitPart, retry: bool| -> bool { if unit_timings { // Name the widest body before LLVM ever sees it: the one // irreducible function in a bundle is the one that sets the @@ -526,7 +582,8 @@ pub fn compile_module_units_native( // not say which (#8583). if let Some(widest) = part.funcs.iter().max_by_key(|f| f.estimated_ir_bytes()) { eprintln!( - "[perry] codegen: {module_prefix}: unit {}/{unit_total}: {} fns, ~{:.1} MiB estimated IR, widest {} (~{:.1} MiB)", + "[perry] codegen: {module_prefix}: {}unit {}/{unit_total}: {} fns, ~{:.1} MiB estimated IR, widest {} (~{:.1} MiB)", + if retry { "retry " } else { "" }, i + 1, part.funcs.len(), part.funcs.iter().map(|f| f.estimated_ir_bytes()).sum::() as f64 / 1_048_576.0, @@ -537,7 +594,10 @@ pub fn compile_module_units_native( } let unit = freeze_unit(part, &external_declarations); if sender.send((i, unit)).is_err() { - break; + return false; + } + if retry { + return true; } let done = frozen.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1; if show_progress && (done == unit_total || done % report_step == 0) { @@ -550,18 +610,102 @@ pub fn compile_module_units_native( eta ); } + true + }; + + // One source unit per worker. Retrying requires retaining that source + // until LLVM answers, but there is no reason to retain a second queued + // source per worker too; freezing the next unit after one completes is + // only a small producer step and keeps the extra peak tightly bounded. + let max_in_flight = jobs.clamp(1, unit_total); + let mut next = 0usize; + let mut in_flight = 0usize; + while next < max_in_flight { + let part = parts[next] + .as_ref() + .expect("an undispatched native unit still owns its lowering graph"); + if !enqueue(next, part, false) { + break; + } + next += 1; + in_flight += 1; + } + + let mut done = 0usize; + while done < unit_total && in_flight != 0 { + let Ok((i, attempt_elapsed, out)) = result_receiver.recv() else { + break; + }; + if let Err(error) = &out { + if let Some(violations) = crate::inprocess::rs4gc_budget_retry(error) { + let retry = parts[i] + .as_mut() + .expect("a retryable native unit keeps its lowering graph"); + match apply_budget_spill_retry(retry.funcs.iter_mut(), &violations) { + Ok(()) if enqueue(i, retry, true) => continue, + Ok(()) => { + slots[i] = Some(Err(anyhow!( + "native codegen retry queue closed for unit {}/{}", + i + 1, + unit_total + ))); + } + Err(retry_error) => { + slots[i] = Some(Err(retry_error.context(format!( + "native codegen unit {}/{} could not honor its RS4GC budget retry: \ + {error:#}", + i + 1, + unit_total + )))); + } + } + } else { + slots[i] = Some(out); + } + } else { + slots[i] = Some(out); + } + + // A final result no longer needs its Rc/RefCell lowering graph. + // Drop it now, not after every unit and LLVM worker has finished. + parts[i].take(); + done += 1; + in_flight -= 1; + if show_progress { + let elapsed = llvm_started.elapsed().as_secs_f64(); + let eta = if done < unit_total { + elapsed / done as f64 * (unit_total - done) as f64 + } else { + 0.0 + }; + eprintln!( + "[perry] codegen: {module_prefix}: LLVM unit {}/{} finished ({:.1}s; {} complete; elapsed {:.1} min; ETA ~{:.1} min)", + i + 1, + unit_total, + attempt_elapsed.as_secs_f64(), + done, + elapsed / 60.0, + eta / 60.0 + ); + } + + if next < unit_total { + let part = parts[next] + .as_ref() + .expect("an undispatched native unit still owns its lowering graph"); + if enqueue(next, part, false) { + next += 1; + in_flight += 1; + } + } } drop(sender); }); let mut objs = Vec::with_capacity(unit_total); for (i, slot) in slots.into_iter().enumerate() { objs.push( - slot.into_inner() - .expect("native codegen-unit slot poisoned") - .expect("every native codegen unit is compiled") - .map_err(|e| { - anyhow!("native codegen unit {}/{} failed: {e:#}", i + 1, unit_total) - })?, + slot.expect("every native codegen unit is compiled") + .with_context(|| format!("native codegen unit {}/{} failed", i + 1, unit_total))?, ); } let merge_started = std::time::Instant::now(); @@ -597,8 +741,18 @@ pub fn compile_module_units_diff( target: Option<&str>, module_prefix: &str, ) -> Result> { - let units = llmod.render_codegen_units(n); - let bytes_text = crate::linker::compile_units_to_object(&units, target)?; + let (bytes_text, text_unit_count) = loop { + let units = llmod.render_codegen_units(n); + match crate::linker::compile_units_to_object(&units, target) { + Ok(bytes) => break (bytes, units.len()), + Err(error) => { + let Some(violations) = crate::inprocess::rs4gc_budget_retry(&error) else { + return Err(error); + }; + apply_budget_spill_retry(llmod.functions_mut(), &violations)?; + } + } + }; match compile_module_units_native(llmod, n, target, module_prefix) { Err(e) => { eprintln!("perry: [ir-diff] native unit construction FAILED (text arm used): {e:#}"); @@ -607,9 +761,9 @@ pub fn compile_module_units_diff( if bytes_text == bytes_native { eprintln!( "perry: [ir-diff] OK — native and text unit arms emit byte-identical merged \ - objects ({} bytes, {} units)", + objects ({} bytes, {} units)", bytes_text.len(), - units.len() + text_unit_count ); } else { eprintln!( @@ -630,27 +784,36 @@ fn plan_for(target: Option<&str>, native_roots: bool) -> (String, Vec) { } pub fn compile_module_native( - llmod: &LlModule, + llmod: &mut LlModule, target: Option<&str>, module_prefix: &str, ) -> Result> { - let context = Context::create(); - let module = build_native_module(&context, llmod)?; - debug_dump(&module, module_prefix); let native_roots = crate::codegen::helpers::native_stack_roots_enabled(); let (effective_target, args) = plan_for(target, native_roots); - // #7982: under the statepoint backends the plan asks for `-S`, so this - // returns assembler TEXT. It must go through the compact-map rewrite and - // the assembler before it can be called an object — the textual path has - // always done this, the native path silently did not, and the link died - // with `ld: unknown file type`. - let bytes = crate::inprocess::optimize_and_emit_module( - &module, - &effective_target, - &args, - native_roots, - )?; - crate::linker::finish_native_emission(bytes, &effective_target, &args) + loop { + let context = Context::create(); + let module = build_native_module(&context, llmod)?; + debug_dump(&module, module_prefix); + // #7982: under the statepoint backends the plan asks for `-S`, so this + // returns assembler TEXT. It must go through the compact-map rewrite + // and the assembler before it can be called an object. + match crate::inprocess::optimize_and_emit_module( + &module, + &effective_target, + &args, + native_roots, + ) { + Ok(bytes) => { + return crate::linker::finish_native_emission(bytes, &effective_target, &args); + } + Err(error) => { + let Some(violations) = crate::inprocess::rs4gc_budget_retry(&error) else { + return Err(error); + }; + apply_budget_spill_retry(llmod.functions_mut(), &violations)?; + } + } + } } /// The debug view under native construction: `PERRY_SAVE_LL=` (which @@ -933,7 +1096,7 @@ mod tests { #[test] fn native_construction_lowers_precise_roots_before_rs4gc() { let _native = crate::codegen::helpers::NativeRootsPin::native(); - let module = precise_root_fixture(false); + let mut module = precise_root_fixture(false); let text_ir = module.to_ir(); assert!( @@ -948,7 +1111,7 @@ mod tests { let text = crate::linker::compile_ll_to_object(&text_ir, None) .expect("trusted text arm emits an object"); - let native = compile_module_native(&module, None, "native_root_diff_fixture") + let native = compile_module_native(&mut module, None, "native_root_diff_fixture") .expect("direct native arm emits an object"); assert_eq!( native, text, @@ -957,6 +1120,70 @@ mod tests { ); } + /// #8679: a real backend budget miss must come back through the native + /// constructor, mutate the lowering-owned function, rebuild the module, + /// and finish emission. The one-instruction cap guarantees that the first + /// RS4GC arm trips without constructing a million-instruction fixture; + /// the successful result and retained shadow IR prove this is a retry, + /// not the former hard refusal or a disabled budget. + #[test] + fn post_rs4gc_budget_retries_with_a_shadow_frame() { + let _native = crate::codegen::helpers::NativeRootsPin::native(); + let mut module = precise_root_fixture(false); + let before = module + .deduped_function_refs() + .into_iter() + .find(|function| function.name == "native_root_diff_fixture") + .expect("fixture function exists before the retry") + .to_ir(); + assert!(before.contains("gc \"statepoint-example\""), "{before}"); + assert!(!before.contains("@js_shadow_frame_enter"), "{before}"); + + let object = crate::inprocess::with_test_rs4gc_budget(1, || { + compile_module_native(&mut module, None, "rs4gc_budget_retry_fixture") + }) + .expect("a post-RS4GC budget miss must spill and retry successfully"); + assert!(!object.is_empty()); + + let retried = module + .deduped_function_refs() + .into_iter() + .find(|function| function.name == "native_root_diff_fixture") + .expect("fixture function survives the retry"); + assert!(retried.spills_roots_to_shadow_frame()); + let after = retried.to_ir(); + assert!(!after.contains("gc \"statepoint-example\""), "{after}"); + assert!(after.contains("@js_shadow_frame_enter"), "{after}"); + assert!(after.contains("@js_shadow_slot_bind"), "{after}"); + assert!(after.contains("@js_shadow_frame_pop"), "{after}"); + } + + /// The reported Claude bundle takes the split-unit worker path. Its retry + /// source must stay on the producer thread (the `LlFunction` graph is not + /// `Send`) while LLVM reports the typed violation from a worker. A compact + /// map would prove the worker silently missed the test cap and kept the + /// statepoint lowering; no map proves the successful object came from the + /// resubmitted shadow-frame unit. + #[test] + fn split_unit_budget_retry_returns_a_shadow_rooted_object() { + let _native = crate::codegen::helpers::NativeRootsPin::native(); + let mut module = precise_root_fixture(true); + let before = module.render_codegen_units(2); + assert!( + before + .iter() + .any(|unit| unit.contains("gc \"statepoint-example\"")), + "fixture must initially send a mapped function through RS4GC" + ); + + let object = crate::inprocess::with_test_rs4gc_budget(1, || { + compile_module_units_native(&mut module, 2, None, "rs4gc_split_budget_retry_fixture") + }) + .expect("a worker budget miss must be re-lowered and resubmitted"); + assert!(!object.is_empty()); + assert_no_compact_gc_map(&object, "budget-retried split native"); + } + #[test] fn split_native_construction_lowers_precise_roots_before_rs4gc() { let _native = crate::codegen::helpers::NativeRootsPin::native(); @@ -1001,12 +1228,13 @@ mod tests { fn native_and_text_arms_agree_on_an_elf_target() { const ELF_TRIPLE: &str = "x86_64-unknown-linux-gnu"; let _native = crate::codegen::helpers::NativeRootsPin::native(); - let module = precise_root_fixture_for(ELF_TRIPLE, false); + let mut module = precise_root_fixture_for(ELF_TRIPLE, false); let text = crate::linker::compile_ll_to_object(&module.to_ir(), Some(ELF_TRIPLE)) .expect("trusted text arm emits an ELF object"); - let native = compile_module_native(&module, Some(ELF_TRIPLE), "native_root_elf_fixture") - .expect("direct native arm emits an ELF object"); + let native = + compile_module_native(&mut module, Some(ELF_TRIPLE), "native_root_elf_fixture") + .expect("direct native arm emits an ELF object"); assert_eq!( &text[..4], @@ -1095,6 +1323,24 @@ mod tests { /// Returns the text arm's object (the trusted reference) so a diff run is /// safe for real builds while surfacing every divergence. pub fn compile_module_diff( + llmod: &mut LlModule, + target: Option<&str>, + module_prefix: &str, +) -> Result> { + loop { + match compile_module_diff_once(llmod, target, module_prefix) { + Ok(bytes) => return Ok(bytes), + Err(error) => { + let Some(violations) = crate::inprocess::rs4gc_budget_retry(&error) else { + return Err(error); + }; + apply_budget_spill_retry(llmod.functions_mut(), &violations)?; + } + } + } +} + +fn compile_module_diff_once( llmod: &LlModule, target: Option<&str>, module_prefix: &str, diff --git a/crates/perry/src/commands/compile/build_cache.rs b/crates/perry/src/commands/compile/build_cache.rs index 0cced3d6dd..9e92fb8672 100644 --- a/crates/perry/src/commands/compile/build_cache.rs +++ b/crates/perry/src/commands/compile/build_cache.rs @@ -41,8 +41,9 @@ const BUILD_CACHE_ENV_VARS: &[&str] = &[ "PERRY_RS4GC", // `-Os` vs `-O3` for every native module. "PERRY_LL_SIZE_OPT", - // The post-RS4GC per-function instruction budget (#8583): a unit that one - // setting refuses must not be served from a build another accepted. + // The post-RS4GC per-function instruction budget (#8583/#8679): a function + // one setting re-lowers must not be served from a build another kept on + // statepoints. "PERRY_LL_RS4GC_MAX_INSTRS", // #8583: the relocation estimate above which a function spills its GC roots // to a shadow frame. It changes which functions carry statepoints, so it diff --git a/crates/perry/src/commands/compile/object_cache.rs b/crates/perry/src/commands/compile/object_cache.rs index 6648cb5c74..8fdc1e1b62 100644 --- a/crates/perry/src/commands/compile/object_cache.rs +++ b/crates/perry/src/commands/compile/object_cache.rs @@ -897,8 +897,8 @@ fn compute_object_cache_key_with_env( "env_ll_size_opt", env_var("PERRY_LL_SIZE_OPT").as_deref().unwrap_or(""), ); - // #8583: the post-RS4GC instruction budget decides whether a unit is - // refused; two settings must never share a cached object. + // #8583/#8679: the post-RS4GC instruction budget decides whether functions + // are re-lowered onto shadow frames; two settings must never share an object. h.field( "env_ll_rs4gc_max_instrs", env_var("PERRY_LL_RS4GC_MAX_INSTRS")