From e0ee1f9c905b23bce572b2dd30501cf411f873a5 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Mon, 9 Mar 2026 11:15:07 -0400 Subject: [PATCH 1/2] Wire TraceProvider variable resolution to @ethdebug/pointers TraceProvider previously stubbed variable resolution, showing only metadata with no resolved values. This wires it to the dereference API so variables with pointers are resolved against the current trace step's machine state. - Add traceStepToMachineState() adapter that converts TraceStep (stack/memory/storage) into Machine.State for pointer resolution - Change TraceProvider's variable computation from synchronous useMemo to async useEffect that resolves each pointer in parallel - Add optional templates and resolveVariables props to TraceProvider - Add @ethdebug/pointers as a dependency to programs-react --- packages/programs-react/package.json | 1 + .../src/components/TraceContext.tsx | 119 ++++++++++++++- packages/programs-react/src/index.ts | 1 + packages/programs-react/src/utils/index.ts | 2 + .../programs-react/src/utils/traceState.ts | 144 ++++++++++++++++++ 5 files changed, 259 insertions(+), 8 deletions(-) create mode 100644 packages/programs-react/src/utils/traceState.ts diff --git a/packages/programs-react/package.json b/packages/programs-react/package.json index 2d207f5b3..0baa592be 100644 --- a/packages/programs-react/package.json +++ b/packages/programs-react/package.json @@ -28,6 +28,7 @@ }, "dependencies": { "@ethdebug/format": "^0.1.0-0", + "@ethdebug/pointers": "^0.1.0-0", "@shikijs/langs": "^2.5.0", "@shikijs/themes": "^2.5.0", "shiki": "^2.5.0" diff --git a/packages/programs-react/src/components/TraceContext.tsx b/packages/programs-react/src/components/TraceContext.tsx index a5201bfd0..c21874722 100644 --- a/packages/programs-react/src/components/TraceContext.tsx +++ b/packages/programs-react/src/components/TraceContext.tsx @@ -7,14 +7,17 @@ import React, { useContext, useState, useCallback, + useEffect, useMemo, } from "react"; -import type { Program } from "@ethdebug/format"; +import type { Pointer, Program } from "@ethdebug/format"; +import { dereference, Data } from "@ethdebug/pointers"; import { type TraceStep, extractVariablesFromInstruction, buildPcToInstructionMap, } from "#utils/mockTrace"; +import { traceStepToMachineState } from "#utils/traceState"; /** * A variable with its resolved value. @@ -93,10 +96,50 @@ export interface TraceProviderProps { program: Program; /** Initial step index (default: 0) */ initialStepIndex?: number; + /** Pointer templates for dereference (default: {}) */ + templates?: Pointer.Templates; + /** Whether to resolve variable values (default: true) */ + resolveVariables?: boolean; /** Children to render */ children: React.ReactNode; } +/** + * Resolve a single variable's pointer against machine + * state, returning the hex-formatted value. + */ +async function resolveVariableValue( + pointer: Pointer, + step: TraceStep, + templates: Pointer.Templates, +): Promise { + const state = traceStepToMachineState(step); + const cursor = await dereference(pointer, { + state, + templates, + }); + const view = await cursor.view(state); + + // Collect values from all regions + const values: Data[] = []; + for (const region of view.regions) { + const data = await view.read(region); + values.push(data); + } + + if (values.length === 0) { + return "0x"; + } + + // Single region: return its hex value + if (values.length === 1) { + return values[0].toHex(); + } + + // Multiple regions: concatenate hex values + return values.map((d) => d.toHex()).join(", "); +} + /** * Provides trace context to child components. */ @@ -104,6 +147,8 @@ export function TraceProvider({ trace, program, initialStepIndex = 0, + templates = {}, + resolveVariables: shouldResolve = true, children, }: TraceProviderProps): JSX.Element { const [currentStepIndex, setCurrentStepIndex] = useState( @@ -120,23 +165,81 @@ export function TraceProvider({ ? pcToInstruction.get(currentStep.pc) : undefined; - // Extract variables from current instruction - const currentVariables = useMemo(() => { + // Extract variable metadata (synchronous) + const extractedVars = useMemo(() => { if (!currentInstruction) { return []; } + return extractVariablesFromInstruction(currentInstruction); + }, [currentInstruction]); + + // Async variable resolution + const [currentVariables, setCurrentVariables] = useState( + [], + ); + + useEffect(() => { + if (extractedVars.length === 0) { + setCurrentVariables([]); + return; + } - const vars = extractVariablesFromInstruction(currentInstruction); - return vars.map((v) => ({ + // Immediately show variables with no values + const initial: ResolvedVariable[] = extractedVars.map((v) => ({ identifier: v.identifier, type: v.type, pointer: v.pointer, - // Value resolution would require the full @ethdebug/pointers machinery - // For now we just show the variable metadata value: undefined, error: undefined, })); - }, [currentInstruction]); + setCurrentVariables(initial); + + if (!shouldResolve || !currentStep) { + return; + } + + // Track whether effect is still current + let cancelled = false; + + // Resolve each variable with a pointer in parallel + const resolved = [...initial]; + const promises = extractedVars.map(async (v, index) => { + if (!v.pointer) { + return; + } + + try { + const value = await resolveVariableValue( + v.pointer as Pointer, + currentStep, + templates, + ); + if (!cancelled) { + resolved[index] = { + ...resolved[index], + value, + }; + setCurrentVariables([...resolved]); + } + } catch (err) { + if (!cancelled) { + resolved[index] = { + ...resolved[index], + error: err instanceof Error ? err.message : String(err), + }; + setCurrentVariables([...resolved]); + } + } + }); + + Promise.all(promises).catch(() => { + // Individual errors already handled above + }); + + return () => { + cancelled = true; + }; + }, [extractedVars, currentStep, shouldResolve, templates]); const stepForward = useCallback(() => { setCurrentStepIndex((prev) => Math.min(prev + 1, trace.length - 1)); diff --git a/packages/programs-react/src/index.ts b/packages/programs-react/src/index.ts index aa32365c2..599e8ff1c 100644 --- a/packages/programs-react/src/index.ts +++ b/packages/programs-react/src/index.ts @@ -57,6 +57,7 @@ export { type ContextThunk, type FindSourceRangeOptions, type ResolverOptions, + traceStepToMachineState, type TraceStep, type MockTraceSpec, } from "#utils/index"; diff --git a/packages/programs-react/src/utils/index.ts b/packages/programs-react/src/utils/index.ts index 9e24ab67b..5f750e9a0 100644 --- a/packages/programs-react/src/utils/index.ts +++ b/packages/programs-react/src/utils/index.ts @@ -21,3 +21,5 @@ export { type TraceStep, type MockTraceSpec, } from "./mockTrace.js"; + +export { traceStepToMachineState } from "./traceState.js"; diff --git a/packages/programs-react/src/utils/traceState.ts b/packages/programs-react/src/utils/traceState.ts new file mode 100644 index 000000000..5a9fa2942 --- /dev/null +++ b/packages/programs-react/src/utils/traceState.ts @@ -0,0 +1,144 @@ +/** + * Adapter for converting TraceStep to Machine.State. + * + * Bridges the trace data from programs-react into the + * Machine.State interface required by @ethdebug/pointers + * for pointer dereferencing. + */ + +import { type Machine, Data } from "@ethdebug/pointers"; +import type { TraceStep } from "./mockTrace.js"; + +/** + * Convert a TraceStep into a Machine.State suitable for + * pointer dereferencing. + * + * @param step - The trace step with EVM state + * @returns A Machine.State backed by the step's data + */ +export function traceStepToMachineState(step: TraceStep): Machine.State { + // Build stack entries (Data objects, 32-byte padded) + const stackEntries = (step.stack || []).map((entry) => + typeof entry === "string" + ? Data.fromHex(entry).padUntilAtLeast(32) + : Data.fromUint(entry).padUntilAtLeast(32), + ); + + // Parse memory from hex string + const memoryData = step.memory ? Data.fromHex(step.memory) : Data.zero(); + + // Build storage map (normalized 32-byte keys) + const storageMap = new Map(); + for (const [slot, value] of Object.entries(step.storage || {})) { + const key = Data.fromHex(slot).padUntilAtLeast(32).toHex(); + storageMap.set(key, Data.fromHex(value).padUntilAtLeast(32)); + } + + const stack: Machine.State.Stack = { + get length() { + return Promise.resolve(BigInt(stackEntries.length)); + }, + async peek({ depth, slice }) { + const index = Number(depth); + if (index >= stackEntries.length) { + throw new Error( + `Stack underflow: depth ${depth} ` + + `exceeds stack size ${stackEntries.length}`, + ); + } + const entry = stackEntries[index]; + if (!slice) { + return entry; + } + const { offset, length } = slice; + const startByte = 32 - Number(offset) - Number(length); + const endByte = startByte + Number(length); + return Data.fromBytes(entry.slice(startByte, endByte)); + }, + }; + + const memory = makeBytesReader(memoryData); + + const storage: Machine.State.Words = { + async read({ slot, slice }) { + const key = slot.padUntilAtLeast(32).toHex(); + const value = storageMap.get(key) || Data.zero().padUntilAtLeast(32); + if (!slice) { + return value; + } + const { offset, length } = slice; + const startByte = 32 - Number(offset) - Number(length); + const endByte = startByte + Number(length); + return Data.fromBytes(value.slice(startByte, endByte)); + }, + }; + + // Returndata from the step, if available + const returndataData = step.returndata + ? Data.fromHex(step.returndata) + : Data.zero(); + + return { + get traceIndex() { + return Promise.resolve(0n); + }, + get programCounter() { + return Promise.resolve(BigInt(step.pc)); + }, + get opcode() { + return Promise.resolve(step.opcode); + }, + stack, + memory, + storage, + calldata: makeBytesReader(Data.zero()), + returndata: makeBytesReader(returndataData), + code: makeBytesReader(Data.zero()), + transient: makeEmptyWordsReader(), + }; +} + +/** + * Create a Machine.State.Bytes reader from a Data buffer. + */ +function makeBytesReader(data: Data): Machine.State.Bytes { + return { + get length() { + return Promise.resolve(BigInt(data.length)); + }, + async read({ slice }) { + const { offset, length } = slice; + const start = Number(offset); + const end = start + Number(length); + if (end > data.length) { + // Zero-pad reads beyond the buffer + const result = new Uint8Array(Number(length)); + const available = Math.max(0, data.length - start); + if (available > 0 && start < data.length) { + result.set(data.slice(start, start + available), 0); + } + return Data.fromBytes(result); + } + return Data.fromBytes(data.slice(start, end)); + }, + }; +} + +/** + * Create an empty Machine.State.Words reader (returns + * zero for all slots). + */ +function makeEmptyWordsReader(): Machine.State.Words { + return { + async read({ slice }) { + const value = Data.zero().padUntilAtLeast(32); + if (!slice) { + return value; + } + const { offset, length } = slice; + const startByte = 32 - Number(offset) - Number(length); + const endByte = startByte + Number(length); + return Data.fromBytes(value.slice(startByte, endByte)); + }, + }; +} From 906e564c8d1b791f555211d1eec499d0fe928a82 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Mon, 9 Mar 2026 11:24:14 -0400 Subject: [PATCH 2/2] Fix slice offset in traceStepToMachineState slice.offset is a direct byte offset from the start of the 32-byte word, not a right-aligned big-endian offset. Use data.slice(offset, offset + length) to match the convention in packages/pointers and packages/evm. --- .../programs-react/src/utils/traceState.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/programs-react/src/utils/traceState.ts b/packages/programs-react/src/utils/traceState.ts index 5a9fa2942..8b5643a6d 100644 --- a/packages/programs-react/src/utils/traceState.ts +++ b/packages/programs-react/src/utils/traceState.ts @@ -51,9 +51,9 @@ export function traceStepToMachineState(step: TraceStep): Machine.State { return entry; } const { offset, length } = slice; - const startByte = 32 - Number(offset) - Number(length); - const endByte = startByte + Number(length); - return Data.fromBytes(entry.slice(startByte, endByte)); + return Data.fromBytes( + entry.slice(Number(offset), Number(offset + length)), + ); }, }; @@ -67,9 +67,9 @@ export function traceStepToMachineState(step: TraceStep): Machine.State { return value; } const { offset, length } = slice; - const startByte = 32 - Number(offset) - Number(length); - const endByte = startByte + Number(length); - return Data.fromBytes(value.slice(startByte, endByte)); + return Data.fromBytes( + value.slice(Number(offset), Number(offset + length)), + ); }, }; @@ -136,9 +136,9 @@ function makeEmptyWordsReader(): Machine.State.Words { return value; } const { offset, length } = slice; - const startByte = 32 - Number(offset) - Number(length); - const endByte = startByte + Number(length); - return Data.fromBytes(value.slice(startByte, endByte)); + return Data.fromBytes( + value.slice(Number(offset), Number(offset + length)), + ); }, }; }