Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 31 additions & 20 deletions docs/QUIRKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,29 +79,40 @@ return {
}
```

## Very long `do` blocks in non-`Effect`/`ST` monads
## Very long `do` blocks

A straight-line `do` block compiles to a chain of `bind`/`discard` whose
continuations nest lexically, and Lua's parser caps how deeply expressions may
nest (~200 levels). A long enough chain fails to **load** (before any code
runs) with:

```
lua: yourfile.lua:NNN: chunk has too many syntax levels
```

`Effect` and `ST` blocks are exempt: the compiler flattens them into a plain
statement sequence, so they have no practical length limit. The limit only
bites **other** monads — `Maybe`, `Either`, `State`, a custom parser/decoder,
or a large applicative (`ado`) constructor — and only at ~200+ straight-line
statements in a single block. That is rare outside machine-generated code and
very wide record decoders. Recursion does **not** trigger it (a recursive
function is a normal call, not nested).

**If you hit it:** split the block into smaller named pieces (extract
sub-computations into separate functions and sequence them), or break a wide
record decode into chunks. A general (monad-agnostic) fix is tracked in
[#104](https://github.com/purescript-lua/purescript-lua/issues/104).
nest (~200 levels). A long enough chain would fail to **load** (before any code
runs) with `chunk has too many syntax levels`.

The compiler now flattens these so they have no practical length limit:

- **`Effect` and `ST`** blocks are lowered to a plain statement sequence
(magic-do).
- **Any other monad** — `Maybe`, `Either`, `State`, a custom parser/decoder —
has its `bind`/`>>=` chain **lambda-lifted**: long chains are split into
segments and each segment's continuation becomes a small named helper, so the
generated nesting stays flat regardless of chain length
([#104](https://github.com/purescript-lua/purescript-lua/issues/104)).

A few related shapes are **not** flattened yet and can still hit the limit at
~200+ levels in a single expression:

- **Applicative chains** built with `ado`/`apply` (`<*>`) or `bindFlipped`
(`=<<`), rather than `do`/`bind`.
- A `bind` chain that forwards more than ~15 distinct earlier-bound variables
through a single cut: the lambda-lifter **bails** (a segment's helpers carry
those forwarded variables *plus* the segment's own binders as upvalues, which
would approach Lua 5.1's 60-upvalue cap, see below) and leaves it nested.
- Deep nesting from non-`bind` constructs: a giant `case` tree, a long string
`<>` concatenation, or a very wide array/record literal.

When one of these would overflow, the compiler now **rejects it with a clear
error** (`Expression nests too deeply for Lua 5.1 …`) instead of emitting a Lua
file that no interpreter can load. **If you hit it:** split the expression into
smaller named pieces (extract sub-computations into separate functions and
sequence them), or break a wide literal/decode into chunks.

## Lua's per-function size limits (locals & upvalues)

Expand Down
12 changes: 12 additions & 0 deletions exe/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,15 @@ handleLuaError =
<> runModuleName modname
<> "."
<> runIdent ident
Lua.NestingTooDeep depth →
die . toString . unlines $
[ "Expression nests too deeply for Lua 5.1 ("
<> show depth
<> " syntax levels; the parser caps at ~200)."
, "A long do/>>= chain in a non-Effect/ST monad is the usual cause, but"
, "applicative (ado/apply) chains, large case trees, and very wide"
, "literals can hit it too. Split the expression into smaller named"
, "pieces. See"
, "https://github.com/purescript-lua/purescript-lua/issues/104 and"
, "https://github.com/purescript-lua/purescript-lua/issues/108"
]
8 changes: 7 additions & 1 deletion lib/Language/PureScript/Backend.hs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Language.PureScript.Backend.IR qualified as IR
import Language.PureScript.Backend.IR.Linker qualified as Linker
import Language.PureScript.Backend.IR.Optimizer (optimizedUberModule)
import Language.PureScript.Backend.Lua qualified as Lua
import Language.PureScript.Backend.Lua.NestingCheck (exceedsNestingLimit)
import Language.PureScript.Backend.Lua.Optimizer (optimizeChunk)
import Language.PureScript.Backend.Lua.Types qualified as Lua
import Language.PureScript.Backend.Types (AppOrModule (..), entryPointModule)
Expand Down Expand Up @@ -43,7 +44,12 @@ compileModules outputDir foreignDir appOrModule = do
& optimizedUberModule
let needsRuntimeLazy = Tagged (any untag needsRuntimeLazys)
chunk ← Lua.fromUberModule foreignDir needsRuntimeLazy appOrModule uberModule
pure CompilationResult {lua = optimizeChunk chunk, ir = uberModule}
let optimizedChunk = optimizeChunk chunk
-- Safety net: reject a chunk that nests too deeply for Lua 5.1's parser
-- rather than emit Lua that cannot be loaded (issue #104). Catches whatever
-- 'flattenDeepBinds' bailed on, plus not-yet-flattened deep constructs.
whenJust (exceedsNestingLimit optimizedChunk) (Oops.throw . Lua.NestingTooDeep)
pure CompilationResult {lua = optimizedChunk, ir = uberModule}

linkerMode ∷ AppOrModule → Linker.LinkMode
linkerMode = \case
Expand Down
Loading
Loading