Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
1e6d5ca
bugc: flatten TCO back-edge JUMP context (#214)
gnidan Jun 18, 2026
3518f49
format: make invoke.target optional for internal calls (#213)
gnidan Jun 18, 2026
5210e38
format: add transform context for compiler optimizations (#212)
gnidan Jul 2, 2026
314b42c
bugc: emit tailcall transform context on TCO back-edge (#217)
gnidan Jul 2, 2026
3d5b461
programs-react: render tailcall transform + fix TCO call stack (#218)
gnidan Jul 2, 2026
185e831
docs: add tail-call optimization tracing example (#219)
gnidan Jul 2, 2026
25dd2ca
docs: link first frame mention to its spec page (#220)
gnidan Jul 2, 2026
13a5843
bugc: preserve function loc/sourceId through optimizer (#223)
gnidan Jul 2, 2026
9bb47f8
web: tracer drawer — opt-level selector, tailcall render, right-colum…
gnidan Jul 2, 2026
3f2fa5e
docs: match TCO example prose to the Opt selector (O0-O3) (#221)
gnidan Jul 2, 2026
e6fb3f1
bugc: emit fold transform on constant folding (#225)
gnidan Jul 2, 2026
4a46330
bugc: emit coalesce transform on read-write merging (#228)
gnidan Jul 2, 2026
f5f66a9
format: spec the inlined-call virtual activation contract (#229)
gnidan Jul 2, 2026
22bf0c8
bugc: add function inlining pass with inline transform (L2) (#230)
gnidan Jul 2, 2026
c03679c
format: clarify inline activation reconstruction (push/pop vs members…
gnidan Jul 3, 2026
410362d
web/programs-react: reconstruct inline virtual activations in tracer …
gnidan Jul 3, 2026
ea80a41
bugc: bracket inlined invoke/return to boundary ops in evmgen (#235)
gnidan Jul 3, 2026
1e90935
format: define context `name` as a referenceable identifier (activati…
gnidan Jul 3, 2026
20a7654
programs-react: adopt locked inline-activation contract (#237)
gnidan Jul 3, 2026
0ca6be7
programs-react: lock inline reconstruction against bracketed + smeare…
gnidan Jul 3, 2026
b2d9e38
format: add Name context type and guard to TS types (#238)
gnidan Jul 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions packages/bugc/src/evmgen/call-contexts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ code {
expect(typeof invoke.declaration!.range!.length).toBe("number");

// Target should be a code pointer (not stack)
expect(Pointer.Region.isCode(call.target.pointer)).toBe(true);
expect(call.target).toBeDefined();
expect(Pointer.Region.isCode(call.target!.pointer)).toBe(true);

// Caller JUMP should NOT have argument pointers
// (args live on the callee JUMPDEST invoke context)
Expand Down Expand Up @@ -156,7 +157,8 @@ code {
expect(call.identifier).toBe("add");

// Target should be a code pointer
expect(Pointer.Region.isCode(call.target.pointer)).toBe(true);
expect(call.target).toBeDefined();
expect(Pointer.Region.isCode(call.target!.pointer)).toBe(true);

// Should have argument pointers matching
// function parameters
Expand Down
27 changes: 25 additions & 2 deletions packages/bugc/src/evmgen/generation/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Memory } from "#evmgen/analysis";
import { calculateSize } from "#evmgen/serialize";

import * as Instruction from "./instruction.js";
import { bracketActivation, carriesActivation } from "./bracket-activation.js";
import { loadValue } from "./values/index.js";
import {
generateTerminator,
Expand Down Expand Up @@ -161,9 +162,31 @@ export function generate<S extends Stack>(
// the runtime predecessor differs from the layout-order
// predecessor.

// Process regular instructions
// Process regular instructions. Invoke/return activation
// discriminators must be bracketed to the first/last emitted op
// of the instruction (see bracket-activation.ts); everything else
// (source mapping, variables, transform markers) rides all ops.
for (const inst of block.instructions) {
result = result.then(Instruction.generate(inst));
const gen = Instruction.generate(inst);
const operationCtx = inst.operationDebug?.context;
if (
!carriesActivation(operationCtx, "invoke") &&
!carriesActivation(operationCtx, "return")
) {
result = result.then(gen);
continue;
}
result = result.peek((state, builder) => {
const start = state.instructions.length;
return builder.then(gen).then((s) => ({
...s,
instructions: bracketActivation(
s.instructions,
start,
operationCtx,
),
}));
});
}

// Emit phi copies for successor blocks before the
Expand Down
164 changes: 164 additions & 0 deletions packages/bugc/src/evmgen/generation/bracket-activation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/**
* Bracket invoke/return activation discriminators onto the boundary
* ops of an IR instruction's emitted op-run.
*
* A single IR instruction lowers to N EVM micro-ops, and the generic
* lowering attaches that instruction's whole `operationDebug` (source
* mapping, variables, transform markers, AND any invoke/return
* discriminators) to every one of those ops. That is correct for
* source/variable/transform context — a debugger wants all N ops
* mapped to the instruction — but WRONG for invoke/return: those are
* positional activation boundaries. An `invoke` marks a single push
* point; a `return` a single pop point. Broadcasting them across the
* whole op-run makes a push/pop reconstruction see every op as both a
* push and a pop.
*
* This module de-smears: for the ops emitted by one instruction, the
* `invoke` discriminator is kept on only the FIRST op, `return` on only
* the LAST op, and stripped from the interior. The `transform`
* membership markers (and source/variables) stay on every op.
*
* It is a general evmgen invariant, not inline-specific: it is a no-op
* for real calls (whose invoke/return already ride single-op JUMP /
* JUMPDEST terminators) and fires only when invoke/return happen to
* ride a multi-op instruction — which today is inlined virtual
* activations.
*/
import type * as Format from "@ethdebug/format";
import type * as Evm from "#evm";

type Ctx = Format.Program.Context;
type Activation = "invoke" | "return";

function isPick(ctx: Ctx): ctx is Ctx & { pick: Ctx[] } {
return (
typeof ctx === "object" &&
ctx !== null &&
"pick" in ctx &&
Array.isArray((ctx as { pick: unknown }).pick)
);
}

function isGather(ctx: Ctx): ctx is Ctx & { gather: Ctx[] } {
return (
typeof ctx === "object" &&
ctx !== null &&
"gather" in ctx &&
Array.isArray((ctx as { gather: unknown }).gather)
);
}

/** Whether ctx carries the given activation key anywhere, reaching
* into pick/gather composites. */
export function carriesActivation(
ctx: Ctx | undefined,
key: Activation,
): boolean {
if (!ctx || typeof ctx !== "object") return false;
if (isPick(ctx)) return ctx.pick.some((c) => carriesActivation(c, key));
if (isGather(ctx)) return ctx.gather.some((c) => carriesActivation(c, key));
return key in ctx;
}

/** The first activation value found for the given key, reaching into
* pick/gather composites. */
function findActivation(ctx: Ctx | undefined, key: Activation): unknown {
if (!ctx || typeof ctx !== "object") return undefined;
if (isPick(ctx)) {
for (const c of ctx.pick) {
const v = findActivation(c, key);
if (v !== undefined) return v;
}
return undefined;
}
if (isGather(ctx)) {
for (const c of ctx.gather) {
const v = findActivation(c, key);
if (v !== undefined) return v;
}
return undefined;
}
return (ctx as Record<string, unknown>)[key];
}

/** Remove invoke and return discriminators anywhere in ctx, reaching
* into pick/gather composites. Returns undefined if nothing remains. */
export function stripActivation(ctx: Ctx | undefined): Ctx | undefined {
if (!ctx || typeof ctx !== "object") return ctx;
if (isPick(ctx)) {
const kids = ctx.pick
.map(stripActivation)
.filter((c): c is Ctx => c !== undefined);
if (kids.length === 0) return undefined;
if (kids.length === 1) return kids[0];
return { pick: kids } as Ctx;
}
if (isGather(ctx)) {
const kids = ctx.gather
.map(stripActivation)
.filter((c): c is Ctx => c !== undefined);
if (kids.length === 0) return undefined;
if (kids.length === 1) return kids[0];
return { gather: kids } as Ctx;
}
const rest = { ...(ctx as Record<string, unknown>) };
delete rest.invoke;
delete rest.return;
return Object.keys(rest).length > 0 ? (rest as Ctx) : undefined;
}

/** Attach an activation discriminator, composing it as a flat sibling
* key on a leaf context (per the flat-composition convention), or
* appending it to a pick/gather composite. */
function attachActivation(
ctx: Ctx | undefined,
key: Activation,
value: unknown,
): Ctx {
const marker = { [key]: value } as Ctx;
if (!ctx || typeof ctx !== "object") return marker;
if (isPick(ctx)) return { pick: [...ctx.pick, marker] } as Ctx;
if (isGather(ctx)) return { gather: [...ctx.gather, marker] } as Ctx;
return { ...(ctx as Record<string, unknown>), [key]: value } as Ctx;
}

/**
* Rewrite the ops emitted by one IR instruction (the tail slice
* `instructions[start..]`) so invoke rides only the first op and
* return only the last op, using the discriminators found on the
* instruction's `operationDebug` context. No-op unless that context
* carries invoke and/or return, so it never touches ordinary code.
*/
export function bracketActivation(
instructions: Evm.Instruction[],
start: number,
operationCtx: Ctx | undefined,
): Evm.Instruction[] {
const end = instructions.length; // exclusive
if (end <= start) return instructions;

const hasInvoke = carriesActivation(operationCtx, "invoke");
const hasReturn = carriesActivation(operationCtx, "return");
if (!hasInvoke && !hasReturn) return instructions;

const invokeValue = hasInvoke
? findActivation(operationCtx, "invoke")
: undefined;
const returnValue = hasReturn
? findActivation(operationCtx, "return")
: undefined;

const out = instructions.slice();
for (let i = start; i < end; i++) {
const op = out[i];
let ctx = stripActivation(op.debug?.context);
if (hasInvoke && i === start) {
ctx = attachActivation(ctx, "invoke", invokeValue);
}
if (hasReturn && i === end - 1) {
ctx = attachActivation(ctx, "return", returnValue);
}
out[i] = { ...op, debug: { ...op.debug, context: ctx } };
}
return out;
}
33 changes: 23 additions & 10 deletions packages/bugc/src/evmgen/generation/control-flow/terminator.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type * as Format from "@ethdebug/format";
import type * as Ir from "#ir";
import { Utils as IrUtils } from "#ir";
import type * as Evm from "#evm";
import type { Stack } from "#evm";
import type { State } from "#evmgen/state";
Expand Down Expand Up @@ -411,16 +412,25 @@ function generateReturnEpilogue<S extends Stack>(
/**
* Build JUMP instruction options for a TCO-replaced tail call.
*
* The JUMP carries BOTH contexts in a gather:
* The JUMP carries three keys on a single flat context
* object:
* - return: the previous iteration's return
* - invoke: the new iteration's call
* - transform: ["tailcall"]
*
* Semantically the debugger sees frame depth stay constant
* across the back-edge JUMP: the previous frame pops, the
* new one pushes, on the same instruction. The function's
* terminal RETURN (elsewhere) emits a return context
* normally, popping the final iteration's frame.
*
* The `transform: ["tailcall"]` key is an additive
* annotation: it does not replace the invoke/return pair
* (which state the source-level facts) but tells debuggers
* the pair was realized as a TCO back-edge rather than a
* real frame push/pop, so they can avoid inventing a
* spurious frame.
*
* The invoke mirrors the normal caller-JUMP invoke
* (identity + declaration + code target, no argument
* pointers). The return omits `data` because TCO does not
Expand All @@ -431,7 +441,7 @@ function generateReturnEpilogue<S extends Stack>(
* resolved later by patchInvokeTarget.
*/
function buildTailCallJumpOptions(tailCall: Ir.Block.TailCall): {
debug: { context: Format.Program.Context };
debug: Ir.Instruction.Debug;
} {
const declaration =
tailCall.declarationLoc && tailCall.declarationSourceId
Expand All @@ -441,14 +451,12 @@ function buildTailCallJumpOptions(tailCall: Ir.Block.TailCall): {
}
: undefined;

const returnCtx: Format.Program.Context.Return = {
const combined: Format.Program.Context.Return &
Format.Program.Context.Invoke = {
return: {
identifier: tailCall.function,
...(declaration ? { declaration } : {}),
},
};

const invoke: Format.Program.Context.Invoke = {
invoke: {
jump: true as const,
identifier: tailCall.function,
Expand All @@ -463,11 +471,16 @@ function buildTailCallJumpOptions(tailCall: Ir.Block.TailCall): {
},
};

const gather: Format.Program.Context.Gather = {
gather: [returnCtx, invoke],
// Route through the shared helper so all transform emission
// (fold/tailcall/coalesce/...) composes consistently: the
// `transform` marker becomes a flat sibling key appended to
// any existing transform array.
return {
debug: IrUtils.addTransform(
{ context: combined as Format.Program.Context },
"tailcall",
),
};

return { debug: { context: gather as Format.Program.Context } };
}

/** PUSH an integer as the smallest PUSHn. */
Expand Down
2 changes: 2 additions & 0 deletions packages/bugc/src/evmgen/generation/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,8 @@ function patchInvokeInContext(
const offset = functionRegistry[invoke.identifier];
if (offset === undefined) return;

if (!invoke.target) return;

const ptr = invoke.target.pointer;
if (Format.Pointer.Region.isCode(ptr)) {
ptr.offset = `0x${offset.toString(16)}`;
Expand Down
Loading
Loading