upvote
Not every tail call is for a loop.

You can have a set of mutually recursive functions, which tail call each other.

In C you can write state machines using "goto" (the implementations with "switch" are typically much more inefficient), but in languages with guaranteed tail call optimizations you can write a state machine where each state is a function.

In general, it is frequent enough to call another function as the last step of a function, even when there is no recursion involved. It is quite stupid for a compiler to use a CALL in such instances, instead of using a JMP. The only problem is that the function calling convention must be compatible with this optimization, while traditionally the C language used an inefficient calling convention that is not compatible with optimizations. That convention is a residue of the time when functions could be used without being declared and it should never be used by modern compilers.

reply
I can't see why the calling convention could matter. Can you give an example?
reply
When the calling convention is such that the caller owns the function arguments, the callee can’t remove/replace them on the stack, but has to keep them across the tail call. In turn, it means that the callee has to clean up the arguments to the tail call, and thus can’t actually make a tail call, unless the argument list happens to be identical to the original call.
reply
But the compiler controls both the caller and callee. It doesn't need to respect any calling convention during a TCO. In fact it won't; it'll jump instead of calling.
reply
If the callee is an exported symbol, the compiler has no choice but to adhere to the calling convention. For the C model of translation units that's the default, only local (declared "static") functions are exempted, and usually also only if their address is not taken. More generally, when the tail call crosses the boundaries of modularization that are supported by separate compilation, a recompilation step at the module-linking level would be required. The other complication is function pointers, which assume a specific calling convention, so either you have to have different function-pointer types with different calling conventions, or the compiler has to generate thunks or similar that translate between different calling conventions.

Of course, a language implementation can arrange for all that; but clearly, calling conventions are relevant here.

reply
Do you often find yourself doing mutual recursion between functions crossing compilation units/modules? I'm not going to say it can't happen, I just don't see it as much of a problem.
reply
> But the compiler controls both the caller and callee.

Why? In functional languages, it's common for an exported function from one compilation context to tail call into an exported function from another.

reply
Functional languages may be designed to support TCO from the ground up, up to supporting it across module boundaries. C is not like that, and calling into an external module necessarily grows the stack.
reply
> C is not like that, and calling into an external module necessarily grows the stack.

This is because of the calling convention, yes? (and to some extent, if you want an accurate stack trace, but I find it acceptable that TCO also includes stack trace erasure)

reply
Yyyyes... But like I said, TCO cannot make use of the calling convention, because it doesn't involve calling. That means you can't have TCO loops crossing module boundaries (or function pointers for that matter). So we're back to my original question: what does it matter what the calling convention is if the compiler has the liberty to compile both functions however it pleases?
reply
With the right calling convention, tail calls could conform to the convention.

A tail call certainly can't use a CALL instruction, because it would set the wrong return address. But that doesn't mean it's not a call; architectures without CALL/RETURN instructions exist, but you can still call into functions and return from them, the compiler just has to do different work.

In a callee cleanup convention, a tail caller could adjust the stack and jump to an unaware tail callee. The original caller and the tail callee would be none the wiser. I don't know enough to really evaluate calling conventions against each other, but it's pretty clear that caller cleanup makes tail call optimization more intrusive.

reply
>In a callee cleanup convention, a tail caller could adjust the stack and jump to an unaware tail callee.

You can still do that with a caller-cleanup convention. Suppose you have a convention like

* Set up stack

* Call

* Clean up stack

and you have functions f(), g(), and h(), where g() and h() use this convention and f() calls into g(), and g() into h(). The sequence of instructions from f() to h() without TCO would be

* f: Set up stack for g()

* f: Call g()

* g: Do work

* g: Set up stack for h()

* g: Call h()

* h: Do work

* h: Return

* g: Clean up stack

* g: Return

* f: Clean up stack

And with TCO:

* f: Set up stack for g()

* f: Call g()

* g: Do work

* g: Move things around on the stack so that h()'s arguments are written where g()'s were. This may require a temporary stack allocation that's released before the next step.

* g: Jump to h()

(At this point it looks as if f() called h() directly.)

* h: Do work

* h: Return

* f: Clean up stack

This is always possible as long as h()'s caller-managed stack allocation is no bigger than g()'s.

reply
h() can't have more arguments than g(): that's an important limitation.
reply
Consider what was being discussed originally, though. If h() has fewer arguments than g() and is in a different module (e.g. a static library) from h() such that the calling convention was necessary, how would h() recurse back to g()?
reply
The example you set up was about f() calling into g() calling into h(). Why do you mention h() calling into g() now?

The whole thread is about how the traditional calling convention makes it difficult to implement TCO in C. Functions with different arity having different stack layout is indeed one of the roadblocks, so I think we agree here, no?

reply
That was in response to a specific point that TCO needed callee-cleanup. But nobody cares about TCO in non-recursive call stacks, and nobody does cross-module recursion. Hence my question: if the only situation where anyone would care whether TCO is being performed is one in which the compiler can see both sides of the call, what does it matter what the calling convention is? The compiler is not bound to use any calling convention to generate the code for the call, it can just generate the caller and callee to be compatible with other and with no one else.
reply
Ah, now I see that indeed we agree without understanding each other. The source is a misinterpretation of your sentence far up the thread:

> I can't see why the calling convention could matter.

The "could matter" was understood as "would influence TCO" and so the rest of the thread was devoted to explain how the two are connected, while you meant "should be fixed and not be changed at the compiler's whims".

