diff --git a/packages/programs-react/src/components/TraceContext.test.tsx b/packages/programs-react/src/components/TraceContext.test.tsx new file mode 100644 index 0000000000..767868549a --- /dev/null +++ b/packages/programs-react/src/components/TraceContext.test.tsx @@ -0,0 +1,123 @@ +/** + * Integration tests for TraceProvider's postcondition-aware + * context selection. Instruction contexts are postconditions, so + * the variables/call-info shown at the step about to execute + * instruction i come from instruction i-1 (program-level context + * at the first step). See effectiveContextForStep. + */ + +import { describe, it, expect } from "vitest"; +import { renderHook, act } from "@testing-library/react"; +import React from "react"; +import type { Program } from "@ethdebug/format"; +import { TraceProvider, useTraceContext } from "./TraceContext.js"; +import type { TraceStep } from "#utils/mockTrace"; + +function instr(offset: number, context: unknown): Program.Instruction { + return { + offset, + operation: { mnemonic: "JUMPDEST", arguments: [] }, + context, + } as unknown as Program.Instruction; +} + +const program = { + context: { variables: [{ identifier: "prog" }] }, + instructions: [ + instr(0, { variables: [{ identifier: "v0" }] }), + instr(3, { variables: [{ identifier: "v1" }] }), + instr(6, { variables: [{ identifier: "v2" }] }), + ], +} as unknown as Program; + +const trace: TraceStep[] = [ + { pc: 0, opcode: "JUMPDEST" }, + { pc: 3, opcode: "JUMPDEST" }, + { pc: 6, opcode: "JUMPDEST" }, +]; + +// Stable identity so the provider's resolution effects don't +// re-run every render (the default `templates={}` would mint a new +// object each render). +const templates = {}; + +function renderTrace() { + return renderHook(() => useTraceContext(), { + wrapper: ({ children }: { children: React.ReactNode }) => ( + + {children} + + ), + }); +} + +const ids = (vars: { identifier?: string }[]) => vars.map((v) => v.identifier); + +describe("TraceProvider postcondition context selection", () => { + it("shows the program-level context at the first step", () => { + const { result } = renderTrace(); + expect(ids(result.current.currentVariables)).toEqual(["prog"]); + }); + + it("shows the previous instruction's variables after stepping", () => { + const { result } = renderTrace(); + + act(() => result.current.jumpToStep(1)); + // step 1 executes pc=3; observed state is the postcondition of + // pc=0, so the panel shows v0 (NOT v1). + expect(ids(result.current.currentVariables)).toEqual(["v0"]); + + act(() => result.current.jumpToStep(2)); + expect(ids(result.current.currentVariables)).toEqual(["v1"]); + }); +}); + +describe("TraceProvider postcondition call-info selection", () => { + const callProgram = { + instructions: [ + instr(0, { invoke: { jump: true, identifier: "sum" } }), + instr(3, { variables: [] }), + ], + } as unknown as Program; + + const callTrace: TraceStep[] = [ + { pc: 0, opcode: "JUMPDEST" }, + { pc: 3, opcode: "JUMPDEST" }, + ]; + + function render() { + return renderHook(() => useTraceContext(), { + wrapper: ({ children }: { children: React.ReactNode }) => ( + + {children} + + ), + }); + } + + it("does not show the invoke while parked on the invoke instruction", () => { + const { result } = render(); + // step 0 is about to execute pc=0 (the invoke); it has not run + // yet, so no call info is shown. + expect(result.current.currentCallInfo).toBeUndefined(); + }); + + it("shows the invoke once its instruction has executed", () => { + const { result } = render(); + act(() => result.current.jumpToStep(1)); + // step 1 observes the postcondition of pc=0, so the invoke of + // "sum" surfaces here. + expect(result.current.currentCallInfo?.kind).toBe("invoke"); + expect(result.current.currentCallInfo?.identifier).toBe("sum"); + }); +}); diff --git a/packages/programs-react/src/components/TraceContext.tsx b/packages/programs-react/src/components/TraceContext.tsx index 795f3f2f44..6557bb65e6 100644 --- a/packages/programs-react/src/components/TraceContext.tsx +++ b/packages/programs-react/src/components/TraceContext.tsx @@ -23,6 +23,7 @@ import { buildCallStack, } from "#utils/mockTrace"; import { traceStepToMachineState } from "#utils/traceState"; +import { effectiveContextForStep } from "#utils/effectiveContext"; /** * Compute a key representing an instruction's source range, @@ -282,13 +283,39 @@ export function TraceProvider({ ? pcToInstruction.get(currentStep.pc) : undefined; + // Instruction contexts are POSTCONDITIONS: the semantic facts and + // pointers shown at the step about to execute instruction i come + // from instruction i-1 (program-level context at the first step). + // Pointer resolution still runs against the state observed at step + // i; only the context selection shifts. See effectiveContextForStep. + const effectiveContext = useMemo( + () => + effectiveContextForStep({ + programContext: program.context, + contextAtPc: (pc) => pcToInstruction.get(pc)?.context, + trace, + stepIndex: currentStepIndex, + }), + [program.context, pcToInstruction, trace, currentStepIndex], + ); + + // A synthetic instruction lets the context-tree extractors (which + // read `.context`) apply uniformly to the program-level base case. + const effectiveInstruction = useMemo( + () => + effectiveContext + ? ({ context: effectiveContext } as Program.Instruction) + : undefined, + [effectiveContext], + ); + // Extract variable metadata (synchronous) const extractedVars = useMemo(() => { - if (!currentInstruction) { + if (!effectiveInstruction) { return []; } - return extractVariablesFromInstruction(currentInstruction); - }, [currentInstruction]); + return extractVariablesFromInstruction(effectiveInstruction); + }, [effectiveInstruction]); // Async variable resolution const [currentVariables, setCurrentVariables] = useState( @@ -456,12 +483,12 @@ export function TraceProvider({ }; }, [callStack, shouldResolve, trace, templates]); - // Extract call info for current instruction (synchronous) + // Extract call info from the effective (postcondition) context. const extractedCallInfo = useMemo((): CallInfo | undefined => { - if (!currentInstruction) { + if (!effectiveInstruction) { return undefined; } - return extractCallInfoFromInstruction(currentInstruction); + return extractCallInfoFromInstruction(effectiveInstruction); }, [currentInstruction]); // Async call info pointer resolution diff --git a/packages/programs-react/src/index.ts b/packages/programs-react/src/index.ts index 8d86107711..0ce79eb4bf 100644 --- a/packages/programs-react/src/index.ts +++ b/packages/programs-react/src/index.ts @@ -70,6 +70,8 @@ export { type FindSourceRangeOptions, type ResolverOptions, traceStepToMachineState, + effectiveContextForStep, + type EffectiveContextInput, type TraceStep, type MockTraceSpec, } from "#utils/index"; diff --git a/packages/programs-react/src/utils/effectiveContext.test.ts b/packages/programs-react/src/utils/effectiveContext.test.ts new file mode 100644 index 0000000000..ea92c6dd7b --- /dev/null +++ b/packages/programs-react/src/utils/effectiveContext.test.ts @@ -0,0 +1,79 @@ +/** + * Tests for effectiveContextForStep — the postcondition-aware + * selection of which instruction context describes the machine + * state observed at a given trace step. + * + * Instruction contexts are POSTCONDITIONS: instruction i's + * context describes the state AFTER i executes. A debugger + * paused at the trace step about to execute instruction i is + * observing the state produced by instruction i-1, so it must + * apply instruction i-1's context. Program-level context is the + * base case for the first step. + */ + +import { describe, it, expect } from "vitest"; +import type { Program } from "@ethdebug/format"; +import { effectiveContextForStep } from "./effectiveContext.js"; + +const programContext = { + code: { range: { offset: 0, length: 1 } }, +} as Program.Context; +const ctxAt10 = { + variables: [{ identifier: "a" }], +} as unknown as Program.Context; +const ctxAt20 = { + variables: [{ identifier: "b" }], +} as unknown as Program.Context; + +const contextByPc = new Map([ + [10, ctxAt10], + [20, ctxAt20], +]); +const contextAtPc = (pc: number) => contextByPc.get(pc); + +const trace = [{ pc: 10 }, { pc: 20 }]; + +describe("effectiveContextForStep", () => { + it("returns the program-level context at the first step", () => { + const result = effectiveContextForStep({ + programContext, + contextAtPc, + trace, + stepIndex: 0, + }); + expect(result).toBe(programContext); + }); + + it("returns undefined at the first step when there is no program context", () => { + const result = effectiveContextForStep({ + programContext: undefined, + contextAtPc, + trace, + stepIndex: 0, + }); + expect(result).toBeUndefined(); + }); + + it("returns the PREVIOUS step's instruction context, not the current step's", () => { + const result = effectiveContextForStep({ + programContext, + contextAtPc, + trace, + stepIndex: 1, + }); + // step 1 executes pc=20; the observed state is the postcondition + // of pc=10, so context(pc=10) must be returned. + expect(result).toBe(ctxAt10); + expect(result).not.toBe(ctxAt20); + }); + + it("returns undefined when the previous step's instruction has no context", () => { + const result = effectiveContextForStep({ + programContext, + contextAtPc: () => undefined, + trace, + stepIndex: 1, + }); + expect(result).toBeUndefined(); + }); +}); diff --git a/packages/programs-react/src/utils/effectiveContext.ts b/packages/programs-react/src/utils/effectiveContext.ts new file mode 100644 index 0000000000..408b9f24fc --- /dev/null +++ b/packages/programs-react/src/utils/effectiveContext.ts @@ -0,0 +1,60 @@ +/** + * Postcondition-aware selection of the instruction context that + * describes the machine state observed at a given trace step. + * + * Instruction contexts are POSTCONDITIONS: instruction i's + * context — its semantic facts AND its pointers — describes the + * machine state AFTER i executes. A trace step observes the state + * BEFORE its instruction executes, so the step about to execute + * instruction i is observing the postcondition of instruction + * i-1. The consumer rule is therefore: prepend the program-level + * context and index the resulting sequence by trace position — + * i.e. apply instruction (i-1)'s context at step i, with the + * program-level context as the base case for the first step. + * + * Pointer resolution still runs against the state observed at + * step i; only the CONTEXT selection shifts. + */ + +import type { Program } from "@ethdebug/format"; + +/** + * Inputs for {@link effectiveContextForStep}. The context source + * is supplied as an accessor so callers with different + * instruction shapes (e.g. `instruction.context` vs + * `instruction.debug.context`) can share this logic. + */ +export interface EffectiveContextInput { + /** Program-level context (base case for the first step). */ + programContext?: Program.Context; + /** Resolve the context carried by the instruction at a pc. */ + contextAtPc(pc: number): Program.Context | undefined; + /** The trace, indexed by step position. */ + trace: ReadonlyArray<{ pc: number }>; + /** The current step position. */ + stepIndex: number; +} + +/** + * Return the context describing the state observed at + * `stepIndex`: the program-level context at the first step, + * otherwise the context of the instruction executed at the + * previous step. + */ +export function effectiveContextForStep({ + programContext, + contextAtPc, + trace, + stepIndex, +}: EffectiveContextInput): Program.Context | undefined { + if (stepIndex <= 0) { + return programContext; + } + + const previous = trace[stepIndex - 1]; + if (!previous) { + return programContext; + } + + return contextAtPc(previous.pc); +} diff --git a/packages/programs-react/src/utils/index.ts b/packages/programs-react/src/utils/index.ts index e6dfdbefb7..881167bb3e 100644 --- a/packages/programs-react/src/utils/index.ts +++ b/packages/programs-react/src/utils/index.ts @@ -28,3 +28,8 @@ export { } from "./mockTrace.js"; export { traceStepToMachineState } from "./traceState.js"; + +export { + effectiveContextForStep, + type EffectiveContextInput, +} from "./effectiveContext.js"; diff --git a/packages/web/src/theme/ProgramExample/TraceDrawer.tsx b/packages/web/src/theme/ProgramExample/TraceDrawer.tsx index 1d734c43f3..02d7420840 100644 --- a/packages/web/src/theme/ProgramExample/TraceDrawer.tsx +++ b/packages/web/src/theme/ProgramExample/TraceDrawer.tsx @@ -27,6 +27,7 @@ import { Executor, createTraceCollector, type TraceStep } from "@ethdebug/evm"; import { dereference, Data, type Machine } from "@ethdebug/pointers"; import { buildCallStack, + effectiveContextForStep, extractCallInfoFromInstruction, extractTransformFromInstruction, type CallFrame, @@ -183,16 +184,27 @@ function TraceDrawerContent(): JSX.Element { return extractSourceRange(instruction.debug.context); }, [trace, currentStep, pcToInstruction]); - // Extract variables from current instruction context - const currentVariables = useMemo(() => { - if (trace.length === 0 || currentStep >= trace.length) return []; - - const step = trace[currentStep]; - const instruction = pcToInstruction.get(step.pc); - if (!instruction?.debug?.context) return []; + // Instruction contexts are POSTCONDITIONS, so the semantic facts + // shown at the step about to execute instruction i come from + // instruction i-1 (bugc emits no program-level context, so the + // first step is empty). Pointer resolution still runs against the + // state observed at step i; only the context selection shifts. + const effectiveContext = useMemo( + () => + effectiveContextForStep({ + programContext: undefined, + contextAtPc: (pc) => pcToInstruction.get(pc)?.debug?.context, + trace, + stepIndex: currentStep, + }), + [pcToInstruction, trace, currentStep], + ); - return extractVariables(instruction.debug.context); - }, [trace, currentStep, pcToInstruction]); + // Extract variables from the effective (postcondition) context. + const currentVariables = useMemo(() => { + if (!effectiveContext) return []; + return extractVariables(effectiveContext); + }, [effectiveContext]); // Adapt the bugc instruction map + evm trace to the shared // programs-react call-stack helpers, which read the @@ -222,11 +234,14 @@ function TraceDrawerContent(): JSX.Element { return formatPcToInstruction.get(step.pc); }, [trace, currentStep, formatPcToInstruction]); - // Extract call info from current instruction context + // Extract call info from the effective (postcondition) context. const currentCallInfo = useMemo(() => { - if (!currentInstruction) return undefined; - return extractCallInfoFromInstruction(currentInstruction); - }, [currentInstruction]); + if (!effectiveContext) return undefined; + return extractCallInfoFromInstruction({ + offset: 0, + context: effectiveContext, + } as unknown as Program.Instruction); + }, [effectiveContext]); // Build the ethdebug/format instruction object for the current step const currentFormatInstruction = useMemo(() => {