Skip to content

Commit 176bffb

Browse files
committed
Merge remote-tracking branch 'origin/main' into compiler-locals-emission
2 parents 0380dcc + 815102a commit 176bffb

2 files changed

Lines changed: 167 additions & 1 deletion

File tree

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/**
2+
* Inlining preserves the call site's source range.
3+
*
4+
* When a call is inlined, the real call instruction (which mapped to
5+
* the call expression, e.g. `square(a)`) is gone and the entry jump
6+
* that carried its range is folded by jump-optimization. Without care
7+
* the call site maps to no instruction. The pass gathers the call-site
8+
* range onto the entry inlined instruction alongside the callee-body
9+
* range it collides with, so the call expression stays mapped.
10+
*/
11+
import { describe, it, expect } from "vitest";
12+
13+
import { compile } from "#compiler";
14+
import { executeProgram } from "#test/evm/behavioral";
15+
import type * as Format from "@ethdebug/format";
16+
17+
// A storage-read argument keeps the inlined body from being folded
18+
// away at O3, so the inline markers survive.
19+
const source = `name Inline;
20+
define {
21+
function square(x: uint256) -> uint256 { return x * x; };
22+
}
23+
storage { [0] r: uint256; [1] a: uint256; }
24+
create { a = 3; }
25+
code { r = square(a); }`;
26+
27+
async function runtimeProgram(level: 0 | 2 | 3): Promise<Format.Program> {
28+
const result = await compile({
29+
to: "bytecode",
30+
source,
31+
optimizer: { level },
32+
});
33+
if (!result.success) throw new Error("compile failed");
34+
return result.value.bytecode.runtimeProgram;
35+
}
36+
37+
/** All `code` leaves reachable through gather/pick, recursively. */
38+
function codeLeaves(ctx: unknown): Array<{ offset: number; length: number }> {
39+
if (!ctx || typeof ctx !== "object") return [];
40+
const c = ctx as Record<string, unknown>;
41+
if (Array.isArray(c.gather)) return c.gather.flatMap(codeLeaves);
42+
if (Array.isArray(c.pick)) return c.pick.flatMap(codeLeaves);
43+
if (c.code && typeof c.code === "object") {
44+
const range = (c.code as { range?: { offset: number; length: number } })
45+
.range;
46+
return range ? [range] : [];
47+
}
48+
return [];
49+
}
50+
51+
/** True if an invoke discriminator is present at any leaf. */
52+
function carriesInvoke(ctx: unknown): boolean {
53+
if (!ctx || typeof ctx !== "object") return false;
54+
const c = ctx as Record<string, unknown>;
55+
if (Array.isArray(c.gather)) return c.gather.some(carriesInvoke);
56+
if (Array.isArray(c.pick)) return c.pick.some(carriesInvoke);
57+
return "invoke" in c;
58+
}
59+
60+
describe("inlining preserves the call-site source range", () => {
61+
for (const level of [2, 3] as const) {
62+
it(`gathers call-site + callee-body ranges on the inlined instruction at O${level}`, async () => {
63+
const program = await runtimeProgram(level);
64+
65+
// The virtual invoke marks the inlined activation; that
66+
// instruction should now carry two distinct code ranges.
67+
const inlined = program.instructions.filter(
68+
(i) => i.context && carriesInvoke(i.context),
69+
);
70+
expect(inlined.length).toBeGreaterThan(0);
71+
72+
const withTwoRanges = inlined.find((i) => {
73+
const ranges = codeLeaves(i.context);
74+
const distinct = new Set(ranges.map((r) => `${r.offset}:${r.length}`));
75+
return distinct.size >= 2;
76+
});
77+
expect(
78+
withTwoRanges,
79+
"an inlined instruction carrying both the call-site and callee-body ranges",
80+
).toBeDefined();
81+
82+
// One of the ranges must be the call expression `square(a)`.
83+
const callSite = source.indexOf("square(a)");
84+
const ranges = codeLeaves(withTwoRanges!.context);
85+
expect(ranges.some((r) => r.offset === callSite)).toBe(true);
86+
});
87+
}
88+
89+
it("does not inline (and needs no gather) at O0", async () => {
90+
const program = await runtimeProgram(0);
91+
const inlined = program.instructions.filter(
92+
(i) =>
93+
i.context &&
94+
carriesInvoke(i.context) &&
95+
i.operation?.mnemonic !== "JUMP" &&
96+
i.operation?.mnemonic !== "JUMPDEST",
97+
);
98+
// At O0 the invoke rides real call JUMP/JUMPDESTs, not spliced
99+
// body instructions — so no non-jump instruction carries it.
100+
expect(inlined.length).toBe(0);
101+
});
102+
103+
it("keeps runtime behavior correct at every level", async () => {
104+
for (const level of [0, 2, 3] as const) {
105+
const res = await executeProgram(source, {
106+
calldata: "",
107+
optimizationLevel: level,
108+
});
109+
expect(res.callSuccess).toBe(true);
110+
expect(await res.getStorage(0n)).toBe(9n); // square(3) = 9
111+
}
112+
});
113+
});

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

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,16 @@ export class InliningStep extends BaseOptimizationStep {
175175
...(declaration ? { declaration } : {}),
176176
};
177177

