Skip to content
Open
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
123 changes: 123 additions & 0 deletions packages/programs-react/src/components/TraceContext.test.tsx
Original file line number Diff line number Diff line change
@@ -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 }) => (
<TraceProvider
trace={trace}
program={program}
templates={templates}
resolveVariables={false}
>
{children}
</TraceProvider>
),
});
}

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 }) => (
<TraceProvider
trace={callTrace}
program={callProgram}
templates={templates}
resolveVariables={false}
>
{children}
</TraceProvider>
),
});
}

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");
});
});
39 changes: 33 additions & 6 deletions packages/programs-react/src/components/TraceContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<Program.Instruction | undefined>(
() =>
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<ResolvedVariable[]>(
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions packages/programs-react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ export {
type FindSourceRangeOptions,
type ResolverOptions,
traceStepToMachineState,
effectiveContextForStep,
type EffectiveContextInput,
type TraceStep,
type MockTraceSpec,
} from "#utils/index";
Expand Down
79 changes: 79 additions & 0 deletions packages/programs-react/src/utils/effectiveContext.test.ts
Original file line number Diff line number Diff line change
@@ -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<number, Program.Context>([
[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();
});
});
60 changes: 60 additions & 0 deletions packages/programs-react/src/utils/effectiveContext.ts
Original file line number Diff line number Diff line change
@@ -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);
}
5 changes: 5 additions & 0 deletions packages/programs-react/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,8 @@ export {
} from "./mockTrace.js";

export { traceStepToMachineState } from "./traceState.js";

export {
effectiveContextForStep,
type EffectiveContextInput,
} from "./effectiveContext.js";
Loading
Loading