Skip to content

Commit e554d9c

Browse files
committed
bugc: test exit counts, tail calls, and exits without data
1 parent 8714233 commit e554d9c

2 files changed

Lines changed: 72 additions & 16 deletions

File tree

‎packages/bugc/src/evmgen/call-timing.test.ts‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,27 @@ create { r = 0; }
5555
code { r = fact(4); }`,
5656
returns: [1n, 2n, 6n, 24n],
5757
},
58+
{
59+
// From O2, TCO turns count's self-call into a back-edge JUMP
60+
// that pops the old frame and pushes the new one at once, so
61+
// the frame depth stays constant. That JUMP carries a `return`
62+
// with no `data`: nothing is returned yet. The only value count
63+
// returns is its last one, from its real exit.
64+
name: "tail call",
65+
source: `name TailCall;
66+
define {
67+
function succ(n: uint256) -> uint256 { return n + 1; };
68+
function count(n: uint256, target: uint256) -> uint256 {
69+
if (n < target) { return count(succ(n), target); }
70+
else { return n; }
71+
};
72+
}
73+
storage { [0] r: uint256; }
74+
create { r = 0; }
75+
code { r = count(0, 5); }`,
76+
// At O0: succ's five returns, then count's six, unwinding
77+
returns: [1n, 2n, 3n, 4n, 5n, 5n, 5n, 5n, 5n, 5n, 5n],
78+
},
5879
];
5980

6081
/** A context's invoke and return leaves, outside inlined bodies */
@@ -143,6 +164,24 @@ describe("call frame timing", () => {
143164
).toBe(functionAt(steps[i].pc));
144165
}
145166

167+
// An exit with no `data` is never run. Bugc gives an if/else
168+
// whose branches both return an empty merge block, with an
169+
// implicit void return that nothing jumps to (O0 and O1; the
170+
// optimizer drops it from O2). A value-returning function
171+
// that runs off its end would reach such an exit, but the
172+
// functions here do not.
173+
const visited = new Set(steps.map(({ pc }) => pc));
174+
for (const instruction of program.instructions) {
175+
const { invoke, return: returned } = events(instruction.context);
176+
const ret = (instruction.context as Format.Program.Context.Return)
177+
?.return;
178+
if (returned !== undefined && invoke === undefined && !ret.data) {
179+
expect(visited, `exit at ${instruction.offset}`).not.toContain(
180+
Number(instruction.offset),
181+
);
182+
}
183+
}
184+
146185
expect(frames).toEqual([]);
147186
expect(read).toHaveLength(calls);
148187
if (level === 0) {

‎packages/bugc/src/evmgen/optimizer-contexts.test.ts‎

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -134,15 +134,21 @@ function countCallSites(program: Format.Program): CallSiteCounts {
134134
}
135135

136136
/**
137-
* Every function listed has a `return` on its exit JUMP(s), no
138-
* other function has one, and no JUMPDEST carries a `return` (the
139-
* continuation carries only the call site's `code`). A function
140-
* has one exit per return epilogue, so the count follows its
141-
* return statements, not its call sites.
137+
* Each function listed has exactly that many exit JUMPs carrying
138+
* a `return`, no other function has one, and no JUMPDEST carries a
139+
* `return` (the continuation carries only the call site's `code`).
140+
* A function has one exit per `return` terminator in its IR, so the
141+
* count follows its return statements, not its call sites. It can
142+
* be one more than the source shows: an if/else whose branches
143+
* both return leaves an empty merge block that gets an implicit
144+
* return, until the optimizer drops it.
142145
*/
143-
function expectExits(counts: CallSiteCounts, functions: string[]): void {
146+
function expectExits(
147+
counts: CallSiteCounts,
148+
exits: Record<string, number>,
149+
): void {
144150
expect(counts.returnJumpdest).toEqual({});
145-
expect(Object.keys(counts.returnExit).sort()).toEqual([...functions].sort());
151+
expect(counts.returnExit).toEqual(exits);
146152
}
147153

148154
describe("optimizer preserves invoke/return contexts", () => {
@@ -176,7 +182,7 @@ code { r = add(10, 20); }`;
176182
// exit JUMP — all naming "add".
177183
expect(counts.invokeJump).toEqual({ add: 1 });
178184
expect(counts.invokeJumpdest).toEqual({ add: 1 });
179-
expectExits(counts, ["add"]);
185+
expectExits(counts, { add: 1 });
180186
}
181187

