fix: flatten Effect/ST do blocks (magic-do) to avoid Lua's nesting limit (#46)#105
Open
Unisay wants to merge 2 commits into
Open
fix: flatten Effect/ST do blocks (magic-do) to avoid Lua's nesting limit (#46)#105Unisay wants to merge 2 commits into
Unisay wants to merge 2 commits into
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.
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.