Proper Tail Calls and Recursion

Tail applications replace the current evaluator frame in an explicit trampoline, so self and mutual tail recursion do not grow the logical continuation.

When this matters

Tail calls matter when a loop is naturally written as recursion. If the recursive call is the final action, Lispex can replace the current continuation instead of stacking another one.

See it run

LISPEX
(let loop ((n 10000) (acc 0)) (if (= n 0) acc (loop (- n 1) (+ acc 1))))

Observed result

OUTPUT
10000

Read the example

Named let creates loop. Each branch either returns acc or immediately calls loop with smaller n and updated acc; there is no pending addition after the call. Ten thousand iterations therefore finish as one logical loop.

How to reason about it

  • Tail positions include final bodies, both if branches, normalized cond/case, final and/or, do, call-with-values, and apply.
  • Proper tail behavior concerns continuation growth, not equal wall-clock time or unlimited execution.
  • Resource limits remain deterministic and profile-specific even for tail-recursive programs.

Choose quickly

ShapeTail call?Why
(if done? answer (loop next))yesthe selected branch result is returned directly
(begin (display x) (loop next))yesthe call is the final form
(+ x (loop next))noaddition remains after the recursive result
(map loop items)no for the callbackmap still owns the callback result
(apply loop args) in tail positionyesapply preserves the caller’s tail position

A common mistake

Non-tail recursion may reach the declared resource limit.

Current boundaries

  • A clean resource fault is not evidence that native and WASM have the same threshold.

Keep going

The recursion guide shows base cases and accumulator conversion. Determinism and Resources explains why tail-safe does not mean unbounded.

Recursion guide · Determinism and resources