Skip to content
Merged
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
106 changes: 83 additions & 23 deletions packages/bugc/src/evmgen/optimizer-contexts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,22 @@ function countCallSites(program: Format.Program): CallSiteCounts {
return counts;
}

/** Count instructions carrying a `transform: ["inline"]` marker. */
function countInline(program: Format.Program): number {
let n = 0;
for (const instr of program.instructions) {
if (!instr.context) continue;
if (
unwrapLeaves(instr.context).some(
(c) => Context.isTransform(c) && c.transform.includes("inline"),
)
) {
n += 1;
}
}
return n;
}

describe("optimizer preserves invoke/return contexts", () => {
const allLevels: OptLevel[] = [0, 1, 2, 3];

Expand All @@ -148,11 +164,23 @@ code { r = add(10, 20); }`;
const program = await compileAt(source, level);
const counts = countCallSites(program);

// One caller JUMP, one callee JUMPDEST, one
// continuation JUMPDEST — all naming "add".
expect(counts.invokeJump).toEqual({ add: 1 });
expect(counts.invokeJumpdest).toEqual({ add: 1 });
expect(counts.returnJumpdest).toEqual({ add: 1 });
if (level >= 2) {
// `add` is a leaf single-return helper: inlining (L2+)
// replaces the real call with a virtual inline
// activation, so there's no caller JUMP for `add`.
expect(counts.invokeJump).toEqual({});
// Inline markers appear on the inlined body; at L3 a
// fully-foldable helper body can be constant-folded to a
// PUSH, dissolving the marker, so only require presence
// at L2.
if (level === 2) expect(countInline(program)).toBeGreaterThan(0);
} else {
// One caller JUMP, one callee JUMPDEST, one
// continuation JUMPDEST — all naming "add".
expect(counts.invokeJump).toEqual({ add: 1 });
expect(counts.invokeJumpdest).toEqual({ add: 1 });
expect(counts.returnJumpdest).toEqual({ add: 1 });
}

// Behavior is still correct.
const result = await executeProgram(source, {
Expand Down Expand Up @@ -185,9 +213,20 @@ code { r = add(2 + 3, 4 * 5); }`;
const program = await compileAt(source, level);
const counts = countCallSites(program);

expect(counts.invokeJump).toEqual({ add: 1 });
expect(counts.invokeJumpdest).toEqual({ add: 1 });
expect(counts.returnJumpdest).toEqual({ add: 1 });
if (level >= 2) {
// `add` inlined at L2+ — virtual inline activation, no
// real caller JUMP.
expect(counts.invokeJump).toEqual({});
// Inline markers appear on the inlined body; at L3 a
// fully-foldable helper body can be constant-folded to a
// PUSH, dissolving the marker, so only require presence
// at L2.
if (level === 2) expect(countInline(program)).toBeGreaterThan(0);
} else {
expect(counts.invokeJump).toEqual({ add: 1 });
expect(counts.invokeJumpdest).toEqual({ add: 1 });
expect(counts.returnJumpdest).toEqual({ add: 1 });
}

const result = await executeProgram(source, {
calldata: "",
Expand Down Expand Up @@ -224,9 +263,20 @@ code {
const program = await compileAt(source, level);
const counts = countCallSites(program);

expect(counts.invokeJump).toEqual({ dbl: 2 });
expect(counts.invokeJumpdest).toEqual({ dbl: 1 });
expect(counts.returnJumpdest).toEqual({ dbl: 2 });
if (level >= 2) {
// Both `dbl` sites are inlined (leaf single-return) into
// separate virtual activations; no real caller JUMPs.
expect(counts.invokeJump).toEqual({});
// Inline markers appear on the inlined body; at L3 a
// fully-foldable helper body can be constant-folded to a
// PUSH, dissolving the marker, so only require presence
// at L2.
if (level === 2) expect(countInline(program)).toBeGreaterThan(0);
} else {
expect(counts.invokeJump).toEqual({ dbl: 2 });
expect(counts.invokeJumpdest).toEqual({ dbl: 1 });
expect(counts.returnJumpdest).toEqual({ dbl: 2 });
}

const result = await executeProgram(source, {
calldata: "",
Expand Down Expand Up @@ -349,18 +399,28 @@ code { r = addThree(1, 2, 3); }`;
const program = await compileAt(source, level);
const counts = countCallSites(program);

expect(counts.invokeJump).toEqual({
addThree: 1,
add: 2,
});
expect(counts.invokeJumpdest).toEqual({
addThree: 1,
add: 1,
});
expect(counts.returnJumpdest).toEqual({
addThree: 1,
add: 2,
});
if (level >= 2) {
// `add` (leaf) inlines into `addThree` at both sites;
// that makes `addThree` itself a leaf, so on a later
// fixpoint iteration it inlines into `main` too. End
// state: no real caller JUMPs — everything is inline
// activations.
expect(counts.invokeJump).toEqual({});
if (level === 2) expect(countInline(program)).toBeGreaterThan(0);
} else {
expect(counts.invokeJump).toEqual({
addThree: 1,
add: 2,
});
expect(counts.invokeJumpdest).toEqual({
addThree: 1,
add: 1,
});
expect(counts.returnJumpdest).toEqual({
addThree: 1,
add: 2,
});
}

const result = await executeProgram(source, {
calldata: "",
Expand Down
7 changes: 6 additions & 1 deletion packages/bugc/src/optimizer/simple-optimizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
ReturnMergingStep,
ReadWriteMergingStep,
TailCallOptimizationStep,
InliningStep,
} from "./steps/index.js";

/**
Expand Down Expand Up @@ -58,9 +59,13 @@ function createOptimizationPipeline(level: number): OptimizationStep[] {
);
}

// Level 2: Add CSE, tail call optimization, and jump optimization
// Level 2: Add inlining, CSE, tail call optimization, and
// jump optimization. Inlining runs first (after L1 fold) so
// TCO/CSE still apply to inlined code and `["fold","inline"]`
// composes.
if (level >= 2) {
steps.push(
new InliningStep(),
new CommonSubexpressionEliminationStep(),
new TailCallOptimizationStep(),
new JumpOptimizationStep(),
Expand Down
1 change: 1 addition & 0 deletions packages/bugc/src/optimizer/steps/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export { BlockMergingStep } from "./block-merging.js";
export { ReturnMergingStep } from "./return-merging.js";
export { ReadWriteMergingStep } from "./read-write-merging.js";
export { TailCallOptimizationStep } from "./tail-call-optimization.js";
export { InliningStep } from "./inlining.js";
144 changes: 144 additions & 0 deletions packages/bugc/src/optimizer/steps/inlining.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/**
* Behavioral tests for the function-inlining pass (level 2).
*
* Inlining must (a) preserve runtime behavior exactly, and
* (b) emit `transform: ["inline"]` on the inlined body so the
* debugger can reconstruct a virtual activation for the call.
*/
import { describe, it, expect } from "vitest";

import { compile } from "#compiler";
import { executeProgram } from "#test/evm/behavioral";
import type * as Format from "@ethdebug/format";
import { Program } from "@ethdebug/format";

const { Context } = Program;

function leaves(ctx: Format.Program.Context): Format.Program.Context[] {
if (Context.isGather(ctx)) return ctx.gather.flatMap(leaves);
if ("pick" in ctx && Array.isArray((ctx as { pick: unknown[] }).pick)) {
return (ctx as { pick: Format.Program.Context[] }).pick.flatMap(leaves);
}
return [ctx];
}

async function inlineMarks(source: string, level: 0 | 1 | 2 | 3) {
const result = await compile({
to: "bytecode",
source,
optimizer: { level },
});
if (!result.success) {
const errors = result.messages.error ?? [];
throw new Error(
"compile failed:\n" +
errors
.map((e: { message?: string }) => e.message ?? String(e))
.join("\n"),
);
}
let count = 0;
for (const instr of result.value.bytecode.runtimeInstructions) {
const ctx = instr.debug?.context;
if (!ctx) continue;
if (
[ctx, ...leaves(ctx)].some(
(c) => Context.isTransform(c) && c.transform.includes("inline"),
)
) {
count += 1;
}
}
return count;
}

describe("function inlining (level 2)", () => {
describe("leaf helper, single return", () => {
const source = `name Demo;
define {
function add(a: uint256, b: uint256) -> uint256 { return a + b; };
}
storage { [0] r: uint256; }
create {}
code { r = add(3, 4); }`;

it("produces the same result at every level", async () => {
for (const level of [0, 1, 2, 3] as const) {
const res = await executeProgram(source, {
calldata: "",
optimizationLevel: level,
});
expect(res.callSuccess).toBe(true);
expect(await res.getStorage(0n)).toBe(7n);
}
});

it("emits no inline marks at level 0", async () => {
expect(await inlineMarks(source, 0)).toBe(0);
});

it("emits inline marks at level 2", async () => {
expect(await inlineMarks(source, 2)).toBeGreaterThan(0);
});
});

describe("multiple call sites", () => {
const source = `name Multi;
define {
function dbl(x: uint256) -> uint256 { return x + x; };
}
storage { [0] r: uint256; }
create { r = 0; }
code {
let a = dbl(5);
let b = dbl(10);
r = a + b;
}`;

it("inlines every site and stays correct", async () => {
for (const level of [0, 1, 2, 3] as const) {
const res = await executeProgram(source, {
calldata: "",
optimizationLevel: level,
});
expect(res.callSuccess).toBe(true);
expect(await res.getStorage(0n)).toBe(30n);
}
expect(await inlineMarks(source, 2)).toBeGreaterThan(0);
});
});

describe("does not inline into a tail-recursive function (protects TCO)", () => {
// `succ` is a leaf, but inlining it into `count`'s recursive
// call arguments would rewrite `count(succ(n))` into
// `count(n + 1)`, which the tail-call optimizer mishandles.
// The pass must leave recursive/TCO'd callers untouched.
const source = `name TailCall;
define {
function succ(n: uint256) -> uint256 { return n + 1; };
function count(n: uint256, target: uint256) -> uint256 {
if (n < target) { return count(succ(n), target); }
else { return n; }
};
}
storage { [0] r: uint256; }
create { r = 0; }
code { r = count(0, 5); }`;

it("stays correct at every level", async () => {
for (const level of [0, 1, 2, 3] as const) {
const res = await executeProgram(source, {
calldata: "",
optimizationLevel: level,
});
expect(res.callSuccess).toBe(true);
expect(await res.getStorage(0n)).toBe(5n);
}
});

it("does not inline succ into the recursive count", async () => {
// No inline markers: succ stays a real call so TCO can fire.
expect(await inlineMarks(source, 2)).toBe(0);
});
});
});
Loading
Loading