You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
#801 introduces tail calls, as a plain desugaring into CALL(_INDIRECT)+RETURN.
This obviously works in a general sense, but it does not implement "real" tail call optimization: some tests in the suite (currently disabled) exercise the stack with deep recursions (e.g. factorial of 1_000_000), and cause an overflow error.
While this is a potential issue, the tail call proposal is currently only required to be present to pass the exception handling suite; however, eventually, we might still want to address it.
There are essentially two scenarios:
simple tail recursion, where f calls itself,
mutually recursive functions, where f calls g, g calls f (more complex scenarios might involve n > 2 functions too).
In the first case the recursion can be (more or less) flattened to a jump to the beginning of the function;
In the second case, multiple solutions could be adopted, such as trampolines, stitching together the bodies of the functions and jump to their logical prologues, or even a task queue.
we can mark any function that uses a return_call instruction during (some new?) analysis pass
we could turn every function that does tail call into a long[] -> long[] (this is already kind of true in a general sense, even though we often optimized and "unbox" the args); the long[] arg is however, built as follows:
args[0] = function index
args[1] = CONT, DONE // i.e. 0, 1
args[2..2+n-1] = reserved for results // generally n=1, so result-index == 2
args[2+n...] = args
The result array is built exactly in the same way, so that args can be even returned and modified in-place (avoiding further allocations)
so, for instance factorial(n = 20, acc = 1) would be:
args = { FACTORIAL_INDEX, CONT, 0, 20, 1 } // 0 is a result placeholder
then each return_call is turned into a plain return args, but the callee must be a special ancillary function that actually does the looping. See below.
Finally, we could then provide Machine#call wrappers with the usual API/ABI for external calls. (This could even work cross-module, provided that the target function is known to be tailrec. Which we could know by e.g. annotating it in the bytecode)
here is a full example with FACTORIAL self-recursive and ODD/EVEN mutual-recursive functions:
Activity
evacchi commentedon Apr 17, 2025
given that:
return_callinstruction during (some new?) analysis passwe could turn every function that does tail call into a
long[] -> long[](this is already kind of true in a general sense, even though we often optimized and "unbox" the args); thelong[]arg is however, built as follows:The result array is built exactly in the same way, so that
argscan be even returned and modified in-place (avoiding further allocations)so, for instance
factorial(n = 20, acc = 1)would be:then each
return_callis turned into a plainreturn args, but the callee must be a special ancillary function that actually does the looping. See below.Finally, we could then provide
Machine#callwrappers with the usual API/ABI for external calls. (This could even work cross-module, provided that the target function is known to be tailrec. Which we could know by e.g. annotating it in the bytecode)here is a full example with FACTORIAL self-recursive and ODD/EVEN mutual-recursive functions:
[-]aot: optimized tail calls[/-][+]compiler: optimized tail calls[/+]