Skip to content

Commit a046048

Browse files
proggeramlugRalph Küpper
andauthored
hir: prealloc closure-capture boxes for lets nested in Try/If (array destructuring) (#5859)
compute_prealloc_for_hoisted_closures collects the function-entry prealloc-box set via collect_top_level_let_ids_stmt, which only matched top-level `Stmt::Let`. But array-destructuring bindings (`let [a, b] = expr`) lower through the iterator protocol wrapped in a `Try` (iterator close), so their leaf `let`s live inside the Try body / its `if`s — never at the statement top level. A hoisted function declaration capturing such a binding was therefore omitted from the prealloc set: the closure captured the un-boxed, not-yet-assigned slot and read `undefined` instead of the current value (e.g. `const [k, setK] = useState(0)` captured by a hoisted handler — the handler saw k === undefined and took the wrong branch). Recurse into `Stmt::Try` (body/catch/finally) and `Stmt::If` (then/else) — the constructs the destructuring lowering emits — when collecting hoisted-let ids. Loops are intentionally not recursed: a per-iteration `let` needs a fresh box per iteration, not a single function-entry prealloc. cargo test -p perry-hir: 221 passed, 0 failed. Co-authored-by: Ralph Küpper <ralph@skelpo.com>
1 parent 071f651 commit a046048

1 file changed

Lines changed: 48 additions & 2 deletions

File tree

crates/perry-hir/src/lower_decl/block.rs

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1351,8 +1351,54 @@ fn collect_refs_in_closure_bodies_expr(expr: &Expr, out: &mut std::collections::
13511351
/// NOT recurse into nested blocks (those are block-scoped — their lets
13521352
/// aren't hoisted to function-entry).
13531353
pub fn collect_top_level_let_ids_stmt(stmt: &Stmt, out: &mut std::collections::HashSet<LocalId>) {
1354-
if let Stmt::Let { id, .. } = stmt {
1355-
out.insert(*id);
1354+
match stmt {
1355+
Stmt::Let { id, .. } => {
1356+
out.insert(*id);
1357+
}
1358+
// Array-destructuring bindings (`let [a, b] = it`) lower to the iterator
1359+
// protocol wrapped in a `Try` (see
1360+
// `destructuring::pattern_binding::lower_array_pattern_binding`), so the
1361+
// leaf `let`s live INSIDE the try body / its `if`s, not at the top level.
1362+
// Recurse into those so a hoisted FnDecl that captures a destructured var
1363+
// still gets its box preallocated at function entry. Without this, the
1364+
// hoisted closure captures the not-yet-assigned, unboxed slot and reads
1365+
// `undefined` — e.g. `const [K,_] = useState(0)` captured by a hoisted
1366+
// handler. Loops are intentionally NOT recursed: a per-iteration `let`
1367+
// needs a fresh box, not a single entry-prealloc.
1368+
Stmt::Try {
1369+
body,
1370+
catch,
1371+
finally,
1372+
} => {
1373+
for s in body {
1374+
collect_top_level_let_ids_stmt(s, out);
1375+
}
1376+
if let Some(c) = catch {
1377+
for s in &c.body {
1378+
collect_top_level_let_ids_stmt(s, out);
1379+
}
1380+
}
1381+
if let Some(f) = finally {
1382+
for s in f {
1383+
collect_top_level_let_ids_stmt(s, out);
1384+
}
1385+
}
1386+
}
1387+
Stmt::If {
1388+
then_branch,
1389+
else_branch,
1390+
..
1391+
} => {
1392+
for s in then_branch {
1393+
collect_top_level_let_ids_stmt(s, out);
1394+
}
1395+
if let Some(eb) = else_branch {
1396+
for s in eb {
1397+
collect_top_level_let_ids_stmt(s, out);
1398+
}
1399+
}
1400+
}
1401+
_ => {}
13561402
}
13571403
}
13581404

0 commit comments

Comments
 (0)