Tail call optimization
Tail call optimization (TCO) support in JavaScript depends on the VM. JavaScriptCore (Safari, Bun) implements TCO, but V8 (Chrome, Node.js) and SpiderMonkey (Firefox) do not. To ensure portable stack safety, js_of_ocaml optimizes common tail call patterns.
What gets optimized ¶
| Pattern | Optimization |
|---|---|
| Self-recursive tail calls | Compiled to a loop |
| Mutually recursive tail calls | Compiled with a trampoline |
| Other tail calls | Not optimized (may overflow) |
To optimize other tail calls, try --effects=cps or --effects=double-translation. This partially transforms the code to continuation-passing style, but has a performance cost. See effect handlers.
Self-recursive functions ¶
Self-recursive tail calls are compiled into loops:
let rec fact x acc =
if x = 0
then acc
else fact (pred x) (acc * x)Generated JavaScript:
function fact(x$1, acc$1){
var x = x$1, acc = acc$1;
for(;;){
if(0 === x) return acc;
var acc$0 = runtime.caml_mul(acc, x), x$0 = x - 1 | 0;
x = x$0;
acc = acc$0;
}
}Mutually recursive functions ¶
Mutually recursive functions use a trampoline—a loop that catches returned continuations and re-invokes them, avoiding deep call stacks:
let rec even n =
match n with
| 0 -> true
| x -> odd (x - 1)
and odd n =
match n with
| 0 -> false
| x -> even (x - 1)Generated JavaScript:
function even$0(counter, n){
if(0 === n) return 1;
var a = n - 1 | 0;
if(counter >= 50) return caml_trampoline_return(odd$0, [0, a]);
var counter$0 = counter + 1 | 0;
return odd$0(counter$0, a);
}
function even(n){return caml_trampoline(even$0(0, n));}
function odd$0(counter, n){
if(0 === n) return 0;
var a = n - 1 | 0;
if(counter >= 50) return caml_trampoline_return(even$0, [0, a]);
var counter$0 = counter + 1 | 0;
return even$0(counter$0, a);
}
function odd(n){return caml_trampoline(odd$0(0, n));}Note: The generated code doesn't return to the trampoline on every call (controlled by the tc_depth parameter, default 50). This balances stack safety with performance.
Patterns not optimized ¶
These patterns are not optimized and may cause stack overflows with deep recursion:
Tail call through a local function:
let rec f x =
let g delta = f (x - delta) in
if x < 0 then 0
else if x mod 2 = 0 then g 2
else g 1Tail call to a function argument:
let bind x f =
match x with
| None -> None
| Some x -> f x (* f is unknown, can't optimize *)For these patterns, you can try --effects=cps.
Tuning ¶
The trampoline depth can be adjusted with --set tc_depth=N (default: 50). Higher values improve performance but increase stack usage.
See also
- Effect handlers — Full tail call optimization with CPS
- Command-line options — The tc_depth parameter