Skip to content

feat(optimizer): collapse Boolean pattern matches to a two-way if (#223) - #290

Merged
Unisay merged 1 commit into
mainfrom
issue-223/collapse-boolean-match
Jul 23, 2026
Merged

feat(optimizer): collapse Boolean pattern matches to a two-way if (#223)#290
Unisay merged 1 commit into
mainfrom
issue-223/collapse-boolean-match

Conversation

@Unisay

@Unisay Unisay commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Closes #223.

What

A pattern match on a Boolean compiled to a three-way if with a synthesized, unreachable default — the shape every inlined show or logShow of a boolean produces:

if a then
  return "true"
elseif false == a then
  return "false"
else
  return error("No patterns matched")
end

a is a Boolean, so true/false already cover it, but no existing rule had a handle on the shape: constantFolding knew only the left-literal true == b case, and the unreachable-branch rules need a literal condition. It now reduces to:

if a then return "true" else return "false" end

How

Both pieces proposed in the issue, reusing the existing folds for the actual collapse:

  1. The boolean-equality fold in constantFolding is completed. false == b and b == false fold to not b, and b == true folds to b, mirroring the existing left-literal true == b case (sound per Note [IR is assumed well-typed]).

  2. New rule propagateKnownCondIntoBranches — the Boolean sibling of propagateKnownCtorThroughLet, the more general of the issue's two alternatives. Inside if c then t else e a variable condition c is true throughout t and false throughout e, so its free occurrences in the branches are replaced with the matching literal; the existing folds then finish the job:

    if c then "true" else (if (false == c) then "false" else Exc)
      -- propagation: in the else branch, c is known false
    if c then "true" else (if (false == false) then "false" else Exc)
      -- constantFolding (existing): false == false → true
    if c then "true" else (if true then "false" else Exc)
      -- removeUnreachableElseBranch (existing)
    if c then "true" else "false"
    

    The rule fires only on a variable condition (re-reading an immutable binding is free and yields the value the test just observed), leaves no free occurrence behind so it cannot re-fire on its own result, and never grows the tree (a Ref becomes a literal node-for-node). It is placed after removeIfWithEqualBranches in the rewrite chain: branches that are equal while still naming c collapse to a single copy, which substituting the two literals first would unequalize — a unit test pins this ordering.

Tests

Written test-first (red before the fix): unit tests pin the three new equality folds, the end-to-end three-way→two-way collapse, then-branch propagation (if a then a else ea or e), and the guards — a genuinely partial match keeps its default, and a guard testing a different variable keeps the error arm. The randomized IR specs pass across 5 extra seeds.

Goldens

The elseif false == idiom disappears from every golden that carried it (9 sites across 6 goldens at current main; the issue counted 16 against an older corpus). Sample from Golden.RecursiveBindings.Test:

     yes_S_0 = function(v_S_2)
-      if v_S_2 then
-        return no_S_1(false)
-      elseif false == v_S_2 then
-        return no_S_1(true)
-      else
-        return error("No patterns matched")
-      end
+      if v_S_2 then return no_S_1(false) else return no_S_1(true) end
     end

The collapse also cascades: the shrunk boolean show bodies now clear the call-site inline budget, so logShow pastes at its call sites in the GenericEq/NumberIsNaN goldens, and in Golden.NumberIsNaN.Test that dead-codes the whole Ring dictionary:

-local Data_Semiring_foreign = {
-  numAdd = function(x) return function(y) return x + y end end,
-  numMul = function(x) return function(y) return x * y end end
-}
 ...
-local Data_Ring_sub = function(dict) return dict.sub end
-local Data_Ring_ringNumber = {
-  sub = Data_Ring_foreign.numSub,
-  Semiring0 = function()
-    return {
-      add = Data_Semiring_foreign.numAdd,
-      zero = 0.0,
-      mul = Data_Semiring_foreign.numMul,
-      one = 1.0
-    }
-  end
-}

Golden.GenericEqTwoTypes.Test keeps its 4 remaining error("No patterns matched") defaults — those are constructor-tag matches on closed sums, explicitly out of scope per the issue. Eval goldens are unchanged, which is the semantic safety net: runtime output is identical, only the shape moved.

A pattern match on a Boolean compiled to a three-way if with a
synthesized default: the scrutinee re-tested behind the first branch
(elseif false == a) and a dead error("No patterns matched") arm,
although true/false already cover the type. No existing rule had a
handle on the shape: the equality fold knew only the left-literal
true == b case, and the unreachable-branch rules need a literal
condition.

Two additions reduce it to if a then ... else ... end. The
boolean-equality fold in constantFolding is completed (false == b and
b == false fold to not b, b == true to b), and the new
propagateKnownCondIntoBranches rule — the Boolean sibling of
propagateKnownCtorThroughLet — substitutes a variable condition's
known value into its branches: c is true throughout the then branch
and false throughout the else branch, so the re-test folds to a
literal condition and the existing rules drop the dead default. The
rule runs after removeIfWithEqualBranches so equal branches still
collapse to one copy, fires only on a variable condition (re-reading
an immutable binding is free), and leaves no free occurrence behind,
so it cannot re-fire on its own result.

Unit tests pin both folds, the end-to-end collapse, and the guards: a
genuinely partial match keeps its default, and only a re-test of the
same variable folds. The elseif false == idiom disappears from every
golden that carried it, and the collapsed show bodies now clear the
call-site inline budget — in the NumberIsNaN golden that cascade
dead-codes the whole Ring dictionary. Eval goldens are unchanged.
@Unisay
Unisay merged commit 62b0213 into main Jul 23, 2026
2 checks passed
@Unisay
Unisay deleted the issue-223/collapse-boolean-match branch July 23, 2026 12:38
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.

Collapse Boolean pattern matches (including inlined show) to a two-way if

1 participant