diff --git a/packages/bugc/src/evmgen/call-contexts.test.ts b/packages/bugc/src/evmgen/call-contexts.test.ts index 244509e91..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; @@ -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); @@ -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); @@ -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", () => { 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)}`; + } +} 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); 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({