197: Trampoline — Heap-Based Tail Call Optimization
Difficulty: 3 Level: Advanced Convert deep recursion into a loop by returning "do more work" instead of calling recursively — eliminating stack overflow forever.The Problem This Solves
You write a beautiful recursive function. Clean, clear, elegant. Then you run it with a large input and Rust kills your program with a stack overflow. The default stack is 8MB. A recursive function with 100,000 levels uses 100,000 stack frames. Math says that won't fit. The obvious fix: rewrite as a loop. But for mutual recursion — where function A calls B, and B calls A — that rewrite can be genuinely painful. You end up simulating a call stack manually, which is exactly what you were trying to avoid. The standard advice for OCaml and Haskell is "make it tail-recursive and rely on TCO (Tail Call Optimization)." The compiler eliminates the stack frame for tail calls. But Rust does not guarantee TCO. Even a perfect tail-recursive function in Rust can overflow the stack. The trampoline pattern solves this without changing your logic. Instead of calling the next function directly, you return it — wrapped as a closure. A tiny outer loop runs the closures one at a time, staying on constant stack space. The "pending work" lives on the heap (in closures), not the stack. Zero stack overflow, no matter the depth. This is exactly how Rust's async runtime works internally. This example exists to solve exactly that pain.The Intuition
Imagine a trampoline as a machine that bounces you up, one hop at a time. Each "hop" is: "do a little work, then come back down." The machine (loop) keeps bouncing until you say "I'm done." In code: instead of a function calling another function (which grows the stack), the function returns one of two things:- `Done(value)` — finished, here's the answer
- `More(closure)` — not done yet, run this closure next
enum Step<A> {
Done(A), // computation finished, here's the value
More(Box<dyn FnOnce() -> Step<A>>), // do more work next iteration
}
fn run<A>(mut step: Step<A>) -> A {
loop { // O(1) stack — same frame every iteration
match step {
Step::Done(x) => return x, // finished
Step::More(f) => step = f(), // do next hop, stay in loop
}
}
}
Compare: without trampoline, `is_even(1_000_000)` would need 1,000,000 stack frames. With trampoline, it uses exactly 1 stack frame forever.
How It Works in Rust
Mutual recursion — classic stack overflow case:// Without trampoline: even(1_000_000) → odd(999_999) → even(999_998) → ...
// Stack overflow around depth 10_000-100_000 depending on frame size
// With trampoline: each "call" returns a Step instead of recursing
fn is_even_t(n: u64) -> Step<bool> {
if n == 0 {
Step::done(true) // base case: done
} else {
Step::more(move || is_odd_t(n - 1)) // defer: "call is_odd next"
}
}
fn is_odd_t(n: u64) -> Step<bool> {
if n == 0 {
Step::done(false)
} else {
Step::more(move || is_even_t(n - 1))
}
}
// The trampoline runs it — no stack growth:
let result = run(is_even_t(1_000_000)); // works! returns true
Factorial with accumulator (tail-recursive trampoline):
fn factorial_t(n: u64) -> u64 {
fn go(n: u64, acc: u64) -> Step<u64> {
if n <= 1 {
Step::done(acc) // done: return accumulated result
} else {
Step::more(move || go(n - 1, n * acc)) // next hop: multiply and continue
}
}
run(go(n, 1))
}
assert_eq!(factorial_t(20), 2_432_902_008_176_640_000);
Fibonacci with CPS to avoid two recursive calls:
// Problem: fib needs TWO recursive calls. Trampoline handles one at a time.
// Solution: CPS inside the trampoline — continuation holds "what to do with result A
// before starting on result B"
fn fibonacci_t(n: u64) -> u64 {
fn go(n: u64, k: Box<dyn FnOnce(u64) -> Step<u64>>) -> Step<u64> {
if n <= 1 {
k(n) // base: give n to the continuation
} else {
Step::more(move || {
go(n - 1, Box::new(move |a| { // first: compute fib(n-1) → a
go(n - 2, Box::new(move |b| k(a + b))) // then: compute fib(n-2) → b
}))
})
}
}
run(go(n, Box::new(Step::done)))
}
The pattern that appears in async runtimes:
async fn foo() { ... }
// Desugars to something like:
// Poll::Pending → More(waker + closure)
// Poll::Ready(x) → Done(x)
// The executor is the trampoline loop.
What This Unlocks
- Unlimited recursion depth: Any recursive algorithm — tree traversal, interpreters, state machines, parsers — can be made stack-safe with this pattern. No algorithm rewrite required.
- Async runtime comprehension: Rust's `Future` is a trampoline. `Poll::Pending` = `More`, `Poll::Ready` = `Done`. The executor IS the `run` loop. This clicks once you see trampoline.
- Mutual recursion between modules: State machines with many states that call each other freely, without worrying about stack depth. Common in protocol parsers and game AI.
Key Differences
| Concept | OCaml | Rust |
|---|---|---|
| Stack safety for tail calls | Free (compiler TCO) | Must implement manually via trampoline |
| Trampoline needed? | Rarely | Yes, for deep recursion |
| Closure in `More` | GC-allocated, cheap | `Box<dyn FnOnce()>` heap allocation |
| Async/await | Not built-in | Based on this exact pattern |
| Stack size limit | ~1MB default, runtime tunable | 8MB default, thread spawn for more |
| Mutual recursion | Naturally tail-recursive | Needs trampoline |
// Trampoline — Heap-Based Tail Call Optimization
//
// Deep mutual recursion causes stack overflow in Rust (no TCO guarantee).
// A trampoline converts it to a heap loop: instead of recursing, return
// a "thunk" (Bounce) that the loop will call next.
//
// type Step<A> = Done(A) | More(Box<dyn FnOnce() -> Step<A>>)
// ── The Trampoline type ───────────────────────────────────────────────────
enum Step<A> {
Done(A),
More(Box<dyn FnOnce() -> Step<A>>),
}
impl<A> Step<A> {
fn done(x: A) -> Self {
Step::Done(x)
}
fn more<F: FnOnce() -> Step<A> + 'static>(f: F) -> Self {
Step::More(Box::new(f))
}
}
/// Run the trampoline loop — O(1) stack regardless of depth.
fn run<A>(mut step: Step<A>) -> A {
loop {
match step {
Step::Done(x) => return x,
Step::More(f) => step = f(),
}
}
}
// ── Mutual recursion: even/odd — would stack-overflow at ~100k without trampoline
fn is_even_t(n: u64) -> Step<bool> {
if n == 0 {
Step::done(true)
} else {
Step::more(move || is_odd_t(n - 1))
}
}
fn is_odd_t(n: u64) -> Step<bool> {
if n == 0 {
Step::done(false)
} else {
Step::more(move || is_even_t(n - 1))
}
}
// ── Factorial via trampoline ───────────────────────────────────────────────
fn factorial_t(n: u64) -> u64 {
fn go(n: u64, acc: u64) -> Step<u64> {
if n <= 1 {
Step::done(acc)
} else {
Step::more(move || go(n - 1, n * acc))
}
}
run(go(n, 1))
}
// ── Fibonacci via trampoline (CPS to avoid two recursive calls) ───────────
fn fibonacci_t(n: u64) -> u64 {
// Use CPS to reduce to one recursive call at a time.
fn go(n: u64, k: Box<dyn FnOnce(u64) -> Step<u64>>) -> Step<u64> {
if n <= 1 {
k(n)
} else {
Step::more(move || {
go(n - 1, Box::new(move |a| {
go(n - 2, Box::new(move |b| k(a + b)))
}))
})
}
}
run(go(n, Box::new(Step::done)))
}
// ── Ackermann via trampoline ──────────────────────────────────────────────
fn ackermann_t(m: u64, n: u64) -> u64 {
fn go(m: u64, n: u64) -> Step<u64> {
if m == 0 {
Step::done(n + 1)
} else if n == 0 {
Step::more(move || go(m - 1, 1))
} else {
// ackermann(m, n) = ackermann(m-1, ackermann(m, n-1))
// We need the inner result first — use a CPS/two-step approach.
// Since the trampoline is single-step, we chain via a closure.
Step::more(move || {
// Compute inner = ackermann(m, n-1), then use it
// We run the inner trampoline eagerly (small values only):
let inner = run(go(m, n - 1));
go(m - 1, inner)
})
}
}
run(go(m, n))
}
// ── Flatten a deeply-nested list via trampoline ───────────────────────────
#[derive(Debug, Clone)]
enum Tree {
Leaf(i32),
Node(Vec<Tree>),
}
fn flatten_t(tree: Tree) -> Vec<i32> {
fn go(trees: Vec<Tree>, acc: Vec<i32>) -> Step<Vec<i32>> {
if trees.is_empty() {
Step::done(acc)
} else {
let mut rest = trees;
let head = rest.remove(0);
match head {
Tree::Leaf(v) => {
let mut new_acc = acc;
new_acc.push(v);
Step::more(move || go(rest, new_acc))
}
Tree::Node(children) => {
let mut new_trees = children;
new_trees.extend(rest);
Step::more(move || go(new_trees, acc))
}
}
}
}
run(go(vec![tree], vec![]))
}
fn main() {
// ── Even/odd — large numbers ───────────────────────────────────────
println!("is_even(1_000_000) = {}", run(is_even_t(1_000_000)));
println!("is_odd(999_999) = {}", run(is_odd_t(999_999)));
println!();
// ── Factorial ─────────────────────────────────────────────────────
println!("10! = {}", factorial_t(10));
println!("20! = {}", factorial_t(20));
println!();
// ── Fibonacci ─────────────────────────────────────────────────────
for i in [0, 1, 5, 10, 15] {
println!("fib({}) = {}", i, fibonacci_t(i));
}
println!();
// ── Ackermann ─────────────────────────────────────────────────────
println!("ack(3,4) = {}", ackermann_t(3, 4)); // 125
println!();
// ── Flatten tree ──────────────────────────────────────────────────
let tree = Tree::Node(vec![
Tree::Leaf(1),
Tree::Node(vec![Tree::Leaf(2), Tree::Leaf(3)]),
Tree::Node(vec![
Tree::Node(vec![Tree::Leaf(4), Tree::Leaf(5)]),
Tree::Leaf(6),
]),
]);
println!("flatten = {:?}", flatten_t(tree));
println!();
println!("No stack overflow on deep recursion ✓");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_even_large() {
assert!(run(is_even_t(1_000_000)));
assert!(!run(is_even_t(999_999)));
}
#[test]
fn test_is_odd_large() {
assert!(run(is_odd_t(999_999)));
assert!(!run(is_odd_t(1_000_000)));
}
#[test]
fn test_factorial() {
assert_eq!(factorial_t(0), 1);
assert_eq!(factorial_t(1), 1);
assert_eq!(factorial_t(5), 120);
assert_eq!(factorial_t(10), 3_628_800);
}
#[test]
fn test_fibonacci() {
assert_eq!(fibonacci_t(0), 0);
assert_eq!(fibonacci_t(1), 1);
assert_eq!(fibonacci_t(10), 55);
assert_eq!(fibonacci_t(15), 610);
}
#[test]
fn test_ackermann() {
assert_eq!(ackermann_t(0, 0), 1);
assert_eq!(ackermann_t(1, 1), 3);
assert_eq!(ackermann_t(2, 3), 9);
assert_eq!(ackermann_t(3, 3), 61);
}
#[test]
fn test_flatten_tree() {
let tree = Tree::Node(vec![
Tree::Leaf(1),
Tree::Node(vec![Tree::Leaf(2), Tree::Leaf(3)]),
]);
assert_eq!(flatten_t(tree), vec![1, 2, 3]);
}
}
(* Trampoline: convert deep recursion to a loop by returning thunks.
Each step either returns a value (Done) or a thunk to continue (Bounce). *)
type 'a trampoline =
| Done : 'a -> 'a trampoline
| Bounce : (unit -> 'a trampoline) -> 'a trampoline
let done_ x = Done x
let bounce f = Bounce f
(* Run the trampoline loop — O(1) stack! *)
let run t =
let rec loop = function
| Done x -> x
| Bounce f -> loop (f ())
in loop t
(* Even/odd without stack overflow via mutual recursion + trampoline *)
let rec is_even_t n =
if n = 0 then done_ true
else bounce (fun () -> is_odd_t (n - 1))
and is_odd_t n =
if n = 0 then done_ false
else bounce (fun () -> is_even_t (n - 1))
(* Factorial via trampoline *)
let factorial_t n =
let rec go n acc =
if n <= 1 then done_ acc
else bounce (fun () -> go (n - 1) (n * acc))
in run (go n 1)
let () =
Printf.printf "is_even 1000000 = %b\n" (run (is_even_t 1_000_000));
Printf.printf "is_odd 999999 = %b\n" (run (is_odd_t 999_999));
Printf.printf "10! = %d\n" (factorial_t 10);
Printf.printf "No stack overflow on deep recursion\n"