From 0e66e1c07a656babd0df528e5de284c452398659 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Tue, 6 Oct 2026 21:31:35 -0400 Subject: [PATCH] bugc: give a caller's call JUMP the invoke's arguments --- packages/bugc/CHANGELOG.md | 7 +++ .../bugc/src/evmgen/call-contexts.test.ts | 51 +++++++++++++++++-- .../evmgen/generation/control-flow/index.ts | 6 ++- .../generation/control-flow/terminator.ts | 30 +++++++++-- .../bugc/src/evmgen/generation/function.ts | 14 +---- 5 files changed, 87 insertions(+), 21 deletions(-) diff --git a/packages/bugc/CHANGELOG.md b/packages/bugc/CHANGELOG.md index f7b5d6634e..0e6145fd4f 100644 --- a/packages/bugc/CHANGELOG.md +++ b/packages/bugc/CHANGELOG.md @@ -89,6 +89,12 @@ support. Changes to the specification itself are tracked in the root has the branch's context: its source range, its variables (the storage variables too) and its transforms. Before, it had no context, so a debugger listed no variables while it ran ([#356]). +- The `invoke` context on a caller's `JUMP` into a function now has the + call's `arguments`, as the callee's entry `JUMPDEST` does: the `JUMP` + leaves them on the stack, and the `JUMPDEST` does not change it. + Before, only the `JUMPDEST`'s `invoke` had them, so a debugger that + opens the frame on the step after the `JUMP` showed it for one step + without its arguments ([#354]). - A call to a function with no return type now compiles as a statement, as in `bump();`, at every optimization level. The function's `return` context has no `data`. Before, IR generation failed with "Cannot convert @@ -300,4 +306,5 @@ First publication. [#349]: https://github.com/ethdebug/format/pull/349 [#351]: https://github.com/ethdebug/format/pull/351 [#352]: https://github.com/ethdebug/format/pull/352 +[#354]: https://github.com/ethdebug/format/pull/354 [#356]: https://github.com/ethdebug/format/pull/356 diff --git a/packages/bugc/src/evmgen/call-contexts.test.ts b/packages/bugc/src/evmgen/call-contexts.test.ts index ac21216188..da8248f419 100644 --- a/packages/bugc/src/evmgen/call-contexts.test.ts +++ b/packages/bugc/src/evmgen/call-contexts.test.ts @@ -3,6 +3,9 @@ import { describe, it, expect } from "vitest"; import { compile } from "#compiler"; import type * as Format from "@ethdebug/format"; import { Pointer, Program } from "@ethdebug/format"; +import { createMachineState } from "@ethdebug/evm"; +import { dereference } from "@ethdebug/pointers"; +import { traceLocals } from "#test/evm/locals"; const { Context } = Program; const { Invocation } = Context.Invoke; @@ -70,7 +73,7 @@ code { it( "should emit invoke context on caller JUMP " + - "(identity + code target, no args)", + "(identity, code target and args)", async () => { const program = await compileProgram(source); @@ -100,12 +103,52 @@ code { expect(call.target).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) - expect(call.arguments).toBeUndefined(); + // The JUMP's invoke opens the frame, so it names the + // arguments too: the JUMP leaves them on the stack, where the + // callee's entry JUMPDEST (which does not change the stack) + // finds them + expect(call.arguments).toBeDefined(); + expect(call.arguments!.pointer).toEqual({ + group: [ + { name: "a", location: "stack", slot: 1 }, + { name: "b", location: "stack", slot: 0 }, + ], + }); }, ); + it("should give the caller JUMP's arguments their values", async () => { + const { executor, steps, instructionAt } = await traceLocals(source); + + const index = steps.findIndex((step) => { + const instruction = instructionAt(step); + return ( + instruction?.operation?.mnemonic === "JUMP" && + Context.isInvoke(instruction.context) + ); + }); + expect(index).toBeGreaterThanOrEqual(0); + + // Contexts are postconditions: read the JUMP's pointers against + // the state after it, at the callee's entry JUMPDEST + const { invoke } = instructionAt(steps[index])! + .context as Format.Program.Context.Invoke; + const state = createMachineState(executor, { + traceStep: steps[index + 1], + }); + const cursor = await dereference( + (invoke as InternalCall).arguments!.pointer, + { state }, + ); + const view = await cursor.view(state); + const values = await Promise.all( + ["a", "b"].map(async (name) => + (await view.read(view.regions.lookup[name])).asUint(), + ), + ); + expect(values).toEqual([10n, 20n]); + }); + it("should emit return context on the callee's exit JUMP", async () => { const program = await compileProgram(source); diff --git a/packages/bugc/src/evmgen/generation/control-flow/index.ts b/packages/bugc/src/evmgen/generation/control-flow/index.ts index 6a1a06b1a8..73275ef413 100644 --- a/packages/bugc/src/evmgen/generation/control-flow/index.ts +++ b/packages/bugc/src/evmgen/generation/control-flow/index.ts @@ -1 +1,5 @@ -export { generateTerminator, generateCallTerminator } from "./terminator.js"; +export { + generateTerminator, + generateCallTerminator, + invokeArguments, +} from "./terminator.js"; diff --git a/packages/bugc/src/evmgen/generation/control-flow/terminator.ts b/packages/bugc/src/evmgen/generation/control-flow/terminator.ts index c4d945042f..6d10a33215 100644 --- a/packages/bugc/src/evmgen/generation/control-flow/terminator.ts +++ b/packages/bugc/src/evmgen/generation/control-flow/terminator.ts @@ -306,10 +306,10 @@ export function generateCallTerminator( currentState = loadValue(arg, { debug })(currentState); } - // Push function address and jump. - // The JUMP gets a simplified invoke context with - // identity and code target only; the full invoke - // with arg pointers lives on the callee JUMPDEST. + // Push function address and jump. The JUMP's invoke opens the + // callee's frame, so it carries the argument pointers, as the + // callee's entry JUMPDEST does: the JUMP leaves the arguments on + // the stack, and the JUMPDEST does not change it. const funcAddrPatchIndex = currentState.instructions.length; // Build declaration source range if available @@ -333,6 +333,7 @@ export function generateCallTerminator( length: 1, }, }, + ...invokeArguments(targetFunc?.parameters ?? []), }, }; // Compose the call-site source range (from the call op's debug) @@ -538,6 +539,27 @@ function withReturn( }; } +/** + * The `arguments` of an internal call's `invoke`, as they are once the + * call's JUMP has run: on the stack, the first argument deepest. + */ +export function invokeArguments( + parameters: Ir.Function.Parameter[], +): Pick { + if (parameters.length === 0) return {}; + return { + arguments: { + pointer: { + group: parameters.map((parameter, i) => ({ + ...(parameter.name ? { name: parameter.name } : {}), + location: "stack" as const, + slot: parameters.length - 1 - i, + })), + }, + }, + }; +} + /** * Build JUMP instruction options for a TCO-replaced tail call. * diff --git a/packages/bugc/src/evmgen/generation/function.ts b/packages/bugc/src/evmgen/generation/function.ts index 3908182369..95c0c21822 100644 --- a/packages/bugc/src/evmgen/generation/function.ts +++ b/packages/bugc/src/evmgen/generation/function.ts @@ -12,6 +12,7 @@ import { type Layout, Memory } from "#evmgen/analysis"; import type { Error as EvmgenError } from "#evmgen/errors"; import * as Block from "./block.js"; +import { invokeArguments } from "./control-flow/index.js"; import { serialize } from "../serialize.js"; import { type Transition } from "../operations.js"; @@ -31,11 +32,6 @@ function generatePrologue( // Add JUMPDEST with function entry annotation. // After this JUMPDEST executes, the callee's args are // on the stack (first arg deepest). - const argPointers = params.map((p, i) => ({ - ...(p.name ? { name: p.name } : {}), - location: "stack" as const, - slot: params.length - 1 - i, - })); // Build declaration source range if available const declaration = @@ -58,13 +54,7 @@ function generatePrologue( length: 1, }, }, - ...(argPointers.length > 0 && { - arguments: { - pointer: { - group: argPointers, - }, - }, - }), + ...invokeArguments(params), }, }; const entryDebug = {