Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions bench/goldens/fnew_Bench.EffectStep.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
chunk: Bench.EffectStep.lua
runtime: LuaJIT 2.1.1741730670
main-chunk FNEW: 11
function-body FNEW: 13
total FNEW: 24
prototypes: 25
function-body FNEW: 12
total FNEW: 23
prototypes: 24
function-body FNEW sites:
Bench.EffectStep.lua:4
Bench.EffectStep.lua:6
Expand All @@ -13,8 +13,7 @@ function-body FNEW sites:
Bench.EffectStep.lua:12
Bench.EffectStep.lua:12
Bench.EffectStep.lua:33
Bench.EffectStep.lua:54
Bench.EffectStep.lua:53
Bench.EffectStep.lua:48
Bench.EffectStep.lua:57
Bench.EffectStep.lua:66
Bench.EffectStep.lua:46
Bench.EffectStep.lua:45
Bench.EffectStep.lua:49
Bench.EffectStep.lua:58
11 changes: 4 additions & 7 deletions bench/goldens/trace_effect_step.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@ workload: n=100000 reps=4 result=1500000
aborts (distinct site -- reason):
Bench.EffectStep.lua:10 -- NYI: bytecode FNEW
Bench.EffectStep.lua:12 -- NYI: bytecode FNEW
Bench.EffectStep.lua:48 -- NYI: bytecode FNEW
Bench.EffectStep.lua:49 -- NYI: bytecode UCLO
Bench.EffectStep.lua:53 -- NYI: bytecode FNEW
Bench.EffectStep.lua:54 -- NYI: bytecode FNEW
Bench.EffectStep.lua:66 -- NYI: bytecode FNEW
Bench.EffectStep.lua:45 -- NYI: bytecode FNEW
Bench.EffectStep.lua:46 -- NYI: bytecode FNEW
Bench.EffectStep.lua:58 -- NYI: bytecode FNEW
Bench.EffectStep.lua:9 -- NYI: bytecode FNEW
bytecode end state (J*=compiled, I*=blacklisted):
Bench.EffectStep.lua:10 IFUNCF
Expand All @@ -20,9 +18,8 @@ bytecode end state (J*=compiled, I*=blacklisted):
Bench.EffectStep.lua:36 IFUNCF
Bench.EffectStep.lua:37 IFUNCF
Bench.EffectStep.lua:39 IFUNCF
Bench.EffectStep.lua:40 JFUNCF
Bench.EffectStep.lua:9 JFUNCF
effect_step.lua:11 JFUNCF
effect_step.lua:13 JFORI
effect_step.lua:13 JFORL
counts: aborts=8 compiled=7 blacklisted=7
counts: aborts=6 compiled=6 blacklisted=7
29 changes: 29 additions & 0 deletions changelog.d/20260714_120000_unisay_case_of_case_if.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
### Added