178+
// The call site's own source range (e.g. `square(a)`). Inlining
179+
// replaces the real call, and jump-optimization later folds the
180+
// entry jump that would carry it — so without this the call site
181+
// maps to no instruction. Preserve it by gathering it onto the
182+
// entry instruction alongside the callee-body range it collides
183+
// with (two source ranges that both apply → gather).
184+
const callSiteCode = (
185+
call.operationDebug?.context as { code?: unknown } | undefined
186+
)?.code;
187+
178188
const entryBlockId = blockRename.get(callee.entry)!;
179189
const returnBlockIds: string[] = [];
180190

@@ -191,13 +201,21 @@ export class InliningStep extends BaseOptimizationStep {
191201
cloned.operationDebug,
192202
"inline",
193203
);
194-
// Virtual invoke on the first instruction of the entry.
204+
// Virtual invoke on the first instruction of the entry,
205+
// plus the call site's source range (gathered with the
206+
// callee-body range it collides with).
195207
if (isEntry && idx === 0) {
196208
cloned.operationDebug = mergeDiscriminator(
197209
cloned.operationDebug,
198210
"invoke",
199211
inlineInvoke,
200212
);
213+
if (callSiteCode !== undefined) {
214+
cloned.operationDebug = gatherCallSite(
215+
cloned.operationDebug,
216+
callSiteCode,
217+
);
218+
}
201219
}
202220
return cloned;
203221
},
@@ -559,3 +577,38 @@ function mergeDiscriminator(
559577
} as Format.Program.Context,
560578
};
561579
}
580+
581+
/**
582+
* Preserve the call site's `code` range on the entry instruction.
583+
* The instruction already maps to the callee body, so the two source
584+
* ranges collide on `code` and are gathered — both apply. The
585+
* callee-body context (with its invoke/transform siblings) stays a
586+
* single gather child so consumers still see those discriminators on
587+
* a leaf, rather than as siblings of `gather`.
588+
*/
589+
function gatherCallSite(
590+
debug: Ir.Instruction.Debug,
591+
callSiteCode: unknown,
592+
): Ir.Instruction.Debug {
593+
const existing = (debug.context ?? {}) as Record<string, unknown>;
594+
const callSite = { code: callSiteCode };
595+
596+
if ("gather" in existing && Array.isArray(existing.gather)) {
597+
// Already a gather — add the call site as another child.
598+
return {
599+
context: {
600+
gather: [callSite, ...(existing.gather as unknown[])],
601+
} as Format.Program.Context,
602+
};
603+
}
604+
if ("code" in existing) {
605+
// Colliding `code` keys — gather both.
606+
return {
607+
context: { gather: [callSite, existing] } as Format.Program.Context,
608+
};
609+
}
610+
// No existing `code` — compose flat.
611+
return {
612+
context: { ...existing, code: callSiteCode } as Format.Program.Context,
613+
};
614+
}

0 commit comments

Comments
 (0)