Warning: Reason support is experimental. We are looking for beta-tester and contributors.

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 1

Tail 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