Graduated @inline directives: arity=N, accessor forms, and a directives file - #254
Merged
Conversation
Contributor
There was a problem hiding this comment.
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 +
defaultmasking) and precedence resolution across header/file/export sources. - Add
--directivesCLI plumbing and golden-harness support for per-goldendirectives.txtfixtures. - 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. |
…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
force-pushed
the
issue-232/inline-directives
branch
from
July 12, 2026 14:10
ba1fad2 to
307e660
Compare
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
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #232.
What
The
@inlinepragma grammar grows from binary always/never to the full directive DSL from the issue:arity=Ninlines 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.defaultexplicitly 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 exportpragmas 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.hsparses both sources intoPragma/Directivesvalues and resolves precedence in one pureresolveModes;AnnotationgainsArity Natural. Sinceexportis itself a valid binding name, the parser backtracks:@inline export alwaysnames a binding calledexport.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.InlinePolicy(never / arity / field / applied-field, keyed byQName, 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$wworker names the policy no longer matches), and pasted byinlineSaturatedCallexactly at qualifying sites, bypassing the size budget. Field policies gateresolveDictionaryProp(never vetoes, always bypasses the budget) and a newinlineAnnotatedProjectionrule handles the arity-gated and applied-field forms.sinkProjectionIntoLetmoves a projection through theletresidue beta reduction leaves behind.reduceKnownCtorRefRead, the through-a-reference companion ofreduceKnownConstructor, folds constructor-eliminating reads through a saturated application of a reference to a top-level constructor binding, so(Op f).value0resolves tofwithout 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.--directivesin the CLI (parse errors fail the build with a proper Megaparsec report), threaded throughcompileModules; the golden harness picks up an optional committeddirectives.txtnext to a golden module's files, so the file input is exercised end-to-end.Also fixed along the way: an
@inlineannotation on a binding whose right-hand side is a bare application, variable reference, or record update was silently dropped during translation, so@inline foo neveron a point-free definition had no effect.Verification
Matches the issue's checklist. A combinator marked
arity=2inlines 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: pureresolveModestests, translation-level tests, and theGolden.DirectivesFilegolden 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-endGolden.DirectiveAritygolden collapsesrunOp (Op f) xthrough beta and case-of-known-constructor to a folded constant, with an eval oracle.Golden.DirectiveAccessorcovers 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
--directivesflips exactly the directed binding, unmatched ghost entries are tolerated, and the emitted Lua runs with the expected output.Notes
spago runre-invokes the backend without build-phase flags, so--directives(like every build flag) applies tospago buildoutput, not the--runre-link. Pre-existing gap, documented in the README.