From 4fe05e2b3159de478ea40da2586f47a26cd5f3dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 2 Oct 2026 08:12:09 +0200 Subject: [PATCH 01/17] fix(gc): refresh macOS arguments mapping leaf classification --- crates/perry-codegen/src/gc_effects/macos-aarch64.tsv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/perry-codegen/src/gc_effects/macos-aarch64.tsv b/crates/perry-codegen/src/gc_effects/macos-aarch64.tsv index 709638c250..6c38748895 100644 --- a/crates/perry-codegen/src/gc_effects/macos-aarch64.tsv +++ b/crates/perry-codegen/src/gc_effects/macos-aarch64.tsv @@ -49,7 +49,7 @@ js_arguments_bundle_get_slow Reenters js_arguments_bundle_index_get Reenters js_arguments_object_alloc Reenters js_arguments_object_alloc_mapped Reenters -js_arguments_object_map_index Reenters +js_arguments_object_map_index Leaf js_array_alloc ThrowOnly js_array_alloc_literal ThrowOnly js_array_alloc_with_length ThrowOnly From 17f13fe7eb86a09252b765e5a0b9a85529b14b3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 2 Oct 2026 08:13:12 +0200 Subject: [PATCH 02/17] Add changeset for macOS arguments leaf table refresh --- changelog.d/11751-macos-arguments-leaf-table.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/11751-macos-arguments-leaf-table.md diff --git a/changelog.d/11751-macos-arguments-leaf-table.md b/changelog.d/11751-macos-arguments-leaf-table.md new file mode 100644 index 0000000000..de7b32869c --- /dev/null +++ b/changelog.d/11751-macos-arguments-leaf-table.md @@ -0,0 +1 @@ +Refresh the macOS GC call-effect classification of `js_arguments_object_map_index` from the actual release-archive classifier artifact. The preallocated mapped-arguments slot store is a leaf; retaining the conservative `Reenters` row caused the macOS classifier gate to fail with one safe drift. Runtime behavior and other-target tables are unchanged. From 4e043e97de0d5e5fa662ec25aee81586e34b2a37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 2 Oct 2026 08:25:47 +0200 Subject: [PATCH 03/17] Refresh Linux and Windows arguments mapping leaf tables from CI artifacts --- changelog.d/11751-arguments-leaf-tables.md | 1 + changelog.d/11751-macos-arguments-leaf-table.md | 1 - crates/perry-codegen/src/gc_effects/linux-x86_64.tsv | 2 +- crates/perry-codegen/src/gc_effects/windows-x86_64.tsv | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 changelog.d/11751-arguments-leaf-tables.md delete mode 100644 changelog.d/11751-macos-arguments-leaf-table.md diff --git a/changelog.d/11751-arguments-leaf-tables.md b/changelog.d/11751-arguments-leaf-tables.md new file mode 100644 index 0000000000..51bd99ecf6 --- /dev/null +++ b/changelog.d/11751-arguments-leaf-tables.md @@ -0,0 +1 @@ +Refresh the macOS, Linux and Windows GC call-effect tables from their actual release-archive classifier artifacts. The preallocated mapped-arguments resolved slot store in `js_arguments_object_map_index` is a leaf on each target. The obsolete conservative `Reenters` row caused each classifier gate to fail with one safe drift. Runtime behavior is unchanged. diff --git a/changelog.d/11751-macos-arguments-leaf-table.md b/changelog.d/11751-macos-arguments-leaf-table.md deleted file mode 100644 index de7b32869c..0000000000 --- a/changelog.d/11751-macos-arguments-leaf-table.md +++ /dev/null @@ -1 +0,0 @@ -Refresh the macOS GC call-effect classification of `js_arguments_object_map_index` from the actual release-archive classifier artifact. The preallocated mapped-arguments slot store is a leaf; retaining the conservative `Reenters` row caused the macOS classifier gate to fail with one safe drift. Runtime behavior and other-target tables are unchanged. diff --git a/crates/perry-codegen/src/gc_effects/linux-x86_64.tsv b/crates/perry-codegen/src/gc_effects/linux-x86_64.tsv index 2e425eb2aa..26f406ff1d 100644 --- a/crates/perry-codegen/src/gc_effects/linux-x86_64.tsv +++ b/crates/perry-codegen/src/gc_effects/linux-x86_64.tsv @@ -49,7 +49,7 @@ js_arguments_bundle_get_slow Reenters js_arguments_bundle_index_get Reenters js_arguments_object_alloc Reenters js_arguments_object_alloc_mapped Reenters -js_arguments_object_map_index Reenters +js_arguments_object_map_index Leaf js_array_alloc Reenters js_array_alloc_literal Reenters js_array_alloc_with_length Reenters diff --git a/crates/perry-codegen/src/gc_effects/windows-x86_64.tsv b/crates/perry-codegen/src/gc_effects/windows-x86_64.tsv index 1d310a3f5b..68f1d23a96 100644 --- a/crates/perry-codegen/src/gc_effects/windows-x86_64.tsv +++ b/crates/perry-codegen/src/gc_effects/windows-x86_64.tsv @@ -49,7 +49,7 @@ js_arguments_bundle_get_slow Reenters js_arguments_bundle_index_get Reenters js_arguments_object_alloc Reenters js_arguments_object_alloc_mapped Reenters -js_arguments_object_map_index Reenters +js_arguments_object_map_index Leaf js_array_alloc Reenters js_array_alloc_literal Reenters js_array_alloc_with_length Reenters From 1be2965dbe9c81554cb6616cc510a245888e3041 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 2 Oct 2026 11:11:40 +0200 Subject: [PATCH 04/17] ci(gc): exercise Windows try roots under RS4GC --- .github/workflows/gc-native-roots.yml | 59 ++++++++++----------------- 1 file changed, 22 insertions(+), 37 deletions(-) diff --git a/.github/workflows/gc-native-roots.yml b/.github/workflows/gc-native-roots.yml index 5532fc6705..e070db053d 100644 --- a/.github/workflows/gc-native-roots.yml +++ b/.github/workflows/gc-native-roots.yml @@ -419,25 +419,10 @@ jobs: for probe in benchmarks/gc_ratchet/probes/*.ts; do total=$((total+1)) name=$(basename "$probe" .ts) - if [ "$RUNNER_OS" = "Windows" ] && [ "$name" = "09_try_catch_roots" ]; then - # #7354 measured negative, pinned as a REFUSAL: windows-msvc - # `try` lowers to WinEH funclet pads, which crash LLVM's - # rewrite-statepoints-for-gc outright (access violation on opt - # 22.1.3, reproducible from an eight-line module). Perry refuses - # the module before the pass runs; this arm pins that it STAYS a - # refusal — never a crash, never a silently rootless binary. It - # goes red the day the pass learns funclet EH, which is the - # prompt to fold 09 into this matrix. - if PERRY_RS4GC=1 ./target/perry-dev/perry "$probe" \ - -o "/tmp/rs4gc-$name" > "/tmp/rs4gc-$name.compile.log" 2>&1; then - echo "::error::$name compiled under RS4GC on Windows — the funclet refusal is gone: either rewrite-statepoints-for-gc learned funclet EH (fold 09 into the matrix) or the refusal was lost" - exit 1 - fi - grep -q "funclet" "/tmp/rs4gc-$name.compile.log" \ - || { echo "::error::$name failed for a reason other than the funclet refusal:"; cat "/tmp/rs4gc-$name.compile.log"; exit 1; } - pass=$((pass+1)) - continue - fi + # #10385 replaced Windows funclets with Perry's landing-pad + # personality. Probe 09 must now execute with precise roots on + # Windows too. Actual funclet IR remains refused by linker.rs's + # rs4gc_funclet_refusal and its unit test (#7354). node --expose-gc --experimental-strip-types "$probe" > "/tmp/rs4gc-$name.oracle" PERRY_RS4GC=1 ./target/perry-dev/perry "$probe" -o "/tmp/rs4gc-$name" # perry appends the platform default extension to an -o with none. @@ -461,11 +446,17 @@ jobs: readelf -S "$out" | grep -q "\.llvm_stackmaps" \ && { echo "::error::$name still carries .llvm_stackmaps — the compact rewrite did not run"; exit 1; } fi + PERRY_GC_DIAG=1 \ PERRY_RS4GC=1 PERRY_GC_FORCE_EVACUATE=1 PERRY_GC_VERIFY_EVACUATION=1 \ PERRY_GC_HEAP_LIMIT=8 PERRY_GC_INCREMENTAL=0 PERRY_CONSERVATIVE_STACK_SCAN=off \ "$out" > "/tmp/rs4gc-$name.out" 2> "/tmp/rs4gc-$name.err" diff "/tmp/rs4gc-$name.oracle" "/tmp/rs4gc-$name.out" \ || { echo "::error::$name diverged from the pinned oracle under RS4GC"; exit 1; } + if [ "$name" = "09_try_catch_roots" ]; then + py=python3; command -v python3 >/dev/null 2>&1 || py=python + "$py" scripts/gc_evacuation_liveness_assert.py "/tmp/rs4gc-$name.err" \ + --probe "$name ($RUNNER_OS RS4GC)" + fi errs="$errs /tmp/rs4gc-$name.err" pass=$((pass+1)) done @@ -485,14 +476,9 @@ jobs: # windows-latest exposes the toolcache python as `python`, not python3. py=python3; command -v python3 >/dev/null 2>&1 || py=python - # The PORTABLE assertion, on every arm. `11_collect_at_depth` is - # deliberate: it contains no `try`, so it compiles under RS4GC - # everywhere. `09_try_catch_roots` does NOT — RS4GC cannot rewrite - # WinEH funclet pads, so `linker.rs`'s `rs4gc_funclet_refusal` rejects - # it on windows-msvc, and the probe loop above only tolerates that - # because it greps the compile log for "funclet". A report assertion - # pinned to a probe that cannot compile on one arm is a gate that - # fails for a reason unrelated to its subject. + # The recursive-depth assertion, on every arm. This probe carries + # live roots across a deep stack; the separate try probe below + # covers roots across normal and unwinding exception edges. # # --only-backend proves the lowering ran on every function; the two # --require-positive checks prove it PRODUCED something. Those counts @@ -510,19 +496,18 @@ jobs: --require-positive records \ --require-positive roots - # The try-specific arm, everywhere RS4GC can compile a `try`. This is + # The try-specific arm on every target, including Windows since + # #10385 replaced funclets with Perry's landing-pad personality. This is # the coverage the probe above cannot give: 128 of 479 gap tests # contain `try {}`, and RS4GC being the only backend that handles them # is the reason the bridge could be deleted (#7339, #7348). - if [ "$RUNNER_OS" != "Windows" ]; then - PERRY_RS4GC=1 ./target/perry-dev/perry \ - benchmarks/gc_ratchet/probes/09_try_catch_roots.ts \ - -o /tmp/rs4gc-try-probe --statepoint-report=json 2> /tmp/rs4gc-try.json - "$py" scripts/statepoint_report_assert.py /tmp/rs4gc-try.json \ - --only-backend rs4gc \ - --require-positive records \ - --require-positive roots - fi + PERRY_RS4GC=1 ./target/perry-dev/perry \ + benchmarks/gc_ratchet/probes/09_try_catch_roots.ts \ + -o /tmp/rs4gc-try-probe --statepoint-report=json 2> /tmp/rs4gc-try.json + "$py" scripts/statepoint_report_assert.py /tmp/rs4gc-try.json \ + --only-backend rs4gc \ + --require-positive records \ + --require-positive roots # Walker liveness, on EVERY arm. A walker that visits zero frames # still lets most probes print the right answer, because other root From 443f9d0b58d5428a7ea67ddf14ba7d6e64f52089 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 2 Oct 2026 11:12:38 +0200 Subject: [PATCH 05/17] changelog: name the Windows try-root probe repair for PR 11755 --- changelog.d/11755-windows-try-root-probe.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelog.d/11755-windows-try-root-probe.md diff --git a/changelog.d/11755-windows-try-root-probe.md b/changelog.d/11755-windows-try-root-probe.md new file mode 100644 index 0000000000..b75af4dccc --- /dev/null +++ b/changelog.d/11755-windows-try-root-probe.md @@ -0,0 +1,4 @@ +Exercise the Windows landing-pad try/catch probe under RS4GC and forced +evacuation. Require positive copying diagnostics and a measured compact-root +report instead of expecting the supported probe to fail compilation. The +compiler refusal and regression coverage for genuine WinEH funclets remain. From 05090dc701d33dd4dd0e7d60bdf6c7f72b291f91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 2 Oct 2026 11:29:21 +0200 Subject: [PATCH 06/17] ci: halve full gap slices after twelve-way timeouts --- .github/workflows/test.yml | 4 ++-- docs/src/testing/ci-tiers.md | 8 ++++---- scripts/ci_plan.py | 13 +++++++------ 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 768273ca66..6ed23a2f54 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3072,7 +3072,7 @@ jobs: # protection requires. 2026-08-16: in the harness's default (auto-optimize) # mode 96% of a shard's wall time was ~10 tests at ~200 s each -- the # feature-stripped runtime rebuild per distinct feature set, redone in - # every shard. That mode is now the full tier's 12-shard arm; PR and sweep + # every shard. That mode is now the full tier's 24-shard arm; PR and sweep # tiers use `fast` mode against one prebuilt release build (~1.5 s/test). strategy: fail-fast: false @@ -3134,7 +3134,7 @@ jobs: # 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, and never + # so it stays in the nightly/release tier at 24 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. diff --git a/docs/src/testing/ci-tiers.md b/docs/src/testing/ci-tiers.md index 8a84c16a28..1410ab4ef6 100644 --- a/docs/src/testing/ci-tiers.md +++ b/docs/src/testing/ci-tiers.md @@ -14,20 +14,20 @@ python3 scripts/ci_plan.py --self-test # the policy's own invariants |---|---|---|---| | **pr** | every `pull_request` push | the required gate. Small, fast, must be green on `main`. | `pr-gate` — **the only required status context** | | **sweep** | every `push` to `main` (coalesced) **+ a two-hourly cron backstop** | post-merge truth for `main`: the PR tier unscoped plus the medium-weight jobs that do not fit the PR budget | `main-gate` | -| **full** | nightly `schedule`, `v*` tags, `workflow_dispatch`, PRs labelled `run-extended-tests` | everything, incl. parity, compile-smoke, doc-tests, package smokes, the 12-shard auto-optimize gap suite | `full-suite-gate` — what `release-packages.yml` waits for | +| **full** | nightly `schedule`, `v*` tags, `workflow_dispatch`, PRs labelled `run-extended-tests` | everything, incl. parity, compile-smoke, doc-tests, package smokes, the 24-shard auto-optimize gap suite | `full-suite-gate` — what `release-packages.yml` waits for | ## The job × tier matrix Generated by `python3 scripts/ci_plan.py --table`; the `lint` job checks that this copy is current. -The full gap tier uses twelve shards with auto-optimize enabled. Compile-smoke +The full gap tier uses twenty-four shards with auto-optimize enabled. Compile-smoke partitions the complete top-level `test-files/*.ts` inventory into four stable round-robin shards, with at most two smoke shards running concurrently. Each file is assigned once before the existing platform exclusions are applied. Every shard retains the default compiler invocation and failure markers; `full-suite-gate` requires the matrix job's aggregate result. These splits -address current eight-way gap jobs reaching their 110-minute bound and the +address twelve-way gap jobs reaching their 110-minute bound and the unsharded smoke job reaching the hosted six-hour limit. Actual CI durations must still establish the new margin. @@ -38,7 +38,7 @@ must still establish the new margin. | `warnings` | yes | yes | yes | | `cargo-test` | yes | yes | yes | | `cargo-test-perry` | | | yes | -| `gap-suite` | 6x fast | 3x fast | 12x full | +| `gap-suite` | 6x fast | 3x fast | 24x full | | `gc-call-effects` | yes | yes | yes | | `gc-stress` | 1x pr | 4x all | 4x all | | `e2e-scoped` | yes | | | diff --git a/scripts/ci_plan.py b/scripts/ci_plan.py index b93a14ea75..c5904780db 100755 --- a/scripts/ci_plan.py +++ b/scripts/ci_plan.py @@ -31,7 +31,7 @@ `await-tests` dispatches this and waits for the `full-suite-gate` job), and PRs carrying the `run-extended-tests` label. The sweep plus the slow/opt-in suites (parity, compile-smoke, doc-tests, package smokes, - the gap suite in its 12-shard auto-optimize mode). + the gap suite in its 24-shard auto-optimize mode). PR SCOPE -------- @@ -123,10 +123,11 @@ GAP_SUITE = { "pr": {"mode": "fast", "total": 6}, "sweep": {"mode": "fast", "total": 3}, - # Current 8-way full runs hit the 110-minute bound while still compiling - # (e.g. run36914319295, shard1 reached137/152). Preserve auto-optimize - # coverage and distribute the complete corpus across more workers. - "full": {"mode": "full", "total": 12}, + # Twelve-way run 36967233926 still hit the 110-minute job bound: + # shard 3 completed only 79/102 fixtures and shard 4 only 82/102. + # Doubling the modulo partition halves each existing slice without + # dropping fixtures or changing auto-optimize and snapshot acceptance. + "full": {"mode": "full", "total": 24}, } # Parity: full tier only, sharded. The unsharded job was killed by GitHub's @@ -463,7 +464,7 @@ def check(name: str, cond: bool): "shards": list(range(1, PERRY_INTEGRATION_SHARDS + 1)), }, ) - check("full: 12 auto-optimize gap shards", full["gap"] == {"mode": "full", "total": 12, "shards": list(range(1, 13)), "update_snapshot": False}) + check("full: 24 auto-optimize gap shards", full["gap"] == {"mode": "full", "total": 24, "shards": list(range(1, 25)), "update_snapshot": False}) check("full: parity sharded (6h-cap kill, 2026-08-16)", full["parity"]["total"] >= 2 and full["parity"]["shards"][0] == 1) check("full: full GC matrix has four shards", full["gc_stress"] == {"mode": "all", "total": 4, "shards": [1, 2, 3, 4]}) From 4c361d249494a51035e5a17e5c51f8b84249269f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 2 Oct 2026 11:30:24 +0200 Subject: [PATCH 07/17] changelog: name the full-gap budget repair for PR 11756 --- changelog.d/11756-full-gap-budget-headroom.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelog.d/11756-full-gap-budget-headroom.md diff --git a/changelog.d/11756-full-gap-budget-headroom.md b/changelog.d/11756-full-gap-budget-headroom.md new file mode 100644 index 0000000000..36e005bb13 --- /dev/null +++ b/changelog.d/11756-full-gap-budget-headroom.md @@ -0,0 +1,4 @@ +Split the full auto-optimize gap suite into twenty-four shards after nine +of twelve workers hit the existing 110-minute limit. Each previous slice is +partitioned into two without dropping fixtures or changing compilation, +snapshots, acceptance thresholds, fast-mode allocations or smoke workers. From 70d6d803badd1f9e1c5fa5e3d7ecb7e3e8da2aea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 2 Oct 2026 12:38:32 +0200 Subject: [PATCH 08/17] fix(ci): link macOS stdlib provider frameworks --- tests/fixtures/issue_8075_provider_gc/stdlib-linker.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/fixtures/issue_8075_provider_gc/stdlib-linker.sh b/tests/fixtures/issue_8075_provider_gc/stdlib-linker.sh index 277b2b6544..720c5a615d 100755 --- a/tests/fixtures/issue_8075_provider_gc/stdlib-linker.sh +++ b/tests/fixtures/issue_8075_provider_gc/stdlib-linker.sh @@ -78,6 +78,10 @@ if [[ -n "$original_export_list" ]]; then fi if [[ "$saw_runtime_rlib" == true && "$host_os" == Darwin ]]; then + # The stdlib provider also links the runtime rlib, whose locale helpers + # call CoreFoundation and Objective-C. The runtime dylib's link flags do + # not propagate to this separate image. + arguments+=('-framework' 'CoreFoundation' '-framework' 'Foundation') arguments+=('-Wl,-rpath,@loader_path' '-Wl,-flat_namespace' '-Wl,-interposable') elif [[ "$saw_runtime_rlib" == true ]]; then # shellcheck disable=SC2016 # $ORIGIN must reach the ELF linker literally. From 390936c50084964bf1dc916082a20d071629f1cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 2 Oct 2026 12:38:59 +0200 Subject: [PATCH 09/17] changelog: record macOS provider framework link repair --- changelog.d/11762-macos-provider-frameworks.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/11762-macos-provider-frameworks.md diff --git a/changelog.d/11762-macos-provider-frameworks.md b/changelog.d/11762-macos-provider-frameworks.md new file mode 100644 index 0000000000..530b977c85 --- /dev/null +++ b/changelog.d/11762-macos-provider-frameworks.md @@ -0,0 +1 @@ +Link CoreFoundation and Foundation when building the macOS stdlib provider fixture with the runtime rlib, matching the existing runtime dylib build. This lets locale helpers resolve their framework and Objective-C dependencies before the provider GC gate executes. Linux and links without a runtime rlib retain their existing flags. From 5a779d7c477f7e70318475f2fe0c8b1c36b9b6c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 2 Oct 2026 14:05:29 +0200 Subject: [PATCH 10/17] fix(ci): export provider feature installer bootstrap symbols --- changelog.d/11762-macos-provider-frameworks.md | 2 ++ tests/fixtures/issue_8075_provider_gc/stdlib-linker.sh | 2 ++ .../issue_8075_provider_gc/stdlib-provider/src/lib.rs | 9 +++++++++ 3 files changed, 13 insertions(+) diff --git a/changelog.d/11762-macos-provider-frameworks.md b/changelog.d/11762-macos-provider-frameworks.md index 530b977c85..d23f87a03a 100644 --- a/changelog.d/11762-macos-provider-frameworks.md +++ b/changelog.d/11762-macos-provider-frameworks.md @@ -1 +1,3 @@ Link CoreFoundation and Foundation when building the macOS stdlib provider fixture with the runtime rlib, matching the existing runtime dylib build. This lets locale helpers resolve their framework and Objective-C dependencies before the provider GC gate executes. Linux and links without a runtime rlib retain their existing flags. + +Retain and export the stdlib feature-installation and registration entry points used by later-loaded apps. The macOS provider GC phase previously passed, but the following Response image failed to load because the compiled-feature installer was absent from the provider export list. diff --git a/tests/fixtures/issue_8075_provider_gc/stdlib-linker.sh b/tests/fixtures/issue_8075_provider_gc/stdlib-linker.sh index 720c5a615d..66646ea58a 100755 --- a/tests/fixtures/issue_8075_provider_gc/stdlib-linker.sh +++ b/tests/fixtures/issue_8075_provider_gc/stdlib-linker.sh @@ -26,6 +26,8 @@ stdlib_provider_exports=( js_response_get_headers js_response_new js_stdlib_init_dispatch + js_stdlib_install_compiled + js_stdlib_register_feature_installer js_stream_unwrap_handle ) diff --git a/tests/fixtures/issue_8075_provider_gc/stdlib-provider/src/lib.rs b/tests/fixtures/issue_8075_provider_gc/stdlib-provider/src/lib.rs index 3419235ba2..f6139de9f0 100644 --- a/tests/fixtures/issue_8075_provider_gc/stdlib-provider/src/lib.rs +++ b/tests/fixtures/issue_8075_provider_gc/stdlib-provider/src/lib.rs @@ -12,6 +12,15 @@ unsafe extern "C" { #[used] static PIN_STDLIB: extern "C" fn() -> i32 = perry_stdlib::common::js_stdlib_process_pending; +// Later-loaded apps register their generated feature installer and call the +// compiled-feature entry point. Keep both available in the provider image. +#[used] +static PIN_STDLIB_INSTALL_COMPILED: extern "C" fn() = + perry_stdlib::common::feature_hooks::js_stdlib_install_compiled; +#[used] +static PIN_STDLIB_REGISTER_FEATURE_INSTALLER: extern "C" fn(extern "C" fn()) = + perry_stdlib::common::feature_hooks::js_stdlib_register_feature_installer; + // A cdylib only retains Rust dependency code reachable from this wrapper. // Keep the exact Web Fetch/Streams surface used by #8038's later-loaded app; // otherwise the app links with dynamic lookups but dlopen fails on the first From 087b532f03f33c99c27731d384b5265cec3f6169 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 2 Oct 2026 14:39:33 +0200 Subject: [PATCH 11/17] fix(ci): route immutable thread-global codegen tests --- scripts/ci_e2e_scope.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/ci_e2e_scope.py b/scripts/ci_e2e_scope.py index 325ca5fda4..ad5aef5c4e 100755 --- a/scripts/ci_e2e_scope.py +++ b/scripts/ci_e2e_scope.py @@ -154,6 +154,7 @@ "static_symbol_hygiene", "string_array_length_9160", "temp_root_operand_temporaries", + "thread_immutable_globals", "typed_array_rmw_8692", "typed_array_update_lowering", "typed_shape_descriptor", From 684dd65ac72d05d297049e2607a5dabaa6218af5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 2 Oct 2026 14:39:56 +0200 Subject: [PATCH 12/17] docs: add thread-global test routing changeset --- changelog.d/11767-thread-global-e2e-routing.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/11767-thread-global-e2e-routing.md diff --git a/changelog.d/11767-thread-global-e2e-routing.md b/changelog.d/11767-thread-global-e2e-routing.md new file mode 100644 index 0000000000..4e466f1b66 --- /dev/null +++ b/changelog.d/11767-thread-global-e2e-routing.md @@ -0,0 +1 @@ +Register the immutable thread-global codegen IR suite in the CI source-to-suite map. This removes the unclassified-suite planner failure introduced when the suite landed and runs its three tests on codegen source changes, preserving mapped-suite timeouts and coverage independent of the named-suite cap. From 1e256b8112ce08173e79bb1957e6845e6628ec4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 2 Oct 2026 14:52:49 +0200 Subject: [PATCH 13/17] fix(ci): split fast PR gap slices after job timeout --- .github/workflows/test.yml | 2 +- changelog.d/11756-full-gap-budget-headroom.md | 7 ++++--- docs/src/testing/ci-tiers.md | 7 +++++-- scripts/ci_plan.py | 7 +++++-- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6ed23a2f54..e150907555 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2991,7 +2991,7 @@ jobs: # --------------------------------------------------------------------------- # gap-suite (was `conformance-smoke`): the gap suite, sharded. Runs in every - # tier -- 6 fast-mode shards on a PR, 3 in the sweep, 12 auto-optimize shards + # tier -- 12 fast-mode shards on a PR, 3 in the sweep, 24 auto-optimize shards # 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. # --------------------------------------------------------------------------- diff --git a/changelog.d/11756-full-gap-budget-headroom.md b/changelog.d/11756-full-gap-budget-headroom.md index 36e005bb13..099a6959ec 100644 --- a/changelog.d/11756-full-gap-budget-headroom.md +++ b/changelog.d/11756-full-gap-budget-headroom.md @@ -1,4 +1,5 @@ Split the full auto-optimize gap suite into twenty-four shards after nine -of twelve workers hit the existing 110-minute limit. Each previous slice is -partitioned into two without dropping fixtures or changing compilation, -snapshots, acceptance thresholds, fast-mode allocations or smoke workers. +of twelve workers hit the existing 110-minute limit. Split the fast PR +arm into twelve shards after its six-way shard 1 also exhausted that bound. +Each previous slice is partitioned into two without dropping fixtures or +changing compilation, snapshots, acceptance thresholds or smoke workers. diff --git a/docs/src/testing/ci-tiers.md b/docs/src/testing/ci-tiers.md index 1410ab4ef6..ccace8bfe9 100644 --- a/docs/src/testing/ci-tiers.md +++ b/docs/src/testing/ci-tiers.md @@ -21,7 +21,10 @@ python3 scripts/ci_plan.py --self-test # the policy's own invariants Generated by `python3 scripts/ci_plan.py --table`; the `lint` job checks that this copy is current. -The full gap tier uses twenty-four shards with auto-optimize enabled. Compile-smoke +The PR gap tier uses twelve fast-mode shards after a six-way shard exhausted +the 110-minute job bound. The full gap tier uses twenty-four shards with +auto-optimize enabled. Both retain every fixture and the existing snapshot +acceptance gate; CI timings must establish the new margin. Compile-smoke partitions the complete top-level `test-files/*.ts` inventory into four stable round-robin shards, with at most two smoke shards running concurrently. Each file is assigned once before the existing platform exclusions are applied. @@ -38,7 +41,7 @@ must still establish the new margin. | `warnings` | yes | yes | yes | | `cargo-test` | yes | yes | yes | | `cargo-test-perry` | | | yes | -| `gap-suite` | 6x fast | 3x fast | 24x full | +| `gap-suite` | 12x fast | 3x fast | 24x full | | `gc-call-effects` | yes | yes | yes | | `gc-stress` | 1x pr | 4x all | 4x all | | `e2e-scoped` | yes | | | diff --git a/scripts/ci_plan.py b/scripts/ci_plan.py index c5904780db..f3c3e7b09c 100755 --- a/scripts/ci_plan.py +++ b/scripts/ci_plan.py @@ -121,7 +121,10 @@ # it is ~28 min, level with gc-stress, for ~170 job-minutes -- against 480 for # the old 8 x auto-optimize shards. GAP_SUITE = { - "pr": {"mode": "fast", "total": 6}, + # Fast PR shard 1 in run36981459549 exhausted its 110-minute bound. + # Twelve modulo slices halve each former six-way slice; the worker and + # no-new-untriaged snapshot gate retain the same complete corpus. + "pr": {"mode": "fast", "total": 12}, "sweep": {"mode": "fast", "total": 3}, # Twelve-way run 36967233926 still hit the 110-minute job bound: # shard 3 completed only 79/102 fixtures and shard 4 only 82/102. @@ -421,7 +424,7 @@ def check(name: str, cond: bool): check("core PR: windows off", not core["jobs"]["windows_build"]) check("core PR: parity off", not core["jobs"]["parity"]) check("core PR: security-audit off (no deps change)", not core["jobs"]["security_audit"]) - check("core PR: 6 fast gap shards", core["gap"] == {"mode": "fast", "total": 6, "shards": [1, 2, 3, 4, 5, 6], "update_snapshot": False}) + check("core PR: 12 fast gap shards", core["gap"] == {"mode": "fast", "total": 12, "shards": list(range(1, 13)), "update_snapshot": False}) check("core PR: cargo-test scoped", core["cargo_test_scope"] == "pr") deps = plan("pull_request", "refs/pull/1/merge", changed=["Cargo.lock"]) From 61f29f134e3ed6ae4638f4e88f74b016bb3bf476 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 2 Oct 2026 16:43:25 +0200 Subject: [PATCH 14/17] fix(ci): route immutable-global tests after classifier refresh --- changelog.d/11751-arguments-leaf-tables.md | 2 ++ scripts/ci_e2e_scope.py | 1 + 2 files changed, 3 insertions(+) diff --git a/changelog.d/11751-arguments-leaf-tables.md b/changelog.d/11751-arguments-leaf-tables.md index 51bd99ecf6..944a6a8847 100644 --- a/changelog.d/11751-arguments-leaf-tables.md +++ b/changelog.d/11751-arguments-leaf-tables.md @@ -1 +1,3 @@ Refresh the macOS, Linux and Windows GC call-effect tables from their actual release-archive classifier artifacts. The preallocated mapped-arguments resolved slot store in `js_arguments_object_map_index` is a leaf on each target. The obsolete conservative `Reenters` row caused each classifier gate to fail with one safe drift. Runtime behavior is unchanged. + +Refresh against current main while preserving its immutable-global helper classifications, and include the landed `thread_immutable_globals` integration suite in the per-PR E2E map so CI can plan the refreshed tree. diff --git a/scripts/ci_e2e_scope.py b/scripts/ci_e2e_scope.py index 325ca5fda4..ad5aef5c4e 100755 --- a/scripts/ci_e2e_scope.py +++ b/scripts/ci_e2e_scope.py @@ -154,6 +154,7 @@ "static_symbol_hygiene", "string_array_length_9160", "temp_root_operand_temporaries", + "thread_immutable_globals", "typed_array_rmw_8692", "typed_array_update_lowering", "typed_shape_descriptor", From 0ea5f9afbef7e5d464cbda8cf2cb92da3d387c21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 2 Oct 2026 17:07:50 +0200 Subject: [PATCH 15/17] fix(ci): route immutable-global tests for provider validation --- changelog.d/11762-macos-provider-frameworks.md | 2 ++ scripts/ci_e2e_scope.py | 1 + 2 files changed, 3 insertions(+) diff --git a/changelog.d/11762-macos-provider-frameworks.md b/changelog.d/11762-macos-provider-frameworks.md index d23f87a03a..69e4f9e0a2 100644 --- a/changelog.d/11762-macos-provider-frameworks.md +++ b/changelog.d/11762-macos-provider-frameworks.md @@ -1,3 +1,5 @@ Link CoreFoundation and Foundation when building the macOS stdlib provider fixture with the runtime rlib, matching the existing runtime dylib build. This lets locale helpers resolve their framework and Objective-C dependencies before the provider GC gate executes. Linux and links without a runtime rlib retain their existing flags. Retain and export the stdlib feature-installation and registration entry points used by later-loaded apps. The macOS provider GC phase previously passed, but the following Response image failed to load because the compiled-feature installer was absent from the provider export list. + +Integrate current main and register its immutable thread-global integration suite in scoped CI, fixing the unclassified-suite planning failure before provider validation. diff --git a/scripts/ci_e2e_scope.py b/scripts/ci_e2e_scope.py index 325ca5fda4..ad5aef5c4e 100755 --- a/scripts/ci_e2e_scope.py +++ b/scripts/ci_e2e_scope.py @@ -154,6 +154,7 @@ "static_symbol_hygiene", "string_array_length_9160", "temp_root_operand_temporaries", + "thread_immutable_globals", "typed_array_rmw_8692", "typed_array_update_lowering", "typed_shape_descriptor", From d863a283192194292b111f88c00fd91899345192 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 2 Oct 2026 18:29:20 +0200 Subject: [PATCH 16/17] fix(gc-effects): refresh Windows immutable-global helper classes --- changelog.d/11756-windows-thread-global-effects.md | 8 ++++++++ crates/perry-codegen/src/gc_effects/windows-x86_64.tsv | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 changelog.d/11756-windows-thread-global-effects.md diff --git a/changelog.d/11756-windows-thread-global-effects.md b/changelog.d/11756-windows-thread-global-effects.md new file mode 100644 index 0000000000..936f3fda24 --- /dev/null +++ b/changelog.d/11756-windows-thread-global-effects.md @@ -0,0 +1,8 @@ +### Fixed + +- Refresh the Windows classifications of `js_thread_global_materialize` and + `js_thread_global_publish` from the generated table produced by the documented + Windows archive classifier at the reviewed CI repair head. Materialization is + `AllocOnly` and publication is `Leaf` on this target. Preserve every other + table entry and all classifier rules and seeds, removing two conservative + drifts that fail the strict full-tier check. diff --git a/crates/perry-codegen/src/gc_effects/windows-x86_64.tsv b/crates/perry-codegen/src/gc_effects/windows-x86_64.tsv index 75f4c8fe97..719dfd0a7f 100644 --- a/crates/perry-codegen/src/gc_effects/windows-x86_64.tsv +++ b/crates/perry-codegen/src/gc_effects/windows-x86_64.tsv @@ -3229,8 +3229,8 @@ js_text_encoder_new Reenters js_text_encoder_stream_new Reenters js_text_encoding_stream_new Reenters js_this_coerce_sloppy Reenters -js_thread_global_materialize Reenters -js_thread_global_publish Reenters +js_thread_global_materialize AllocOnly +js_thread_global_publish Leaf js_thread_has_pending Leaf js_thread_parallel_filter Reenters js_thread_parallel_map Reenters From 6e6a66b167dd32595b309aa7e7af501fe57bb7c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 2 Oct 2026 18:47:29 +0200 Subject: [PATCH 17/17] fix(ci): apply witness GC settings during compilation and execution --- .github/workflows/gc-moving-witnesses.yml | 6 +- changelog.d/11756-gc-witness-fixture-env.md | 9 ++ scripts/gc_matrix_fixture_env.py | 124 ++++++++++++++++ scripts/gc_matrix_fixture_env_test.py | 133 ++++++++++++++++++ scripts/gc_repsel_matrix.sh | 89 ++++++++++-- ..._11590_packed_loop_global_cache_rooting.ts | 1 + .../test_gap_gc_call_argument_rooting.ts | 1 + .../test_gap_gc_container_value_rooting.ts | 1 + 8 files changed, 353 insertions(+), 11 deletions(-) create mode 100644 changelog.d/11756-gc-witness-fixture-env.md create mode 100644 scripts/gc_matrix_fixture_env.py create mode 100644 scripts/gc_matrix_fixture_env_test.py diff --git a/.github/workflows/gc-moving-witnesses.yml b/.github/workflows/gc-moving-witnesses.yml index dd7f2899b8..9eb0967d9e 100644 --- a/.github/workflows/gc-moving-witnesses.yml +++ b/.github/workflows/gc-moving-witnesses.yml @@ -214,7 +214,7 @@ jobs: # The filter exists only to spare docs-only PRs a compiler build. If # the listing is empty or the API failed, `set -e` already aborted, so # the job cannot silently fall through to "not relevant". - if grep -qE '^(crates/|scripts/gc_repsel_matrix\.sh$|test-files/test_gap_gc_|test-parity/gc_repsel_|Cargo\.(toml|lock)$|\.node-version$|\.github/workflows/gc-moving-witnesses\.yml$)' changed.txt; then + if grep -qE '^(crates/|scripts/gc_repsel_matrix\.sh$|scripts/gc_matrix_fixture_env(_test)?\.py$|test-files/test_gap_gc_|test-parity/gc_repsel_|Cargo\.(toml|lock)$|\.node-version$|\.github/workflows/gc-moving-witnesses\.yml$)' changed.txt; then echo "run=true" >> "$GITHUB_OUTPUT" echo "Change touches collector-relevant paths; running the witnesses." else @@ -253,6 +253,10 @@ jobs: # drop out of the gate silently. node-version-file: .node-version + - name: Check witness settings reach compilation and execution + if: steps.relevance.outputs.run == 'true' + run: bash scripts/gc_repsel_matrix.sh --self-test-fixture-env + - name: Build perry and the runtime archives if: steps.relevance.outputs.run == 'true' env: diff --git a/changelog.d/11756-gc-witness-fixture-env.md b/changelog.d/11756-gc-witness-fixture-env.md new file mode 100644 index 0000000000..e4ed6beaf3 --- /dev/null +++ b/changelog.d/11756-gc-witness-fixture-env.md @@ -0,0 +1,9 @@ +### Fixed + +- Apply declared GC witness settings during both compilation and execution of + the moving loop-poll arm, so seeded and protected witnesses select the matching + instrumented runtime. Validate the settings as literal GC assignments, keep + the compiled group separate from the safepoint control, and record the actual + compile and run settings. Exercise routing failure controls in the witness + workflow. Preserve the other arms, witness workloads, triage and movement + requirements. diff --git a/scripts/gc_matrix_fixture_env.py b/scripts/gc_matrix_fixture_env.py new file mode 100644 index 0000000000..d6d8114223 --- /dev/null +++ b/scripts/gc_matrix_fixture_env.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +"""Read the loop_polls fixture's GC parity-env without interpreting shell code. + +The parity harness uses whitespace-separated KEY=VALUE assignments. Restrict +the matrix extension to GC witness settings: metadata cannot change an OFF +arm, compiler selection, library providers, pressure, or the shipped control. +""" + +from __future__ import annotations + +import argparse +from decimal import Decimal +from pathlib import Path +import re +import sys + + +DIRECTIVE = re.compile(r"^\s*//\s*parity-env:\s*(.*)$", re.MULTILINE) +ON_KEYS = { + "PERRY_GC_MOVING_LOOP_POLLS", + "PERRY_GC_FORCE_EVACUATE", + "PERRY_GC_VERIFY_EVACUATION", +} +UINT_KEYS = { + "PERRY_GC_SCHEDULE_SEED", + "PERRY_GC_SCHEDULE_ALLOC_KB", + "PERRY_GC_PROTECT_FROMSPACE_DEPTH", +} + + +def parse(text: str) -> str: + lines = DIRECTIVE.findall(text) + if not lines: + return "" + if len(lines) != 1 or not lines[0].strip(): + raise ValueError("require exactly one nonempty parity-env directive") + values: dict[str, str] = {} + for assignment in lines[0].split(): + key, sep, value = assignment.partition("=") + if not sep or key in values: + raise ValueError(f"invalid or duplicate assignment {assignment!r}") + if key in ON_KEYS: + valid = value == "1" + elif key in UINT_KEYS: + valid = bool(re.fullmatch(r"[0-9]{1,20}", value)) and int(value) <= 2**64 - 1 + if key == "PERRY_GC_PROTECT_FROMSPACE_DEPTH": + valid = valid and int(value) > 0 + elif key == "PERRY_GC_SCHEDULE_RATE": + valid = bool(re.fullmatch(r"(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)", value)) + valid = valid and Decimal(0) <= Decimal(value) <= Decimal(1) + elif key == "PERRY_GC_PROTECT_FROMSPACE": + valid = value in ("1", "poison") + else: + raise ValueError(f"unsupported GC witness setting {key!r}") + if not valid: + raise ValueError(f"invalid GC witness setting {assignment!r}") + values[key] = value + if any(key in values for key in ("PERRY_GC_SCHEDULE_RATE", "PERRY_GC_SCHEDULE_ALLOC_KB")): + if "PERRY_GC_SCHEDULE_SEED" not in values: + raise ValueError("schedule rate/allocation gating requires a seed") + if "PERRY_GC_PROTECT_FROMSPACE_DEPTH" in values and "PERRY_GC_PROTECT_FROMSPACE" not in values: + raise ValueError("from-space depth requires protection") + return " ".join(f"{key}={value}" for key, value in values.items()) + + +def fixture_env(arm: str, text: str) -> str: + # This early return is deliberate: even malformed metadata must not alter + # compilation or execution of a shipped/default/OFF control. + return parse(text) if arm == "loop_polls" else "" + + +def self_test() -> None: + witness = "// parity-env: PERRY_GC_SCHEDULE_SEED=10061 PERRY_GC_SCHEDULE_RATE=1 PERRY_GC_SCHEDULE_ALLOC_KB=0 PERRY_GC_PROTECT_FROMSPACE=1\n" + assert fixture_env("loop_polls", witness) == witness.split(": ", 1)[1].strip() + for arm in ("shipped_default", "default", "safepoint_minor", "gen_gc_off", "wb_off", "rep_ptr_shape_off"): + assert fixture_env(arm, witness) == "" + assert fixture_env(arm, "// parity-env: PATH=/tmp/evil") == "" + assert parse("// no metadata\n") == "" + assert parse(" // parity-env: PERRY_GC_MOVING_LOOP_POLLS=1\n") == "PERRY_GC_MOVING_LOOP_POLLS=1" + invalid = ( + "PATH=/tmp/evil", "PERRY_NO_AUTO_OPTIMIZE=1", "PERRY_RUNTIME_DIR=/tmp/foreign", + "PERRY_GC_MOVING_LOOP_POLLS=0", "PERRY_GC_SCHEDULE_SEED=$(touch /tmp/evil)", + "PERRY_GC_SCHEDULE_SEED=1;echo", "PERRY_GC_SCHEDULE_SEED=`id`", + "PERRY_GC_SCHEDULE_SEED=1 PERRY_GC_SCHEDULE_SEED=2", + "PERRY_GC_SCHEDULE_SEED=18446744073709551616", "PERRY_GC_SCHEDULE_SEED=-1", + "PERRY_GC_SCHEDULE_RATE=1", "PERRY_GC_SCHEDULE_SEED=1 PERRY_GC_SCHEDULE_RATE=NaN", + "PERRY_GC_SCHEDULE_SEED=1 PERRY_GC_SCHEDULE_RATE=1.01", "PERRY_GC_PROTECT_FROMSPACE_DEPTH=0", + "PERRY_GC_PROTECT_FROMSPACE_DEPTH=4", + "", "PERRY_GC_FORCE_EVACUATE=1\n// parity-env: PERRY_GC_VERIFY_EVACUATION=1", + ) + for settings in invalid: + try: + parse("// parity-env: " + settings) + except ValueError: + pass + else: + raise AssertionError(f"accepted unsafe/malformed metadata: {settings!r}") + print("GC matrix fixture env self-test: PASS (syntax, injection rejection, OFF/control isolation)") + from gc_matrix_fixture_env_test import self_test as routing_self_test + + routing_self_test() + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("fixture", type=Path, nargs="?") + parser.add_argument("--arm", default="loop_polls") + parser.add_argument("--self-test", action="store_true") + args = parser.parse_args() + if args.self_test: + self_test() + return 0 + if args.fixture is None: + parser.error("fixture is required") + try: + print(fixture_env(args.arm, args.fixture.read_text(encoding="utf-8"))) + except (OSError, ValueError) as exc: + print(f"{args.fixture}: {exc}", file=sys.stderr) + return 2 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/gc_matrix_fixture_env_test.py b/scripts/gc_matrix_fixture_env_test.py new file mode 100644 index 0000000000..2a5542a2a2 --- /dev/null +++ b/scripts/gc_matrix_fixture_env_test.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +"""Exercise matrix env dispatch with command probes, never GC acceptance. + +Copies the original suffix fixture verbatim. The probes do not compile or run +TypeScript and emit no GC counters: moving cells must stay UNVER. Receipts +check the environment actually delivered to each compile/run command. +""" + +from __future__ import annotations + +import json +import os +from pathlib import Path +import shutil +import subprocess +import tempfile + + +ROOT = Path(__file__).resolve().parent.parent +TEST = "test_gap_gc_string_suffix_cursor" +ARMS = "loop_polls,safepoint_minor,shipped_default,wb_off" + + +def assignments(text: str) -> dict[str, str]: + return dict(word.split("=", 1) for word in text.split()) + + +def run_probe(root: Path, matrix: str, metadata: str | None = None) -> subprocess.CompletedProcess[str]: + (root / "scripts/gc_repsel_matrix.sh").write_text(matrix) + if metadata is not None: + (root / f"test-files/{TEST}.ts").write_text(metadata) + env = {key: value for key, value in os.environ.items() if not key.startswith("PERRY_")} + env.update(PATH=str(root / "probes") + os.pathsep + env["PATH"], + ROUTING_RECEIPTS=str(root / "receipts.jsonl")) + receipts = root / "receipts.jsonl" + receipts.unlink(missing_ok=True) + return subprocess.run( + ["bash", str(root / "scripts/gc_repsel_matrix.sh"), "--no-build", "--arms", ARMS, + "--jobs", "1", "--defer-liveness", "--json", str(root / "report.json")], + env=env, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=30, + ) + + +def check_receipts(root: Path, result: subprocess.CompletedProcess[str]) -> None: + assert result.returncode == 0, result.stdout[-2000:] + report = json.loads((root / "report.json").read_text()) + receipts = [json.loads(line) for line in (root / "receipts.jsonl").read_text().splitlines()] + runs = [receipt for receipt in receipts if receipt["stage"] == "run"] + assert len(runs) == len(report["cells"]) == 4 + for cell, receipt in zip(report["cells"], runs): + compile_env, run_env = assignments(cell["compile_env"]), assignments(cell["run_env"]) + assert compile_env == receipt["compile_env"], (cell["arm"], "compile receipt", receipt) + assert run_env == receipt["run_env"], (cell["arm"], "run receipt", receipt) + if cell["arm"] == "loop_polls": + for effective in (compile_env, run_env): + assert effective["PERRY_GC_SCHEDULE_SEED"] == "10061" + assert effective["PERRY_GC_SCHEDULE_ALLOC_KB"] == "0" + assert effective["PERRY_GC_PROTECT_FROMSPACE"] == "1" + assert effective["PERRY_GC_VERIFY_EVACUATION"] == "1" + assert compile_env["PERRY_GC_MOVING_LOOP_POLLS"] == "1" + assert run_env["PERRY_GC_HEAP_LIMIT"] == "8" + else: + for effective in (compile_env, run_env): + assert "PERRY_GC_SCHEDULE_SEED" not in effective, (cell["arm"], effective) + assert "PERRY_GC_PROTECT_FROMSPACE" not in effective, (cell["arm"], effective) + if cell["arm"] == "shipped_default": + assert compile_env == {} + assert run_env == {"PERRY_GC_TRACE": "1", "PERRY_GC_DIAG": "1"} + assert cell["result"] == "PASS" + else: + assert cell["result"] == "UNVER", "command probes must never certify moving GC" + if cell["arm"] == "wb_off": + assert compile_env == {"PERRY_WRITE_BARRIERS": "0"} + assert run_env["PERRY_WRITE_BARRIERS"] == "0" + + +def self_test() -> None: + matrix = (ROOT / "scripts/gc_repsel_matrix.sh").read_text() + original_fixture = (ROOT / f"test-files/{TEST}.ts").read_text() + with tempfile.TemporaryDirectory(prefix="gc-matrix-routing-test-") as work: + root = Path(work) + for directory in ("scripts", "test-files", "test-parity", "probes", "target/release"): + (root / directory).mkdir(parents=True) + shutil.copy(ROOT / "scripts/gc_matrix_fixture_env.py", root / "scripts") + shutil.copy(ROOT / ".node-version", root) + (root / f"test-files/{TEST}.ts").write_text(original_fixture) + (root / "test-parity/gc_repsel_corpus.txt").write_text(TEST + "\n") + (root / "probes/node").write_text( + "#!/usr/bin/env python3\nimport sys\nfrom pathlib import Path\n" + "print('v' + Path('.node-version').read_text().strip() if '--version' in sys.argv else 'routing-probe-only')\n" + ) + compiler = root / "target/release/perry" + compiler.write_text( + "#!/usr/bin/env python3\nimport json, os, sys\nfrom pathlib import Path\n" + "captured = {k:v for k,v in os.environ.items() if k.startswith('PERRY_') and k != 'PERRY_BIN'}\n" + "receipt = {'stage':'compile', 'compile_env':captured}\n" + "with open(os.environ['ROUTING_RECEIPTS'], 'a') as f: f.write(json.dumps(receipt)+'\\n')\n" + "output = Path(sys.argv[sys.argv.index('-o')+1])\n" + "runtime = '#!/usr/bin/env python3\\nimport json, os\\n'\n" + "runtime += 'receipt = '+repr({'stage':'run','compile_env':captured})+'\\n'\n" + "runtime += \"receipt['run_env'] = {k:v for k,v in os.environ.items() if k.startswith('PERRY_')}\\n\"\n" + "runtime += \"with open(os.environ['ROUTING_RECEIPTS'], 'a') as f: f.write(json.dumps(receipt)+'\\\\n')\\n\"\n" + "runtime += \"print('routing-probe-only')\\n\"\n" + "output.write_text(runtime); output.chmod(0o755)\n" + ) + compiler.chmod(0o755) + (root / "probes/node").chmod(0o755) + check_receipts(root, run_probe(root, matrix)) + mutations = { + "missing compile-time instruments": ( + 'effective_cenv="$effective_cenv ${FIXTURE_ENVS[$ti]}"', 'effective_cenv="$effective_cenv"'), + "metadata leaks to OFF/control arms": ( + 'if [ "$id" = loop_polls ] &&', 'if [ "$id" != absent ] &&'), + "compile group aliases safepoint_minor": ( + 'slug="${slug}_fixture"; fixture_group=1', 'fixture_group=1'), + } + for name, (old, new) in mutations.items(): + assert old in matrix + result = run_probe(root, matrix.replace(old, new)) + try: + check_receipts(root, result) + except (AssertionError, KeyError): + pass + else: + raise AssertionError(f"routing proof failed to reject sabotage: {name}") + result = run_probe(root, matrix, "// parity-env: PERRY_NO_AUTO_OPTIMIZE=1\n" + original_fixture) + assert result.returncode == 2, result.stdout + assert not (root / "receipts.jsonl").exists(), "malformed metadata reached compiler" + print("GC matrix routing self-test: PASS (actual command env, 3 sabotage controls, early refusal; no GC acceptance)") + + +if __name__ == "__main__": + self_test() diff --git a/scripts/gc_repsel_matrix.sh b/scripts/gc_repsel_matrix.sh index 049f95f09d..aee2ad0e2f 100755 --- a/scripts/gc_repsel_matrix.sh +++ b/scripts/gc_repsel_matrix.sh @@ -73,6 +73,7 @@ # [--shard N/M] [--defer-liveness] # [--list-arms] [--liveness-report-only] # [--self-test-liveness-parser] +# [--self-test-fixture-env] set -uo pipefail # Sum objects actually relocated by completed copying minors. The diagnostic @@ -113,6 +114,7 @@ JSON_OUT="" PROFILE="release" LIVENESS_REPORT_ONLY=0 SELF_TEST_LIVENESS_PARSER=0 +SELF_TEST_FIXTURE_ENV=0 SHARD_INDEX=1 SHARD_COUNT=1 DEFER_LIVENESS=0 @@ -143,6 +145,7 @@ while [ $# -gt 0 ]; do --defer-liveness) DEFER_LIVENESS=1; shift ;; --list-arms) ARMS_SEL="__list__"; shift ;; --self-test-liveness-parser) SELF_TEST_LIVENESS_PARSER=1; shift ;; + --self-test-fixture-env) SELF_TEST_FIXTURE_ENV=1; shift ;; # Local exploration only (e.g. a `--filter` narrow enough that an arm # legitimately has nothing to bite). CI never passes this: the whole # point of #7255 is that an inert arm must be able to turn a run red. @@ -152,6 +155,13 @@ while [ $# -gt 0 ]; do esac done +if [ "$SELF_TEST_FIXTURE_ENV" = 1 ]; then + exec python3 "$SCRIPT_DIR/gc_matrix_fixture_env.py" --self-test +fi +case "$PRESSURE_MB" in + ''|*[!0-9]*) echo "invalid --pressure '$PRESSURE_MB' (expected integer MB)" >&2; exit 2 ;; +esac + case "$SHARD_INDEX:$SHARD_COUNT" in *[!0-9:]*|:*|*:) echo "invalid --shard '$SHARD_INDEX/$SHARD_COUNT' (expected positive integers)" >&2 @@ -455,6 +465,21 @@ fi echo "==> shard $SHARD_INDEX/$SHARD_COUNT: ${#CORPUS[@]}/$CORPUS_TOTAL corpus files (stable manifest round-robin)" progress "selected files=${#CORPUS[@]} corpus_total=$CORPUS_TOTAL" +# Fixture settings belong only to the explicit loop-poll witness arm. Validate +# before compiling/running anything; no eval, shell quoting, or arbitrary env +# keys are accepted. Apply the same settings at compile time so auto-optimize +# selects gc-instruments for seeded/protected witnesses (freshness.rs). +FIXTURE_ENVS=() +for b in "${CORPUS[@]}"; do + fixture_env="" + case ",$SELECTED," in + *,loop_polls,*) + fixture_env="$(python3 "$SCRIPT_DIR/gc_matrix_fixture_env.py" "test-files/$b.ts")" || exit 2 + ;; + esac + FIXTURE_ENVS+=("$fixture_env") +done + # --------------------------------------------------------------------------- # Oracle + compiler. THE ORACLE VERSION IS LOAD-BEARING: a test the oracle # cannot run would drop out of the gate silently, so refuse to run at all. @@ -503,17 +528,24 @@ done # --------------------------------------------------------------------------- ARM_IDS=(); ARM_CENVS=(); ARM_RENVS=(); ARM_LIVES=(); ARM_NOTES=(); ARM_SLUGS=() GROUP_SLUGS=(); GROUP_ENVS=() +GROUP_FIXTURES=() for rec in "${ARMS[@]}"; do id="$(arm_field "$rec" 1)" case ",$SELECTED," in *",$id,"*) ;; *) continue ;; esac cenv="$(arm_field "$rec" 2)" slug="$(printf '%s' "${cenv:-_base}" | tr -c 'A-Za-z0-9' '_')" + fixture_group=0 + # safepoint_minor has the same base compile env; never share its binaries + # with loop_polls when the latter carries fixture-specific instruments. + if [ "$id" = loop_polls ]; then slug="${slug}_fixture"; fixture_group=1; fi ARM_IDS+=("$id"); ARM_CENVS+=("$cenv"); ARM_RENVS+=("$(arm_field "$rec" 3)") ARM_LIVES+=("$(arm_field "$rec" 4)"); ARM_NOTES+=("$(arm_field "$rec" 5)") ARM_SLUGS+=("$slug") known=0 for g in ${GROUP_SLUGS[@]+"${GROUP_SLUGS[@]}"}; do [ "$g" = "$slug" ] && known=1 && break; done - if [ "$known" = 0 ]; then GROUP_SLUGS+=("$slug"); GROUP_ENVS+=("$cenv"); fi + if [ "$known" = 0 ]; then + GROUP_SLUGS+=("$slug"); GROUP_ENVS+=("$cenv"); GROUP_FIXTURES+=("$fixture_group") + fi done NARMS="${#ARM_IDS[@]}" [ "$NARMS" -gt 0 ] || { echo "no arms selected ($ARMS_SEL)" >&2; exit 2; } @@ -532,15 +564,45 @@ progress "compile-start env=_warm test=${CORPUS[0]}" || { echo "${RED}warm-up compile failed${NC} (see $WORK/bin/_warm/warm.log)" >&2; } progress "compile-result env=_warm test=${CORPUS[0]} result=$([ -x "$WORK/bin/_warm/warm" ] && echo PASS || echo FAIL)" +# Seed/protection metadata selects a second auto-optimize runtime feature set. +# Warm it serially too, preserving normal shipping archive selection and the +# existing parallelism for all corpus compiles that follow. +ti=0 +while [ "$ti" -lt "${#CORPUS[@]}" ]; do + fixture_env="${FIXTURE_ENVS[$ti]}" + case "$fixture_env" in + *PERRY_GC_SCHEDULE_SEED=*|*PERRY_GC_PROTECT_FROMSPACE=*) + progress "compile-start env=_warm_fixture test=${CORPUS[$ti]} compile_env=$fixture_env" + # All words have been validated as literal GC KEY=VALUE tokens. + # shellcheck disable=SC2086 + env $fixture_env "$PERRY_BIN" "test-files/${CORPUS[$ti]}.ts" -o "$WORK/bin/_warm/fixture" \ + > "$WORK/bin/_warm/fixture.log" 2>&1 \ + || { echo "${RED}fixture warm-up compile failed${NC}" >&2; } + progress "compile-result env=_warm_fixture test=${CORPUS[$ti]} result=$([ -x "$WORK/bin/_warm/fixture" ] && echo PASS || echo FAIL)" + break + ;; + esac + ti=$((ti+1)) +done + echo "==> compiling ${#CORPUS[@]} files x ${#GROUP_SLUGS[@]} compile-env groups (jobs=$JOBS)" gi=0 while [ "$gi" -lt "${#GROUP_SLUGS[@]}" ]; do slug="${GROUP_SLUGS[$gi]}"; cenv="${GROUP_ENVS[$gi]}" - mkdir -p "$WORK/bin/$slug" + mkdir -p "$WORK/bin/$slug" "$WORK/env/$slug" + ti=0 + for b in "${CORPUS[@]}"; do + effective_cenv="$cenv" + if [ "${GROUP_FIXTURES[$gi]}" = 1 ] && [ -n "${FIXTURE_ENVS[$ti]}" ]; then + effective_cenv="$effective_cenv ${FIXTURE_ENVS[$ti]}" + fi + printf '%s\n' "$effective_cenv" > "$WORK/env/$slug/$b" + ti=$((ti+1)) + done printf '%s\n' "${CORPUS[@]}" | WORK="$WORK" PERRY_BIN="$PERRY_BIN" CENV="$cenv" SLUG="$slug" \ PROGRESS_OUT="$PROGRESS_OUT" SHARD_INDEX="$SHARD_INDEX" SHARD_COUNT="$SHARD_COUNT" \ xargs -P "$JOBS" -I{} sh -c \ - 'echo " compile env=$SLUG test=$1"; [ -z "$PROGRESS_OUT" ] || printf "%s shard=%s/%s compile-start env=%s test=%s\n" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$SHARD_INDEX" "$SHARD_COUNT" "$SLUG" "$1" >> "$PROGRESS_OUT"; if env $CENV "$PERRY_BIN" "test-files/$1.ts" -o "$WORK/bin/$SLUG/$1" > "$WORK/bin/$SLUG/$1.log" 2>&1; then [ -z "$PROGRESS_OUT" ] || printf "%s shard=%s/%s compile-result env=%s test=%s result=PASS\n" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$SHARD_INDEX" "$SHARD_COUNT" "$SLUG" "$1" >> "$PROGRESS_OUT"; else echo "COMPILEFAIL $SLUG $1"; [ -z "$PROGRESS_OUT" ] || printf "%s shard=%s/%s compile-result env=%s test=%s result=FAIL\n" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$SHARD_INDEX" "$SHARD_COUNT" "$SLUG" "$1" >> "$PROGRESS_OUT"; fi' _ {} + 'CENV=$(cat "$WORK/env/$SLUG/$1"); echo " compile env=$SLUG test=$1"; [ -z "$PROGRESS_OUT" ] || printf "%s shard=%s/%s compile-start env=%s test=%s compile_env=%s\n" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$SHARD_INDEX" "$SHARD_COUNT" "$SLUG" "$1" "$CENV" >> "$PROGRESS_OUT"; if env $CENV "$PERRY_BIN" "test-files/$1.ts" -o "$WORK/bin/$SLUG/$1" > "$WORK/bin/$SLUG/$1.log" 2>&1; then [ -z "$PROGRESS_OUT" ] || printf "%s shard=%s/%s compile-result env=%s test=%s result=PASS\n" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$SHARD_INDEX" "$SHARD_COUNT" "$SLUG" "$1" >> "$PROGRESS_OUT"; else echo "COMPILEFAIL $SLUG $1"; [ -z "$PROGRESS_OUT" ] || printf "%s shard=%s/%s compile-result env=%s test=%s result=FAIL\n" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$SHARD_INDEX" "$SHARD_COUNT" "$SLUG" "$1" >> "$PROGRESS_OUT"; fi' _ {} gi=$((gi+1)) done @@ -562,20 +624,26 @@ triage_reason() { # $1 test, $2 arm END { exit(found ? 0 : 1) }' } -CELLS=(); EVID=(); CYC=(); EVA=(); SCA=(); REC=() +CELLS=(); EVID=(); CYC=(); EVA=(); SCA=(); REC=(); CELL_CENVS=(); CELL_RENVS=() n_pass=0; n_unver=0; n_fail=0; n_xfail=0 ai=0 while [ "$ai" -lt "$NARMS" ]; do id="${ARM_IDS[$ai]}"; slug="${ARM_SLUGS[$ai]}"; live="${ARM_LIVES[$ai]}" - renv="$(printf '%s' "${ARM_RENVS[$ai]}" | sed -e "s/%P%/$PRESSURE_ENV/" -e "s/%E%/$EVAC_ENV/")" - [ "$renv" = "-" ] && renv="" + arm_renv="$(printf '%s' "${ARM_RENVS[$ai]}" | sed -e "s/%P%/$PRESSURE_ENV/" -e "s/%E%/$EVAC_ENV/")" + [ "$arm_renv" = "-" ] && arm_renv="" echo "==> arm $id" ti=0 while [ "$ti" -lt "${#CORPUS[@]}" ]; do b="${CORPUS[$ti]}"; bin="$WORK/bin/$slug/$b"; idx=$((ti*NARMS+ai)) + renv="$arm_renv" + if [ "$id" = loop_polls ] && [ -n "${FIXTURE_ENVS[$ti]}" ]; then + renv="$renv ${FIXTURE_ENVS[$ti]}" + fi + CELL_CENVS[idx]="$(cat "$WORK/env/$slug/$b")" + CELL_RENVS[idx]="$renv PERRY_GC_TRACE=1 PERRY_GC_DIAG=1" echo " run [$((ti+1))/${#CORPUS[@]}] test=$b env=$slug arm=$id" - progress "cell-start test=$b env=$slug arm=$id" - cycles=0; evacuated=0; scavenged=0 + progress "cell-start test=$b env=$slug arm=$id compile_env=${CELL_CENVS[$idx]} run_env=${CELL_RENVS[$idx]}" + cycles=0; evacuated=0; scavenged=0; reclaimed=0 if [ ! -x "$bin" ]; then result="FAIL"; ev="compile-failed" else @@ -793,9 +861,10 @@ JSON_REPORT="${JSON_OUT:-$WORK/matrix.json}" # characters that would make this invalid JSON, so a malformed # report can never be the reason the gate fails. ev_json="$(printf '%s' "${EVID[$idx]:-}" | tr '"\\' "''")" - printf '{"test":"%s","arm":"%s","result":"%s","cycles":%d,"evacuated":%d,"scavenged":%d,"reclaimed":%d,"evidence":"%s"}' \ + printf '{"test":"%s","arm":"%s","result":"%s","cycles":%d,"evacuated":%d,"scavenged":%d,"reclaimed":%d,"evidence":"%s","compile_env":"%s","run_env":"%s"}' \ "${CORPUS[$ti]}" "${ARM_IDS[$ai]}" "${CELLS[$idx]:-?}" \ - "${CYC[$idx]:-0}" "${EVA[$idx]:-0}" "${SCA[$idx]:-0}" "${REC[$idx]:-0}" "$ev_json" + "${CYC[$idx]:-0}" "${EVA[$idx]:-0}" "${SCA[$idx]:-0}" "${REC[$idx]:-0}" "$ev_json" \ + "${CELL_CENVS[$idx]}" "${CELL_RENVS[$idx]}" ai=$((ai+1)) done ti=$((ti+1)) diff --git a/test-files/test_gap_gc_11590_packed_loop_global_cache_rooting.ts b/test-files/test_gap_gc_11590_packed_loop_global_cache_rooting.ts index 34b4cf7cfc..017e523fb7 100644 --- a/test-files/test_gap_gc_11590_packed_loop_global_cache_rooting.ts +++ b/test-files/test_gap_gc_11590_packed_loop_global_cache_rooting.ts @@ -1,3 +1,4 @@ +// parity-env: PERRY_GC_MOVING_LOOP_POLLS=1 PERRY_GC_SCHEDULE_SEED=11590 PERRY_GC_SCHEDULE_RATE=1 PERRY_GC_SCHEDULE_ALLOC_KB=4 PERRY_GC_PROTECT_FROMSPACE=1 PERRY_GC_VERIFY_EVACUATION=1 // #11590: a packed-f64 range loop at module scope caches every loop-invariant // module global it reads in an entry alloca, for both loop clones. When the // global holds a heap receiver — here an array grown from `[]` by `d[j] = v`, diff --git a/test-files/test_gap_gc_call_argument_rooting.ts b/test-files/test_gap_gc_call_argument_rooting.ts index 38c55fec1c..7c18b40471 100644 --- a/test-files/test_gap_gc_call_argument_rooting.ts +++ b/test-files/test_gap_gc_call_argument_rooting.ts @@ -47,6 +47,7 @@ // it. That is the same shape of blind spot `js_implicit_this_set` (#7226) and // `js_regexp_new` (#7227) each cost a round for. // +// parity-env: PERRY_GC_MOVING_LOOP_POLLS=1 PERRY_GC_SCHEDULE_SEED=1 PERRY_GC_SCHEDULE_RATE=1 PERRY_GC_SCHEDULE_ALLOC_KB=4 PERRY_GC_PROTECT_FROMSPACE=1 PERRY_GC_VERIFY_EVACUATION=1 // LIVE BY CONSTRUCTION. `churn` keeps allocating AFTER the back-edge poll that // collects, so the abandoned from-space bytes are recycled before the callee // reads them — a stale read returns wrong text instead of the right answer out diff --git a/test-files/test_gap_gc_container_value_rooting.ts b/test-files/test_gap_gc_container_value_rooting.ts index 93834de786..fec0ab7258 100644 --- a/test-files/test_gap_gc_container_value_rooting.ts +++ b/test-files/test_gap_gc_container_value_rooting.ts @@ -1,3 +1,4 @@ +// parity-env: PERRY_GC_MOVING_LOOP_POLLS=1 PERRY_GC_SCHEDULE_SEED=7949 PERRY_GC_SCHEDULE_RATE=1 PERRY_GC_SCHEDULE_ALLOC_KB=4 PERRY_GC_PROTECT_FROMSPACE=1 PERRY_GC_VERIFY_EVACUATION=1 // #7949: JS values retained in ordinary Rust containers across allocating calls. // // `Object.groupBy` / `Map.groupBy` accumulate every `(key, item)` pair into a