From 59dd341fe16d26180c76aedf18ac2918dbe426b8 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Thu, 2 Apr 2026 05:21:13 -0400 Subject: [PATCH] Fix call stack display for recursive function calls The call stack dedup logic compared only function name and call type, causing recursive calls (e.g. count -> count) to be collapsed into a single frame. Now also checks whether the previous frame was pushed on the immediately preceding step, which distinguishes the compiler's duplicate invoke contexts (caller JUMP + callee JUMPDEST on consecutive steps) from genuine recursive calls (same name but steps far apart). Fixed in both programs-react buildCallStack and web TraceDrawer's duplicated call stack logic. --- .../programs-react/src/utils/mockTrace.ts | 20 +++++++++++-------- .../src/theme/ProgramExample/TraceDrawer.tsx | 18 ++++++++++------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/packages/programs-react/src/utils/mockTrace.ts b/packages/programs-react/src/utils/mockTrace.ts index 78af909c3..23d8483ed 100644 --- a/packages/programs-react/src/utils/mockTrace.ts +++ b/packages/programs-react/src/utils/mockTrace.ts @@ -298,15 +298,19 @@ export function buildCallStack( } if (callInfo.kind === "invoke") { - // The compiler emits invoke on both the caller JUMP and - // callee entry JUMPDEST. Skip if the top frame already - // matches this call. + // The compiler emits invoke on both the caller JUMP + // and callee entry JUMPDEST for the same call. These + // occur on consecutive trace steps. Only skip if the + // top frame matches AND was pushed on the immediately + // preceding step — otherwise this is a new call (e.g. + // recursion with the same function name). const top = stack[stack.length - 1]; - if ( - !top || - top.identifier !== callInfo.identifier || - top.callType !== callInfo.callType - ) { + const isDuplicate = + top && + top.identifier === callInfo.identifier && + top.callType === callInfo.callType && + top.stepIndex === i - 1; + if (!isDuplicate) { stack.push({ identifier: callInfo.identifier, stepIndex: i, diff --git a/packages/web/src/theme/ProgramExample/TraceDrawer.tsx b/packages/web/src/theme/ProgramExample/TraceDrawer.tsx index 088fa8d3f..99cdea4b8 100644 --- a/packages/web/src/theme/ProgramExample/TraceDrawer.tsx +++ b/packages/web/src/theme/ProgramExample/TraceDrawer.tsx @@ -118,14 +118,18 @@ function TraceDrawerContent(): JSX.Element { if (info.kind === "invoke") { // The compiler emits invoke on both the caller - // JUMP and callee entry JUMPDEST. Skip if the - // top frame already matches this call. + // JUMP and callee entry JUMPDEST for the same + // call. These occur on consecutive trace steps. + // Only skip if the top frame matches AND was + // pushed on the immediately preceding step — + // otherwise this is a new call (e.g. recursion). const top = frames[frames.length - 1]; - if ( - !top || - top.identifier !== info.identifier || - top.callType !== info.callType - ) { + const isDuplicate = + top && + top.identifier === info.identifier && + top.callType === info.callType && + top.stepIndex === i - 1; + if (!isDuplicate) { frames.push({ identifier: info.identifier, stepIndex: i,