Skip to content

gc: no-move window for lazy intrinsic towers (#7251) + CI arm for ZEAL+VERIFY_EVACUATION (#7254) - #7723

Merged
proggeramlug merged 4 commits into
mainfrom
gc/7251-7254-gate-arms
Aug 9, 2026
Merged

gc: no-move window for lazy intrinsic towers (#7251) + CI arm for ZEAL+VERIFY_EVACUATION (#7254)#7723
proggeramlug merged 4 commits into
mainfrom
gc/7251-7254-gate-arms

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Two GC issues, both "a real hazard exists and nothing exercises it" — closes #7251, closes #7254.

#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 GcSuppressScope for the whole build, matching #7217's fix exactly.

The issue's own blocker was the gate, not the fix — three prior attempts all passed with the window deleted:

  1. Arm-and-let-ordinary-allocation-reach-it (the populate_global_this_builtins gate's own approach) never reaches gc_check_trigger() at all: a tower is three orders of magnitude smaller than an arena block. Fixed by forcing the current block (very nearly) full before calling the builder, so its first allocation unconditionally takes the slow path.
  2. Re-exec the test binary still isn't first-touch, because the six intrinsic-tower statics (TYPED_ARRAY_INTRINSIC_PTR, GENERATOR_FUNCTION_INTRINSIC_PTR, …) were plain process-global AtomicI64s — built once per process, so whichever test ran first on that binary poisoned every later test's "never built" precondition. Fixed by converting them to per_test_global! (the mechanism Process-global side tables are cleared by test guards that readers are not required to take — three flakes in two days #7672 built for exactly this ordering hazard): each libtest thread gets its own zeroed instance, and non-test builds expand to the identical bare static.
  3. Recording gc_is_suppressed() from inside the builder read true even with the scope removed, because it was leaking attempt 2's ordering hazard in a different shape (an earlier test's populate_global_this_builtins call opens its own outer window around both builders). Sidestepped by calling the tower builders directly, never through the bootstrap.
  4. (Found while building this PR.) A first draft armed the ArenaBytes/malloc byte-threshold trigger under force_legacy_gc_pacing, and it also passed with the fix deleted — vacuously. Under that pacing, an ArenaBytes trigger being "due" only starts a budgeted cycle that needs an explicit host-safepoint pump to advance; nothing pumps one inline, so gc_collection_count() never moves whether or not the builder is suppressed. Fixed by arming GC_OLD_RECLAIM_PENDING instead — the one branch gc_check_trigger services synchronously regardless of pacing, and the exact lever global_bootstrap.rs's own GC: #7207's spread-accessor reproducer still SIGSEGVs on main under the evac_minor arm (allocation-point relocation, not the safepoint route) #7217 gate uses.

New test: crates/perry-runtime/src/gc/tests/lazy_intrinsic_towers.rs, two tests (generator_tower_runs_in_a_no_move_window, typed_array_intrinsic_tower_runs_in_a_no_move_window), each asserting both halves (CLAUDE.md's fourth way a gate can't fail): no collection ran during the builder call, AND the same armed request is serviced once the window closes.

Sabotage, run by hand: commenting out either GcSuppressScope::new() line turns its test red (a collection ran inside …), confirmed on this branch after the pacing fix (attempt 4's false start would not have caught it — verified that too, by running the vacuous first draft against the sabotaged tree and watching it pass).

#7254 — PERRY_GC_ZEAL + PERRY_GC_VERIFY_EVACUATION, exercised in CI for the first time

Reproduced, 3/3, on current main (post-#7719). Repro exactly as filed:

PERRY_GC_MOVING_LOOP_POLLS=1 perry test-files/test_gap_repsel_p4a3_ptr_numarray.ts -o bin
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 ./bin

Liveness proven via PERRY_GC_DIAG=1, not merely a clean exit: 14,072 copying-minor runs, 10,775 objects actually copied across the run. The instrument was genuinely armed.

New finding, not in the issue: the panic surface has moved. It now reads "stale forwarded pointer in native stack-map roots", not "shadow stack roots". gc/roots/stack_maps.rs's visit_stack_map_root_slots is the statepoint/stack-map native root walker (docs/src/internals/rfc-rooting-by-construction.md's layer 2), which is live on this aarch64 host. So on current main this is a layer-2 (emitted-code-liveness) defect, not layer-3 (hand-written runtime Rust) as the issue's own comment classified it — the classification predates the statepoint route becoming reachable here. Filing this as new evidence on #7254 rather than silently changing the issue's framing.

Sized the population (the issue's own suggested first step), all 59 test-parity/gc_repsel_corpus.txt files under the exact pairing:

result count detail
PASS 20 byte-correct, no panic
INERT (zeal's own exit-70 verdict) 3 test_gap_specabi_{reassign,recursion_escape,view_detach} — zeal correctly self-reported no collection ran; not a defect
FAIL — confirmed panic 3 test_gap_repsel_p4a_inline_tiers, test_gap_repsel_p4a3_ptr_numarray (the filed repro), test_gap_repsel_element_shape_loop_clone — all three the identical native stack-map roots signature
TIMEOUT (40s budget) 33 see caveat below

Caveat on the 33 timeouts, stated plainly rather than folded into a claim: this sweep ran under sustained host load of 30–55 (20 concurrent users, confirmed via uptime throughout, not a transient spike) and competed with this same PR's own concurrent rebuilds. I cannot currently separate "host contention" from "ZEAL forcing an evacuating minor at every back-edge poll is just legitimately much more expensive than any other matrix arm on tight-loop reproducers" from "a genuine second defect" for those 33 files. The concentration is suspicious — it is almost the entire test_gap_gc_* rooting-reproducer corpus specifically — so it is worth a dedicated follow-up on a quiet host with a longer per-file budget, but asserting more than that here would be exactly the overclaim CLAUDE.md's knob-kill policy warns about.

What's fixed vs. gated, and why: the panic itself is NOT fixed in this PR. This is a statepoint/native-root-walker liveness bug (layer 2), the same class CLAUDE.md documents as needing careful, disassembly-driven campaigns (the #7154/#7341 family) — not something to rush without conclusive root-causing, given the project's own cautionary precedent (#7211: an author actively thinking about rooting still shipped a wrong four-clause predicate). A wrong fix here risks masking a real bug rather than closing it. A gate that pins the defect is shipped instead, per the task's own explicit allowance: "a gate with no fix is still better than today's nothing."

Why not gc_repsel_matrix.sh: a zeal_verify arm registered there joins every corpus file via --arms all (nightly/push), and the 90-minute job budget cannot responsibly absorb an unsized population of multi-minute-plus cells discovered under contested conditions. Extended scripts/gc_instrument_smoke.sh instead (already wired into gc-stress's required-ish per-PR step) with a new arm 5, bounded and fast:

  • 5a — the pairing on the script's existing small fixture: correct output, exit 0, AND live (copied_objects>0 under the verifier) — proves the pairing is non-vacuous and produces no false positive on known-good code.
  • 5b — a pinned regression witness against test_gap_repsel_p4a3_ptr_numarray: asserts it panics with stale forwarded pointer, and fails loudly (with instructions) if that ever stops being true — either because the defect got fixed (delete the block, promote the file) or because the failure silently changed shape (needs a fresh look, not silence).

Verified locally end-to-end (./scripts/gc_instrument_smoke.sh <perry-dev-binary>): all 5 arms pass, including 5a (3,894 objects copied, live) and 5b (reproduces #7254 as pinned).

What this PR does NOT do

Test plan

  • crates/perry-runtime/src/gc/tests/lazy_intrinsic_towers.rs: 2/2 passing on this branch (rebased onto origin/main @ 72ef47a)
  • Sabotage: both tests turn red when their GcSuppressScope is removed (verified twice — once before the pacing-mechanism fix, which incorrectly stayed green, and once after, which correctly went red)
  • ./scripts/gc_instrument_smoke.sh <perry-dev binary>: all 5 arms pass locally, including the new gc: PERRY_GC_ZEAL + PERRY_GC_VERIFY_EVACUATION reports a stale forwarded pointer in shadow-stack roots (10/10, pre-existing, no CI arm sets the pair) #7254 pairing (5a non-vacuous + correct, 5b pins the known reproducer)
  • bash -n + shellcheck clean on the modified script
  • cargo check -p perry-runtime -p perry-runtime-static -p perry-stdlib -p perry-stdlib-static --tests: 0 errors
  • cargo fmt --all -- --check: clean
  • Full cargo test --release --workspace (not run in this session — scoped --lib runs above cover the changed surface; left for CI)

Summary by CodeRabbit

  • Bug Fixes

    • Improved garbage-collection safety while initializing generator and typed-array functionality.
    • Prevented unsafe object relocation during intrinsic setup.
    • Added safeguards and regression coverage for stale pointer detection under aggressive GC verification.
  • Tests

    • Added coverage for lazy intrinsic initialization and collection-trigger behavior.
    • Expanded GC smoke testing with evacuation verification scenarios.
  • Chores

    • Updated the project version to 0.5.1419.
    • Documented the latest GC-related fixes and test coverage.

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The runtime now protects lazy generator and typed-array intrinsic construction from moving GC, isolates intrinsic roots in tests, and adds GC-pressure and evacuation regression coverage. Workspace and documentation versions were updated to 0.5.1419.

Changes

GC intrinsic construction safety

Layer / File(s) Summary
Per-test intrinsic roots
crates/perry-runtime/src/object/mod.rs
Intrinsic pointer statics use per-test storage in test builds and ordinary static storage otherwise.
Intrinsic builder suppression
crates/perry-runtime/src/object/global_this/generator.rs, crates/perry-runtime/src/object/global_this/typed_array.rs
Both intrinsic builders establish GcSuppressScope while constructing raw-pointer object graphs.
GC-pressure construction tests
crates/perry-runtime/src/gc/tests/lazy_intrinsic_towers.rs, crates/perry-runtime/src/gc/tests/mod.rs, changelog.d/7723-gc-gate-arms.md
Tests arm synchronous OldReclaim collection, verify deferral during construction, and verify servicing after suppression ends.
GC instrumentation smoke coverage
scripts/gc_instrument_smoke.sh, changelog.d/7723-gc-gate-arms.md
A new smoke-test arm validates actual evacuation and requires the pinned stale-forwarded-pointer panic from the regression reproducer.

Release metadata

Layer / File(s) Summary
Version updates
Cargo.toml, CLAUDE.md
The workspace package version and documented current version changed from 0.5.1418 to 0.5.1419.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant TestThread
  participant IntrinsicBuilder
  participant GcSuppressScope
  participant gc_check_trigger
  participant OldReclaim
  TestThread->>IntrinsicBuilder: Build intrinsic tower
  IntrinsicBuilder->>GcSuppressScope: Enter no-move window
  IntrinsicBuilder->>gc_check_trigger: Trigger allocation check
  gc_check_trigger-->>OldReclaim: Keep collection pending
  IntrinsicBuilder->>GcSuppressScope: Close scope
  TestThread->>OldReclaim: Service collection on next allocation
Loading

Possibly related issues

  • PerryTS/perry issue 7251: Covers no-move windows and fail-capable GC tests for both lazy intrinsic-tower builders.

Possibly related PRs

  • PerryTS/perry#7249: Extends GcSuppressScope protection to lazy generator and typed-array intrinsic builders.
  • PerryTS/perry#7196: Adds related GC instrumentation and regression coverage.
  • PerryTS/perry#7338: Relates to forced-evacuation validation in scripts/gc_instrument_smoke.sh.

Suggested labels: bug

Suggested reviewers: jdalton, thehypnoo

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies both primary changes: no-move windows for intrinsic towers and the ZEAL plus evacuation verification CI arm.
Description check ✅ Passed The description is detailed and covers the changes, related issues, rationale, limitations, and test results, although some template headings are omitted.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch gc/7251-7254-gate-arms

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Ralph Küpper added 4 commits August 9, 2026 19:47
Wrap build_generator_tower and ensure_typed_array_intrinsic in
GcSuppressScope, matching #7217's fix for populate_global_this_builtins.
Convert the six intrinsic-tower AtomicI64 statics to per_test_global!
so a gate test can rely on first-touch state regardless of libtest's
run order (the ordering hazard that sank two of #7251's three prior
gate attempts). Add crates/perry-runtime/src/gc/tests/lazy_intrinsic_towers.rs.
#7254)

No CI arm previously set this pairing: gc_repsel_matrix.sh's verify_evac
never sets ZEAL, its zeal-adjacent arms never set VERIFY_EVACUATION, and
this script's own zeal arms never set VERIFY_EVACUATION either. Add arm 5:
the pairing on the existing small fixture (non-vacuity + no false positive
on known-good code) plus a pinned-regression witness against #7254's exact
open reproducer (test_gap_repsel_p4a3_ptr_numarray), which must keep
panicking with "stale forwarded pointer" until the underlying defect is
fixed, at which point this block is the thing to delete.

Not routed through gc_repsel_matrix.sh's arm registry: a sizing sweep of
all 59 gc_repsel_corpus.txt files under this exact pairing found 3
confirmed panics (same signature) but also a striking concentration of
multi-minute-plus runs on the test_gap_gc_* reproducer corpus -- ZEAL
forces an evacuating minor at every back-edge poll, which no other matrix
arm does. That population is not yet triaged (investigated under sustained
host load of 30-55, which makes timeout-vs-genuine-cost undecidable), so
this stays a small bounded script rather than joining --arms all today.
@proggeramlug
proggeramlug force-pushed the gc/7251-7254-gate-arms branch from 500b2aa to 82525e9 Compare August 9, 2026 17:47
@proggeramlug

Copy link
Copy Markdown
Contributor Author

Merging as v0.5.1419 — and the best thing here is the gate you threw away

You caught your own vacuous gate. The first draft armed the ArenaBytes trigger under force_legacy_gc_pacing and passed with the fix deleted — because that pacing never synchronously collects, so suppression could not matter either way. You noticed, discarded it, and rebuilt on the GC_OLD_RECLAIM_PENDING lever (global_bootstrap.rs's own #7217 mechanism), plus forcing an arena block full so the tiny tower's first allocation reaches gc_check_trigger() at all. Then re-verified in both directions: fix present → 2/2 pass, fix removed → 2/2 fail at the invariant assertion.

That is the single most common way a gate ships unable to fail, and it is the third time today a first draft of a check turned out to be satisfied by "nothing happened". Discarding it rather than shipping it is the right instinct.

#7251 — fixed and gated

GcSuppressScope added to build_generator_tower and ensure_typed_array_intrinsic, matching #7217. You also fixed the ordering hazard behind two of the three prior failed attempts: the six tower AtomicI64 statics are now per_test_global! (#7672's mechanism), so each libtest thread gets a guaranteed-fresh "never built" tower regardless of run order. That is why the earlier attempts kept failing for reasons unrelated to the bug.

#7254 — reproduced, gated rather than fixed, and the issue's own label is stale

Reproduced 3/3, with liveness proven rather than assumed: 14,072 copying-minor runs, 10,775 objects copied under PERRY_GC_DIAG=1.

The finding that isn't in the issue: the panic surface has moved. It now reads "stale forwarded pointer in native stack-map roots", not shadow-stack roots — gc/roots/stack_maps.rs's statepoint walker is live on this host, so the issue's "layer 3" classification is out of date. Anyone picking this up from the issue text alone would have looked in the wrong place.

Gating rather than fixing is the right call here — this is the #7154/#7341 statepoint-liveness class, and #7211 is a direct precedent against a rushed fix. Arm 5a proves the pairing is non-vacuous and false-positive-free on known-good code; 5b is a pinned regression witness against the exact reproducer that fails loudly if the panic ever stops (fixed → promote it; changed shape → investigate). All 5 arms verified locally.

The restraint I want to note

You sized the population across all 59 corpus files — 20 PASS, 3 correctly INERT, 3 confirmed panics — and then refused to treat the 33 timeouts as a finding, because the sweep ran under sustained host load of 30–55 and timeout-vs-genuine-cost-vs-noise is undecidable from that data. Saying so plainly, and keeping the gate out of --arms all until the population is sized on a quiet host, is worth more than a number that would have been quoted for months.

Gates 19/19.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@changelog.d/7723-gc-gate-arms.md`:
- Line 1: Update the changelog entry to remove the claim that both GC issues are
closed or that `#7254` is fixed. Describe the added regression coverage, while
accurately noting that the stale native-root defect remains unfixed.

In `@crates/perry-runtime/src/gc/tests/lazy_intrinsic_towers.rs`:
- Around line 50-55: Correct the intrinsic-root count from six to eight in the
documentation comment near the listed per_test_global! statics in
crates/perry-runtime/src/gc/tests/lazy_intrinsic_towers.rs lines 50-55 and in
changelog.d/7723-gc-gate-arms.md lines 24-34; make no other changes.

In `@scripts/gc_instrument_smoke.sh`:
- Line 274: Update the grep assertion in the arm 5b verification block to match
the verifier’s complete error prefix, rather than accepting any output
containing “stale forwarded pointer.” Preserve the existing failure behavior
while ensuring unrelated failures with that text do not satisfy the check.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 02c89708-2195-4ae0-ba7c-f7372c47eaff

📥 Commits

Reviewing files that changed from the base of the PR and between ca8c0d6 and 82525e9.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (9)
  • CLAUDE.md
  • Cargo.toml
  • changelog.d/7723-gc-gate-arms.md
  • crates/perry-runtime/src/gc/tests/lazy_intrinsic_towers.rs
  • crates/perry-runtime/src/gc/tests/mod.rs
  • crates/perry-runtime/src/object/global_this/generator.rs
  • crates/perry-runtime/src/object/global_this/typed_array.rs
  • crates/perry-runtime/src/object/mod.rs
  • scripts/gc_instrument_smoke.sh

@@ -0,0 +1,82 @@
Closed two GC issues in the "a real hazard exists and nothing exercises it" class.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Do not state that #7254 is closed.

This PR does not fix the stale native-root defect. Lines 65-77 state that it remains unfixed. Describe the added regression coverage instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@changelog.d/7723-gc-gate-arms.md` at line 1, Update the changelog entry to
remove the claim that both GC issues are closed or that `#7254` is fixed. Describe
the added regression coverage, while accurately noting that the stale
native-root defect remains unfixed.

Comment on lines +50 to +55
//! 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the intrinsic-root count.

The lists contain eight statics, not six: two typed-array roots and six generator-related roots.

  • crates/perry-runtime/src/gc/tests/lazy_intrinsic_towers.rs#L50-L55: replace “six” with “eight”.
  • changelog.d/7723-gc-gate-arms.md#L24-L34: replace “six” with “eight”.
📍 Affects 2 files
  • crates/perry-runtime/src/gc/tests/lazy_intrinsic_towers.rs#L50-L55 (this comment)
  • changelog.d/7723-gc-gate-arms.md#L24-L34
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/gc/tests/lazy_intrinsic_towers.rs` around lines 50 -
55, Correct the intrinsic-root count from six to eight in the documentation
comment near the listed per_test_global! statics in
crates/perry-runtime/src/gc/tests/lazy_intrinsic_towers.rs lines 50-55 and in
changelog.d/7723-gc-gate-arms.md lines 24-34; make no other changes.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Match the verifier failure prefix.

Line 274 accepts any nonzero failure that contains stale forwarded pointer. This can pass arm 5b for an unrelated failure that emits that text. Match the verifier error prefix so the arm enforces its stated failure mode.

Proposed fix
-if ! grep -q 'stale forwarded pointer' <<<"$repro_out"; then
+if ! grep -q 'gc evacuation verification failed: stale forwarded pointer' <<<"$repro_out"; then
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if ! grep -q 'stale forwarded pointer' <<<"$repro_out"; then
if ! grep -q 'gc evacuation verification failed: stale forwarded pointer' <<<"$repro_out"; then
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/gc_instrument_smoke.sh` at line 274, Update the grep assertion in the
arm 5b verification block to match the verifier’s complete error prefix, rather
than accepting any output containing “stale forwarded pointer.” Preserve the
existing failure behavior while ensuring unrelated failures with that text do
not satisfy the check.

@proggeramlug
proggeramlug merged commit cbb682d into main Aug 9, 2026
1 of 16 checks passed
@proggeramlug
proggeramlug deleted the gc/7251-7254-gate-arms branch August 9, 2026 17:52
proggeramlug pushed a commit that referenced this pull request Aug 9, 2026
… no-op

crates/perry-runtime/src/gc/tests/runtime_roots/generator_attach_prototype.rs's
three tests were red on main: js_generator_attach_prototype and
js_generator_attach_closure_prototype no longer moved their receiver under an
alloc-point copying minor, and the shipped-default witness never saw its
trigger armed.

warm_generator_intrinsics() called js_generator_attach_prototype(TAG_UNDEFINED, 0)
to pre-build the generator intrinsic tower before the timed call under test.
That never worked: js_generator_attach_prototype returns at its very first
line for any non-pointer obj, so the "warm-up" touched nothing. It went
unnoticed because GENERATOR_FUNCTION_INTRINSIC_PTR and its five siblings were
plain process-global AtomicI64s pre-#7723 - some earlier test in the same
binary had almost always already built the tower, so the real call under test
found it cached regardless of what warm_generator_intrinsics() did.

#7723 converted those six statics to per_test_global! specifically so each
test starts from a guaranteed first-touch state (crates/perry-runtime/src/gc/tests/lazy_intrinsic_towers.rs's
whole point). That is a correct, deliberate change - it took away the
accidental cross-test priming these three tests had been relying on. With
nothing pre-built, the real call now pays the dozens-of-allocations tower
build itself, inside build_generator_tower's GcSuppressScope (#7251's no-move
window for that build). That suppression window swallows the arena trigger
the test injected via arm_collection_on_next_block for the rest of the call:
no copying minor ever runs before the tower build's own scope closes, and by
then intermediate's own allocation no longer needs a new arena block, so the
trigger is never serviced. Confirmed with instrumented gc_check_trigger /
GcSuppressScope traces comparing the last-good commit against #7723: on the
last-good commit the real call's first allocation reaches gc_check_trigger
unsuppressed and services the trigger directly; on #7723 the entire ~1800-call
tower build runs suppressed first and nothing ever re-triggers afterward.

Fix warm_generator_intrinsics() to call crate::object::ensure_generator_intrinsics()
directly - the same builder lazy_intrinsic_towers.rs uses - so it does what
its name and doc comment always claimed. This does not touch the
liveness/deferral assertions those tests make; it only repairs the test's own
setup helper.

Bisected via git checkout of each of today's three merges in an isolated
worktree: c907953 (pre-#7721) passes; ca8c0d6 (#7721, moving-loop poll
default flip) passes; cbb682d (#7723, no-move window + per_test_global
towers) is the first commit where all three fail. #7724 is uninvolved.
proggeramlug added a commit that referenced this pull request Aug 9, 2026
… no-op (#7731)

* fix(gc): warm_generator_intrinsics must call the tower builder, not a no-op

crates/perry-runtime/src/gc/tests/runtime_roots/generator_attach_prototype.rs's
three tests were red on main: js_generator_attach_prototype and
js_generator_attach_closure_prototype no longer moved their receiver under an
alloc-point copying minor, and the shipped-default witness never saw its
trigger armed.

warm_generator_intrinsics() called js_generator_attach_prototype(TAG_UNDEFINED, 0)
to pre-build the generator intrinsic tower before the timed call under test.
That never worked: js_generator_attach_prototype returns at its very first
line for any non-pointer obj, so the "warm-up" touched nothing. It went
unnoticed because GENERATOR_FUNCTION_INTRINSIC_PTR and its five siblings were
plain process-global AtomicI64s pre-#7723 - some earlier test in the same
binary had almost always already built the tower, so the real call under test
found it cached regardless of what warm_generator_intrinsics() did.

#7723 converted those six statics to per_test_global! specifically so each
test starts from a guaranteed first-touch state (crates/perry-runtime/src/gc/tests/lazy_intrinsic_towers.rs's
whole point). That is a correct, deliberate change - it took away the
accidental cross-test priming these three tests had been relying on. With
nothing pre-built, the real call now pays the dozens-of-allocations tower
build itself, inside build_generator_tower's GcSuppressScope (#7251's no-move
window for that build). That suppression window swallows the arena trigger
the test injected via arm_collection_on_next_block for the rest of the call:
no copying minor ever runs before the tower build's own scope closes, and by
then intermediate's own allocation no longer needs a new arena block, so the
trigger is never serviced. Confirmed with instrumented gc_check_trigger /
GcSuppressScope traces comparing the last-good commit against #7723: on the
last-good commit the real call's first allocation reaches gc_check_trigger
unsuppressed and services the trigger directly; on #7723 the entire ~1800-call
tower build runs suppressed first and nothing ever re-triggers afterward.

Fix warm_generator_intrinsics() to call crate::object::ensure_generator_intrinsics()
directly - the same builder lazy_intrinsic_towers.rs uses - so it does what
its name and doc comment always claimed. This does not touch the
liveness/deferral assertions those tests make; it only repairs the test's own
setup helper.

Bisected via git checkout of each of today's three merges in an isolated
worktree: c907953 (pre-#7721) passes; ca8c0d6 (#7721, moving-loop poll
default flip) passes; cbb682d (#7723, no-move window + per_test_global
towers) is the first commit where all three fail. #7724 is uninvolved.

* changelog: add fragment for #7731 (generator-attach-pacing)

* chore: bump version to 0.5.1422

Claude-Session: https://claude.ai/code/session_01Y1QZ5wUP9gRSwpiweT4Wix

---------

Co-authored-by: Ralph Küpper <ralph@skelpo.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant