feat(ir): push case-of-case over the IfThenElse decision tree - #277
Merged
Conversation
A boolean-returning decision tree consumed in a strict position sits in expression position, where codegen wraps it in an IIFE allocated and called per evaluation. Two guarded pushes dissolve the dominant shapes: * pushEqIntoIfBranches distributes a scalar literal compared against an if-tree into the branches — the Eq sibling of reduceKnownConstructor's tag-read distribution, guarded by eqFoldsThrough (every leaf must constant-fold) so no residual comparison survives. The leaf folds then meet constantFolding and the boolean-if rules, collapsing the tree to a flat condition: the shape an inlined Ord comparison leaves once the tag read distributes over its Ordering tree (#180). * pushIfCondIntoBranches pushes an IfThenElse out of the condition of another into its branches, restricted to the shapes where pushing cannot duplicate work: the inner branches are boolean literals (the residual ifs fold right away), or the outer branches are trivial by isInlinableValue — binder-free, so the copies cannot break GUC. The scalar-literal Eq folds of constantFolding are factored into foldEqLiterals, shared with the eqFoldsThrough guard. Seven goldens lose the IIFE from their hot loops (flat comparison in CharLiterals, FieldCaching, LongCallbackChain, TailRecM2Shadow; a reduced residual tree in Loopification, Primops, UncurryEffect); eval outputs are byte-identical.
An if with exactly one boolean-literal branch is a short-circuiting operator in disguise: if p then True else b is p or b, if p then False else b is not p and b, and the mirrored shapes likewise. The other branch is a Bool too, pinned by the literal (Note [IR is assumed well-typed]), and Lua's and/or evaluate exactly what the branches evaluated, in the same order — but unlike a branch, an operator survives in condition position without an IIFE. reduceBooleanIf gains the four cases next to its existing two-literal collapses. This is what a pushed comparison (pushEqIntoIfBranches) leaves behind when more than one leaf folds the same way: the Ordering trees compared against GT collapse to if n < 0 then false else n ~= 0, which now flattens to not (n < 0) and n ~= 0 — Loopification, Primops and UncurryEffect lose their last residual IIFE. Two identity folds join foldPrimBinOp (a and true → a, a or false → a): an early and/or fold can catch a branch whose literal only appears after a later inlining round, and without the identities the ceremony survives to the output. The annihilator duals (a and false, a or true) stay unfolded, since collapsing them would skip evaluating a. The golden luacheck run now ignores W581, which suggests rewriting not (x < y) to x >= y — the NaN-unsafe inverse codegen deliberately never emits. Part of #203.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR improves the IR optimizer’s handling of case-of-case patterns over IfThenElse decision trees so boolean decision trees no longer remain in expression position (which forces Lua codegen to wrap them in an IIFE). This directly targets the recurring “Ordering tag-tree compared against a literal” shape and “if-in-condition-of-if” shape described in #203, reducing per-iteration allocation/call overhead in tight recursive loops while preserving evaluation order.
Changes:
- Add two new optimizer rewrites: distributing
EqoverIfThenElsewhen every leaf comparison constant-folds, and pushing a conditionIfThenElseinto outer branches when safe. - Extend boolean-if reduction to handle half-literal cases by collapsing them into short-circuiting
and/orprimops, and add identity folds fora and true/a or false. - Update IR optimizer unit tests, golden
.ir/.luaoutputs, and adjust golden luacheck to ignore W581 (NaN-unsafe rewrite suggestion).
Reviewed changes
Copilot reviewed 24 out of 24 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| lib/Language/PureScript/Backend/IR/Optimizer.hs | Implements the new decision-tree rewrites and supporting folds in the shared optimizer pipeline. |
| test/Language/PureScript/Backend/IR/Optimizer/Spec.hs | Adds unit tests to pin the new rewrites and the new primop identity folds. |
| test/Language/PureScript/Backend/Lua/Golden/Spec.hs | Ignores luacheck W581 for generated Lua goldens to avoid NaN-unsafe rewrite suggestions. |
| test/ps/output/Golden.BugListGenericEq.Test/golden.ir | Golden IR updated to reflect decision-tree collapse into and/or chains. |
| test/ps/output/Golden.BugListGenericEq.Test/golden.lua | Golden Lua updated to reflect decision-tree collapse into and/or chains. |
| test/ps/output/Golden.CharLiterals.Test/golden.ir | Golden IR updated to reflect Eq-over-tree folding into a flat comparison. |
| test/ps/output/Golden.CharLiterals.Test/golden.lua | Golden Lua updated to reflect Eq-over-tree folding into a flat comparison. |
| test/ps/output/Golden.FieldCaching.Test/golden.ir | Golden IR updated to reflect Eq-over-tree folding into a flat comparison. |
| test/ps/output/Golden.FieldCaching.Test/golden.lua | Golden Lua updated to reflect Eq-over-tree folding into a flat comparison. |
| test/ps/output/Golden.GenericEqTwoTypes.Test/golden.ir | Golden IR updated to reflect decision-tree collapse into and chains. |
| test/ps/output/Golden.GenericEqTwoTypes.Test/golden.lua | Golden Lua updated to reflect decision-tree collapse into and chains. |
| test/ps/output/Golden.LongCallbackChain.Test/golden.ir | Golden IR updated so the condition becomes a flat comparison (removing IIFE shape). |
| test/ps/output/Golden.LongCallbackChain.Test/golden.lua | Golden Lua updated so the loop condition becomes a flat comparison (removing IIFE shape). |
| test/ps/output/Golden.Loopification.Test/golden.ir | Golden IR updated to reflect GT-comparison collapsing to not (n < k) and n ~= k. |
| test/ps/output/Golden.Loopification.Test/golden.lua | Golden Lua updated to reflect GT-comparison collapsing to not (n < k) and n ~= k. |
| test/ps/output/Golden.Primops.Test/golden.ir | Golden IR updated to reflect GT-comparison collapsing to a flat boolean expression. |
| test/ps/output/Golden.Primops.Test/golden.lua | Golden Lua updated to reflect GT-comparison collapsing to a flat boolean expression. |
| test/ps/output/Golden.StringCodePoints.Test/golden.ir | Golden IR updated to reflect boolean-if collapse into or/and with error on mismatch. |
| test/ps/output/Golden.StringCodePoints.Test/golden.lua | Golden Lua updated to reflect boolean-if collapse into or/and with error on mismatch. |
| test/ps/output/Golden.TailRecM2Shadow.Test/golden.ir | Golden IR updated so the LT check is a direct < condition (removing IIFE shape). |
| test/ps/output/Golden.TailRecM2Shadow.Test/golden.lua | Golden Lua updated so the LT check is a direct < condition (removing IIFE shape). |
| test/ps/output/Golden.UncurryEffect.Test/golden.ir | Golden IR updated to reflect GT-comparison collapsing to not (n < k) and n ~= k. |
| test/ps/output/Golden.UncurryEffect.Test/golden.lua | Golden Lua updated to reflect GT-comparison collapsing to not (n < k) and n ~= k. |
| changelog.d/20260714_120000_unisay_case_of_case_if.md | Adds a changelog fragment describing the optimizer improvements and their motivation. |
The case-of-case pushes strip the IIFE the Ordering comparison in Bench.EffectStep compiled to, so the linked artifact allocates one fewer closure family: function-body FNEW 13 → 12, and the trace report drops the aborts and the compiled trace that closure caused (aborts 8 → 6, compiled 7 → 6). Deterministic counters only; a codegen improvement, accepted via ./bench/ci --accept.
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 #203.
Problem
The IR compiles pattern matches to decision trees of nested
IfThenElse. When such a tree is consumed in a strict position it sits in expression position, and codegen wraps it in an IIFE that is allocated and called on every evaluation. For the recursive functions inGolden.LongCallbackChainandGolden.TailRecM2Shadowthat means on every loop iteration.The issue predates #180, when the tree sat directly in the condition of another if. Today the dominant shape is different: the tag-read distribution rewrites
ReflectCtorover anOrderingtree into an if-tree of tag strings, so the condition holdsEq "…LT" (IfThenElse …). Seven goldens carried this shape.Approach
Three rewrites of the same case-of-case family, all in the shared rewrite chain of
optimizedExpressionM.pushEqIntoIfBranchesdistributes a scalar literal compared against an if-tree into its branches:Eq lit (If c t e)becomesIf c (Eq lit t) (Eq lit e), in both orientations. The guard,eqFoldsThrough, mirrorsreflectFoldsThroughfrom the tag-read distribution: the rule fires only when every leaf of the tree constant-folds against the literal. No residual comparison can survive, and the only duplicated operand is a scalar literal. The folded leaves then meetconstantFoldingand the boolean-if rules, which collapse the whole condition to a flat comparison.pushIfCondIntoBranchesis the transformation as stated in the issue:IfThenElse (IfThenElse c a b) x ybecomesIfThenElse c (IfThenElse a x y) (IfThenElse b x y), with the issue's guard. Either the inner branches are boolean literals, in which case the residual ifs fold right away and each outer branch survives in exactly one copy, or the outer branches are trivial byisInlinableValue. Trivial values are binder-free, so the copies cannot violate GUC. Evaluation order is preserved exactly:c, thenaorb, thenxory.reduceBooleanIfgains the four half-literal cases next to its existing two-literal collapses:if p then True else bbecomesp or b,if p then False else bbecomesnot p and b, and the mirrored shapes likewise. The non-literal branch is aBooltoo, pinned by the literal (Note [IR is assumed well-typed]), and Lua'sand/orevaluate exactly what the branches evaluated, in the same order, but survive in condition position without an IIFE. This is what a pushedGTcomparison collapses to when two leaves fold toFalse:if n < 0 then false else n ~= 0, which now flattens tonot (n < 0) and n ~= 0.Two supporting changes ride along. The scalar-literal
Eqcases ofconstantFoldingare factored intofoldEqLiteralsand shared with theeqFoldsThroughguard, so the guard and the fold cannot drift apart. AndfoldPrimBinOpgains the identity foldsa and true→aanda or false→a: an early and/or fold can catch a branch whose literal only appears after a later inlining round, and without the identities that ceremony survives to the output. The annihilator duals (a and false,a or true) stay unfolded, since collapsing them would skip evaluatinga.Verification
Unit tests pin all three rewrites. The LongCallbackChain microcosm folds to a flat comparison end to end (both orientations) and the GT microcosm to a flat boolean expression, a non-foldable leaf declines, trivial outer branches push while non-trivial ones stay put, each half-literal shape folds to its operator while an if with no literal branch stays, and the literal-branches arm of the condition push is exercised by direct rule application. That arm is unreachable through
optimizedExpression, wherereduceBooleanIfcollapses two-literal inner ifs bottom-up before the outer node is inspected.Ten goldens change,
.irand.luaonly. The condition becomes a flat comparison inCharLiterals,FieldCaching,LongCallbackChainandTailRecM2Shadow:if n < 0 theninstead ofif "…LT" == (function() … end)() then. TheGTcomparisons inLoopification,PrimopsandUncurryEffectflatten tonot (n < 0) and n ~= 0. The generic-equality chains inBugListGenericEq,GenericEqTwoTypesandStringCodePointscollapse their boolean elseif ladders into and/or chains.Eval goldens are byte-identical. The full suite passes clean and across four extra Hedgehog seeds.
The golden luacheck run now ignores W581, which suggests rewriting
not (x < y)tox >= y. That flip is the NaN-unsafe inverse codegen deliberately never emits (the printer negates==to~=because it is exact, and keepsnotover<for the same reason), so the warning has no actionable fix.