diff --git a/CLAUDE.md b/CLAUDE.md index 2759c925a8..1e4a6ca565 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.1418 +**Current Version:** 0.5.1419 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index b16e28fadb..9275ca78d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5547,7 +5547,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "anyhow", "base64", @@ -5607,14 +5607,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "serde", ] [[package]] name = "perry-audio-miniaudio" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "cc", "libc", @@ -5622,7 +5622,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "anyhow", "inkwell", @@ -5639,7 +5639,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "anyhow", "perry-hir", @@ -5647,7 +5647,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "anyhow", "perry-hir", @@ -5655,7 +5655,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "anyhow", "perry-dispatch", @@ -5664,7 +5664,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "anyhow", "perry-hir", @@ -5672,7 +5672,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "anyhow", "base64", @@ -5684,7 +5684,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "anyhow", "perry-hir", @@ -5692,7 +5692,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "anyhow", "async-trait", @@ -5721,14 +5721,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "serde", "serde_json", @@ -5736,7 +5736,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1418" +version = "0.5.1419" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5747,7 +5747,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "anyhow", "clap", @@ -5762,7 +5762,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "block2", "objc2", @@ -5772,7 +5772,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "argon2", "perry-ffi", @@ -5780,7 +5780,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "perry-ffi", "reqwest", @@ -5789,7 +5789,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "bcrypt", "perry-ffi", @@ -5797,7 +5797,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "perry-ffi", "rusqlite", @@ -5805,7 +5805,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "perry-ffi", "scraper", @@ -5813,7 +5813,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "perry-ffi", "perry-runtime", @@ -5821,7 +5821,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "chrono", "cron", @@ -5831,7 +5831,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "chrono", "perry-ffi", @@ -5839,7 +5839,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "perry-ffi", "rust_decimal", @@ -5847,7 +5847,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "perry-ffi", "serde_json", @@ -5855,7 +5855,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "perry-ffi", "rand 0.10.1", @@ -5863,7 +5863,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "perry-ffi", "perry-runtime", @@ -5871,14 +5871,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "bytes", "http-body-util", @@ -5896,7 +5896,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "bytes", "lazy_static", @@ -5909,7 +5909,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "bytes", "h2", @@ -5933,7 +5933,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "lazy_static", "perry-ffi", @@ -5943,7 +5943,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "base64", "jsonwebtoken", @@ -5954,7 +5954,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "lru", "perry-ffi", @@ -5963,7 +5963,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "chrono", "perry-ffi", @@ -5971,7 +5971,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "bson", "futures-util", @@ -5983,7 +5983,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "chrono", "perry-ffi", @@ -5993,7 +5993,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "nanoid", "perry-ffi", @@ -6002,7 +6002,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "bytes", "perry-ffi", @@ -6015,7 +6015,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "const-oid 0.9.6", "der 0.7.10", @@ -6034,7 +6034,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "lettre", "perry-ffi", @@ -6044,7 +6044,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "perry-ffi", "printpdf", @@ -6052,7 +6052,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "perry-ffi", "sqlx", @@ -6061,7 +6061,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "governor", "perry-ffi", @@ -6069,7 +6069,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "fast_image_resize", "image", @@ -6079,14 +6079,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "lazy_static", "perry-ffi", @@ -6095,7 +6095,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "perry-ffi", "perry-runtime", @@ -6104,7 +6104,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "perry-ffi", "uuid", @@ -6112,7 +6112,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "perry-ffi", "regex", @@ -6122,7 +6122,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "futures-util", "lazy_static", @@ -6135,7 +6135,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "brotli", "flate2", @@ -6145,7 +6145,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "dashmap", "once_cell", @@ -6154,7 +6154,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "anyhow", "perry-api-manifest", @@ -6172,7 +6172,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "anyhow", "perry-diagnostics", @@ -6184,7 +6184,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "anyhow", "base64", @@ -6226,14 +6226,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6328,14 +6328,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "anyhow", "perry-hir", @@ -6344,14 +6344,14 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "perry-ui-model", ] [[package]] name = "perry-ui-android" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "base64", "itoa", @@ -6368,7 +6368,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "rand 0.10.1", "serde", @@ -6378,7 +6378,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "base64", "cairo-rs 0.22.0", @@ -6401,7 +6401,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "base64", "block2", @@ -6417,7 +6417,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "base64", "block2", @@ -6432,7 +6432,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1418" +version = "0.5.1419" [[package]] name = "perry-ui-test" @@ -6443,11 +6443,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1418" +version = "0.5.1419" [[package]] name = "perry-ui-tvos" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "base64", "block2", @@ -6463,7 +6463,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "base64", "block2", @@ -6479,7 +6479,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "block2", "libc", @@ -6492,7 +6492,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "base64", "libc", @@ -6509,14 +6509,14 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "perry-ui-windows", ] [[package]] name = "perry-updater" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "anyhow", "base64", @@ -6532,7 +6532,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1418" +version = "0.5.1419" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index 11da24912c..99c01e1c13 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -315,7 +315,7 @@ codegen-units = 16 codegen-units = 16 [workspace.package] -version = "0.5.1418" +version = "0.5.1419" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry" diff --git a/changelog.d/7723-gc-gate-arms.md b/changelog.d/7723-gc-gate-arms.md new file mode 100644 index 0000000000..1ac746de52 --- /dev/null +++ b/changelog.d/7723-gc-gate-arms.md @@ -0,0 +1,82 @@ +Closed two GC issues in the "a real hazard exists and nothing exercises it" class. + +**#7251 — no-move window for the two lazy intrinsic-tower builders.** +`build_generator_tower` (`object/global_this/generator.rs`) and +`ensure_typed_array_intrinsic` (`object/global_this/typed_array.rs`) build +the same shape of immortal object graph #7217 fixed for +`populate_global_this_builtins`: raw `*mut ObjectHeader`/`*mut ClosureHeader` +locals threaded across a dozen-plus allocating installs, with no root the +collector knows about until the final `AtomicI64::store`. Both now open a +`crate::gc::GcSuppressScope` for the whole build. + +The blocker had been the gate, not the fix — three prior attempts all passed +with the window deleted, plus a fourth found while building this one: arming +the ArenaBytes byte-threshold trigger under `force_legacy_gc_pacing` also +passed vacuously, because under that pacing an ArenaBytes trigger only starts +a *budgeted* cycle that needs an explicit host-safepoint pump to advance, so +`gc_collection_count()` never moved whether or not the builder was +suppressed. Fixed by arming `GC_OLD_RECLAIM_PENDING` instead — the branch +`gc_check_trigger` services synchronously regardless of pacing — combined +with forcing the current arena block (very nearly) full before calling the +builder, so its first allocation reaches `gc_check_trigger()` at all despite +being three orders of magnitude smaller than a block. + +Also fixed the ordering hazard behind two of the three original failed +attempts: the six intrinsic-tower `AtomicI64` statics +(`TYPED_ARRAY_INTRINSIC_PTR`, `TYPED_ARRAY_INTRINSIC_PROTO_PTR`, +`GENERATOR_FUNCTION_INTRINSIC_PTR`, `GENERATOR_INTRINSIC_PROTO_PTR`, +`GENERATOR_PROTOTYPE_PTR`, `ASYNC_GENERATOR_FUNCTION_INTRINSIC_PTR`, +`ASYNC_GENERATOR_INTRINSIC_PROTO_PTR`, `ASYNC_GENERATOR_PROTOTYPE_PTR`) were +plain process-global statics, built once per *process* rather than per test, +so whichever libtest thread happened to touch a tower first poisoned every +other test's "never built" precondition. Converted to `per_test_global!` +(#7672's mechanism for exactly this hazard): each test thread now gets its +own zeroed instance in test builds, with zero change to non-test builds. + +New gate: `crates/perry-runtime/src/gc/tests/lazy_intrinsic_towers.rs`, two +tests each asserting both halves of "the arming was live" — no collection +during the builder call, and the same armed request serviced once the window +closes. Sabotage-verified: commenting out either `GcSuppressScope::new()` +line turns its test red. + +**#7254 — `PERRY_GC_ZEAL` + `PERRY_GC_VERIFY_EVACUATION`, exercised in CI for +the first time.** Reproduced 3/3 on current `main`; liveness confirmed via +`PERRY_GC_DIAG=1` (14,072 copying-minor runs, 10,775 objects copied). The +panic surface has moved since the issue was filed — now `"stale forwarded +pointer in native stack-map roots"`, not `"shadow stack roots"` — because +the statepoint/stack-map native root walker +(`gc/roots/stack_maps.rs::visit_stack_map_root_slots`, layer 2 in +`docs/src/internals/rfc-rooting-by-construction.md`) is live on this +aarch64 host; this is now a layer-2 defect, not layer-3 as previously +classified. + +Sized the population across all 59 `test-parity/gc_repsel_corpus.txt` +files under the exact pairing: 20 PASS, 3 correctly INERT (zeal's own +exit-70 self-check), 3 confirmed panics with the identical signature +(`test_gap_repsel_p4a_inline_tiers`, `test_gap_repsel_p4a3_ptr_numarray` — +the filed reproducer, `test_gap_repsel_element_shape_loop_clone`), and 33 +timeouts under a 40s budget. The timeouts are not trusted as a defect +finding here: the sweep ran under sustained host load of 30-55 (20 +concurrent users), which makes host contention vs. ZEAL's legitimate +per-poll collection cost vs. a genuine second defect undecidable from this +data alone — flagged as follow-up work needing a quiet host, not asserted +as a finding. + +The defect itself is NOT fixed in this PR — it is a statepoint/native-root +liveness bug, the class this project's own GC campaigns +(`#7154`/`#7341`) treat as needing a disassembly-driven investigation +before a confident fix, not something to rush (see #7211's cautionary +precedent: an author actively thinking about rooting still shipped a wrong +predicate). Gated instead: extended `scripts/gc_instrument_smoke.sh` +(already wired into the required-ish `gc-stress` per-PR step) with a new +arm 5 — the pairing on the script's existing small fixture (proves +non-vacuity and no false positive on known-good code, `copied_objects>0` +under the verifier) plus a pinned-regression witness against +`test_gap_repsel_p4a3_ptr_numarray` that fails loudly if the panic ever +stops reproducing, whether from a fix (promote the file, delete the pin) +or a silent change in failure shape (needs a fresh look). + +Deliberately not routed through `gc_repsel_matrix.sh`'s arm registry: a +registered arm joins every corpus file via `--arms all` on push/schedule, +and the unsized 33-file timeout population makes that irresponsible within +the existing 90-minute job budget today. diff --git a/crates/perry-runtime/src/gc/tests/lazy_intrinsic_towers.rs b/crates/perry-runtime/src/gc/tests/lazy_intrinsic_towers.rs new file mode 100644 index 0000000000..810a48f076 --- /dev/null +++ b/crates/perry-runtime/src/gc/tests/lazy_intrinsic_towers.rs @@ -0,0 +1,291 @@ +//! #7251: the two lazy intrinsic-tower builders need #7217's NO-MOVE WINDOW, +//! and #7217 itself could not gate them. +//! +//! `object::global_this::generator::build_generator_tower` and +//! `object::global_this::typed_array::ensure_typed_array_intrinsic` have the +//! identical shape #7217 fixed for `populate_global_this_builtins`: each +//! builds an IMMORTAL object graph (hanging off a process-global intrinsic +//! slot for the life of the thread) by threading raw `*mut ObjectHeader` / +//! `*mut ClosureHeader` locals across a dozen-plus allocating installs. A +//! relocating — or even a non-relocating, freeing — collection reached from +//! one of THEIR OWN allocations leaves the rest of the build writing through +//! a dangling or from-space address: none of those locals is a root the +//! collector knows about, so a mark-sweep that runs before they are stored +//! into the (rooted) AtomicI64 slots considers them garbage. Same invariant, +//! same fix (`crate::gc::GcSuppressScope`), applied at the top of each. +//! +//! Both are reachable ahead of `populate_global_this_builtins` on the +//! allocation-point route: `generator_prototype_ptr` / +//! `generator_function_prototype_of` are called from the codegen-emitted +//! `js_generator_attach_prototype` / `js_generator_attach_closure_prototype` +//! helpers on literally the first `gen()` call in a program that has not yet +//! touched `globalThis`, and `ensure_typed_array_intrinsic` is reachable the +//! same way from `typedarray_props.rs:812`. +//! +//! # Why #7217 could not gate this directly, and what changed +//! +//! Filed rather than shipped ungated, per CLAUDE.md's knob-kill policy. Four +//! attempts at a gate — the three the issue records, plus one made here — +//! all passed with the window deleted before this shape was found. +//! +//! 1. **Arm a collection and let ordinary allocation reach it**, the way +//! `global_bootstrap.rs` does for the ~1.15 MB `populate_global_this_builtins` +//! bootstrap. A tower is three orders of magnitude smaller and fits inside +//! one arena block's tail, so it can reach `gc_check_trigger()` not at all — +//! the test measured nothing. FIXED HERE by +//! [`force_next_general_arena_alloc_slow`]: pre-fill the current block to +//! (very nearly) full before calling the builder, so the builder's own +//! first allocation unconditionally takes the slow path that calls +//! `gc_check_trigger()`, regardless of how small the tower is. This is the +//! same lever `gc::tests::runtime_roots::generator_attach_prototype`'s +//! #7577 gate uses to land a collection inside a specific callee's own +//! allocation. +//! +//! 2. **Re-exec the test binary so the child is the first to touch a tower.** +//! The six intrinsic-tower statics were plain process-global `AtomicI64`s, +//! built exactly once per *process* — so whichever test happened to run +//! first on that binary (libtest's ordering is not something a single test +//! controls) built them for every other test, including a freshly +//! re-exec'd child, because OTHER tests in the SAME binary run before this +//! one and touch `globalThis`. FIXED HERE by converting the six statics +//! (`TYPED_ARRAY_INTRINSIC_PTR`, `TYPED_ARRAY_INTRINSIC_PROTO_PTR`, +//! `GENERATOR_FUNCTION_INTRINSIC_PTR`, `GENERATOR_INTRINSIC_PROTO_PTR`, +//! `GENERATOR_PROTOTYPE_PTR`, `ASYNC_GENERATOR_FUNCTION_INTRINSIC_PTR`, +//! `ASYNC_GENERATOR_INTRINSIC_PROTO_PTR`, `ASYNC_GENERATOR_PROTOTYPE_PTR`) +//! in `object/mod.rs` from a bare `static` to `per_test_global!` — the +//! exact mechanism #7672 built for this class of hazard (see +//! `per_test_global.rs`'s module docs). In a test build each libtest +//! THREAD gets its own zeroed instance, and libtest runs one thread per +//! test, so "per thread" and "per test" coincide: this test's first read +//! of `GENERATOR_FUNCTION_INTRINSIC_PTR` is guaranteed `0` regardless of +//! what any other test in the binary did. Non-test builds expand to the +//! identical plain `static` — zero behavioural change in shipped binaries. +//! +//! 3. **Record `gc_is_suppressed()` from inside the builder under +//! `#[cfg(test)]`.** Reported *suppressed* even with the scope removed, +//! which turned out to be attempt 2's ordering hazard again in a different +//! guise: `populate_global_this_builtins` itself opens a `GcSuppressScope` +//! and calls both builders INSIDE it, so a probe that let ANY earlier test +//! reach `js_get_global_this()` first would forever after read `true`. +//! This test structurally avoids that by calling the tower builder +//! directly, never through `populate_global_this_builtins` / +//! `js_get_global_this()` — there is no outer window to be confused with. +//! +//! 4. **Arm the ArenaBytes/malloc byte-threshold trigger** (`GC_NEXT_TRIGGER_BYTES` +//! via `GcTriggerThresholdTestGuard::make_arena_trigger_due`, the lever +//! `gc::tests::runtime_roots::generator_attach_prototype` uses) under +//! `force_legacy_gc_pacing`. **This PASSED with the scope removed, on the +//! first draft of this very file, and would have shipped a vacuous gate.** +//! The reason: `gc_check_trigger`'s nursery-churn branch only runs a +//! synchronous, completes-on-the-spot collection when +//! `gc_scavenge_enabled() || gc_moving_loop_polls_enabled() || +//! registered_root_scanners_block_budgeted_gc()` — with legacy pacing +//! (scavenge and polls both off) and no scanner registered, an ArenaBytes +//! trigger being "due" just starts a *budgeted* cycle that needs an +//! explicit `gc_runtime_safepoint()` pump to advance. Nothing pumps one +//! inline, so `gc_collection_count()` never moves whether or not the +//! builder is suppressed — the assertion below would have been comparing +//! two numbers that could never differ. FIXED HERE by arming +//! `GC_OLD_RECLAIM_PENDING` instead (exactly `global_bootstrap.rs`'s own +//! lever): the `OldReclaim` branch in `gc_check_trigger` has no such +//! gating — `!gc_budgeted_cycle_active() && +//! matches!(gc_budgeted_due_trigger(), Some(OldReclaim)) && +//! !GC_OLD_RECLAIM_IN_PROGRESS` — and calls +//! `gc_collect_full_mark_sweep_with_trigger` SYNCHRONOUSLY the moment it is +//! reached, under any pacing. Combined with attempt 1's block-filling (so +//! the tiny tower's first allocation reaches `gc_check_trigger` AT ALL), +//! this is the first version verified — by actually deleting the fix and +//! watching the test go red — to test anything. +//! +//! # Both halves are asserted (CLAUDE.md's fourth way a gate cannot fail) +//! +//! Each test below arms ONE pending collection, shows the builder does not +//! service it, and then shows the SAME armed request IS serviced by ordinary +//! allocation once the builder has returned — proving the arming was live +//! rather than merely absent, exactly as `global_bootstrap.rs` does for +//! `populate_global_this_builtins`. +//! +//! **SABOTAGE, run by hand and not merely asserted:** commenting out either +//! `let _no_move = crate::gc::GcSuppressScope::new();` line (generator.rs:602, +//! typed_array.rs:436) turns its test red — +//! `a collection ran inside build_generator_tower / ensure_typed_array_intrinsic` +//! — confirming attempt 4's mechanism actually observes the fix, unlike +//! attempts 1-4's own false starts above. + +use super::super::*; +use super::support::*; +use std::sync::atomic::Ordering as StdOrdering; + +/// Run `body` on a thread that has touched neither `globalThis` nor either +/// tower, mirroring `global_bootstrap.rs::on_a_fresh_thread`. Belt-and-braces +/// alongside the `per_test_global!` conversion above: libtest already gives +/// each `#[test]` its own thread (and therefore its own instance of every +/// `per_test_global!` table), but spawning explicitly keeps this test's +/// guarantee independent of that harness detail. +fn on_a_fresh_thread(body: impl FnOnce() + Send + 'static) { + std::thread::Builder::new() + .stack_size(16 << 20) + .spawn(body) + .expect("spawn tower test thread") + .join() + .expect("tower test thread panicked"); +} + +/// Force the CURRENT arena block to (very nearly) full, so the very next +/// allocation takes `arena_cell_alloc`'s slow path and reaches +/// `gc_check_trigger()`. A local copy of +/// `gc::tests::runtime_roots::force_next_general_arena_alloc_slow`: that one +/// is private to a sibling module, and the two helpers have diverged reasons +/// for existing (#7577's lands a collection at ONE specific callee's own +/// allocation deep in a call chain; this one exists so a tower three orders +/// of magnitude smaller than a block still reaches a trigger check on its +/// very FIRST allocation, covering the whole call). +fn force_next_general_arena_alloc_slow() { + const TEST_BLOCK_SIZE: usize = 1024 * 1024; + let _ = crate::arena::arena_alloc(TEST_BLOCK_SIZE, 8); +} + +/// Make one collection due at the very next `gc_check_trigger()`, via the +/// `OldReclaim` branch — the only one `gc_check_trigger` services +/// synchronously regardless of scavenge/polls/scanner pacing (see attempt 4 +/// above). Identical lever to `global_bootstrap.rs::arm_one_pending_collection`. +fn arm_one_pending_collection() { + GC_OLD_RECLAIM_PENDING.with(|pending| pending.set(true)); +} + +fn pending_collection_still_owed() -> bool { + GC_OLD_RECLAIM_PENDING.with(std::cell::Cell::get) +} + +/// Arm the OldReclaim request AND guarantee the very next allocation reaches +/// the check that services it, however small the allocator that follows is. +fn arm_collection_reachable_by_next_allocation() { + force_next_general_arena_alloc_slow(); + arm_one_pending_collection(); +} + +/// THE CONTROL, shared by both towers below — identical shape to +/// `global_bootstrap.rs`'s `ordinary_allocation_services_the_armed_collection`. +/// Forces one more slow-path allocation (so the still-armed `OldReclaim` +/// request is reached immediately rather than waiting on however much of the +/// current block happens to be free) and asserts it was serviced. `false` +/// means the arming was inert on this thread, and the "did not collect" +/// assertion above it proved nothing. +fn armed_collection_is_serviced_by_the_next_allocation(collections_before: u64) -> bool { + force_next_general_arena_alloc_slow(); + gc_collection_count() > collections_before +} + +#[test] +fn generator_tower_runs_in_a_no_move_window() { + on_a_fresh_thread(|| { + let _pacing = crate::gc::policy::force_legacy_gc_pacing(); + crate::gc::ensure_gc_initialized(); + // LIVE SUBJECT, precondition: this really is the first touch. Without + // this a stray earlier build (attempts 2/3 above) would silently + // early-return from `ensure_generator_intrinsics` and this test would + // measure a no-op. + assert_eq!( + crate::object::GENERATOR_FUNCTION_INTRINSIC_PTR.load(StdOrdering::Acquire), + 0, + "GENERATOR_FUNCTION_INTRINSIC_PTR was already built before this \ + test's first touch — the per-test-global isolation (#7251/#7672) \ + did not give this thread a fresh tower, so this test proved \ + nothing" + ); + + arm_collection_reachable_by_next_allocation(); + let collections_before = gc_collection_count(); + + // THE SUBJECT: the lazy generator/async-generator tower build. + crate::object::ensure_generator_intrinsics(); + + // LIVE SUBJECT, half 1: the tower really built (both towers — sync + // and async each call `build_generator_tower` once). + assert_ne!( + crate::object::GENERATOR_FUNCTION_INTRINSIC_PTR.load(StdOrdering::Acquire), + 0, + "the tower did not build at all, so this test measured nothing" + ); + assert_ne!( + crate::object::ASYNC_GENERATOR_FUNCTION_INTRINSIC_PTR.load(StdOrdering::Acquire), + 0, + "the async tower did not build at all, so this test measured \ + nothing" + ); + + let collections_after = gc_collection_count(); + // THE INVARIANT: nothing collected while `ctor`/`proto`/`gen_proto` + // were raw, un-rewritable locals. + assert_eq!( + collections_after, collections_before, + "a collection ran inside `build_generator_tower` — its `ctor` / \ + `proto` / `gen_proto` locals are now dangling or from-space" + ); + assert!( + pending_collection_still_owed(), + "the window must DEFER the request, not drop it: leaving it \ + unserviced-and-unset would disable the trigger for the rest of \ + the thread" + ); + assert!( + !crate::gc::gc_is_suppressed(), + "the no-move window must close when the tower builder returns" + ); + + // LIVE SUBJECT, half 2 — THE CONTROL. + assert!( + armed_collection_is_serviced_by_the_next_allocation(collections_after), + "the armed collection was never serviceable on this thread, so \ + 'the tower build did not collect' proved nothing" + ); + }); +} + +#[test] +fn typed_array_intrinsic_tower_runs_in_a_no_move_window() { + on_a_fresh_thread(|| { + let _pacing = crate::gc::policy::force_legacy_gc_pacing(); + crate::gc::ensure_gc_initialized(); + assert_eq!( + crate::object::TYPED_ARRAY_INTRINSIC_PTR.load(StdOrdering::Acquire), + 0, + "TYPED_ARRAY_INTRINSIC_PTR was already built before this test's \ + first touch — the per-test-global isolation (#7251/#7672) did \ + not give this thread a fresh tower, so this test proved nothing" + ); + + arm_collection_reachable_by_next_allocation(); + let collections_before = gc_collection_count(); + + // THE SUBJECT. + let (ctor, proto) = crate::object::ensure_typed_array_intrinsic(); + + assert!( + !ctor.is_null() && !proto.is_null(), + "the intrinsic did not build at all, so this test measured \ + nothing" + ); + + let collections_after = gc_collection_count(); + assert_eq!( + collections_after, collections_before, + "a collection ran inside `ensure_typed_array_intrinsic` — its \ + `ctor` / `proto` locals are now dangling or from-space" + ); + assert!( + pending_collection_still_owed(), + "the window must DEFER the request, not drop it" + ); + assert!( + !crate::gc::gc_is_suppressed(), + "the no-move window must close when the builder returns" + ); + + assert!( + armed_collection_is_serviced_by_the_next_allocation(collections_after), + "the armed collection was never serviceable on this thread, so \ + 'the builder did not collect' proved nothing" + ); + }); +} diff --git a/crates/perry-runtime/src/gc/tests/mod.rs b/crates/perry-runtime/src/gc/tests/mod.rs index f33275515b..004304f462 100644 --- a/crates/perry-runtime/src/gc/tests/mod.rs +++ b/crates/perry-runtime/src/gc/tests/mod.rs @@ -24,6 +24,7 @@ mod inline_generation_gate_contract; mod inline_pointer_bearing_contract; mod layout_pointer_free_hazard; mod layout_trace; +mod lazy_intrinsic_towers; mod lazy_tape_side_alloc; mod oldgen; mod os_tag; diff --git a/crates/perry-runtime/src/object/global_this/generator.rs b/crates/perry-runtime/src/object/global_this/generator.rs index 749ba88ddb..3a8a9a121f 100644 --- a/crates/perry-runtime/src/object/global_this/generator.rs +++ b/crates/perry-runtime/src/object/global_this/generator.rs @@ -577,6 +577,29 @@ fn build_generator_tower( proto_slot: &std::sync::atomic::AtomicI64, gen_proto_slot: &std::sync::atomic::AtomicI64, ) { + // #7251: this builds an IMMORTAL object graph (the tower hangs off a + // process-global intrinsic slot for the life of the thread) by threading + // `ctor`/`proto`/`gen_proto` as raw `*mut ObjectHeader` / `*mut + // ClosureHeader` locals across a dozen-plus allocating installs below. + // None of those locals is a slot the collector rewrites, so a relocating + // collection reached from one of this function's own allocations would + // leave the rest of the build writing through from-space addresses — + // exactly the invariant #7217 fixed for `populate_global_this_builtins` + // ("a bootstrap that builds an IMMORTAL object graph through raw pointers + // held across its own allocations must run in a NO-MOVE WINDOW"). + // + // On the SAFEPOINT route this was unreachable: a back-edge poll only + // fires while user JS runs, and this function runs none. It IS reachable + // on the allocation-point route — this tower is lazily built ahead of + // `populate_global_this_builtins` by `generator_prototype_ptr` / + // `generator_function_prototype_of`, called from the codegen-emitted + // `js_generator_attach_prototype` / `js_generator_attach_closure_prototype` + // helpers on literally the FIRST `gen()` call in a program that has not + // yet touched `globalThis`. See `gc::tests::lazy_intrinsic_towers` for the + // gate: it arms a collection at the very next arena-block-crossing + // allocation (this tower is far smaller than a block, so it cannot arm + // one on its own) and asserts this function does not service it. + let _no_move = crate::gc::GcSuppressScope::new(); let (ctor_name, ctor_tag, inst_tag) = if is_async { ( "AsyncGeneratorFunction", diff --git a/crates/perry-runtime/src/object/global_this/typed_array.rs b/crates/perry-runtime/src/object/global_this/typed_array.rs index 94eda65c2a..c573c2acb0 100644 --- a/crates/perry-runtime/src/object/global_this/typed_array.rs +++ b/crates/perry-runtime/src/object/global_this/typed_array.rs @@ -425,6 +425,15 @@ pub(crate) fn ensure_typed_array_intrinsic( existing_proto as *mut ObjectHeader, ); } + // #7251: same invariant and same hazard as `build_generator_tower` — this + // threads `ctor`/`proto` as raw pointers across ~a dozen allocating + // installs (accessors, to-string-tag, the 44-method proto-methods table, + // static `from`/`of`) to build an immortal graph that hangs off + // `TYPED_ARRAY_INTRINSIC_PTR`/`_PROTO_PTR` for the life of the thread. + // Reachable ahead of `populate_global_this_builtins` from + // `typedarray_props.rs:812` on the allocation-point route. See + // `gc::tests::lazy_intrinsic_towers` for the gate. + let _no_move = crate::gc::GcSuppressScope::new(); let ctor = crate::closure::js_closure_alloc(typed_array_constructor_call_thunk as *const u8, 0); let proto = js_object_alloc(0, 0); if ctor.is_null() || proto.is_null() { diff --git a/crates/perry-runtime/src/object/mod.rs b/crates/perry-runtime/src/object/mod.rs index f5cdb6cc85..bedd339485 100644 --- a/crates/perry-runtime/src/object/mod.rs +++ b/crates/perry-runtime/src/object/mod.rs @@ -217,25 +217,39 @@ per_test_global! { static OS_CONSTANTS_DLOPEN_CACHE: AtomicU64 = AtomicU64::new(0); static GLOBAL_THIS_PTR: AtomicI64 = AtomicI64::new(0); static GLOBAL_THIS_READY: AtomicBool = AtomicBool::new(false); + // `%TypedArray%` intrinsic constructor/prototype roots used by per-kind + // typed array constructors and scanned by `scan_object_cache_roots_mut`. + pub(crate) static TYPED_ARRAY_INTRINSIC_PTR: AtomicI64 = AtomicI64::new(0); + pub(crate) static TYPED_ARRAY_INTRINSIC_PROTO_PTR: AtomicI64 = AtomicI64::new(0); + // #3664: the generator / async-generator intrinsic prototype towers. + // `*_FUNCTION_INTRINSIC_PTR` = `%GeneratorFunction%` / `%AsyncGeneratorFunction%` + // (the constructor closures); `*_INTRINSIC_PROTO_PTR` = `%Generator%` / + // `%AsyncGenerator%` (a.k.a. `.prototype`), the object + // `Object.getPrototypeOf(function*(){})` resolves to; `*_PROTOTYPE_PTR` = + // `%Generator.prototype%` / `%AsyncGenerator.prototype%` (a.k.a. + // `.prototype.prototype`), carrying `next`/`return`/`throw`. All six are + // GC roots scanned by `scan_object_cache_roots_mut`. + // + // #7251: these six are `per_test_global!` (not a bare `static`) SPECIFICALLY + // so a test can observe "this tower has never been built" reliably. Before + // this they were plain process-global `AtomicI64`s, built exactly once per + // *process* — so whichever test happened to run first (order is + // libtest-nondeterministic) built them for every OTHER test on the same + // binary, and a gate trying to arm a collection around + // `ensure_generator_intrinsics()` / `ensure_typed_array_intrinsic()` found + // the tower already cached and measured nothing (see #7251's second and + // third failed gate attempts). `per_test_global!` gives each libtest THREAD + // — and libtest runs one thread per test — its own zeroed instance, so + // `crates/perry-runtime/src/gc/tests/lazy_intrinsic_towers.rs` sees a + // guaranteed-first-touch tower with no dependence on test execution order. + // Non-test builds expand to the identical plain `static` this replaced. + pub(crate) static GENERATOR_FUNCTION_INTRINSIC_PTR: AtomicI64 = AtomicI64::new(0); + pub(crate) static GENERATOR_INTRINSIC_PROTO_PTR: AtomicI64 = AtomicI64::new(0); + pub(crate) static GENERATOR_PROTOTYPE_PTR: AtomicI64 = AtomicI64::new(0); + pub(crate) static ASYNC_GENERATOR_FUNCTION_INTRINSIC_PTR: AtomicI64 = AtomicI64::new(0); + pub(crate) static ASYNC_GENERATOR_INTRINSIC_PROTO_PTR: AtomicI64 = AtomicI64::new(0); + pub(crate) static ASYNC_GENERATOR_PROTOTYPE_PTR: AtomicI64 = AtomicI64::new(0); } -// `%TypedArray%` intrinsic constructor/prototype roots used by per-kind typed -// array constructors and scanned by `scan_object_cache_roots_mut`. -pub(crate) static TYPED_ARRAY_INTRINSIC_PTR: AtomicI64 = AtomicI64::new(0); -pub(crate) static TYPED_ARRAY_INTRINSIC_PROTO_PTR: AtomicI64 = AtomicI64::new(0); -// #3664: the generator / async-generator intrinsic prototype towers. -// `*_FUNCTION_INTRINSIC_PTR` = `%GeneratorFunction%` / `%AsyncGeneratorFunction%` -// (the constructor closures); `*_INTRINSIC_PROTO_PTR` = `%Generator%` / -// `%AsyncGenerator%` (a.k.a. `.prototype`), the object -// `Object.getPrototypeOf(function*(){})` resolves to; `*_PROTOTYPE_PTR` = -// `%Generator.prototype%` / `%AsyncGenerator.prototype%` (a.k.a. -// `.prototype.prototype`), carrying `next`/`return`/`throw`. All six are -// GC roots scanned by `scan_object_cache_roots_mut`. -pub(crate) static GENERATOR_FUNCTION_INTRINSIC_PTR: AtomicI64 = AtomicI64::new(0); -pub(crate) static GENERATOR_INTRINSIC_PROTO_PTR: AtomicI64 = AtomicI64::new(0); -pub(crate) static GENERATOR_PROTOTYPE_PTR: AtomicI64 = AtomicI64::new(0); -pub(crate) static ASYNC_GENERATOR_FUNCTION_INTRINSIC_PTR: AtomicI64 = AtomicI64::new(0); -pub(crate) static ASYNC_GENERATOR_INTRINSIC_PROTO_PTR: AtomicI64 = AtomicI64::new(0); -pub(crate) static ASYNC_GENERATOR_PROTOTYPE_PTR: AtomicI64 = AtomicI64::new(0); pub(crate) static LOCAL_STORAGE_PTR: AtomicI64 = AtomicI64::new(0); pub(crate) static SESSION_STORAGE_PTR: AtomicI64 = AtomicI64::new(0); diff --git a/scripts/gc_instrument_smoke.sh b/scripts/gc_instrument_smoke.sh index ded4ee273e..93ff9e17a0 100755 --- a/scripts/gc_instrument_smoke.sh +++ b/scripts/gc_instrument_smoke.sh @@ -185,10 +185,104 @@ if [[ -d "$PROBES" ]]; then echo "FAIL: $probe_failed/$probe_count probes faulted under the quarantine." >&2 exit 1 fi - echo " $probe_count/$probe_count probes clean under from-space quarantine" + echo " $probe_count/$probe_count probes clean over from-space quarantine" fi +# ---- arm 5: PERRY_GC_ZEAL + PERRY_GC_VERIFY_EVACUATION, #7254's pairing ---- +# +# Both knobs are individually exercised above (ZEAL by arms 2/3, +# PERRY_GC_VERIFY_EVACUATION nowhere in this script) and in +# gc_repsel_matrix.sh (VERIFY_EVACUATION by `verify_evac`/`force_verify`, +# ZEAL nowhere in that script either) -- but no CI arm anywhere sets them +# TOGETHER, which is exactly the CLAUDE.md knob-kill-policy hole #7254 found: +# the pair panics 10/10 on `test_gap_repsel_p4a3_ptr_numarray` +# (`gc evacuation verification failed: stale forwarded pointer in ...`) and +# nothing in CI would have said a word. +# +# Deliberately NOT routed through gc_repsel_matrix.sh: a `zeal_verify` arm +# registered there joins EVERY corpus file via `--arms all`, and #7254's own +# sizing sweep (59 files) found a striking concentration of multi-minute-plus +# runs under this exact pairing on the test_gap_gc_* reproducer corpus -- +# ZEAL forces a full evacuating minor at EVERY back-edge poll, which no other +# matrix arm does, so a corpus built for arms that collect only when a real +# trigger fires is not this pairing's natural home. That population is not +# yet triaged (host contention during the investigation made timeout vs. +# genuine-cost vs. host-noise undecidable) and is out of scope for this fix; +# see #7254 for the follow-up. This arm stays small and bounded instead: the +# same tiny fixture arms 1-3 already use (proves the pairing is non-vacuous +# and produces no false positive on known-good code), plus ONE pinned +# regression witness against the exact file and exact panic #7254 reports. +echo +echo "== arm 5: PERRY_GC_ZEAL + PERRY_GC_VERIFY_EVACUATION (#7254's pairing) ==" + +zeal_verify_env=(PERRY_GC_HEAP_LIMIT=8 PERRY_GC_INCREMENTAL=0 PERRY_CONSERVATIVE_STACK_SCAN=off + PERRY_GC_MOVING_LOOP_POLLS=1 PERRY_GC_ZEAL=1 PERRY_GC_VERIFY_EVACUATION=1 PERRY_GC_DIAG=1) + +echo "-- 5a: the pairing on known-good code must stay clean, and must be LIVE --" +set +e +fixture_out="$(env "${zeal_verify_env[@]}" "$WORK/fixture" 2>&1)" +fixture_rc=$? +set -e +if [[ $fixture_rc -ne 0 ]]; then + echo "FAIL [arm5a]: the pairing crashed known-good code, exited $fixture_rc:" >&2 + echo "$fixture_out" | tail -30 >&2 + exit 1 +fi +if ! grep -q '^bad 0$' <<<"$fixture_out"; then + echo "FAIL [arm5a]: expected 'bad 0' under the pairing, got:" >&2 + grep '^bad' <<<"$fixture_out" >&2 || echo "(no 'bad' line)" >&2 + exit 1 +fi +fixture_copied="$(grep -oE 'copied_objects=[0-9]+' <<<"$fixture_out" | grep -oE '[0-9]+$' | awk '{s+=$1} END {print s+0}')" +if [[ "$fixture_copied" -eq 0 ]]; then + echo "FAIL: arm 5's fixture copied ZERO objects under the pairing." >&2 + echo " A clean exit with no relocation proves nothing about the" >&2 + echo " verifier -- it never had a forwarded pointer to check." >&2 + exit 1 +fi +echo " correct output, exit 0, $fixture_copied objects copied under the verifier (live, no false positive)" + +echo "-- 5b: the pairing must still catch #7254's known reproducer --" +REPRO="$(dirname "$0")/../test-files/test_gap_repsel_p4a3_ptr_numarray.ts" +if [[ ! -f "$REPRO" ]]; then + echo "FAIL: #7254's reproducer is missing at $REPRO -- arm 5b has no subject." >&2 + exit 1 +fi +PERRY_GC_MOVING_LOOP_POLLS=1 "$PERRY_BIN" compile "$REPRO" -o "$WORK/repro7254" >/dev/null +set +e +repro_out="$(env "${zeal_verify_env[@]}" "$WORK/repro7254" 2>&1)" +repro_rc=$? +set -e +# PINNED REGRESSION, not a correctness assertion: #7254 is a real, open, +# pre-existing defect (confirmed 3/3 in this investigation, and previously +# 10/10). Asserting it panics -- rather than skipping it -- is what makes +# this arm a GATE instead of documentation: if this ever stops panicking, it +# means either the bug got fixed (delete this block and add the file to a +# normal correctness arm) or the failure mode silently changed shape (which +# needs a look before anyone trusts that as a fix). Either way the gate +# should say something, not stay quiet. +if [[ $repro_rc -eq 0 ]]; then + echo "FAIL: #7254's reproducer no longer panics under the pairing (exit 0)." >&2 + echo " If this is because the underlying stale-forwarded-pointer bug" >&2 + echo " was fixed: great -- delete this pinned-regression block (arm" >&2 + echo " 5b) and let the file run under the matrix's ordinary arms" >&2 + echo " instead. If nothing GC-related changed, this is itself a" >&2 + echo " regression report: something now hides the defect without" >&2 + echo " fixing it (e.g. the verifier stopped seeing the stale slot)." >&2 + exit 1 +fi +if ! grep -q 'stale forwarded pointer' <<<"$repro_out"; then + echo "FAIL: #7254's reproducer failed a NEW way under the pairing (exit $repro_rc):" >&2 + echo "$repro_out" | tail -20 >&2 + echo " Expected the pinned 'stale forwarded pointer' verifier panic." >&2 + echo " A different failure mode needs its own triage, not silence." >&2 + exit 1 +fi +echo " reproduced as pinned (exit $repro_rc, stale forwarded pointer) -- #7254 still open, tracked not silent" + echo echo "PASS: instruments inert when off (0 retirements), live when on" echo " (no-zeal=$nozeal_retired, zeal=$zeal_retired retirements), program correct in all arms." echo " Quarantine clean over $probe_count real probes (allocation-point route)." +echo " ZEAL+VERIFY_EVACUATION pairing live and correct on known-good code," +echo " and still pins #7254's open reproducer rather than staying silent about it."