You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 1000in \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
The rule is disabled (removed from rewriteRulesInOrder, definition kept) and stays disabled.
The idiomatic replacement is a float-in pass at the IR level, where usage information is available: sink a Let binding toward its use site (into an IfThenElse branch that is the only user, next to a sole use), but never across a lambda, following the same discipline GHC's FloatIn applies (see Peyton Jones, Partain, Santos, "Let-floating: moving bindings to give faster programs", 1996). That recovers the legitimate part of what this rule may have been after (upvalue-to-local conversion, fewer live upvalues under Lua 5.1's limit of 60 per function) without touching evaluation timing or sharing.
pushDeclarationsDownTheInnerScope(lib/Language/PureScript/Backend/Lua/Optimizer.hs) rewritesinto
whenever an outer function body consists of
localdeclarations followed byreturn 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 makeletthe standard tool for sharing work across calls, so the degradation can be asymptotic:compiles to exactly the outer-locals-then-return-function shape this rule matches, and after the rewrite
buildBigTableruns on every call ofmemoized 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 badInputshould raise at partial application; after the rewrite the raise happens only when the result is applied tob. Since generated code also useserrorfor 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.luafiles change, all in the same direction. Typical diff (MaybeChain,TailRecM2Shadow):With the rule on,
bind/purewere resolved on every call of the partially applied function; without it, once. All eval goldens (the hand-verified runtime oracle) pass unchanged.Decision
rewriteRulesInOrder, definition kept) and stays disabled.Letbinding toward its use site (into anIfThenElsebranch that is the only user, next to a sole use), but never across a lambda, following the same discipline GHC's FloatIn applies (see Peyton Jones, Partain, Santos, "Let-floating: moving bindings to give faster programs", 1996). That recovers the legitimate part of what this rule may have been after (upvalue-to-local conversion, fewer live upvalues under Lua 5.1's limit of 60 per function) without touching evaluation timing or sharing.Found during a backend audit on 2026-07-02.