@@ -8,6 +8,7 @@ import type { Program } from "@ethdebug/format";
88import {
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.
201205describe ( "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