From 79ff5e3c4a10c268605e31c11591874afe607980 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Thu, 2 Jul 2026 20:35:16 -0400 Subject: [PATCH] web/programs-react: reconstruct inline virtual activations in tracer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inlined internal calls (level-2 `inline` transform) produced VIRTUAL activations that the call stack mis-rendered: because the extractor checks invoke before return, an inlined instruction's context (which carries both) always read as invoke, so buildCallStack pushed a frame per inline site and never popped — phantom `dbl > dbl` frames leaked to the end of the trace, indistinguishable from real calls. - CallInfo/CallFrame: add `isInline` (virtual activation tag). - extractCallInfoFromInstruction: set `isInline` from the `inline` transform marker (alongside the existing `isTailCall`). - buildCallStack: tag virtual frames; add a defensive per-instruction membership guard that tears down any trailing virtual frame once execution reaches an instruction without the `inline` marker, so a virtual activation can never leak into caller code. - Render an `⧉ inline` chip (dashed, italic) on the call-stack frame in both the docs TraceDrawer and the standalone CallStackDisplay, mirroring the tailcall chip — distinct from a real call. Verified end-to-end against real O2 bytecode: each inline site now shows one virtual `dbl` activation across its body and the stack returns to top level between/after sites (no phantom frames). --- .../src/components/CallStackDisplay.css | 10 +- .../src/components/CallStackDisplay.tsx | 8 + .../src/utils/mockTrace.test.ts | 163 ++++++++++++++++++ .../programs-react/src/utils/mockTrace.ts | 56 ++++-- .../src/theme/ProgramExample/TraceDrawer.css | 10 +- .../src/theme/ProgramExample/TraceDrawer.tsx | 8 + 6 files changed, 243 insertions(+), 12 deletions(-) diff --git a/packages/programs-react/src/components/CallStackDisplay.css b/packages/programs-react/src/components/CallStackDisplay.css index 90afee044..33790e6a2 100644 --- a/packages/programs-react/src/components/CallStackDisplay.css +++ b/packages/programs-react/src/components/CallStackDisplay.css @@ -49,7 +49,8 @@ color: var(--programs-text-muted, #888); } -.call-stack-tailcall { +.call-stack-tailcall, +.call-stack-inline { margin-left: 4px; padding: 0 5px; border-radius: 8px; @@ -60,3 +61,10 @@ color: var(--programs-transform-text, #8250df); border: 1px solid var(--programs-transform-accent, #a475f9); } + +/* Virtual (inline) activations use a dashed border to read as + "not a real frame" while sharing the transform palette. */ +.call-stack-inline { + border-style: dashed; + font-style: italic; +} diff --git a/packages/programs-react/src/components/CallStackDisplay.tsx b/packages/programs-react/src/components/CallStackDisplay.tsx index 983f75be1..f8dfc4b05 100644 --- a/packages/programs-react/src/components/CallStackDisplay.tsx +++ b/packages/programs-react/src/components/CallStackDisplay.tsx @@ -102,6 +102,14 @@ export function CallStackDisplay({ ⮌ tail call )} + {frame.isInline && ( + + ⧉ inline + + )} ))} diff --git a/packages/programs-react/src/utils/mockTrace.test.ts b/packages/programs-react/src/utils/mockTrace.test.ts index 1d0bef30b..f9901e6a6 100644 --- a/packages/programs-react/src/utils/mockTrace.test.ts +++ b/packages/programs-react/src/utils/mockTrace.test.ts @@ -188,3 +188,166 @@ describe("flat (production) TCO back-edge shape", () => { expect(stack.some((f) => f.isTailCall)).toBe(false); }); }); + +// Inlined internal calls (level-2 `inline` transform) produce +// VIRTUAL activations, not real ones. The compiler brackets an +// inlined body with a virtual invoke on the entry-first +// instruction and a virtual return on the exit-last instruction; +// every inlined instruction carries transform:["inline"]. The +// call stack must reconstruct the virtual frame, tag it, and +// tear it down when execution leaves the inlined body — so it +// reads distinctly from a real call and never leaks a phantom +// frame into caller code. +describe("inline virtual activations", () => { + const entryInvoke = { + code: { source: { id: "0" }, range: { offset: 0, length: 1 } }, + transform: ["inline"], + invoke: { jump: true, identifier: "dbl" }, + }; + const bodyMark = { + code: { source: { id: "0" }, range: { offset: 1, length: 1 } }, + transform: ["inline"], + }; + const exitReturn = { + code: { source: { id: "0" }, range: { offset: 2, length: 1 } }, + transform: ["inline"], + return: { identifier: "dbl" }, + }; + const callerMark = { + code: { source: { id: "0" }, range: { offset: 3, length: 1 } }, + }; + + describe("extractCallInfoFromInstruction inline flag", () => { + it("marks isInline on a virtual (inline) invoke", () => { + const info = extractCallInfoFromInstruction(instr(0, entryInvoke)); + expect(info?.kind).toBe("invoke"); + expect(info?.isInline).toBe(true); + }); + + it("marks isInline on a virtual (inline) return", () => { + const info = extractCallInfoFromInstruction(instr(0, exitReturn)); + expect(info?.kind).toBe("return"); + expect(info?.isInline).toBe(true); + }); + + it("leaves isInline falsy for a plain (real) invoke", () => { + const info = extractCallInfoFromInstruction( + instr(0, { invoke: { jump: true, identifier: "dbl" } }), + ); + expect(info?.isInline).toBeFalsy(); + }); + }); + + describe("buildCallStack virtual frame lifetime", () => { + // A single inlined body: entry / body / exit / caller. + const trace: TraceStep[] = [ + { pc: 0, opcode: "PUSH1" }, // entry invoke → push virtual dbl + { pc: 1, opcode: "ADD" }, // inlined body instruction + { pc: 2, opcode: "MSTORE" }, // exit return → pop + { pc: 3, opcode: "JUMPDEST" }, // caller code (no inline marker) + ]; + const program = { + instructions: [ + instr(0, entryInvoke), + instr(1, bodyMark), + instr(2, exitReturn), + instr(3, callerMark), + ], + } as unknown as Program; + const pcToInstruction = buildPcToInstructionMap(program); + + it("pushes a virtual frame tagged isInline at the entry", () => { + const stack = buildCallStack(trace, pcToInstruction, 0); + expect(stack).toHaveLength(1); + expect(stack[0].identifier).toBe("dbl"); + expect(stack[0].isInline).toBe(true); + }); + + it("keeps the virtual frame open across the inlined body", () => { + const stack = buildCallStack(trace, pcToInstruction, 1); + expect(stack).toHaveLength(1); + expect(stack[0].isInline).toBe(true); + }); + + it("pops the virtual frame at the exit return", () => { + const stack = buildCallStack(trace, pcToInstruction, 2); + expect(stack).toHaveLength(0); + }); + + it("does not leak a phantom frame into caller code", () => { + const stack = buildCallStack(trace, pcToInstruction, 3); + expect(stack).toHaveLength(0); + }); + }); + + describe("two gap-separated inline sites of the same helper", () => { + const trace: TraceStep[] = [ + { pc: 0, opcode: "PUSH1" }, // site 1 entry + { pc: 2, opcode: "MSTORE" }, // site 1 exit + { pc: 3, opcode: "JUMPDEST" }, // caller gap (no inline) + { pc: 10, opcode: "PUSH1" }, // site 2 entry + { pc: 12, opcode: "MSTORE" }, // site 2 exit + { pc: 13, opcode: "JUMPDEST" }, // caller + ]; + const program = { + instructions: [ + instr(0, entryInvoke), + instr(2, exitReturn), + instr(3, callerMark), + instr(10, entryInvoke), + instr(12, exitReturn), + instr(13, callerMark), + ], + } as unknown as Program; + const pcToInstruction = buildPcToInstructionMap(program); + + it("shows depth 1 while inside the second body", () => { + const stack = buildCallStack(trace, pcToInstruction, 3); + expect(stack).toHaveLength(1); + expect(stack[0].isInline).toBe(true); + }); + + it("is empty after both sites — no accumulation", () => { + const stack = buildCallStack(trace, pcToInstruction, 5); + expect(stack).toHaveLength(0); + }); + }); + + describe("defensive membership guard", () => { + // A virtual invoke whose exit return never arrives (residual + // smear / dropped marker): the frame must still be torn down + // when execution reaches a non-inline caller instruction, + // rather than leaking to the end of the trace. + const trace: TraceStep[] = [ + { pc: 0, opcode: "PUSH1" }, // virtual invoke → push + { pc: 1, opcode: "ADD" }, // still inside the body + { pc: 3, opcode: "JUMPDEST" }, // caller code, no inline marker + ]; + const program = { + instructions: [ + instr(0, entryInvoke), + instr(1, bodyMark), + instr(3, callerMark), + ], + } as unknown as Program; + const pcToInstruction = buildPcToInstructionMap(program); + + it("keeps the frame while inline membership holds", () => { + expect(buildCallStack(trace, pcToInstruction, 1)).toHaveLength(1); + }); + + it("force-pops a stale virtual frame at a non-inline instr", () => { + expect(buildCallStack(trace, pcToInstruction, 2)).toHaveLength(0); + }); + }); + + it("leaves a real call frame's isInline falsy", () => { + const trace: TraceStep[] = [{ pc: 0, opcode: "JUMPDEST" }]; + const program = { + instructions: [instr(0, { invoke: { jump: true, identifier: "f" } })], + } as unknown as Program; + const pcToInstruction = buildPcToInstructionMap(program); + const stack = buildCallStack(trace, pcToInstruction, 0); + expect(stack[0].isInline).toBeFalsy(); + }); +}); diff --git a/packages/programs-react/src/utils/mockTrace.ts b/packages/programs-react/src/utils/mockTrace.ts index 9279c85b4..eb9c55581 100644 --- a/packages/programs-react/src/utils/mockTrace.ts +++ b/packages/programs-react/src/utils/mockTrace.ts @@ -125,6 +125,12 @@ export interface CallInfo { * (TCO), reusing the current frame rather than nesting. */ isTailCall?: boolean; + /** + * True when an `inline` transform is present on the same + * instruction — this invoke/return belongs to an inlined + * (virtual) activation, not a real call. + */ + isInline?: boolean; } /** @@ -178,10 +184,15 @@ export function extractCallInfoFromInstruction( if (!info) { return undefined; } - const isTailCall = extractTransformFromContext(instruction.context).includes( - "tailcall", - ); - return isTailCall ? { ...info, isTailCall: true } : info; + const transforms = extractTransformFromContext(instruction.context); + const decorated: CallInfo = { ...info }; + if (transforms.includes("tailcall")) { + decorated.isTailCall = true; + } + if (transforms.includes("inline")) { + decorated.isInline = true; + } + return decorated; } function extractCallInfoFromContext( @@ -329,6 +340,13 @@ export interface CallFrame { * (TCO). The frame was reused in place rather than nested. */ isTailCall?: boolean; + /** + * True when this frame is a VIRTUAL activation reconstructed + * from an inlined body (transform:["inline"]) rather than a + * real call. Its instructions were spliced into the caller; + * no JUMP occurred. + */ + isInline?: boolean; } /** @@ -360,12 +378,15 @@ export function buildCallStack( continue; } + // Per-instruction inline membership drives the defensive + // guard below: an inlined body's instructions all carry + // transform:["inline"], so a virtual frame is only valid + // while that marker holds. + const isInlineInstr = + extractTransformFromInstruction(instruction).includes("inline"); const callInfo = extractCallInfoFromInstruction(instruction); - if (!callInfo) { - continue; - } - if (callInfo.isTailCall) { + if (callInfo?.isTailCall) { // A TCO back-edge carries both return and invoke on a // single instruction: the previous iteration returns // and the next iteration is invoked, reusing the same @@ -393,7 +414,7 @@ export function buildCallStack( continue; } - if (callInfo.kind === "invoke") { + if (callInfo?.kind === "invoke") { // The compiler emits invoke on both the caller JUMP // and callee entry JUMPDEST for the same call. These // occur on consecutive trace steps. Only skip if the @@ -423,14 +444,29 @@ export function buildCallStack( callType: callInfo.callType, argumentNames: argResult?.names, argumentPointers: argResult?.pointers, + // Tag virtual activations so the widget can render + // them distinctly from real calls. + ...(callInfo.isInline ? { isInline: true } : {}), }); } - } else if (callInfo.kind === "return" || callInfo.kind === "revert") { + } else if (callInfo?.kind === "return" || callInfo?.kind === "revert") { // Pop the matching frame if (stack.length > 0) { stack.pop(); } } + + // Defensive membership guard: a virtual (inline) frame must + // not stay open once execution leaves the inlined body. If + // the current instruction carries no inline marker, tear down + // any trailing virtual frames — belt-and-suspenders against a + // dropped or incomplete virtual return so a phantom activation + // can never leak into caller code. + if (!isInlineInstr) { + while (stack.length > 0 && stack[stack.length - 1].isInline) { + stack.pop(); + } + } } return stack; diff --git a/packages/web/src/theme/ProgramExample/TraceDrawer.css b/packages/web/src/theme/ProgramExample/TraceDrawer.css index d320e269e..401a318f1 100644 --- a/packages/web/src/theme/ProgramExample/TraceDrawer.css +++ b/packages/web/src/theme/ProgramExample/TraceDrawer.css @@ -267,7 +267,8 @@ border-left: 3px solid #8250df; } -.call-stack-tailcall { +.call-stack-tailcall, +.call-stack-inline { margin-left: 4px; padding: 0 5px; border-radius: 8px; @@ -279,6 +280,13 @@ border: 1px solid rgba(130, 80, 223, 0.45); } +/* Virtual (inline) activations read as "not a real frame": dashed + border + italic, sharing the transform purple tint. */ +.call-stack-inline { + border-style: dashed; + font-style: italic; +} + /* Trace panels */ .trace-panels { display: grid; diff --git a/packages/web/src/theme/ProgramExample/TraceDrawer.tsx b/packages/web/src/theme/ProgramExample/TraceDrawer.tsx index cf68e4d71..78023423b 100644 --- a/packages/web/src/theme/ProgramExample/TraceDrawer.tsx +++ b/packages/web/src/theme/ProgramExample/TraceDrawer.tsx @@ -618,6 +618,14 @@ function TraceDrawerContent(): JSX.Element { ⮌ tail call )} + {frame.isInline && ( + + ⧉ inline + + )} ))