Problem
A self-recursive tail call stays a call. PUC Lua has proper TCO (return f(...) reuses the frame), so this is purely a constant factor: CALL/RET machinery and argument shuffling on every iteration. Under LuaJIT tail recursion does trace, but a real while true do is the canonically better shape — a stable loop marker, loop-invariant hoisting, and the cached locals from #174 stage 1 get hoisted out of the loop. Recursive go helpers are the dominant iteration form in generated code (span, takeFallback, the fold fallbacks).
Approach
After #24: a worker whose every self-call AppN f$w [e₁…eₙ] sits in tail position (tail-position analysis over the Abs body spine through Let and IfThenElse) lowers to while true with parameter reassignment — Lua's multiple assignment a, b = e₁, e₂ gives simultaneity for free. Implemented at lowering in the Lua codegen, not as an IR node: the expression IR needs no loop constructor (the same choice magicDo makes for statements; purs does its TCO→while the same way when generating JS). Mutual and non-tail recursion are out of scope — that is what the tailRecM layer of the libraries is for.
The transform is behaviour-preserving: PUC's existing TCO already guarantees O(1) stack for these shapes, so it changes constants only.
Prerequisites / Relations
Depends on #24 (uncurried workers), and therefore transitively on #179, which supplies #24 its n-ary call node. The dependency is essential, not incidental: a curried self-call go = λi. … go(x) is a tail call only of the innermost lambda, so the "loop" is smeared across two entry levels and cannot be expressed as one while without uncurrying — a worker with real arity makes the tail position syntactically honest. The cached-local hoisting benefit comes from #174 stage 1.
Verification / Measurement
Measured by #172 — add a tail-recursive countdown microbenchmark, tracking the PUC constant factor and the LuaJIT trace shape separately. Under LuaJIT the payoff is qualitative, not just constant: the curried Data.Array.foldl driver is ~85x slower and blacklists the JIT trace, and loopification makes the loop trace-compilable. The one observable behavioural difference is the shape of error tracebacks (acceptable); everything else is identical.
Problem
A self-recursive tail call stays a call. PUC Lua has proper TCO (
return f(...)reuses the frame), so this is purely a constant factor: CALL/RET machinery and argument shuffling on every iteration. Under LuaJIT tail recursion does trace, but a realwhile true dois the canonically better shape — a stable loop marker, loop-invariant hoisting, and the cached locals from #174 stage 1 get hoisted out of the loop. Recursivegohelpers are the dominant iteration form in generated code (span,takeFallback, the fold fallbacks).Approach
After #24: a worker whose every self-call
AppN f$w [e₁…eₙ]sits in tail position (tail-position analysis over the Abs body spine through Let and IfThenElse) lowers towhile truewith parameter reassignment — Lua's multiple assignmenta, b = e₁, e₂gives simultaneity for free. Implemented at lowering in the Lua codegen, not as an IR node: the expression IR needs no loop constructor (the same choice magicDo makes for statements; purs does its TCO→while the same way when generating JS). Mutual and non-tail recursion are out of scope — that is what thetailRecMlayer of the libraries is for.The transform is behaviour-preserving: PUC's existing TCO already guarantees O(1) stack for these shapes, so it changes constants only.
Prerequisites / Relations
Depends on #24 (uncurried workers), and therefore transitively on #179, which supplies #24 its n-ary call node. The dependency is essential, not incidental: a curried self-call
go = λi. … go(x)is a tail call only of the innermost lambda, so the "loop" is smeared across two entry levels and cannot be expressed as onewhilewithout uncurrying — a worker with real arity makes the tail position syntactically honest. The cached-local hoisting benefit comes from #174 stage 1.Verification / Measurement
Measured by #172 — add a tail-recursive countdown microbenchmark, tracking the PUC constant factor and the LuaJIT trace shape separately. Under LuaJIT the payoff is qualitative, not just constant: the curried
Data.Array.foldldriver is ~85x slower and blacklists the JIT trace, and loopification makes the loop trace-compilable. The one observable behavioural difference is the shape of error tracebacks (acceptable); everything else is identical.