Lift the *.Uncurried mk wrappers to n-ary function definitions (#227) - #229
Merged
Conversation
…ions (#227) The lifter's two remaining shape gaps close: a multi-parameter function literal now lifts to a single n-ary AbsN (declining varargs and duplicate parameter names, which Lua binds to the last same-named parameter), and a nullary call lifts as an application to the EffectRunArg marker — the shape magic-do emits, erased back to an empty argument list at code generation. mkFn2..10, mkSTFn1..10 and mkEffectFn1..10 join the allowlist as inline-always IR, so a definition like add3 = mkFn3 \a b c -> … beta-reduces to the n-ary literal itself instead of re-currying every call through two closures, and the mk accessors drop out of the emitted FFI tables like the run ones (#198). runFn0/mkFn0 stay absent by policy, not liftability: forcing a pure Fn0 must not be marked an effect run. The allowlist Haddock now also warns future extenders (#187) off the Effect/ST core, whose thunk-shaped bodies became technically liftable with this change: magic-do recognises bind chains by name, so inlining a lifted core during the optimizer fixpoint would blind it. Lifting the core behind a marker magic-do understands is tracked as #228.
#227) The module doc gains the mk half, and a new point-free mulByFn = mkFn2 mul reduces the wrapper's lifted body against a dictionary-resolved intMul rather than a literal lambda. The goldens show the payoff: add3, mul2 and mulByFn become plain n-ary literals, sumST becomes function(a, b) return pure(a + b)() end, and the Data.Function.Uncurried, Effect.Uncurried and Control.Monad.ST.Uncurried foreign tables disappear from the output. The hand-written eval oracle gains the 48 line for the new runFn2 mulByFn 6 8 site.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR extends the Lua foreign lifter so the *.Uncurried constructor (mk*) wrappers become fully transparent to the IR optimizer, allowing definitions like mkFn3 (\a b c -> ...) to collapse to direct n-ary Lua function literals (eliminating per-call closure allocations) and removing the mk* accessors from emitted FFI tables, matching the earlier run* lifting work.
Changes:
- Extend
ForeignLiftto lift multi-parameter Lua function literals into a single IRAbsN, with guards against varargs and duplicate parameter names. - Lift Lua nullary calls
fn()into an application using theEffectRunArgmarker (so they participate in the same effect-run representation as magic-do, and are erased back to()at codegen). - Expand the foreign allowlist to include
mkFn2..10,mkSTFn1..10, andmkEffectFn1..10, and update unit + golden tests (including an added point-freemkFn2 mulcase).
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| lib/Language/PureScript/Backend/Lua/ForeignLift.hs | Adds the two new lift shapes (n-ary AbsN literals and nullary-call → EffectRunArg) and expands the allowlist to include mk* wrappers. |
| test/Language/PureScript/Backend/Lua/ForeignLift/Spec.hs | Adds unit tests covering the new lift cases and new decline cases (vararg, duplicate params, under-applied multi-param head). |
| test/ps/src/Golden/UncurriedLift/Test.purs | Extends the runnable golden to include mk* lifting expectations, adding mulByFn = mkFn2 mul. |
| test/ps/output/Golden.UncurriedLift.Test/golden.lua | Updates golden output showing mk* wrappers eliminated and direct n-ary Lua functions emitted. |
| test/ps/output/Golden.UncurriedLift.Test/golden.ir | Updates golden IR showing mk* wrappers lifted into AbsN and effect-run markers applied where appropriate. |
| test/ps/output/Golden.UncurriedLift.Test/eval/golden.txt | Updates eval oracle to include the new 48 output line. |
| test/ps/output/Golden.UncurriedLift.Test/corefn.json | Refreshes the committed CoreFn JSON to reflect the updated PureScript golden module. |
| changelog.d/20260711_123511_unisay_lift_uncurried_mk_wrappers.md | Adds a changelog fragment documenting the mk* lifting behavior and allowlist expansion. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #227.
#226 lifted the
runhalf of the uncurried wrappers: a saturatedrunFn3 add3 1 2 3compiles toadd3(1, 2, 3). The constructors stayed opaque FFI, though, and they undid much of the win.add3 = mkFn3 \a b c -> a + b + cstill emitted asmkFn3(function(a) return function(b) return function(c) … end end end), uncurried on the outside but re-curried on the inside viafn(a)(b)(c), so every call of aFn3built this way allocated two intermediate closures. This PR lifts themkhalf. The definition now compiles tofunction(a, b, c) return a + b + c endwith no closures at all, and themk*accessors disappear from the emitted FFI tables the same way therun*ones did in #226.Changes
1. Lift the mk wrappers (
ForeignLift). Two shapes join the translatable subset. A multi-parameter function literal lifts to a single n-aryAbsN, provided every parameter is named and the names are pairwise distinct: a vararg declines, and so does a duplicate name, because Lua binds the body's reference to the last same-named parameter and anAbsNwould silently miscompile it. A nullary callfn()(the trailing effect run of themk{ST,Effect}FnNwrappers) lifts to an application of theEffectRunArgmarker, the same shape magic-do emits; codegen erases it back to an empty argument list. The literal-head arity guard from #226 stays exact, with the marker counting as one argument, so a thunk head passes and any wider literal declines.mkFn2…mkFn10,mkSTFn1…mkSTFn10, andmkEffectFn1…mkEffectFn10join the allowlist.runFn0/mkFn0stay off it by policy, not liftability: their bodies force a pureFn0with a nullary call, which must not be marked an effect run.2. Guard the allowlist against lifting the Effect/ST core. With nullary calls translatable, the core (
Effect.bindE/pureE,Control.Monad.ST.Internal.bind_/pure_) becomes technically liftable, and the allowlist Haddock now warns the broader-allowlist follow-up (#187) off it. Magic-do recognises bind chains by name, so an inline-always core would be inlined away during the optimizer fixpoint, which runs before magic-do, and that recognition would go blind: no flat do-chunks, and none of the chunked statement sequences that keep the output under Lua's local-variable limits (#19). Lifting the core behind a marker magic-do understands is tracked in #228.Before / after
Pure definitions collapse to n-ary literals, including the point-free
mulByFn = mkFn2 mul, whose wrapped function arrives through dictionary resolution rather than as a literal lambda:An effectful body whose thunk head is not a literal erases the marker into a direct force:
One shape keeps a residual closure. A do-block body (
logTwice = mkEffectFn2 \a b -> do …) is a statement sequence, and the codegen thunk fusion from #226 only covers single-call bodies, so the run of the magic-do chunk emits as an IIFE inside the n-ary literal: one closure per call, down from the wrapper's two. A tail-position IIFE collapse in the Lua optimizer would finish the job; #230 tracks that as optimizer work independent of the lifter.The
Data.Function.Uncurried,Effect.Uncurried, andControl.Monad.ST.Uncurriedforeign tables are gone from the golden output entirely.Verification
The lifter specs were written first and red: five new positive shapes (
mkFn2,mkEffectFn2,mkSTFn1, a bare multi-parameter literal, a nullary call) plus new declines (a vararg, duplicate parameter names, an under-applied multi-parameter head). TheUncurriedLiftgolden gains themulByFncase, with its hand-written eval oracle line (48) added before any run; the checked golden pipeline lints scoping, uniqueness, and well-appliedness at every pass boundary. Fullcabal test allis green (718 examples), the spec suite was repeated on three fresh Hedgehog seeds,luacheckreports no errors, and the bench FNEW census is unchanged (the bench modules use nomk*wrappers).