Skip to content

Lift the *.Uncurried mk wrappers to n-ary function definitions (#227) - #229

Merged
Unisay merged 2 commits into
mainfrom
issue-227/lift-uncurried-mk-wrappers
Jul 11, 2026
Merged

Lift the *.Uncurried mk wrappers to n-ary function definitions (#227)#229
Unisay merged 2 commits into
mainfrom
issue-227/lift-uncurried-mk-wrappers

Conversation

@Unisay

@Unisay Unisay commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

Closes #227.

#226 lifted the run half of the uncurried wrappers: a saturated runFn3 add3 1 2 3 compiles to add3(1, 2, 3). The constructors stayed opaque FFI, though, and they undid much of the win. add3 = mkFn3 \a b c -> a + b + c still emitted as mkFn3(function(a) return function(b) return function(c) … end end end), uncurried on the outside but re-curried on the inside via fn(a)(b)(c), so every call of a Fn3 built this way allocated two intermediate closures. This PR lifts the mk half. The definition now compiles to function(a, b, c) return a + b + c end with no closures at all, and the mk* accessors disappear from the emitted FFI tables the same way the run* 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-ary AbsN, 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 an AbsN would silently miscompile it. A nullary call fn() (the trailing effect run of the mk{ST,Effect}FnN wrappers) lifts to an application of the EffectRunArg marker, 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. mkFn2mkFn10, mkSTFn1mkSTFn10, and mkEffectFn1mkEffectFn10 join the allowlist. runFn0/mkFn0 stay off it by policy, not liftability: their bodies force a pure Fn0 with 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:

-- before:
M.Golden_UncurriedLift_Test_add3 = M.Data_Function_Uncurried_foreign.mkFn3(function( a )
  return function(b) return function(c) return a + b + c end end
end)

-- after:
M.Golden_UncurriedLift_Test_add3 = function(a, b, c)
  return a + b + c
end
M.Golden_UncurriedLift_Test_mulByFn = function(a, b)
  return a * b
end

An effectful body whose thunk head is not a literal erases the marker into a direct force:

M.Golden_UncurriedLift_Test_sumST = function(a, b)
  return M.Control_Monad_ST_Internal_applicativeST.pure(a + b)()
end

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, and Control.Monad.ST.Uncurried foreign 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). The UncurriedLift golden gains the mulByFn case, 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. Full cabal test all is green (718 examples), the spec suite was repeated on three fresh Hedgehog seeds, luacheck reports no errors, and the bench FNEW census is unchanged (the bench modules use no mk* wrappers).

Unisay added 2 commits July 11, 2026 12:37
…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.
@Unisay
Unisay requested a review from Copilot July 11, 2026 10:56
@Unisay Unisay self-assigned this Jul 11, 2026
@Unisay
Unisay marked this pull request as ready for review July 11, 2026 10:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ForeignLift to lift multi-parameter Lua function literals into a single IR AbsN, with guards against varargs and duplicate parameter names.
  • Lift Lua nullary calls fn() into an application using the EffectRunArg marker (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, and mkEffectFn1..10, and update unit + golden tests (including an added point-free mkFn2 mul case).

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.

@Unisay
Unisay merged commit 72f88a5 into main Jul 11, 2026
3 checks passed
@Unisay
Unisay deleted the issue-227/lift-uncurried-mk-wrappers branch July 11, 2026 11:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Lift the *.Uncurried mk wrappers to n-ary function definitions

2 participants