182188
// Behavior is still correct.
@@ -217,7 +223,7 @@ code { r = add(2 + 3, 4 * 5); }`;
217223
} else {
218224
expect(counts.invokeJump).toEqual({ add: 1 });
219225
expect(counts.invokeJumpdest).toEqual({ add: 1 });
220-
expectExits(counts, ["add"]);
226+
expectExits(counts, { add: 1 });
221227
}
222228

223229
const result = await executeProgram(source, {
@@ -262,7 +268,7 @@ code {
262268
} else {
263269
expect(counts.invokeJump).toEqual({ dbl: 2 });
264270
expect(counts.invokeJumpdest).toEqual({ dbl: 1 });
265-
expectExits(counts, ["dbl"]);
271+
expectExits(counts, { dbl: 1 });
266272
}
267273

268274
const result = await executeProgram(source, {
@@ -302,7 +308,10 @@ code { r = fact(5); }`;
302308
// fact's exit JUMPs carry its returns.
303309
expect(counts.invokeJump).toEqual({ fact: 2 });
304310
expect(counts.invokeJumpdest).toEqual({ fact: 1 });
305-
expectExits(counts, ["fact"]);
311+
// Two return statements, plus the implicit return of the
312+
// empty merge block after the if/else (no predecessors),
313+
// which the optimizer drops from level 2.
314+
expectExits(counts, { fact: level < 2 ? 3 : 2 });
306315

307316
const result = await executeProgram(source, {
308317
calldata: "",
@@ -348,7 +357,10 @@ code { r = isEven(4); }`;
348357
isEven: 1,
349358
isOdd: 1,
350359
});
351-
expectExits(counts, ["isEven", "isOdd"]);
360+
// As in `fact`: two returns each, plus an unreachable
361+
// implicit return each until level 2.
362+
const exits = level < 2 ? 3 : 2;
363+
expectExits(counts, { isEven: exits, isOdd: exits });
352364

353365
const result = await executeProgram(source, {
354366
calldata: "",
@@ -399,7 +411,7 @@ code { r = addThree(1, 2, 3); }`;
399411
addThree: 1,
400412
add: 1,
401413
});
402-
expectExits(counts, ["addThree", "add"]);
414+
expectExits(counts, { addThree: 1, add: 1 });
403415
}
404416

405417
const result = await executeProgram(source, {
@@ -438,7 +450,10 @@ code { r = check(3, 4); }`;
438450

439451
expect(counts.invokeJump).toEqual({ check: 1 });
440452
expect(counts.invokeJumpdest).toEqual({ check: 1 });
441-
expectExits(counts, ["check"]);
453+
// Three return statements, in three blocks: the two
454+
// `return 42` blocks stay apart, and no if/else leaves an
455+
// implicit return.
456+
expectExits(counts, { check: 3 });
442457

443458
const result = await executeProgram(source, {
444459
calldata: "",
@@ -485,7 +500,9 @@ code { r = count(0, 5); }`;
485500
// self-call. succ is still a separate function call.
486501
expect(counts.invokeJump).toEqual({ count: 2, succ: 1 });
487502
expect(counts.invokeJumpdest).toEqual({ count: 1, succ: 1 });
488-
expectExits(counts, ["count", "succ"]);
503+
// count: two returns plus an unreachable implicit return
504+
// after the if/else.
505+
expectExits(counts, { count: 3, succ: 1 });
489506
// At level 1 there are no TCO back-edge JUMPs.
490507
expect(counts.returnJump).toEqual({});
491508
});
@@ -512,7 +529,7 @@ code { r = count(0, 5); }`;
512529

513530
// count's and succ's exit JUMPs carry return
514531
// contexts as usual.
515-
expectExits(counts, ["count", "succ"]);
532+
expectExits(counts, { count: 1, succ: 1 });
516533

517534
// The TCO back-edge JUMP additionally carries a
518535
// return context for `count` (the previous

0 commit comments

Comments
 (0)