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(() => {