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);