Browse documentation

0160 - Concise arrow bodies make proper tail calls, and closed numeric frames give way to them

Date: 2026-10-26 Area: compiler, bytecode VM Related: ADR 0101, ADR 0127, Bytecode VM

Context

The compiler marked a call in tail position with CALL_FLAG_TAIL only in a return statement's value. A concise arrow body compiled its expression as an ordinary value, so (n) => n === 0 ? x : f(n - 1) stacked a frame per call and failed with RangeError after the default 2,200 nested calls, while the same function with { return …; } ran at any depth. ES2026 §15.10.1 IsInTailPosition treats a ConciseBody like a FunctionBody, and §15.10.2 HasCallInTailPosition reaches its expression through the same rules as a return.

ADR 0127 lets the collector skip the registers of closed numeric frames because no other frame runs above one: a closed numeric arrow calls only itself, and its concise body compiled those self-calls as OP_CALL_SELF_NUM. It noted that proper tail calls in concise bodies would break that, and would have to bring the marking back. A self-call in tail position becomes a generic tail call, which sets up a frame; from inside a closed numeric frame it set one up above it.

Decision

A concise arrow body compiles its expression with the tail-position rules a return uses, under the same conditions: strict code, not async. A conditional in tail position compiles its condition as any conditional does, so the fused compare-and-jump instructions are kept.

An arrow whose concise body has a self-call in tail position gets no closed numeric frames. Its numeric proof still holds, so its parameters keep their Number type and the numeric immediate instructions, but its non-tail self-calls are ordinary calls. The collector keeps skipping closed numeric frames, and the invariant ADR 0127 rests on still holds.

Consequences

Recursion through a tail call in a concise arrow body no longer depends on the stack limit, as in a block body. Infinite recursion written that way, such as const f = () => f(), now runs until a timeout or instruction limit rather than throwing RangeError; tests that need the overflow call from a block body.

A numeric self-recursion that mixes tail and non-tail self-calls loses the scalar frame for its non-tail calls. One with only non-tail self-calls, such as Fibonacci, keeps it.