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:
ifbranches (both then and else)- The last expression in
do,let, and function bodies when,cond,and,or(these expand toifanddo)
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) ;; => 5000050000Mutual 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) ;; => trueNon-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 runloop 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) ;; => :doneIn 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:
MINO_RECUR(fromrecur): rebinds parameters and loops within the same function.MINO_TAIL_CALL(from a tail-position call): switches to the target function and loops. Works across function boundaries, enabling mutual recursion.
This is the same approach used by Scheme and Erlang. The C stack stays flat regardless of recursion depth.