From f2fda2dca29ffab628fca89fabce8c5222fd99cd Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Thu, 2 Apr 2026 08:41:56 -0400 Subject: [PATCH 1/4] bugc: update invoke contexts per spec fix (#207) - Callee JUMPDEST: full invoke with arg pointers and code target (resolved at module-level patching) - Caller JUMP: identity + code target only, no arg pointers - Target pointers changed from stack to code location --- .../bugc/src/evmgen/call-contexts.test.ts | 184 ++++++++++-------- .../generation/control-flow/terminator.ts | 34 +--- .../bugc/src/evmgen/generation/function.ts | 45 ++++- 3 files changed, 152 insertions(+), 111 deletions(-) diff --git a/packages/bugc/src/evmgen/call-contexts.test.ts b/packages/bugc/src/evmgen/call-contexts.test.ts index 244509e91..d1178850f 100644 --- a/packages/bugc/src/evmgen/call-contexts.test.ts +++ b/packages/bugc/src/evmgen/call-contexts.test.ts @@ -68,52 +68,44 @@ 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) + const ptr = call.target.pointer as Record; + expect(ptr.location).toBe("code"); + expect(ptr.offset).toBeDefined(); + + // 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); @@ -142,32 +134,53 @@ 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 + const ptr = call.target.pointer as Record; + expect(ptr.location).toBe("code"); + expect(ptr.offset).toBeDefined(); - // 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); @@ -331,32 +344,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", () => { diff --git a/packages/bugc/src/evmgen/generation/control-flow/terminator.ts b/packages/bugc/src/evmgen/generation/control-flow/terminator.ts index 82df52806..bc0ba0131 100644 --- a/packages/bugc/src/evmgen/generation/control-flow/terminator.ts +++ b/packages/bugc/src/evmgen/generation/control-flow/terminator.ts @@ -210,19 +210,11 @@ export function generateCallTerminator( } // 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 @@ -232,10 +224,6 @@ export function generateCallTerminator( } : 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, @@ -243,20 +231,16 @@ export function generateCallTerminator( ...(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, diff --git a/packages/bugc/src/evmgen/generation/function.ts b/packages/bugc/src/evmgen/generation/function.ts index 896f3498f..759c30aec 100644 --- a/packages/bugc/src/evmgen/generation/function.ts +++ b/packages/bugc/src/evmgen/generation/function.ts @@ -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"; @@ -53,8 +53,9 @@ function generatePrologue( ...(declaration ? { declaration } : {}), target: { pointer: { - location: "stack" as const, - slot: 0, + location: "code" as const, + offset: 0, + length: 1, }, }, ...(argPointers.length > 0 && { @@ -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, +): 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)}`; + } +} From 9af7a0712a1311fe8e1c14c356f532e5145ba8d0 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Thu, 2 Apr 2026 08:45:25 -0400 Subject: [PATCH 2/4] fix: use Pointer.Region.isCode type guard in tests --- packages/bugc/src/evmgen/call-contexts.test.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/bugc/src/evmgen/call-contexts.test.ts b/packages/bugc/src/evmgen/call-contexts.test.ts index d1178850f..43f1b76cb 100644 --- a/packages/bugc/src/evmgen/call-contexts.test.ts +++ b/packages/bugc/src/evmgen/call-contexts.test.ts @@ -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; @@ -97,9 +97,7 @@ code { expect(typeof invoke.declaration!.range!.length).toBe("number"); // Target should be a code pointer (not stack) - const ptr = call.target.pointer as Record; - expect(ptr.location).toBe("code"); - expect(ptr.offset).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) @@ -157,9 +155,7 @@ code { expect(call.identifier).toBe("add"); // Target should be a code pointer - const ptr = call.target.pointer as Record; - expect(ptr.location).toBe("code"); - expect(ptr.offset).toBeDefined(); + expect(Pointer.Region.isCode(call.target.pointer)).toBe(true); // Should have argument pointers matching // function parameters From 501025488af0264cfc510839e5fae67524d36b4e Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Thu, 2 Apr 2026 08:54:50 -0400 Subject: [PATCH 3/4] programs-react: propagate arg names from callee JUMPDEST The invoke spec change moved argument pointers from the caller JUMP to the callee entry JUMPDEST. The call stack builder was updating argumentPointers from the duplicate callee step but not argumentNames, causing names to show as _0 instead of the actual parameter name. --- packages/programs-react/src/utils/mockTrace.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/programs-react/src/utils/mockTrace.ts b/packages/programs-react/src/utils/mockTrace.ts index ddbee9dfa..26a912fc7 100644 --- a/packages/programs-react/src/utils/mockTrace.ts +++ b/packages/programs-react/src/utils/mockTrace.ts @@ -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); From 6189b2630f2560ae08bc92515a1461167a4396f2 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Thu, 2 Apr 2026 10:32:08 -0400 Subject: [PATCH 4/4] web: propagate arg names from callee JUMPDEST in TraceDrawer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same fix as the programs-react mockTrace.ts change — the duplicated call stack builder in TraceDrawer.tsx also needs to update argumentNames from the callee entry step, not just argumentPointers. --- packages/web/src/theme/ProgramExample/TraceDrawer.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/web/src/theme/ProgramExample/TraceDrawer.tsx b/packages/web/src/theme/ProgramExample/TraceDrawer.tsx index e4cff9529..cd9b37416 100644 --- a/packages/web/src/theme/ProgramExample/TraceDrawer.tsx +++ b/packages/web/src/theme/ProgramExample/TraceDrawer.tsx @@ -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({