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
73 changes: 50 additions & 23 deletions packages/bugc/src/evmgen/optimizer-contexts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,11 +148,18 @@ 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+)
// splices its body into the caller, so there's no real
// caller JUMP for `add`.
expect(counts.invokeJump).toEqual({});
} 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 +192,15 @@ 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+ — its body is spliced in, no real
// caller JUMP. Constant-foldable args don't change that.
expect(counts.invokeJump).toEqual({});
} 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 +237,15 @@ 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 leaf single-return calls, inlined
// at L2+ into the caller — no real caller JUMPs remain.
expect(counts.invokeJump).toEqual({});
} 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 +368,26 @@ 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 remain.
expect(counts.invokeJump).toEqual({});
} 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
6 changes: 5 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,12 @@ 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.
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";
157 changes: 157 additions & 0 deletions packages/bugc/src/optimizer/steps/inlining.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/**
* Behavioral + structural tests for the function-inlining pass
* (level 2).
*
* Inlining must (a) preserve runtime behavior exactly, and
* (b) actually replace eligible calls with the callee's body —
* a fully-inlined callee is deleted and no `call` to it remains.
*/
import { describe, it, expect } from "vitest";

import { compile } from "#compiler";
import * as Ir from "#ir";
import { executeProgram } from "#test/evm/behavioral";

async function optimizedIr(
source: string,
level: 0 | 1 | 2 | 3,
): Promise<Ir.Module> {
const result = await compile({
to: "ir",
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"),
);
}
return result.value.ir;
}

/** All functions in the module, including <main> and <create>. */
function allFunctions(module: Ir.Module): Ir.Function[] {
return [
module.main,
...(module.create ? [module.create] : []),
...module.functions.values(),
];
}

/** Count `call` terminators targeting `callee` across the module. */
function countCalls(module: Ir.Module, callee: string): number {
let n = 0;
for (const fn of allFunctions(module)) {
for (const block of fn.blocks.values()) {
const t = block.terminator;
if (t.kind === "call" && t.function === callee) n += 1;
}
}
return n;
}

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("keeps the real call at levels 0-1", async () => {
for (const level of [0, 1] as const) {
const ir = await optimizedIr(source, level);
expect(ir.functions.has("add")).toBe(true);
expect(countCalls(ir, "add")).toBe(1);
}
});

it("inlines the call and deletes the callee at level 2", async () => {
const ir = await optimizedIr(source, 2);
expect(countCalls(ir, "add")).toBe(0);
expect(ir.functions.has("add")).toBe(false);
});
});

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);
}
// Both sites inlined; callee deleted.
const ir = await optimizedIr(source, 2);
expect(countCalls(ir, "dbl")).toBe(0);
expect(ir.functions.has("dbl")).toBe(false);
});
});

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 () => {
// succ stays a real call so TCO can fire on count.
const ir = await optimizedIr(source, 2);
expect(ir.functions.has("succ")).toBe(true);
expect(countCalls(ir, "succ")).toBeGreaterThan(0);
});
});
});
Loading
Loading