diff --git a/packages/bugc/CHANGELOG.md b/packages/bugc/CHANGELOG.md index f7b5d6634e..b389b3f480 100644 --- a/packages/bugc/CHANGELOG.md +++ b/packages/bugc/CHANGELOG.md @@ -89,6 +89,13 @@ 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 instructions that run before a function's first statement now have + a `variables` context: the memory setup at the start of the code (and + `create`) blocks, and a function's entry `JUMPDEST` and prologue. They + list the storage variables and the locals in scope, a local without a + pointer: a parameter is not yet in its home there, and the `invoke`'s + `arguments` point at it on the stack. Before, they listed no variables, + so a debugger showed none for those steps ([#358]). - 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 @@ -301,3 +308,4 @@ First publication. [#351]: https://github.com/ethdebug/format/pull/351 [#352]: https://github.com/ethdebug/format/pull/352 [#356]: https://github.com/ethdebug/format/pull/356 +[#358]: https://github.com/ethdebug/format/pull/358 diff --git a/packages/bugc/src/evmgen/debug/local-variables.soundness.test.ts b/packages/bugc/src/evmgen/debug/local-variables.soundness.test.ts index f2ab39d7f7..ef8018ffed 100644 --- a/packages/bugc/src/evmgen/debug/local-variables.soundness.test.ts +++ b/packages/bugc/src/evmgen/debug/local-variables.soundness.test.ts @@ -1052,3 +1052,61 @@ describe.each(levels)("locals hold the program's values at O%i", (level) => { (program) => check(program, level), ); }); + +describe("variables before a function's first statement", () => { + // Contexts are postconditions: the step after an instruction shows + // its context. Every step lists the storage variable; from the + // callee's entry JUMPDEST through its prologue, the parameters. + const source = `name Prelude; +define { + function add(a: uint256, b: uint256) -> uint256 { + let c = a + b; + return c; + }; +} +storage { [0] r: uint256; } +code { let x = 7; r = add(x, 3); }`; + + for (const level of [0, 1] as Level[]) { + it(`lists variables at every step (O${level})`, async () => { + const { steps, instructionAt } = await traceLocals(source, level); + const unlisted = steps + .map((_, i) => i) + .filter( + (i) => + i > 0 && + localsOf(instructionAt(steps[i - 1])?.context).length === 0, + ); + expect(unlisted).toEqual([]); + }); + + it(`lists the parameters in the prologue (O${level})`, async () => { + const { program } = await traceLocals(source, level); + const at = program.instructions.findIndex( + (instruction) => + instruction.operation?.mnemonic === "JUMPDEST" && + Program.Context.isInvoke(instruction.context), + ); + // The entry JUMPDEST, then the prologue: the ops that map to the + // whole function, before its body's + const whole = codeOffset(program.instructions[at + 1].context); + const rest = program.instructions.slice(at + 1); + const prologue = [ + program.instructions[at], + ...rest.slice( + 0, + rest.findIndex( + (instruction) => codeOffset(instruction.context) !== whole, + ), + ), + ]; + expect(prologue.length).toBeGreaterThan(1); + for (const instruction of prologue) { + const names = localsOf(instruction.context).map((v) => v.identifier); + expect(names, `pc ${instruction.offset}`).toEqual( + expect.arrayContaining(["r", "a", "b"]), + ); + } + }); + } +}); diff --git a/packages/bugc/src/evmgen/debug/local-variables.ts b/packages/bugc/src/evmgen/debug/local-variables.ts index a8376c2195..845135db09 100644 --- a/packages/bugc/src/evmgen/debug/local-variables.ts +++ b/packages/bugc/src/evmgen/debug/local-variables.ts @@ -459,6 +459,33 @@ export function withoutFrameLocals( }; } +/** + * The variables in scope before a function's entry block: during the + * memory setup of main or create, or a function's entry JUMPDEST and + * prologue. They are the entry block's, as listed just before its first + * instruction. No local is in its home yet (the prologue is still + * setting the frame up), so a local is listed without a pointer. + */ +export function preludeVariables(func: Ir.Function): VariableEntry[] { + const block = func.blocks.get(func.entry); + if (!block) return []; + const first = block.instructions[0] ?? block.terminator; + const context = (block.entryDebug?.context ?? + first.operationDebug?.context) as Record | undefined; + if (!context) return []; + + const locals = new Set( + [...(func.ssaVariables?.values() ?? [])].map((ssa) => ssa.name), + ); + return liftVariables(context).variables.map((entry) => { + if (entry.identifier === undefined || !locals.has(entry.identifier)) { + return entry; + } + const { pointer: _, ...rest } = entry; + return rest; + }); +} + /** The source offsets of an instruction/terminator: of its `code` * context, or of each `code` in a `gather` (all of which apply). */ function codeOffsets(context: unknown): number[] { diff --git a/packages/bugc/src/evmgen/generation/block.ts b/packages/bugc/src/evmgen/generation/block.ts index c678bd9c90..24ef450170 100644 --- a/packages/bugc/src/evmgen/generation/block.ts +++ b/packages/bugc/src/evmgen/generation/block.ts @@ -21,7 +21,11 @@ import { generateCallTerminator, } from "./control-flow/index.js"; import { annotateTop } from "./values/identify.js"; -import { withReturned, withStackLocals } from "../debug/local-variables.js"; +import { + preludeVariables, + withReturned, + withStackLocals, +} from "../debug/local-variables.js"; /** * Generate code for a basic block @@ -59,7 +63,11 @@ export function generate( ? { sourceId: func.sourceId, loc: func.loc } : undefined; result = result.then( - initializeMemory(state.memory.nextStaticOffset, sourceInfo), + initializeMemory( + state.memory.nextStaticOffset, + sourceInfo, + func ? preludeVariables(func) : [], + ), ); } @@ -385,23 +393,23 @@ function storePhiDest( function initializeMemory( nextStaticOffset: number, sourceInfo?: { sourceId: string; loc: Ast.SourceLocation }, + variables: Format.Program.Context.Variables["variables"] = [], ): Transition { const { PUSHn, MSTORE } = operations; - const debug = sourceInfo - ? { - context: { - code: { - source: { id: sourceInfo.sourceId }, - range: sourceInfo.loc, - }, - } as Format.Program.Context, - } - : { - context: { - remark: "initialize free memory pointer", - } as Format.Program.Context, - }; + const debug = { + context: { + ...(sourceInfo + ? { + code: { + source: { id: sourceInfo.sourceId }, + range: sourceInfo.loc, + }, + } + : { remark: "initialize free memory pointer" }), + ...(variables.length > 0 ? { variables } : {}), + } as Format.Program.Context, + }; const { PUSH0 } = operations; diff --git a/packages/bugc/src/evmgen/generation/function.ts b/packages/bugc/src/evmgen/generation/function.ts index 3908182369..3f5a8115b2 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 { preludeVariables } from "../debug/local-variables.js"; import { serialize } from "../serialize.js"; import { type Transition } from "../operations.js"; @@ -67,8 +68,15 @@ function generatePrologue( }), }, }; + // Until the prologue has stored the arguments in the frame, the + // parameters are listed without a pointer (the invoke's + // `arguments` point at them on the stack) + const variables = preludeVariables(func); const entryDebug = { - context: entryInvoke as Format.Program.Context, + context: { + ...entryInvoke, + ...(variables.length > 0 ? { variables } : {}), + } as Format.Program.Context, }; currentState = { ...currentState, @@ -78,7 +86,7 @@ function generatePrologue( ], }; - const d = makePrologueDebug(func); + const d = makePrologueDebug(func, variables); const frameSize = currentState.memory.frameSize; if (frameSize !== undefined) { @@ -264,21 +272,23 @@ function emitFpRelativeStore( ); } -function makePrologueDebug(func: Ir.Function): Debug { - return func.sourceId && func.loc - ? { - context: { - code: { - source: { id: func.sourceId }, - range: func.loc, - }, - } as Format.Program.Context, - } - : { - context: { - remark: "prologue: allocate call frame", - } as Format.Program.Context, - }; +function makePrologueDebug( + func: Ir.Function, + variables: Format.Program.Context.Variables["variables"], +): Debug { + return { + context: { + ...(func.sourceId && func.loc + ? { + code: { + source: { id: func.sourceId }, + range: func.loc, + }, + } + : { remark: "prologue: allocate call frame" }), + ...(variables.length > 0 ? { variables } : {}), + } as Format.Program.Context, + }; } /**