Skip to content

fix(codegen): use live receiver in stable packed loops - #9270

Merged
proggeramlug merged 1 commit into
PerryTS:mainfrom
proggeramlug:fix/9117-packed-loop-live-receiver
Aug 31, 2026
Merged

fix(codegen): use live receiver in stable packed loops#9270
proggeramlug merged 1 commit into
PerryTS:mainfrom
proggeramlug:fix/9117-packed-loop-live-receiver

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Summary

Port the root fix already present on release/v0.5.1519 to main. Stable packed loops now consume the live Array address returned by admission, preventing direct reads through a stale forwarding stub after cross-call growth.

Changes

Related issue

Fixes #9117

Test plan

  • Baseline exact release blocker: stable packed loop reads stale forwarded array after cross-call growth #9117 test reproduced 165171 instead of 499500.
  • RUST_TEST_THREADS=1 cargo test -p perry --test issue_5139_object_arraylike_method_dispatch any_typed_real_arrays_keep_array_mutator_semantics -- --exact --nocapture (1/1)
  • cargo test -p perry --test issue_8655_array_subclass_indexing (2/2)
  • cargo test -p perry --test issue_8690_loop_versioned_arraylike (3/3)
  • cargo test -p perry --test issue_8773_closure_capture_packed_loops (4/4)
  • ./scripts/pre-tag-check.sh --quick
  • cargo build --release clean (not run; scoped release static-runtime build and tests above passed)
  • Full workspace test suite (not run; scoped regression targets above passed)
  • Existing runtime and IR regression tests cover the affected behavior.
  • Documentation update is not applicable; this is an internal codegen correctness fix.
  • Platform UI build is not applicable.

Screenshots / output

Before: grow: 1000 1000 165171

After: the regression assertion passes with grow: 1000 1000 499500.

Checklist

  • I have NOT bumped the workspace version or edited CLAUDE.md / CHANGELOG.md (maintainer handles these at merge)
  • My commit follows the loose fix: prefix convention used in the log
  • I have read CONTRIBUTING.md and agree to the Code of Conduct

Summary by CodeRabbit

  • Bug Fixes

    • Fixed stable packed loops after array growth so they continue using the live array address instead of a stale forwarding reference.
    • Improved loop handling for arrays referenced through ordinary local variables.
  • Tests

    • Updated regression coverage to verify live array receivers are used during optimized loops.

@proggeramlug
proggeramlug force-pushed the fix/9117-packed-loop-live-receiver branch from dd2e5fb to 7125b99 Compare August 31, 2026 10:45
@coderabbitai

coderabbitai Bot commented Aug 31, 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: da78e42b-0b5d-4b30-a725-48d27c933bc1

📥 Commits

Reviewing files that changed from the base of the PR and between 29df37a and 7125b99.

📒 Files selected for processing (4)
  • changelog.d/9270-stable-packed-forwarded-receiver.md
  • crates/perry-codegen/src/stmt/stable_packed_loop.rs
  • crates/perry/tests/issue_8655_array_subclass_indexing.rs
  • crates/perry/tests/issue_8690_loop_versioned_arraylike.rs

Included review availability: Your plan provides up to 8 included reviews per hour; 3 remain after this review.


📝 Walkthrough

Walkthrough

Stable packed-loop admission now always uses js_packed_arraylike_loop_guard_live. The fast clone consumes the returned live Array address, including for ordinary locals. Regression assertions and the changelog reflect this behavior.

Changes

Stable packed loop receiver fix

Layer / File(s) Summary
Live receiver admission
crates/perry-codegen/src/stmt/stable_packed_loop.rs
The admission path always calls js_packed_arraylike_loop_guard_live and uses its returned address for the admission check and fast clone.
Regression coverage and changelog
crates/perry/tests/issue_8655_array_subclass_indexing.rs, crates/perry/tests/issue_8690_loop_versioned_arraylike.rs, changelog.d/9270-stable-packed-forwarded-receiver.md
IR assertions now require the live guard call. The changelog records the stale forwarding fix.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to 7125b

