Skip to content

feat(ir): uncurry the shapes that only appear after magicDo and flattening - #264

Merged
Unisay merged 5 commits into
mainfrom
issue-200/late-uncurry-rerun
Jul 13, 2026
Merged

feat(ir): uncurry the shapes that only appear after magicDo and flattening#264
Unisay merged 5 commits into
mainfrom
issue-200/late-uncurry-rerun

Conversation

@Unisay

@Unisay Unisay commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Closes #200.

The worker/wrapper uncurrying pass (#24) now runs a second time at the end of the IR pipeline, after flattenDeepBinds, followed by a single dce pass. The late run catches the saturated call sites that do not exist at the early run's position: the effect-run spines magicDo completes, and the $kont helpers minted by flattening. The helpers are saturated by construction, so their curried kontN(live)(x) calls become kontN$w(live, x) and the wrappers die in the closing dce pass.

One correction to the issue's premise surfaced while updating goldens. An effect function with two or more real arguments is already split by the early run, because its real-argument spine is saturated before magicDo runs: bindE (f a b) k contains the full f(a)(b). For those functions the late run adds nothing; the thunk stays inside the early worker, and absorbing it there is a possible follow-up. The family the late run actually wins is the unary effect action. f :: A -> Effect B has manifest arity 1, below the split threshold, until magicDo appends the thunk parameter, and the late split absorbs that parameter into the worker: a fully applied effect statement compiles to the single call f$w(a) instead of f(a)() with a thunk allocated per call. The new Golden/UncurryEffect test covers this end to end, with an eval oracle for semantics.

Three pieces of groundwork make the rerun sound:

  • isEffectRun now recognises an effect run by its trailing EffectRunArg instead of only the singleton application. Without this, rewriting f(a)(b)(run) to f$w(a, b, run) would strip the statement's protections and dead code elimination would silently drop the effect. New DCE and inliner tests cover the protections; the Lua codegen already erased trailing markers from n-ary argument lists.
  • The pass is now rerun-safe. A wrapper built by the earlier run (recognised by its delegate-call body) is never split again, since that would mint a duplicate $w binding. It re-registers its arity instead, so saturated sites that appeared between the runs still reach the existing worker. A non-wrapper candidate whose $w name is taken is left alone entirely.
  • The cleanup after the late run is a lone dce pass, not the optimize+dce fixpoint the issue sketched. Optimize's use-once inlining would paste the $kont workers (each called exactly once) back into their call sites one fixpoint round at a time, re-nesting exactly what flattenDeepBinds had just flattened and tripping Lua's parser nesting cap again.

Golden churn is confined to the four Long*Bind modules (the $kont split) plus the new test, and every eval oracle passes unchanged. The full suite is green (899 examples) and the randomized specs hold across five extra seeds. The new bench/macro/effect_step bench records the improved shape and commits baseline counters. Wall-clock on that micro is flat: the step body's ST-ref FFI calls are still curried and dominate the saved call and thunk allocation, which is itself an argument for the follow-ups below.

Two follow-up candidates came out of this work and stayed out of scope: absorbing the thunk parameter into workers the early run already split (the logShow$w(x, true)() shape visible in the Primops golden), and distributing an effect-run application into IfThenElse branches, which an if-terminated effect loop still needs before loopification (#181) can turn it into a while loop.

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 enhances the PureScript→Lua IR optimization pipeline by rerunning the worker/wrapper uncurrying pass late (after flattenDeepBinds) so it can rewrite saturated call shapes that only appear after magicDo and flattening, then performing a final DCE sweep to drop newly-dead wrappers. It also updates effect-run detection so the rewritten n-ary worker calls remain protected as effect statements.

Changes:

  • Rerun the Uncurry worker/wrapper split late in the optimizer pipeline and follow it with a single DCE pass.
  • Extend isEffectRun to recognize effect runs by a trailing EffectRunArg (including in n-ary AppN calls).
  • Add/refresh tests, goldens, and a micro-benchmark covering the late-uncurry shapes.

Reviewed changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
lib/Language/PureScript/Backend/IR/Optimizer.hs Adds a late uncurry pass (uncurry-late) after flattenDeepBinds, followed by a single DCE cleanup pass.
lib/Language/PureScript/Backend/IR/Types.hs Updates isEffectRun to treat any AppN with trailing EffectRunArg as an effect statement.
lib/Language/PureScript/Backend/IR/Uncurry.hs Makes the pass rerun-safe by recognizing previously-generated wrappers and reusing existing workers; tracks rewrite-only cases.
test/Language/PureScript/Backend/IR/Uncurry/Spec.hs Adds coverage for rerunning the pass over already-split bindings and for taken-worker-name scenarios (top-level and local).
test/Language/PureScript/Backend/IR/Optimizer/Spec.hs Adds a regression test ensuring use-once inlining does not inline an n-ary worker effect run (must remain a statement).
test/Language/PureScript/Backend/IR/DCE/Spec.hs Adds a regression test ensuring DCE preserves an unreferenced n-ary worker effect run statement.
test/ps/src/Golden/UncurryEffect/Test.purs New golden test source exercising late-uncurry behavior around magicDo-produced effect-run spines.
test/ps/output/Golden.UncurryEffect.Test/corefn.json New committed CoreFn JSON for the new golden test module.
test/ps/output/Golden.UncurryEffect.Test/golden.ir New IR golden showing late-stage shapes (including uncurried worker forms with trailing run markers).
test/ps/output/Golden.UncurryEffect.Test/golden.lua New Lua golden showing the emitted Lua shape for the new test.
test/ps/output/Golden.UncurryEffect.Test/eval/golden.txt New eval oracle pinning runtime behavior for the new golden test.
test/ps/output/Golden.UncurryEffect.Test/eval/.gitignore Golden eval directory ignore file for actual output.
test/ps/src/Bench/EffectStep.purs New benchmark module targeting the unary-effect-step shape that benefits from the late uncurry rerun.
bench/macro/effect_step.lua New benchmark macro driver for Bench.EffectStep.
bench/goldens/trace_effect_step.txt Baseline LuaJIT trace/abort report for the new benchmark.
bench/goldens/fnew_Bench.EffectStep.txt Baseline LuaJIT FNEW site counts for the new benchmark.
test/ps/output/Golden.LongStateBind.Test/golden.lua Golden churn reflecting $kont helper saturation now rewritten to worker calls.
test/ps/output/Golden.LongMaybeBindModule.Test/golden.lua Golden churn reflecting $kont helper saturation now rewritten to worker calls.
test/ps/output/Golden.LongMaybeBind.Test/golden.lua Golden churn reflecting $kont helper saturation now rewritten to worker calls.
test/ps/output/Golden.LongExceptBind.Test/golden.lua Golden churn reflecting $kont helper saturation now rewritten to worker calls.
changelog.d/20260713_160000_unisay_late_uncurry_rerun.md Changelog fragment describing the late uncurry rerun and effect-run protection changes.

Comment thread test/ps/src/Golden/UncurryEffect/Test.purs Outdated
Comment thread test/ps/src/Golden/UncurryEffect/Test.purs Outdated
@Unisay Unisay self-assigned this Jul 13, 2026
Unisay added a commit that referenced this pull request Jul 13, 2026
- test/ps/src/Golden/UncurryEffect/Test.purs:7 — module haddock now says
  countdown demonstrates the late split while the small tick inlines at
  its statement sites
  (#264 (comment))
- test/ps/src/Golden/UncurryEffect/Test.purs:17 — tick's comment
  describes the inline-before-split outcome instead of claiming a direct
  worker call
  (#264 (comment))

corefn.json regenerated for the shifted source spans; golden.ir/.lua and
the eval oracle are unchanged.
Unisay added 5 commits July 13, 2026 16:28
The late uncurry run (#200) rewrites a saturated effect-run spine
f(a)(b)(EffectRunArg) into the n-ary worker call f$w(a, b, EffectRunArg),
burying the marker in the argument list. isEffectRun previously matched
only the singleton application, so such a statement would lose its
protections: dead-code elimination would drop it with its unreferenced
binder, and local inlining would paste it past its statement position.
Recognise the run by its trailing marker instead — the marker only ever
terminates the spine it runs, so the trailing position is precise.
A second run of the worker/wrapper split meets its own earlier output,
and re-splitting a wrapper would mint a duplicate $w binding — a GUC
violation for locals and a shadowed binding at the top level. Classify
candidates against the earlier run's output: a wrapper delegating to its
own worker re-registers its arity, so saturated sites that appeared
between the runs are still rewritten to the existing worker; a candidate
whose worker name is otherwise taken (call-site inlining can paste a
worker body back into its wrapper) is left alone entirely. The
WasRewritten signal stays precise for the new no-split site rewrites.
Groundwork for the late rerun of #200.
Close the pipeline with a second uncurry run (#200) plus a dce pass. The
late run sees the two saturated families that do not exist at the early
run's position: the effect-run spines magic-do completes — a unary
effect action is below the split threshold until the thunk parameter
lifts it to arity 2, and the worker absorbs that parameter, so a fully
applied effect statement becomes one n-ary call instead of a curried
call plus a thunk allocation and force — and the $kont helpers minted
by flattening, saturated by construction.

The cleanup is a single dce pass, deliberately not an optimize+dce
fixpoint: optimize's use-once inlining would paste the $kont workers
(each called exactly once) back into their call sites round by round,
re-nesting exactly what flattenDeepBinds just flattened.

Golden churn: the four Long*Bind modules pin the $kont workers; the new
UncurryEffect golden pins the absorbed thunk parameter, the direct
worker statement calls, the dce-protected result statements, and the
wrapper surviving for value uses — with an eval oracle for semantics.
A hot ST loop through a unary effect step, always fully applied and
immediately run — the family the late uncurry run (#200) splits. The
step call is now one n-ary worker call per iteration instead of a
curried call returning a freshly allocated thunk plus the call forcing
it; the committed counters are the regression baseline. Wall-clock on
this micro is flat: the step body's ST-ref reads and writes are curried
FFI calls that dominate the saved call and allocation.
- test/ps/src/Golden/UncurryEffect/Test.purs:7 — module haddock now says
  countdown demonstrates the late split while the small tick inlines at
  its statement sites
  (#264 (comment))
- test/ps/src/Golden/UncurryEffect/Test.purs:17 — tick's comment
  describes the inline-before-split outcome instead of claiming a direct
  worker call
  (#264 (comment))

corefn.json regenerated for the shifted source spans; golden.ir/.lua and
the eval oracle are unchanged.
@Unisay
Unisay force-pushed the issue-200/late-uncurry-rerun branch from 9f530e2 to 0fa419e Compare July 13, 2026 14:29
@Unisay
Unisay merged commit 6c2400f into main Jul 13, 2026
2 checks passed
@Unisay
Unisay deleted the issue-200/late-uncurry-rerun branch July 13, 2026 15:24
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.

Uncurry the shapes that only appear after magicDo and flattening

2 participants