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
14 changes: 14 additions & 0 deletions changelog.d/20260625_201941_unisay_notes_ir_inliner.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
### Changed

- Documented three more IR invariants as GHC-style `Note`s, cited from their
dependent sites: `Note [Inline annotations and inlining heuristics]` (how an
`@inline` pragma travels from a comment through the annotation map and the
expression `ann` to the optimizer's decision, plus the linker's synthesised
`Inline.Always`), `Note [Inliner annotations must all be consumed]` (the
annotation map is a linear resource whose leftovers `runRepM` reports, which
is how a typo'd pragma surfaces), and `Note [Newtype constructors are erased]`
(the three `isNewtype` sites where construction is identity, application
unwraps, and matching skips the constructor). The `Ref` `Index` type now also
points at the existing `Note [Sequential scoping of Let bindings]`, which
already covers the per-name De Bruijn scheme. Comments only; no change to
generated code. Continues #44.
35 changes: 35 additions & 0 deletions lib/Language/PureScript/Backend/IR.hs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,19 @@ instance MonadWriter Any RepM where
(a, f) ← repM
a <$ modify' \ctx → ctx {needsRuntimeLazy = f (needsRuntimeLazy ctx)}

{- Note [Inliner annotations must all be consumed]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The @Map Name Annotation@ in the translation 'Context' is a linear resource.
'parseAnnotations' fills it from the module's @\@inline@ pragmas, keyed by the
binding name each pragma names; 'useAnnotation' removes an entry as it attaches
the annotation to that binding; 'runRepM' then checks the map is empty and
errors with 'UnusedAnnotations' if anything is left over.

The leftover check is how a misspelled or misplaced pragma is reported: an
@\@inline@ whose name matches no top-level binding is never drained, so it
surfaces as an error instead of being silently ignored. See also
Note [Inline annotations and inlining heuristics].
-}
runRepM
∷ Context
→ RepM a
Expand Down Expand Up @@ -108,6 +121,7 @@ mkModule cfnModule contextDataTypes = do
, moduleForeigns
}

-- See Note [Inliner annotations must all be consumed]
parseAnnotations ∷ Cfn.Module Cfn.Ann → Either CoreFnError (Map Name Annotation)
parseAnnotations currentModule =
Cfn.moduleComments currentModule
Expand All @@ -123,6 +137,7 @@ parseAnnotations currentModule =
& first
(CoreFnError (Cfn.moduleName currentModule) . AnnotationParsingError)

-- See Note [Inliner annotations must all be consumed]
useAnnotation ∷ Name → RepM (Maybe Annotation)
useAnnotation name = do
ctx ← get
Expand Down Expand Up @@ -268,6 +283,7 @@ mkConstructor cfnAnn ann properTyName properCtorName fields = do
let tyName = mkTyName properTyName
contextModuleName ← gets (Cfn.moduleName . contextModule)
algTy ← algebraicTy contextModuleName tyName
-- See Note [Newtype constructors are erased]
pure
if isNewtype cfnAnn
then identity
Expand Down Expand Up @@ -315,6 +331,7 @@ mkAbstraction ann i e = Abs ann param <$> makeExpr e
"$__unused" → paramUnused
n → paramNamed (Name n)

-- See Note [Newtype constructors are erased]
mkApplication ∷ CfnExp → CfnExp → RepM Exp
mkApplication e1 e2 =
if isNewtype (Cfn.extractAnn e1)
Expand Down Expand Up @@ -636,6 +653,7 @@ mkBinder matchExp = go mempty
, nestedMatches = mempty
}
Cfn.ConstructorBinder ann qTypeName qCtorName binders →
-- See Note [Newtype constructors are erased]
if isNewtype ann
then case binders of
[binder] → go stepsToFocus binder
Expand Down Expand Up @@ -761,6 +779,23 @@ generateName prefix =
put $ ctx {lastGeneratedNameIndex = lastGeneratedNameIndex + 1}
pure $ prefix <> toText (show lastGeneratedNameIndex)

{- Note [Newtype constructors are erased]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A newtype constructor has no runtime representation: wrapping is the identity.
Three sites implement that single convention and must stay consistent, all
keyed off 'isNewtype':

* Construction is identity: 'mkConstructor' returns 'identity' instead of a
'Ctor' for a newtype, so @N x@ builds just @x@.
* Application unwraps: 'mkApplication' on a newtype constructor emits only
the argument, discarding the constructor application.
* Matching skips the constructor: 'mkBinder' on a newtype
'ConstructorBinder' recurses straight into the single inner binder, with no
tag test.

If these drift apart (say construction erases but matching still tests a tag),
the generated code matches against a value that was never wrapped.
-}
isNewtype ∷ Cfn.Ann → Bool
isNewtype = \case
Just Cfn.IsNewtype → True
Expand Down
23 changes: 23 additions & 0 deletions lib/Language/PureScript/Backend/IR/Inliner.hs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,29 @@ import Text.Megaparsec.Char.Lexer qualified as ML

type Pragma = (Name, Annotation)

{- Note [Inline annotations and inlining heuristics]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
An @\@inline <name> always@ / @\@inline <name> never@ pragma in a module comment
travels to the optimizer's inlining decision through several stages:

1. 'pragmaParser' parses one pragma comment into a 'Pragma' (the bound 'Name'
and an 'Annotation').
2. 'Language.PureScript.Backend.IR.parseAnnotations' collects them into a
@Map Name Annotation@ in the translation context, and 'useAnnotation'
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
Note [Foreign bindings structure emitted by the Linker]).
-}
data Annotation = Always | Never
deriving stock (Show, Eq, Ord)

Expand Down
1 change: 1 addition & 0 deletions lib/Language/PureScript/Backend/IR/Linker.hs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ foreignBindings Module {moduleName, modulePath, moduleForeigns} =
| not (null moduleForeigns)
]

-- See Note [Inline annotations and inlining heuristics]
foreignNamesBindings ∷ [(QName, Exp)] =
moduleForeigns <&> \(_ann, name) →
( QName moduleName name
Expand Down
1 change: 1 addition & 0 deletions lib/Language/PureScript/Backend/IR/Optimizer.hs
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,7 @@ inlineLocalBinding grouping body =
then substitute name 0 inlinee body
else body

-- See Note [Inline annotations and inlining heuristics]
isInlinableExpr ∷ Exp → Bool
isInlinableExpr expr =
hasInlineAnnotation expr || isRef expr || isNonRecursiveLiteral expr
Expand Down
1 change: 1 addition & 0 deletions lib/Language/PureScript/Backend/IR/Types.hs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ instance Monoid Info where
data AlgebraicType = SumType | ProductType
deriving stock (Generic, Eq, Ord, Show, Enum, Bounded)

-- See Note [Sequential scoping of Let bindings] for what this index selects
newtype Index = Index {unIndex ∷ Natural}
deriving newtype (Show, Eq, Ord, Num, Enum, Real, Integral)

Expand Down
Loading