Skip to content

Graduated @inline directives: arity=N, accessor forms, and a directives file - #254

Merged
Unisay merged 5 commits into
mainfrom
issue-232/inline-directives
Jul 12, 2026
Merged

Graduated @inline directives: arity=N, accessor forms, and a directives file#254
Unisay merged 5 commits into
mainfrom
issue-232/inline-directives

Conversation

@Unisay

@Unisay Unisay commented Jul 12, 2026

Copy link
Copy Markdown
Collaborator

Closes #232.

What

The @inline pragma grammar grows from binary always/never to the full directive DSL from the issue:

@inline [export] name[.label|...label] (default | never | always | arity=N)   -- module header
Some.Module.name[.label|...label] (default | never | always | arity=N)        -- a --directives file line

arity=N inlines a binding only at call sites applying at least N arguments and pins it as a shared reference everywhere else, so a curried combinator specializes at saturated sites without losing its shared definition. The accessor forms attach a policy to one field of a dictionary record (.label) or to a field of the record a binding returns when applied (...label), instead of the whole record. default explicitly resets a target to the built-in heuristics, masking weaker sources. The new --directives <file> option supplies fully-qualified directives project-wide.

Sources layer by specificity, per target: a local module-header pragma beats the directives file, which beats @inline export pragmas shipped by the defining module, which beat the heuristics. Header pragmas stay strictly validated (a target matching nothing is still an error). Unmatched file entries are ignored so a shared file can cover optional dependencies, which is the property #242 needs.

