Skip to content

bugc: fix recursive and branch miscompilation with canonical block-boundary stack discipline - #283

Open
gnidan wants to merge 1 commit into
mainfrom
compiler-cfg-stack-tracking
Open

bugc: fix recursive and branch miscompilation with canonical block-boundary stack discipline#283
gnidan wants to merge 1 commit into
mainfrom
compiler-cfg-stack-tracking

Conversation

@gnidan

@gnidan gnidan commented Aug 6, 2026

Copy link
Copy Markdown
Member

Recursive functions were miscompiled at every optimization level: a self-recursive call set up its callee with a corrupted operand stack and the callee read garbage arguments. The root cause was that evmgen tracked the operand stack by threading it through block layout order rather than the control-flow graph, so the compiler's model of the stack desynced from the runtime stack wherever a block's layout neighbor was not its actual predecessor. Leftover block-local scratch went unaccounted, the pre-call cleanup undercounted, and the callee's arguments were wrong.

This replaces layout-order threading with a canonical block-boundary stack invariant. Every block is entered with a statically known operand stack determined by its role in the control-flow graph: a call continuation is entered with the callee's return value on top, and every other block is entered empty. Each block then canonicalizes its stack on exit so the invariant holds for its successors — a jump clears any leftover scratch, and a branch loads its condition and drops the scratch beneath it, so both successors are entered empty. Return and call terminators already reduced the stack to their canonical hand-off. With the entry stack reconstructed from the CFG and the exit stack canonicalized, the tracked model matches the runtime stack again and the existing per-instruction and terminator logic is exact.

The fix turned out simpler than the design anticipated: no memory spilling of branch conditions or return values is needed. Loading the operand and then dropping the scratch beneath it keeps the change self-contained in the terminator lowering, and the branch condition is recovered by the ordinary value loader whether it lives on the stack or in memory.

The new tests verify results by executing the compiled bytecode. Tail, mutual, and tree-shaped recursion all compute correctly at levels 0 through 3, as do if/else diamonds and a function whose return block has multiple predecessors leaving different scratch depths (the multi-predecessor case that the layout-order model could desync). for-loops are covered through level 2.

Known gap, not addressed here: for-loops revert at optimization level 3 (they are correct at levels 0–2). This is pre-existing on main — the same revert reproduces with this change reverted — and has a distinct root cause in level-3 loop lowering (tail-call back-edge plus block-merging), unrelated to the stack-tracking model corrected here. It is being tracked separately.

Fixes #275.

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://ethdebug.github.io/format/pr-preview/pr-283/

Built to branch gh-pages at 2026-08-06 01:28 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@gnidan
gnidan force-pushed the compiler-cfg-stack-tracking branch 2 times, most recently from 02a72f5 to 78968f6 Compare August 6, 2026 01:06
@gnidan
gnidan force-pushed the compiler-cfg-stack-tracking branch from 78968f6 to e139563 Compare August 6, 2026 01:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bugc: self-recursive calls are miscompiled by linear stack tracking in evmgen

1 participant