AFAICT your example isn't complete which makes it harder to check. I complemented it somewhat and was able to compile it.
Using ILSpy
one sees that the mutual recursion is still in place:
// F#: | ')' -> matchClosing '(' stack str
case ')':
return Program.matchClosing@39('(', stack, str);
// F#: | matched' t (tail s)
return Program.matched'@28(t, s.Tail);
So while it should be technically possible to unpack two mutually tail recursive function into a loop it's not done.
When checking the IL code we see that the the calls are tagged with .tail
// F#: | matchClosing '(' stack str
IL_0083: tail. // Here
IL_0085: call bool Program::matchClosing@39(char, class [FSharp.Core]Microsoft.FSharp.Collections.FSharpList`1<char>, valuetype Program/SubString)
// F#: | matched' t (tail s)
IL_002a: tail. // Here
IL_002c: call bool Program::'matched\'@28'(class [FSharp.Core]Microsoft.FSharp.Collections.FSharpList`1<char>, valuetype Program/SubString)
The .NET Jitter is in release mode kind enough to consider .tail
flag
// As you can see when debugging the code in WinDbg
02410bdf e8fbd3176b call clr!JIT_TailCall (6d58dfdf)
We also see when we debug in WinDbg that the stack don't grow. Unfortunately when looking at clr!JIT_TailCall
it does a fair amount of work meaning while it doesn't consume stack it consumes clock cycles instead like noted here: How to eliminate time spent in JIT_TailCall for functions that are genuinely non-recursive
However in Debug mode (and at least older versions of Mono) .tail
flag is ignored
// As you can see when debugging the code in WinDbg (this is a normal call)
02f619c1 e8c2f4ffff call 02f60e88
We also see when we debug in WinDbg that the stack grow.
So the answer to your question should be:
- No, the F# compiler isn't able to transform the mutually tail recursive calls into a loop.
- However, the F# compiler tags the calls with a
.tail
attribute
- The Release mode JIT:er kindly considers the
.tail
attributes and generates tail calls that don't grow the stack (but are ineffecient)
- In Debug mode (and possibly mono)
.tail
attributes are ignored and no tail calls are generated by the JIT:er and the stack will grow.