Skip to content

Packed-loop clone does not flush loop-carried locals on an unwind edge (regression from #9185) #9210

Description

@proggeramlug

#9185 admitted Stmt::Throw to the packed-f64 versioned loop. That is a silent wrong answer on main today, and I am reverting the admission in the linked PR. This issue records what would be needed to re-admit it.

The defect

break, continue and return leave the fast clone through normal CFG edges, and those edges flush the loop-carried locals back to their frame slots. An unwind edge does not. So any local the loop wrote reads back stale after the throw:

const arr: number[] = [];
for (let i = 0; i < 64; i++) arr.push(i);
const PRE = new Error("boom");

let s = 0;
try {
  for (let i = 0; i < arr.length; i++) { if (arr[i] === 40) throw PRE; s += arr[i]; }
} catch (e) { console.log(s); }   // perry: 0     node: 780

Measured on 84185b5656, both orderings wrong:

shape node perry PERRY_PACKED_LOOP_ABRUPT=0
taken break, read after 780 780 780
taken continue, read after 1024 1024 1024
taken throw, accumulate after 780 0 780
taken throw, accumulate before 820 0 820
taken throw, read via closure 780 780 780

The 0.5.1220 release (pre-#9185) also gives 780, confirming the regression.

Why the existing tests passed

#9185 shipped two taken-throw tests and both are blind to this. throwPre reads the thrown value and an untouched parameter; throwValue throws s itself. Both observe the clone's live SSA value, which is correct — never the frame slot left behind. The closure row above is correct for the same underlying reason from the other side: a captured accumulator is boxed rather than register-promoted, so there is no promoted copy to lose.

The general rule this is the second instance of: an abrupt-exit optimisation is only tested by an exit that is actually taken AND a subsequent read of something the loop wrote. #9154's labeled-break bug had the identical shape.

To re-admit

Emit the loop-carried writeback at the throw site rather than relying on the exit block, i.e. flush promoted locals immediately before the throw is emitted. The admission predicate is not the missing piece; the flush is. Worth confirming the same edge is handled for any other construct that can leave a clone without passing through its exit block.

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