dotnet / csharplang

The official repo for the design of the C# programming language
11.41k stars 1.02k forks source link

Proposal: `tail return` recursive calls #2304

Open agocke opened 5 years ago

agocke commented 5 years ago

There are a number of instances where it may be clearer to express your algorithm via recursion, especially tail recursion, but unfortunately all recursion in C# currently requires O(n) space for just the call stack, meaning that your algorithm cannot take less than O(n) space.

It is proposed that C# provide a new statement form and a new expression form to allow tail-recursive calls to consume O(1) space.

Syntax and Semantics

The statement form would be tail return <expr>, analogous to yield return, where the expr target is required and must be a tail recursive call.

The expression form would be tail <expr>, where the <expr> would have the same restrictions as the statement form, and the tail <expr> expression would only be legal as the expression in an expression-bodied member.

Codegen

There is a tail instruction in the CLR, but if that instruction is not reliable or desired, the compiler can lower this into a goto after assigning the new arguments for the recursive call.

For instance,

void M(int x)
{
    ...
    tail return M(x - 1);
}

Could be rewritten to

void M(int x)
{
    start:
    ...
    x = x - 1;
    goto start;
}

Limitations

Because the compiler cannot reliably rewrite inter-procedural calls, this would only currently be legal for tail-recursive calls. It would not be allowed for e.g., mutually tail recursive calls. If the CLR can reliably perform tail calls without significant downsides on all supported platforms, the feature could be broadened from the current specification.

hez2010 commented 3 years ago

IIRC tail recursive call optimization is guaranteed by JIT: https://sharplab.io/#v2:EYLgxg9gTgpgtADwGwBYA0AXEBDAzgWwB8BLAOwwAIBZACjMoQEoBYAKAG82LuLiAzCjQQUAvCIoBGRhQACAdkkBuLj3nUhFOJMbLWAXyA==

timcassell commented 2 years ago

@hez2010 Maybe in simple cases like that, but what about with virtual and non-self recursion?

void Func()
{
    // ...
    FuncVirt();
}

protected virtual void FuncVirt()
{
    // ...
    Func2();
}

void Func2()
{
    // ...
    if (x)
    {
        Func();
    }
}