Proper Tail Calls

mino optimizes all function calls in tail position. Self-recursion, mutual recursion, and general tail calls run in constant stack space with no special syntax required.

What tail position means

A function call is in tail position when it is the last thing a function does before returning. mino recognizes tail position in:

When the evaluator encounters a function call in tail position, it returns a trampoline value instead of growing the C stack. The caller's trampoline loop picks it up and jumps to the new function. No stack frame is allocated.

Self-recursion

Write recursive functions naturally. As long as the recursive call is in tail position, it runs in constant stack space:

(defn countdown (n)
  (if (= n 0) :done (countdown (- n 1))))

(countdown 1000000) ;; => :done (no stack overflow)

Accumulator patterns work the same way:

(defn sum-to (n acc)
  (if (= n 0) acc (sum-to (- n 1) (+ acc n))))

(sum-to 100000 0) ;; => 5000050000

Mutual recursion

Two or more functions can call each other in tail position without stack growth:

(defn is-even? (n)
  (if (= n 0) true (is-odd? (- n 1))))

(defn is-odd? (n)
  (if (= n 0) false (is-even? (- n 1))))

(is-even? 100000) ;; => true

Non-tail calls

If a call is not in tail position, normal recursion applies and will use stack space. This is correct behavior. For example, both calls in a tree traversal are non-tail:

(defn fib (n)
  (if (< n 2)
    n
    (+ (fib (- n 1))    ;; not tail: + still needs to run
       (fib (- n 2))))) ;; not tail: + still needs to run

loop and recur

loop/recur provide explicit iteration for code that reads better as a loop than as recursion. They continue to work exactly as before:

(loop (n 100 acc 0)
  (if (= n 0)
    acc
    (recur (- n 1) (+ acc n))))

With proper tail calls, loop/recur are a stylistic choice rather than a necessity. Use whichever reads more clearly for the task at hand.

trampoline

trampoline is available for code that returns thunks (zero-argument functions) to defer work:

(defn bounce (n)
  (if (= n 0) :done (fn () (bounce (- n 1)))))

(trampoline bounce 100000) ;; => :done

In practice, trampoline is rarely needed since tail calls are already optimized. It exists as a convenience for patterns that explicitly return thunks.

How it works

The evaluator tracks whether each expression is in tail position via an internal flag. When a user-defined function call occurs in tail position, the evaluator returns a MINO_TAIL_CALL sentinel carrying the target function and arguments, instead of recursing into the function body immediately.

The apply_callable trampoline loop handles two kinds of sentinel:

This is the same approach used by Scheme and Erlang. The C stack stays flat regardless of recursion depth.