fix: flatten Effect/ST do blocks (magic-do) to avoid Lua's nesting limit (#46)#105
Merged
Conversation
…mit (#46) A long straight-line do block desugars to a chain of bind/discard whose continuations nest lexically. Past ~200 levels Lua's parser rejects the chunk ("chunk has too many syntax levels", LUAI_MAXCCALLS), so the file fails to load before any code runs. Add an IR pass (IR.MagicDo) that recognises bind/discard/pure specialised to the Effect or ST monad — whose value is a nullary thunk — and rewrites the chain into a flat statement sequence (local x = m(); ...; return r()). Recognition normalises the application head (resolving module-local aliases, projecting fields out of literal dictionaries, beta-reducing) until the Effect/ST bind instance is exposed, so it sees through the forms the optimizer leaves behind. The flat statements are chunked into nested thunks of <=150 locals to stay under Lua 5.1's other limit, LUAI_MAXVARS (200 locals per function). The pass runs as the final step of optimizedUberModule: after renameShadowedNames (so locals are uniquely named and no De Bruijn shifting is needed) and after dead-code elimination (so the statements introduced for discard are not dropped as dead). Other monads keep their bind calls; the generic deeply-nested case is tracked in #104. Adds the Golden.LongDoBlock regression (300-statement Effect block) and regenerates the Effect/ST goldens. Eval goldens are unchanged, confirming semantics are preserved.
Add docs/QUIRKS.md for users compiling PureScript to Lua: the Lua 5.1 target floor, how PureScript values map onto Lua, how to write FFI (foreign-module shape), the residual long-do-block limit for non-Effect/ST monads (#46/#104), and stack-safety via MonadRec. Cross-reference it from CLAUDE.md's Known Pitfalls.
Documents the second class of Lua 5.1 size limit that magic-do's chunking keeps the output under: ~200 local variables per function (LUAI_MAXVARS) and ~60 upvalues (LUAI_MAXUPVALUES), separate from the parser-nesting cap. Both have bitten the compiler before (#19); flattening Effect/ST do blocks into a chunked statement sequence avoids them. Also records the FFI export-paren requirement: each exported value must be wrapped as `key = (<lua expression>)` or the FFI parser rejects it.
There was a problem hiding this comment.
Pull request overview
Adds a backend IR lowering pass (“magic-do”) that rewrites deeply-nested Effect/ST bind/discard chains into flat Lua statement sequences (with chunking) to avoid Lua 5.1’s parser nesting limit, plus a regression golden and user-facing documentation of Lua-target quirks.
Changes:
- Introduce
Language.PureScript.Backend.IR.MagicDoand run it at the end ofoptimizedUberModule. - Add a new long
Effectdo-block golden regression plus regenerated goldens reflecting the new lowering. - Add
docs/QUIRKS.mdand link it fromCLAUDE.md.
Reviewed changes
Copilot reviewed 29 out of 31 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
pslua.cabal |
Registers the new IR pass module in the library build. |
lib/Language/PureScript/Backend/IR/Optimizer.hs |
Hooks magicDo into the optimizer pipeline after renameShadowedNames. |
lib/Language/PureScript/Backend/IR/MagicDo.hs |
New IR rewrite pass to flatten Effect/ST do-blocks and chunk locals for Lua limits. |
docs/QUIRKS.md |
Adds user-facing documentation of Lua 5.1 constraints and backend-specific gotchas. |
CLAUDE.md |
References the new docs/QUIRKS.md from the “Known Pitfalls” section. |
test/ps/golden/Golden/LongDoBlock/Test.purs |
New PureScript source golden that triggers deep Effect do nesting. |
test/ps/output/Golden.LongDoBlock.Test/golden.lua |
New generated Lua golden showing flattened/chunked sequencing. |
test/ps/output/Golden.LongDoBlock.Test/eval/golden.txt |
New eval golden output (1..300) for the long do-block test. |
test/ps/output/Golden.LongDoBlock.Test/eval/.gitignore |
Ignores eval harness actual.txt for the new golden directory. |
test/ps/output/Golden.TailRecM2Shadow.Test/golden.ir |
Regenerated IR golden reflecting magic-do lowering behavior. |
test/ps/output/Golden.TailRecM2Shadow.Test/golden.lua |
Regenerated Lua golden reflecting magic-do lowering behavior. |
test/ps/output/Golden.StringCodePoints.Test/golden.ir |
Regenerated IR golden reflecting magic-do lowering behavior. |
test/ps/output/Golden.StringCodePoints.Test/golden.lua |
Regenerated Lua golden reflecting magic-do lowering behavior. |
test/ps/output/Golden.ProfunctorDictLens.Test/golden.ir |
Regenerated IR golden reflecting magic-do lowering behavior. |
test/ps/output/Golden.ProfunctorDictLens.Test/golden.lua |
Regenerated Lua golden reflecting magic-do lowering behavior. |
test/ps/output/Golden.MaybeChain.Test/golden.ir |
Regenerated IR golden reflecting magic-do lowering behavior. |
test/ps/output/Golden.MaybeChain.Test/golden.lua |
Regenerated Lua golden reflecting magic-do lowering behavior. |
test/ps/output/Golden.GenericEqTwoTypes.Test/golden.ir |
Regenerated IR golden reflecting magic-do lowering behavior. |
test/ps/output/Golden.GenericEqTwoTypes.Test/golden.lua |
Regenerated Lua golden reflecting magic-do lowering behavior. |
test/ps/output/Golden.DerivedFunctor.Test/golden.ir |
Regenerated IR golden reflecting magic-do lowering behavior. |
test/ps/output/Golden.DerivedFunctor.Test/golden.lua |
Regenerated Lua golden reflecting magic-do lowering behavior. |
test/ps/output/Golden.CharLiterals.Test/golden.ir |
Regenerated IR golden reflecting magic-do lowering behavior. |
test/ps/output/Golden.CharLiterals.Test/golden.lua |
Regenerated Lua golden reflecting magic-do lowering behavior. |
test/ps/output/Golden.BugListGenericEq.Test/golden.ir |
Regenerated IR golden reflecting magic-do lowering behavior. |
test/ps/output/Golden.BugListGenericEq.Test/golden.lua |
Regenerated Lua golden reflecting magic-do lowering behavior. |
test/ps/output/Golden.ArrayPatternMatch.Test/golden.ir |
Regenerated IR golden reflecting magic-do lowering behavior. |
test/ps/output/Golden.ArrayPatternMatch.Test/golden.lua |
Regenerated Lua golden reflecting magic-do lowering behavior. |
test/ps/output/Golden.ArrayOfUnits.Test/golden.ir |
Regenerated IR golden reflecting magic-do lowering behavior. |
test/ps/output/Golden.ArrayOfUnits.Test/golden.lua |
Regenerated Lua golden reflecting magic-do lowering behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Rewrite extractAnn/modifyAnn/extractBinderAnn as \case over their final argument and normalise the data-type Haddock to fourmolu's canonical style.
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.
Fixes #46.
Problem
A long straight-line
doblock desugars to a chain ofbind/discardwhose continuations nest lexically. Past ~200 levels Lua's parser rejects the chunk withchunk has too many syntax levels(LUAI_MAXCCALLS), so the file fails to load — before any code runs. The prelude test suites hit this in practice.Approach
A new IR pass
Language.PureScript.Backend.IR.MagicDorecognisesbind/discard/purespecialised to the Effect or ST monad — whose runtime value is a nullary thunk — and rewrites the nested chain into a flat statement sequence:This mirrors the magic-do pass of the upstream JS backend and
purs-backend-es, but is realised as a rewrite into existingLet/Absconstructs rather than a new IR node (which would ripple through everyRawExptraversal, including the De Bruijn machinery behind #37/#56).Key points:
bindinstance is exposed. The optimizer inlinesdiscardtodiscardUnit.discard = bind, so the chain head is rarely a bareControl.Bind.bind; normalisation sees through that.LUAI_MAXVARS(200 locals/function). The statements are split into nested thunks of ≤150 locals, so both the nesting limit and the local-variable limit stay satisfied.optimizedUberModule: afterrenameShadowedNames(locals are uniquely named, so moving binders into aLetneeds no De Bruijn shifting) and after dead-code elimination (so thelocal _ =statements introduced fordiscardare not dropped as dead). Folding it into the single pipeline definition means both the compiler and the golden-test harness pick it up — no divergence.bindcalls (theirbindis not "run a thunk"). A long straight-linedoinMaybe/Either/State/… can still hit the parser limit; the generic, monad-agnostic fallback is tracked in Generic fallback: flatten deeply-nested do-blocks for non-Effect/ST monads #104.Testing
Golden.LongDoBlockregression — a 300-statement Effect block that previously failed to load now evaluates and prints1..300.eval/golden.txtpreserved, onlygolden.ir/golden.luaregenerated), confirming semantics are preserved — includingMaybe(MaybeChain) andtailRecM(TailRecM2Shadow), which are deliberately not flattened.237 examples, 0 failures(unit + property + golden + eval + luacheck).Docs
Adds
docs/QUIRKS.md(user-facing Lua-target guide) and notes the residual non-Effect/ST limit there.