Tail Call Optimization¶
Current Status¶
Detection: ✅ Implemented Transformation: ✅ Implemented
The Onion compiler detects tail-recursive methods during compilation and rewrites
them into a while(true) loop, so a self-recursive call in tail position no longer
grows the JVM call stack (preventing StackOverflowError for deep recursion, e.g.
10,000+ calls).
Only direct self-recursion is rewritten by this phase; mutual recursion between
two or more methods is handled separately by MutualRecursionOptimization, which
runs immediately afterward in the pipeline.
How It Works¶
Detection Phase¶
The compiler analyzes each method to identify tail-recursive calls:
- Tail Position Analysis: Checks if the last statement (or statements in control-flow branches) is a self-call
- Recursive Search: Searches through
StatementBlockandIfStatementnodes to find tail calls - Method Matching: Verifies that the call target matches the current method (same name, class, and parameter types)
- Dispatch Safety: Only methods that cannot be overridden — private, static, or final — are eligible. A self-call in a public overridable method could be dynamically dispatched to a subclass override at runtime, so rewriting its body into a loop would silently change behavior; such methods are skipped.
- Mutual-Recursion Exclusion: A method annotated
@TailRecursive(used to mark participants in mutual recursion) is skipped here and left forMutualRecursionOptimization.
Supported Patterns¶
The detector recognizes tail recursion in:
- Direct tail calls:
return method(args) - Conditional branches: both
thenandelsebranches ofifstatements - Nested blocks: recursive search through statement blocks
Example¶
def factorial(n: Int, acc: Int): Int {
if (n <= 1) {
return acc
}
return factorial(n - 1, n * acc) // ✅ detected and transformed
}
Transformation Phase¶
Once a method is confirmed tail-recursive and eligible, it is rewritten:
- Loop Variable Allocation: Each parameter gets a corresponding loop variable
- Parameter Rewriting: Every reference to a parameter within the body is rewritten to reference its loop variable instead
- Loop Construction: The (rewritten) body is wrapped in a
while(true)loop - Tail Call Replacement: Each tail call is replaced with loop-variable updates
followed by the loop continuing (no
continuekeyword needed — falling through to the end of thewhile(true)body re-enters it)
// Before
def factorial(n: Int, acc: Int): Int {
if (n <= 1) return acc
return factorial(n - 1, n * acc)
}
// After (conceptually)
def factorial(n: Int, acc: Int): Int {
while (true) {
if (n <= 1) return acc
val n_next = n - 1
val acc_next = n * acc
n = n_next
acc = acc_next
// loop restarts
}
}
Viewing the Optimization¶
Compile with the --verbose flag to trace which methods are transformed and why
others are skipped:
Sample output:
[TCO] Method YourClass.factorial: hasTailCall=true
[TCO] Optimizing tail-recursive method: YourClass.factorial
[TCO] Skipping overridable method: YourClass.someOverridableMethod
[TCO] Skipping @TailRecursive annotated method: YourClass.mutuallyRecursiveMethod
Implementation Details¶
File Location¶
- Source:
src/main/scala/onion/compiler/optimization/TailCallOptimization.scala - Pipeline Integration: runs between
TypingandAsmCodeGeneration, immediately beforeMutualRecursionOptimization
Compiler Pipeline¶
Parsing → Rewriting → Typing → [TailCallOptimization] → MutualRecursionOptimization → AsmCodeGeneration
Testing¶
- Spec:
src/test/scala/onion/compiler/tools/TailCallOptimizationSpec.scala - Example programs in
src/test/run/:tail_recursion_factorial.on,tail_recursion_simple.on,tail_recursion_direct.on,tail_recursion_private.on,tail_recursion_public.on,tail_recursion_test.on
References¶
- Tail Call Optimization (Wikipedia)
- Related implementation: Scala's
@tailrecannotation - Similar optimization in functional languages: Haskell, Scheme, OCaml