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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,3 +208,11 @@ target. Foreign bindings accept only whole-binding `always`/`never`/`default`
(their implementation is opaque to the optimizer). Note that `spago run`
re-invokes the backend without build-phase flags, so `--directives` (like all
build flags) applies to `spago build` output, not to the `--run` re-link.

Specializations of an `arity=N` target need no directives of their own: a
top-level binding that applies the target to `k` arguments — a hand-written
partial application, or the binding the PureScript compiler's CSE floats for
a repeated dictionary application — inherits `arity=(N-k)` while
under-applied and `always` once saturated, transitively through chains of
such bindings. An explicit directive on the specialization overrides the
derived one.
13 changes: 13 additions & 0 deletions changelog.d/20260727_200000_unisay_derived_inline_directives.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
### Added

- Inline directives are now derived for specialization bindings (#241). A
top-level binding whose settled shape applies a directive-carrying
combinator to some of its arguments — the shape purs's common-subexpression
pass floats for repeated dictionary applications (`bind = Control.Bind.bind
bindStateT`), and the shape of a hand-written partial application — needs
no pragma of its own: with the combinator under `@inline f arity=N` and the
binding applying `k` arguments, the binding inherits `arity=(N-k)` when
under-applied and always-inline when saturated (`k >= N`), transitively
through chains of such bindings. A specialized combinator then inlines at
its qualifying call sites with no hand-written annotation, while explicit
directives on the specialization keep full precedence.
375 changes: 239 additions & 136 deletions lib/Language/PureScript/Backend/IR/Optimizer.hs

Large diffs are not rendered by default.

175 changes: 175 additions & 0 deletions test/Language/PureScript/Backend/IR/Optimizer/Spec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -3149,6 +3149,181 @@ spec = describe "IR Optimizer" do
)
]

describe "derives directives for specializations (issue #241)" do
let mainModule = moduleNameFromString "Main"
extern = moduleNameFromString "Extern"
g = refImported extern (Name "g")
checked = either (fail . show) pure . optimizedUberModuleChecked mempty
namesOf uber =
[ name
| Standalone (QName _ (Name name), _) ←
Linker.uberModuleBindings uber
]
-- g a (g b (g c (g a …))) — an opaque chain nothing folds, far
-- over 'inlineSizeBudget', so only a directive can paste the
-- bindings built from it.
combineBody a b c =
foldr
(application . application g)
c
(concat (replicate 12 [a, b, c]))
combineName = QName mainModule (Name "combine")
combineRef = refImported mainModule (Name "combine")
combineDef =
abstraction (paramNamed (Name "a")) $
abstraction (paramNamed (Name "b")) $
abstraction (paramNamed (Name "c")) $
combineBody
(refLocal (Name "a"))
(refLocal (Name "b"))
(refLocal (Name "c"))

it "marks a specialization saturated at the directed arity always" do
-- spec = combine 1 2 — applied to exactly the directed arity, so
-- every use of spec stands for a qualifying call of combine: the
-- derived always-inline directive pastes it at both sites with no
-- annotation of its own, and the folds collapse them.
let specName = QName mainModule (Name "spec")
specRef = refImported mainModule (Name "spec")
specDef =
application
(application combineRef (literalInt 1))
(literalInt 2)
optimized ←
checked
Linker.UberModule
{ uberModuleForeigns = []
, uberModuleBindings =
[ Standalone (combineName, setAnn (Just (Arity 2)) combineDef)
, Standalone (specName, specDef)
]
, uberModuleExports =
[ (Name "main1", application specRef (literalInt 7))
, (Name "main2", application specRef (literalInt 8))
]
}
namesOf optimized `shouldBe` []
Linker.uberModuleExports optimized
`shouldBe` [
( Name "main1"
, combineBody (literalInt 1) (literalInt 2) (literalInt 7)
)
,
( Name "main2"
, combineBody (literalInt 1) (literalInt 2) (literalInt 8)
)
]

