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
74 changes: 73 additions & 1 deletion packages/bugc/src/evmgen/generation/control-flow/terminator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,23 @@ export function generateTerminator<S extends Stack>(
}

case "jump": {
// When this jump replaces a tail-recursive call (TCO),
// attach a gather context to the JUMP combining the
// previous iteration's return and the new iteration's
// invoke. Depth stays constant: one pops, one pushes,
// on the same instruction. The function's terminal
// RETURN pops the final iteration's frame normally.
const invokeOptions = term.tailCall
? buildTailCallJumpOptions(term.tailCall)
: undefined;

return pipe<S>()
.peek((state, builder) => {
const patchIndex = state.instructions.length;

return builder
.then(PUSH2([0, 0]), { as: "counter" })
.then(JUMP())
.then(JUMP(invokeOptions))
.then((newState) => ({
...newState,
patches: [
Expand Down Expand Up @@ -398,6 +408,68 @@ function generateReturnEpilogue<S extends Stack>(
}) as Transition<S, Stack>;
}

/**
* Build JUMP instruction options for a TCO-replaced tail call.
*
* The JUMP carries BOTH contexts in a gather:
* - return: the previous iteration's return
* - invoke: the new iteration's call
*
* Semantically the debugger sees frame depth stay constant
* across the back-edge JUMP: the previous frame pops, the
* new one pushes, on the same instruction. The function's
* terminal RETURN (elsewhere) emits a return context
* normally, popping the final iteration's frame.
*
* The invoke mirrors the normal caller-JUMP invoke
* (identity + declaration + code target, no argument
* pointers). The return omits `data` because TCO does not
* materialize the intermediate return value — the actual
* return happens later at the function's terminal RETURN.
*
* The invoke target uses placeholder offset 0 and is
* resolved later by patchInvokeTarget.
*/
function buildTailCallJumpOptions(tailCall: Ir.Block.TailCall): {
debug: { context: Format.Program.Context };
} {
const declaration =
tailCall.declarationLoc && tailCall.declarationSourceId
? {
source: { id: tailCall.declarationSourceId },
range: tailCall.declarationLoc,
}
: undefined;

const returnCtx: Format.Program.Context.Return = {
return: {
identifier: tailCall.function,
...(declaration ? { declaration } : {}),
},
};

const invoke: Format.Program.Context.Invoke = {
invoke: {
jump: true as const,
identifier: tailCall.function,
...(declaration ? { declaration } : {}),
target: {
pointer: {
location: "code" as const,
offset: 0,
length: 1,
},
},
},
};

const gather: Format.Program.Context.Gather = {
gather: [returnCtx, invoke],
};

return { debug: { context: gather as Format.Program.Context } };
}

/** PUSH an integer as the smallest PUSHn. */
function pushImm(value: number, debug: Ir.Block.Debug): Evm.Instruction[] {
if (value === 0) {
Expand Down
17 changes: 16 additions & 1 deletion packages/bugc/src/evmgen/generation/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -501,14 +501,29 @@ export function patchFunctionCalls(
* Resolve placeholder code pointer offsets in invoke debug
* contexts. The codegen emits `{ location: "code", offset: 0 }`
* as a placeholder; this replaces offset with the actual
* function entry address from the registry.
* function entry address from the registry. Walks into
* gather contexts so TCO back-edge JUMPs (which pair an
* invoke with a return) are patched too.
*/
function patchInvokeTarget(
inst: Evm.Instruction,
functionRegistry: Record<string, number>,
): void {
const ctx = inst.debug?.context;
if (!ctx) return;
patchInvokeInContext(ctx, functionRegistry);
}

function patchInvokeInContext(
ctx: Format.Program.Context,
functionRegistry: Record<string, number>,
): void {
if (Format.Program.Context.isGather(ctx)) {
for (const sub of ctx.gather) {
patchInvokeInContext(sub, functionRegistry);
}
return;
}

if (!Format.Program.Context.isInvoke(ctx)) return;

Expand Down
Loading
Loading