feat: implement tail call optimization (TCO)

Add trampoline-based tail call optimization to prevent stack overflow
on deeply recursive tail-recursive functions. The implementation:

- Extends EvalResult with TailCall variant for deferred evaluation
- Adds trampoline loop in eval_expr() to handle tail calls iteratively
- Propagates tail position through If, Let, Match, and Block expressions
- Updates all builtin callbacks to handle tail calls via eval_call_to_value
- Includes tests for deep recursion (10000+ calls) and accumulator patterns

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-13 04:42:53 -05:00
parent 052db9c88f
commit df5c0a1a32
2 changed files with 135 additions and 58 deletions

View File

@@ -1282,5 +1282,41 @@ c")"#;
let result = eval(source);
assert!(result.is_ok(), "Expected success but got: {:?}", result);
}
#[test]
fn test_tail_call_optimization() {
// This test verifies that tail-recursive functions don't overflow the stack.
// Without TCO, a countdown from 10000 would cause a stack overflow.
let source = r#"
fn countdown(n: Int): Int = if n <= 0 then 0 else countdown(n - 1)
let result = countdown(10000)
"#;
assert_eq!(eval(source).unwrap(), "0");
}
#[test]
fn test_tail_call_with_accumulator() {
// Test TCO with an accumulator pattern (common for tail-recursive sum)
let source = r#"
fn sum_to(n: Int, acc: Int): Int = if n <= 0 then acc else sum_to(n - 1, acc + n)
let result = sum_to(1000, 0)
"#;
// Sum from 1 to 1000 = 1000 * 1001 / 2 = 500500
assert_eq!(eval(source).unwrap(), "500500");
}
#[test]
fn test_tail_call_in_match() {
// Test that TCO works through match expressions
let source = r#"
fn process(opt: Option<Int>, acc: Int): Int = match opt {
Some(n) => if n <= 0 then acc else process(Some(n - 1), acc + n),
None => acc
}
let result = process(Some(100), 0)
"#;
// Sum from 1 to 100 = 5050
assert_eq!(eval(source).unwrap(), "5050");
}
}
}