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
8 changes: 8 additions & 0 deletions packages/bugc/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
58 changes: 58 additions & 0 deletions packages/bugc/src/evmgen/debug/local-variables.soundness.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"]),
);
}
});
}
});
27 changes: 27 additions & 0 deletions packages/bugc/src/evmgen/debug/local-variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> | 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[] {
Expand Down
40 changes: 24 additions & 16 deletions packages/bugc/src/evmgen/generation/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -59,7 +63,11 @@ export function generate<S extends Stack>(
? { sourceId: func.sourceId, loc: func.loc }
: undefined;
result = result.then(
initializeMemory(state.memory.nextStaticOffset, sourceInfo),
initializeMemory(
state.memory.nextStaticOffset,
sourceInfo,
func ? preludeVariables(func) : [],
),
);
}

Expand Down Expand Up @@ -385,23 +393,23 @@ function storePhiDest<S extends Stack>(
function initializeMemory<S extends Stack>(
nextStaticOffset: number,
sourceInfo?: { sourceId: string; loc: Ast.SourceLocation },
variables: Format.Program.Context.Variables["variables"] = [],
): Transition<S, S> {
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;

Expand Down
44 changes: 27 additions & 17 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 { preludeVariables } from "../debug/local-variables.js";
import { serialize } from "../serialize.js";
import { type Transition } from "../operations.js";

Expand Down Expand Up @@ -67,8 +68,15 @@ function generatePrologue<S extends Stack>(
}),
},
};
// 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,
Expand All @@ -78,7 +86,7 @@ function generatePrologue<S extends Stack>(
],
};

const d = makePrologueDebug(func);
const d = makePrologueDebug(func, variables);
const frameSize = currentState.memory.frameSize;

if (frameSize !== undefined) {
Expand Down Expand Up @@ -264,21 +272,23 @@ function emitFpRelativeStore<S extends Stack>(
);
}

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,
};
}

/**
Expand Down
Loading