Skip to content

Commit 46585fd

Browse files
authored
bugc: keep an inlined body's control flow inside the body (#356)
1 parent 6c81c10 commit 46585fd

5 files changed

Lines changed: 130 additions & 9 deletions

File tree

‎packages/bugc/CHANGELOG.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,16 @@ support. Changes to the specification itself are tracked in the root
7979

8080
### Fixed
8181

82+
- At optimization levels 2 and 3, an inlined body's control flow now has
83+
a `transform: ["inline"]` context: the branch of an `if`, the jumps
84+
between the body's blocks, each block's `JUMPDEST`, and the jump that
85+
carries the body's `return`. Before, only the body's other instructions
86+
had it, so a debugger that reads the transform closed the inlined frame
87+
at the body's first branch ([#356]).
88+
- The block that bugc puts on a branch edge into a block with phis now
89+
has the branch's context: its source range, its variables (the storage
90+
variables too) and its transforms. Before, it had no context, so a
91+
debugger listed no variables while it ran ([#356]).
8292
- A call to a function with no return type now compiles as a statement,
8393
as in `bump();`, at every optimization level. The function's `return`
8494
context has no `data`. Before, IR generation failed with "Cannot convert
@@ -290,3 +300,4 @@ First publication.
290300
[#349]: https://github.com/ethdebug/format/pull/349
291301
[#351]: https://github.com/ethdebug/format/pull/351
292302
[#352]: https://github.com/ethdebug/format/pull/352
303+
[#356]: https://github.com/ethdebug/format/pull/356

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

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,11 @@ export function generate<S extends Stack>(
8484
}
8585
}
8686

87+
// A block of an inlined body is entered inside that body
88+
const entryDebug = withEntryInline(block);
89+
8790
if (callSiteCode) {
88-
const entry = block.entryDebug;
91+
const entry = entryDebug;
8992
const continuationDebug = {
9093
...entry,
9194
context: {
@@ -95,7 +98,7 @@ export function generate<S extends Stack>(
9598
};
9699
result = result.then(JUMPDEST({ debug: continuationDebug }));
97100
} else {
98-
result = result.then(JUMPDEST({ debug: block.entryDebug }));
101+
result = result.then(JUMPDEST({ debug: entryDebug }));
99102
}
100103

101104
// Annotate TOS with dest variable if this is a continuation with return value.
@@ -262,6 +265,46 @@ export function generate<S extends Stack>(
262265
.done();
263266
}
264267

268+
/**
269+
* A block's entry debug, with an `inline` transform for each inlined
270+
* body the block's entry is in: each one its first operation is in,
271+
* but for those whose invoke it carries (its entry is just before
272+
* them).
273+
*/
274+
function withEntryInline(block: Ir.Block): Ir.Block.Debug | undefined {
275+
const first = block.instructions[0] ?? block.terminator;
276+
const context = first.operationDebug?.context;
277+
const depth =
278+
inlineCount(context) - Ir.Utils.activationsOf(context, "invoke").length;
279+
if (depth <= 0) return block.entryDebug;
280+
return {
281+
...block.entryDebug,
282+
...Ir.Utils.addTransform(
283+
block.entryDebug,
284+
...Array<"inline">(depth).fill("inline"),
285+
),
286+
};
287+
}
288+
289+
/** How many `inline` transforms a context has, gathered ones too */
290+
function inlineCount(context: Format.Program.Context | undefined): number {
291+
if (!context || typeof context !== "object") return 0;
292+
const { transform, gather } = context as {
293+
transform?: unknown;
294+
gather?: unknown;
295+
};
296+
const own = Array.isArray(transform)
297+
? transform.filter((id) => id === "inline").length
298+
: 0;
299+
return Array.isArray(gather)
300+
? own +
301+
(gather as Format.Program.Context[]).reduce(
302+
(sum, c) => sum + inlineCount(c),
303+
0,
304+
)
305+
: own;
306+
}
307+
265308
/**
266309
* Generate code for the phi nodes of the block that `predecessor`
267310
* jumps to. The phis copy in parallel: one phi's source may be another

‎packages/bugc/src/evmgen/inline-bracket.test.ts‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { describe, it, expect } from "vitest";
1515

1616
import { compile } from "#compiler";
1717
import { executeProgram } from "#test/evm/behavioral";
18+
import { traceLocals, localsOf } from "#test/evm/locals";
1819
import type * as Format from "@ethdebug/format";
1920
import { Program } from "@ethdebug/format";
2021

@@ -206,3 +207,52 @@ describe("bracketing is a no-op for single-op invoke/return carriers", () => {
206207
expect(t.ret).toBeGreaterThan(0);
207208
});
208209
});
210+
211+
// An inlined body with an `if`: its branch, the blocks it jumps to, and
212+
// the empty block bugc puts on a branch edge into a block with phis (b
213+
// is a phi after the `if`) are all part of the body
214+
const branchingBody = (t: number) => `name Cap;
215+
define {
216+
function cap(x: uint256) -> uint256 {
217+
let b = x * 2;
218+
if (b > 100) { b = 100; }
219+
return b;
220+
};
221+
}
222+
storage { [0] r: uint256; [1] t: uint256; }
223+
create { t = ${t}; }
224+
code { r = cap(t); }`;
225+
226+
describe("an inlined body's control flow is part of the body", () => {
227+
for (const level of [2, 3] as const) {
228+
for (const t of [70, 10]) {
229+
it(`marks every step of the body inline (t = ${t}, O${level})`, async () => {
230+
const { steps, instructionAt } = await traceLocals(
231+
branchingBody(t),
232+
level,
233+
);
234+
let open = 0;
235+
let invoked = 0;
236+
const outside: number[] = [];
237+
const unlisted: number[] = [];
238+
steps.forEach((step, i) => {
239+
const instruction = instructionAt(step);
240+
const f = flags({ debug: instruction });
241+
if (f.invoke) {
242+
open += 1;
243+
invoked += 1;
244+
}
245+
if (open > 0 && !f.inline) outside.push(i);
246+
if (open > 0 && localsOf(instruction?.context).length === 0) {
247+
unlisted.push(i);
248+
}
249+
if (f.return) open -= 1;
250+
});
251+
expect(invoked).toBe(1);
252+
expect(open).toBe(0);
253+
expect(outside).toEqual([]);
254+
expect(unlisted).toEqual([]);
255+
});
256+
}
257+
}
258+
});

‎packages/bugc/src/evmgen/split-edges.ts‎

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,30 @@ function splitFunction(func: Ir.Function): Ir.Function {
2323

2424
// Route the edge from predId into targetId through a new empty
2525
// block if targetId has phis; return the block to branch to
26-
const split = (predId: string, targetId: string): string => {
26+
const split = (predId: string, pred: Ir.Block, targetId: string): string => {
2727
const target = blocks.get(targetId);
2828
if (!target || target.phis.length === 0) {
2929
return targetId;
3030
}
3131

32+
// The edge is the branch's: it has the branch's source range,
33+
// variables and transforms (as `inline`, in an inlined body), but
34+
// not its invoke or return
35+
const { context, inlineSites, origin } = pred.terminator.operationDebug;
36+
const edgeContext = Ir.Utils.withoutActivations(context);
3237
const edgeId = `${predId}_to_${targetId}`;
3338
blocks.set(edgeId, {
3439
id: edgeId,
3540
instructions: [],
36-
// No debug context - compiler-generated edge block
37-
terminator: { kind: "jump", target: targetId, operationDebug: {} },
41+
terminator: {
42+
kind: "jump",
43+
target: targetId,
44+
operationDebug: {
45+
...(edgeContext ? { context: edgeContext } : {}),
46+
...(inlineSites ? { inlineSites } : {}),
47+
...(origin ? { origin } : {}),
48+
},
49+
},
3850
predecessors: new Set([predId]),
3951
phis: [],
4052
debug: {},
@@ -60,9 +72,9 @@ function splitFunction(func: Ir.Function): Ir.Function {
6072
}
6173

6274
const { trueTarget, falseTarget } = pred.terminator;
63-
const newTrue = split(predId, trueTarget);
75+
const newTrue = split(predId, pred, trueTarget);
6476
const newFalse =
65-
falseTarget === trueTarget ? newTrue : split(predId, falseTarget);
77+
falseTarget === trueTarget ? newTrue : split(predId, pred, falseTarget);
6678
blocks.set(predId, {
6779
...blocks.get(predId)!,
6880
terminator: {

‎packages/bugc/src/optimizer/steps/inlining.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -270,9 +270,11 @@ export class InliningStep extends BaseOptimizationStep {
270270
operationDebug: { ...returnedJump },
271271
};
272272
} else {
273+
// The body's own control flow (an `if`'s branch, the jumps
274+
// between its blocks) is part of the body, too
273275
terminator = remapTerminator(t, remapValue, blockRename);
274276
terminator.operationDebug = {
275-
...terminator.operationDebug,
277+
...Ir.Utils.addTransform(terminator.operationDebug, "inline"),
276278
inlineSites: inlineSites(t.operationDebug),
277279
...(t.operationDebug?.origin
278280
? { origin: t.operationDebug.origin }
@@ -354,8 +356,11 @@ export class InliningStep extends BaseOptimizationStep {
354356
returnId = from[0];
355357
continue;
356358
}
359+
// The jump that carries the return is the body's exit, as a
360+
// real call's exit JUMP is the callee's: the body runs up to
361+
// it and through it
357362
block.terminator.operationDebug = withActivationsDebug(
358-
block.terminator.operationDebug,
363+
Ir.Utils.addTransform(block.terminator.operationDebug, "inline"),
359364
{ returns },
360365
);
361366
break;

0 commit comments

Comments
 (0)