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
7 changes: 3 additions & 4 deletions bench/goldens/fnew_Bench.CurriedStep.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
chunk: Bench.CurriedStep.lua
runtime: LuaJIT 2.1.1741730670
main-chunk FNEW: 1
function-body FNEW: 1
total FNEW: 2
prototypes: 3
function-body FNEW: 0
total FNEW: 1
prototypes: 2
function-body FNEW sites:
Bench.CurriedStep.lua:12
9 changes: 9 additions & 0 deletions bench/goldens/fnew_Bench.MutualStep.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
chunk: Bench.MutualStep.lua
runtime: LuaJIT 2.1.1741730670
main-chunk FNEW: 1
function-body FNEW: 2
total FNEW: 3
prototypes: 4
function-body FNEW sites:
Bench.MutualStep.lua:24
Bench.MutualStep.lua:27
7 changes: 3 additions & 4 deletions bench/goldens/fnew_Bench.StateStep.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
chunk: Bench.StateStep.lua
runtime: LuaJIT 2.1.1741730670
main-chunk FNEW: 3
function-body FNEW: 2
total FNEW: 5
prototypes: 6
function-body FNEW: 1
total FNEW: 4
prototypes: 5
function-body FNEW sites:
Bench.StateStep.lua:9
Bench.StateStep.lua:44
2 changes: 1 addition & 1 deletion bench/goldens/tnew_Bench.CurriedStep.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ runtime: LuaJIT 2.1.1741730670
main-chunk TNEW+TDUP: 1
function-body TNEW+TDUP: 0
total TNEW+TDUP: 1
prototypes: 3
prototypes: 2
function-body TNEW+TDUP sites:
7 changes: 7 additions & 0 deletions bench/goldens/tnew_Bench.MutualStep.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
chunk: Bench.MutualStep.lua
runtime: LuaJIT 2.1.1741730670
main-chunk TNEW+TDUP: 1
function-body TNEW+TDUP: 0
total TNEW+TDUP: 1
prototypes: 4
function-body TNEW+TDUP sites:
2 changes: 1 addition & 1 deletion bench/goldens/tnew_Bench.StateStep.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ runtime: LuaJIT 2.1.1741730670
main-chunk TNEW+TDUP: 2
function-body TNEW+TDUP: 0
total TNEW+TDUP: 2
prototypes: 6
prototypes: 5
function-body TNEW+TDUP sites:
3 changes: 1 addition & 2 deletions bench/goldens/trace_curried_step.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ spec: curried_step
runtime: LuaJIT 2.1.1741730670
workload: n=100000 reps=4 result=5000050000
aborts (distinct site -- reason):
Bench.CurriedStep.lua:12 -- NYI: bytecode FNEW
Bench.CurriedStep.lua:6 -- inner loop in root trace
bytecode end state (J*=compiled, I*=blacklisted):
Bench.CurriedStep.lua:5 JLOOP
curried_step.lua:10 JFUNCF
curried_step.lua:12 JFORI
curried_step.lua:12 JFORL
counts: aborts=2 compiled=4 blacklisted=0
counts: aborts=1 compiled=4 blacklisted=0
12 changes: 12 additions & 0 deletions bench/goldens/trace_mutual_step.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
spec: mutual_step
runtime: LuaJIT 2.1.1741730670
workload: n=100000 reps=4 result=5000050000
aborts (distinct site -- reason):
Bench.MutualStep.lua:24 -- NYI: bytecode FNEW
Bench.MutualStep.lua:8 -- inner loop in root trace
bytecode end state (J*=compiled, I*=blacklisted):
Bench.MutualStep.lua:7 JLOOP
mutual_step.lua:11 JFUNCF
mutual_step.lua:13 JFORI
mutual_step.lua:13 JFORL
counts: aborts=2 compiled=4 blacklisted=0
3 changes: 1 addition & 2 deletions bench/goldens/trace_state_step.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ spec: state_step
runtime: LuaJIT 2.1.1741730670
workload: n=100000 reps=4 result=99998
aborts (distinct site -- reason):
Bench.StateStep.lua:44 -- NYI: bytecode FNEW
Bench.StateStep.lua:9 -- NYI: bytecode FNEW
state_step.lua:32 -- NYI: bytecode FNEW
bytecode end state (J*=compiled, I*=blacklisted):
Expand All @@ -13,4 +12,4 @@ bytecode end state (J*=compiled, I*=blacklisted):
state_step.lua:13 JFUNCF
state_step.lua:34 JFORI
state_step.lua:34 JFORL
counts: aborts=3 compiled=4 blacklisted=3
counts: aborts=2 compiled=4 blacklisted=3
16 changes: 16 additions & 0 deletions bench/macro/mutual_step.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-- The mutual twin of curried_step: the same hot accumulator loop, split
-- across two workers tail-calling each other. Dispatched, the pair runs
-- as one while-true loop over a branch selector; undispatched, every
-- iteration pays a real call for the transition.
return {
artifact = "Bench.MutualStep",
n = 100000,
drive = function(mod, n)
return mod.run(n)
end,
ideal = function(n)
local acc = 0
for i = n, 1, -1 do acc = acc + i end
return acc
end,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
### Added

- Loopification now covers mutual recursion and join points (#234), the two
shapes #181 left as calls. The members of a recursive group's mutual
tail-call cycle lower to one `while true do` dispatcher over a branch
selector plus shared argument slots — every transition, sibling or self, is
one simultaneous multiple assignment — while the member bindings survive as
entry wrappers, so non-tail uses, the curried wrappers left in the group by
uncurrying, and external callers keep working unchanged. A chunk-local
helper only ever tail-called from the enclosing body (a `where`-bound `go`,
a shared continuation) loses its function shell entirely: its parameters
hoist as locals, each entry call becomes an assignment falling through into
the helper's body — an already-loopified worker's entries fall straight
into its loop, and chains of such helpers flatten one round at a time.
Under LuaJIT the dispatcher is the shape the trace compiler wants: the new
`mutual_step` macrobenchmark trace-compiles to parity with hand-written Lua
(~21x over the cross-calling shape), and join-point fusion brings
`curried_step`'s per-iteration closure allocations to zero. Under PUC the
dispatcher trades tail-call machinery for a selector test and per-branch
parameter rebinds, a small constant-factor regression on pure transition
loops that substituting slot names for parameters would remove.
137 changes: 110 additions & 27 deletions lib/Language/PureScript/Backend/Lua.hs
Original file line number Diff line number Diff line change
Expand Up @@ -92,15 +92,61 @@ fromUberModule foreigns needsRuntimeLazy appOrModule uber = (`evalStateT` 0) do
recBinds ← forM (toList recGroup) \(IR.QName modname name, irExp) →
(modname,name,) . asExpression
<$> fromIR foreigns Set.empty modname irExp
pure $ DList.fromList do
(modname, name, exp) ← recBinds
-- 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)
-- A recursive member references itself (and its siblings)
-- through the module-scope table, mirroring the Ref case of
-- 'fromIR'.
let memberSelf modname name =
Loopify.SelfField
Fixture.moduleName
(qualifyName modname (fromName name))
-- The members of every mutual tail-call cycle lower to one
-- while-true dispatcher plus entry wrappers (issue #234); see
-- Language.PureScript.Backend.Lua.Loopify.
dispatched ←
forM
( Loopify.planGroupDispatch
[ (memberSelf modname name, exp)
| (modname, name, exp) ← recBinds
]
)
\group → do
selector ← freshName "$sel"
slots ←
replicateM (Loopify.dispatchArity group) (freshName "$a")
let leader = NE.head (Loopify.dispatchMembers group)
-- The leader's Self already carries the qualified
-- module-scope field, so the dispatcher derives its
-- name from it directly.
dispatcherName =
Name.makeSafe $
Name.toText (Loopify.selfName (Loopify.dispatchSelf leader))
<> "$loop"
dispatcherSelf =
Loopify.SelfField Fixture.moduleName dispatcherName
(dispatcherExp, wrappers) =
Loopify.emitDispatchGroup
dispatcherSelf
selector
slots
group
pure ((dispatcherName, dispatcherExp), wrappers)
let wrapperByIndex = concatMap snd dispatched
pure $
DList.fromList
[ Lua.assign
( Lua.VarField
(Lua.ann (Lua.varName Fixture.moduleName))
dispatcherName
)
dispatcherExp
| ((dispatcherName, dispatcherExp), _) ← dispatched
]
<> DList.fromList do
(index, (modname, name, exp)) ← zip [0 ..] recBinds
let luaExp = case List.lookup index wrapperByIndex of
Just wrapper → wrapper
Nothing → Loopify.loopify (memberSelf modname name) exp
pure $ mkBinding modname (fromName name) luaExp

returnExp ←
case appOrModule of
Expand Down Expand Up @@ -137,9 +183,12 @@ mkBinding modname name =
(Lua.ann (Lua.varName Fixture.moduleName))
(qualifyName modname name)

-- Chunks are finalized here and in the 'IR.AbsN' case of 'fromIR', so
-- both fuse the chunk's join points first (issue #234); see
-- Language.PureScript.Backend.Lua.Loopify.
asExpression ∷ Either Lua.Chunk Lua.Exp → Lua.Exp
asExpression = \case
Left chunk → Lua.chunkToExpression chunk
Left chunk → Lua.chunkToExpression (Loopify.joinifyChunk chunk)
Right expr → expr

fromName ∷ HasCallStack ⇒ IR.Name → Lua.Name
Expand Down Expand Up @@ -262,7 +311,7 @@ fromIR foreigns topLevelNames modname ir = case ir of
List.dropWhileEnd isUnusedParam (toList params)
]
pure . Right $ case body of
Left chunk → Lua.functionDef luaParams chunk
Left chunk → Lua.functionDef luaParams (Loopify.joinifyChunk chunk)
Right e → Lua.functionDef luaParams [Lua.return e]
-- Running the literal thunk that a saturated lifted @*.Uncurried@ effect
-- wrapper reduces to — @(\_ -> fn(a, …)) EffectRunArg@ — is just the call
Expand Down Expand Up @@ -341,27 +390,61 @@ fromIR foreigns topLevelNames modname ir = case ir of
IR.Standalone (_ann, name, expr) →
DList.singleton . Lua.local1 (fromName name) <$> goExp expr
IR.RecursiveGroup grp → do
let binds =
toList grp <&> \(_ann, fromName → name, _) →
Lua.local0
( if Set.member (qualifyName modname name) topLevelNames
then qualifyName modname name
else name
)
assignments ← forM (toList grp) \(_ann, fromName → name, expr) → do
compiled ← forM (toList grp) \(_ann, name, expr) → do
luaExp ← goExp expr
-- 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
let luaName = fromName name
(target, self)
| Set.member (qualifyName modname luaName) topLevelNames =
( qualifyName modname luaName
, Loopify.SelfField
Fixture.moduleName
(qualifyName modname name)
(qualifyName modname luaName)
)
| otherwise = (name, Loopify.SelfLocal name)
goExp expr
<&> Lua.assign (Lua.VarName target)
. Loopify.loopify self
| otherwise = (luaName, Loopify.SelfLocal luaName)
pure (name, target, self, luaExp)
-- The members of every mutual tail-call cycle dispatch through
-- a shared local (issue #234), mirroring the top-level case in
-- 'fromUberModule'.
dispatched ←
forM
( Loopify.planGroupDispatch
[(self, luaExp) | (_, _, self, luaExp) ← compiled]
)
\group → do
selector ← freshName "$sel"
slots ←
replicateM (Loopify.dispatchArity group) (freshName "$a")
let leader = NE.head (Loopify.dispatchMembers group)
dispatcherName =
Name.makeSafe $
Name.toText (Loopify.selfName (Loopify.dispatchSelf leader))
<> "$loop"
(dispatcherExp, wrappers) =
Loopify.emitDispatchGroup
(Loopify.SelfLocal dispatcherName)
selector
slots
group
pure ((dispatcherName, dispatcherExp), wrappers)
let wrapperByIndex = concatMap snd dispatched
binds =
[ Lua.local0 dispatcherName
| ((dispatcherName, _), _) ← dispatched
]
<> [Lua.local0 target | (_, target, _, _) ← compiled]
assignments =
[ Lua.assign (Lua.VarName dispatcherName) dispatcherExp
| ((dispatcherName, dispatcherExp), _) ← dispatched
]
<> [ Lua.assign (Lua.VarName target) luaExp'
| (index, (_, target, self, luaExp)) ←
zip [0 ..] compiled
, let luaExp' = case List.lookup index wrapperByIndex of
Just wrapper → wrapper
Nothing → Loopify.loopify self luaExp
]
pure $ DList.fromList binds <> DList.fromList assignments
pure . Left . DList.toList $
recs <> either DList.fromList (DList.singleton . Lua.return) body
Expand Down
Loading