Skip to content

Codegen: 'Unexpected bound reference Local b index 1' compiling Data.Array.foldRecM (index inflated by optimizer) #56

@Unisay

Description

@Unisay

Summary

pslua aborts during Lua code generation with

Unexpected bound reference: Ref Nothing (Local (Name "b")) 1 in module Data.Array

when compiling Data.Array. The crash is raised by the Lua backend (Lua.UnexpectedRefBound, see lib/Language/PureScript/Backend/Lua.hs:214) and is reported to the user via exe/Main.hs:105. This blocks compiling the purescript-lua-arrays port entirely (spago build succeeds, but pslua --entry Data.Array fails).

This is a pslua code-generator bug, not a fork bug: the offending function is stock upstream purescript-arrays code, and the bad reference is produced inside pslua's own IR optimizer, not present in the CoreFn that purs emits.

Triggering construct

The single declaration Data.Array.foldRecM is sufficient to trigger the crash. Verified by reducing the module's CoreFn to that one declaration: pslua still aborts with the identical error.

foldRecM :: forall m a b. MonadRec m => (b -> a -> m b) -> b -> Array a -> m b
foldRecM f b array = tailRecM2 go b 0
  where
  go res i
    | i >= length array = pure (Done res)
    | otherwise = do
        res' <- f res (unsafePartial (unsafeIndex array i))
        pure (Loop { a: res', b: i + 1 })

The shape that matters:

  1. A top-level Abs binder named b (the second parameter of foldRecM).
  2. A typeclass-dictionary Let group that purs floats around the lambdas (the MonadRec/Monad/Bind/Applicative accessors — tailRecM2, bind1, pure1, Monad0), which pslua's inlineLocalBindings later inlines because each is used once.
  3. A single reference to that b binder, in the body tailRecM2 go b 0.

There is exactly one binder named b and exactly one reference to it in the whole declaration. The record literal { a: res', b: i + 1 } uses b only as a record label, not as a binder, so there is no source-level shadowing of b at all. Therefore the only correct De Bruijn index for that reference is 0.

Root cause

By the time the Lua backend runs, the reference to b carries index 1 instead of 0. The Lua backend (fromIR, Ref case) only accepts local references at index 0 — see Note [Locals are uniquely named after renameShadowedNames] in lib/Language/PureScript/Backend/IR/Optimizer.hs. The renaming pass renameShadowedNames only rewrites a local reference when its scope map holds a rename entry at the matching index (lib/Language/PureScript/Backend/IR/Optimizer.hs:136-142); for b there is only one binder, so the scope has no entry at index 1, the reference is left untouched, and the backend then rejects it.

The index 1 is introduced during IR optimization, not by purs. The likely mechanism is the inlining of the single-use dictionary Let bindings (inlineLocalBindings -> inlineLocalBinding -> substitute, lib/Language/PureScript/Backend/IR/Optimizer.hs:381-396). substitute descends through the enclosing Abs b and, on the way, applies shift 1 b 0 to the replacement / threads the index through binders (lib/Language/PureScript/Backend/IR/Types.hs:668-704). shift increments the index of every Local "b" reference at or above minIndex (lib/Language/PureScript/Backend/IR/Types.hs:752-757). When this bookkeeping runs through the dictionary-Let + nested-Abs structure that purs produces for foldRecM, the one legitimate b reference (which should stay at index 0, bound by the enclosing Abs b) gets bumped to index 1, so it ends up pointing past its only binder. This is the same class of bug as #37 (a sibling/over-eager index leaving a local reference unbound after optimization), reached here through the dictionary-binding inlining path rather than DCE.

I have intentionally not pinned the exact faulty line; the symptom (index inflated from 0 to 1 with only one binder in scope) is reproduced and isolated, and the fix should restore the invariant that renameShadowedNames relies on.

Impact

  • Severity: high for the array port. Data.Array cannot be compiled to Lua at all, which blocks purescript-lua-arrays and any downstream package depending on Data.Array.
  • foldRecM is ordinary upstream code; any module that combines a constrained polymorphic function (typeclass dictionary args, which purs lowers to floated Let bindings) with a where/do body that references one of the outer parameters can hit the same index-inflation path. So this is not specific to Data.Array.

Reproduction

Toolchain: purs 0.15.15, pslua at Unisay/purescript-lua@6fdc70c (current master).

In a checkout of purescript-lua-arrays (the master branch) after spago build -u '-g corefn':

pslua --foreign-path . --ps-output output --entry Data.Array --lua-output-file dist/Data.Array.lua
# => Unexpected bound reference: Ref Nothing (Local (Name "b")) 1 in module Data.Array

Minimal isolation (reduce output/Data.Array/corefn.json to only the foldRecM declaration, keep everything else identical) still reproduces the exact same error, confirming foldRecM is the sole trigger.

How it was found

Triaging a failing nix develop -c ./scripts/build in the purescript-lua-arrays fork. Bisected at the CoreFn declaration level: of the three top-level declarations in Data.Array that reference a local named b (foldRecM, foldM, filterA), removing only foldRecM from the CoreFn makes the error disappear; keeping foldRecM as the lone declaration reproduces it.

Metadata

Metadata

Assignees

Labels

No labels
No labels

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