From ddafee6a6d6b3ad7f647c2d3805004c5680d4ab1 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Thu, 16 Jul 2026 01:36:03 -0400 Subject: [PATCH] bugc: carry the call-site source range on invoke/return contexts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A function call's caller JUMP (invoke) and continuation JUMPDEST (return) previously carried only the callee's identity and definition range — never the source range of the call expression itself. Stepping onto either mapped to the callee's definition, not to the call site. Compose the call-site `code` range (already in hand as the call op's operationDebug) flat alongside the invoke and return contexts. invoke/return and code are disjoint keys, so this is plain flat composition. Behavior and the invoke/return identity are unchanged. Covers the real caller JUMP (O0/O1) and the continuation JUMPDEST. Deferred: the TCO back-edge JUMP (needs the call-site loc threaded into the tail-call IR) and inlined calls (the surviving instruction already carries the callee-body range, so the call site must be gathered rather than flat-composed) — both follow separately. --- packages/bugc/src/evmgen/generation/block.ts | 15 +++- .../generation/control-flow/terminator.ts | 23 ++++- .../bugc/src/evmgen/invoke-callsite.test.ts | 90 +++++++++++++++++++ 3 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 packages/bugc/src/evmgen/invoke-callsite.test.ts diff --git a/packages/bugc/src/evmgen/generation/block.ts b/packages/bugc/src/evmgen/generation/block.ts index ab203771c9..3198da6309 100644 --- a/packages/bugc/src/evmgen/generation/block.ts +++ b/packages/bugc/src/evmgen/generation/block.ts @@ -67,6 +67,7 @@ export function generate( // Check if this is a call continuation let isContinuation = false; let calledFunction = ""; + let callSiteCode: Format.Program.Context.Code["code"] | undefined; if (func && predecessor) { const predBlock = func.blocks.get(predecessor); if ( @@ -75,6 +76,15 @@ export function generate( ) { isContinuation = true; calledFunction = predBlock.terminator.function; + // The continuation resumes at the call expression, so + // carry the call site's source range onto the return + // context (disjoint keys — flat composition). + const ctx = predBlock.terminator.operationDebug?.context as + | Record + | undefined; + if (ctx && "code" in ctx) { + callSiteCode = ctx.code as Format.Program.Context.Code["code"]; + } } } @@ -105,7 +115,10 @@ export function generate( }, }; const continuationDebug = { - context: returnCtx as Format.Program.Context, + context: { + ...returnCtx, + ...(callSiteCode ? { code: callSiteCode } : {}), + } as Format.Program.Context, }; result = result.then(JUMPDEST({ debug: continuationDebug })); } else { diff --git a/packages/bugc/src/evmgen/generation/control-flow/terminator.ts b/packages/bugc/src/evmgen/generation/control-flow/terminator.ts index 933f104f84..9334d11a1d 100644 --- a/packages/bugc/src/evmgen/generation/control-flow/terminator.ts +++ b/packages/bugc/src/evmgen/generation/control-flow/terminator.ts @@ -10,6 +10,20 @@ import { type Transition, operations, pipe } from "#evmgen/operations"; import { valueId, loadValue } from "../values/index.js"; +/** + * Extract the `code` source-range context from an instruction or + * terminator debug, if present. + */ +function codeContext( + debug: { context?: Format.Program.Context } | undefined, +): Format.Program.Context.Code["code"] | undefined { + const ctx = debug?.context as Record | undefined; + if (ctx && "code" in ctx) { + return ctx.code as Format.Program.Context.Code["code"]; + } + return undefined; +} + /** * Generate code for a block terminator */ @@ -249,8 +263,15 @@ export function generateCallTerminator( }, }, }; + // Compose the call-site source range (from the call op's debug) + // flat alongside the invoke, so the caller JUMP maps back to the + // call expression. invoke and code are disjoint keys. + const callSiteCode = codeContext(debug); const invokeContext = { - context: invoke as Format.Program.Context, + context: { + ...invoke, + ...(callSiteCode ? { code: callSiteCode } : {}), + } as Format.Program.Context, }; currentState = { diff --git a/packages/bugc/src/evmgen/invoke-callsite.test.ts b/packages/bugc/src/evmgen/invoke-callsite.test.ts new file mode 100644 index 0000000000..21e4862545 --- /dev/null +++ b/packages/bugc/src/evmgen/invoke-callsite.test.ts @@ -0,0 +1,90 @@ +/** + * The call-site source range is carried on the invoke/return + * contexts of a function call, so a debugger stepping onto the + * caller JUMP or the continuation JUMPDEST maps back to the call + * expression (not just the callee's definition). + */ +import { describe, it, expect } from "vitest"; + +import { compile } from "#compiler"; +import { executeProgram } from "#test/evm/behavioral"; +import { Program } from "@ethdebug/format"; +import type * as Format from "@ethdebug/format"; + +const { Context } = Program; + +const source = `name Adder; +define { + function add(a: uint256, b: uint256) -> uint256 { return a + b; }; +} +storage { [0] r: uint256; } +create { r = 0; } +code { r = add(3, 4); }`; + +async function runtimeProgram(): Promise { + const result = await compile({ + to: "bytecode", + source, + optimizer: { level: 0 }, + }); + if (!result.success) throw new Error("compile failed"); + return result.value.bytecode.runtimeProgram; +} + +function hasCodeRange(ctx: Record): boolean { + const code = ctx.code as { range?: { offset?: unknown; length?: unknown } }; + return ( + !!code && + typeof code.range?.offset === "number" && + typeof code.range?.length === "number" + ); +} + +describe("call-site source range on invoke/return contexts", () => { + it("the caller JUMP invoke context carries a call-site code range", async () => { + const program = await runtimeProgram(); + const invokeJump = program.instructions.find( + (i) => + i.operation?.mnemonic === "JUMP" && + i.context !== undefined && + Context.isInvoke(i.context), + ); + expect(invokeJump, "invoke JUMP").toBeDefined(); + expect(hasCodeRange(invokeJump!.context as Record)).toBe( + true, + ); + }); + + it("the continuation JUMPDEST return context carries a call-site code range", async () => { + const program = await runtimeProgram(); + const contJumpdest = program.instructions.find( + (i) => + i.operation?.mnemonic === "JUMPDEST" && + i.context !== undefined && + Context.isReturn(i.context), + ); + expect(contJumpdest, "continuation JUMPDEST").toBeDefined(); + expect(hasCodeRange(contJumpdest!.context as Record)).toBe( + true, + ); + }); + + it("still identifies the invoke by name and keeps behavior", async () => { + const program = await runtimeProgram(); + const invokeJump = program.instructions.find( + (i) => + i.operation?.mnemonic === "JUMP" && + i.context !== undefined && + Context.isInvoke(i.context), + ); + const ctx = invokeJump!.context as Format.Program.Context.Invoke; + expect(ctx.invoke.identifier).toBe("add"); + + const res = await executeProgram(source, { + calldata: "", + optimizationLevel: 0, + }); + expect(res.callSuccess).toBe(true); + expect(await res.getStorage(0n)).toBe(7n); + }); +});