Skip to content
Closed
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
7 changes: 7 additions & 0 deletions packages/bugc/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
51 changes: 47 additions & 4 deletions packages/bugc/src/evmgen/call-contexts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);

Expand Down
6 changes: 5 additions & 1 deletion packages/bugc/src/evmgen/generation/control-flow/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
export { generateTerminator, generateCallTerminator } from "./terminator.js";
export {
generateTerminator,
generateCallTerminator,
invokeArguments,
} from "./terminator.js";
30 changes: 26 additions & 4 deletions packages/bugc/src/evmgen/generation/control-flow/terminator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,10 +306,10 @@ export function generateCallTerminator<S extends Stack>(
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
Expand All @@ -333,6 +333,7 @@ export function generateCallTerminator<S extends Stack>(
length: 1,
},
},
...invokeArguments(targetFunc?.parameters ?? []),
},
};
// Compose the call-site source range (from the call op's debug)
Expand Down Expand Up @@ -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<Format.Program.Context.Invoke.Invocation.InternalCall, "arguments"> {
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.
*
Expand Down
14 changes: 2 additions & 12 deletions packages/bugc/src/evmgen/generation/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -31,11 +32,6 @@ function generatePrologue<S extends Stack>(
// 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 =
Expand All @@ -58,13 +54,7 @@ function generatePrologue<S extends Stack>(
length: 1,
},
},
...(argPointers.length > 0 && {
arguments: {
pointer: {
group: argPointers,
},
},
}),
...invokeArguments(params),
},
};
const entryDebug = {
Expand Down
Loading