Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
182 changes: 98 additions & 84 deletions packages/bugc/src/evmgen/call-contexts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { describe, it, expect } from "vitest";

import { compile } from "#compiler";
import type * as Format from "@ethdebug/format";
import { Program } from "@ethdebug/format";
import { Pointer, Program } from "@ethdebug/format";

const { Context } = Program;
const { Invocation } = Context.Invoke;
Expand Down Expand Up @@ -68,52 +68,42 @@ code {
result = add(10, 20);
}`;

it("should emit invoke context on caller JUMP", async () => {
const program = await compileProgram(source);
it(
"should emit invoke context on caller JUMP " +
"(identity + code target, no args)",
async () => {
const program = await compileProgram(source);

const invokeJumps = findInstructionsWithContext(
program,
"JUMP",
Context.isInvoke,
);

expect(invokeJumps.length).toBeGreaterThanOrEqual(1);
const invokeJumps = findInstructionsWithContext(
program,
"JUMP",
Context.isInvoke,
);

const { invoke } = invokeJumps[0].context;
expect(Invocation.isInternalCall(invoke)).toBe(true);
expect(invokeJumps.length).toBeGreaterThanOrEqual(1);

const call = invoke as InternalCall;
expect(call.jump).toBe(true);
expect(call.identifier).toBe("add");
const { invoke } = invokeJumps[0].context;
expect(Invocation.isInternalCall(invoke)).toBe(true);

// Should have declaration source range
expect(invoke.declaration).toBeDefined();
expect(invoke.declaration!.source).toEqual({ id: "0" });
expect(invoke.declaration!.range).toBeDefined();
expect(typeof invoke.declaration!.range!.offset).toBe("number");
expect(typeof invoke.declaration!.range!.length).toBe("number");

// Should have target pointer
expect(call.target.pointer).toBeDefined();

// Should have argument pointers
expect(call.arguments).toBeDefined();
const group = (call.arguments!.pointer as { group: unknown[] }).group;

expect(group).toHaveLength(2);
// First arg (a) is deepest on stack
expect(group[0]).toEqual({
name: "a",
location: "stack",
slot: 1,
});
// Second arg (b) is on top
expect(group[1]).toEqual({
name: "b",
location: "stack",
slot: 0,
});
});
const call = invoke as InternalCall;
expect(call.jump).toBe(true);
expect(call.identifier).toBe("add");

// Should have declaration source range
expect(invoke.declaration).toBeDefined();
expect(invoke.declaration!.source).toEqual({ id: "0" });
expect(invoke.declaration!.range).toBeDefined();
expect(typeof invoke.declaration!.range!.offset).toBe("number");
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);

// Caller JUMP should NOT have argument pointers
// (args live on the callee JUMPDEST invoke context)
expect(call.arguments).toBeUndefined();
},
);

it("should emit return context on continuation JUMPDEST", async () => {
const program = await compileProgram(source);
Expand Down Expand Up @@ -142,32 +132,51 @@ code {
});
});

it("should emit invoke context on callee entry JUMPDEST", async () => {
const program = await compileProgram(source);
it(
"should emit invoke context on callee entry " +
"JUMPDEST with args and code target",
async () => {
const program = await compileProgram(source);

// The callee entry point, not the continuation
const invokeJumpdests = findInstructionsWithContext(
program,
"JUMPDEST",
Context.isInvoke,
);
// The callee entry point, not the continuation
const invokeJumpdests = findInstructionsWithContext(
program,
"JUMPDEST",
Context.isInvoke,
);

expect(invokeJumpdests.length).toBeGreaterThanOrEqual(1);
expect(invokeJumpdests.length).toBeGreaterThanOrEqual(1);

const { invoke } = invokeJumpdests[0].context;
expect(Invocation.isInternalCall(invoke)).toBe(true);

const { invoke } = invokeJumpdests[0].context;
expect(Invocation.isInternalCall(invoke)).toBe(true);
const call = invoke as InternalCall;
expect(call.jump).toBe(true);
expect(call.identifier).toBe("add");

const call = invoke as InternalCall;
expect(call.jump).toBe(true);
expect(call.identifier).toBe("add");
// Target should be a code pointer
expect(Pointer.Region.isCode(call.target.pointer)).toBe(true);

// Should have argument pointers matching
// function parameters
expect(call.arguments).toBeDefined();
const group = (call.arguments!.pointer as { group: unknown[] }).group;
// Should have argument pointers matching
// function parameters
expect(call.arguments).toBeDefined();
const group = (call.arguments!.pointer as { group: unknown[] }).group;

expect(group).toHaveLength(2);
});
expect(group).toHaveLength(2);
// First arg (a) is deepest on stack
expect(group[0]).toEqual({
name: "a",
location: "stack",
slot: 1,
});
// Second arg (b) is on top
expect(group[1]).toEqual({
name: "b",
location: "stack",
slot: 0,
});
},
);

it("should emit contexts in correct instruction order", async () => {
const program = await compileProgram(source);
Expand Down Expand Up @@ -331,32 +340,37 @@ code {
result = double(7);
}`;