How

  • IR/Inliner.hs parses both sources into Pragma/Directives values and resolves precedence in one pure resolveModes; Annotation gains Arity Natural. Since export is itself a valid binding name, the parser backtracks: @inline export always names a binding called export.
  • Translation (IR.hs) resolves the three sources per module and attaches winners to ann slots: whole-binding winners on the binding root, accessor winners on the object-literal field they select (validated against the binding's shape). The linear-resource leftover check now exempts file-origin targets. Directives on foreign names accept whole-binding always/never/default only.
  • The optimizer generalizes the up-front never-set into an InlinePolicy (never / arity / field / applied-field, keyed by QName, collected once from the pristine uber-module). Arity names are vetoed for whole-binding inlining and for the uncurry split (which would rewrite their call sites to $w worker names the policy no longer matches), and pasted by inlineSaturatedCall exactly at qualifying sites, bypassing the size budget. Field policies gate resolveDictionaryProp (never vetoes, always bypasses the budget) and a new inlineAnnotatedProjection rule handles the arity-gated and applied-field forms.
  • Two rules complete the cascade a directive-driven paste starts. sinkProjectionIntoLet moves a projection through the let residue beta reduction leaves behind. reduceKnownCtorRefRead, the through-a-reference companion of reduceKnownConstructor, folds constructor-eliminating reads through a saturated application of a reference to a top-level constructor binding, so (Op f).value0 resolves to f without pasting the constructor. The sink rule alone deepens the existing specialize pass: the LongReaderBind and LongWriterBind goldens shrink as Identity/WriterT dictionary towers now resolve fully, with byte-identical eval output.
  • Plumbing: --directives in the CLI (parse errors fail the build with a proper Megaparsec report), threaded through compileModules; the golden harness picks up an optional committed directives.txt next to a golden module's files, so the file input is exercised end-to-end.

Also fixed along the way: an @inline annotation on a binding whose right-hand side is a bare application, variable reference, or record update was silently dropped during translation, so @inline foo never on a point-free definition had no effect.

Verification

Matches the issue's checklist. A combinator marked arity=2 inlines at a two-argument site and stays a reference at a one-argument site (plus at-least-N, budget-bypass, non-lambda paste, and no-uncurry-split unit tests). Precedence is covered at three levels: pure resolveModes tests, translation-level tests, and the Golden.DirectivesFile golden with a committed fixture where the file's always beats an exported never while a local never beats the file's always. The end-to-end Golden.DirectiveArity golden collapses runOp (Op f) x through beta and case-of-known-constructor to a folded constant, with an eval oracle. Golden.DirectiveAccessor covers both accessor forms, including an over-budget dictionary constructor that only the directive can resolve.

Beyond the suite (803 examples green), the flag was verified differentially on the real CLI: compiling the same entry with and without --directives flips exactly the directed binding, unmatched ghost entries are tolerated, and the emitted Lua runs with the expected output.

Notes

@Unisay
Unisay requested a review from Copilot July 12, 2026 14:04
@Unisay Unisay self-assigned this Jul 12, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends pslua’s IR inlining controls from binary @inline always|never into a richer directive DSL (default|never|always|arity=N plus .label / ...label accessors) and adds a project-wide --directives <file> input. The change fits into the compiler pipeline by parsing/merging directive sources during CoreFn→IR translation, then enforcing the resolved policies during IR optimization (including new projection/cascade rules), with end-to-end coverage via golden fixtures and new unit/property tests.

Changes:

  • Implement graduated inlining directives (arity-gated inlining + field/applied-field accessors + default masking) and precedence resolution across header/file/export sources.
  • Add --directives CLI plumbing and golden-harness support for per-golden directives.txt fixtures.
  • Extend optimizer rewrite rules to honor the new policy model and deepen projection/case-of-known-constructor cascades (reflected in updated goldens).

Reviewed changes

Copilot reviewed 36 out of 37 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
test/ps/src/Golden/DirectivesFile/Test.purs New PS source to exercise directives-file vs exported/local precedence.
test/ps/src/Golden/DirectivesFile/M1.purs Defines bindings with exported/local pragmas used by the precedence golden.
test/ps/src/Golden/DirectiveArity/Test.purs New PS source exercising arity=N end-to-end with an eval oracle.
test/ps/src/Golden/DirectiveAccessor/Test.purs New PS source exercising .label and ...label accessor forms with an eval oracle.
test/ps/output/Golden.LongWriterBind.Test/golden.lua Updated generated Lua golden reflecting deeper specialization/projection folding.
test/ps/output/Golden.LongReaderBind.Test/golden.lua Updated generated Lua golden reflecting deeper specialization/projection folding.
test/ps/output/Golden.LongReaderBind.Test/golden.ir Updated generated IR golden reflecting deeper specialization/projection folding.
test/ps/output/Golden.DirectivesFile.Test/golden.lua New generated Lua golden for directives-file precedence fixture.
test/ps/output/Golden.DirectivesFile.Test/golden.ir New generated IR golden for directives-file precedence fixture.
test/ps/output/Golden.DirectivesFile.Test/directives.txt New directives-file fixture committed alongside the golden.
test/ps/output/Golden.DirectivesFile.Test/corefn.json New CoreFn output for the new golden module.
test/ps/output/Golden.DirectivesFile.M1/golden.lua New generated Lua golden for the helper module.
test/ps/output/Golden.DirectivesFile.M1/golden.ir New generated IR golden for the helper module.
test/ps/output/Golden.DirectivesFile.M1/corefn.json New CoreFn output for the helper module.
test/ps/output/Golden.DirectiveArity.Test/golden.lua New generated Lua golden showing arity-driven collapse.
test/ps/output/Golden.DirectiveArity.Test/golden.ir New generated IR golden showing arity annotation attachment and effects.
test/ps/output/Golden.DirectiveArity.Test/eval/golden.txt New eval oracle for the arity golden.
test/ps/output/Golden.DirectiveArity.Test/eval/.gitignore Adds ignored actual output for eval mode.
test/ps/output/Golden.DirectiveArity.Test/corefn.json New CoreFn output for the arity golden module.
test/ps/output/Golden.DirectiveAccessor.Test/golden.lua New generated Lua golden showing accessor-directed projection resolution.
test/ps/output/Golden.DirectiveAccessor.Test/golden.ir New generated IR golden showing field annotations on object literals.
test/ps/output/Golden.DirectiveAccessor.Test/eval/golden.txt New eval oracle for the accessor golden.
test/ps/output/Golden.DirectiveAccessor.Test/eval/.gitignore Adds ignored actual output for eval mode.
test/ps/output/Golden.DirectiveAccessor.Test/corefn.json New CoreFn output for the accessor golden module.
test/Language/PureScript/Backend/Lua/Golden/Spec.hs Golden harness now reads optional directives.txt fixtures and passes directives into IR.mkModule.
test/Language/PureScript/Backend/IR/Spec.hs New translation-level tests for directive attachment, precedence, and foreign/directives-file behavior.
test/Language/PureScript/Backend/IR/Optimizer/Spec.hs Adds optimizer tests for arity policies, accessor policies, and ctor-read folding through references.
test/Language/PureScript/Backend/IR/Inliner/Spec.hs Expands inliner parser/precedence tests for the new DSL and directives file format.
README.md Documents graduated directives and the new --directives CLI option.
lib/Language/PureScript/Backend/IR/Optimizer.hs Introduces InlinePolicy, arity-gated inlining, accessor-policy rules, and new projection/case folding rules.
lib/Language/PureScript/Backend/IR/Inliner.hs Implements directive types, parsing for header/file formats, and precedence resolution.
lib/Language/PureScript/Backend/IR.hs Threads directives into module translation, attaches winners to binding roots/fields, and updates error reporting.
lib/Language/PureScript/Backend.hs Threads Directives through compileModules into IR.mkModule.
exe/Main.hs Parses --directives file with Megaparsec and passes directives into compilation.
exe/Cli.hs Adds --directives option and help text.
changelog.d/20260712_154500_unisay_graduated_inline_directives.md Changelog entry for the new directive system and related optimizer improvements.

Comment thread lib/Language/PureScript/Backend/IR.hs
Comment thread lib/Language/PureScript/Backend/IR.hs
Unisay added 4 commits July 12, 2026 16:10
…232)

