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
+
+ )}
))