You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
#226 (issue #198) lifted the run half of the uncurried wrappers: a saturated runFn3 add3 1 2 3 now compiles to add3(1, 2, 3). The constructors stayed opaque FFI, and they undo much of the win. add3 = mkFn3 \a b c -> a + b + c still emits as mkFn3(function(a) return function(b) return function(c) ... end end end): the wrapper uncurries the call from the outside but re-curries on the inside via fn(a)(b)(c), so every call of a Fn3 built this way allocates two intermediate closures. The goal is add3 = function(a, b, c) return a + b + c end, with zero closures and the mk* accessors gone from the emitted FFI tables the same way the run* ones went in #226.
Foreign shapes
From the fork FFI (Data.Function.Uncurried, Control.Monad.ST.Uncurried, Effect.Uncurried):
mkFn2..mkFn10 are function(fn) return function(a, …, n) return fn(a)…(n) end end: a multi-parameter function literal wrapping a curried chain;
mkSTFn1..mkSTFn10 and mkEffectFn1..mkEffectFn10 are the same shape with a trailing force: return fn(a)…(n)();
mkFn0 wraps a nullary call fn() and stays opaque, symmetric with runFn0; mkFn1 and runFn1 have no foreign at all (they are PureScript-side id).
Approach
Extend the foreign lifter (#178) with two shapes, mirroring how #198 handled the run half, and allowlist mkFn2..mkFn10, mkSTFn1..mkSTFn10, mkEffectFn1..mkEffectFn10:
A multi-parameter function literal (all parameters named; a vararg declines) lifts to a single n-ary AbsN, not to nested unary Abs. The nested encoding is exactly the misapplication the lifter's current decline comment warns about. The n-ary AbsN codegen already exists (the Program transformation to uncurry functions for which all applications are fully saturated. #24 uncurrying workers emit through it), and the WellApplied linter invariant only constrains literal-lambda heads of AppN, which this translation does not create.
A nullary call fn() in an FFI body is the run of an effect thunk. It lifts to an application of EffectRunArg, so isEffectRun recognises it throughout the IR pipeline and codegen erases the marker back to a plain fn(), the same treatment magic-do's own effect runs get.
With both in place, mkFn3 lifts to \fn -> AbsN [a, b, c] (fn(a)(b)(c)), and a mkFn3 \a b c -> … site beta-reduces layer by layer to a bare n-ary AbsN, i.e. a direct Lua function(a, b, c) definition. Compositions with the run half reduce end to end: runFn3 (mkFn3 f) x y z collapses to f x y z, and mkEffectFn2 (runEffectFn2 h) either reduces to a direct AppN h [a, b] (the marker lands on the thunk's unused parameter and is dropped) or is left as the effect-run redex that the codegen fusion from #226 already collapses.
One guardrail belongs in the allowlist comment: after this change the Effect core (bindE, pureE) becomes technically liftable (their bodies are pure return trees), but it must not be allowlisted. Magic-do recognises bind chains by name, and inline-always lifting of bindE in the optimizer fixpoint (which runs before magic-do) would blind that recognition, which would cost the flat do-chunks and with them the handling of Lua's nesting and locals limits (#19). The broader-allowlist follow-up (#187) needs that warning spelled out.
The cleaner endgame is marker-driven magic-do: normalise every effect run to EffectRunArg before magic-do runs, and build do-chunks from those semantics instead of recognising bindE/pureE by name. That would remove the name-driven fragility the guardrail above works around, but it means redesigning magic-do's recognition and the way chunk boundaries keep generated functions within Lua's limits (#19), so it is tracked separately in #228.
#226 (issue #198) lifted the
runhalf of the uncurried wrappers: a saturatedrunFn3 add3 1 2 3now compiles toadd3(1, 2, 3). The constructors stayed opaque FFI, and they undo much of the win.add3 = mkFn3 \a b c -> a + b + cstill emits asmkFn3(function(a) return function(b) return function(c) ... end end end): the wrapper uncurries the call from the outside but re-curries on the inside viafn(a)(b)(c), so every call of aFn3built this way allocates two intermediate closures. The goal isadd3 = function(a, b, c) return a + b + c end, with zero closures and themk*accessors gone from the emitted FFI tables the same way therun*ones went in #226.Foreign shapes
From the fork FFI (
Data.Function.Uncurried,Control.Monad.ST.Uncurried,Effect.Uncurried):mkFn2..mkFn10arefunction(fn) return function(a, …, n) return fn(a)…(n) end end: a multi-parameter function literal wrapping a curried chain;mkSTFn1..mkSTFn10andmkEffectFn1..mkEffectFn10are the same shape with a trailing force:return fn(a)…(n)();mkFn0wraps a nullary callfn()and stays opaque, symmetric withrunFn0;mkFn1andrunFn1have no foreign at all (they are PureScript-sideid).Approach
Extend the foreign lifter (#178) with two shapes, mirroring how #198 handled the run half, and allowlist
mkFn2..mkFn10,mkSTFn1..mkSTFn10,mkEffectFn1..mkEffectFn10:AbsN, not to nested unaryAbs. The nested encoding is exactly the misapplication the lifter's current decline comment warns about. The n-aryAbsNcodegen already exists (the Program transformation to uncurry functions for which all applications are fully saturated. #24 uncurrying workers emit through it), and theWellAppliedlinter invariant only constrains literal-lambda heads ofAppN, which this translation does not create.fn()in an FFI body is the run of an effect thunk. It lifts to an application ofEffectRunArg, soisEffectRunrecognises it throughout the IR pipeline and codegen erases the marker back to a plainfn(), the same treatment magic-do's own effect runs get.With both in place,
mkFn3lifts to\fn -> AbsN [a, b, c] (fn(a)(b)(c)), and amkFn3 \a b c -> …site beta-reduces layer by layer to a bare n-aryAbsN, i.e. a direct Luafunction(a, b, c)definition. Compositions with the run half reduce end to end:runFn3 (mkFn3 f) x y zcollapses tof x y z, andmkEffectFn2 (runEffectFn2 h)either reduces to a directAppN h [a, b](the marker lands on the thunk's unused parameter and is dropped) or is left as the effect-run redex that the codegen fusion from #226 already collapses.One guardrail belongs in the allowlist comment: after this change the Effect core (
bindE,pureE) becomes technically liftable (their bodies are pure return trees), but it must not be allowlisted. Magic-do recognises bind chains by name, and inline-always lifting ofbindEin the optimizer fixpoint (which runs before magic-do) would blind that recognition, which would cost the flat do-chunks and with them the handling of Lua's nesting and locals limits (#19). The broader-allowlist follow-up (#187) needs that warning spelled out.Future direction (#228)
The cleaner endgame is marker-driven magic-do: normalise every effect run to
EffectRunArgbefore magic-do runs, and build do-chunks from those semantics instead of recognisingbindE/pureEby name. That would remove the name-driven fragility the guardrail above works around, but it means redesigning magic-do's recognition and the way chunk boundaries keep generated functions within Lua's limits (#19), so it is tracked separately in #228.