Skip to content

Case-of-known-constructor through a let-bound scrutinee (#214)#216

Merged
Unisay merged 3 commits into
mainfrom
issue-214/known-ctor-through-let
Jul 9, 2026
Merged

Case-of-known-constructor through a let-bound scrutinee (#214)#216
Unisay merged 3 commits into
mainfrom
issue-214/known-ctor-through-let

Conversation

@Unisay

@Unisay Unisay commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Closes #214. Builds on #213 (now on main).

What

reduceKnownConstructor (#177) only fires on a constructor that is syntactically in place, and a scrutinee read more than once is never in place. Beta reduction Let-binds a non-trivial argument rather than substitute it into every occurrence (the #167 discipline), and a dictionary method reads its scrutinee several times: a tag test, a payload read, a fallthrough tag test. So an inlined and beta-reduced method lands on

let v = Just (x + 1) in
  if justTag == ReflectCtor v then … v.value0 …
  else if nothingTag == ReflectCtor v then Nothing else …

and nothing folds. The rules see ReflectCtor (Ref v) and ObjectProp (Ref v) "value0", never the constructor, so the whole match stays opaque behind the Let. That is a near-zero win for exactly the multiply-read-scrutinee methods (bind, apply), which are most of them.

How

propagateKnownCtorThroughLet propagates a known constructor from a Standalone Let binding into the binder's reads. When the RHS is a saturated Ctor application and the binder is read only through constructor-eliminating reads:

  • each ReflectCtor v folds to the tag string (sum types only, as in reduceKnownConstructor);
  • each field read (ObjectProp v "valueᵢ", DataArgumentByIndex i v) folds to a fresh field-binder bound once to the iᵗʰ argument, GHC's case-binder to field-binder split, so a non-trivial argument read at several sites is evaluated once rather than copied to each site;
  • the binding is dropped, its unread arguments discarded with the same licence reduceKnownConstructor has for a field read's siblings.

Trivial and dead field-binders then inline or DCE away, and with the reads folded the surrounding Eq/if meets constant folding and the unreachable-branch rules, collapsing the decision tree to its live arm. That is the case-of-known-constructor payoff of #177, now reached through the Let.

The rule declines when the binder is read as a whole value anywhere (a sibling binding, or a non-eliminating position such as an argument to a function): dropping it would dangle the reference, and keeping it while binding the fields would duplicate the arguments. A product-type ReflectCtor read has no tag row to fold to, so it too leaves a whole-value read and the rule declines. It also guards against a self-referencing RHS, which cannot arise under the global uniqueness condition but can on the non-GUC input optimizedExpression is exercised on directly.

Why now

This is the enabler that makes dictionary-method inlining (#180) pay off. Once #180 inlines a method and beta reduction Let-binds the dictionary value, the payload is read through exactly this Let-bound shape; without this rule #180 would inline the method but leave the match opaque. #180 stacks on this branch.

Verification

New optimizer specs cover the fold and its refusals: the tag fold unlocking the live branch, the payload dropped when only the tag is read, a field read at several sites bound once (proving no duplication), the whole-value and partial-application declines, and an end-to-end collapse to straight-line code run through the checked pipeline so a GUC or scoping violation would fail rather than pass silently. A randomized property stresses the fold across scalar payloads, checking the binder is eliminated and the free-reference multiset never grows.

Full suite green (cabal test all): 702 examples, 0 failures, and the IR optimizer specs stay green across several seeds. No structural golden moved and no eval oracle moved, matching the near-zero standalone impact the issue predicts, since the shape appears once method inlining lands. fourmolu and hlint clean, warning-free build.

)

reduceKnownConstructor only fires on a constructor in place, and a
scrutinee read more than once is never in place: beta reduction Let-binds
a non-trivial argument rather than substitute it, and a dictionary method
reads its scrutinee several times (a tag test, a payload read, a
fallthrough tag test). So an inlined and beta-reduced method lands on
`let v = Just (x + 1) in if justTag == ReflectCtor v then ... v.value0
... else ...` and nothing folds.

Add propagateKnownCtorThroughLet: when a Standalone Let binding's RHS is a
saturated constructor application and the binder is read only through
constructor-eliminating reads, fold each ReflectCtor to the tag string and
each field read to a fresh field-binder bound once to its argument (GHC's
case-binder to field-binder split, so an argument read at several sites is
evaluated once, not duplicated), then drop the binding. Trivial and dead
field-binders inline or DCE away, and the folded reads let the surrounding
Eq/if collapse to its live arm — the #177 payoff reached through a Let.

The rule declines when the binder is read as a whole value, which dropping
it would dangle and keeping it would duplicate the arguments, and it guards
against a self-referencing RHS on non-GUC input. This is the enabler that
makes dictionary-method inlining (#180) pay off; standalone impact is near
zero, so no golden moves.
@Unisay Unisay added area: ir IR / optimizer / DCE / inliner Optimisation A Compiler Optimisation labels Jul 9, 2026
@Unisay Unisay requested a review from Copilot July 9, 2026 09:52
@Unisay Unisay self-assigned this Jul 9, 2026
@Unisay Unisay marked this pull request as ready for review July 9, 2026 09:52

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

Adds an IR optimizer rewrite to enable “case-of-known-constructor” simplification when the scrutinee constructor is hidden behind a Let (the common shape produced by beta-reduction + multi-read dictionary methods), so downstream constant-folding and unreachable-branch elimination can collapse decision trees into straight-line code.

Changes:

  • Introduces propagateKnownCtorThroughLet and wires it into optimizedExpressionM after reduceKnownConstructor.
  • Adds optimizer specs (unit + property) covering successful folds and decline conditions, plus an end-to-end checked-pipeline regression.
  • Adds a changelog fragment documenting the new optimization.

Reviewed changes

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

File Description
lib/Language/PureScript/Backend/IR/Optimizer.hs Adds the new let-scrutinee constructor propagation rewrite and hooks it into the optimizer fixpoint.
test/Language/PureScript/Backend/IR/Optimizer/Spec.hs Adds tests validating the rewrite’s behavior, refusals, and pipeline integration.
changelog.d/20260709_114305_unisay_known_ctor_through_let.md Documents the new optimizer capability and its intended impact.

Comment thread lib/Language/PureScript/Backend/IR/Optimizer.hs
Comment thread test/Language/PureScript/Backend/IR/Optimizer/Spec.hs Outdated
- Optimizer.hs — decline propagateKnownCtorThroughLet on an out-of-range
  DataArgumentByIndex: guard hasWholeValueRead by the constructor arity so
  the read counts as whole-value and the rule declines (like
  reduceKnownConstructor), instead of stranding a fresh field-binder
  (#216 (comment))
- Optimizer/Spec.hs — compare the field-binder-split test up to alphaEq,
  not the incidental "$field0" fresh-supply name
  (#216 (comment))
- Optimizer/Spec.hs — add a property fuzzing arity vs index asserting
  well-scopedness, so the suite catches the out-of-range dangling binder
@Unisay Unisay requested a review from Copilot July 9, 2026 12:43

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

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

Comment thread lib/Language/PureScript/Backend/IR/Optimizer.hs
Comment thread lib/Language/PureScript/Backend/IR/Optimizer.hs
Comment thread changelog.d/20260709_114305_unisay_known_ctor_through_let.md Outdated
- changelog.d/…_known_ctor_through_let.md — reword the closing line to drop
  the "golden" shorthand and state the observable impact instead
  (#216 (comment))
@Unisay Unisay merged commit 0c9706c into main Jul 9, 2026
2 checks passed
@Unisay Unisay deleted the issue-214/known-ctor-through-let branch July 9, 2026 13:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: ir IR / optimizer / DCE / inliner Optimisation A Compiler Optimisation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Case-of-known-constructor through a let-bound scrutinee

2 participants