it("should emit single-element argument group", async () => {
const program = await compileProgram(singleArgSource);
it(
"should emit single-element argument group " + "on callee JUMPDEST",
async () => {
const program = await compileProgram(singleArgSource);

const invokeJumps = findInstructionsWithContext(
program,
"JUMP",
Context.isInvoke,
);
// Args are on the callee JUMPDEST, not the
// caller JUMP
const invokeJumpdests = findInstructionsWithContext(
program,
"JUMPDEST",
Context.isInvoke,
);

expect(invokeJumps.length).toBeGreaterThanOrEqual(1);
expect(invokeJumpdests.length).toBeGreaterThanOrEqual(1);

const { invoke } = invokeJumps[0].context;
expect(Invocation.isInternalCall(invoke)).toBe(true);
const { invoke } = invokeJumpdests[0].context;
expect(Invocation.isInternalCall(invoke)).toBe(true);

const call = invoke as InternalCall;
expect(call.arguments).toBeDefined();
const group = (call.arguments!.pointer as { group: unknown[] }).group;
const call = invoke as InternalCall;
expect(call.arguments).toBeDefined();
const group = (call.arguments!.pointer as { group: unknown[] }).group;

// Single arg at stack slot 0
expect(group).toHaveLength(1);
expect(group[0]).toEqual({
name: "x",
location: "stack",
slot: 0,
});
});
// Single arg at stack slot 0
expect(group).toHaveLength(1);
expect(group[0]).toEqual({
name: "x",
location: "stack",
slot: 0,
});
},
);
});

