fix(transform): per-iteration binding a closure writes survives an await (#6354) - #6571
Conversation
|
Warning Review limit reached
Next review available in: 40 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughThe generator transform now detects written loop captures that cross suspension points, rewrites them through per-iteration heap cells, and applies existing snapshot handling afterward. Tests cover async loops, generators, nested loops, and ChangesWritten suspended loop captures
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant GeneratorLowering
participant CaptureAnalysis
participant CellRewrite
participant CaptureSnapshot
GeneratorLowering->>CaptureAnalysis: collect written suspended loop captures
CaptureAnalysis-->>GeneratorLowering: selected LocalIds
GeneratorLowering->>CellRewrite: rewrite selected bindings to cells
GeneratorLowering->>CaptureSnapshot: process remaining suspended captures
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 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 |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
test-files/test_gap_6354_async_written_binding_across_await.ts (1)
1-4: 📐 Maintainability & Code Quality | 🔵 TrivialRun tests against the pinned Node version.
As per coding guidelines, ensure you run the gap suite against the exact Node version pinned in
.node-version. Do not substitute another Node release because the oracle version affects correctness results.🤖 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 `@test-files/test_gap_6354_async_written_binding_across_await.ts` around lines 1 - 4, Run the gap test suite for test_gap_6354_async_written_binding_across_await.ts using exactly the Node version specified in .node-version. Do not substitute another Node release, and verify the test passes under the pinned version.Source: Coding guidelines
crates/perry-transform/src/generator/per_iteration.rs (1)
81-82: 📐 Maintainability & Code Quality | 🔵 TrivialUse correct profiles for local iteration.
As per coding guidelines for
**/*.rsfiles, usecargo check -p perryand thencargo build --profile perry-dev -p perryfor normal local compiler iteration; reserve release or dist profiles for optimization-sensitive or shipping builds.🤖 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/generator/per_iteration.rs` around lines 81 - 82, Update the local Rust validation workflow associated with per_iteration.rs to use cargo check -p perry followed by cargo build --profile perry-dev -p perry. Do not use release or dist profiles unless performing optimization-sensitive or shipping validation.Source: Coding guidelines
🤖 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 `@crates/perry-transform/src/generator/per_iteration.rs`:
- Around line 1023-1049: Remove the early return from the Expr::Closure branch
in rewrite_cells_in_stmt so processing falls through to walk_expr_children_mut
after rewriting the closure body and capture lists. Preserve the existing cell
demotion logic while ensuring closure child expressions, including default
parameters and computed properties, are traversed and rewritten.
---
Nitpick comments:
In `@crates/perry-transform/src/generator/per_iteration.rs`:
- Around line 81-82: Update the local Rust validation workflow associated with
per_iteration.rs to use cargo check -p perry followed by cargo build --profile
perry-dev -p perry. Do not use release or dist profiles unless performing
optimization-sensitive or shipping validation.
In `@test-files/test_gap_6354_async_written_binding_across_await.ts`:
- Around line 1-4: Run the gap test suite for
test_gap_6354_async_written_binding_across_await.ts using exactly the Node
version specified in .node-version. Do not substitute another Node release, and
verify the test passes under the pinned version.
🪄 Autofix (Beta)
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: fd2d210d-5eb0-48de-be63-3508842a01e4
📒 Files selected for processing (4)
crates/perry-transform/src/generator/lower.rscrates/perry-transform/src/generator/mod.rscrates/perry-transform/src/generator/per_iteration.rstest-files/test_gap_6354_async_written_binding_across_await.ts
…ait (PerryTS#6354) A per-iteration `let` that a closure WRITES and that is still read after an `await` in the same loop body collapsed onto a single binding: every closure observed the last iteration's value. This is the residual left by PerryTS#6345. Its snapshot only copies READ-ONLY captures (`captures \ mutable_captures`) — a value snapshot would drop a later write — so a written binding stayed in `mutable_captures`, kept its single activation-wide box, and collapsed. Fix: reduce the write case to the already-solved read-only case. Before the PerryTS#6345 passes run, back each such binding with a one-element heap cell (`let acc = i` -> `let acc = [i]`, reads/writes -> `acc[0]`). The binding VARIABLE then holds a per-iteration array REFERENCE that is never reassigned, so it lands in `captures \ mutable_captures` and PerryTS#6345 snapshots the reference per iteration; writes go to the shared element and stay visible to every closure of the same iteration. Entirely in `perry-transform` — no codegen/HIR change. Candidates: a block-scoped loop-body `let` that some in-loop closure lists in `mutable_captures` and that is live across a suspend. The rewrite reaches every reference including closure param defaults. Bindings referenced via a bare-`LocalId` array/set mutation intrinsic (`arr.push`, `set.add`, …) or a `with` fallback are excluded (the rewrite can't retarget those) and left as-is. Covers plain async functions, async generators, and sync generators. New gap fixture exercises closure writes, `++`/`--`, enclosing-scope writes before and after the suspend, a closure param default, write-sharing across a suspend, multiple bindings, nested loops, while/do-while/for-of, and the `var`-stays-collapsed guard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
8f5f183 to
d7e7bf1
Compare
|
Thanks @coderabbitai — addressed the actionable finding and pushed:
On the two nitpicks (both process reminders, no code change needed):
|
|
Tip For best results, initiate chat on the files or code changes.
🐇 ✅ ✏️ Learnings added
If you found this review helpful, would you consider giving us a shout-out on X? Thank you for using CodeRabbit! |
Summary
Fixes #6354. A per-iteration
letthat a closure writes and that is still read after anawaitin the same loop body collapsed onto a single binding — every closure observed the last iteration's value:This is the residual left by #6345 / #6353. That fix's snapshot only copies read-only captures (
captures \ mutable_captures) — a value snapshot would silently drop a later write — so a written binding stayed inmutable_captures, kept its single activation-wide box, and collapsed.Approach
Reduce the write case to the already-solved read-only case. Before the #6345 passes run, back each such binding with a one-element heap cell:
Now the binding variable holds a per-iteration array reference that is never reassigned, so it lands in
captures \ mutable_capturesand #6345 snapshots the reference per iteration; writes go to the shared element and stay visible to every closure of the same iteration. This is entirely aperry-transformchange — no codegen / HIR-contract change.Candidates: a block-scoped loop-body
letthat some in-loop closure lists inmutable_capturesand that is live across a suspend. Bindings referenced via a bare-LocalIdarray/set mutation intrinsic (arr.push,set.add,arr.pop(), …) or awithfallback — forms the cell rewrite can't retarget — are excluded and left as-is (the pre-existing collapse persists for that rare shape, but no new corruption).Validation
test_gap_6354_async_written_binding_across_await.ts— byte-identical to Node 26. Covers plain async functions, async generators, sync generators, closure writes,++/--, enclosing-scope writes before and after the suspend, write-sharing across a suspend, multiple bindings, nested loops,while/do-while/for-of, and avar-stays-collapsed guard.let/constbinding collapses for closures created in a loop body (every closure sees the last value) #6345 fixture is unchanged (still byte-identical to Node). ItsclosureWritesBindingcase usedlet acc = 0(same value every iteration), so a collapse coincidentally equalled the correct answer — it never actually exercised this residual, which is why async: a per-iteration binding that a closure WRITES and that survives an await still collapses #6354 was filed separately.cargo test -p perry-transform: all pass.Per the contributor guidance, this PR does not bump the version or touch the changelog.
🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
letorconstbindings that are later read afterawait,yield, or other generator suspensions.for,while,do/while,for-of, nested-loop, synchronous generator, and asynchronous generator scenarios.Tests