- Case-of-case pushes over the `IfThenElse` decision tree in the IR optimizer
(#203). A scalar literal compared against an if-tree distributes into the
branches when every leaf comparison constant-folds — the shape an inlined
`Ord` comparison leaves behind once the tag read distributes over its
`Ordering` tree (#180) — and an if sitting in the condition of another if
pushes into its branches when the inner branches are boolean literals or
the outer branches are trivial to re-emit. Both shapes sat in expression
position, where codegen wraps them in an IIFE allocated and called per
evaluation — on every iteration, for the recursive functions in
`Golden.LongCallbackChain` and `Golden.TailRecM2Shadow`. The pushed
comparison collapses to a flat condition — `if n < 0 then` instead of
`if "…LT" == (function() … end)() then` — across seven goldens, with eval
outputs unchanged.

- Half-literal boolean ifs fold to short-circuiting operators (#203):
`if p then True else b` becomes `p or b`, `if p then False else b` becomes
`not p and b`, and the two mirrored shapes likewise — the completion of
`reduceBooleanIf`, whose two-literal cases already collapsed to the bare
condition. Lua's `and`/`or` evaluate exactly what the branches evaluated,
in the same order, and unlike a branch an operator survives in condition
position without an IIFE: the `Ordering` trees compared against `GT`
(`Golden.Loopification`, `Golden.Primops`, `Golden.UncurryEffect`) now
flatten all the way to `not (n < 0) and n ~= 0`. Two identity primop folds
ride along (`a and true` → `a`, `a or false` → `a` — nothing is skipped);
the annihilator duals stay, since folding them would skip evaluating `a`.
Luacheck's W581 (suggesting the NaN-unsafe `not (x < y)` → `x >= y` flip)
is now ignored for generated goldens.
162 changes: 143 additions & 19 deletions lib/Language/PureScript/Backend/IR/Optimizer.hs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import Language.PureScript.Backend.IR.Types
, literalInt
, literalString
, paramName
, primBinOp
, primNot
, refImported
, rewriteExpBottomUpM
Expand Down Expand Up @@ -796,6 +797,8 @@ optimizedExpressionM policy env =
`thenRewrite` removeIfWithEqualBranches
`thenRewrite` flipNegatedIf
`thenRewrite` reduceBooleanIf
`thenRewrite` pushEqIntoIfBranches
`thenRewrite` pushIfCondIntoBranches
`thenRewrite` inlineLocalBindings
)

Expand All @@ -821,19 +824,12 @@ assumption the type checker would not already guarantee.
constantFolding ∷ Applicative m ⇒ RewriteRuleM m Ann
constantFolding =
pure . \case
Eq _ (LiteralBool _ a) (LiteralBool _ b) →
Just $ literalBool $ a == b
Eq _ a b
| Just result ← foldEqLiterals a b →
Just $ literalBool result
Eq _ (LiteralBool _ True) b →
-- 'b' must be of type Bool; see Note [IR is assumed well-typed]
Just b
Eq _ (LiteralInt _ a) (LiteralInt _ b) →
Just $ literalBool $ a == b
Eq _ (LiteralFloat _ a) (LiteralFloat _ b) →
Just $ literalBool $ a == b
Eq _ (LiteralChar _ a) (LiteralChar _ b) →
Just $ literalBool $ a == b
Eq _ (LiteralString _ a) (LiteralString _ b) →
Just $ literalBool $ a == b
-- See Note [IR primops] and Note [Folding primops follows Lua 5.1]
PrimBinOp _ op a b → foldPrimBinOp op a b
PrimNot _ a → foldPrimNot a
Expand Down Expand Up @@ -869,7 +865,12 @@ primops]). The per-operator caveats:
collapse a known-boolean first operand (@true and b == b@,
@false or b == b@, and the two annihilators): sound because Lua
@and@/@or@ short-circuit, so dropping the second operand is exactly
what the runtime does.
what the runtime does. An identity /second/ operand (@a and true@,
@a or false@) also folds to @a@ — nothing is skipped, and the
literal cannot change a boolean @a@'s value (see Note [IR is
assumed well-typed]). The annihilator duals (@a and false@,
@a or true@) are left to the runtime, since folding them would skip
@a@'s evaluation.
-}

-- | The IEEE-double exactness ceiling; integer folds bail beyond it.
Expand Down Expand Up @@ -899,9 +900,11 @@ foldPrimBinOp op l r = case (op, l, r) of
(PrimAnd, LiteralBool _ a, LiteralBool _ b) → Just (literalBool (a && b))
(PrimAnd, LiteralBool _ True, b) → Just b
(PrimAnd, LiteralBool _ False, _) → Just (literalBool False)
(PrimAnd, a, LiteralBool _ True) → Just a
(PrimOr, LiteralBool _ a, LiteralBool _ b) → Just (literalBool (a || b))
(PrimOr, LiteralBool _ True, _) → Just (literalBool True)
(PrimOr, LiteralBool _ False, b) → Just b
(PrimOr, a, LiteralBool _ False) → Just a
_ → Nothing
where
intFold ∷ Integer → Integer → Integer → Maybe Exp
Expand Down Expand Up @@ -934,6 +937,20 @@ foldPrimNot = \case
PrimNot _ e → Just e
_ → Nothing

