Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bench/goldens/fnew_Bench.CurriedStep.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ function-body FNEW sites:
Bench.CurriedStep.lua:3
Bench.CurriedStep.lua:7
Bench.CurriedStep.lua:10
Bench.CurriedStep.lua:21
Bench.CurriedStep.lua:23
2 changes: 1 addition & 1 deletion bench/goldens/trace_curried_step.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ aborts (distinct site -- reason):
bytecode end state (J*=compiled, I*=blacklisted):
Bench.CurriedStep.lua:10 IFUNCF
Bench.CurriedStep.lua:10 JFUNCF
Bench.CurriedStep.lua:15 IFUNCF
Bench.CurriedStep.lua:16 ILOOP
Bench.CurriedStep.lua:3 IFUNCF
Bench.CurriedStep.lua:3 JFUNCF
Bench.CurriedStep.lua:7 IFUNCF
Expand Down
15 changes: 15 additions & 0 deletions changelog.d/20260707_220000_unisay_loopification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
### Added

- Self-recursive tail calls lower to `while true do` loops with parameter
reassignment (#181): a recursive binding — an uncurried worker or a plain
unary function, top-level or `let`-bound — whose tail position self-calls
becomes a loop, with `return go$w(e₁, e₂)` turning into the simultaneous
multiple assignment `p₁, p₂ = e₁, e₂`. Non-tail self-calls and other exits
are untouched; a body that captures a parameter inside a nested closure
(e.g. a CPS-style accumulator) is left recursive, since reassignment would
corrupt the captured environment. PUC Lua already runs these shapes in
constant stack via tail-call optimization, so the change is a constant
factor there (no per-iteration CALL/RET and argument shuffling); under
LuaJIT the loop is the shape the trace compiler wants, completing the
uncurrying story of #24 for hot recursive workers like `span` and the fold
fallbacks. The only observable difference is the shape of error tracebacks.
30 changes: 21 additions & 9 deletions lib/Language/PureScript/Backend/Lua.hs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import Language.PureScript.Backend.IR.Query (usesRuntimeLazy)
import Language.PureScript.Backend.Lua.Fixture qualified as Fixture
import Language.PureScript.Backend.Lua.Key qualified as Key
import Language.PureScript.Backend.Lua.Linker.Foreign qualified as Foreign
import Language.PureScript.Backend.Lua.Loopify qualified as Loopify
import Language.PureScript.Backend.Lua.Name qualified as Lua
import Language.PureScript.Backend.Lua.Name qualified as Name
import Language.PureScript.Backend.Lua.Types (ParamF (..))
Expand Down Expand Up @@ -87,7 +88,13 @@ fromUberModule foreigns needsRuntimeLazy appOrModule uber = (`evalStateT` 0) do
<$> fromIR foreigns Set.empty modname irExp
pure $ DList.fromList do
(modname, name, exp) ← recBinds
pure $ mkBinding modname (fromName name) exp
-- A self-recursive member references itself through the
-- module-scope table, mirroring the Ref case of 'fromIR'.
let self =
Loopify.SelfField
Fixture.moduleName
(qualifyName modname (fromName name))
pure $ mkBinding modname (fromName name) (Loopify.loopify self exp)

returnExp ←
case appOrModule of
Expand Down Expand Up @@ -271,15 +278,20 @@ fromIR foreigns topLevelNames modname ir = case ir of
then qualifyName modname name
else name
)
assignments ← forM (toList grp) \(_ann, fromName → name, expr) →
assignments ← forM (toList grp) \(_ann, fromName → name, expr) → do
-- The self-reference mirrors the Ref case below: through the
-- module-scope table for a top-level name, plain otherwise.
let (target, self)
| Set.member (qualifyName modname name) topLevelNames =
( qualifyName modname name
, Loopify.SelfField
Fixture.moduleName
(qualifyName modname name)
)
| otherwise = (name, Loopify.SelfLocal name)
goExp expr
<&> Lua.assign
( Lua.VarName
( if Set.member (qualifyName modname name) topLevelNames
then qualifyName modname name
else name
)
)
<&> Lua.assign (Lua.VarName target)
. Loopify.loopify self
pure $ DList.fromList binds <> DList.fromList assignments
pure . Left . DList.toList $
recs <> either DList.fromList (DList.singleton . Lua.return) body
Expand Down
Loading