Problem
betaReduce (lib/Language/PureScript/Backend/IR/Optimizer.hs) rewrites App (Abs (ParamNamed param) body) r by substituting r for every occurrence of param, with no look at either the shape of r or the number of occurrences. In a strict language that duplicates evaluation: (\x -> ... x ... x ...) (g y) becomes ... (g y) ... (g y) ..., and the work of g y now runs once per occurrence instead of once.
The path is live. Redexes with non-trivial arguments show up whenever the inliner puts a lambda into call-head position, which it does for used-once bindings and for @inline always annotations; the more the inliner improves, the more such redexes this rule sees.
This is a performance defect, not a correctness one. Effect and ST computations are nullary thunks in the IR, so evaluating a duplicated argument builds the thunk twice but runs the effect exactly as many times as the body calls it. The remaining evaluation-order edge — an Exception inside the duplicated or dropped argument — follows the policy the pipeline already declares for let-bound values (see the FloatIn module note: discardable and reorderable once nothing observes them).
Approach
Apply GHC's rule: substitute only when the argument is trivial (a Ref or a literal) or the parameter occurs at most once; otherwise rewrite the redex to Let param = r in body and leave the decision to inlineLocalBindings, which already counts occurrences.
Prerequisites / Relations
Optimizer correctness/efficiency bug, independent. Related to #142 and #143: once their occurrence-annotation rework lands, the occurrence check here becomes a field read instead of a traversal.
Verification / Measurement
A redex (\x -> ... x ... x ...) (g y) with a non-trivial argument no longer duplicates g y into every occurrence of the parameter; instead it is let-bound and the argument's work is evaluated once. Trivial arguments (a Ref or literal) and single-occurrence parameters still substitute directly.
Problem
betaReduce(lib/Language/PureScript/Backend/IR/Optimizer.hs) rewritesApp (Abs (ParamNamed param) body) rby substitutingrfor every occurrence ofparam, with no look at either the shape ofror the number of occurrences. In a strict language that duplicates evaluation:(\x -> ... x ... x ...) (g y)becomes... (g y) ... (g y) ..., and the work ofg ynow runs once per occurrence instead of once.The path is live. Redexes with non-trivial arguments show up whenever the inliner puts a lambda into call-head position, which it does for used-once bindings and for
@inline alwaysannotations; the more the inliner improves, the more such redexes this rule sees.This is a performance defect, not a correctness one. Effect and ST computations are nullary thunks in the IR, so evaluating a duplicated argument builds the thunk twice but runs the effect exactly as many times as the body calls it. The remaining evaluation-order edge — an
Exceptioninside the duplicated or dropped argument — follows the policy the pipeline already declares for let-bound values (see the FloatIn module note: discardable and reorderable once nothing observes them).Approach
Apply GHC's rule: substitute only when the argument is trivial (a
Refor a literal) or the parameter occurs at most once; otherwise rewrite the redex toLet param = r in bodyand leave the decision toinlineLocalBindings, which already counts occurrences.Prerequisites / Relations
Optimizer correctness/efficiency bug, independent. Related to #142 and #143: once their occurrence-annotation rework lands, the occurrence check here becomes a field read instead of a traversal.
Verification / Measurement
A redex
(\x -> ... x ... x ...) (g y)with a non-trivial argument no longer duplicatesg yinto every occurrence of the parameter; instead it is let-bound and the argument's work is evaluated once. Trivial arguments (aRefor literal) and single-occurrence parameters still substitute directly.