Skip to content

Derive transitive inline directives on specialization bindings - #324

Merged
Unisay merged 2 commits into
mainfrom
issue-241/derived-inline-directives
Jul 28, 2026
Merged

Derive transitive inline directives on specialization bindings#324
Unisay merged 2 commits into
mainfrom
issue-241/derived-inline-directives

Conversation

@Unisay

@Unisay Unisay commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Closes #241.

Problem

With graduated @inline directives in place (#232), a directive names the generic combinator, but the bindings that actually appear at call sites are often its specializations: purs's common-subexpression pass floats a repeated dictionary application into a top-level binding (bind = Control.Bind.bind bindStateT, verbatim from real CoreFn output), and users write the same shape by hand as top-level partial applications. None of these carry a directive of their own, so each had to be annotated by hand for the directive to keep working through the indirection.

Mechanism

The arity=N policy composes over application, which is what makes derivation mechanical. For a binding a = f x₁ … xₖ where f carries arity=N: when k < N, a call site applying N − k more arguments to a reconstructs — once a's right-hand side is pasted there — a site applying at least N arguments to f, exactly a qualifying site of the explicit directive, so a inherits arity=(N − k); when k ≥ N, the right-hand side is itself already a qualifying call, so every use of a stands for the specialized value and a inherits always-inline (the arity=0 reading of the same arithmetic). An explicit directive on the specialization always wins over derivation. The rationale lives in Note [Derived inline directives].

The optimizer pipeline is now split into two phases around the derivation point (OptimizerPhases). Derivation reads the module settled by the post-merge optimize+dce fixpoint rather than the pristine input: a specialization referenced once has already dissolved into its use site by then, so only the shared survivors are inspected. It runs before the uncurry worker/wrapper split, so a derived arity joins the uncurry veto like an explicit one, and before the post-uncurry fixpoint, whose whole-binding inlining performs the derived always-inline pastes; derived arities paste at qualifying call sites in the specialize fixpoint exactly like explicit ones. One left-to-right fold over the bindings both derives transitively (a standalone binding references only earlier bindings, so a chain of specializations resolves in a single pass) and bounds the derivation.

End to end

Golden.DirectiveDerived.Test carries -- @inline combine arity=2 on a three-parameter function too big for any heuristic paste, plus two pragma-free specializations: onePlusTwo = combine 1 2 (saturated at the directed arity) and oneOnly = combine 1 (one argument short). Before this change both survive as shared bindings and every site calls them (from the golden committed with the spec commit):

local Golden_DirectiveDerived_Test_onePlusTwo = function(c_S_0)
  return c_S_0 - 1 - 1 - 2 - 2 - 1 - 3 - 2 - 4 - 1 - 5 - 2 - 6 - 1 - 7 - 2 - 8 - 1 - 9 - 2 - 10 - 1 - 11 - 2 - 12 - 1 - 13 - 2 - 14 - 1 - 15 - 2 - 16 - 1 - 17 - 2 - 18 - 1 - 19 - 2 - 20
end
-- ...
  local _ = Effect_Console_log(Data_Show_showIntImpl(Golden_DirectiveDerived_Test_onePlusTwo(100)))()
  local _ = Effect_Console_log(Data_Show_showIntImpl(Golden_DirectiveDerived_Test_onePlusTwo(200)))()
  local _ = Effect_Console_log(Data_Show_showIntImpl(Golden_DirectiveDerived_Test_oneOnly(50)(60)))()

After: onePlusTwo derives always-inline, pastes at both sites and folds to constants; oneOnly derives arity=1, pastes at its applied site, and stays a pinned shared binding for the bare use that passes it to a function:

  local _ = Effect_Console_log(Data_Show_showIntImpl(-140))()
  local _ = Effect_Console_log(Data_Show_showIntImpl(-40))()
  local _ = Effect_Console_log(Data_Show_showIntImpl(-660))()
  return Effect_Console_log(Data_Show_showIntImpl(Golden_DirectiveDerived_Test_apply34(Golden_DirectiveDerived_Test_oneOnly)))()
end)()

The hand-written eval oracle (-140, -40, -660, -246) passes in both states, so the transform is semantics-preserving. The corefn for this module also shows purs's CSE emitting genuine synthetic floats (sub = Data.Ring.sub ringInt, show = Data.Show.show showInt, discard = Control.Bind.discard discardUnit bindEffect) — the shapes this derivation exists for, once the default directive pack (#242) names their targets.

Tests

Four pipeline-level specs (derives directives for specializations) cover the saturated and under-applied derivations, transitivity through a chain (an inner specialization deriving from an outer one's derived entry, kept alive through the settle phase by a second use), and the explicit-directive precedence guard; three were red before the implementing commit. The golden module exercises the module-header pragma path end to end. No other golden moved: the corpus carries no arity= directives yet, so derivation fires nowhere else.

Unisay added 2 commits July 27, 2026 20:31
Pipeline-level specs for the directive derivation on specialization
bindings: a binding shaped f a1..ak where f carries @inline arity=N
inherits always-inline when k >= N and arity=(N-k) when under-applied,
transitively through chains, never overriding an explicit directive.

The Golden.DirectiveDerived.Test module and its hand-computed eval
oracle are committed in the pre-derivation state: golden.ir/golden.lua
show the specialization surviving as a shared binding with un-inlined
call sites, so the implementing commit carries the reviewable diff of
what derivation changes. The unit specs are red at this commit.
A top-level binding whose settled shape applies an @inline arity=N
target to k arguments inherits arity=(N-k) while under-applied and
always-inline once saturated: a site applying N-k more arguments to
the binding reconstructs, after the paste, a qualifying site of the
explicit directive, so the derivation is the compositional reading of
the arity policy (Note [Derived inline directives]).

The pipeline is split into two phases around the derivation point
(OptimizerPhases): derivedInlinePolicy reads the module settled by the
post-merge optimize+dce fixpoint (use-once specializations are already
dissolved), and the lower phase runs under the extended policy, so
derived arities join the uncurry veto and the arity call-site gate,
and derived always entries are pasted by the post-uncurry fixpoint.
One left-to-right fold over the bindings derives transitively and
bounds the derivation; explicit root directives are never overridden.

Golden.DirectiveDerived.Test goldens move from the shared-binding
state committed with the specs to the derived state: the saturated
specialization is pasted and folds to constants at both sites, the
under-applied one pastes at its applied site and stays a pinned shared
binding for the bare use. The hand-written eval oracle is unchanged.
@Unisay Unisay self-assigned this Jul 28, 2026
@Unisay
Unisay marked this pull request as ready for review July 28, 2026 07:51
@Unisay
Unisay merged commit 2401ee7 into main Jul 28, 2026
2 checks passed
@Unisay
Unisay deleted the issue-241/derived-inline-directives branch July 28, 2026 07:51
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.

Auto-infer transitive inline directives on synthetic specializations

1 participant