Skip to content

Long do blocks generate Lua that exceeds the parser nesting limit #46

@Unisay

Description

@Unisay

Reported as a side note in #32: a long enough do block fails at runtime. The failure is not an actual stack overflow during evaluation, it happens before any code runs. Lua refuses to parse the file: lua: golden.lua:144: chunk has too many syntax levels.

The cause is the shape of the generated code. Each statement of a do block compiles to a bind continuation that is lexically nested inside the previous one:

return M.discard(M.Effect_Console_foreign.log("61"))(function()
  return M.discard(M.Effect_Console_foreign.log("62"))(function()
    return M.discard(M.Effect_Console_foreign.log("63"))(function()
      ...

Lua's parser has a hard recursion limit (LUAI_MAXCCALLS, 200 by default), so a do block with roughly 200 statements produces a chunk that no stock Lua interpreter will load. The repro is mechanical:

main :: Effect Unit
main = do
  log "1"
  log "2"
  -- ... 300 lines total
  log "300"

To make this an eval golden, generate the module and the expected output:

cd test/ps/golden/Golden && mkdir -p LongDoBlock
{ echo 'module Golden.LongDoBlock.Test where'; echo
  echo 'import Prelude'
  echo 'import Effect (Effect)'
  echo 'import Effect.Console (log)'; echo
  echo 'main :: Effect Unit'
  echo 'main = do'
  for i in $(seq 1 300); do echo "  log \"$i\""; done
} > LongDoBlock/Test.purs
mkdir -p ../../output/Golden.LongDoBlock.Test/eval
seq 1 300 > ../../output/Golden.LongDoBlock.Test/eval/golden.txt

This is a different root than the instance dictionary overflow fixed for #32, which is why it gets its own issue. Fixing it means making the generated code lexically flatter, and none of the options are local tweaks:

  • A magic-do style rewrite for Effect, like the upstream JS backend: turn chains of bind/discard over the Effect monad into sequential statements inside one function body. Most effective, but requires recognizing the Effect dictionaries.
  • Hoisting continuation lambdas into named locals. This only helps for discard chains where continuations capture nothing; a x <- m bind captures x, so the continuation cannot leave the enclosing scope, and the nesting stays.
  • Documenting the limit and recommending users split long do blocks. Cheap, but it leaves generated code that a stock interpreter cannot load, and the prelude test suites hit it in practice.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: codegenLua code generation / printingbugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions