You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
captured variable reassigned inside a labeled block is not boxed — closure reads stale snapshot #5869
A captured variable that is reassigned inside a labeled block is not boxed: closures capture the creation-time value and never observe the reassignment. Reads through the closure see a stale snapshot while direct reads in the same scope see the live value.
The closure reads .length === 0 on the stale empty array while JSON.stringify in the same scope prints all four elements.
Root cause
The boxing analysis in crates/perry-codegen/src/boxed_vars.rs is inconsistent on Stmt::Labeled: collect_write_ids_in_stmt (~1179) HAS a Labeled arm, but five sibling walkers do not —
collect_closure_refs_and_writes_in_stmt (_ => {} at ~737)
collect_outer_writes_in_stmt (~970)
collect_for_init_ids (~641)
collect_self_recursive_closure_ids (~596)
collect_let_types_in_stmts (~1362-1367)
A reassignment inside a labeled block is invisible to outer_writes (and a closure inside a labeled block contributes no closure_refs), so the captured+mutated variable is never boxed → the closure keeps a creation-time snapshot.
Why this matters
Minified bundles emit labeled blocks (e: { … break e }) pervasively; hand-written repros don't — which is exactly why this class of bug reproduces only in large bundles. The failure signature (a captured array reading .length === 0 while JSON.stringify in the same scope shows the elements, and a fresh local array "fixing" it) matches the long-standing unexplained misbehavior seen in a 13.8MB Next.js app-page bundle's webpack factory.
Suggested fix
Add Stmt::Labeled arms (recurse into the body) to all five walkers, mirroring the existing arm at ~1179.
--trace hir --focus factory2 shows perry-hir lowers the labeled block to Labeled { body: DoWhile { body: [LocalSet(1, Closure { captures: [0], mutable_captures: [], … }), LabeledBreak], condition: false } } — note mutable_captures: []: the HIR mutable-capture classifier doesn't see closures nested under the Labeled/DoWhile(false) wrapper, so it classifies n as a non-mutable capture. With #5871 the codegen creation site now stores the box pointer (boxing analysis sees the capture), but the closure body still reads the capture raw (HIR says non-mutable) — the call returns the box-pointer bits as a denormal number, the classic unboxed-capture signature.
Fix direction: teach the perry-hir mutable-capture classification walk to descend Stmt::Labeled (and the synthesized DoWhile(false) wrapper) when pairing closures with later writes. A regression test is already in the tree, #[ignore]d: crates/perry/tests/issue_5869_labeled_block_capture_boxing.rs::closure_inside_labeled_block_counts_as_capture — un-ignore it when the HIR side lands.
Fixed by #5871 (boxing analysis descends into labeled blocks) and #5949 (the residual: boxed locals must not feed the typed-ABI closure specializations).
Re-verified on current main: both the original repro (write inside the label) and the residual from the comment thread (closure created inside the label) are byte-identical to node — the latter previously printed a denormal (2.64e-311) instead of 42.
Both regression tests in crates/perry/tests/issue_5869_labeled_block_capture_boxing.rs are live (no longer #[ignore]d).
Activity
proggeramlug commentedon Jul 2, 2026
PR #5871 fixes the codegen half (all five
boxed_vars.rswalkers now descend intoStmt::Labeled); the write-inside-label repro passes.Residual (this issue stays open): the closure-created-INSIDE-the-label variant still misbehaves, one layer up:
--trace hir --focus factory2shows perry-hir lowers the labeled block toLabeled { body: DoWhile { body: [LocalSet(1, Closure { captures: [0], mutable_captures: [], … }), LabeledBreak], condition: false } }— notemutable_captures: []: the HIR mutable-capture classifier doesn't see closures nested under theLabeled/DoWhile(false)wrapper, so it classifiesnas a non-mutable capture. With #5871 the codegen creation site now stores the box pointer (boxing analysis sees the capture), but the closure body still reads the capture raw (HIR says non-mutable) — the call returns the box-pointer bits as a denormal number, the classic unboxed-capture signature.Fix direction: teach the perry-hir mutable-capture classification walk to descend
Stmt::Labeled(and the synthesizedDoWhile(false)wrapper) when pairing closures with later writes. A regression test is already in the tree,#[ignore]d:crates/perry/tests/issue_5869_labeled_block_capture_boxing.rs::closure_inside_labeled_block_counts_as_capture— un-ignore it when the HIR side lands.fix(codegen): #5869 — boxing analysis must descend into labeled blocks
fix(codegen): boxing analysis must descend into labeled blocks (#5871)
fix(codegen): #5869 residual — boxed locals must not appear in module…
fix(codegen): #5869 residual — boxed locals must not feed the typed-A…
fix(codegen): #5982 — module-global captures must not feed the typed-…
proggeramlug commentedon Jul 13, 2026
Fixed by #5871 (boxing analysis descends into labeled blocks) and #5949 (the residual: boxed locals must not feed the typed-ABI closure specializations).
Re-verified on current main: both the original repro (write inside the label) and the residual from the comment thread (closure created inside the label) are byte-identical to node — the latter previously printed a denormal (
2.64e-311) instead of42.Both regression tests in
crates/perry/tests/issue_5869_labeled_block_capture_boxing.rsare live (no longer#[ignore]d).perf(codegen): keep the numeric-array specialization when the array i…
perf(codegen): keep the numeric-array specialization when the array i…