Skip to content

bugc: bracket inlined invoke/return to boundary ops in evmgen#235

Merged
gnidan merged 2 commits into
transform-contextfrom
compiler-inline-desmear
Jul 3, 2026
Merged

bugc: bracket inlined invoke/return to boundary ops in evmgen#235
gnidan merged 2 commits into
transform-contextfrom
compiler-inline-desmear

Conversation

@gnidan

@gnidan gnidan commented Jul 3, 2026

Copy link
Copy Markdown
Member

Fixes the inlined virtual-activation mis-render (#23): the tracer's call
stack never popped (phantom frames) because every op of an inlined body
carried BOTH invoke and return.

Root cause — evmgen lowering, not the optimizer

An IR instruction lowers to N EVM micro-ops, and the generic lowering
attaches that instruction's whole operationDebug to every op. Right for
source/variables/transform (all N ops map to the instruction) but wrong
for invoke/return: those are positional activation boundaries (one
push point, one pop point). Broadcasting them across the op-run makes
push/pop reconstruction see every op as both.

Verified per-pass that the optimizer IR stays correctly bracketed through
inlining and all L2/L3 passes (invoke/return counts stable); the smear
appears only after lowering. Real calls are unaffected — their
invoke/return ride single-op JUMP/JUMPDEST terminators; only inlined
activations put them on multi-op compute instructions.

Fix

bracket-activation.ts (wired into block generation): for the ops emitted
by one instruction, keep invoke on only the first op, return on only
the last op, stripped from the interior. transform:["inline"] +
source/variables stay on all ops (membership preserved). A general
evmgen invariant
, not inline-specific — no-op for single-op terminators
(confirmed: real-call fixtures unchanged). Reaches invoke/return nested in
pick/gather composites, not just flat leaves (per architect's note).

Tests

New inline-bracket.test.ts: one push + one pop per site on dbl@2-sites
(both=0), invoke-before-return ordering, entry-vs-exit placement on a
multi-instruction body, unchanged runtime at O0–O3. Full bugc suite green
(428 passed, 22 pre-existing skips).

No schema change, no optimizer change, no inlining.ts change.

Inlined virtual activations mis-rendered in the tracer: the call
stack never popped (phantom frames). Root cause was in evmgen
lowering, not the optimizer.

An IR instruction lowers to N EVM micro-ops, and the generic
lowering attaches that instruction's whole operationDebug to every
op. That is right for source/variables/transform context (all N ops
map to the instruction) but wrong for invoke/return, which are
positional activation boundaries: invoke marks one push point,
return one pop point. Broadcasting them across the whole op-run
makes push/pop reconstruction see every op as both a push and a pop.

The optimizer IR is already correctly bracketed (verified per-pass:
invoke/return counts are stable through inlining + all L2/L3 passes);
the smear appears only after lowering. Real calls are unaffected
because their invoke/return ride single-op JUMP/JUMPDEST terminators
— only inlined activations put them on multi-op compute instructions.

Fix (bracket-activation.ts, wired into block generation): for the
ops emitted by one instruction, keep invoke on only the first op and
return on only the last op, stripped from the interior; transform
markers and source/variables stay on all ops. General evmgen
invariant, not inline-specific — a no-op for single-op terminators.
Reaches invoke/return nested in pick/gather composites, not just
flat leaves.

Tests: new inline-bracket.test.ts asserts one push + one pop per
site on dbl@2-sites (both=0), invoke-before-return ordering, entry
vs exit placement on a multi-instruction body, and unchanged runtime
behavior at O0-O3. Full suite green (428 passed).
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-07-03 01:13 UTC

Architect's requested safety net for the general invoke/return
bracketing invariant: assert it stays a no-op where invoke/return
ride single-op carriers.

- tailcall back-edge (tailRecursiveSum, O2): the combined
  invoke+return on the one back-edge JUMP survives (both>=1).
- mutually recursive real calls (never inlined): no op carries
  both, and invoke/return are still present (real call tracing
  intact).
@gnidan gnidan merged commit ea80a41 into transform-context Jul 3, 2026
4 checks passed
@gnidan gnidan deleted the compiler-inline-desmear branch July 3, 2026 01:08
gnidan added a commit that referenced this pull request Jul 3, 2026
…d shapes (#239)

Add fixtures for both emission shapes the reconstruction must handle:
- bracketed (post #235): invoke on the body's first op, return on the
  last, transform:["inline"] on all — frame visible across every body
  op INCLUDING the exit op (close-after), gone at the gap.
- legacy smeared: every op carries invoke+return+inline — close-after
  still yields exactly one frame per body, no accumulation across
  gap-separated bodies.

Verified end-to-end on real O2 (dbl@2 sites, #235 bracketed emission):
one virtual frame per body incl. the exit op, top level between/after.
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