Extend the pragma grammar to [export] name[.label|...label]
(default|never|always|arity=N), add a directives-file parser with
fully-qualified targets, and resolve the three explicit sources by
precedence (local header > directives file > exported header) at
translation time. Whole-binding annotations attach to binding roots
(including App/Var/ObjectUpdate roots, which previously dropped them);
accessor annotations attach to the object-literal field they select.
Header pragmas stay strict; unmatched directives-file entries are
ignored so shared files may cover modules absent from a build.
…#232)

Generalize the up-front never-name collection into an InlinePolicy record
(never/arity/field/applied-field, keyed by QName) threaded through the
pipeline. An arity=N binding is vetoed for whole-binding inlining and the
uncurry split, and pasted by the call-site inliner exactly at sites
applying at least N arguments — bypassing the size budget and the
manifest-lambda requirement, since the directive is an explicit user
override.
…#232)

Gate resolveDictionaryProp by .label field policies (never vetoes,
always bypasses the size budget), add inlineAnnotatedProjection for the
arity-gated .label form and the ...label forms (pasting the record
constructor under the projection), and sink projections through lets so
the paste's beta-residue keeps folding. The sink rule alone deepens the
existing specialize cascade: the LongReaderBind/LongWriterBind goldens
shrink as Identity/WriterT dictionary towers now resolve through the
lets their construction leaves behind (eval oracles unchanged).
…#232)

Add the --directives CLI option (parsed up front, threaded through
compileModules into per-module resolution) and an optional committed
directives.txt fixture next to a golden module's files so the harness
exercises the same input. Fold constructor-eliminating reads through a
saturated application of a reference to a top-level constructor binding
(reduceKnownCtorRefRead) — the missing link that lets a directive-driven
paste finish the beta/case-of-known-constructor cascade. New goldens
cover the arity=1 collapse (with eval oracle), file-beats-export and
local-beats-file precedence, and both accessor forms; README documents
the DSL and the refreshed --help output.
@Unisay
Unisay force-pushed the issue-232/inline-directives branch from ba1fad2 to 307e660 Compare July 12, 2026 14:10
@Unisay
Unisay marked this pull request as ready for review July 12, 2026 14:10
- lib/Language/PureScript/Backend/IR.hs:216 — treat a directives-file
  arity on a foreign binding as non-applicable (ignored) like every
  other file entry that does not apply, instead of erroring; module-
  header pragmas stay strict
  (#254 (comment))
- test/Language/PureScript/Backend/IR/Spec.hs — regression test for the
  lenient path
@Unisay
Unisay merged commit a58ae92 into main Jul 12, 2026
2 checks passed
@Unisay
Unisay deleted the issue-232/inline-directives branch July 12, 2026 14:40
Unisay added a commit that referenced this pull request Jul 12, 2026
#254 (graduated @inline directives) merged to main after this branch was
written and added Golden.DirectiveArity/DirectiveAccessor plus touched
Golden.LongWriterBind's codegen. Rebasing this branch onto it exposes
shareForeignAccessors to foreign accessors used at two or more sites in
those goldens (showIntImpl, log, concatArray) for the first time, which
now materialize as promoted shared locals instead of repeated field
reads — the same effect already accepted for the other 40+9 goldens in
the first commit, just triggered by the new base. eval/golden.txt is
byte-identical for all three tests; only golden.ir/golden.lua move.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Graduated @inline directives: arity=N, accessor forms, and a directives file

2 participants