Skip to content

Commit 5bb6224

Browse files
committed
bugc: list variables before a function's first statement
1 parent 46585fd commit 5bb6224

5 files changed

Lines changed: 144 additions & 33 deletions

File tree

‎packages/bugc/CHANGELOG.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,13 @@ support. Changes to the specification itself are tracked in the root
8989
has the branch's context: its source range, its variables (the storage
9090
variables too) and its transforms. Before, it had no context, so a
9191
debugger listed no variables while it ran ([#356]).
92+
- The instructions that run before a function's first statement now have
93+
a `variables` context: the memory setup at the start of the code (and
94+
`create`) blocks, and a function's entry `JUMPDEST` and prologue. They
95+
list the storage variables and the locals in scope, a local without a
96+
pointer: a parameter is not yet in its home there, and the `invoke`'s
97+
`arguments` point at it on the stack. Before, they listed no variables,
98+
so a debugger showed none for those steps ([#358]).
9299
- A call to a function with no return type now compiles as a statement,
93100
as in `bump();`, at every optimization level. The function's `return`
94101
context has no `data`. Before, IR generation failed with "Cannot convert
@@ -301,3 +308,4 @@ First publication.
301308
[#351]: https://github.com/ethdebug/format/pull/351
302309
[#352]: https://github.com/ethdebug/format/pull/352
303310
[#356]: https://github.com/ethdebug/format/pull/356
311+
[#358]: https://github.com/ethdebug/format/pull/358

‎packages/bugc/src/evmgen/debug/local-variables.soundness.test.ts‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1052,3 +1052,61 @@ describe.each(levels)("locals hold the program's values at O%i", (level) => {
10521052
(program) => check(program, level),
10531053
);
10541054
});
1055+
1056+
describe("variables before a function's first statement", () => {
1057+
// Contexts are postconditions: the step after an instruction shows
1058+
// its context. Every step lists the storage variable; from the
1059+
// callee's entry JUMPDEST through its prologue, the parameters.
1060+
const source = `name Prelude;
1061+
define {
1062+
function add(a: uint256, b: uint256) -> uint256 {
1063+
let c = a + b;
1064+
return c;
1065+
};
1066+
}
1067+
storage { [0] r: uint256; }
1068+
code { let x = 7; r = add(x, 3); }`;
1069+
1070+
for (const level of [0, 1] as Level[]) {
1071+
it(`lists variables at every step (O${level})`, async () => {
1072+
const { steps, instructionAt } = await traceLocals(source, level);
1073+
const unlisted = steps
1074+
.map((_, i) => i)
1075+
.filter(
1076+
(i) =>
1077+
i > 0 &&
1078+
localsOf(instructionAt(steps[i - 1])?.context).length === 0,
1079+
);
1080+
expect(unlisted).toEqual([]);
1081+
});
1082+
1083+
it(`lists the parameters in the prologue (O${level})`, async () => {
1084+
const { program } = await traceLocals(source, level);
1085+
const at = program.instructions.findIndex(
1086+
(instruction) =>
1087+
instruction.operation?.mnemonic === "JUMPDEST" &&
1088+
Program.Context.isInvoke(instruction.context),
1089+
);
1090+
// The entry JUMPDEST, then the prologue: the ops that map to the
1091+
// whole function, before its body's
1092+
const whole = codeOffset(program.instructions[at + 1].context);
1093+
const rest = program.instructions.slice(at + 1);
1094+
const prologue = [
1095+
program.instructions[at],
1096+
...rest.slice(
1097+
0,
1098+
rest.findIndex(
1099+
(instruction) => codeOffset(instruction.context) !== whole,
1100+
),
1101+
),
1102+
];
1103+
expect(prologue.length).toBeGreaterThan(1);
1104+
for (const instruction of prologue) {
1105+
const names = localsOf(instruction.context).map((v) => v.identifier);
1106+
expect(names, `pc ${instruction.offset}`).toEqual(
1107+
expect.arrayContaining(["r", "a", "b"]),
1108+
);
1109+
}
1110+
});
1111+
}
1112+
});

‎packages/bugc/src/evmgen/debug/local-variables.ts‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,33 @@ export function withoutFrameLocals(
459459
};
460460
}
461461

462+
/**
463+
* The variables in scope before a function's entry block: during the
464+
* memory setup of main or create, or a function's entry JUMPDEST and
465+
* prologue. They are the entry block's, as listed just before its first
466+
* instruction. No local is in its home yet (the prologue is still
467+
* setting the frame up), so a local is listed without a pointer.
468+
*/
469+
export function preludeVariables(func: Ir.Function): VariableEntry[] {
470+
const block = func.blocks.get(func.entry);
471+
if (!block) return [];
472+
const first = block.instructions[0] ?? block.terminator;
473+
const context = (block.entryDebug?.context ??
474+
first.operationDebug?.context) as Record<string, unknown> | undefined;
475+
if (!context) return [];
476+
477+
const locals = new Set(
478+
[...(func.ssaVariables?.values() ?? [])].map((ssa) => ssa.name),
479+
);
480+
return liftVariables(context).variables.map((entry) => {
481+
if (entry.identifier === undefined || !locals.has(entry.identifier)) {
482+
return entry;
483+
}
484+
const { pointer: _, ...rest } = entry;
485+
return rest;
486+
});
487+
}
488+
462489
/** The source offsets of an instruction/terminator: of its `code`
463490
* context, or of each `code` in a `gather` (all of which apply). */
464491
function codeOffsets(context: unknown): number[] {

‎packages/bugc/src/evmgen/generation/block.ts‎

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ import {
2121
generateCallTerminator,
2222
} from "./control-flow/index.js";
2323
import { annotateTop } from "./values/identify.js";
24-
import { withReturned, withStackLocals } from "../debug/local-variables.js";
24+
import {
25+
preludeVariables,
26+
withReturned,
27+
withStackLocals,
28+
} from "../debug/local-variables.js";
2529

2630
/**
2731
* Generate code for a basic block
@@ -59,7 +63,11 @@ export function generate<S extends Stack>(
5963
? { sourceId: func.sourceId, loc: func.loc }
6064
: undefined;
6165
result = result.then(
62-
initializeMemory(state.memory.nextStaticOffset, sourceInfo),
66+
initializeMemory(
67+
state.memory.nextStaticOffset,
68+
sourceInfo,
69+
func ? preludeVariables(func) : [],
70+
),
6371
);
6472
}
6573

@@ -385,23 +393,23 @@ function storePhiDest<S extends Stack>(
385393
function initializeMemory<S extends Stack>(
386394
nextStaticOffset: number,
387395
sourceInfo?: { sourceId: string; loc: Ast.SourceLocation },
396+
variables: Format.Program.Context.Variables["variables"] = [],
388397
): Transition<S, S> {
389398
const { PUSHn, MSTORE } = operations;
390399

391-
const debug = sourceInfo
392-
? {
393-
context: {
394-
code: {
395-
source: { id: sourceInfo.sourceId },
396-
range: sourceInfo.loc,
397-
},
398-
} as Format.Program.Context,
399-
}
400-
: {
401-
context: {
402-
remark: "initialize free memory pointer",
403-
} as Format.Program.Context,
404-
};
400+
const debug = {
401+
context: {
402+
...(sourceInfo
403+
? {
404+
code: {
405+
source: { id: sourceInfo.sourceId },
406+
range: sourceInfo.loc,
407+
},
408+
}
409+
: { remark: "initialize free memory pointer" }),
410+
...(variables.length > 0 ? { variables } : {}),
411+
} as Format.Program.Context,
412+
};
405413

406414
const { PUSH0 } = operations;
407415

‎packages/bugc/src/evmgen/generation/function.ts‎

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { type Layout, Memory } from "#evmgen/analysis";
1212
import type { Error as EvmgenError } from "#evmgen/errors";
1313

1414
import * as Block from "./block.js";
15+
import { preludeVariables } from "../debug/local-variables.js";
1516
import { serialize } from "../serialize.js";
1617
import { type Transition } from "../operations.js";
1718

@@ -67,8 +68,15 @@ function generatePrologue<S extends Stack>(
6768
}),
6869
},
6970
};
71+
// Until the prologue has stored the arguments in the frame, the
72+
// parameters are listed without a pointer (the invoke's
73+
// `arguments` point at them on the stack)
74+
const variables = preludeVariables(func);
7075
const entryDebug = {
71-
context: entryInvoke as Format.Program.Context,
76+
context: {
77+
...entryInvoke,
78+
...(variables.length > 0 ? { variables } : {}),
79+
} as Format.Program.Context,
7280
};
7381
currentState = {
7482
...currentState,
@@ -78,7 +86,7 @@ function generatePrologue<S extends Stack>(
7886
],
7987
};
8088

81-
const d = makePrologueDebug(func);
89+
const d = makePrologueDebug(func, variables);
8290
const frameSize = currentState.memory.frameSize;
8391

8492
if (frameSize !== undefined) {
@@ -264,21 +272,23 @@ function emitFpRelativeStore<S extends Stack>(
264272
);
265273
}
266274

267-
function makePrologueDebug(func: Ir.Function): Debug {
268-
return func.sourceId && func.loc
269-
? {
270-
context: {
271-
code: {
272-
source: { id: func.sourceId },
273-
range: func.loc,
274-
},
275-
} as Format.Program.Context,
276-
}
277-
: {
278-
context: {
279-
remark: "prologue: allocate call frame",
280-
} as Format.Program.Context,
281-
};
275+
function makePrologueDebug(
276+
func: Ir.Function,
277+
variables: Format.Program.Context.Variables["variables"],
278+
): Debug {
279+
return {
280+
context: {
281+
...(func.sourceId && func.loc
282+
? {
283+
code: {
284+
source: { id: func.sourceId },
285+
range: func.loc,
286+
},
287+
}
288+
: { remark: "prologue: allocate call frame" }),
289+
...(variables.length > 0 ? { variables } : {}),
290+
} as Format.Program.Context,
291+
};
282292
}
283293

284294
/**

0 commit comments

Comments
 (0)