This document describes how the portable tail-call elimination used for calls to .tailcall routines works.
Tail-call elimination refers to the process of turning a tail-call into a sibling-call, that is, a call that re-uses the same stack frame as the caller.
The following .tailcall procedures:
proc a(): int {.tailcall.} = result = 1 result = 2 return proc b(): int {.tailcall.} = return a()
are transformed into the internal equivalent of:
proc a(env: pointer): Continuation[int] = # the original `result` simply becomes a normal variable, so that result # assignments, taking the address of `result`, etc. all continue to work var result': int result' = 1 result' = 2 return Continuation[int](done: true, result: result') proc b(env: pointer): Continuation[int] = return Continuation[int](done: false, next: a)
A Continuation is either terminal (it stores the procedure's result) or non-terminal (it stores the procedure to continue with).
Since .tailcall routines are allowed to call other .tailcall routines with arbitrary signatures, but Continuation can only store procedures of a single type, thunks and a separate parameter storage are used:
proc a(x, y: int): int {.tailcall.} = x + y proc b(): int {.tailcall.} = a(1, 2)
are transformed into:
proc a(x, y: int, env: pointer): Continuation[int] {.tailcall.} = return Continuation[int](done: true, result: x + y) proc a_apply(env: ptr (int, int)): Continuation[int] {.tailcall.} = return a(env[][0], env[][1]) proc b(env: pointer): Continuation[int] {.tailcall.} = store env, (1, 2) return Continuation[int](done: false, next: a_apply)
store is a magic procedure responsible for writing the argument tuple to the storage in a type-safe fashion.
The storage is fixed in size and allocated on the stack. This gets around having to use heap allocation, but it also puts a hard limit on the maximum available space occupied by parameters.
How parameters are stored depends on their type and passing mode:
Since sink parameters may internally be passed by reference, a temporary location has to be allocated for them, as the env may be modified by the callee (which would clobber the storage already in use by a sink parameter).
When a .tailcall routine is called from a routine that is not a .tailcall routine, no guaranteed tail call (and thus sibling call) takes places.
The caller receives the Continuation returned by the .tailcall callee and "trampolines" it to completion. Example:
proc a(x, y: int): int {.tailcall.} = x + y proc b(): int = result = a(4, 5) echo result
becomes:
proc a(x, y: int, env: pointer): Continuation[int] = return Continuation[int](done: true, result: x + y) proc b(): int = var env: Storage var cont = a(4, 5, addr env) while not cont.done: cont = cont.next(addr env) result = cont.val echo result