Skip to content

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

Merged
proggeramlug merged 3 commits into
mainfrom
fix/generator-attach-pacing
Aug 9, 2026
Merged

fix(gc): warm_generator_intrinsics must call the tower builder, not a no-op#7731
proggeramlug merged 3 commits into
mainfrom
fix/generator-attach-pacing

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Summary

cargo test -p perry-runtime --lib was red on main (blocking every open PR
via the per-PR --lib --bins gate), with all three
gc::tests::runtime_roots::generator_attach_prototype tests failing:

  • attach_prototype_survives_an_alloc_point_copying_minor_inside_the_call
  • attach_closure_prototype_survives_an_alloc_point_copying_minor_inside_the_call
  • the_shipped_default_defers_the_trigger_out_of_the_callees_window

Root cause

Bisected in an isolated worktree by checking out each of today's three
merges (c9079531dca8c0d617 (#7721) → cbb682d31 (#7723) →
c156f8a41 (#7724)) and running just this test file:

#7723 did not break js_generator_attach_prototype /
js_generator_attach_closure_prototype.
It correctly converted the six
generator-intrinsic-tower AtomicI64 statics
(GENERATOR_FUNCTION_INTRINSIC_PTR and siblings) from plain process-globals
to per_test_global!, so every test starts from a guaranteed first-touch
"tower not yet built" state instead of whichever test happened to run first
in the process silently pre-warming it for everyone else. That is exactly
what gc::tests::lazy_intrinsic_towers needs to gate #7251, and it is the
right fix for a real cross-test hazard.

What it exposed was a latent bug in this test file's own setup helper:

fn warm_generator_intrinsics() {
    let _ = crate::object::js_generator_attach_prototype(
        f64::from_bits(crate::value::TAG_UNDEFINED),
        0,
    );
}

js_generator_attach_prototype returns at its very first line for any
non-pointer obj (!jv.is_pointer()), so this call never reached
generator_prototype_ptr / ensure_generator_intrinsics() — it always was a
no-op. Pre-#7723 that didn't matter: the tower statics were process-global,
and by the time this test ran, 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.

Once the towers became per-thread (per-test), that accidental priming went
away. With nothing pre-built, the real call now pays the
dozens-of-allocations tower build itself, inside build_generator_tower's
GcSuppressScope (#7251's own no-move window for that build — also correct).
Traced with instrumented gc_check_trigger / GcSuppressScope logging: on
the last-good commit the real call's first allocation reaches
gc_check_trigger unsuppressed and services the test's injected trigger
directly (1 relevant call); on #7723 the ~1800-call tower build runs entirely
inside one suppression window first, swallowing every trigger check that
happens during it, and by the time the window closes the following
allocations no longer need a new arena block, so the trigger is never
serviced — "subject not live" in the two alloc-point tests, and the
deferral witness's GC_SAFEPOINT_PENDING never gets set in the third.

Judgement call

This is neither "the PR broke behaviour" nor "the test's premise is now
wrong" — #7723's production and test-isolation changes are both correct and
their invariants are unaffected. It's a pre-existing bug in this test file's
own helper, masked by accident until the isolation fix removed the accident.
Fixed the helper to call the real builder
(crate::object::ensure_generator_intrinsics(), the same one
lazy_intrinsic_towers.rs uses) instead of a call shape that never reached
it. The liveness/deferral assertions in the three tests are untouched.

Test plan

  • cargo test -p perry-runtime --lib generator_attach_prototype -- --test-threads=1 — 3/3 pass, repeated 3x for flake-checking
  • cargo test -p perry-runtime --lib lazy_intrinsic_towers -- --test-threads=1 — 2/2 pass (adjacent gc: no-move window for lazy intrinsic towers (#7251) + CI arm for ZEAL+VERIFY_EVACUATION (#7254) #7723 gate, unaffected)
  • cargo test -p perry-runtime --lib --no-fail-fast -- --test-threads=1
    — 1961 passed, 0 failed, 4 ignored (one unrelated timing-sensitive test,
    promise::keyed_table::tests::settling_many_keys_is_not_quadratic, flaked
    once under host load 40–70 and passed clean on every isolated re-run;
    confirmed unrelated to this change)
  • cargo fmt --all -- --check clean

Summary by CodeRabbit

  • Bug Fixes
    • Fixed generator prototype initialization in runtime tests, preventing related failures when intrinsic statics are isolated per test.
  • Documentation
    • Added a changelog entry describing the generator initialization fix.
    • Updated the documented project version to 0.5.1422.
  • Chores
    • Bumped the workspace version to 0.5.1422.

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ce9f9274-0a5b-42da-a607-dceed3d0001a

📥 Commits

Reviewing files that changed from the base of the PR and between e1c7ba4 and 5a7a852.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • CLAUDE.md
  • Cargo.toml
  • changelog.d/7731-generator-attach-pacing.md
  • crates/perry-runtime/src/gc/tests/runtime_roots/generator_attach_prototype.rs

📝 Walkthrough

Walkthrough

The generator test warm-up now calls ensure_generator_intrinsics() directly. A changelog entry records the fix. The workspace and documented versions are updated from 0.5.1421 to 0.5.1422.

Changes

Generator intrinsic initialization

Layer / File(s) Summary
Direct intrinsic warm-up
crates/perry-runtime/src/gc/tests/runtime_roots/generator_attach_prototype.rs, changelog.d/7731-generator-attach-pacing.md
The warm-up helper directly initializes generator intrinsics. The changelog documents the previous no-op call and the correction.
Version metadata update
Cargo.toml, CLAUDE.md
The workspace package version and documented current version change to 0.5.1422.

Estimated code review effort: 1 (Trivial) | ~5 minutes

Possibly related PRs

  • PerryTS/perry#7584: Both changes modify the generator_attach_prototype runtime tests.
  • PerryTS/perry#7723: This change updates generator intrinsic warm-up and initialization behavior.

Suggested labels: bug

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the primary fix: calling the generator intrinsic tower builder instead of a no-op.
Description check ✅ Passed The description clearly explains the problem, root cause, fix, and validation, although it omits some template headings and checklist items.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 fix/generator-attach-pacing

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 3 commits August 9, 2026 21:21
… 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
proggeramlug force-pushed the fix/generator-attach-pacing branch from 7687752 to 5a7a852 Compare August 9, 2026 19:21
@proggeramlug

Copy link
Copy Markdown
Contributor Author

Merging as v0.5.1422 — this unblocks main

cargo test -p perry-runtime --lib is gated per-PR, so these three red tests were blocking every open PR. Bisected properly rather than guessed — three builds, one per suspect:

commit result
c9079531d (pre-#7721) 3/3 pass
ca8c0d617 (#7721, poll default flip) 3/3 pass
cbb682d31 (#7723) 3/3 fail — first bad commit
c156f8a41 (#7724) uninvolved

The diagnosis is the interesting part, and it is not "#7723 broke it"

warm_generator_intrinsics() called js_generator_attach_prototype(TAG_UNDEFINED, 0), expecting it to build the generator tower as a side effect. It never did — that function returns at its very first line for any non-pointer obj. The warm-up was dead code from the day it was written.

It went unnoticed because the six tower AtomicI64 statics were process-global: some earlier test in the same binary had almost always already built the tower, so the real call under test found it cached no matter what the warm-up did. The test passed for a reason that had nothing to do with its setup.

#7723 converted those statics to per_test_global! precisely so every test starts from a guaranteed first-touch state — which is its whole point for lazy_intrinsic_towers. That removed the accidental cross-test priming and exposed the dead helper. The real call then paid the ~1800-call tower build itself, inside build_generator_tower's GcSuppressScope, which swallowed the injected arena trigger; by the time suppression lifted, no allocation needed a new block, so the trigger was never serviced and GC_SAFEPOINT_PENDING stayed false.

So both of #7723's changes are correct. This is a latent bug in the test's own setup, unmasked by an isolation fix — which is exactly what test isolation is for. Fixed by calling crate::object::ensure_generator_intrinsics() directly, the same builder lazy_intrinsic_towers.rs uses.

The liveness assertions are untouched, and they are why any of this was found: "LIVE SUBJECT: the trigger must actually have been due and deferred. Without this the test also passes when nothing was armed at all." Without that guard these three would have gone green on a build where nothing was armed, and the dead warm-up would still be there.

Verification

cargo test -p perry-runtime --lib --no-fail-fast: 1961 passed, 0 failed, 4 ignored. The three target tests pass 3/3 in isolation with --test-threads=1.

On my side

I merged #7721/#7723/#7724 without running cargo test -p perry-runtime --lib — my local gate covered the lint job's 19 steps but not the cargo-test job at all, so it reported green while a gated check was red. That is the same defect as a partial gate reporting on commands it never ran, one level up. The gate now runs perry-runtime --lib and perry-codegen --lib too, and it caught this immediately on the next PR.

@proggeramlug
proggeramlug merged commit 73d1e1d into main Aug 9, 2026
1 of 16 checks passed
@proggeramlug
proggeramlug deleted the fix/generator-attach-pacing branch August 9, 2026 19:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant