diff --git a/packages/programs-react/src/components/CallInfoPanel.css b/packages/programs-react/src/components/CallInfoPanel.css
new file mode 100644
index 000000000..1adfe5dcc
--- /dev/null
+++ b/packages/programs-react/src/components/CallInfoPanel.css
@@ -0,0 +1,85 @@
+.call-info-panel {
+ font-size: 0.9em;
+}
+
+.call-info-banner {
+ padding: 6px 10px;
+ border-radius: 4px;
+ font-weight: 500;
+ margin-bottom: 6px;
+}
+
+.call-info-banner-invoke {
+ background: var(--programs-invoke-bg, #e8f4fd);
+ color: var(--programs-invoke-text, #0969da);
+ border-left: 3px solid var(--programs-invoke-accent, #0969da);
+}
+
+.call-info-banner-return {
+ background: var(--programs-return-bg, #dafbe1);
+ color: var(--programs-return-text, #1a7f37);
+ border-left: 3px solid var(--programs-return-accent, #1a7f37);
+}
+
+.call-info-banner-revert {
+ background: var(--programs-revert-bg, #ffebe9);
+ color: var(--programs-revert-text, #cf222e);
+ border-left: 3px solid var(--programs-revert-accent, #cf222e);
+}
+
+.call-info-refs {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ padding: 4px 0;
+}
+
+.call-info-ref {
+ display: flex;
+ align-items: baseline;
+ gap: 6px;
+ padding: 2px 0;
+}
+
+.call-info-ref-label {
+ font-weight: 500;
+ color: var(--programs-text-muted, #888);
+ min-width: 80px;
+ text-align: right;
+ flex-shrink: 0;
+}
+
+.call-info-ref-resolved {
+ font-family: monospace;
+ font-size: 0.9em;
+ word-break: break-all;
+}
+
+.call-info-ref-error {
+ color: var(--programs-error, #cf222e);
+ font-style: italic;
+}
+
+.call-info-ref-pending {
+ color: var(--programs-text-muted, #888);
+ font-style: italic;
+}
+
+.call-info-ref-pointer {
+ margin-top: 2px;
+}
+
+.call-info-ref-pointer summary {
+ cursor: pointer;
+ color: var(--programs-text-muted, #888);
+ font-size: 0.85em;
+}
+
+.call-info-ref-pointer-json {
+ font-size: 0.8em;
+ padding: 4px 8px;
+ background: var(--programs-bg-code, #f6f8fa);
+ border-radius: 3px;
+ overflow-x: auto;
+ max-height: 200px;
+}
diff --git a/packages/programs-react/src/components/CallInfoPanel.tsx b/packages/programs-react/src/components/CallInfoPanel.tsx
new file mode 100644
index 000000000..b18e58d78
--- /dev/null
+++ b/packages/programs-react/src/components/CallInfoPanel.tsx
@@ -0,0 +1,131 @@
+/**
+ * Panel showing call context info for the current instruction.
+ *
+ * Displays a banner for invoke/return/revert events and
+ * lists resolved pointer ref values (arguments, return
+ * data, etc.).
+ */
+
+import React from "react";
+import {
+ useTraceContext,
+ type ResolvedCallInfo,
+ type ResolvedPointerRef,
+} from "./TraceContext.js";
+
+// CSS is expected to be imported by the consuming application
+// import "./CallInfoPanel.css";
+
+export interface CallInfoPanelProps {
+ /** Whether to show raw pointer JSON */
+ showPointers?: boolean;
+ /** Custom class name */
+ className?: string;
+}
+
+function formatBanner(info: ResolvedCallInfo): string {
+ const name = info.identifier || "(anonymous)";
+
+ if (info.kind === "invoke") {
+ const prefix =
+ info.callType === "external"
+ ? "Calling (external)"
+ : info.callType === "create"
+ ? "Creating"
+ : "Calling";
+ return `${prefix} ${name}()`;
+ }
+
+ if (info.kind === "return") {
+ return `Returned from ${name}()`;
+ }
+
+ // revert
+ if (info.panic !== undefined) {
+ return `Reverted: panic 0x${info.panic.toString(16)}`;
+ }
+ return `Reverted in ${name}()`;
+}
+
+function bannerClassName(kind: ResolvedCallInfo["kind"]): string {
+ if (kind === "invoke") {
+ return "call-info-banner-invoke";
+ }
+ if (kind === "return") {
+ return "call-info-banner-return";
+ }
+ return "call-info-banner-revert";
+}
+
+/**
+ * Shows call context info when the current instruction
+ * has an invoke, return, or revert context.
+ */
+export function CallInfoPanel({
+ showPointers = false,
+ className = "",
+}: CallInfoPanelProps): JSX.Element | null {
+ const { currentCallInfo } = useTraceContext();
+
+ if (!currentCallInfo) {
+ return null;
+ }
+
+ return (
+
+
+ {formatBanner(currentCallInfo)}
+
+
+ {currentCallInfo.pointerRefs.length > 0 && (
+
+ {currentCallInfo.pointerRefs.map((ref) => (
+
+ ))}
+
+ )}
+
+ );
+}
+
+interface PointerRefItemProps {
+ ref_: ResolvedPointerRef;
+ showPointer: boolean;
+}
+
+function PointerRefItem({
+ ref_,
+ showPointer,
+}: PointerRefItemProps): JSX.Element {
+ return (
+
+
{ref_.label}:
+
+ {ref_.error ? (
+
+ Error: {ref_.error}
+
+ ) : ref_.value !== undefined ? (
+ {ref_.value}
+ ) : (
+ (resolving...)
+ )}
+
+
+ {showPointer && !!ref_.pointer && (
+
+ Pointer
+
+ {JSON.stringify(ref_.pointer, null, 2)}
+
+
+ )}
+
+ );
+}
diff --git a/packages/programs-react/src/components/CallStackDisplay.css b/packages/programs-react/src/components/CallStackDisplay.css
new file mode 100644
index 000000000..5d928c292
--- /dev/null
+++ b/packages/programs-react/src/components/CallStackDisplay.css
@@ -0,0 +1,46 @@
+.call-stack {
+ font-size: 0.85em;
+ padding: 4px 8px;
+}
+
+.call-stack-empty {
+ color: var(--programs-text-muted, #888);
+}
+
+.call-stack-breadcrumb {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 2px;
+}
+
+.call-stack-separator {
+ color: var(--programs-text-muted, #888);
+ user-select: none;
+}
+
+.call-stack-frame {
+ display: inline-flex;
+ align-items: center;
+ background: none;
+ border: 1px solid transparent;
+ border-radius: 3px;
+ padding: 1px 4px;
+ cursor: pointer;
+ font-family: inherit;
+ font-size: inherit;
+ color: var(--programs-link, #0366d6);
+}
+
+.call-stack-frame:hover {
+ background: var(--programs-bg-hover, rgba(0, 0, 0, 0.05));
+ border-color: var(--programs-border, #ddd);
+}
+
+.call-stack-name {
+ font-weight: 500;
+}
+
+.call-stack-parens {
+ color: var(--programs-text-muted, #888);
+}
diff --git a/packages/programs-react/src/components/CallStackDisplay.tsx b/packages/programs-react/src/components/CallStackDisplay.tsx
new file mode 100644
index 000000000..fbc55c994
--- /dev/null
+++ b/packages/programs-react/src/components/CallStackDisplay.tsx
@@ -0,0 +1,62 @@
+/**
+ * Displays the current call stack as a breadcrumb trail.
+ */
+
+import React from "react";
+import { useTraceContext } from "./TraceContext.js";
+
+// CSS is expected to be imported by the consuming application
+// import "./CallStackDisplay.css";
+
+export interface CallStackDisplayProps {
+ /** Custom class name */
+ className?: string;
+}
+
+/**
+ * Renders the call stack as a breadcrumb.
+ *
+ * Shows function names separated by arrows, e.g.:
+ * main() -> transfer() -> _update()
+ */
+export function CallStackDisplay({
+ className = "",
+}: CallStackDisplayProps): JSX.Element {
+ const { callStack, jumpToStep } = useTraceContext();
+
+ if (callStack.length === 0) {
+ return (
+
+ (top level)
+
+ );
+ }
+
+ return (
+
+
+ {callStack.map((frame, index) => (
+
+ {index > 0 && (
+ {" -> "}
+ )}
+ jumpToStep(frame.stepIndex)}
+ title={
+ `Step ${frame.stepIndex + 1}` +
+ (frame.callType ? ` (${frame.callType})` : "")
+ }
+ type="button"
+ >
+
+ {frame.identifier || "(anonymous)"}
+
+ ()
+
+
+ ))}
+
+
+ );
+}
diff --git a/packages/programs-react/src/components/TraceContext.tsx b/packages/programs-react/src/components/TraceContext.tsx
index c21874722..4a4ef9d4c 100644
--- a/packages/programs-react/src/components/TraceContext.tsx
+++ b/packages/programs-react/src/components/TraceContext.tsx
@@ -14,8 +14,12 @@ import type { Pointer, Program } from "@ethdebug/format";
import { dereference, Data } from "@ethdebug/pointers";
import {
type TraceStep,
+ type CallInfo,
+ type CallFrame,
extractVariablesFromInstruction,
+ extractCallInfoFromInstruction,
buildPcToInstructionMap,
+ buildCallStack,
} from "#utils/mockTrace";
import { traceStepToMachineState } from "#utils/traceState";
@@ -35,6 +39,36 @@ export interface ResolvedVariable {
error?: string;
}
+/**
+ * A resolved pointer ref with its label and value.
+ */
+export interface ResolvedPointerRef {
+ /** Label for this pointer (e.g., "target", "arguments") */
+ label: string;
+ /** The raw pointer */
+ pointer: unknown;
+ /** Resolved hex value */
+ value?: string;
+ /** Error if resolution failed */
+ error?: string;
+}
+
+/**
+ * Call info with resolved pointer values.
+ */
+export interface ResolvedCallInfo {
+ /** The kind of call event */
+ kind: "invoke" | "return" | "revert";
+ /** Function name */
+ identifier?: string;
+ /** Call variant for invoke contexts */
+ callType?: "internal" | "external" | "create";
+ /** Panic code for revert contexts */
+ panic?: number;
+ /** Resolved pointer refs */
+ pointerRefs: ResolvedPointerRef[];
+}
+
/**
* State provided by the Trace context.
*/
@@ -53,6 +87,10 @@ export interface TraceState {
currentInstruction: Program.Instruction | undefined;
/** Variables in scope at current step */
currentVariables: ResolvedVariable[];
+ /** Call stack at current step */
+ callStack: CallFrame[];
+ /** Call info for current instruction (if any) */
+ currentCallInfo: ResolvedCallInfo | undefined;
/** Whether we're at the first step */
isAtStart: boolean;
/** Whether we're at the last step */
@@ -241,6 +279,93 @@ export function TraceProvider({
};
}, [extractedVars, currentStep, shouldResolve, templates]);
+ // Build call stack by scanning instructions up to current step
+ const callStack = useMemo(
+ () => buildCallStack(trace, pcToInstruction, currentStepIndex),
+ [trace, pcToInstruction, currentStepIndex],
+ );
+
+ // Extract call info for current instruction (synchronous)
+ const extractedCallInfo = useMemo((): CallInfo | undefined => {
+ if (!currentInstruction) {
+ return undefined;
+ }
+ return extractCallInfoFromInstruction(currentInstruction);
+ }, [currentInstruction]);
+
+ // Async call info pointer resolution
+ const [currentCallInfo, setCurrentCallInfo] = useState<
+ ResolvedCallInfo | undefined
+ >(undefined);
+
+ useEffect(() => {
+ if (!extractedCallInfo) {
+ setCurrentCallInfo(undefined);
+ return;
+ }
+
+ // Immediately show call info without resolved values
+ const initial: ResolvedCallInfo = {
+ kind: extractedCallInfo.kind,
+ identifier: extractedCallInfo.identifier,
+ callType: extractedCallInfo.callType,
+ panic: extractedCallInfo.panic,
+ pointerRefs: extractedCallInfo.pointerRefs.map((ref) => ({
+ label: ref.label,
+ pointer: ref.pointer,
+ value: undefined,
+ error: undefined,
+ })),
+ };
+ setCurrentCallInfo(initial);
+
+ if (!shouldResolve || !currentStep) {
+ return;
+ }
+
+ let cancelled = false;
+ const resolved = [...initial.pointerRefs];
+
+ const promises = extractedCallInfo.pointerRefs.map(async (ref, index) => {
+ try {
+ const value = await resolveVariableValue(
+ ref.pointer as Pointer,
+ currentStep,
+ templates,
+ );
+ if (!cancelled) {
+ resolved[index] = {
+ ...resolved[index],
+ value,
+ };
+ setCurrentCallInfo({
+ ...initial,
+ pointerRefs: [...resolved],
+ });
+ }
+ } catch (err) {
+ if (!cancelled) {
+ resolved[index] = {
+ ...resolved[index],
+ error: err instanceof Error ? err.message : String(err),
+ };
+ setCurrentCallInfo({
+ ...initial,
+ pointerRefs: [...resolved],
+ });
+ }
+ }
+ });
+
+ Promise.all(promises).catch(() => {
+ // Individual errors already handled
+ });
+
+ return () => {
+ cancelled = true;
+ };
+ }, [extractedCallInfo, currentStep, shouldResolve, templates]);
+
const stepForward = useCallback(() => {
setCurrentStepIndex((prev) => Math.min(prev + 1, trace.length - 1));
}, [trace.length]);
@@ -272,6 +397,8 @@ export function TraceProvider({
currentStep,
currentInstruction,
currentVariables,
+ callStack,
+ currentCallInfo,
isAtStart: currentStepIndex === 0,
isAtEnd: currentStepIndex >= trace.length - 1,
stepForward,
diff --git a/packages/programs-react/src/components/index.ts b/packages/programs-react/src/components/index.ts
index 886c22d62..222acf051 100644
--- a/packages/programs-react/src/components/index.ts
+++ b/packages/programs-react/src/components/index.ts
@@ -22,6 +22,8 @@ export {
type TraceState,
type TraceProviderProps,
type ResolvedVariable,
+ type ResolvedCallInfo,
+ type ResolvedPointerRef,
} from "./TraceContext.js";
export {
@@ -37,3 +39,10 @@ export {
type VariableInspectorProps,
type StackInspectorProps,
} from "./VariableInspector.js";
+
+export {
+ CallStackDisplay,
+ type CallStackDisplayProps,
+} from "./CallStackDisplay.js";
+
+export { CallInfoPanel, type CallInfoPanelProps } from "./CallInfoPanel.js";
diff --git a/packages/programs-react/src/index.ts b/packages/programs-react/src/index.ts
index 599e8ff1c..2fa17ad36 100644
--- a/packages/programs-react/src/index.ts
+++ b/packages/programs-react/src/index.ts
@@ -26,13 +26,19 @@ export {
TraceProgress,
VariableInspector,
StackInspector,
+ CallStackDisplay,
+ CallInfoPanel,
type TraceState,
type TraceProviderProps,
type ResolvedVariable,
+ type ResolvedCallInfo,
+ type ResolvedPointerRef,
type TraceControlsProps,
type TraceProgressProps,
type VariableInspectorProps,
type StackInspectorProps,
+ type CallStackDisplayProps,
+ type CallInfoPanelProps,
} from "#components/index";
// Shiki utilities
@@ -51,7 +57,11 @@ export {
createMockTrace,
findInstructionAtPc,
extractVariablesFromInstruction,
+ extractCallInfoFromInstruction,
buildPcToInstructionMap,
+ buildCallStack,
+ type CallInfo,
+ type CallFrame,
type DynamicInstruction,
type DynamicContext,
type ContextThunk,
@@ -67,3 +77,5 @@ export {
// import "@ethdebug/programs-react/components/SourceContents.css";
// import "@ethdebug/programs-react/components/TraceControls.css";
// import "@ethdebug/programs-react/components/VariableInspector.css";
+// import "@ethdebug/programs-react/components/CallStackDisplay.css";
+// import "@ethdebug/programs-react/components/CallInfoPanel.css";
diff --git a/packages/programs-react/src/utils/index.ts b/packages/programs-react/src/utils/index.ts
index 5f750e9a0..a79f07b08 100644
--- a/packages/programs-react/src/utils/index.ts
+++ b/packages/programs-react/src/utils/index.ts
@@ -17,9 +17,13 @@ export {
createMockTrace,
findInstructionAtPc,
extractVariablesFromInstruction,
+ extractCallInfoFromInstruction,
buildPcToInstructionMap,
+ buildCallStack,
type TraceStep,
type MockTraceSpec,
+ type CallInfo,
+ type CallFrame,
} from "./mockTrace.js";
export { traceStepToMachineState } from "./traceState.js";
diff --git a/packages/programs-react/src/utils/mockTrace.ts b/packages/programs-react/src/utils/mockTrace.ts
index 6adef9e05..f3234e735 100644
--- a/packages/programs-react/src/utils/mockTrace.ts
+++ b/packages/programs-react/src/utils/mockTrace.ts
@@ -100,6 +100,195 @@ function extractVariablesFromContext(
return [];
}
+/**
+ * Info about a function call context on an instruction.
+ */
+export interface CallInfo {
+ /** The kind of call event */
+ kind: "invoke" | "return" | "revert";
+ /** Function name (from Function.Identity) */
+ identifier?: string;
+ /** Call variant for invoke contexts */
+ callType?: "internal" | "external" | "create";
+ /** Panic code for revert contexts */
+ panic?: number;
+ /** Named pointer refs to resolve */
+ pointerRefs: Array<{
+ label: string;
+ pointer: unknown;
+ }>;
+}
+
+/**
+ * Extract call info (invoke/return/revert) from an
+ * instruction's context tree.
+ */
+export function extractCallInfoFromInstruction(
+ instruction: Program.Instruction,
+): CallInfo | undefined {
+ if (!instruction.context) {
+ return undefined;
+ }
+ return extractCallInfoFromContext(instruction.context);
+}
+
+function extractCallInfoFromContext(
+ context: Program.Context,
+): CallInfo | undefined {
+ // Use unknown intermediate to avoid strict type checks
+ // on the context union — we discriminate by key presence
+ const ctx = context as unknown as Record;
+
+ if ("invoke" in ctx) {
+ const inv = ctx.invoke as Record;
+ const pointerRefs: CallInfo["pointerRefs"] = [];
+
+ let callType: CallInfo["callType"];
+ if ("jump" in inv) {
+ callType = "internal";
+ collectPointerRef(pointerRefs, "target", inv.target);
+ collectPointerRef(pointerRefs, "arguments", inv.arguments);
+ } else if ("message" in inv) {
+ callType = "external";
+ collectPointerRef(pointerRefs, "target", inv.target);
+ collectPointerRef(pointerRefs, "gas", inv.gas);
+ collectPointerRef(pointerRefs, "value", inv.value);
+ collectPointerRef(pointerRefs, "input", inv.input);
+ } else if ("create" in inv) {
+ callType = "create";
+ collectPointerRef(pointerRefs, "value", inv.value);
+ collectPointerRef(pointerRefs, "salt", inv.salt);
+ collectPointerRef(pointerRefs, "input", inv.input);
+ }
+
+ return {
+ kind: "invoke",
+ identifier: inv.identifier as string | undefined,
+ callType,
+ pointerRefs,
+ };
+ }
+
+ if ("return" in ctx) {
+ const ret = ctx.return as Record;
+ const pointerRefs: CallInfo["pointerRefs"] = [];
+ collectPointerRef(pointerRefs, "data", ret.data);
+ collectPointerRef(pointerRefs, "success", ret.success);
+
+ return {
+ kind: "return",
+ identifier: ret.identifier as string | undefined,
+ pointerRefs,
+ };
+ }
+
+ if ("revert" in ctx) {
+ const rev = ctx.revert as Record;
+ const pointerRefs: CallInfo["pointerRefs"] = [];
+ collectPointerRef(pointerRefs, "reason", rev.reason);
+
+ return {
+ kind: "revert",
+ identifier: rev.identifier as string | undefined,
+ panic: rev.panic as number | undefined,
+ pointerRefs,
+ };
+ }
+
+ // Walk gather/pick to find call info
+ if ("gather" in ctx && Array.isArray(ctx.gather)) {
+ for (const sub of ctx.gather as Program.Context[]) {
+ const info = extractCallInfoFromContext(sub);
+ if (info) {
+ return info;
+ }
+ }
+ }
+
+ if ("pick" in ctx && Array.isArray(ctx.pick)) {
+ for (const sub of ctx.pick as Program.Context[]) {
+ const info = extractCallInfoFromContext(sub);
+ if (info) {
+ return info;
+ }
+ }
+ }
+
+ return undefined;
+}
+
+function collectPointerRef(
+ refs: CallInfo["pointerRefs"],
+ label: string,
+ value: unknown,
+): void {
+ if (value && typeof value === "object" && "pointer" in value) {
+ refs.push({ label, pointer: (value as { pointer: unknown }).pointer });
+ }
+}
+
+/**
+ * A frame in the call stack.
+ */
+export interface CallFrame {
+ /** Function name */
+ identifier?: string;
+ /** The step index where this call was invoked */
+ stepIndex: number;
+ /** The call type */
+ callType?: "internal" | "external" | "create";
+}
+
+/**
+ * Build a call stack by scanning instructions from
+ * step 0 to the given step index.
+ */
+export function buildCallStack(
+ trace: TraceStep[],
+ pcToInstruction: Map,
+ upToStep: number,
+): CallFrame[] {
+ const stack: CallFrame[] = [];
+
+ for (let i = 0; i <= upToStep && i < trace.length; i++) {
+ const step = trace[i];
+ const instruction = pcToInstruction.get(step.pc);
+ if (!instruction) {
+ continue;
+ }
+
+ const callInfo = extractCallInfoFromInstruction(instruction);
+ if (!callInfo) {
+ continue;
+ }
+
+ if (callInfo.kind === "invoke") {
+ // The compiler emits invoke on both the caller JUMP and
+ // callee entry JUMPDEST. Skip if the top frame already
+ // matches this call.
+ const top = stack[stack.length - 1];
+ if (
+ !top ||
+ top.identifier !== callInfo.identifier ||
+ top.callType !== callInfo.callType
+ ) {
+ stack.push({
+ identifier: callInfo.identifier,
+ stepIndex: i,
+ callType: callInfo.callType,
+ });
+ }
+ } else if (callInfo.kind === "return" || callInfo.kind === "revert") {
+ // Pop the matching frame
+ if (stack.length > 0) {
+ stack.pop();
+ }
+ }
+ }
+
+ return stack;
+}
+
/**
* Build a map of PC to instruction for quick lookup.
*/
diff --git a/packages/web/src/theme/ProgramExample/CallInfoPanel.css b/packages/web/src/theme/ProgramExample/CallInfoPanel.css
new file mode 100644
index 000000000..1adfe5dcc
--- /dev/null
+++ b/packages/web/src/theme/ProgramExample/CallInfoPanel.css
@@ -0,0 +1,85 @@
+.call-info-panel {
+ font-size: 0.9em;
+}
+
+.call-info-banner {
+ padding: 6px 10px;
+ border-radius: 4px;
+ font-weight: 500;
+ margin-bottom: 6px;
+}
+
+.call-info-banner-invoke {
+ background: var(--programs-invoke-bg, #e8f4fd);
+ color: var(--programs-invoke-text, #0969da);
+ border-left: 3px solid var(--programs-invoke-accent, #0969da);
+}
+
+.call-info-banner-return {
+ background: var(--programs-return-bg, #dafbe1);
+ color: var(--programs-return-text, #1a7f37);
+ border-left: 3px solid var(--programs-return-accent, #1a7f37);
+}
+
+.call-info-banner-revert {
+ background: var(--programs-revert-bg, #ffebe9);
+ color: var(--programs-revert-text, #cf222e);
+ border-left: 3px solid var(--programs-revert-accent, #cf222e);
+}
+
+.call-info-refs {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ padding: 4px 0;
+}
+
+.call-info-ref {
+ display: flex;
+ align-items: baseline;
+ gap: 6px;
+ padding: 2px 0;
+}
+
+.call-info-ref-label {
+ font-weight: 500;
+ color: var(--programs-text-muted, #888);
+ min-width: 80px;
+ text-align: right;
+ flex-shrink: 0;
+}
+
+.call-info-ref-resolved {
+ font-family: monospace;
+ font-size: 0.9em;
+ word-break: break-all;
+}
+
+.call-info-ref-error {
+ color: var(--programs-error, #cf222e);
+ font-style: italic;
+}
+
+.call-info-ref-pending {
+ color: var(--programs-text-muted, #888);
+ font-style: italic;
+}
+
+.call-info-ref-pointer {
+ margin-top: 2px;
+}
+
+.call-info-ref-pointer summary {
+ cursor: pointer;
+ color: var(--programs-text-muted, #888);
+ font-size: 0.85em;
+}
+
+.call-info-ref-pointer-json {
+ font-size: 0.8em;
+ padding: 4px 8px;
+ background: var(--programs-bg-code, #f6f8fa);
+ border-radius: 3px;
+ overflow-x: auto;
+ max-height: 200px;
+}
diff --git a/packages/web/src/theme/ProgramExample/CallStackDisplay.css b/packages/web/src/theme/ProgramExample/CallStackDisplay.css
new file mode 100644
index 000000000..5d928c292
--- /dev/null
+++ b/packages/web/src/theme/ProgramExample/CallStackDisplay.css
@@ -0,0 +1,46 @@
+.call-stack {
+ font-size: 0.85em;
+ padding: 4px 8px;
+}
+
+.call-stack-empty {
+ color: var(--programs-text-muted, #888);
+}
+
+.call-stack-breadcrumb {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 2px;
+}
+
+.call-stack-separator {
+ color: var(--programs-text-muted, #888);
+ user-select: none;
+}
+
+.call-stack-frame {
+ display: inline-flex;
+ align-items: center;
+ background: none;
+ border: 1px solid transparent;
+ border-radius: 3px;
+ padding: 1px 4px;
+ cursor: pointer;
+ font-family: inherit;
+ font-size: inherit;
+ color: var(--programs-link, #0366d6);
+}
+
+.call-stack-frame:hover {
+ background: var(--programs-bg-hover, rgba(0, 0, 0, 0.05));
+ border-color: var(--programs-border, #ddd);
+}
+
+.call-stack-name {
+ font-weight: 500;
+}
+
+.call-stack-parens {
+ color: var(--programs-text-muted, #888);
+}
diff --git a/packages/web/src/theme/ProgramExample/TraceDrawer.css b/packages/web/src/theme/ProgramExample/TraceDrawer.css
index 4da55e7be..9f36d24c0 100644
--- a/packages/web/src/theme/ProgramExample/TraceDrawer.css
+++ b/packages/web/src/theme/ProgramExample/TraceDrawer.css
@@ -127,6 +127,83 @@
text-align: center;
}
+/* Call stack breadcrumb */
+.call-stack-bar {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 2px;
+ padding: 4px 12px;
+ font-size: 12px;
+ background: var(--ifm-background-surface-color);
+ border-bottom: 1px solid var(--ifm-color-emphasis-200);
+ flex-shrink: 0;
+}
+
+.call-stack-label {
+ font-size: 11px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ color: var(--ifm-color-content-secondary);
+ margin-right: 4px;
+}
+
+.call-stack-toplevel {
+ color: var(--ifm-color-content-secondary);
+ font-style: italic;
+}
+
+.call-stack-sep {
+ color: var(--ifm-color-content-secondary);
+ padding: 0 2px;
+ user-select: none;
+}
+
+.call-stack-frame-btn {
+ background: none;
+ border: 1px solid transparent;
+ border-radius: 3px;
+ padding: 1px 4px;
+ cursor: pointer;
+ font-family: var(--ifm-font-family-monospace);
+ font-size: 12px;
+ font-weight: 500;
+ color: var(--ifm-color-primary);
+}
+
+.call-stack-frame-btn:hover {
+ background: var(--ifm-color-emphasis-100);
+ border-color: var(--ifm-color-emphasis-300);
+}
+
+/* Call info banner */
+.call-info-bar {
+ padding: 4px 12px;
+ font-size: 12px;
+ font-weight: 500;
+ flex-shrink: 0;
+ border-bottom: 1px solid var(--ifm-color-emphasis-200);
+}
+
+.call-info-invoke {
+ background: var(--ifm-color-info-contrast-background);
+ color: var(--ifm-color-info-darkest);
+ border-left: 3px solid var(--ifm-color-info);
+}
+
+.call-info-return {
+ background: var(--ifm-color-success-contrast-background);
+ color: var(--ifm-color-success-darkest);
+ border-left: 3px solid var(--ifm-color-success);
+}
+
+.call-info-revert {
+ background: var(--ifm-color-danger-contrast-background);
+ color: var(--ifm-color-danger-darkest);
+ border-left: 3px solid var(--ifm-color-danger);
+}
+
/* 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 72c69418b..0cae58678 100644
--- a/packages/web/src/theme/ProgramExample/TraceDrawer.tsx
+++ b/packages/web/src/theme/ProgramExample/TraceDrawer.tsx
@@ -85,6 +85,62 @@ function TraceDrawerContent(): JSX.Element {
return extractVariables(instruction.debug.context);
}, [trace, currentStep, pcToInstruction]);
+ // Extract call info from current instruction context
+ const currentCallInfo = useMemo(() => {
+ if (trace.length === 0 || currentStep >= trace.length) {
+ return undefined;
+ }
+
+ const step = trace[currentStep];
+ const instruction = pcToInstruction.get(step.pc);
+ if (!instruction?.debug?.context) return undefined;
+
+ return extractCallInfo(instruction.debug.context);
+ }, [trace, currentStep, pcToInstruction]);
+
+ // Build call stack by scanning invoke/return/revert up to
+ // current step
+ const callStack = useMemo(() => {
+ const frames: Array<{
+ identifier?: string;
+ stepIndex: number;
+ callType?: string;
+ }> = [];
+
+ for (let i = 0; i <= currentStep && i < trace.length; i++) {
+ const step = trace[i];
+ const instruction = pcToInstruction.get(step.pc);
+ if (!instruction?.debug?.context) continue;
+
+ const info = extractCallInfo(instruction.debug.context);
+ if (!info) continue;
+
+ if (info.kind === "invoke") {
+ // The compiler emits invoke on both the caller
+ // JUMP and callee entry JUMPDEST. Skip if the
+ // top frame already matches this call.
+ const top = frames[frames.length - 1];
+ if (
+ !top ||
+ top.identifier !== info.identifier ||
+ top.callType !== info.callType
+ ) {
+ frames.push({
+ identifier: info.identifier,
+ stepIndex: i,
+ callType: info.callType,
+ });
+ }
+ } else if (info.kind === "return" || info.kind === "revert") {
+ if (frames.length > 0) {
+ frames.pop();
+ }
+ }
+ }
+
+ return frames;
+ }, [trace, currentStep, pcToInstruction]);
+
// Compile source and run trace in one shot.
// Takes source directly to avoid stale-state issues.
const compileAndTrace = useCallback(async (sourceCode: string) => {
@@ -298,6 +354,36 @@ function TraceDrawerContent(): JSX.Element {
+
+ Call Stack:
+ {callStack.length === 0 ? (
+ (top level)
+ ) : (
+ callStack.map((frame, i) => (
+
+ {i > 0 && (
+ ›
+ )}
+ setCurrentStep(frame.stepIndex)}
+ type="button"
+ >
+ {frame.identifier || "(anonymous)"}
+
+
+ ))
+ )}
+
+
+ {currentCallInfo && (
+
+ {formatCallBanner(currentCallInfo)}
+
+ )}
+
Instructions
@@ -468,6 +554,90 @@ function VariablesDisplay({ variables }: VariablesDisplayProps): JSX.Element {
);
}
+/**
+ * Info about a call context (invoke/return/revert).
+ */
+interface CallInfoResult {
+ kind: "invoke" | "return" | "revert";
+ identifier?: string;
+ callType?: string;
+}
+
+/**
+ * Extract call info from an ethdebug format context object.
+ */
+function extractCallInfo(context: unknown): CallInfoResult | undefined {
+ if (!context || typeof context !== "object") {
+ return undefined;
+ }
+
+ const ctx = context as Record
;
+
+ if ("invoke" in ctx && ctx.invoke) {
+ const inv = ctx.invoke as Record;
+ let callType: string | undefined;
+ if ("jump" in inv) callType = "internal";
+ else if ("message" in inv) callType = "external";
+ else if ("create" in inv) callType = "create";
+
+ return {
+ kind: "invoke",
+ identifier: inv.identifier as string | undefined,
+ callType,
+ };
+ }
+
+ if ("return" in ctx && ctx.return) {
+ const ret = ctx.return as Record;
+ return {
+ kind: "return",
+ identifier: ret.identifier as string | undefined,
+ };
+ }
+
+ if ("revert" in ctx && ctx.revert) {
+ const rev = ctx.revert as Record;
+ return {
+ kind: "revert",
+ identifier: rev.identifier as string | undefined,
+ };
+ }
+
+ // Walk gather/pick
+ if ("gather" in ctx && Array.isArray(ctx.gather)) {
+ for (const sub of ctx.gather) {
+ const info = extractCallInfo(sub);
+ if (info) return info;
+ }
+ }
+
+ if ("pick" in ctx && Array.isArray(ctx.pick)) {
+ for (const sub of ctx.pick) {
+ const info = extractCallInfo(sub);
+ if (info) return info;
+ }
+ }
+
+ return undefined;
+}
+
+/**
+ * Format a call info banner string.
+ */
+function formatCallBanner(info: CallInfoResult): string {
+ const name = info.identifier || "(anonymous)";
+ switch (info.kind) {
+ case "invoke": {
+ const prefix = info.callType === "create" ? "Creating" : "Calling";
+ return `${prefix} ${name}()`;
+ }
+ case "return":
+ return `Returned from ${name}()`;
+ case "revert":
+ return `Reverted in ${name}()`;
+ }
+}
+
/**
* Extract variables from an ethdebug format context object.
*/
diff --git a/packages/web/src/theme/ProgramExample/TraceViewer.tsx b/packages/web/src/theme/ProgramExample/TraceViewer.tsx
index 59ee577f0..c2348beb7 100644
--- a/packages/web/src/theme/ProgramExample/TraceViewer.tsx
+++ b/packages/web/src/theme/ProgramExample/TraceViewer.tsx
@@ -12,6 +12,8 @@ import {
TraceProgress,
VariableInspector,
StackInspector,
+ CallStackDisplay,
+ CallInfoPanel,
useTraceContext,
type TraceStep,
} from "@ethdebug/programs-react";
@@ -20,6 +22,8 @@ import {
import "./TraceViewer.css";
import "./TraceControls.css";
import "./VariableInspector.css";
+import "./CallStackDisplay.css";
+import "./CallInfoPanel.css";
export interface TraceViewerProps {
/** The execution trace */
@@ -97,6 +101,7 @@ function TraceViewerContent({
+
@@ -118,6 +123,8 @@ function TraceViewerContent({
+
+
{showVariables && (
Variables
diff --git a/packages/web/src/theme/ProgramExample/index.ts b/packages/web/src/theme/ProgramExample/index.ts
index 47a0e1113..c930d1b4d 100644
--- a/packages/web/src/theme/ProgramExample/index.ts
+++ b/packages/web/src/theme/ProgramExample/index.ts
@@ -17,13 +17,19 @@ export {
TraceProgress,
VariableInspector,
StackInspector,
+ CallStackDisplay,
+ CallInfoPanel,
type TraceState,
type TraceProviderProps,
type ResolvedVariable,
+ type ResolvedCallInfo,
+ type ResolvedPointerRef,
type TraceControlsProps,
type TraceProgressProps,
type VariableInspectorProps,
type StackInspectorProps,
+ type CallStackDisplayProps,
+ type CallInfoPanelProps,
} from "@ethdebug/programs-react";
// Also re-export utilities for convenience
@@ -33,12 +39,16 @@ export {
createMockTrace,
findInstructionAtPc,
extractVariablesFromInstruction,
+ extractCallInfoFromInstruction,
buildPcToInstructionMap,
+ buildCallStack,
type DynamicInstruction,
type DynamicContext,
type ContextThunk,
type TraceStep,
type MockTraceSpec,
+ type CallInfo,
+ type CallFrame,
} from "@ethdebug/programs-react";
// Local Docusaurus-specific components