Repro
run(cb: fn) { cb() }
main() {
seen = 0
j = 0
while j < 20 {
local = j
run() callback { local = local * 2 }
seen = seen + local
j = j + 1
}
println("${seen}")
}
error: invalid type argument of unary '*' (have 'int')
local = j
error: passing argument 1 of '_aether_make_closure_0' makes pointer from integer without a cast
Verified on main (0.670.0) and on the #2019 branch — the two schemes for the cell differ, the miscompile is the same.
What happens
local is assigned inside the closure, so it is a promoted capture and must live in a heap cell (int* local). But its first assignment is inside a while body, and hoist_loop_vars (compiler/codegen/codegen_stmt.c) pre-declares every variable first assigned in a loop body at the top of the body as a plain value:
int local; // the hoist
*local = j; // the first assignment, now seen as a reassignment through the cell
The hoist marks the name declared, so the first-assignment path — the one that would have emitted the cell declaration — takes the "already declared, write through the pointer" branch instead, against an int.
Fix shape
hoist_loop_vars (and hoist_if_branch_vars, which has the same job for if arms) must not pre-declare a promoted capture as a value. Either skip promoted names there — the in-body first assignment then declares the cell where it is assigned, with its scope-exit release — or hoist the cell itself, which needs the per-iteration allocate/release to still land inside the body. Skipping is the smaller and the correct one: a cell is per-iteration state, and the body already knows how to declare one.
Needs a regression test that runs the loop above and checks 380, and the same for an if arm.
Found while writing tests/regression/test_capture_cell_lifetime.ae for #2019; that test leaves this shape out so the two do not block each other.
Repro
Verified on main (0.670.0) and on the #2019 branch — the two schemes for the cell differ, the miscompile is the same.
What happens
localis assigned inside the closure, so it is a promoted capture and must live in a heap cell (int* local). But its first assignment is inside awhilebody, andhoist_loop_vars(compiler/codegen/codegen_stmt.c) pre-declares every variable first assigned in a loop body at the top of the body as a plain value:The hoist marks the name declared, so the first-assignment path — the one that would have emitted the cell declaration — takes the "already declared, write through the pointer" branch instead, against an
int.Fix shape
hoist_loop_vars(andhoist_if_branch_vars, which has the same job forifarms) must not pre-declare a promoted capture as a value. Either skip promoted names there — the in-body first assignment then declares the cell where it is assigned, with its scope-exit release — or hoist the cell itself, which needs the per-iteration allocate/release to still land inside the body. Skipping is the smaller and the correct one: a cell is per-iteration state, and the body already knows how to declare one.Needs a regression test that runs the loop above and checks
380, and the same for anifarm.Found while writing
tests/regression/test_capture_cell_lifetime.aefor #2019; that test leaves this shape out so the two do not block each other.