Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions crates/perry-transform/src/generator/lower.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,17 @@ pub fn transform_generator_function_with_extra_captures(
await_async_generator_yield_operands(&mut func.body, next_local_id);
}

// #6345: decide which loop bindings must NOT be hoisted into the
// activation-wide box frame, and snapshot the ones that outlive a suspend
// into per-state locals. Both run BEFORE `linearize_body` so the inserted
// `Let`s land in the same state as the closure that reads them, and before
// `local_id_before` below so the new ids are not swept into
// `extra_local_ids` (which is force-preallocated).
let per_iteration_ids = collect_per_iteration_ids(&func.body);
let mut no_hoist_ids =
snapshot_suspended_loop_captures(&mut func.body, next_local_id, &per_iteration_ids);
no_hoist_ids.extend(per_iteration_ids);

let state_id = alloc_local(next_local_id);
let done_id = alloc_local(next_local_id);
let sent_id = alloc_local(next_local_id); // value passed by caller via next(val)
Expand Down Expand Up @@ -219,8 +230,23 @@ pub fn transform_generator_function_with_extra_captures(
}
}

// Collect hoisted var IDs first so we know which Lets to rewrite
let hoisted_for_rewrite = collect_hoisted_vars(&func.body);
// Collect hoisted var IDs first so we know which Lets to rewrite.
//
// #6345: NOT every body `Let` may be hoisted. A `let`/`const` declared in a
// loop gets a FRESH binding per iteration, and a closure made in iteration
// k must capture iteration k's binding. Hoisting moves the declaration into
// the activation-wide `PreallocateBoxes` frame (one box per call), which
// collapses every iteration onto a single cell — so all closures read the
// last value (`for (let i…) { const j = i; fns.push(() => j) }` printed the
// final `j` N times). `no_hoist_ids` (computed above) holds the bindings
// that keep their in-loop declaration — where codegen re-executes and
// re-boxes them every iteration, exactly as the non-async path does — plus
// the per-state snapshot locals. `var` and anything else live across an
// `await` is excluded there and keeps today's hoisting.
let hoisted_for_rewrite: Vec<(LocalId, String, Type)> = collect_hoisted_vars(&func.body)
.into_iter()
.filter(|(id, _, _)| !no_hoist_ids.contains(id))
.collect();
let mut hoisted_ids: std::collections::HashSet<LocalId> =
hoisted_for_rewrite.iter().map(|(id, _, _)| *id).collect();
// The lifted param prologue defines locals (destructured targets + temps)
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-transform/src/generator/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ mod id_scan;
mod iter_result_rewrite;
mod linearize;
mod lower;
mod per_iteration;
mod rewrite_returns;

// Explicit named re-exports so siblings can reach each other via
Expand All @@ -41,6 +42,7 @@ pub(crate) use linearize::{
pub(crate) use lower::{
transform_generator_function, transform_generator_function_with_extra_captures,
};
pub(crate) use per_iteration::{collect_per_iteration_ids, snapshot_suspended_loop_captures};
pub(crate) use rewrite_returns::{
body_contains_return, prepend_done_before_returns, rewrite_catch_returns_to_iter_result,
rewrite_iter_results_in_stmts, rewrite_returns_as_done, rewrite_returns_to_labeled_break,
Expand Down
Loading
Loading