{- | Fold an equality of two scalar literals, or 'Nothing' when either
side is not a scalar literal. Two literals of different kinds also
decline: such a comparison is ill-typed input (see Note [IR is assumed
well-typed]), not a provable inequality.
-}
foldEqLiterals ∷ RawExp ann → RawExp ann' → Maybe Bool
foldEqLiterals l r = case (l, r) of
(LiteralBool _ a, LiteralBool _ b) → Just (a == b)
(LiteralInt _ a, LiteralInt _ b) → Just (a == b)
(LiteralFloat _ a, LiteralFloat _ b) → Just (a == b)
(LiteralChar _ a, LiteralChar _ b) → Just (a == b)
(LiteralString _ a, LiteralString _ b) → Just (a == b)
_ → Nothing

{- | Folds a record projection into the record constructor:
@{ foo: 1, bar: 2 }.foo@ becomes @1@, and a projection through a record
update takes the patched value (or reaches into the updated record when
Expand Down Expand Up @@ -1629,19 +1646,32 @@ flipNegatedIf =
Just (IfThenElse ann cond elseBranch thenBranch)
_ → Nothing

{- | Collapse an if whose branches are the two boolean literals to the
condition or its negation:
{- | Collapse an if with boolean-literal branches into boolean
operators. Two literal branches make the if the condition or its
negation:

* @if p then True else False@ ⟶ @p@;
* @if p then False else True@ ⟶ @not p@ (a 'PrimNot' — a node the IR
only gained with the primops of issue #178).

Every 'Ord' comparison and @/=@ decays to this shape: their 'case' over
the result compiles to a two-way boolean decision tree, so once the
foreign comparison bodies lift to primops (#178) it is the dominant
residual. @p@ is a 'Bool' evaluated once with pure literal branches, so
the rewrite is semantics-preserving (see Note [IR is assumed
well-typed]).
One literal branch makes it a short-circuiting operator (issue #203);
the other branch is a 'Bool' too, pinned by the literal (see
Note [IR is assumed well-typed]):

* @if p then True else b@ ⟶ @p or b@;
* @if p then False else b@ ⟶ @not p and b@;
* @if p then a else True@ ⟶ @not p or a@;
* @if p then a else False@ ⟶ @p and a@.

Every 'Ord' comparison and @/=@ decays to the two-literal shape: their
'case' over the result compiles to a two-way boolean decision tree, so
once the foreign comparison bodies lift to primops (#178) it is the
dominant residual. The half-literal shapes are what a pushed comparison
('pushEqIntoIfBranches') collapses to when the tree has more than one
leaf folding the same way. Lua's @and@\/@or@ short-circuit, so each
fold evaluates exactly what the branches evaluated, in the same order —
and unlike a branch, an operator survives in condition position without
an IIFE.
-}
reduceBooleanIf ∷ Applicative m ⇒ RewriteRuleM m Ann
reduceBooleanIf =
Expand All @@ -1650,6 +1680,14 @@ reduceBooleanIf =
Just cond
IfThenElse _ cond (LiteralBool _ False) (LiteralBool _ True) →
Just (primNot cond)
IfThenElse _ cond (LiteralBool _ True) elseBranch →
Just (primBinOp PrimOr cond elseBranch)
IfThenElse _ cond (LiteralBool _ False) elseBranch →
Just (primBinOp PrimAnd (primNot cond) elseBranch)
IfThenElse _ cond thenBranch (LiteralBool _ True) →
Just (primBinOp PrimOr (primNot cond) thenBranch)
IfThenElse _ cond thenBranch (LiteralBool _ False) →
Just (primBinOp PrimAnd cond thenBranch)
_ → Nothing

removeUnreachableThenBranch ∷ Applicative m ⇒ RewriteRuleM m Ann
Expand All @@ -1665,6 +1703,92 @@ removeUnreachableElseBranch e = pure case e of
Just thenBranch
_ → Nothing

{- | Case-of-case over a comparison (issue #203), the 'Eq' sibling of
'reduceKnownConstructor'\'s tag-read distribution: a scalar literal
compared against an 'IfThenElse' tree distributes into the branches,
where each leaf comparison meets 'constantFolding' and the boolean-if
rules collapse the tree to a flat condition. Without the push the tree
sits in expression position — an IIFE in the generated Lua, allocated
and called per evaluation — the shape an inlined 'Ord' comparison
leaves behind once the tag read distributes over its 'Ordering'
decision tree (issue #180). Guarded by 'eqFoldsThrough', mirroring
'reflectFoldsThrough': the rule fires only when every leaf folds, so it
never leaves a residual comparison behind, and the duplicated operand
is a scalar literal, free to re-emit. Evaluation order is preserved:
the branch conditions ran before the comparison and still do. Each push
strictly shrinks the tree under the 'Eq', so the rewrite terminates.
-}
pushEqIntoIfBranches ∷ Applicative m ⇒ RewriteRuleM m Ann
pushEqIntoIfBranches =
pure . \case
Eq ann lit (IfThenElse ifAnn cond t e)
| eqFoldsThrough lit t
, eqFoldsThrough lit e →
Just $ IfThenElse ifAnn cond (Eq ann lit t) (Eq ann lit e)
Eq ann (IfThenElse ifAnn cond t e) lit
| eqFoldsThrough lit t
, eqFoldsThrough lit e →
Just $ IfThenElse ifAnn cond (Eq ann t lit) (Eq ann e lit)
_ → Nothing

{- | Whether comparing this expression against the literal folds away
completely: it is a scalar literal of the same kind (the comparison
folds to a boolean), or an 'IfThenElse' whose branches both do. Only
then does 'pushEqIntoIfBranches' distribute a comparison into an
'IfThenElse', so the rewrite never trades one expression-position tree
for another.
-}
eqFoldsThrough ∷ RawExp ann → RawExp ann' → Bool
eqFoldsThrough lit = go
where
go = \case
IfThenElse _ann _cond t e → go t && go e
leaf → isJust (foldEqLiterals lit leaf)

{- | Case-of-case over an 'IfThenElse' whose condition is itself an
'IfThenElse' decision tree (issue #203):

@
IfThenElse (IfThenElse c a b) x y
==> IfThenElse c (IfThenElse a x y) (IfThenElse b x y)
@

The tree in condition position — an IIFE in the generated Lua — becomes
statement-position ifs, and evaluation order is preserved exactly: @c@,
then @a@ or @b@, then @x@ or @y@. The unrestricted transformation would
need join points to avoid duplicating @x@ and @y@, so the rewrite is
restricted to the shapes where pushing cannot duplicate work:

* @a@ and @b@ are boolean literals: the residual ifs are folded right
away by 'removeUnreachableThenBranch'\/'removeUnreachableElseBranch'
and each of @x@, @y@ survives in exactly one copy;

* alternatively @x@ and @y@ are trivial by the 'isInlinableValue'
test — free to re-emit, and binder-free, so the copies cannot break
the unique-binders invariant.

Each push strictly shrinks the expression in condition position, so the
rewrite terminates.
-}
pushIfCondIntoBranches ∷ Applicative m ⇒ RewriteRuleM m Ann
pushIfCondIntoBranches =
pure . \case
IfThenElse ann (IfThenElse condAnn c a b) x y
| (isBoolLiteral a && isBoolLiteral b)
|| (isInlinableValue x && isInlinableValue y) →
Just $
IfThenElse
condAnn
c
(IfThenElse ann a x y)
(IfThenElse ann b x y)
_ → Nothing
where
isBoolLiteral ∷ RawExp ann → Bool
isBoolLiteral = \case
LiteralBool {} → True
_ → False

-- Inlining is a tricky business:
-- https://www.microsoft.com/en-us/research/wp-content/uploads/2002/07/inline.pdf

Expand Down
Loading