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
15 changes: 14 additions & 1 deletion packages/bugc/src/evmgen/generation/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export function generate<S extends Stack>(
// 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 (
Expand All @@ -75,6 +76,15 @@ export function generate<S extends Stack>(
) {
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<string, unknown>
| undefined;
if (ctx && "code" in ctx) {
callSiteCode = ctx.code as Format.Program.Context.Code["code"];
}
}
}

Expand Down Expand Up @@ -105,7 +115,10 @@ export function generate<S extends Stack>(
},
};
const continuationDebug = {
context: returnCtx as Format.Program.Context,
context: {
...returnCtx,
...(callSiteCode ? { code: callSiteCode } : {}),
} as Format.Program.Context,
};
result = result.then(JUMPDEST({ debug: continuationDebug }));
} else {
Expand Down
23 changes: 22 additions & 1 deletion packages/bugc/src/evmgen/generation/control-flow/terminator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> | undefined;
if (ctx && "code" in ctx) {
return ctx.code as Format.Program.Context.Code["code"];
}
return undefined;
}

/**
* Generate code for a block terminator
*/
Expand Down Expand Up @@ -249,8 +263,15 @@ export function generateCallTerminator<S extends Stack>(
},
},
};
// 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 = {
Expand Down
90 changes: 90 additions & 0 deletions packages/bugc/src/evmgen/invoke-callsite.test.ts
Original file line number Diff line number Diff line change
@@ -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<Format.Program> {
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<string, unknown>): 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<string, unknown>)).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<string, unknown>)).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);
});
});
Loading