Skip to content

captured variable reassigned inside a labeled block is not boxed — closure reads stale snapshot #5869

Description

@proggeramlug

Summary

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.

Repro (vs node --experimental-strip-types)

function factory() {
  let arr: any = [];
  const read = () => arr.length;
  outer: {
    arr = [1, 2, 3, 4];
    break outer;
  }
  console.log("len:", read());
  console.log("json:", JSON.stringify(arr));
}
factory();

node:

len: 4
json: [1,2,3,4]

perry (main):

len: 0
json: [1,2,3,4]

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions