Skip to content

A mutated capture first assigned inside a loop body miscompiles: the loop hoist declares it as a plain value, the assignment writes through it as a cell #2024

Description

@nicolas-maman

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.

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