Skip to content

bugc: list the caller's locals at the entry of inlined code as at the call - #380

Open
gnidan wants to merge 6 commits into
mainfrom
bugc-pointer-gap
Open

gnidan wants to merge 6 commits into
mainfrom
bugc-pointer-gap

Conversation

@gnidan

@gnidan gnidan commented Oct 13, 2026 •

Copy link
Copy Markdown
Member

At -O 2 and up, the JUMPDEST where an inlined function's code starts listed the caller's locals with no pointer, for that one step, though their bytes had not moved. The jump before it and the steps after the return both had the pointers. For example, a contract that copies a struct from storage to a let player: Player in memory, then calls an inlined _rolledHit(), gives player no pointer at that JUMPDEST only.

The cause is in local-variable tracking. A block's entry is placed just before its first instruction. At the start of inlined code, that instruction carries the invoke, so the entry lists the caller's locals. But the position used to decide which version of each local is current was that instruction's origin, which is in the callee. The caller's versions have no place there, so tracking fell back to the position after optimization, where a local with a version the optimizer removed gets no pointer.

The fix: inlining now records where the call was before optimization (callOrigin) on the op that carries the invoke, and the entry is placed there. The call's own position is the caller's state as the call begins (a call's result is defined in its continuation), so the entry lists what the call itself lists. This holds whatever jump optimization does later: the jump into inlined code can be a branch, a jump from a block where two branches meet, or the jump that returns from another inlined function, and none of these carries the call's position. Where other passes combine an instruction's debug (folding, CSE, read/write merging, nested inlining), withPlacement keeps callOrigin with the instruction's other placement.

The tests walk the trace. At O2 and O3, for a call straight on, on one branch, after two branches meet, with a constant argument (so the callee's first instruction folds away), and across a call that an inlined function inlines, the step before the invoke is a JUMPDEST that lists the struct local with a pointer, and the soundness check reads the right value through it. A local given a constant between two inlined calls has no pointer at the second call's JUMPDEST: its value there is the constant, which has no place, and its old place still holds the old value. These tests fail on main.

In a larger contract, the output changes only at the JUMPDESTs that start inlined code: two at O2 and one at O3, each now listing the same variables as the jump before it. Bytecode does not change at any level, and the output at O0 and O1 is identical.

The entry of an inlined function's body is the caller's position, but
it was placed at the body's first instruction's origin, which is in the
callee. There the caller's versions are not placed, so every caller's
local with a removed version lost its pointer for that one step. Use
the origin of the jump into the inlined code instead.
@github-actions

github-actions Bot commented Oct 13, 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-380/

Built to branch gh-pages at 2026-10-13 20:34 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

The jump into inlined code is not always the call: jump optimization
can send the jump that returns from one inlined function straight into
the next, or a branch straight into inlined code. Have inlining record
where the call was before optimization (callOrigin) on the op that
carries the invoke, and place the entry there: at the call's own
position, which is the caller's state as the call begins. Test each
shape by execution, with the values the local holds.
@gnidan gnidan changed the title bugc: keep the caller's locals located at the entry of inlined code bugc: list the caller's locals at the entry of inlined code as at the call Oct 13, 2026
withPlacement kept an instruction's inline sites, origin and returned
debug, but not callOrigin, so folding, CSE, read/write merging and
nested inlining dropped it while keeping the invoke. Keep it, and test
a constant argument and a nested call; run (m) at O3 too.

This branch has not been deployed

No deployments
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.

1 participant