Skip to content

Lua optimizer rule pushDeclarationsDownTheInnerScope loses sharing and delays evaluation of let-bound values #136

Description

@Unisay

pushDeclarationsDownTheInnerScope (lib/Language/PureScript/Backend/Lua/Optimizer.hs) rewrites

function(a) local t = expensive(a); return function(b) ... t ... end end

into

function(a) return function(b) local t = expensive(a); ... t ... end end

whenever an outer function body consists of local declarations followed by return function ... end. The rule dates back to the initial commit and its motivation is not documented anywhere (the spec only pins the mechanics).

Two consequences:

1. Sharing is lost

Before the rewrite, expensive(a) runs once per partial application; after it, once per call of the inner function. PureScript's strict semantics make let the standard tool for sharing work across calls, so the degradation can be asymptotic:

memoized :: Int -> Int
memoized = let table = buildBigTable 1000 in \n -> lookup table n

compiles to exactly the outer-locals-then-return-function shape this rule matches, and after the rewrite buildBigTable runs on every call of memoized n.

2. Evaluation is delayed, which is observable

In a strict language the let is evaluated when the outer function is applied. Moving the declaration into the inner function delays it until full application, so effects escape their intended timing. The clearest case is an error:

f = \a -> let checked = validate a in \b -> use checked b

f badInput should raise at partial application; after the rewrite the raise happens only when the result is applied to b. Since generated code also uses error for pattern-match failures, this shifts where a crash surfaces.

What the rule actually pessimizes

Disabling the rule and regenerating the goldens shows it was hitting type-class dictionary resolution: 16 golden.lua files change, all in the same direction. Typical diff (MaybeChain, TailRecM2Shadow):

     apply = (function()
+      local bind = M.Control_Bind_bind(M.Effect_monadEffect.Bind1())
       return function(f)
-        local bind = M.Control_Bind_bind(M.Effect_monadEffect.Bind1())
         return function(a)

With the rule on, bind/pure were resolved on every call of the partially applied function; without it, once. All eval goldens (the hand-verified runtime oracle) pass unchanged.

Decision

Found during a backend audit on 2026-07-02.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    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