Tail call
A tail call is a call made as the last action of a function: the caller returns the callee's result unchanged. Since the caller has nothing left to do, its stack frame is no longer needed, and the call can be compiled as a jump that reuses the frame[1]. A language implementation that does this for every tail call is properly tail recursive[2], and in it a loop can be written as a recursive function without the stack growing. Scheme requires proper tail recursion[3], and OCaml and most functional languages provide it; the Java virtual machine, Python and most JavaScript engines do not.
Overview
When a function calls another, the caller's state, its local variables and where to resume, is saved in a stack frame until the callee returns. If the call is the caller's last action, nothing will be done with that saved state except to pass the result on, and the callee can return straight to the caller's caller. A function whose recursive calls are all tail calls then runs in constant stack space, like a loop:
Summing a list with a non-tail call and with a tail call.
(* The addition happens after the recursive call returns: not a tail call. *)let rec sum = function [] -> 0 | x :: xs -> x + sum xs(* The running total is passed along; the recursive call is the lastthing the function does. *)let rec sum_acc acc = function [] -> acc | x :: xs -> sum_acc (acc + x) xs
Running it on ten million elements with an 8 MB system stack.
sum_acc 0 xs -> 49999995000000sum xs -> Stack_overflow
In sum the addition waits for the recursive call, so every element of the list has a frame on the stack when the end is reached. In sum_acc the partial sum is passed in an accumulator, the recursive call is the last thing the function does, and the stack does not grow. The limit that sum runs into is set by the runtime. In OCaml 4 it is the system stack, 8 MB here; in OCaml 5 it is a separate limit on OCaml's own stacks, the l parameter of OCAMLRUNPARAM, 1 GiB by default. The length of list at which sum fails depends on that limit; sum_acc runs on a list of any length.
Tail position
Whether a call is a tail call is a syntactic property of where it appears. The tail positions of an expression are defined by induction: the body of a function is in tail position, and
where the underlined subexpressions are in tail position if the whole expression is. A call in tail position is a tail call. The arguments of a call and the operands of an operator are never in tail position, and neither is the body of try … with, because the exception handler must remain installed until the body has returned. OCaml checks the expectation that a call is a tail call when it is marked with the [@tailcall] attribute:
Two calls marked as expected tail calls, neither of which is.
let rec sum = function [] -> 0 | x :: xs -> x + (sum [@tailcall]) xs(* A call inside the body of try ... with is not in tail position: thehandler has to stay installed until the call returns. *)let rec find_index p i = function| [] -> raise Not_found| x :: xs -> try if p x then i else (find_index [@tailcall]) p (i + 1) xs with Exit -> -1
The compiler's warnings.
File "t3_warn.ml", line 1, characters 48-68:1 | let rec sum = function [] -> 0 | x :: xs -> x + (sum [@tailcall]) xs^^^^^^^^^^^^^^^^^^^^Warning 51 [wrong-tailcall-expectation]: expected tailcallFile "t3_warn.ml", line 7, characters 38-75:7 | | x :: xs -> try if p x then i else (find_index [@tailcall]) p (i + 1) xs with Exit -> -1^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^Warning 51 [wrong-tailcall-expectation]: expected tailcall
The second case is a common mistake in OCaml: wrapping a recursive loop in an exception handler at every step makes each iteration keep a frame. The usual fix is to handle the exception outside the loop, or to use match … with exception, whose exception branch covers only the matched expression, so that the recursive call in a value branch stays in tail position.
Implementation
A tail call is compiled by placing the callee's arguments where the callee expects them, releasing the caller's frame, and jumping to the callee instead of calling it. A self-recursive tail call becomes a jump to the start of the function, which is a loop. The code that OCaml's native compiler generates for sum_acc has no call at all:
The core of the x86-64 code for sum_acc, abridged, with comments added.
camlT2_loop__sum_acc:....L101: ... # (entry of the loop)testb $1, %bl # empty list?je .L100...ret # return acc in %rax.L100:movq 8(%rbx), %rdi # xsmovq (%rbx), %rbx # xleaq -1(%rax,%rbx), %rax # acc + x, on tagged integersmovq %rdi, %rbxjmp .L101 # the recursive call
Steele argued in 1977 that procedure calls had a reputation for being expensive because of how they were implemented, and that a call in tail position is a "GOTO that passes arguments"[1]. Tail calls to other functions, not only to the function itself, are also jumps, so mutually recursive functions run in constant space as well. A finite-state machine can be written with one function per state and one tail call per transition:
A state machine that tests binary numbers for divisibility by three, and mutually recursive even and odd.
(* Tail calls between different functions: a state machine where eachstate is a function and each transition a tail call. It recognizesbinary numbers divisible by three, reading bits left to right; thestate is the remainder so far. *)let rec r0 s i = if i = String.length s then true else if s.[i] = '0' then r0 s (i + 1) else r1 s (i + 1)and r1 s i = if i = String.length s then false else if s.[i] = '0' then r2 s (i + 1) else r0 s (i + 1)and r2 s i = if i = String.length s then false else if s.[i] = '0' then r1 s (i + 1) else r2 s (i + 1)let rec even n = n = 0 || odd (n - 1)and odd n = n <> 0 && even (n - 1)
Running it.
9 = 1001 divisible by 3: true10 = 1010 divisible by 3: false21 = 10101 divisible by 3: true100 = 1100100 divisible by 3: false255 = 11111111 divisible by 3: true10,000,000 ones: divisible by 3: trueeven 100_000_001 -> false
If a callee needs more space for its arguments on the stack than the caller received, the caller's frame cannot simply be reused; calling conventions in which the callee removes its own arguments, or which pass arguments in registers, avoid the problem. In continuation-passing style every call is a tail call, and compilers that use CPS as their intermediate language rely on them being jumps[14].
Proper tail recursion
Clinger made the requirement precise by defining it as a property of space consumption: an implementation is properly tail recursive if it runs every program in no more space, up to a constant factor, than a reference machine that never keeps frames for tail calls[2]. The definition matters because it constrains the implementation, not the program: in a properly tail-recursive language, an unbounded loop written as tail recursion is correct, and in one that is not, it is a program that fails on large inputs. Abelson and Sussman distinguish accordingly between a recursive procedure, one that calls itself, and a recursive process, one whose execution needs space for deferred operations; a tail-recursive procedure generates an iterative process[5].
Accumulators
A function that performs work after its recursive call can often be made tail recursive by passing that work forward in an extra argument, as sum_acc does. When the pending work is an associative operation with a unit, such as addition, the accumulator holds the partial result:
where correctness is the invariant . When it is not, the accumulator has to hold a representation of the pending work, which in general is a continuation: converting a function to continuation-passing style makes every call a tail call, at the cost of allocating closures on the heap in place of frames on the stack.
Tail recursion modulo cons
Many functions that build a list are not tail recursive only because of a constructor applied to the recursive result, as in map: f x :: map f xs. The constructor could be allocated before the call with a hole where the tail goes, and the call could fill in the hole, after which the call is the last action. This transformation, tail recursion modulo cons, has been known since the 1970s, and OCaml performs it, since version 4.14, for functions annotated with [@tail_mod_cons][6][7]:
map, map with tail recursion modulo cons, and map by reversing an accumulated list.
(* map is not tail recursive: the cons waits for the recursive call. *)let rec map f = function [] -> [] | x :: xs -> f x :: map f xs(* With [@tail_mod_cons] the compiler allocates the cons cell first, witha hole for its tail, and passes the hole to the recursive call, whichfills it in. The call becomes a tail call. *)let[@tail_mod_cons] rec map_trmc f = function [] -> [] | x :: xs -> f x :: map_trmc f xs(* The usual alternative: an accumulator, and a reversal at the end. *)let map_rev f xs = List.rev (List.rev_map f xs)
Running it on ten million elements with an 8 MB system stack.
map -> Stack_overflowmap_trmc -> 10000000map_rev -> 10000000
The transformed function writes into a block after it has been allocated, which is safe because the block is not visible to any other code until the function returns. The standard library's List.map and several other list functions have been defined this way since OCaml 5.1. The alternative of accumulating the result in reverse and reversing it at the end also runs in constant stack space, but traverses the list twice and allocates it twice.
In programming languages
Scheme was the first language to require proper tail recursion, in its definition from 1975[4][3]. Standard ML, OCaml, Haskell, Erlang, Elixir and Lua implement it; in Haskell, where evaluation is lazy, the question of stack use depends on evaluation more than on tail position. ECMAScript 2015 made proper tail calls part of the JavaScript standard in strict mode[8], but of the major engines only JavaScriptCore, in Safari, implemented them.
The Java virtual machine has no tail-call instruction, and its security model inspected the call stack, which made eliminating frames difficult[11]. Scala therefore turns self-recursive tail calls into loops, and @tailrec reports an error when that is not possible, but does not eliminate other tail calls; Kotlin has tailrec for the same purpose, and Clojure an explicit recur. Python does not eliminate tail calls, and its designer has argued that doing so would make stack traces less useful for debugging[12]. C and C++ compilers eliminate tail calls as an optimization when it is safe, but do not guarantee it; Clang's [[clang::musttail]] attribute makes a tail call an error if it cannot be compiled as a jump[13]. WebAssembly added return_call instructions for tail calls, and Rust has a proposed become keyword for them.
Trampolines
Where the target does not support tail calls, a compiler or programmer can use a trampoline: each function returns either its result or a description of the next call, and a loop at the top performs the calls, so the stack never holds more than one of them[9]:
A trampoline for mutually recursive even and odd.
(* A trampoline: for languages without tail calls. A function returnseither a result or the next call to make, and a loop makes the calls. *)type 'a bounce = Done of 'a | Call of (unit -> 'a bounce)let rec run = function Done x -> x | Call f -> run (f ())let rec even n = if n = 0 then Done true else Call (fun () -> odd (n - 1))and odd n = if n = 0 then Done false else Call (fun () -> even (n - 1))
Running it.
run (even 100_000_001) -> false
Each bounce allocates a closure and returns to the loop, which makes trampolined code several times slower than direct tail calls. Compilers of Scheme to C have used trampolines, or Baker's technique of letting the C stack grow and discarding it with longjmp when it is full, having copied the live data to the heap[10].
Debugging
A tail call removes the caller's frame, so a stack trace taken in the callee does not show the caller. In a properly tail-recursive language this is the cost of running loops in constant space, and debuggers for such languages sometimes keep a bounded history of recent tail calls to compensate.
History
Tail calls were understood as jumps in the implementation of Lisp and Algol-like languages in the 1960s. Sussman and Steele made proper tail recursion part of the definition of Scheme in 1975[4], and Steele's 1977 paper argued that procedure calls, compiled correctly, need be no more expensive than jumps[1]. Clinger gave the formal definition in terms of space in 1998[2]. OCaml added the [@tailcall] check in 2016 and tail recursion modulo cons in version 4.14[6].
see also
- Continuation-passing styleContinuation-passing style (CPS) is a way of writing programs in which no function returns: each takes an extra argument, its continuation, a function that represents the rest of the computation, and calls it with the result. Every call becomes a tail call, evaluation order is made explicit, and control operators such as exceptions, backtracking and call/cc become ordinary functions. Compilers for functional languages use CPS, or the closely related A-normal form, as an intermediate language.
- ClosureA closure is a function value together with the environment in which it was created: the bindings of the free variables its body refers to. Closures are what make functions first-class values in a lexically scoped language. Compilers implement them by closure conversion, which turns each function into a code pointer paired with a record of captured values.
- Space leakMemory a program holds longer than it needs to. In a lazy language the usual cause is a chain of unevaluated thunks: a lazy left fold over a list builds one suspended addition per element and only performs them at the end. A heap profile by closure type shows it as a growing THUNK band.
- Effect handlerAn effect handler runs code that may perform effects, and handles each effect by receiving it together with the continuation from the point where it was performed, which it can resume, discard or store. Handlers generalize exception handlers, and let generators, async I/O, schedulers, state and nondeterminism be written as libraries while the code that uses them stays in direct style. They come from the theory of algebraic effects of Plotkin, Power and Pretnar. OCaml 5 implements them with one-shot continuations: each can be resumed at most once, which keeps them cheap, since a continuation is the suspended stack itself.
- DefunctionalizationDefunctionalization is a whole-program transformation that removes higher-order functions. Each lambda expression in the program is replaced by a constructor of a data type, carrying the lambda's free variables, and each call of an unknown function is replaced by a call to a single first-order apply function that matches on the constructor and runs the corresponding body. Applied to the continuations of a program in continuation-passing style, it turns a recursive evaluator into an abstract machine whose stack is an explicit data structure.
- Lazy evaluationLazy evaluation, or call-by-need, delays computing an expression until its value is needed and then remembers the value, so it is computed at most once. It lets programs define infinite structures and consume only part of them, separates generating candidate values from choosing among them, and avoids work whose result is never used. Its costs are the memory held by unevaluated suspensions, which can cause space leaks, and harder reasoning about when work happens. Haskell is lazy by default; OCaml provides it explicitly through Lazy.t and Seq.
referenced by
further reading
- [1]G. L. Steele Jr., “Debunking the ‘expensive procedure call’ myth, or, procedure call implementations considered harmful, or, LAMBDA: the ultimate GOTO”, ACM Annual Conference (1977).
- [2]W. D. Clinger, “Proper tail recursion and space efficiency”, PLDI (1998).
- [3]R. Kelsey, W. Clinger, J. Rees (eds.), “Revised⁵ report on the algorithmic language Scheme”, Higher-Order and Symbolic Computation 11 (1998).
- [4]G. J. Sussman, G. L. Steele Jr., “Scheme: an interpreter for extended lambda calculus”, MIT AI Memo 349 (1975).
- [5]H. Abelson, G. J. Sussman, Structure and Interpretation of Computer Programs, §1.2.1, MIT Press (2nd ed., 1996).
- [6]F. Bour, B. Clément, G. Scherer, “Tail modulo cons”, Journées Francophones des Langages Applicatifs (2021).
- [7]The OCaml manual, “The ‘Tail Modulo Constructor’ program transformation”.
- [8]ECMA-262, ECMAScript 2015 Language Specification, §14.6 “Tail position calls” (2015).
- [9]S. E. Ganz, D. P. Friedman, M. Wand, “Trampolined style”, ICFP (1999).
- [10]H. G. Baker, “CONS should not CONS its arguments, part II: Cheney on the M.T.A.”, SIGPLAN Notices 30 (1995).
- [11]M. Schinz, M. Odersky, “Tail call elimination on the Java Virtual Machine”, Electronic Notes in Theoretical Computer Science 59 (2001).
- [12]G. van Rossum, “Tail recursion elimination” and “Final words on tail calls”, Neopythonic (2009).
- [13]Clang documentation, “musttail” statement attribute.
- [14]A. W. Appel, Compiling with Continuations, Cambridge University Press (1992).
last updated