Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions changelog.d/20260625_210853_unisay_inline_never_veto.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
### Fixed

- `@inline <name> never` now actually prevents inlining. The annotation was
parsed and stored but never consulted as a veto: both inlining sites decided
with `isInlinableExpr expr || <used once>`, so a `never`-annotated binding
that was a reference, a small literal, or used once was still inlined. The
optimizer now collects the `never`-annotated binding names once up front, so
the veto survives later rewrites that drop the annotation, and refuses to
inline those bindings regardless of the heuristic or the single-use rule
(#131).
21 changes: 12 additions & 9 deletions lib/Language/PureScript/Backend/IR/Inliner.hs
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,18 @@ travels to the optimizer's inlining decision through several stages:
moves each into the annotated binding's 'Ann' as the binding is
translated (see Note [Inliner annotations must all be consumed]).
3. From there the 'Annotation' rides along as the expression's @ann@.
4. 'Language.PureScript.Backend.IR.Optimizer.isInlinableExpr' reads it back
with 'getAnn': @Just Always@ forces inlining. @Just Never@ and @Nothing@
are treated alike -- both leave the heuristic (a ref, a small literal, or
a single-use binding) to decide. So @never@ currently only withholds the
forced case; it does not veto heuristic inlining.

The linker also synthesises @Just Always@ directly: each foreign name is bound
to an 'ObjectProp' marked 'Inline.Always' so the wrapper around the FFI object
is always inlined away (see
4. The optimizer reads it back: @Just Always@ (via
'Language.PureScript.Backend.IR.Optimizer.isInlinableExpr') forces
inlining. For @Just Never@, 'optimizedUberModule' collects the annotated
binding names once up front (so the veto survives later rewrites that drop
the annotation off a binding's root) and refuses to inline them. @Nothing@
leaves the ref / small-literal / single-use heuristic to decide.

Pragmas reach this map only for non-foreign top-level bindings (the ones
'useAnnotation' drains as it translates them). The linker synthesises
@Just Always@ separately and independently of any pragma: each foreign name is
bound to an 'ObjectProp' marked 'Inline.Always' so the wrapper around the FFI
object is always inlined away (see
Note [Foreign bindings structure emitted by the Linker]).
-}
data Annotation = Always | Never
Expand Down
42 changes: 32 additions & 10 deletions lib/Language/PureScript/Backend/IR/Optimizer.hs
Original file line number Diff line number Diff line change
Expand Up @@ -38,27 +38,33 @@ import Language.PureScript.Backend.IR.Types
)

optimizedUberModule ∷ UberModule → UberModule
optimizedUberModule =
idempotently (eliminateDeadCode . optimizeModule)
optimizedUberModule uber =
uber
& idempotently (eliminateDeadCode . optimizeModule neverNames)
-- by merging foreign bindings into the main bindings, we can
-- unblock even more optimizations, e.g. inline foreign bindings.
>>> mergeForeignsIntoBindings
>>> idempotently (eliminateDeadCode . optimizeModule)
& mergeForeignsIntoBindings
& idempotently (eliminateDeadCode . optimizeModule neverNames)
-- Must run last among the index-sensitive passes:
-- see Note [Locals are uniquely named after renameShadowedNames]
>>> renameShadowedNames
& renameShadowedNames
-- Magic-do is the final lowering (issue #46): it relies on the unique
-- naming established above and preserves it, and must run after dead-code
-- elimination so the statements it introduces for `discard` are not
-- dropped as dead. See Language.PureScript.Backend.IR.MagicDo.
>>> magicDo
& magicDo
-- Flatten the remaining deeply-nested expression trees (issues #104, #108):
-- continuation/bind chains of any monad (lambda-lifted into $kont helpers)
-- and applicative/flipped-bind application spines (A-normalised into $tmp
-- locals). Runs after magicDo (which consumes Effect/ST chains, leaving only
-- non-Effect/ST ones) and likewise consumes and preserves the unique naming.
-- See Language.PureScript.Backend.IR.FlattenDeepBinds.
>>> flattenDeepBinds
& flattenDeepBinds
where
-- Collect @inline never bindings once from the pristine module: later
-- rewrites may strip the annotation off a binding's root, so the veto keys
-- off the name (see Note [Inline annotations and inlining heuristics]).
neverNames = neverInlineNames uber

mergeForeignsIntoBindings ∷ UberModule → UberModule
mergeForeignsIntoBindings uberModule@UberModule {..} =
Expand Down Expand Up @@ -230,8 +236,22 @@ idempotently = fix $ \i f a →
-- tr ∷ Show x ⇒ String → x → y → y
-- tr l x y = trace ("\n\n" <> l <> "\n" <> (toString . pShow) x <> "\n") y

optimizeModule ∷ UberModule → UberModule
optimizeModule UberModule {..} =
{- | The top-level bindings annotated @inline never@, collected once from the
pristine module. Later rewrites can drop the annotation off a binding's root
expression (e.g. constant folding replaces it with a fresh node), so the veto
must key off the name rather than re-reading the annotation after optimization.
See Note [Inline annotations and inlining heuristics].
-}
neverInlineNames ∷ UberModule → Set QName
neverInlineNames UberModule {uberModuleBindings} =
Set.fromList
[ qname
| Standalone (qname, expr) ← uberModuleBindings
, getAnn expr == Just Never
]

optimizeModule ∷ Set QName → UberModule → UberModule
optimizeModule neverNames UberModule {..} =
UberModule
{ uberModuleForeigns
, uberModuleBindings = uberModuleBindings'
Expand All @@ -249,7 +269,9 @@ optimizeModule UberModule {..} =
withBinding binding (bindings, exports) =
case binding of
Standalone (qname, optimizedExpression → expr) →
if isInlinableExpr expr || isUsedOnce qname
-- See Note [Inline annotations and inlining heuristics]
if qname `Set.notMember` neverNames
&& (isInlinableExpr expr || isUsedOnce qname)
then
Comment thread
Unisay marked this conversation as resolved.
Comment thread
Unisay marked this conversation as resolved.
( substituteInBindings qname expr bindings
, substituteInExports qname expr exports
Expand Down
25 changes: 25 additions & 0 deletions test/Language/PureScript/Backend/IR/Optimizer/Spec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Data.Map qualified as Map
import Hedgehog (PropertyT, annotateShow, forAll, (===))
import Hedgehog.Gen qualified as Gen
import Language.PureScript.Backend.IR.Gen qualified as Gen
import Language.PureScript.Backend.IR.Inliner (Annotation (Never))
import Language.PureScript.Backend.IR.Linker (LinkMode (..))
import Language.PureScript.Backend.IR.Linker qualified as Linker
import Language.PureScript.Backend.IR.Names
Expand Down Expand Up @@ -165,6 +166,30 @@ spec = describe "IR Optimizer" do
annotateShow original
optimizedExpression original === original

describe "respects @inline never (issue #131)" do
test "keeps a never-annotated top-level binding instead of inlining it" do
let mainModule = moduleNameFromString "Main"
-- foo = (1 == 1) with `@inline never`. Constant folding rewrites the
-- root to `true` (dropping the annotation) and foo is used once, so
-- without a name-based veto it would be inlined away.
fooExp = Eq (Just Never) (literalInt 1) (literalInt 1)
original =
Linker.UberModule
{ uberModuleForeigns = []
, uberModuleBindings =
[Standalone (QName mainModule (Name "foo"), fooExp)]
, uberModuleExports =
[(Name "main", refImported mainModule (Name "foo") 0)]
}
optimized = optimizedUberModule original
fooKept =
[ qn
| Standalone (qn, _) ← Linker.uberModuleBindings optimized
, qn == QName mainModule (Name "foo")
]
annotateShow optimized
fooKept === [QName mainModule (Name "foo")]
Comment thread
Unisay marked this conversation as resolved.

describe "inliner unlocks more optimizations" do
test "constant folding after inlining" do
name ← forAll Gen.name
Expand Down
Loading