Skip to content

Lower self-recursive tail calls of uncurried workers to while loops (loopification) #181

Description

@Unisay

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    OptimisationA Compiler Optimisationarea: codegenLua code generation / printing

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions