diff --git a/.github/workflows/gc-moving-witnesses.yml b/.github/workflows/gc-moving-witnesses.yml index 09ef1d41b1..5d8113dc19 100644 --- a/.github/workflows/gc-moving-witnesses.yml +++ b/.github/workflows/gc-moving-witnesses.yml @@ -341,6 +341,46 @@ jobs: "PERRY_GC_MOVING_LOOP_POLLS=1." % len(cells)) PY + # ★★ The DEPENDENCY-SCALE RUNTIME witness (#7717, the one unmet ask of + # #7280). + # + # Everything above this line is a curated reproducer. #7280 measured why + # that is not enough: the curated corpus passed 25/25 while twenty lines + # of stock `zod` failed 5/40. It is a distribution problem rather than a + # size one — dependency-shaped code is dominated by `js_object_assign_one` + # (object spread) and `js_new_function_construct`, populations the curated + # files barely produce. + # + # `gc-root-dominance` already compiles this same corpus, but it is a + # STATIC pass over emitted IR and never runs a program, so it cannot see a + # runtime-side cache holding a raw heap pointer. This step runs it. + # + # The env knobs and every assertion live in the script, not here, so that + # reproducing a CI failure is one command rather than a re-read of this + # YAML. In particular the script pins `PERRY_NO_AUTO_OPTIMIZE=1`, which is + # a deliberate departure from the matrix above: the auto-optimizer relinks + # the runtime WITHOUT the `diagnostics` feature, and `diagnostics` is what + # emits the `[gc-fromspace-protect]` line this gate reads as its proof + # that the quarantine engaged. + - name: Install the npm dependency the dependency-scale witness runs + if: steps.relevance.outputs.run == 'true' + # `zod` is this repo's own package.json devDependency, pinned by + # package-lock.json and governed by the same soak window as everything + # else in that file — not a fixture invented for this job. Same + # invocation `gc-root-dominance` uses for the same corpus. + run: npm ci --ignore-scripts --no-audit --no-fund + + # GATING. No pipe, no `|| true`. The script asserts its own subject ran + # three independent ways — the `[gc-schedule]` verdict, `copying_minors` / + # `moved_objects` / `loop_polls` re-read from it, and a non-zero count of + # retired from-space page-sets — because `PERRY_GC_PROTECT_FROMSPACE=1` on + # a run with no copying minor protects NOTHING and still exits clean. + # #7717 records hitting exactly that and nearly recording the wrong + # conclusion. + - name: Run the dependency-scale runtime witness + if: steps.relevance.outputs.run == 'true' + run: ./scripts/gc_dep_scale_witness.sh + - name: Upload the witness report if: always() && steps.relevance.outputs.run == 'true' uses: actions/upload-artifact@v7 diff --git a/CLAUDE.md b/CLAUDE.md index b8a0eb7e85..753f15bddb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Perry is a native TypeScript compiler written in Rust that compiles TypeScript source code directly to native executables. It uses SWC for TypeScript parsing and LLVM for code generation. -**Current Version:** 0.5.1459 +**Current Version:** 0.5.1460 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index 0ac0c6c6e9..54362bfd8c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5547,7 +5547,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "anyhow", "base64", @@ -5607,14 +5607,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "serde", ] [[package]] name = "perry-audio-miniaudio" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "cc", "libc", @@ -5622,7 +5622,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "anyhow", "inkwell", @@ -5639,7 +5639,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "anyhow", "perry-hir", @@ -5647,7 +5647,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "anyhow", "perry-hir", @@ -5655,7 +5655,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "anyhow", "perry-dispatch", @@ -5664,7 +5664,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "anyhow", "perry-hir", @@ -5672,7 +5672,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "anyhow", "base64", @@ -5684,7 +5684,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "anyhow", "perry-hir", @@ -5692,7 +5692,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "anyhow", "async-trait", @@ -5721,14 +5721,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "serde", "serde_json", @@ -5736,7 +5736,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1459" +version = "0.5.1460" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5747,7 +5747,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "anyhow", "clap", @@ -5762,7 +5762,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "block2", "objc2", @@ -5772,7 +5772,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "argon2", "perry-ffi", @@ -5780,7 +5780,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "perry-ffi", "reqwest", @@ -5789,7 +5789,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "bcrypt", "perry-ffi", @@ -5797,7 +5797,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "perry-ffi", "rusqlite", @@ -5805,7 +5805,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "perry-ffi", "scraper", @@ -5813,7 +5813,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "perry-ffi", "perry-runtime", @@ -5821,7 +5821,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "chrono", "cron", @@ -5831,7 +5831,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "chrono", "perry-ffi", @@ -5839,7 +5839,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "perry-ffi", "rust_decimal", @@ -5847,7 +5847,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "perry-ffi", "serde_json", @@ -5855,7 +5855,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "perry-ffi", "rand 0.10.1", @@ -5863,7 +5863,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "perry-ffi", "perry-runtime", @@ -5871,14 +5871,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "bytes", "http-body-util", @@ -5896,7 +5896,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "bytes", "lazy_static", @@ -5909,7 +5909,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "bytes", "h2", @@ -5933,7 +5933,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "lazy_static", "perry-ffi", @@ -5943,7 +5943,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "base64", "jsonwebtoken", @@ -5954,7 +5954,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "lru", "perry-ffi", @@ -5963,7 +5963,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "chrono", "perry-ffi", @@ -5971,7 +5971,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "bson", "futures-util", @@ -5983,7 +5983,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "chrono", "perry-ffi", @@ -5993,7 +5993,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "nanoid", "perry-ffi", @@ -6002,7 +6002,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "bytes", "perry-ffi", @@ -6015,7 +6015,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "const-oid 0.9.6", "der 0.7.10", @@ -6034,7 +6034,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "lettre", "perry-ffi", @@ -6044,7 +6044,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "perry-ffi", "printpdf", @@ -6052,7 +6052,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "perry-ffi", "sqlx", @@ -6061,7 +6061,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "governor", "perry-ffi", @@ -6069,7 +6069,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "fast_image_resize", "image", @@ -6079,14 +6079,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "lazy_static", "perry-ffi", @@ -6095,7 +6095,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "perry-ffi", "perry-runtime", @@ -6104,7 +6104,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "perry-ffi", "uuid", @@ -6112,7 +6112,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "perry-ffi", "regex", @@ -6122,7 +6122,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "futures-util", "lazy_static", @@ -6135,7 +6135,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "brotli", "flate2", @@ -6145,7 +6145,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "dashmap", "once_cell", @@ -6154,7 +6154,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "anyhow", "perry-api-manifest", @@ -6172,7 +6172,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "anyhow", "perry-diagnostics", @@ -6184,7 +6184,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "anyhow", "base64", @@ -6226,14 +6226,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6328,14 +6328,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "anyhow", "perry-hir", @@ -6344,14 +6344,14 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "perry-ui-model", ] [[package]] name = "perry-ui-android" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "base64", "itoa", @@ -6368,7 +6368,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "rand 0.10.1", "serde", @@ -6378,7 +6378,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "base64", "cairo-rs 0.22.0", @@ -6401,7 +6401,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "base64", "block2", @@ -6417,7 +6417,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "base64", "block2", @@ -6432,7 +6432,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1459" +version = "0.5.1460" [[package]] name = "perry-ui-test" @@ -6443,11 +6443,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1459" +version = "0.5.1460" [[package]] name = "perry-ui-tvos" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "base64", "block2", @@ -6463,7 +6463,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "base64", "block2", @@ -6479,7 +6479,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "block2", "libc", @@ -6492,7 +6492,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "base64", "libc", @@ -6509,14 +6509,14 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "perry-ui-windows", ] [[package]] name = "perry-updater" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "anyhow", "base64", @@ -6532,7 +6532,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1459" +version = "0.5.1460" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index d0441dc2c5..431c2eb967 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -315,7 +315,7 @@ codegen-units = 16 codegen-units = 16 [workspace.package] -version = "0.5.1459" +version = "0.5.1460" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry" diff --git a/changelog.d/7793-dep-scale-runtime-witness.md b/changelog.d/7793-dep-scale-runtime-witness.md new file mode 100644 index 0000000000..141d4c5df9 --- /dev/null +++ b/changelog.d/7793-dep-scale-runtime-witness.md @@ -0,0 +1,26 @@ +**The moving collector now has a dependency-scale RUNTIME witness in CI** (#7717, the one unmet ask of #7280). + +Everything gating this family was either a synthetic fixture or a static pass over emitted IR, and neither can observe what the #7154 class actually presents as. `scripts/gc_root_dominance_check.py` reads LLVM IR, so a runtime-side `static` or `thread_local!` holding a raw heap pointer is structurally invisible to it; and the defect is invisible to every runtime GC probe *at* the collection too, because there is nothing for the collector to find — it surfaces cycles later, in a different function, as `TypeError: value is not a function`. + +Scale is the point, not size. #7280 measured it: the curated 25-file corpus passed 25/25 while twenty lines of stock `zod` failed 5/40, because dependency-shaped code is dominated by `js_object_assign_one` (object spread) and `js_new_function_construct` — populations the curated files barely produce. + +`scripts/gc_dep_scale_witness.sh` runs `test-files/gc-dep-corpus/main.ts` — the same 81-module zod corpus `gc-root-dominance` already compiles for its static check, whose own header always said it "is run as the acceptance workload for the moving collector" — under `PERRY_GC_SCHEDULE_SEED=1 PERRY_GC_SCHEDULE_RATE=1 PERRY_GC_PROTECT_FROMSPACE=1 PERRY_GC_PROTECT_FROMSPACE_DEPTH=800`, and is wired into `gc-moving-witnesses.yml`. + +Measured on `main`: 8,020 forced collections, all 8,020 of them copying minors, 852,107 objects relocated, 63,936 back-edge polls, 8,020 retired quarantine page-sets, and the answer byte-identical to the same binary run without the schedule. + +**What makes it able to fail.** The trap #7717 names is a run that exits clean having witnessed nothing, so the script asserts its subject ran three independent ways: the `[gc-schedule]` exit verdict; `copying_minors` / `moved_objects` / `loop_polls` re-read from it rather than trusting that the binary still exits 70; and a non-zero count of retired from-space page-sets, because `PERRY_GC_PROTECT_FROMSPACE=1` on a run with no copying minor protects nothing and still exits 0. Both directions were verified by sabotage rather than asserted: with the schedule disabled it fails on the missing verdict, and with the quarantine disabled it fails with "protected NOTHING and this run's cleanliness means nothing" — which is precisely the wrong conclusion #7717 records nearly recording. + +Two findings worth keeping: + +- **`PERRY_NO_AUTO_OPTIMIZE=1` is load-bearing here**, and is a deliberate departure from `gc_repsel_matrix.sh`, which omits it on purpose. Without it the compile reaches the auto-optimizer, which relinks the runtime as `features=async-runtime,web-fetch` — no `diagnostics`, and `diagnostics` is what emits the `[gc-fromspace-protect]` line this gate reads as its proof. The assertions are fail-closed so a stripped runtime goes red rather than quietly green, but red-for-the-wrong-reason is still a broken gate. +- **Depth 800 is load-bearing, not decorative.** The quarantine runs saturated on this workload (`sets_held=800/800`), so lowering it silently narrows the window in which a stale dereference is still catchable. The default of 4 is far too small for real code — #7154's own reproducer needed 800. + +The gate is deliberately **not** added to branch protection's required contexts: a new gate has never been green, so promoting it immediately would block every open PR. It should be observed on `main` first, then promoted — leaving that second step undone is how `gc-stress` ended up reporting failures without blocking anything. + +Also corrected while writing this: #7717 says loop polls are "default off since #7161". They are default **ON** since #7721 — `moving_safepoint_polls_enabled_from_env` is `!matches!(value, Some("0") | Some("off") | Some("false"))` — so the witness does not need to opt into them, and the run above confirms 63,936 polls fired without the flag. + +Ported from `PERRY_GC_ZEAL` to `PERRY_GC_SCHEDULE_SEED=1 PERRY_GC_SCHEDULE_RATE=1` +during the merge audit: #7741 retired the zeal knob between this PR's authoring +and its landing, and the witness's fail-closed verdict assertion would have gone +red on the missing `[gc-zeal]` line — red for the wrong reason, which is still a +broken gate. Re-measured and re-sabotaged post-port; numbers in the merge comment. diff --git a/scripts/gc_dep_scale_witness.sh b/scripts/gc_dep_scale_witness.sh new file mode 100755 index 0000000000..5056fb8056 --- /dev/null +++ b/scripts/gc_dep_scale_witness.sh @@ -0,0 +1,177 @@ +#!/usr/bin/env bash +# The DEPENDENCY-SCALE RUNTIME witness for the moving collector (#7717, the one +# unmet ask of #7280). +# +# Reproduce a CI failure with exactly this, from the repo root: +# +# npm ci --ignore-scripts --no-audit --no-fund +# cargo build --release -p perry -p perry-runtime-static -p perry-stdlib-static +# ./scripts/gc_dep_scale_witness.sh +# +# The env knobs live HERE and not in the YAML on purpose: a retyped invocation +# that drops one of them produces a run that exits 0 while witnessing nothing, +# which is the failure this file exists to prevent. +# +# WHY A RUNTIME WITNESS, WHEN TWO STATIC GATES ALREADY COMPILE THIS CORPUS +# +# `gc-root-dominance` compiles `test-files/gc-dep-corpus/` and checks the +# emitted LLVM IR. That cannot see a runtime-side cache holding a raw heap +# pointer — `scripts/gc_root_dominance_check.py` reads IR, and a `static` or +# `thread_local!` in perry-runtime is structurally invisible to it. The #7154 +# class is also invisible to every runtime GC probe AT the collection, because +# there is nothing for the collector to find; it surfaces cycles later, in a +# different function, as `TypeError: value is not a function`. +# +# So the only instrument that can observe it is: run real dependency-shaped code +# under a collector that relocates constantly, with the from-space quarantine +# armed deep enough that a stale pointer faults at the faulting instruction +# instead of silently reading recycled bytes. +# +# WHY DEPENDENCY-SCALE RATHER THAN A FIXTURE. #7280 measured it: the curated +# 25-file corpus passed 25/25 while twenty lines of stock `zod` failed 5/40. It +# is a distribution problem, not a size one — the dependency-scale corpus is +# dominated by `js_object_assign_one` (object spread) and +# `js_new_function_construct`, populations the curated files barely produce. +# +# WHAT MAKES THIS ABLE TO FAIL (CLAUDE.md's four ways a gate cannot) +# +# 1. No `continue-on-error`, no `|| true`, no pipe between a producer and its +# check — every command's status is the gate's status (`set -euo pipefail`). +# 2. It asserts its SUBJECT RAN, three independent ways, and any one of them +# missing is a hard failure: +# * the `[gc-schedule]` exit verdict, which since #7604 (ported to the +# seeded schedule by #7741) makes the binary itself exit 70 when a +# rate-1 run forced no collection or relocated +# nothing — so a vacuous run is red without this script's help; +# * `copying_minors` and `loop_polls` are re-read here anyway, because +# "the binary would have exited 70" is a claim about a version of the +# binary, and this gate should not depend on that staying true; +# * `[gc-fromspace-protect] retired_set=#N`, because +# `PERRY_GC_PROTECT_FROMSPACE=1` on a run with no copying minor +# protects NOTHING and still exits clean. #7717 records hitting exactly +# that and nearly recording the wrong conclusion. +# 3. The answer is compared against the same binary run WITHOUT the schedule. A stale +# root corrupts values, so identical output across the two is the property +# that matters. (It is not diffed against node: the corpus imports zod by +# SOURCE PATH — `node_modules/zod/src/index.js` resolving to `.ts` — which +# is what makes its modules native to Perry and what node cannot resolve.) +# 4. Promotion to a required context is deliberately NOT done here; a new gate +# has never been green, so it is observed on `main` first. +# +# DEPTH 800 IS NOT A ROUND NUMBER. `PERRY_GC_PROTECT_FROMSPACE_DEPTH` bounds how +# many retired page-sets stay quarantined, and the default of 4 is far too small +# for real code: #7154's own reproducer needed 800 because the value crossed 600 +# polls between its last valid observation and its stale use. Measured on this +# workload the quarantine runs saturated — `sets_held=800/800` — so the depth is +# load-bearing rather than decorative, and lowering it silently narrows the +# window in which a stale dereference can still be caught. + +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$ROOT" + +PERRY_BIN="${PERRY_BIN:-$ROOT/target/release/perry}" +RUNTIME_DIR="${PERRY_RUNTIME_DIR:-$ROOT/target/release}" +ENTRY="${ENTRY:-test-files/gc-dep-corpus/main.ts}" +OUT_DIR="$(mktemp -d)" +trap 'rm -rf "$OUT_DIR"' EXIT + +fail() { echo "::error::$*" >&2; exit 1; } + +[ -x "$PERRY_BIN" ] || fail "PERRY_BIN=$PERRY_BIN is not executable. Build: cargo build --release -p perry -p perry-runtime-static -p perry-stdlib-static" +for lib in libperry_runtime.a libperry_stdlib.a; do + [ -s "$RUNTIME_DIR/$lib" ] || fail "$RUNTIME_DIR/$lib is missing. perry-runtime and perry-stdlib are rlib-only; the archives come from the -static wrapper crates." +done +# The dependency IS the test. Its absence must be an error rather than a +# silently smaller witness. +[ -f node_modules/zod/src/index.ts ] || fail "node_modules/zod/src/index.ts is missing — run: npm ci --ignore-scripts --no-audit --no-fund" +[ -f "$ENTRY" ] || fail "$ENTRY is missing" + +export PERRY_RUNTIME_DIR="$RUNTIME_DIR" +# Same reason gc-moving-witnesses.yml sets it: PERRY_GC_MOVING_LOOP_POLLS is a +# compile-time gate that build_cache.rs does not key (#7183), so the build-level +# cache cannot tell a polled binary from an unpolled one. The object cache does +# key it; disabling the build cache costs one compile here. +export PERRY_DISABLE_BUILD_CACHE=1 +# THIS ONE IS NOT OPTIONAL, and it is where this script deliberately departs +# from `scripts/gc_repsel_matrix.sh`, which omits it on purpose. +# +# Measured while writing this: without it the compile reaches the +# auto-optimizer, which RELINKS the runtime as +# `features=async-runtime,web-fetch` — note what is absent. `diagnostics` is +# what emits `[gc-fromspace-protect]`, i.e. the only evidence that the +# quarantine engaged at all, and it is one of the three things this gate +# asserts. The matrix can afford to let the auto-optimizer choose because it +# re-reads whichever trace format it gets; this witness cannot, because a +# stripped runtime removes its evidence rather than changing its shape. +# +# The assertions below are fail-closed, so a stripped runtime would go red +# rather than quietly pass — but red-for-the-wrong-reason is still a broken +# gate, and the relink also costs minutes of CI on every run. +export PERRY_NO_AUTO_OPTIMIZE=1 + +BIN="$OUT_DIR/gc-dep-witness" +echo "==> compiling $ENTRY" +"$PERRY_BIN" "$ENTRY" -o "$BIN" || fail "the witness workload did not compile" +[ -x "$BIN" ] || fail "$BIN was not produced" + +echo "==> baseline run (no schedule), for the answer to be compared against" +"$BIN" > "$OUT_DIR/plain.out" 2> "$OUT_DIR/plain.err" \ + || fail "the workload failed WITHOUT the schedule — that is a plain breakage, not a rooting witness. stderr: $(tail -5 "$OUT_DIR/plain.err")" +[ -s "$OUT_DIR/plain.out" ] || fail "the workload printed nothing; there is no answer to compare" + +echo "==> witness run: rate-1 schedule + from-space quarantine at depth 800" +# PERRY_GC_DIAG=1 is what emits `[gc-fromspace-protect]`, which is the only +# evidence that the quarantine engaged. Both are eprintln-only diagnostics. +set +e +PERRY_GC_SCHEDULE_SEED=1 \ +PERRY_GC_SCHEDULE_RATE=1 \ +PERRY_GC_PROTECT_FROMSPACE=1 \ +PERRY_GC_PROTECT_FROMSPACE_DEPTH=800 \ +PERRY_GC_DIAG=1 \ + "$BIN" > "$OUT_DIR/sched.out" 2> "$OUT_DIR/sched.err" +status=$? +set -e + +if [ "$status" -ne 0 ]; then + echo "--- last 40 lines of witness stderr ---" >&2 + tail -40 "$OUT_DIR/sched.err" >&2 || true + case "$status" in + 70) fail "the rate-1 run exercised nothing (exit 70). This is NOT a pass: see the [gc-schedule] lines above for which of forced_collections / copying_minors / loop_polls was zero." ;; + 139|134|11) fail "the witness FAULTED under the from-space quarantine (exit $status). This is the #7154 class doing what it does: a stale from-space pointer was dereferenced. The reporter above names the address, the retiring minor, and the last-known object's type." ;; + *) fail "the witness exited $status under the schedule" ;; + esac +fi + +if ! diff -u "$OUT_DIR/plain.out" "$OUT_DIR/sched.out" > "$OUT_DIR/answer.diff"; then + echo "--- answer differs between the plain and scheduled runs ---" >&2 + cat "$OUT_DIR/answer.diff" >&2 + fail "the workload computed a DIFFERENT answer under a relocating collector. A stale root corrupts values; this is the symptom without the fault." +fi + +verdict="$(grep -F '[gc-schedule] forced_collections=' "$OUT_DIR/sched.err" | tail -1 || true)" +[ -n "$verdict" ] || fail "no [gc-schedule] verdict line was printed, so nothing proves a collection happened. Were PERRY_GC_SCHEDULE_SEED=1 PERRY_GC_SCHEDULE_RATE=1 actually set, and is this binary new enough to emit the rate-1 verdict (#7604, retired-zeal port #7741)?" + +read_field() { echo "$verdict" | grep -oE "$1=[0-9]+" | cut -d= -f2; } +cycles="$(read_field copying_minors)" +moved="$(read_field moved_objects)" +polls="$(read_field loop_polls)" +: "${cycles:=0}" "${moved:=0}" "${polls:=0}" + +[ "$cycles" -gt 0 ] || fail "copying_minors=0 — nothing was relocated, so a stale-root witness could not have failed no matter how broken the rooting is." +[ "$moved" -gt 0 ] || fail "moved_objects=0 — the collector ran but relocated nothing, which is the same vacuous result." +# Back-edge polls are what put a collection INSIDE a loop body. Without them a +# rate-1 run only collects at event-loop boundaries and no loop is covered. +[ "$polls" -gt 0 ] || fail "loop_polls=0 — every collection came from an event-loop boundary, so no loop body was covered. Codegen emits no poll for a provably alloc-free body, nor for the specialized for/for-of/for-in lowerings." + +retired="$(grep -cF '[gc-fromspace-protect]' "$OUT_DIR/sched.err" || true)" +: "${retired:=0}" +[ "$retired" -gt 0 ] || fail "the from-space quarantine never retired a page-set, so PERRY_GC_PROTECT_FROMSPACE=1 protected NOTHING and this run's cleanliness means nothing (#7717)." + +echo +echo "$verdict" +echo "[gc-dep-scale-witness] retired page-sets: $retired" +echo "[gc-dep-scale-witness] answer identical with and without a relocating collector:" +sed 's/^/ /' "$OUT_DIR/plain.out" +echo "[gc-dep-scale-witness] PASS"