Skip to content

bugc: keep variables right at inlined code - #341

Merged
gnidan merged 2 commits into
mainfrom
inline-variables
Oct 6, 2026
Merged

gnidan merged 2 commits into
mainfrom
inline-variables

Conversation

@gnidan

@gnidan gnidan commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

At an inlined function's code, bugc gathers the call site's code range with the callee's, since the two collide. The storage variables from irgen rode along inside the callee's gathered context, and the callee's locals went in a flat variables beside the gather. That says the right thing (all of a gather's contexts apply, and so do the keys beside it), but it splits one list in two: a reader that takes the flat list loses total and calls at every inlined step, and a local can't hide a storage variable of its name the way it does elsewhere. Now the storage variables move up into the one flat variables list, and the gather holds only the colliding code ranges (and the transform and activation markers). A gathered context with a frame keeps its variables, which hold only in that frame.

An inlined function's locals were also listed outside its body. Contexts are postconditions, so they belong from the op that invokes the function up to, not including, the op that returns from it. The op that carries the inlined return now lists the caller's variables, as a real call's return JUMPDEST does: the inliner records the caller's placement on the instruction that returns, and code generation stamps that op with the caller's snapshot. The jump the inliner puts in place of the callee's return keeps the call's source range, inline sites and origin, so it lists the caller's variables too, and so does the entry block's JUMPDEST just before the invoke.

An inlined function that starts with an inlined call, or returns one's result, has an empty entry (or return) block, so its invoke (or return) rode a jump that jump optimization removed, and the call stack didn't balance. The inliner now passes over such blocks to the body's first (or last) instruction, and a marker left on a call carries over when that call is inlined. Each inlined function has its own invoke and return: outermost invoke first, innermost return first, also when several land on one op.

The new soundness tests run small programs at O2 and O3: a local is listed only from its function's invoke up to its return; the op that returns lists the caller's locals, where the callee's have the same names; storage variables are listed at every inlined step, with no variables left in a gather; and an inlined function that starts or ends with an inlined call gets invoke outer, invoke inner, return inner, return outer. The bytecode doesn't change (checked on every example at every level).

Found while stepping through weights.bug at O2 in the debugger demo. The reader side (@ethdebug/programs-react didn't look into a gather when the context had its own variables) is fixed in #342.

@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-10-06 14:26 UTC

gnidan added 2 commits October 6, 2026 10:15
In inlined code, the instructions gather the call site's `code` with
the callee's, and irgen's storage variables rode in the callee's
gathered context while the callee's locals sat in a flat `variables`
beside the gather. All of them apply, but a reader that took the flat
list lost the storage variables, and a local could not hide a storage
variable of its name. Now one flat `variables` list holds both.

An inlined function's locals were also listed outside its body: on
the jump back to the caller's continuation, after its return, and on
its entry block's JUMPDEST, before its invoke. That jump now keeps the
call's source range, inline sites and origin, so it lists the
caller's variables, and the entry lists the caller's scope.
…t inlined activations

Contexts are postconditions, so the op that carries an inlined
function's `return` is past it: like a real call's return JUMPDEST, it
now lists the caller's variables. The inliner records the caller's
placement on the instruction that returns (`returned`), and code
generation stamps that op with the caller's snapshot.

An inlined function that starts with an inlined call, or returns one's
result, had an empty entry (or return) block, so its invoke (or
return) rode a jump that jump optimization removed. The inliner now
passes over such blocks to the body's first (or last) instruction, and
a marker left on a call carries over when that call is inlined, so
each inlined function has its own invoke and return, outermost invoke
first and innermost return first. Several markers on one op keep that
order.

Variables in a gathered context with a `frame` stay there: they hold
only in that frame.
@gnidan
gnidan merged commit a40ae8b into main Oct 6, 2026
7 checks passed
@gnidan
gnidan deleted the inline-variables branch October 6, 2026 14:22
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