Source: src/hql/transpiler/optimize/tco-optimizer.ts, src/hql/transpiler/optimize/mutual-tco-optimizer.ts, src/hql/transpiler/optimize/tail-position-analyzer.ts
TCO applies to named fn declarations with self-recursive or mutually recursive tail calls:
fn-decl ::= '(' 'fn' name params body ')'
fn*-decl ::= '(' 'fn*' name params body ')'
tail-call ::= recursive call to 'name' in tail position
mutual-call ::= tail call to a function in the same SCC group
An expression is in tail position if:
return statementif/ternary) that is in tail position&&, ||) that is in tail positiontreatYieldDelegateAsTail: the argument of yield* in tail positiontry/catch/finallywhile, for, for-of loopsSelf-recursive TCO is applied per-function in generateFnFunctionDeclaration. It uses checkTailRecursion() from the shared tail-position analyzer to verify:
If both conditions hold, the function is transformed.
Input pattern:
(fn name [p1 p2 ... pn]
(if base-test
base-value
(name arg1 arg2 ... argn)))
Output pattern:
function name(p1, p2, ..., pn) {
while (true) {
if (base-test) return base-value;
[p1, p2, ..., pn] = [arg1, arg2, ..., argn];
}
}
The transformation handles:
ReturnStatement with direct recursive call -> destructuring parameter reassignmentReturnStatement with ConditionalExpression -> converted to IfStatement with branches transformedIfStatement -> both branches recursively transformedBlockStatement -> all statements recursively transformed(fn countdown [n]
(if (<= n 0)
"done"
(countdown (- n 1))))
Compiles to:
function countdown(n) {
while (true) {
if (n <= 0)
return 'done';
else
[n] = [n - 1];
}
}
(fn factorial [n acc]
(if (<= n 1)
acc
(factorial (- n 1) (* n acc))))
Compiles to:
function factorial(n, acc) {
while (true) {
if (n <= 1)
return acc;
else
[n, acc] = [n - 1, n * acc];
}
}
(fn collatz [n steps]
(if (=== n 1)
steps
(if (=== (% n 2) 0)
(collatz (/ n 2) (+ steps 1))
(collatz (+ (* n 3) 1) (+ steps 1)))))
Compiles to:
function collatz(n, steps) {
while (true) {
if (n === 1)
return steps;
else if (n % 2 === 0)
[n, steps] = [n / 2, steps + 1];
else
[n, steps] = [n * 3 + 1, steps + 1];
}
}
Mutual recursion is handled by a separate optimizer (mutual-tco-optimizer.ts) applied at module level before individual function code generation.
fn declarations (async functions are skipped)findTailCallsToFunctions(). For generators, self-calls are included. For sync functions, self-calls are excluded (handled by while-loop TCO).return () => otherFn(args) (thunk)yield* tail calls are replaced with return { [Symbol.for("__hql_gen_thunk")]: true, next: () => otherFn(args) } (tagged thunk)__hql_trampoline(() => fn(args)) for sync or __hql_trampoline_gen(() => fn(args)) for generators.The mutual TCO transformer also handles:
SequenceExpression (from do blocks): transforms the last expression(() => { ... return tailCall(); })()): transforms the IIFE bodyConditionalExpression branches: recursively transforms both branches(fn is-even [n]
(if (=== n 0) true (is-odd (- n 1))))
(fn is-odd [n]
(if (=== n 0) false (is-even (- n 1))))
Transforms to (conceptually):
function is_even(n) {
if (n === 0) return true;
return () => is_odd(n - 1); // thunk
}
function is_odd(n) {
if (n === 0) return false;
return () => is_even(n - 1); // thunk
}
// Call site: __hql_trampoline(() => is_even(10000))
(fn* gen-a [n] (if (=== n 0) "done" (yield* (gen-b (- n 1)))))
(fn* gen-b [n] (yield* (gen-a n)))
Transforms to (conceptually):
function* gen_a(n) {
if (n === 0) return "done";
return { [Symbol.for("__hql_gen_thunk")]: true, next: () => gen_b(n - 1) };
}
function* gen_b(n) {
return { [Symbol.for("__hql_gen_thunk")]: true, next: () => gen_a(n) };
}
// Call site: __hql_trampoline_gen(() => gen_a(10000))
__hql_trampoline(thunk)Executes a thunk and keeps calling the result while it remains a function:
function __hql_trampoline(thunk) {
let result = thunk();
while (typeof result === "function") {
result = result();
}
return result;
}
__hql_trampoline_gen(createInitial)Generator trampoline that handles tagged-thunk objects with Symbol.for("__hql_gen_thunk").
await naturally breaks the call stack, no TCO needed(fn factorial [n]
(if (<= n 1)
1
(* n (factorial (- n 1))))) // Wrapped in multiplication
Remains as:
function factorial(n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
(fn add [a b]
(+ a b))
No transformation applied - no recursion detected.
Recursive calls inside try/catch/finally blocks are not in tail position per JavaScript semantics.
src/hql/transpiler/optimize/tco-optimizer.tssrc/hql/transpiler/optimize/mutual-tco-optimizer.tssrc/hql/transpiler/optimize/tail-position-analyzer.tssrc/common/runtime-helper-impl.tssrc/hql/transpiler/pipeline/ir-to-typescript.tstests/unit/tco.test.ts