diff --git a/packages/bugc/CHANGELOG.md b/packages/bugc/CHANGELOG.md
index 7bb8f32bd..21e94e594 100644
--- a/packages/bugc/CHANGELOG.md
+++ b/packages/bugc/CHANGELOG.md
@@ -92,6 +92,10 @@ support. Changes to the specification itself are tracked in the root
merged writes repeated it. A marker with no op on its path to carry
it is dropped: debug information does not change the code
([#384]).
+- At the JUMPDEST where an inlined function's code starts, the caller's
+ locals are listed as they are at the call, with the same pointers.
+ Before, a local with a version the optimizer removed was listed with
+ no pointer at that one step ([#380]).
- A local defined before a call keeps its pointer after the call.
The dominator tree that local-variable tracking uses left out a
call's edge to its continuation, so every local defined before a call
@@ -571,6 +575,7 @@ First publication.
[#377]: https://github.com/ethdebug/format/pull/377
[#378]: https://github.com/ethdebug/format/pull/378
[#379]: https://github.com/ethdebug/format/pull/379
+[#380]: https://github.com/ethdebug/format/pull/380
[#383]: https://github.com/ethdebug/format/pull/383
[#384]: https://github.com/ethdebug/format/pull/384
[#388]: https://github.com/ethdebug/format/pull/388
diff --git a/packages/bugc/src/evmgen/debug/local-variables.soundness.test.ts b/packages/bugc/src/evmgen/debug/local-variables.soundness.test.ts
index 34b4fd5e0..70a42d5bb 100644
--- a/packages/bugc/src/evmgen/debug/local-variables.soundness.test.ts
+++ b/packages/bugc/src/evmgen/debug/local-variables.soundness.test.ts
@@ -14,7 +14,9 @@ import { Program } from "@ethdebug/format";
import { executeProgram } from "#test/evm/behavioral";
import {
type Level,
+ type Local,
type LocalsProgram,
+ type LocalsTrace,
type Shape,
check,
localsOf,
@@ -57,6 +59,112 @@ code {
r = h;
}`;
+/** The same, with the call only on one branch */
+const callInBranch = `name CallInBranch;
+define {
+ struct P { a: uint64; b: uint32; };
+ function f() -> uint256 { return r + 3; };
+}
+storage { [0] r: uint256; [1] ps: mapping
; [2] c: uint256; }
+create { c = 1; }
+code {
+ let p: P = ps[msg.sender];
+ p.a = p.a + 5;
+ if (c == 1) { r = f(); }
+ p.b = p.b + 1;
+ ps[msg.sender] = p;
+}`;
+
+/** The same, with the call where two branches meet */
+const callAfterBranch = `name CallAfterBranch;
+define {
+ struct P { a: uint64; b: uint32; };
+ function f() -> uint256 { return r + 3; };
+}
+storage {
+ [0] r: uint256; [1] ps: mapping; [2] c: uint256;
+ [3] s: uint256;
+}
+create { c = 1; }
+code {
+ let p: P = ps[msg.sender];
+ p.a = p.a + 5;
+ if (c == 1) { s = 1; }
+ let h = f();
+ p.b = p.b + 1;
+ ps[msg.sender] = p;
+ r = h;
+}`;
+
+/** The same, with a constant argument: the callee's first
+ * instruction folds away once inlined */
+const constantArgument = `name ConstantArgument;
+define {
+ struct P { a: uint64; b: uint32; };
+ function f(a: uint256) -> uint256 { return a + 3; };
+}
+storage { [0] r: uint256; [1] ps: mapping; }
+create {}
+code {
+ let p: P = ps[msg.sender];
+ p.a = p.a + 5;
+ let h = f(4);
+ if (h == 0) { return; }
+ p.b = p.b + 1;
+ ps[msg.sender] = p;
+ r = h;
+}`;
+
+/** The same, in an inlined function, across a call it inlines */
+const nestedCall = `name NestedCall;
+define {
+ struct P { a: uint64; b: uint32; };
+ function g(q: uint256) -> uint256 { let w = q * 3; return w; };
+ function f(n: uint256) -> uint256 {
+ let p: P = ps[msg.sender];
+ p.a = p.a + 5;
+ let t = g(n);
+ p.b = p.b + 1;
+ ps[msg.sender] = p;
+ return t;
+ };
+}
+storage { [0] r: uint256; [1] ps: mapping; }
+create { r = 1; }
+code { r = f(r); }`;
+
+/** A local given a constant between two calls */
+const constantBetweenCalls = `name ConstantBetweenCalls;
+define {
+ function f(a: uint256) -> uint256 { return a + 1; };
+ function g(b: uint256) -> uint256 { return b * 2; };
+}
+storage { [0] r: uint256; [1] s: uint256; }
+create { s = 3; }
+code {
+ let x = s + 1;
+ let h = f(x);
+ x = 7;
+ let k = g(h);
+ r = k + x;
+}`;
+
+/** The same, with the second call on a branch */
+const constantBeforeBranch = `name ConstantBeforeBranch;
+define {
+ function f(a: uint256) -> uint256 { return a + 1; };
+ function g(b: uint256) -> uint256 { return b * 2; };
+}
+storage { [0] r: uint256; [1] s: uint256; }
+create { s = 3; }
+code {
+ let x = s + 1;
+ let h = f(x);
+ x = 7;
+ if (h > 1) { r = g(h); }
+ r = r + x;
+}`;
+
describe("which locals are listed", () => {
it("(a) does not list a local before its declaration", async () => {
const source = `name PreDef;
@@ -271,6 +379,73 @@ code {
];
};
+ /** The steps just before each invoke of `callee` */
+ const beforeInvokes = (run: LocalsTrace, callee: string) =>
+ run.steps.flatMap((step, k) =>
+ k > 0 &&
+ activations(run.instructionAt(step)?.context, "invoke").includes(callee)
+ ? [run.steps[k - 1]]
+ : [],
+ );
+
+ it.each([2, 3] as const)(
+ "(l) lists the caller's locals located where inlined code starts at O%i",
+ async (level) => {
+ // The step before the invoke is the JUMPDEST that starts the
+ // inlined code. It changes nothing, so `p` is where it was at the
+ // step before (and holds its value there: see `programs`). At
+ // O3, block merging can leave no entry between them
+ const sources = {
+ // (by the function called)
+ acrossCall: [acrossCall, "f"],
+ callInBranch: [callInBranch, "f"],
+ callAfterBranch: [callAfterBranch, "f"],
+ constantArgument: [constantArgument, "f"],
+ nestedCall: [nestedCall, "g"],
+ };
+ for (const [name, [source, callee]] of Object.entries(sources)) {
+ const run = await traceLocals(source, level);
+ const steps = beforeInvokes(run, callee);
+ expect(steps.length, name).toBe(1);
+ for (const step of steps) {
+ if (step.opcode !== "JUMPDEST" && level === 3) continue;
+ expect(step.opcode, name).toBe("JUMPDEST");
+ const p = localsOf(run.instructionAt(step)?.context).find(
+ (v) => v.identifier === "p",
+ );
+ expect(p?.pointer, `${name}, pc ${step.pc}`).toBeDefined();
+ }
+ }
+ },
+ );
+
+ // At O3, `g`'s code follows `f`'s return in one block in
+ // `constantBetweenCalls`, with no entry between them
+ it.each([
+ [2, "constantBetweenCalls", constantBetweenCalls],
+ [2, "constantBeforeBranch", constantBeforeBranch],
+ [3, "constantBeforeBranch", constantBeforeBranch],
+ ] as const)(
+ "(m) lists a local as it is just before an inlined call at O%i (%s)",
+ async (level, _, source) => {
+ // `x = 7` comes between the calls: at `g`'s entry, `x` is 7,
+ // which has no place, so `x` has no pointer (its old place still
+ // holds 4); `h` is located
+ const run = await traceLocals(source, level);
+ const steps = beforeInvokes(run, "g");
+ expect(steps.length).toBe(1);
+ for (const step of steps) {
+ expect(step.opcode).toBe("JUMPDEST");
+ const pointer = (name: string) =>
+ localsOf(run.instructionAt(step)?.context).find(
+ (v) => v.identifier === name,
+ )?.pointer;
+ expect(pointer("x"), `x at pc ${step.pc}`).toBeUndefined();
+ expect(pointer("h"), `h at pc ${step.pc}`).toBeDefined();
+ }
+ },
+ );
+
it.each([2, 3] as const)(
"(h) lists an inlined function's locals only within its body at O%i",
async (level) => {
@@ -967,6 +1142,71 @@ code {
h: { values: [3n] },
},
},
+ ...(
+ [
+ ["on one branch", callInBranch, {}],
+ ["after a branch", callAfterBranch, { h: { values: [3n] } }],
+ [
+ "with a constant argument",
+ constantArgument,
+ { h: { values: [7n] }, a: { values: [4n] } },
+ ],
+ [
+ "in an inlined function",
+ nestedCall,
+ {
+ n: { values: [1n] },
+ t: { values: [3n] },
+ q: { values: [1n] },
+ w: { values: [3n] },
+ },
+ ],
+ ] as Array<[string, string, Record]>
+ ).map(
+ ([where, source, others]): LocalsProgram => ({
+ name: `a struct local across a call ${where}`,
+ source,
+ locals: {
+ p: {
+ shape: {
+ kind: "struct",
+ fields: {
+ a: { kind: "scalar", size: 8 },
+ b: { kind: "scalar", size: 4 },
+ },
+ },
+ everyLevel: true,
+ values: [
+ { a: 0n, b: 0n },
+ { value: { a: 5n, b: 0n }, after: "p.a = p.a + 5" },
+ { value: { a: 5n, b: 1n }, after: "p.b = p.b + 1" },
+ ],
+ },
+ ...others,
+ },
+ }),
+ ),
+ {
+ name: "a local given a constant before a call on a branch",
+ source: constantBeforeBranch,
+ locals: {
+ x: { values: [4n, { value: 7n, after: "x = 7" }] },
+ h: { values: [5n] },
+ a: { values: [4n] },
+ b: { values: [5n] },
+ },
+ },
+ {
+ name: "a local given a constant between two calls",
+ source: constantBetweenCalls,
+ locals: {
+ x: { values: [4n, { value: 7n, after: "x = 7" }] },
+ h: { values: [5n] },
+ k: { values: [10n] },
+ a: { values: [4n] },
+ b: { values: [5n] },
+ },
+ },
{
name: "a void call",
source: `name VoidCall;
diff --git a/packages/bugc/src/evmgen/debug/local-variables.ts b/packages/bugc/src/evmgen/debug/local-variables.ts
index 53a0da2db..deb18cc35 100644
--- a/packages/bugc/src/evmgen/debug/local-variables.ts
+++ b/packages/bugc/src/evmgen/debug/local-variables.ts
@@ -796,8 +796,16 @@ function enrichFunction(
for (const [blockId, block] of func.blocks) {
// The block's entry: just before its first instruction (or its
// terminator), with that instruction's scope and inline site. Just
- // before an inlined function's invoke, that is the caller's.
+ // before an inlined function's invoke, that is the caller's, at
+ // the call: the instruction's own origin is in the callee, where
+ // the caller's versions are not placed, so the position is the
+ // call's before optimization (`callOrigin`). A call is a
+ // terminator, and its result is defined in its continuation, so
+ // the call's own position is the caller's state as the call
+ // begins, as for the call itself; `stamp` shifts the position
+ // back one, so the origin is given one past it.
const first = block.instructions[0] ?? block.terminator;
+ const callOrigin = first.operationDebug?.callOrigin;
const result = spilled.get(blockId);
const sites = first.operationDebug?.inlineSites ?? [];
const invokes = Ir.Utils.activationsOf(
@@ -809,6 +817,10 @@ function enrichFunction(
? {
...first.operationDebug,
inlineSites: sites.slice(0, -Math.min(invokes, sites.length)),
+ origin: callOrigin && {
+ ...callOrigin,
+ index: callOrigin.index + 1,
+ },
}
: first.operationDebug,
blockId,
diff --git a/packages/bugc/src/ir/spec/instruction.ts b/packages/bugc/src/ir/spec/instruction.ts
index 6d6462876..0ae29c1db 100644
--- a/packages/bugc/src/ir/spec/instruction.ts
+++ b/packages/bugc/src/ir/spec/instruction.ts
@@ -63,6 +63,12 @@ export namespace Instruction {
inlineSites?: string[];
/** Where the instruction was before optimization */
origin?: Function.Origin;
+ /**
+ * For the op that invokes an inlined function: where the call was
+ * before optimization. Just before the invoke, the caller's
+ * variables are as they were there, at the call.
+ */
+ callOrigin?: Function.Origin;
/**
* Listed locals whose value may be on the stack: code generation
* gives each a stack pointer where the stack holds its temp.
diff --git a/packages/bugc/src/ir/utils/debug.test.ts b/packages/bugc/src/ir/utils/debug.test.ts
index 1656a18fa..32e0d513e 100644
--- a/packages/bugc/src/ir/utils/debug.test.ts
+++ b/packages/bugc/src/ir/utils/debug.test.ts
@@ -1,6 +1,12 @@
import { describe, it, expect } from "vitest";
import type * as Ir from "#ir";
-import { markersOf, placeActivations } from "./debug.js";
+import {
+ combineDebugContexts,
+ dropInstructions,
+ markersOf,
+ withoutActivationsDebug,
+ placeActivations,
+} from "./debug.js";
const invoke = (identifier: string) => ({ invoke: { identifier } });
const ret = (identifier: string) => ({ return: { identifier } });
@@ -86,4 +92,120 @@ describe("placeActivations", () => {
});
expect(placed.before?.returned).toBe(returned);
});
+
+ const at = (block: string): Ir.Function.Origin => ({
+ function: "main",
+ block,
+ index: 0,
+ });
+
+ it("carries `callOrigin` with a moved invoke", () => {
+ const placed = placeActivations(
+ [invoke("f")],
+ undefined,
+ { before: plain(), after: { ...plain(), callOrigin: at("other") } },
+ at("call"),
+ );
+ expect(placed.after?.callOrigin).toEqual(at("call"));
+ expect(placed.before?.callOrigin).toBeUndefined();
+ });
+
+ it("keeps the call origin of the outermost invoke", () => {
+ // Moved after, an invoke comes before the op's own: it is outermost
+ const after = placeActivations(
+ [invoke("f")],
+ undefined,
+ { after: { ...debug(invoke("g")), callOrigin: at("g") } },
+ at("f"),
+ );
+ expect(after.after?.callOrigin).toEqual(at("f"));
+ // Moved before, it comes after the op's own
+ const before = placeActivations(
+ [invoke("f")],
+ undefined,
+ { before: { ...debug(invoke("e")), callOrigin: at("e") } },
+ at("f"),
+ );
+ expect(before.before?.callOrigin).toEqual(at("e"));
+ });
+
+ it("gives no call origin to invokes split from the first", () => {
+ // `callOrigin` is `f`'s: `g`'s call origin is not known
+ const placed = placeActivations(
+ [invoke("f"), ret("f"), invoke("g")],
+ undefined,
+ { before: plain(), after: plain() },
+ at("f"),
+ );
+ expect(names(placed.before)).toEqual(["invoke f", "return f"]);
+ expect(placed.before?.callOrigin).toEqual(at("f"));
+ expect(names(placed.after)).toEqual(["invoke g"]);
+ expect(placed.after?.callOrigin).toBeUndefined();
+ });
+});
+
+describe("dropInstructions", () => {
+ it("carries a dropped instruction's invoke with its `callOrigin`", () => {
+ const callOrigin: Ir.Function.Origin = {
+ function: "main",
+ block: "entry",
+ index: 3,
+ };
+ const dropped = {
+ kind: "env",
+ op: "msg_sender",
+ dest: "t1",
+ operationDebug: { ...debug(invoke("f")), callOrigin },
+ } as unknown as Ir.Instruction;
+ const kept = {
+ kind: "env",
+ op: "msg_value",
+ dest: "t2",
+ operationDebug: plain(),
+ } as unknown as Ir.Instruction;
+ const block = {
+ id: "inl0$entry",
+ phis: [],
+ instructions: [dropped, kept],
+ terminator: { kind: "jump", target: "next", operationDebug: {} },
+ predecessors: new Set(),
+ debug: {},
+ } as unknown as Ir.Block;
+
+ const { instructions } = dropInstructions(block, new Set([dropped]), {
+ isEntry: false,
+ });
+ expect(instructions).toHaveLength(1);
+ expect(names(instructions[0].operationDebug)).toEqual(["invoke f"]);
+ expect(instructions[0].operationDebug.callOrigin).toEqual(callOrigin);
+ });
+});
+
+describe("combineDebugContexts", () => {
+ it("keeps the call origin of the debug that carries the invoke", () => {
+ const callOrigin: Ir.Function.Origin = {
+ function: "main",
+ block: "entry",
+ index: 2,
+ };
+ const combined = combineDebugContexts(plain(), {
+ ...debug(invoke("f")),
+ callOrigin,
+ });
+ expect(names(combined)).toEqual(["invoke f"]);
+ expect(combined.callOrigin).toEqual(callOrigin);
+ });
+
+ it("keeps no call origin from a debug its invokes were taken off", () => {
+ // The invoke kept has no call origin: none is paired with it
+ const combined = combineDebugContexts(
+ withoutActivationsDebug({
+ ...debug(invoke("e")),
+ callOrigin: { function: "main", block: "entry", index: 2 },
+ }),
+ debug(invoke("f")),
+ );
+ expect(names(combined)).toEqual(["invoke f"]);
+ expect(combined.callOrigin).toBeUndefined();
+ });
});
diff --git a/packages/bugc/src/ir/utils/debug.ts b/packages/bugc/src/ir/utils/debug.ts
index 4ab959633..2b334114f 100644
--- a/packages/bugc/src/ir/utils/debug.ts
+++ b/packages/bugc/src/ir/utils/debug.ts
@@ -12,12 +12,17 @@ export function combineDebugContexts(
): Ir.Instruction.Debug {
// The first debug is the instruction that stays in place (others
// are folded into it), so the result keeps its inline sites and
- // origin.
- return withPlacement(combineContexts(debugs), debugs[0]);
+ // origin; its call origin, unless another's invoke is kept first.
+ const callOrigin = callOriginOf(debugs);
+ return {
+ ...withPlacement(combineContexts(debugs), debugs[0]),
+ ...(callOrigin ? { callOrigin } : {}),
+ };
}
-/** Keep an instruction's inline sites, origin and (if it returns from
- * an inlined function) its debug once returned on a new debug. */
+/** Keep an instruction's inline sites, origin, (if it invokes an
+ * inlined function) its call's origin and (if it returns from one) its
+ * debug once returned on a new debug. */
function withPlacement(
debug: Ir.Instruction.Debug,
from: Ir.Instruction.Debug | undefined,
@@ -26,6 +31,7 @@ function withPlacement(
...debug,
...(from?.inlineSites ? { inlineSites: from.inlineSites } : {}),
...(from?.origin ? { origin: from.origin } : {}),
+ ...(from?.callOrigin ? { callOrigin: from.callOrigin } : {}),
...(from?.returned ? { returned: from.returned } : {}),
};
}
@@ -541,11 +547,14 @@ export function withActivations(
) as Format.Program.Context;
}
-/** A debug without its `invoke` and `return` discriminators */
+/**
+ * A debug without its `invoke` and `return` discriminators, nor the
+ * call origin of its invokes
+ */
export function withoutActivationsDebug(
debug: Ir.Instruction.Debug | undefined,
): Ir.Instruction.Debug {
- const { context: _, ...rest } = debug ?? {};
+ const { context: _, callOrigin: __, ...rest } = debug ?? {};
const context = withoutActivations(debug?.context);
return { ...rest, ...(context ? { context } : {}) };
}
@@ -577,21 +586,43 @@ function inOrder(markers: ActivationMarker[]): boolean {
);
}
-/** A debug whose markers are these (in order: see `inOrder`) */
+/**
+ * The call origin of the first of these debugs to carry an invoke:
+ * that of the outermost invoke among them, as they run in order
+ */
+export function callOriginOf(
+ debugs: (Ir.Instruction.Debug | undefined)[],
+): Ir.Function.Origin | undefined {
+ return debugs.find(
+ (debug) => activationsOf(debug?.context, "invoke").length > 0,
+ )?.callOrigin;
+}
+
+/** Whether markers include an invoke */
+const hasInvoke = (markers: ActivationMarker[]) =>
+ markers.some((m) => "invoke" in m);
+
+/**
+ * A debug whose markers are these (in order: see `inOrder`), with the
+ * debug once returned of their returns and the call origin of their
+ * first invoke
+ */
function withMarkers(
debug: Ir.Instruction.Debug,
markers: ActivationMarker[],
returned: Ir.Instruction.Debug | undefined,
+ callOrigin: Ir.Function.Origin | undefined,
): Ir.Instruction.Debug {
const context = withActivations(withoutActivations(debug.context), {
invokes: markers.flatMap((m) => ("invoke" in m ? [m.invoke] : [])),
returns: markers.flatMap((m) => ("return" in m ? [m.return] : [])),
});
- const { context: _, returned: __, ...rest } = debug;
+ const { context: _, returned: __, callOrigin: ___, ...rest } = debug;
return {
...rest,
...(context ? { context } : {}),
...(returned ? { returned } : {}),
+ ...(callOrigin ? { callOrigin } : {}),
};
}
@@ -627,6 +658,8 @@ export function placeActivations(
before,
after,
}: { before?: Ir.Instruction.Debug; after?: Ir.Instruction.Debug },
+ // The call origin of the markers' first invoke (see `callOriginOf`)
+ callOrigin?: Ir.Function.Origin,
): Placement {
if (markers.length === 0) return { before, after, dropped: [] };
const own = { before: markersOf(before), after: markersOf(after) };
@@ -660,6 +693,9 @@ export function placeActivations(
before,
[...own.before, ...early],
returns(early) ? (returned ?? before.returned) : before.returned,
+ hasInvoke(own.before) || !hasInvoke(early)
+ ? before.callOrigin
+ : callOrigin,
)
: before,
after:
@@ -672,6 +708,13 @@ export function placeActivations(
: returns(late)
? returned
: after.returned,
+ // `callOrigin` is the first invoke's: with invokes on both
+ // sides, the ones after have none known (and get none)
+ hasInvoke(late)
+ ? hasInvoke(early)
+ ? undefined
+ : callOrigin
+ : after.callOrigin,
)
: after,
dropped: [],
@@ -705,6 +748,7 @@ export function dropInstructions(
// The markers of the dropped instructions since the last one kept
let pending: ActivationMarker[] = [];
let pendingReturned: Ir.Instruction.Debug | undefined;
+ let pendingCallOrigin: Ir.Function.Origin | undefined;
const settle = (after: Ir.Instruction.Debug | undefined) => {
if (pending.length === 0) return after;
@@ -714,13 +758,16 @@ export function dropInstructions(
: isEntry
? undefined
: (block.entryActivations ?? {});
- const placed = placeActivations(pending, pendingReturned, {
- before,
- after,
- });
+ const placed = placeActivations(
+ pending,
+ pendingReturned,
+ { before, after },
+ pendingCallOrigin,
+ );
dropped.push(...placed.dropped);
pending = [];
pendingReturned = undefined;
+ pendingCallOrigin = undefined;
if (placed.before !== before) {
if (last) {
const next = { ...last, operationDebug: placed.before! };
@@ -738,6 +785,9 @@ export function dropInstructions(
for (const inst of block.instructions) {
if (drop.has(inst)) {
+ if (!hasInvoke(pending)) {
+ pendingCallOrigin = callOriginOf([inst.operationDebug]);
+ }
pending.push(...markersOf(inst.operationDebug));
if (activationsOf(inst.operationDebug.context, "return").length > 0) {
pendingReturned = inst.operationDebug.returned;
diff --git a/packages/bugc/src/optimizer/steps/block-merging.ts b/packages/bugc/src/optimizer/steps/block-merging.ts
index b4dcd7432..7fed56c4c 100644
--- a/packages/bugc/src/optimizer/steps/block-merging.ts
+++ b/packages/bugc/src/optimizer/steps/block-merging.ts
@@ -143,10 +143,12 @@ function moveJumpActivations(
? undefined
: (from.entryActivations ?? {});
const after = (first ?? toJump)?.operationDebug;
- const placed = Ir.Utils.placeActivations(markers, returned, {
- before,
- after,
- });
+ const placed = Ir.Utils.placeActivations(
+ markers,
+ returned,
+ { before, after },
+ Ir.Utils.callOriginOf([jump, entry]),
+ );
if (placed.before !== before) {
if (last) {
from.instructions[from.instructions.length - 1] = {
diff --git a/packages/bugc/src/optimizer/steps/inlining.test.ts b/packages/bugc/src/optimizer/steps/inlining.test.ts
index e16b2aef6..3aea91155 100644
--- a/packages/bugc/src/optimizer/steps/inlining.test.ts
+++ b/packages/bugc/src/optimizer/steps/inlining.test.ts
@@ -259,3 +259,32 @@ code { r = cap(t); }`,
expect(jumps).toContainEqual(["inl0$", "inl0$inner$"]);
});
});
+
+describe("an inlined function's invoke the optimizer moves", () => {
+ it("keeps where the call was before optimization", async () => {
+ // The callee's first instruction reads msg.sender, which the caller
+ // read already: common subexpression elimination drops it, and the
+ // invoke moves onto the next instruction
+ const module = await optimizedIr(
+ `name MovedInvoke;
+define {
+ function f() -> uint256 { return (msg.sender as uint256) % 7; };
+}
+storage { [0] r: uint256; [1] s: mapping; }
+create {}
+code { r = s[msg.sender]; r = r + f(); }`,
+ 2,
+ );
+ const carriers = [...module.main.blocks.values()]
+ .flatMap((block) => [...block.instructions, block.terminator])
+ .filter(
+ (op) =>
+ Ir.Utils.activationsOf(op.operationDebug?.context, "invoke").length >
+ 0,
+ );
+ expect(carriers).toHaveLength(1);
+ const [carrier] = carriers;
+ expect("op" in carrier && carrier.op).not.toBe("msg_sender");
+ expect(carrier.operationDebug.callOrigin?.function).toBe("main");
+ });
+});
diff --git a/packages/bugc/src/optimizer/steps/inlining.ts b/packages/bugc/src/optimizer/steps/inlining.ts
index 7fa040cbf..e87d4b34c 100644
--- a/packages/bugc/src/optimizer/steps/inlining.ts
+++ b/packages/bugc/src/optimizer/steps/inlining.ts
@@ -209,7 +209,17 @@ export class InliningStep extends BaseOptimizationStep {
call.operationDebug?.context,
"invoke",
);
- const callDebug = withoutActivationsDebug(call.operationDebug);
+ const callDebug = Ir.Utils.withoutActivationsDebug(call.operationDebug);
+ // Where the call of the outermost function invoked was before
+ // optimization: just before the invoke is the caller's code there
+ const callOrigin =
+ pendingInvokes.length > 0
+ ? call.operationDebug?.callOrigin
+ : call.operationDebug?.origin;
+ const invoked = (debug: Ir.Instruction.Debug): Ir.Instruction.Debug => {
+ const { callOrigin: _, ...rest } = debug;
+ return callOrigin ? { ...rest, callOrigin } : rest;
+ };
const callSiteCode = (callDebug.context as { code?: unknown } | undefined)
?.code;
@@ -228,7 +238,9 @@ export class InliningStep extends BaseOptimizationStep {
// returned: the caller's, at the call or past those returns
let after = callDebug;
if (pendingReturns.length > 0) {
- after = withoutActivationsDebug(continuation!.terminator.operationDebug);
+ after = Ir.Utils.withoutActivationsDebug(
+ continuation!.terminator.operationDebug,
+ );
continuation!.terminator.operationDebug = after;
}
const invokes = [...pendingInvokes, inlineInvoke];
@@ -338,11 +350,13 @@ export class InliningStep extends BaseOptimizationStep {
const first = block.instructions[0];
const t = block.terminator;
if (first) {
- first.operationDebug = withActivationsDebug(
- callSiteCode === undefined
- ? first.operationDebug
- : gatherCallSite(first.operationDebug, callSiteCode),
- { invokes },
+ first.operationDebug = invoked(
+ withActivationsDebug(
+ callSiteCode === undefined
+ ? first.operationDebug
+ : gatherCallSite(first.operationDebug, callSiteCode),
+ { invokes },
+ ),
);
break;
}
@@ -354,7 +368,9 @@ export class InliningStep extends BaseOptimizationStep {
entryId = t.target;
continue;
}
- t.operationDebug = withActivationsDebug(t.operationDebug, { invokes });
+ t.operationDebug = invoked(
+ withActivationsDebug(t.operationDebug, { invokes }),
+ );
break;
}
@@ -732,15 +748,6 @@ function withActivationsDebug(
return { ...debug, ...(context ? { context } : {}) };
}
-/** A debug without activation discriminators */
-function withoutActivationsDebug(
- debug: Ir.Instruction.Debug | undefined,
-): Ir.Instruction.Debug {
- const { context: _, ...rest } = debug ?? {};
- const context = Ir.Utils.withoutActivations(debug?.context);
- return { ...rest, ...(context ? { context } : {}) };
-}
-
/**
* Preserve the call site's `code` range on the entry instruction.
* The instruction already maps to the callee body, so the two source
diff --git a/packages/bugc/src/optimizer/steps/jump-optimization.ts b/packages/bugc/src/optimizer/steps/jump-optimization.ts
index 660c5a44b..51e455374 100644
--- a/packages/bugc/src/optimizer/steps/jump-optimization.ts
+++ b/packages/bugc/src/optimizer/steps/jump-optimization.ts
@@ -167,6 +167,7 @@ export class JumpOptimizationStep extends BaseOptimizationStep {
for (const { from, skipped } of skipping) {
const markers: Ir.Utils.ActivationMarker[] = [];
let returned: Ir.Instruction.Debug | undefined;
+ const carriers: (Ir.Instruction.Debug | undefined)[] = [];
const visited = new Set();
let current = skipped;
while (jumpTargets.has(current) && !visited.has(current)) {
@@ -177,6 +178,7 @@ export class JumpOptimizationStep extends BaseOptimizationStep {
block.terminator.operationDebug,
]) {
markers.push(...Ir.Utils.markersOf(debug));
+ carriers.push(debug);
if (Ir.Utils.activationsOf(debug?.context, "return").length > 0) {
returned = debug?.returned;
}
@@ -197,10 +199,12 @@ export class JumpOptimizationStep extends BaseOptimizationStep {
this.edgesInto(func, current, reachable) === 1
? (target.entryActivations ?? {})
: undefined;
- const placed = Ir.Utils.placeActivations(markers, returned, {
- before: jump?.operationDebug,
- after: entry,
- });
+ const placed = Ir.Utils.placeActivations(
+ markers,
+ returned,
+ { before: jump?.operationDebug, after: entry },
+ Ir.Utils.callOriginOf(carriers),
+ );
if (jump && placed.before !== jump.operationDebug) {
source.terminator = { ...jump, operationDebug: placed.before! };
}
diff --git a/packages/bugc/src/optimizer/steps/read-write-merging.ts b/packages/bugc/src/optimizer/steps/read-write-merging.ts
index 406e32348..b6bfce683 100644
--- a/packages/bugc/src/optimizer/steps/read-write-merging.ts
+++ b/packages/bugc/src/optimizer/steps/read-write-merging.ts
@@ -258,6 +258,9 @@ export class ReadWriteMergingStep extends BaseOptimizationStep {
const markers = writes.flatMap(({ operationDebug }) =>
Ir.Utils.markersOf(operationDebug),
);
+ const callOrigin = Ir.Utils.callOriginOf(
+ writes.map(({ operationDebug }) => operationDebug),
+ );
const returned = [...writes]
.reverse()
.find(
@@ -361,18 +364,23 @@ export class ReadWriteMergingStep extends BaseOptimizationStep {
const returns = markers.filter((marker) => "return" in marker);
const placed = ordered
? {
- before: Ir.Utils.placeActivations(invokes, undefined, {
- after: first.operationDebug,
- }).after,
+ before: Ir.Utils.placeActivations(
+ invokes,
+ undefined,
+ { after: first.operationDebug },
+ callOrigin,
+ ).after,
after: Ir.Utils.placeActivations(returns, returned, {
before: mergedWrite.operationDebug,
}).before,
dropped: [],
}
- : Ir.Utils.placeActivations(markers, returned, {
- before: first.operationDebug,
- after: mergedWrite.operationDebug,
- });
+ : Ir.Utils.placeActivations(
+ markers,
+ returned,
+ { before: first.operationDebug, after: mergedWrite.operationDebug },
+ callOrigin,
+ );
first.operationDebug = placed.before!;
mergedWrite.operationDebug = placed.after!;
recordDropped(context, this.name, placed.dropped);