Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 41 additions & 2 deletions packages/programs-react/src/components/CallStackDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,49 @@ export interface CallStackDisplayProps {
* Shows function names separated by arrows, e.g.:
* main() -> transfer() -> _update()
*/
function formatArgs(
frame: { identifier?: string; stepIndex: number },
resolvedCallStack: Array<{
stepIndex: number;
resolvedArgs?: Array<{
name: string;
value?: string;
}>;
}>,
): string {
const resolved = resolvedCallStack.find(
(r) => r.stepIndex === frame.stepIndex,
);
if (!resolved?.resolvedArgs) {
return "";
}
return resolved.resolvedArgs
.map((arg) => {
if (arg.value === undefined) {
return arg.name;
}
const decimal = formatAsDecimal(arg.value);
return `${arg.name}: ${decimal}`;
})
.join(", ");
}

function formatAsDecimal(hex: string): string {
try {
const n = BigInt(hex);
if (n <= 9999n) {
return n.toString();
}
return hex;
} catch {
return hex;
}
}

export function CallStackDisplay({
className = "",
}: CallStackDisplayProps): JSX.Element {
const { callStack, jumpToStep } = useTraceContext();
const { callStack, resolvedCallStack, jumpToStep } = useTraceContext();

if (callStack.length === 0) {
return (
Expand Down Expand Up @@ -53,7 +92,7 @@ export function CallStackDisplay({
{frame.identifier || "(anonymous)"}
</span>
<span className="call-stack-parens">
({frame.argumentNames ? frame.argumentNames.join(", ") : ""})
({formatArgs(frame, resolvedCallStack)})
</span>
</button>
</React.Fragment>
Expand Down
113 changes: 113 additions & 0 deletions packages/programs-react/src/components/TraceContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import React, {
useCallback,
useEffect,
useMemo,
useRef,
} from "react";
import type { Pointer, Program } from "@ethdebug/format";
import { dereference, Data } from "@ethdebug/pointers";
Expand Down Expand Up @@ -119,6 +120,24 @@ export interface ResolvedCallInfo {
pointerRefs: ResolvedPointerRef[];
}

/**
* A call frame with resolved argument values.
*/
export interface ResolvedCallFrame {
/** Function name */
identifier?: string;
/** The step index where this call was invoked */
stepIndex: number;
/** The call type */
callType?: "internal" | "external" | "create";
/** Argument names paired with resolved values */
resolvedArgs?: Array<{
name: string;
value?: string;
error?: string;
}>;
}

/**
* State provided by the Trace context.
*/
Expand All @@ -139,6 +158,8 @@ export interface TraceState {
currentVariables: ResolvedVariable[];
/** Call stack at current step */
callStack: CallFrame[];
/** Call stack with resolved argument values */
resolvedCallStack: ResolvedCallFrame[];
/** Call info for current instruction (if any) */
currentCallInfo: ResolvedCallInfo | undefined;
/** Whether we're at the first step */
Expand Down Expand Up @@ -339,6 +360,97 @@ export function TraceProvider({
[trace, pcToInstruction, currentStepIndex],
);

// Resolve argument values for call stack frames.
// Cache by stepIndex so we don't re-resolve frames that
// haven't changed when the user steps forward.
const argCacheRef = useRef<Map<number, ResolvedCallFrame["resolvedArgs"]>>(
new Map(),
);

const [resolvedCallStack, setResolvedCallStack] = useState<
ResolvedCallFrame[]
>([]);

useEffect(() => {
if (callStack.length === 0) {
setResolvedCallStack([]);
return;
}

// Build initial resolved frames using cached values
const initial: ResolvedCallFrame[] = callStack.map((frame) => ({
identifier: frame.identifier,
stepIndex: frame.stepIndex,
callType: frame.callType,
resolvedArgs: argCacheRef.current.get(frame.stepIndex),
}));
setResolvedCallStack(initial);

if (!shouldResolve) {
return;
}

let cancelled = false;
const resolved = [...initial];

// Resolve frames that aren't cached yet
const promises = callStack.map(async (frame, index) => {
if (argCacheRef.current.has(frame.stepIndex)) {
return;
}

const names = frame.argumentNames;
const pointers = frame.argumentPointers;
if (!pointers || pointers.length === 0) {
return;
}

const step = trace[frame.stepIndex];
if (!step) {
return;
}

const args: NonNullable<ResolvedCallFrame["resolvedArgs"]> = pointers.map(
(_, i) => ({
name: names?.[i] ?? `_${i}`,
}),
);

const resolvePromises = pointers.map(async (ptr, i) => {
try {
const value = await resolveVariableValue(
ptr as Pointer,
step,
templates,
);
args[i] = { ...args[i], value };
} catch (err) {
args[i] = {
...args[i],
error: err instanceof Error ? err.message : String(err),
};
}
});

await Promise.all(resolvePromises);

if (!cancelled) {
argCacheRef.current.set(frame.stepIndex, args);
resolved[index] = {
...resolved[index],
resolvedArgs: args,
};
setResolvedCallStack([...resolved]);
}
});

Promise.all(promises).catch(() => {});

return () => {
cancelled = true;
};
}, [callStack, shouldResolve, trace, templates]);

// Extract call info for current instruction (synchronous)
const extractedCallInfo = useMemo((): CallInfo | undefined => {
if (!currentInstruction) {
Expand Down Expand Up @@ -488,6 +600,7 @@ export function TraceProvider({
currentInstruction,
currentVariables,
callStack,
resolvedCallStack,
currentCallInfo,
isAtStart: currentStepIndex === 0,
isAtEnd: currentStepIndex >= trace.length - 1,
Expand Down
1 change: 1 addition & 0 deletions packages/programs-react/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export {
type TraceProviderProps,
type ResolvedVariable,
type ResolvedCallInfo,
type ResolvedCallFrame,
type ResolvedPointerRef,
} from "./TraceContext.js";

Expand Down
1 change: 1 addition & 0 deletions packages/programs-react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export {
type TraceProviderProps,
type ResolvedVariable,
type ResolvedCallInfo,
type ResolvedCallFrame,
type ResolvedPointerRef,
type TraceControlsProps,
type TraceProgressProps,
Expand Down
35 changes: 25 additions & 10 deletions packages/programs-react/src/utils/mockTrace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,8 @@ export interface CallFrame {
callType?: "internal" | "external" | "create";
/** Named arguments (from invoke context) */
argumentNames?: string[];
/** Individual argument pointers for value resolution */
argumentPointers?: unknown[];
}

/**
Expand Down Expand Up @@ -310,12 +312,21 @@ export function buildCallStack(
top.identifier === callInfo.identifier &&
top.callType === callInfo.callType &&
top.stepIndex === i - 1;
if (!isDuplicate) {
if (isDuplicate) {
// Use the callee entry step for resolution —
// the argument pointers reference stack slots
// that are valid at the JUMPDEST, not the JUMP
const argResult = extractArgInfo(instruction);
top.stepIndex = i;
top.argumentPointers = argResult?.pointers;
} else {
const argResult = extractArgInfo(instruction);
stack.push({
identifier: callInfo.identifier,
stepIndex: i,
callType: callInfo.callType,
argumentNames: extractArgNames(instruction),
argumentNames: argResult?.names,
argumentPointers: argResult?.pointers,
});
}
} else if (callInfo.kind === "return" || callInfo.kind === "revert") {
Expand All @@ -330,16 +341,15 @@ export function buildCallStack(
}

/**
* Extract argument names from an instruction's invoke
* context, if present.
* Extract argument names and pointers from an
* instruction's invoke context, if present.
*/
function extractArgNames(
function extractArgInfo(
instruction: Program.Instruction,
): string[] | undefined {
): { names?: string[]; pointers?: unknown[] } | undefined {
const ctx = instruction.context as Record<string, unknown> | undefined;
if (!ctx) return undefined;

// Find the invoke field (may be nested in gather)
const invoke = findInvokeField(ctx);
if (!invoke) return undefined;

Expand All @@ -353,18 +363,23 @@ function extractArgNames(
if (!Array.isArray(group)) return undefined;

const names: string[] = [];
let hasAny = false;
const pointers: unknown[] = [];
let hasAnyName = false;
for (const entry of group) {
const name = entry.name as string | undefined;
if (name) {
names.push(name);
hasAny = true;
hasAnyName = true;
} else {
names.push("_");
}
pointers.push(entry);
}

return hasAny ? names : undefined;
return {
names: hasAnyName ? names : undefined,
pointers,
};
}

function findInvokeField(
Expand Down
4 changes: 2 additions & 2 deletions packages/programs-react/src/utils/traceState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ export function traceStepToMachineState(step: TraceStep): Machine.State {
return Promise.resolve(BigInt(stackEntries.length));
},
async peek({ depth, slice }) {
const index = Number(depth);
if (index >= stackEntries.length) {
const index = stackEntries.length - 1 - Number(depth);
if (index < 0 || index >= stackEntries.length) {
throw new Error(
`Stack underflow: depth ${depth} ` +
`exceeds stack size ${stackEntries.length}`,
Expand Down
Loading
Loading