Skip to content

Commit 20a7654

Browse files
authored
programs-react: adopt locked inline-activation contract (#237)
Per architect's rulings on #23, replace the pragmatic invoke-wins reconstruction with the locked contract: - extractCallEvents: expose ALL call events (ordered invoke-before- return) so a co-located invoke+return context is never swallowed as invoke-only. extractCallInfoFromInstruction now returns the first event (display banner unchanged). Parsing split into parseInvoke/parseReturn/parseRevert + collectCallInfos. - buildCallStack close-after semantics: an invoke opens a frame inclusive of its instruction; a return closes it AFTER (deferred until the viewed step advances past it), so a frame is visible while parked on its return-bearing instruction — uniformly for real and virtual frames, and correct for a single-instruction inlined body. - marker-keyed dedup: the caller-JUMP/callee-JUMPDEST collapse now also requires matching inline-ness, so a virtual invoke never merges with an adjacent real invoke of the same name. - generalized membership guard: force-pop trailing virtual frames whose depth exceeds the instruction's inline-marker count (handles nested inlining and residual smear). Tests: close-after lifetime, single-op body, adjacent-site split via the return, marker-keyed dedup, nested (double-marker) inlining, and a real-call close-after regression. 59 programs-react tests green. Verified end-to-end on real O2 (dbl@2 sites): one virtual frame per body, top level between/after, no phantom — holds even pre-de-smear.
1 parent 1e90935 commit 20a7654

2 files changed

Lines changed: 368 additions & 154 deletions

File tree

‎packages/programs-react/src/utils/mockTrace.test.ts‎

Lines changed: 197 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type { Program } from "@ethdebug/format";
88
import {
99
extractTransformFromInstruction,
1010
extractCallInfoFromInstruction,
11+
extractCallEvents,
1112
buildCallStack,
1213
buildPcToInstructionMap,
1314
type TraceStep,
@@ -194,10 +195,13 @@ describe("flat (production) TCO back-edge shape", () => {
194195
// inlined body with a virtual invoke on the entry-first
195196
// instruction and a virtual return on the exit-last instruction;
196197
// every inlined instruction carries transform:["inline"]. The
197-
// call stack must reconstruct the virtual frame, tag it, and
198-
// tear it down when execution leaves the inlined body — so it
199-
// reads distinctly from a real call and never leaks a phantom
200-
// frame into caller code.
198+
// call stack reconstructs the virtual frame via close-after
199+
// push/pop (a frame is visible AT its return-bearing instruction
200+
// and popped on advance), tags it, and — belt-and-suspenders —
201+
// tears down any trailing virtual frame the moment execution
202+
// reaches an instruction whose inline-marker count is below the
203+
// open virtual depth. So it reads distinctly from a real call and
204+
// never leaks a phantom frame into caller code.
201205
describe("inline virtual activations", () => {
202206
const entryInvoke = {
203207
code: { source: { id: "0" }, range: { offset: 0, length: 1 } },
@@ -216,6 +220,14 @@ describe("inline virtual activations", () => {
216220
const callerMark = {
217221
code: { source: { id: "0" }, range: { offset: 3, length: 1 } },
218222
};
223+
// A body that emits to a single EVM op: invoke and return
224+
// co-locate on one instruction (the degenerate bracketed case).
225+
const singleOpBody = {
226+
code: { source: { id: "0" }, range: { offset: 0, length: 1 } },
227+
transform: ["inline"],
228+
invoke: { jump: true, identifier: "dbl" },
229+
return: { identifier: "dbl" },
230+
};
219231

220232
describe("extractCallInfoFromInstruction inline flag", () => {
221233
it("marks isInline on a virtual (inline) invoke", () => {
@@ -238,13 +250,31 @@ describe("inline virtual activations", () => {
238250
});
239251
});
240252

241-
describe("buildCallStack virtual frame lifetime", () => {
253+
describe("extractCallEvents exposes both discriminators", () => {
254+
it("returns invoke then return, in order, for a co-located context", () => {
255+
const events = extractCallEvents(instr(0, singleOpBody));
256+
expect(events.map((e) => e.kind)).toEqual(["invoke", "return"]);
257+
expect(events.every((e) => e.isInline)).toBe(true);
258+
});
259+
260+
it("returns a single invoke event for a pure invoke", () => {
261+
const events = extractCallEvents(instr(0, entryInvoke));
262+
expect(events.map((e) => e.kind)).toEqual(["invoke"]);
263+
});
264+
265+
it("returns a single return event for a pure return", () => {
266+
const events = extractCallEvents(instr(0, exitReturn));
267+
expect(events.map((e) => e.kind)).toEqual(["return"]);
268+
});
269+
});
270+
271+
describe("buildCallStack virtual frame lifetime (close-after)", () => {
242272
// A single inlined body: entry / body / exit / caller.
243273
const trace: TraceStep[] = [
244274
{ pc: 0, opcode: "PUSH1" }, // entry invoke → push virtual dbl
245275
{ pc: 1, opcode: "ADD" }, // inlined body instruction
246-
{ pc: 2, opcode: "MSTORE" }, // exit return → pop
247-
{ pc: 3, opcode: "JUMPDEST" }, // caller code (no inline marker)
276+
{ pc: 2, opcode: "MSTORE" }, // exit return (still inside frame)
277+
{ pc: 3, opcode: "JUMPDEST" }, // caller code (frame gone)
248278
];
249279
const program = {
250280
instructions: [
@@ -269,17 +299,39 @@ describe("inline virtual activations", () => {
269299
expect(stack[0].isInline).toBe(true);
270300
});
271301

272-
it("pops the virtual frame at the exit return", () => {
302+
it("still shows the frame AT the exit return (close-after)", () => {
273303
const stack = buildCallStack(trace, pcToInstruction, 2);
274-
expect(stack).toHaveLength(0);
304+
expect(stack).toHaveLength(1);
305+
expect(stack[0].isInline).toBe(true);
275306
});
276307

277-
it("does not leak a phantom frame into caller code", () => {
308+
it("pops the frame once execution advances past the return", () => {
278309
const stack = buildCallStack(trace, pcToInstruction, 3);
279310
expect(stack).toHaveLength(0);
280311
});
281312
});
282313

314+
describe("single-op inlined body (co-located invoke+return)", () => {
315+
const trace: TraceStep[] = [
316+
{ pc: 0, opcode: "PUSH1" }, // the whole body: invoke+return
317+
{ pc: 1, opcode: "JUMPDEST" }, // caller code
318+
];
319+
const program = {
320+
instructions: [instr(0, singleOpBody), instr(1, callerMark)],
321+
} as unknown as Program;
322+
const pcToInstruction = buildPcToInstructionMap(program);
323+
324+
it("shows the virtual frame AT the single body op", () => {
325+
const stack = buildCallStack(trace, pcToInstruction, 0);
326+
expect(stack).toHaveLength(1);
327+
expect(stack[0].isInline).toBe(true);
328+
});
329+
330+
it("pops after advancing off the body op", () => {
331+
expect(buildCallStack(trace, pcToInstruction, 1)).toHaveLength(0);
332+
});
333+
});
334+
283335
describe("two gap-separated inline sites of the same helper", () => {
284336
const trace: TraceStep[] = [
285337
{ pc: 0, opcode: "PUSH1" }, // site 1 entry
@@ -305,6 +357,7 @@ describe("inline virtual activations", () => {
305357
const stack = buildCallStack(trace, pcToInstruction, 3);
306358
expect(stack).toHaveLength(1);
307359
expect(stack[0].isInline).toBe(true);
360+
expect(stack[0].stepIndex).toBe(3);
308361
});
309362

310363
it("is empty after both sites — no accumulation", () => {
@@ -313,6 +366,110 @@ describe("inline virtual activations", () => {
313366
});
314367
});
315368

369+
describe("two ADJACENT inline sites split by the return", () => {
370+
// No caller gap between sites: the return marker (not the
371+
// membership guard, which can't see a boundary between two
372+
// inline-marked instructions) is what closes site 1 before
373+
// site 2 opens.
374+
const trace: TraceStep[] = [
375+
{ pc: 0, opcode: "PUSH1" }, // site 1 entry
376+
{ pc: 1, opcode: "MSTORE" }, // site 1 exit
377+
{ pc: 2, opcode: "PUSH1" }, // site 2 entry (immediately)
378+
{ pc: 3, opcode: "MSTORE" }, // site 2 exit
379+
{ pc: 5, opcode: "JUMPDEST" }, // caller
380+
];
381+
const program = {
382+
instructions: [
383+
instr(0, entryInvoke),
384+
instr(1, exitReturn),
385+
instr(2, entryInvoke),
386+
instr(3, exitReturn),
387+
instr(5, callerMark),
388+
],
389+
} as unknown as Program;
390+
const pcToInstruction = buildPcToInstructionMap(program);
391+
392+
it("does not merge or accumulate — one frame, rooted at site 2", () => {
393+
const stack = buildCallStack(trace, pcToInstruction, 2);
394+
expect(stack).toHaveLength(1);
395+
expect(stack[0].stepIndex).toBe(2);
396+
});
397+
398+
it("is empty after both sites", () => {
399+
expect(buildCallStack(trace, pcToInstruction, 4)).toHaveLength(0);
400+
});
401+
});
402+
403+
describe("marker-keyed dedup", () => {
404+
// A real call and an inlined body of the SAME name on
405+
// consecutive steps must NOT be merged by the caller-JUMP /
406+
// callee-JUMPDEST dedup — they are distinct activations.
407+
const realInvoke = { invoke: { jump: true, identifier: "dbl" } };
408+
const trace: TraceStep[] = [
409+
{ pc: 0, opcode: "JUMP" }, // real invoke of dbl
410+
{ pc: 1, opcode: "PUSH1" }, // virtual (inline) invoke of dbl
411+
];
412+
const program = {
413+
instructions: [instr(0, realInvoke), instr(1, entryInvoke)],
414+
} as unknown as Program;
415+
const pcToInstruction = buildPcToInstructionMap(program);
416+
417+
it("keeps a real and a virtual dbl as two separate frames", () => {
418+
const stack = buildCallStack(trace, pcToInstruction, 1);
419+
expect(stack).toHaveLength(2);
420+
expect(stack[0].isInline).toBeFalsy();
421+
expect(stack[1].isInline).toBe(true);
422+
});
423+
});
424+
425+
describe("nested inlining (double inline marker)", () => {
426+
// Helper A inlined into helper B which is itself inlined:
427+
// A's body instructions are members of both bodies and carry
428+
// transform:["inline","inline"]. Two virtual frames stack; the
429+
// inner returns first, leaving the outer.
430+
const entryB = {
431+
transform: ["inline"],
432+
invoke: { jump: true, identifier: "B" },
433+
};
434+
const entryA = {
435+
transform: ["inline", "inline"],
436+
invoke: { jump: true, identifier: "A" },
437+
};
438+
const exitA = {
439+
transform: ["inline", "inline"],
440+
return: { identifier: "A" },
441+
};
442+
const bodyB = { transform: ["inline"] }; // back to just B's body
443+
const trace: TraceStep[] = [
444+
{ pc: 0, opcode: "PUSH1" }, // enter B
445+
{ pc: 1, opcode: "PUSH1" }, // enter A (inside B)
446+
{ pc: 2, opcode: "MSTORE" }, // exit A
447+
{ pc: 3, opcode: "ADD" }, // back in B only
448+
];
449+
const program = {
450+
instructions: [
451+
instr(0, entryB),
452+
instr(1, entryA),
453+
instr(2, exitA),
454+
instr(3, bodyB),
455+
],
456+
} as unknown as Program;
457+
const pcToInstruction = buildPcToInstructionMap(program);
458+
459+
it("stacks two virtual frames inside the inner body", () => {
460+
const stack = buildCallStack(trace, pcToInstruction, 1);
461+
expect(stack).toHaveLength(2);
462+
expect(stack[0].identifier).toBe("B");
463+
expect(stack[1].identifier).toBe("A");
464+
});
465+
466+
it("drops to the outer frame after the inner returns", () => {
467+
const stack = buildCallStack(trace, pcToInstruction, 3);
468+
expect(stack).toHaveLength(1);
469+
expect(stack[0].identifier).toBe("B");
470+
});
471+
});
472+
316473
describe("defensive membership guard", () => {
317474
// A virtual invoke whose exit return never arrives (residual
318475
// smear / dropped marker): the frame must still be torn down
@@ -341,13 +498,38 @@ describe("inline virtual activations", () => {
341498
});
342499
});
343500

344-
it("leaves a real call frame's isInline falsy", () => {
345-
const trace: TraceStep[] = [{ pc: 0, opcode: "JUMPDEST" }];
501+
describe("real calls (regression: close-after applies uniformly)", () => {
502+
// A real call: caller JUMP + callee JUMPDEST (deduped), then a
503+
// return. The frame is visible at its return step and popped on
504+
// advance — same close-after rule as virtual frames.
505+
const trace: TraceStep[] = [
506+
{ pc: 0, opcode: "JUMP" }, // caller invoke
507+
{ pc: 1, opcode: "JUMPDEST" }, // callee entry invoke (dedup)
508+
{ pc: 2, opcode: "JUMP" }, // callee return
509+
{ pc: 3, opcode: "JUMPDEST" }, // back in caller
510+
];
346511
const program = {
347-
instructions: [instr(0, { invoke: { jump: true, identifier: "f" } })],
512+
instructions: [
513+
instr(0, { invoke: { jump: true, identifier: "f" } }),
514+
instr(1, { invoke: { jump: true, identifier: "f" } }),
515+
instr(2, { return: { identifier: "f" } }),
516+
instr(3, { code: { source: { id: "0" }, range: {} } }),
517+
],
348518
} as unknown as Program;
349519
const pcToInstruction = buildPcToInstructionMap(program);
350-
const stack = buildCallStack(trace, pcToInstruction, 0);
351-
expect(stack[0].isInline).toBeFalsy();
520+
521+
it("collapses the caller/callee invoke double into one frame", () => {
522+
expect(buildCallStack(trace, pcToInstruction, 1)).toHaveLength(1);
523+
});
524+
525+
it("still shows the frame AT its return instruction", () => {
526+
const stack = buildCallStack(trace, pcToInstruction, 2);
527+
expect(stack).toHaveLength(1);
528+
expect(stack[0].isInline).toBeFalsy();
529+
});
530+
531+
it("pops the real frame on advancing past the return", () => {
532+
expect(buildCallStack(trace, pcToInstruction, 3)).toHaveLength(0);
533+
});
352534
});
353535
});

0 commit comments

Comments
 (0)