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
38 changes: 38 additions & 0 deletions changelog.d/20260724_190000_unisay_native_loop_combinators.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
### Added

- The Effect/ST loop combinators lower to native Lua loops (#233):
`foreachE arr f` compiles to `for i = 1, #arr do … end`, `forE lo hi f`
to the half-open `for i = lo, hi - 1 do … end`, `whileE cond body` to a
`while` loop, with the ST twins (`Control.Monad.ST.Internal.foreach` /
`for` / `while`) lowered identically. A run previously compiled to a
foreign higher-order combinator handed a closure it called once per
iteration:

```lua
local _ = Effect_foreachE({ 10, 20, 30 })(function(n)
return Effect_Ref_modify_(function(v) return v + n end, sum)
end)()
```

now becomes the loop the foreign implementation ran, with the body
lambda inlined and its parameter as the loop variable — no foreign
call and no per-iteration closure:

```lua
do
local xs = { 10, 20, 30 }
for i = 1, #xs do
local n = xs[i]
Effect_Ref_modify_(function(v) return v + n end, sum)()
end
end
```

Recognition is by qualified name on a saturated application run by
magic-do, so a same-named user combinator in another module — or a
first-class loop combinator passed around unapplied — keeps the
ordinary call. Non-atomic arguments pre-bind to block-scoped locals,
preserving the foreign call's once-per-argument evaluation, and a
thunk body too large to splice under Lua's active-locals cap keeps the
per-iteration call it had (see
`Language.PureScript.Backend.Lua.NativeLoop`).
33 changes: 33 additions & 0 deletions lib/Language/PureScript/Backend/Lua.hs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ 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.NativeLoop qualified as NativeLoop
import Language.PureScript.Backend.Lua.Types (ParamF (..))
import Language.PureScript.Backend.Lua.Types qualified as Lua
import Language.PureScript.Backend.Types (AppOrModule (..))
Expand Down Expand Up @@ -279,6 +280,15 @@ fromIR foreigns topLevelNames modname ir = case ir of
(IR.AbsN _ (IR.ParamUnused _ :| []) body@IR.AppN {})
(IR.EffectRunArg _ :| []) →
Right <$> goExp body
-- The run of a saturated Effect/ST loop-combinator application lowers
-- to the native Lua loop instead of the foreign call (issue #233); see
-- Language.PureScript.Backend.Lua.NativeLoop. The chunk carries no
-- 'Lua.Return' — the run yields no values, exactly like the foreign
-- thunk falling off its end — so every consumer of a 'Left' chunk
-- (spliced statements, a scope-call expression) preserves semantics.
loopRun@IR.AppN {}
| Just loop ← NativeLoop.matchLoopRun loopRun →
Left <$> NativeLoop.lowerLoop go freshName loop
IR.AppN _ann fn args → do
e ← goExp fn
-- See Note [Nullary functions and Prim.undefined]. PS inserts a
Expand Down Expand Up @@ -314,6 +324,20 @@ fromIR foreigns topLevelNames modname ir = case ir of
body ← go bodyExp
recs ←
bindings & foldMapM \case
-- A statement whose RHS is a recognised loop run emits the
-- native loop directly instead of `local x = <scope call>`. The
-- run yields no values, so the binder reads nil either way:
-- `local x` declares exactly that, and the discard binder — never
-- referenced (the 'RefToDiscard' lint pins this) — needs no
-- declaration at all.
IR.Standalone (_ann, name, expr)
| Just loop ← NativeLoop.matchLoopRun expr → do
loopStmts ← NativeLoop.lowerLoop go freshName loop
pure $
DList.fromList loopStmts
<> if name == IR.discardName
then mempty
else DList.singleton (Lua.local0 (fromName name))
IR.Standalone (_ann, name, expr) →
DList.singleton . Lua.local1 (fromName name) <$> goExp expr
IR.RecursiveGroup grp → do
Expand Down Expand Up @@ -430,6 +454,15 @@ fromIR foreigns topLevelNames modname ir = case ir of
goExp ∷ IR.Exp → LuaM e Lua.Exp
goExp = asExpression <<$>> go

{- | Mint a codegen-fresh local name: the prefix plus a counter drawn
from the 'LuaM' supply, mangled by 'Name.makeSafe' (@$i0@ → @_S_i0@).
The prefix must start with @$@ — unreachable from PureScript
identifiers, and none of the IR passes' @$@-prefixed supplies mint
these prefixes — so the name cannot capture or be captured.
-}
freshName ∷ Text → LuaM e Lua.Name
freshName prefix = lift $ state \n → (Name.makeSafe (prefix <> show n), n + 1)

--------------------------------------------------------------------------------
-- Helpers ---------------------------------------------------------------------

Expand Down
Loading