This localized codegen fix prevents optimized packed loops from reading through a stale receiver after array growth while preserving the generic fallback for invalid admission. The affected regressions pass, and no actionable merge-blocking risk remains after normal checks and review.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 3 files. (1 skipped: 1… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the primary codegen fix: using the live receiver in stable packed loops.
Description check ✅ Passed The description follows the repository template and covers the summary, concrete changes, related issue, test results, screenshots/output, and checklist. It clearly identifies the release build and fu…
Linked Issues check ✅ Passed The pull request satisfies issue #9117. It uses the live guard for ordinary and captured receivers, passes the returned live address to the fast clone, preserves the slow clone for zero, updates the r…
Out of Scope Changes check ✅ Passed All changes are within the scope of issue #9117. The codegen fix, related IR regression updates, targeted tests, and changelog fragment directly support the stable packed-loop correction.
Full details: Description check

Explanation

The description follows the repository template and covers the summary, concrete changes, related issue, test results, screenshots/output, and checklist. It clearly identifies the release build and full workspace tests that were not run.

Full details: Linked Issues check

Explanation

The pull request satisfies issue #9117. It uses the live guard for ordinary and captured receivers, passes the returned live address to the fast clone, preserves the slow clone for zero, updates the required IR assertions, and reports the required targeted tests and quick pre-tag checks as passing.

Full details: Docstring Coverage

Explanation

Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 3 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@proggeramlug

Copy link
Copy Markdown
Contributor Author

Merged.

The defect class is the one that costs the most to find later: a local naming an Array growth forwarding stub, then the fast clone reloading and de-tagging that stale local for direct reads. Consuming the live address the guard already computed is the right shape — the guard was doing the work and codegen was throwing the answer away.

Probed the shapes that produce a stub rather than trusting the description:

  • growth across a call boundary (grow(arr) pushing 300 elements, then summing) — 303/44856
  • growth mid-loop on the receiver being iterated (if (i === 10) a.push(999)) — the case where the reload happens after the move
  • a captured receiver, the arm that already used the live guard, as a control
  • 30 rounds of growth interleaved with 20k-allocation churn, so a collection lands inside the loop

All byte-identical to node 26.5.1, and identical again under PERRY_GC_FORCE_EVACUATE=1 and PERRY_GC_PROTECT_FROMSPACE=1 with a seeded aggressive schedule — the arm that faults at the faulting instruction if a stale from-space address is dereferenced.

Worth noting the cost this accepts: the non-captured arm now always calls js_packed_arraylike_loop_guard_live (I64) instead of the cheaper js_packed_arraylike_loop_guard (I32). That is a real per-loop-entry cost paid for correctness, and it is the right trade — but it is the kind of thing that looks like an unexplained regression to whoever benchmarks this path next, so the comment naming #9117 is doing useful work.

Also re-ran the #9246 and #9250 array-store probes against this build; both still match node, so the tier changes and this one do not interact.

Validation: perry-runtime 2872 passed / 0 failed at RUST_TEST_THREADS=1; perry-codegen 31 suites / 0 failures; all 60 lint gates plus the TLS checkers green. Validated on a shared branch with #9258, #9260 and #9269.

@proggeramlug
proggeramlug merged commit c1c8623 into PerryTS:main Aug 31, 2026
30 checks passed
@proggeramlug

Copy link
Copy Markdown
Contributor Author

Audited the diff (not the description) — this is a genuine root-cause fix and I'd merge it once pr-gate is present and passing.

The change removes the capture/ordinary-local branch so both paths call js_packed_arraylike_loop_guard_live and consume the address it returns. That matches #9117's symptom precisely: the old ordinary-local path called the boolean guard, validated the live target, then discarded the result and reloaded the stale local. Storage stayed intact — which is why sampled values read correctly — while the reduction walked the forwarding stub. Exactly the reported 165171 instead of 499500.

The two IR-contract edits are tightenings, not weakenings, which is the thing I check for:

- system.contains("call i32 @js_packed_arraylike_loop_guard(")
+ system.contains("call i64 @js_packed_arraylike_loop_guard_live(")

guard returns i32 (a proof bit); guard_live returns i64 (the live address). The updated contracts demand the stronger artifact, and the assertion messages were updated to say so rather than left stale.

One note for the record, since it may save someone else the detour: the PR summary says the fix is "already present on release/v0.5.1519". I could not verify that. stable_packed_loop.rs does differ between 01463b8161 (the previous candidate) and main, but the difference is in match_candidate — the candidate had a None => ctx.prelowered_zero_for_counter? arm that main dropped — and both have exactly one guard_live call site. So the provenance claim doesn't hold up as written. It doesn't matter for the merge: the change stands on its own reasoning.

Verification caveat worth honouring: #9117 is optimization-state dependent — it passed on candidate 01463b8161 while still being broken, which is why I wrongly closed it earlier today. A green targeted run is not sufficient evidence on its own here. What makes this convincing is the root cause being identified and addressed, not the test going green.

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.

release blocker: stable packed loop reads stale forwarded array after cross-call growth

1 participant