Skip to content

compiler: optimized tail calls #804

Open

Description

@evacchi

#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.

Prior art

Activity

  1. evacchi commented on Apr 17, 2025

    @evacchi
    CollaboratorAuthor

    given that:

    1. we fully control the ABI for functions
    2. 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:

    class Scratch {
        public static void main(String[] args) {
            // ABI is fIndex, status = [CONT|DONE], result, args...
            long[] result = tailCall(FACTORIAL, CONT, 0, 20, 1);
            System.out.println(result[resultSlot]);
            result = tailCall(ODD, CONT, 0, 1_000_000_000);
            System.out.println(result[resultSlot] != 0); // odd == false
            result = tailCall(EVEN, CONT, 0, 1_000_000_000);
            System.out.println(result[resultSlot] != 0); // even == true
        }
    
        static long DONE = 1, CONT = 0;
        static long FACTORIAL = 0;
        static long EVEN = 1;
        static long ODD = 2;
        static int fSlot = 0;
        static int statusSlot = 1;
        static int resultSlot = 2;
        static int argBegin = 3;
    
        static long[] factorial(long... args) {
            long n = args[argBegin + 0], acc = args[argBegin + 1];
            if (n <= 1) {
                args[statusSlot] = DONE;
                args[resultSlot] = acc;
                return args;
            } else {
                args[statusSlot] = CONT;
                args[argBegin + 0] = n - 1;
                args[argBegin + 1] = n * acc;
                return args;
            }
        }
    
        static long[] even(long... args) {
            long fIndex = EVEN, n = args[argBegin + 0];
            if (n == 0) {
                args[statusSlot] = DONE;
                args[resultSlot] = 1; // TRUE
                return args;
            } else {
                args[fSlot] = ODD;
                args[argBegin+0] = Math.abs(n)-1;
                return args;
            }
        }
    
        static long[] odd(long... args) {
            long n = args[argBegin + 0];
            if (n == 0) {
                args[statusSlot] = DONE;
                args[resultSlot] = 0; // FALSE
                return args;
            } else {
                args[fSlot] = EVEN;
                args[argBegin + 0] = Math.abs(n)-1;
                return args;
            }
        }
    
    
        static long[] tailCall(long... args) {
            var status = CONT;
            do {
                args = dispatchTailCall(args);
                status = args[statusSlot];
            } while (status == CONT);
            return args;
        }
    
        static long[] dispatchTailCall(long... args) {
            if (args[0] == FACTORIAL) {
                return factorial(args);
            }
            if (args[0] == EVEN) {
                return even(args);
            }
            if (args[0] == ODD) {
                return odd(args);
            }
            throw new IllegalArgumentException();
        }
    
    
    }
    
  2. changed the title [-]aot: optimized tail calls[/-] [+]compiler: optimized tail calls[/+] on Jul 24, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions