Problem
An ST/Effect loop stays a call into a foreign higher-order combinator. Effect.foreachE arr f, forE lo hi f, whileE cond body and their ST twins compile to a foreign function handed a Lua closure that it calls once per iteration: one closure allocation and one call per step, on top of the combinator's own call. magicDo flattens the surrounding bind-chain but never touches the loop: the body stays a lambda passed to a foreign iterator.
Lowered at codegen time they become native loops:
foreachE arr f → for i = 1, #arr do <body> end with f's parameter bound to arr[i]
forE lo hi f → for i = lo, hi - 1 do <body> end
whileE cond body → while cond() do <body> end
so both the per-iteration closure and the foreign call disappear. This is the largest untapped constant-factor gap, and it is exactly the imperative shape hot code uses (Data.Array.ST, VDom-style traversals).
A grep of lib/ confirms pslua has no lowering for any of these combinators today.
Approach
A codegen-time, name-keyed native-inline table matched to pslua's target. Match a saturated application of a known loop combinator by qualified name and spine shape, and emit the corresponding Lua statement instead of the call, inlining the body lambda as the loop body with its parameter as the loop variable:
- ranged
forE lo hi \i -> ... → for i = lo, hi - 1 do ... end (PureScript forE is the half-open [lo, hi), so the numeric for runs to hi - 1);
foreachE arr \x -> ... → for i = 1, #arr do ... end with x bound to arr[i], the shape the foreign implementation itself uses;
whileE \_ -> cond \_ -> body → while cond do ... end.
The menu is portable; the emission is Lua-specific. Fire only on the Effect/ST dictionaries (the gate magicDo already uses), never on a polymorphic foreachE whose dictionary is unresolved.
Prerequisites / Relations
Independent to land. Relates to the magicDo work: #182 (recognise Effect/ST by qualified name) and #228 (marker-driven do-chunks) both make the effect core easier to match structurally, and #182's own design record names "compiling forE/whileE/tailRecM over Effect into native loops" as a revisit trigger. This is that work. Composes with #239 (ref-cell unboxing): a native loop mutating an unboxed local is the end shape. Orthogonal to loopification (#181), which builds loops from tail recursion rather than from combinators.
Verification / Measurement
A foreachE/forE body compiles to a Lua for with no per-iteration closure and no foreign call in golden.lua, and a matching eval golden proves identical runtime output (iteration count, side-effect order). A LuaJIT FNEW-census drop on the loop body through the #172 harness. A non-Effect/ST use of the same combinator names is left as a call, the soundness guard, tested directly.
Problem
An
ST/Effectloop stays a call into a foreign higher-order combinator.Effect.foreachE arr f,forE lo hi f,whileE cond bodyand theirSTtwins compile to a foreign function handed a Lua closure that it calls once per iteration: one closure allocation and one call per step, on top of the combinator's own call. magicDo flattens the surrounding bind-chain but never touches the loop: the body stays a lambda passed to a foreign iterator.Lowered at codegen time they become native loops:
foreachE arr f→for i = 1, #arr do <body> endwithf's parameter bound toarr[i]forE lo hi f→for i = lo, hi - 1 do <body> endwhileE cond body→while cond() do <body> endso both the per-iteration closure and the foreign call disappear. This is the largest untapped constant-factor gap, and it is exactly the imperative shape hot code uses (
Data.Array.ST, VDom-style traversals).A grep of
lib/confirms pslua has no lowering for any of these combinators today.Approach
A codegen-time, name-keyed native-inline table matched to pslua's target. Match a saturated application of a known loop combinator by qualified name and spine shape, and emit the corresponding Lua statement instead of the call, inlining the body lambda as the loop body with its parameter as the loop variable:
forE lo hi \i -> ...→for i = lo, hi - 1 do ... end(PureScriptforEis the half-open[lo, hi), so the numericforruns tohi - 1);foreachE arr \x -> ...→for i = 1, #arr do ... endwithxbound toarr[i], the shape the foreign implementation itself uses;whileE \_ -> cond \_ -> body→while cond do ... end.The menu is portable; the emission is Lua-specific. Fire only on the
Effect/STdictionaries (the gate magicDo already uses), never on a polymorphicforeachEwhose dictionary is unresolved.Prerequisites / Relations
Independent to land. Relates to the magicDo work: #182 (recognise Effect/ST by qualified name) and #228 (marker-driven do-chunks) both make the effect core easier to match structurally, and #182's own design record names "compiling forE/whileE/tailRecM over Effect into native loops" as a revisit trigger. This is that work. Composes with #239 (ref-cell unboxing): a native loop mutating an unboxed local is the end shape. Orthogonal to loopification (#181), which builds loops from tail recursion rather than from combinators.
Verification / Measurement
A
foreachE/forEbody compiles to a Luaforwith no per-iteration closure and no foreign call ingolden.lua, and a matching eval golden proves identical runtime output (iteration count, side-effect order). A LuaJIT FNEW-census drop on the loop body through the #172 harness. A non-Effect/STuse of the same combinator names is left as a call, the soundness guard, tested directly.