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
1 change: 1 addition & 0 deletions packages/programs-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
119 changes: 111 additions & 8 deletions packages/programs-react/src/components/TraceContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -93,17 +96,59 @@ 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<string> {
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.
*/
export function TraceProvider({
trace,
program,
initialStepIndex = 0,
templates = {},
resolveVariables: shouldResolve = true,
children,
}: TraceProviderProps): JSX.Element {
const [currentStepIndex, setCurrentStepIndex] = useState(
Expand All @@ -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<ResolvedVariable[]>(
[],
);

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));
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 @@ -57,6 +57,7 @@ export {
type ContextThunk,
type FindSourceRangeOptions,
type ResolverOptions,
traceStepToMachineState,
type TraceStep,
type MockTraceSpec,
} from "#utils/index";
Expand Down
2 changes: 2 additions & 0 deletions packages/programs-react/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@ export {
type TraceStep,
type MockTraceSpec,
} from "./mockTrace.js";

export { traceStepToMachineState } from "./traceState.js";
144 changes: 144 additions & 0 deletions packages/programs-react/src/utils/traceState.ts
Original file line number Diff line number Diff line change
@@ -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<string, Data>();
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;
return Data.fromBytes(
entry.slice(Number(offset), Number(offset + length)),
);
},
};

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;
return Data.fromBytes(
value.slice(Number(offset), Number(offset + length)),
);
},
};

// 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;
return Data.fromBytes(
value.slice(Number(offset), Number(offset + length)),
);
},
};
}
Loading