Repository navigation
bugc: list the caller's locals at the entry of inlined code as at the call - #380
Merged
Merged
Conversation
Contributor
|
gnidan
force-pushed
the
bugc-pointer-gap
branch
from
October 15, 2026 01:58
4a51610 to
25db9de
Compare
… call Inlining records where the call was before optimization (callOrigin) on the op that carries the invoke; the entry of the inlined code is placed there, so the caller's locals are listed as the call lists them. withPlacement keeps callOrigin where debugs are combined.
When a pass moves an op's invoke onto a neighbour (dropInstructions, jump optimization, block merging, read/write merging), the call origin of its outermost invoke goes with it, as returned goes with a return. Inlining uses Ir.Utils.withoutActivationsDebug.
placeActivations takes the first invoke's call origin; where the invokes split between two ops, those after get none. Combining debugs keeps the call origin of the debug whose invoke comes first. Test (m) at O3 with the second call on a branch.
gnidan
force-pushed
the
bugc-pointer-gap
branch
from
October 15, 2026 16:04
25db9de to
7dbbb41
Compare
withoutActivationsDebug now drops callOrigin too, so no debug keeps a call origin with no invoke for a combine to pair with another's.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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: Playerin memory, then calls an inlined_rolledHit(), givesplayerno 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), the result keeps thecallOriginof the debug whose invoke comes first. Where a pass moves invokes onto a neighbour (dropInstructions, jump optimization, block merging, read/write merging; #384), the call origin of the first one goes with it, asreturnedgoes with a return. If the moved invokes split between the op before and the op after, the invokes after get no call origin, so their entry falls back to listing without the call's position (which can leave a pointer out, but never points at a stale value).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. Where O3 block merging leaves no JUMPDEST between the caller's code and the inlined code (as for the nested call), that step is skipped. 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. This is checked at O2 with the second call right after the first, and at O2 and O3 with the second call on a branch (at O3 the first shape has no JUMPDEST there). These tests fail on main. Unit tests check that
placeActivations,dropInstructionsandcombineDebugContextscarrycallOriginwith an invoke, including the split, and an IR test checks it where CSE drops an inlined body's first instruction.Over all examples and the two blog contracts, at O0 to O3, bytecode is identical to main, and the output changes only at JUMPDESTs that start inlined code. Most now list the same variables as the jump before them. In
functions.bug, the entry of the second inlinedaddalso locatessum1, the first call's result, which the jump returning from the first call leaves without a pointer; the value read through it is right.