describe("return epilogue source maps", () => {
Expand Down
34 changes: 9 additions & 25 deletions packages/bugc/src/evmgen/generation/control-flow/terminator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,19 +210,11 @@ export function generateCallTerminator<S extends Stack>(
}

// Push function address and jump.
// The JUMP gets an invoke context: after JUMP executes,
// the function has been entered with args on the stack.
// The JUMP gets a simplified invoke context with
// identity and code target only; the full invoke
// with arg pointers lives on the callee JUMPDEST.
const funcAddrPatchIndex = currentState.instructions.length;

// Build argument pointers: after the JUMP, the callee
// sees args on the stack in order (first arg deepest).
const params = targetFunc?.parameters;
const argPointers = args.map((_arg, i) => ({
...(params?.[i]?.name ? { name: params[i].name } : {}),
location: "stack" as const,
slot: args.length - 1 - i,
}));

// Build declaration source range if available
const declaration =
targetFunc?.loc && targetFunc?.sourceId
Expand All @@ -232,31 +224,23 @@ export function generateCallTerminator<S extends Stack>(
}
: undefined;

// Invoke context describes state after JUMP executes:
// the callee has been entered with args on the stack.
// target points to the function address at stack slot 0
// (consumed by JUMP, but describes the call target).
const invoke: Format.Program.Context.Invoke = {
invoke: {
jump: true as const,
identifier: funcName,
...(declaration ? { declaration } : {}),
target: {
pointer: {
location: "stack" as const,
slot: 0,
location: "code" as const,
offset: 0,
length: 1,
},
},
...(argPointers.length > 0 && {
arguments: {
pointer: {
group: argPointers,
},
},
}),
},
};
const invokeContext = { context: invoke as Format.Program.Context };
const invokeContext = {
context: invoke as Format.Program.Context,
};

currentState = {
...currentState,
Expand Down
45 changes: 42 additions & 3 deletions packages/bugc/src/evmgen/generation/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Function-level code generation
*/

import type * as Format from "@ethdebug/format";
import * as Format from "@ethdebug/format";
import * as Ir from "#ir";
import type * as Evm from "#evm";
import type { Stack } from "#evm";
Expand Down Expand Up @@ -53,8 +53,9 @@ function generatePrologue<S extends Stack>(
...(declaration ? { declaration } : {}),
target: {
pointer: {
location: "stack" as const,
slot: 0,
location: "code" as const,
offset: 0,
length: 1,
},
},
...(argPointers.length > 0 && {
Expand Down Expand Up @@ -483,8 +484,46 @@ export function patchFunctionCalls(
patchedBytecode[bytePos + 1] = lowByte;
}

// Patch invoke context code pointers. During codegen,
// invoke targets use placeholder offset 0; resolve them
// to the actual function entry from the registry.
for (const inst of patchedInstructions) {
patchInvokeTarget(inst, functionRegistry);
}

return {
bytecode: patchedBytecode,
instructions: patchedInstructions,
};
}

/**
* Resolve placeholder code pointer offsets in invoke debug
* contexts. The codegen emits `{ location: "code", offset: 0 }`
* as a placeholder; this replaces offset with the actual
* function entry address from the registry.
*/
function patchInvokeTarget(
inst: Evm.Instruction,
functionRegistry: Record<string, number>,
): void {
const ctx = inst.debug?.context;
if (!ctx) return;

if (!Format.Program.Context.isInvoke(ctx)) return;

const { invoke } = ctx;
if (!Format.Program.Context.Invoke.Invocation.isInternalCall(invoke)) {
return;
}

if (!invoke.identifier) return;

const offset = functionRegistry[invoke.identifier];
if (offset === undefined) return;

const ptr = invoke.target.pointer;
if (Format.Pointer.Region.isCode(ptr)) {
ptr.offset = `0x${offset.toString(16)}`;
}
}
4 changes: 3 additions & 1 deletion packages/programs-react/src/utils/mockTrace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,9 +315,11 @@ export function buildCallStack(
if (isDuplicate) {
// Use the callee entry step for resolution —
// the argument pointers reference stack slots
// that are valid at the JUMPDEST, not the JUMP
// that are valid at the JUMPDEST, not the JUMP.
// Argument names also live on the callee entry.
const argResult = extractArgInfo(instruction);
top.stepIndex = i;
top.argumentNames = argResult?.names ?? top.argumentNames;
top.argumentPointers = argResult?.pointers;
} else {
const argResult = extractArgInfo(instruction);
Expand Down
4 changes: 3 additions & 1 deletion packages/web/src/theme/ProgramExample/TraceDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,10 @@ function TraceDrawerContent(): JSX.Element {
if (isDuplicate) {
// Use the callee entry step for resolution —
// argument pointers reference stack slots
// valid at the JUMPDEST, not the JUMP
// valid at the JUMPDEST, not the JUMP.
// Argument names also live on the callee entry.
top.stepIndex = i;
top.argumentNames = info.argumentNames ?? top.argumentNames;
top.argumentPointers = info.argumentPointers;
} else {
frames.push({
Expand Down
Loading