And you are right, of course: if a function is static and its address is never taken, the compiler can choose whatever calling strategy it wants, possibly one that facilitates TCO.

reply
It is usually assumed that it does not control the callee and the jump has to preserve the calling convention for a call.
reply
If it doesn't control both then TCO is impossible, because the stack will grow with each recursive step, as it's just performing a normal call.
reply
Have your mind blown: https://godbolt.org/z/xnn3PPxvW
reply
Extremely specific example is uncompelling.
reply
It is a simple counterexample to your incorrect statement.
reply
It's possible for calling conventions where the callee is responsible for stack cleanup before return.
reply
> What practical patterns are enabled by TCO in C?

Continuation Passing Style - an important construction for interpreters, but which is also useful for compilers as it's a nice way to do control flow analysis, data flow analysis and more.

The missing feature is closures - functions which capture values from their static environment, which are basically needed to make CPS useful. GCC has nested functions, but they cannot capture without making the stack executable, which is terrible. There's a proposal[1] to get closures into C, but at present you need to simulate the capturing yourself, which is cumbersome, but can be done efficiently.

[1]:https://thephd.dev/_vendor/future_cxx/papers/C%20-%20Functio...

reply
> My impression is that every tail call can written as a loop much more naturally.

Which is more natural? (please just assume my wonky pseudo code syntax makes sense)

   printall(List) -> 
      foreach item in List {
         print_item(item)
     }.

   printall([Head | Tail]) ->
       print_item(Head),
       printall(Tail);
   printall([]) -> ok.

    
IMHO, both of these need to be taught, neither is particularly more natural. In addition, as others have described, TCO makes a lot of sense for interpreters and state machines.
reply
> TCO makes a lot of sense for interpreters and state machines.

The reason that performant implementations prefer TCO is because the only reliable knob that clang and gcc provide to control which locals are spilled to stack vs. kept in registers is via calling convention constraints. One could accomplish the same performance without TCO'd recursion if there existed an annotation for local variables designating them as spill/no-spill. But that doesn't exist in clang or gcc - the "register" keyword in the C standard was supposed to be for exactly that, but it's ignored in both compilers.

reply
That's not entirely true, but it's a valid reason to prefer using musttail.

`register` is a hint if you don't specify which register you want to use - however, if you specify the register it will clobber it.

    noinline void bar() 
    {
        register void *parent __asm__("r10");
        ...
    }
You can also use GCCs extended asm syntax to clobber a register for specific portions of code - such as the start of a function where you expect a register to have been given a value from the caller just before the call. Use `volatile` to prevent the compiler from making certain assumptions that might remove or reorder the instruction - as long as it is at the top it should execute immediately after the function prelude and before any of the function body.

    noinline void bar() 
    {
        void *volatile parent;
        // set parent = %r10 before anything else.
        asm volatile ("mov{q}\t{%%r10, %0|%0, r10}" : "=r"(parent) : : "r10");
        ...
    }
Note that this will probably be less efficient than the former example, but maybe useful where you want to limit the scope in which `r10` is clobbered.

In both cases you would set the register immediately before making the call, again using `volatile`. Since `r10` is not used by a typical call in SYSV - it's the static chain pointer in the SYSV convention, but otherwise usable as a GP register, a call will not overwrite it.

    void foo()
    {
        struct foo_frame {
            int x;
        } locals = { 
            .x = 999
        };

        // Set `r10` to our function's local frame
        asm volatile("mov{q}\t{%0, %%r10|r10, %0}" : : "r"(&locals) : "r10")
        
        bar();
    }
That's pretty ugly but we can write a few macros to implement it more tersely - we can use this to have efficient closures in C without requiring an executable stack. (There's also `__builtin_call_with_static_chain`, but I've found it more troublesome to use than the manual way).

Demo: https://godbolt.org/z/cM9d8e1r5

For other registers which are part of the regular calling convention, we might be able to clobber them if they wouldn't normally be used for the call. Eg, if our function takes regular 2 arguments, they would be in `rdi` and `rsi` - so we could use `rdx`, `rcx`, `r8`, `r9` like the above, but if our function took 6 or more regular arguments we wouldn't be able to use any of these in this way. If we wanted a custom calling convention we could just make all functions have zero-arguments and perform all of the setting and capturing ourself - which gives us more control than using [[musttail]] - though less portable, and may prevent optimizations the compiler could otherwise make.

reply
> What practical patterns are enabled by TCO in C?

It's important in interpreters. Here's an example: https://blog.reverberate.org/2021/04/21/musttail-efficient-i...

reply
It is simple to convert factorial to tail recursive form. In lua, which has tco:

    local factorial do
      local function impl(n, acc)
        if n == 1 then
          return acc
        else
          return impl(n - 1, acc * n)
        end
      end
      factorial = function(n)
        if n < 0 then
          error("factorial input is negative")
        elseif n <= 1 then
          return 1
        else
          return impl(n - 1, n)
        end
      end
    end
You could replace impl with an imperative loop:

    local acc = 1
    repeat
      acc = acc * n
      n = n - 1
    until n == 1
    return acc
Personally, I find this ugly compared to the tail recursive solution. The loop version only seems more natural if you primarily think in loops. Tail recursion is strictly more powerful than looping since every imperative loop can trivially be converted to a tail recursive function, but the reverse is not true.
reply