fix(deforest): the caller's binding kept a growth-forwarding stub after a deforested call (#7661) - #7751
Conversation
📝 WalkthroughWalkthroughThe deforestation transform now preserves producer accumulator returns and assigns relocated array heads back to caller bindings. Structural tests and a runtime smoke test cover direct, recursive, and threshold growth cases. Project version metadata and the changelog were updated. ChangesDeforestation growth forwarding
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
…er a deforested call (#7661)
9d2c38b to
18e8bc6
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
crates/perry-transform/src/deforest/call_sites.rs (1)
249-269: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy liftMark an immutable fusion target mutable before emitting
LocalSet.Line 268 writes
outer_id, but this rewrite does not update the existingStmt::Letthat ownsouter_id. If the source usesconst outer = [], the transformed HIR contains a write to a binding still markedmutable: false. Lines 432-438 establish that this flag is an analysis contract.Track fused target IDs and mark their declarations mutable, or rewrite through a mutable compiler-generated binding. Add a regression for
const outer = []followed by a fused producer call.🤖 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-transform/src/deforest/call_sites.rs` around lines 249 - 269, Update the fusion rewrite around the LocalSet of outer_id so the binding declaration for every fused target is marked mutable before emitting the write. Track the affected target IDs and adjust the existing Stmt::Let metadata, or route the assignment through a mutable compiler-generated binding, while preserving the producer call and write-back behavior. Add a regression covering const outer = [] followed by a fused producer call.
🤖 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 `@Cargo.toml`:
- Line 318: Remove the contributor-managed version updates: restore the
workspace package version at Cargo.toml lines 318-318 and restore the Current
Version value at CLAUDE.md lines 11-11. Keep the existing changelog fragment and
leave release metadata changes to maintainers.
---
Outside diff comments:
In `@crates/perry-transform/src/deforest/call_sites.rs`:
- Around line 249-269: Update the fusion rewrite around the LocalSet of outer_id
so the binding declaration for every fused target is marked mutable before
emitting the write. Track the affected target IDs and adjust the existing
Stmt::Let metadata, or route the assignment through a mutable compiler-generated
binding, while preserving the producer call and write-back behavior. Add a
regression covering const outer = [] followed by a fused producer call.
🪄 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: a498b15b-f960-4a87-89ce-6b10d291ca57
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (7)
CLAUDE.mdCargo.tomlchangelog.d/7751-deforest-growth-forwarding.mdcrates/perry-transform/src/deforest/call_sites.rscrates/perry-transform/src/deforest/producer_rewrite.rscrates/perry-transform/src/deforest/tests.rstest-files/test_deforest_growth_forwarding.ts
|
|
||
| [workspace.package] | ||
| version = "0.5.1438" | ||
| version = "0.5.1439" |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Remove contributor-managed release metadata.
This PR already includes changelog.d/7751-deforest-growth-forwarding.md. Remove these version updates unless this is a maintainer-owned main landing change.
Cargo.toml#L318-L318: restore the workspace package version.CLAUDE.md#L11-L11: restore the Current Version value.
As per coding guidelines, version updates apply only when landing on main. Based on learnings, contributors must use the PR-keyed changelog fragment and leave release metadata to maintainers.
📍 Affects 2 files
Cargo.toml#L318-L318(this comment)CLAUDE.md#L11-L11
🤖 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 `@Cargo.toml` at line 318, Remove the contributor-managed version updates:
restore the workspace package version at Cargo.toml lines 318-318 and restore
the Current Version value at CLAUDE.md lines 11-11. Keep the existing changelog
fragment and leave release metadata changes to maintainers.
Sources: Coding guidelines, Learnings
Merging as v0.5.1439The diagnosis rejects both of the issue's own candidates and is better for it. Not the The before/after IR makes it unarguable — The line that decides how this had to be tested
That is why it survived until #7612 dereferenced a head directly and I verified they have a subject rather than taking the claim: re-introducing the drop ( Two details that would have been easy to get wrongThe The updated existing test, and the reason recorded. Marking The framing of the outcome is also right: this turns Cost is one store per deforested call. |
Fixes #7661.
Where the stub comes from
The issue's two candidates were the
alwaysinline+$spec_i32_bspecialization pair and the return/assignment lowering. It is neither — it is the deforestation pass, and the mechanism is more direct than "a stale head leaks through".crates/perry-transform/src/deforest/rewrites an array-producing function to fill a caller-allocated accumulator. Its own module doc, point 7:So the reproducer becomes:
js_array_growdoes not grow in place. The callee'sout.pushwrite-back re-points its own out-param slot; nothing re-pointskeep. And the producer rewrite dropped the trailingreturn out(step 2), so there was no value to store back either. Both halves are visible in--trace llvmon the issue's exact reproducer:That also explains the issue's observation that a module-scope
keep.push(x); keep.pop();changes behaviour: it is the only thing that writes a resolved head back into the global.The fix
No new HIR node, no codegen change — the producer already holds the correct head in its out-param slot at the point it used to fall off the end.
producer_rewrite.rs— KEEP the trailingreturn out. Step 4 already substitutesout_local_id → out_param, so it becomesreturn <out_param>: the live head, after every realloc write-back.detect.rsguarantees exactly one top-levelStmt::Return(Some(Expr::LocalGet(out_id)))and that it is the last top-level statement, so there is nothing else this could be keeping.call_sites.rs— all three rewrites store the result back over the caller's binding:keep = f(n, keep)outer = f(args, outer)mutable, because it is now written twice. Leaving itconstwhile storing through it would make every analysis that trustsmutable: falsewrong.After:
Verified in IR that the recursive shape gets it too — both
tree(...)self-calls store their result back into the out-param slot before the next use.This is what the issue asked for: it turns
js_array_refresh_local_headfrom a correctness obligation every future consumer of a raw array head must remember into an optimization.Coverage is structural, and it was sabotage-checked
Behaviour cannot see this bug. Every runtime entry point resolves the forwarding chain through
clean_arr_ptr, so the program prints the right answer either way — which is exactly why it went unnoticed until #7612 dereferenced a head directly andSIGBUSed at N = 17.So the load-bearing tests assert the transform's output shape, in
deforest/tests.rs(--lib, so they run on every PR):producer_returns_the_out_param_not_undefinedplain_call_site_stores_the_returned_head_back_over_the_bindingBoth were verified to FAIL against the pre-fix transform — I reverted the two source changes, re-ran, and confirmed 2 failed / 11 passed, then restored. A structural test that has never failed is a test with no subject; these have one.
One existing test needed updating rather than fixing:
deforests_producer_called_from_class_methodwalks for the surviving call underStmt::Expr, which is now one level deeper inside theLocalSet. Its walk now unwraps, and the comment records why that matters (a non-unwrapping walk would have collected nothing and compared[] == [1]— a failure, but only because the expectation is non-empty).test-files/test_deforest_growth_forwarding.tsexercises all three call-site shapes end-to-end including N = 17, and its header states plainly that it is a smoke test, not a detector.Validation
cargo test -p perry-transform --lib: 58 passed, 0 failed (13 deforest, incl. the 2 new).build(1000), a depth-9treewith consumer-fuse + recursion,build(17)): byte-identical.test-files/parity sweep: run and compared againsttest-parity/gap_snapshot.json.cargo fmt --all --check,scripts/check_file_size.sh: clean.Cost: one store per deforested call. No allocation, no call, no change to the fusion itself.
No version bump (maintainer bumps at merge).
Summary by CodeRabbit
Bug Fixes
Tests
Documentation
Chores