it "gives an under-applied specialization the decremented arity" do
-- partial = combine 1 — one argument short of the directed
-- arity: a site applying one more argument reconstructs a
-- qualifying combine call and pastes, while a bare use keeps the
-- shared binding pinned.
let partialName = QName mainModule (Name "partial")
partialRef = refImported mainModule (Name "partial")
partialDef = application combineRef (literalInt 1)
optimized ←
checked
Linker.UberModule
{ uberModuleForeigns = []
, uberModuleBindings =
[ Standalone (combineName, setAnn (Just (Arity 2)) combineDef)
, Standalone (partialName, partialDef)
]
, uberModuleExports =
[
( Name "main1"
, application
(application partialRef (literalInt 5))
(literalInt 6)
)
, (Name "main2", partialRef)
]
}
namesOf optimized `shouldBe` ["combine", "partial"]
Linker.uberModuleExports optimized
`shouldBe` [
( Name "main1"
, combineBody (literalInt 1) (literalInt 5) (literalInt 6)
)
, (Name "main2", partialRef)
]

it "derives transitively through a chain of specializations" do
-- outer = combine 1 inherits arity=2; inner = outer 2 inherits
-- arity=1 from the derived entry, not an explicit one — the
-- chain resolves in one pass because a binding may only
-- reference earlier bindings.
let outerName = QName mainModule (Name "outer")
outerRef = refImported mainModule (Name "outer")
innerName = QName mainModule (Name "inner")
innerRef = refImported mainModule (Name "inner")
optimized ←
checked
Linker.UberModule
{ uberModuleForeigns = []
, uberModuleBindings =
[ Standalone (combineName, setAnn (Just (Arity 3)) combineDef)
, Standalone
(outerName, application combineRef (literalInt 1))
, Standalone (innerName, application outerRef (literalInt 2))
]
, uberModuleExports =
[
( Name "main1"
, application
(application outerRef (literalInt 7))
(literalInt 8)
)
, (Name "main2", application innerRef (literalInt 9))
, -- The second inner site keeps the binding multi-use,
-- so the settle phase cannot dissolve it before the
-- derivation reads its shape.
(Name "main3", application innerRef (literalInt 10))
]
}
namesOf optimized `shouldBe` []
Linker.uberModuleExports optimized
`shouldBe` [
( Name "main1"
, combineBody (literalInt 1) (literalInt 7) (literalInt 8)
)
,
( Name "main2"
, combineBody (literalInt 1) (literalInt 2) (literalInt 9)
)
,
( Name "main3"
, combineBody (literalInt 1) (literalInt 2) (literalInt 10)
)
]

it "never overrides an explicit directive on a specialization" do
-- The explicit never on the specialization wins over the arity
-- the shape would otherwise derive: no site pastes.
let partialName = QName mainModule (Name "partial")
partialRef = refImported mainModule (Name "partial")
partialDef =
setAnn (Just Never) (application combineRef (literalInt 1))
site x y =
application
(application partialRef (literalInt x))
(literalInt y)
optimized ←
checked
Linker.UberModule
{ uberModuleForeigns = []
, uberModuleBindings =
[ Standalone (combineName, setAnn (Just (Arity 2)) combineDef)
, Standalone (partialName, partialDef)
]
, uberModuleExports =
[(Name "main1", site 5 6), (Name "main2", site 7 8)]
}
namesOf optimized `shouldBe` ["combine", "partial"]
Linker.uberModuleExports optimized
`shouldBe` [(Name "main1", site 5 6), (Name "main2", site 7 8)]

describe "keeps foreign module tables hoisted (issue #175)" do
-- A foreign module's value table must stay a single shared binding:
-- its export values (some of which are Lua table constructors with
Expand Down
1 change: 1 addition & 0 deletions test/ps/output/Golden.DirectiveDerived.Test/corefn.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
actual.txt
4 changes: 4 additions & 0 deletions test/ps/output/Golden.DirectiveDerived.Test/eval/golden.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-140
-40
-660
-246
Loading