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
15 changes: 15 additions & 0 deletions packages/programs-react/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ root [`CHANGELOG.md`](../../CHANGELOG.md).

## Unreleased

### Fixed

- `extractVariablesFromInstruction` now reads every `variables` list in a
context: its own and those of each context in its `gather`. Before, a
context with its own `variables` hid those in its `gather`, so at
bugc's inlined code the storage variables disappeared. Of a `pick`,
only one of whose contexts holds, it now lists only the variables that
all of its contexts list alike (the same identifier, declaration and
pointer), without a type they differ on; before, it took the first
context with variables. Entries
for one variable (the same identifier and declaration, in the same
`frame`) compose into one; an entry without a declaration stays apart
([#342]).

## 0.1.0-preview.1 — 2026-10-03

Updated `@ethdebug/format` to `0.1.0-draft.1` and `@ethdebug/pointers` to
Expand Down Expand Up @@ -38,3 +52,4 @@ First publication.
[#298]: https://github.com/ethdebug/format/pull/298
[#299]: https://github.com/ethdebug/format/pull/299
[#300]: https://github.com/ethdebug/format/pull/300
[#342]: https://github.com/ethdebug/format/pull/342
153 changes: 153 additions & 0 deletions packages/programs-react/src/utils/mockTrace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
extractTransformFromInstruction,
extractCallInfoFromInstruction,
extractCallEvents,
extractVariablesFromInstruction,
buildCallStack,
buildPcToInstructionMap,
type TraceStep,
Expand All @@ -32,6 +33,158 @@ function instr(offset: number, context: unknown): Program.Instruction {
} as unknown as Program.Instruction;
}

describe("extractVariablesFromInstruction", () => {
const total = { identifier: "total", pointer: { location: "storage" } };
const x = { identifier: "x", pointer: { location: "memory" } };
const names = (context: unknown) =>
extractVariablesFromInstruction(instr(0, context)).map((v) => v.identifier);

it("reads a flat variables list", () => {
expect(names({ variables: [total, x] })).toEqual(["total", "x"]);
});

it("reads variables from every gathered context", () => {
expect(
names({
gather: [
{ code: {}, variables: [total] },
{ code: {}, variables: [x] },
],
}),
).toEqual(["total", "x"]);
});

it("reads variables beside a gather and within it", () => {
// Inlined code: the callee's local beside a gather of the call
// site's and the callee's source ranges; all of them apply
expect(
names({
variables: [x],
gather: [{ code: {} }, { code: {}, variables: [total] }],
}),
).toEqual(["x", "total"]);
});

it("composes two entries for one variable", () => {
const declaration = { source: { id: 0 }, range: { offset: 1 } };
const typed = { identifier: "x", declaration, type: { kind: "uint" } };
const located = { identifier: "x", declaration, pointer: x.pointer };
expect(
extractVariablesFromInstruction(
instr(0, {
gather: [{ variables: [typed] }, { variables: [located] }],
}),
),
).toEqual([{ ...typed, ...located }]);
});

const declared = (identifier: string, offset: number, pointer?: unknown) => ({
identifier,
declaration: { source: { id: 0 }, range: { offset, length: 1 } },
...(pointer ? { pointer } : {}),
});
const memory = (offset: number) => ({ location: "memory", offset });

it("lists only what every context of a pick agrees on", () => {
const a = declared("a", 1, memory(0));
const b = declared("b", 2, memory(32));
expect(
extractVariablesFromInstruction(
instr(0, { pick: [{ variables: [a, b] }, { variables: [b] }] }),
),
).toEqual([b]);
});

it("lists nothing of a pick whose contexts disagree on a pointer", () => {
expect(
names({
pick: [
{ variables: [declared("a", 1, memory(0))] },
{ variables: [declared("a", 1, memory(32))] },
],
}),
).toEqual([]);
});

it("drops a type the contexts of a pick differ on", () => {
const pointer = { location: "stack", slot: 0 };
const x = (type: unknown) => ({ ...declared("x", 1, pointer), type });
expect(
extractVariablesFromInstruction(
instr(0, {
pick: [
{ variables: [x({ kind: "uint", bits: 8 })] },
{ variables: [x({ kind: "int", bits: 8 })] },
],
}),
),
).toEqual([declared("x", 1, pointer)]);
});

it("reads a pick within a gather, and gathers within a pick", () => {
const a = declared("a", 1, memory(0));
const b = declared("b", 2, memory(32));
const c = declared("c", 3, memory(64));
expect(
names({
gather: [
{ variables: [c] },
{
pick: [
{ gather: [{ variables: [a] }, { variables: [b] }] },
{ variables: [b] },
],
},
],
}),
).toEqual(["c", "b"]);
});

it("keeps entries without a declaration apart", () => {
expect(
extractVariablesFromInstruction(
instr(0, {
gather: [
{ variables: [{ identifier: "x", type: { kind: "uint" } }] },
{ variables: [{ identifier: "x", pointer: x.pointer }] },
],
}),
),
).toHaveLength(2);
});

it("keeps entries from different frames apart", () => {
const typed = { ...declared("x", 1), type: { kind: "uint" } };
const located = declared("x", 1, memory(0));
expect(
extractVariablesFromInstruction(
instr(0, {
gather: [
{ frame: "source", variables: [typed] },
{ frame: "ir", variables: [located] },
],
}),
),
).toEqual([typed, located]);
});

it("matches declarations whatever the order of their keys", () => {
const typed = { ...declared("x", 1), type: { kind: "uint" } };
const located = {
identifier: "x",
declaration: { range: { length: 1, offset: 1 }, source: { id: 0 } },
pointer: memory(0),
};
expect(
extractVariablesFromInstruction(
instr(0, {
gather: [{ variables: [typed] }, { variables: [located] }],
}),
),
).toHaveLength(1);
});
});

describe("extractTransformFromInstruction", () => {
it("returns identifiers from a direct transform context", () => {
const i = instr(0, { transform: ["tailcall"] });
Expand Down
133 changes: 110 additions & 23 deletions packages/programs-react/src/utils/mockTrace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,45 +60,132 @@ export function findInstructionAtPc(
return program.instructions?.find((instr) => instr.offset === pc);
}

/**
* A variable entry, as a `variables` context lists it.
*/
type VariableEntry = {
identifier?: string;
declaration?: unknown;
type?: unknown;
pointer?: unknown;
};

/** A variable entry, with the `frame` of the context that lists it */
interface Listed {
entry: VariableEntry;
frame?: string;
}

/**
* Extract variables that are in scope at a given instruction.
*
* This walks the context and extracts variables from Variables contexts.
* All of a context's keys apply, and so do all of a `gather`'s
* contexts, so this reads every `variables` list: the context's own
* and each gathered context's. Only one of a `pick`'s contexts holds,
* so of a pick it reads only the variables that every one of its
* contexts lists alike (the same identifier, declaration and
* pointer), without a type they differ on. Entries for one variable (the same identifier and
* declaration, in the same frame) compose into one; an entry without
* a declaration stays apart, as does an entry from another frame.
*/
export function extractVariablesFromInstruction(
instruction: Program.Instruction,
): Array<{ identifier?: string; type?: unknown; pointer?: unknown }> {
): VariableEntry[] {
if (!instruction.context) {
return [];
}

return extractVariablesFromContext(instruction.context);
const joined: Listed[] = [];
for (const listed of listedIn(instruction.context)) {
const same = joined.find((other) => sameVariable(other, listed));
if (same) {
same.entry = { ...same.entry, ...listed.entry };
} else {
joined.push({ ...listed });
}
}
return joined.map(({ entry }) => entry);
}

function extractVariablesFromContext(
context: Program.Context,
): Array<{ identifier?: string; type?: unknown; pointer?: unknown }> {
// Variables context
if ("variables" in context && Array.isArray(context.variables)) {
return context.variables;
function listedIn(context: Program.Context, frame?: string): Listed[] {
const ctx = context as unknown as Record<string, unknown>;
const here = typeof ctx.frame === "string" ? ctx.frame : frame;

const own = Array.isArray(ctx.variables)
? (ctx.variables as VariableEntry[]).map((entry) => ({
entry,
frame: here,
}))
: [];

// Gather context (all of its contexts apply)
const gathered = Array.isArray(ctx.gather)
? (ctx.gather as Program.Context[]).flatMap((c) => listedIn(c, here))
: [];

// Pick context (one of its contexts applies): only what all agree on
const picked: Listed[] = [];
if (Array.isArray(ctx.pick) && ctx.pick.length > 0) {
const [first, ...others] = (ctx.pick as Program.Context[]).map((c) =>
listedIn(c, here),
);
for (const listed of first) {
const matches = others.map((branch) =>
branch.find(
(other) =>
sameVariable(listed, other) &&
same(listed.entry.pointer, other.entry.pointer),
),
);
if (matches.some((match) => match === undefined)) continue;
// A type the contexts differ on holds in only some of them
const { type, ...rest } = listed.entry;
const typed = matches.every((match) => same(type, match!.entry.type));
picked.push({ ...listed, entry: typed ? listed.entry : rest });
}
}

// Gather context (combines multiple contexts)
if ("gather" in context && Array.isArray(context.gather)) {
return context.gather.flatMap(extractVariablesFromContext);
}
return [...own, ...gathered, ...picked];
}

// Pick context (picks from multiple contexts - take first with variables)
if ("pick" in context && Array.isArray(context.pick)) {
for (const subContext of context.pick) {
const vars = extractVariablesFromContext(subContext);
if (vars.length > 0) {
return vars;
}
}
}
/** Whether two entries are for one variable: the same identifier and
* declaration (both given), in the same frame */
function sameVariable(a: Listed, b: Listed): boolean {
const x = a.entry.declaration as Declaration | undefined;
const y = b.entry.declaration as Declaration | undefined;
return (
a.frame === b.frame &&
a.entry.identifier === b.entry.identifier &&
x !== undefined &&
y !== undefined &&
x.source?.id === y.source?.id &&
x.range?.offset === y.range?.offset &&
x.range?.length === y.range?.length
);
}

return [];
type Declaration = {
source?: { id?: unknown };
range?: { offset?: unknown; length?: unknown };
};

/** Structural equality, whatever the order of keys */
function same(a: unknown, b: unknown): boolean {
if (a === b) return true;
if (typeof a !== "object" || typeof b !== "object" || !a || !b) {
return false;
}
if (Array.isArray(a) !== Array.isArray(b)) return false;
const keys = Object.keys(a);
return (
keys.length === Object.keys(b).length &&
keys.every((key) =>
same(
(a as Record<string, unknown>)[key],
(b as Record<string, unknown>)[key],
),
)
);
}

/**
Expand Down
Loading