Make a rewrite rule's left-hand side data, and prove it against the switch (#248, #746 v1.0) - #938
Conversation
…witch (#248, #746 v1.0) #746 v1.0 asks for "pattern matching as a data structure, not a `switch`: matchable, enumerable, testable". Every rewrite in the library is currently an arm of a `switch` expression, and three separate things tier 2 wants turn out to be blocked on that one fact: a rule cannot carry its own justification tier, a rule cannot be addressed individually (#825), and an e-graph cannot match against an e-class because there is no pattern object to match with (measured in #746's own e-graph evaluation, where the prototype had to extract a representative term per class instead). MatchPattern a left-hand side as a value: a hole, a typed hole, a literal, a node MatchedRule one rule -- name, pattern, side condition, builder, and its own Soundness MatchedRuleSet an ordered list of them, enumerable, whose tier is derived from its rules Three properties the `switch` cannot have, each with a test: - **A repeated name binds the same subexpression both times**, which is what the existing rules spell out by hand as `when any1 == any1a`. Here it falls out of the matcher. - **A set's tier is the weakest of its rules, derived rather than declared.** The registry's thirty sets all declare `SoundUnderAssumptions` because a set's tier is the minimum over its arms and one conditional arm drags the rest down; per-rule tiers are what makes the field mean anything, and they cannot exist while an arm is not a value. - **The rules are enumerable and named**, so one can be listed, tested alone, or named in a bug. **The load-bearing part is the equivalence test, not the types.** `DivisionPreparing` is expressed as data beside the `switch` that already expresses it, and both are run over every expression a small grammar produces -- 500+ of them -- and required to agree on all. The test also asserts the rules actually *fire* on a fair number of them, so agreement cannot be two things both doing nothing. That makes replacing a `switch` a mechanical step, provable one set at a time, rather than a wholesale leap. Deliberately not here: associativity and commutativity in the matcher, so `a + b` does not match `b + a`. That is what #248 is actually about, and guessing at its shape before the plain case is proven is how the first version of a matcher gets built twice. Everything is `internal` for the same reason -- the shape should survive a few more sets before it becomes public surface. Measured: suite 7075 passed / 0 failed, 5 of them new; casbench 116/119 with 0 wrong, 0 error, 0 timeout. #248 #746 Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…holds (#248) The first set was three rules with no side conditions, which proves the types compile rather than that they generalise. CollapseMultipleFractions is the test that means something: eight rules, an order that is load-bearing -- the quotient-times-quotient rule has to be tried before the general product rule or the general one swallows it -- and a predicate on a hole, `Integer { IsPositive: true }`. **One feature was added and nothing else changed.** MatchPattern.Any<T>(name, where) is the C# property pattern as data: a predicate on the node, which travels with the hole and can be read off a rule, as against a condition about the match as a whole, which belongs in the rule's `when` and cannot. Both sets are now expressed as data and both agree with their `switch` on every one of the 500+ generated expressions. Two properties get tests of their own now that there is a set with real structure. A predicate on a hole is checked, so `(x/y)^2` fires and `(x/y)^(-2)`, `(x/y)^0` and `(x/y)^z` do not. And the order of the rules is load-bearing in the data exactly as it is in the switch: reversing the list makes the general rule match `(a/b) * (c/d)` first, which is the collision the written order avoids. A switch gets that ordering by accident of being written top to bottom; a list has to mean it. Measured: suite 7081 passed / 0 failed, 11 of them in this file. #248 Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
|
Pushed a second commit, because one rule set proves the types compile rather than that the shape generalises.
One feature was added and nothing else changed. Both sets now agree with their
Suite 7081 / 0. |
…commutativity (#248, #752) The third set, and the one that exercises a condition about the *match as a whole* rather than about one hole. `(a^b)^c = a^(b*c)` holds for a positive base whatever the exponents, and for any base when the outer exponent is whole -- and is false outside those two, which is #752: applied unconditionally it turned sqrt(x^2) into x, which at -0.63 is -0.63 where the expression is 0.63. Expressed as data the guard reads `c is Integer || a.Evaled is Real { IsPositive: true }`, two bindings at once, which no predicate on a single hole can say. So the design now covers holes, literals, repeated names, typed holes, per-hole predicates, rule-level guards, ordered sets whose order is load-bearing, and a Soundness per rule -- and three sets are expressed in it, each proven against the `switch` that already expresses it. **This is also the first rule here whose tier carries information.** The condition is what makes it SoundUnderAssumptions, and a reader sees the condition and the tier in one place; in the `switch` the tier lives on the set and the condition lives forty lines from it. The differential comparison needed care and the care is the point. PowerRules is a large set and an earlier arm may fire on the same expression, so a case counts only where the switch either did nothing or produced exactly the power-of-a-power answer. **That the rule cannot be isolated from its switch any other way is itself the argument for rules being data**, and it is written into the test rather than into a commit message that nobody reads twice. Two more tests come from the mathematics rather than from the switch: the guard decides firing -- (x^2)^3 and (2^x)^(1/2) fire, (x^2)^(1/2) and (x^2)^(3/2) do not, the last two being #752 -- and where it fires the value survives at a negative point, which is where the unguarded version went wrong. Measured: suite 7091 passed / 0 failed, 21 of them in this file. #248 #752 Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
… four (#248) This is what #248 is actually for. CommonRules writes k*p + k*q = k*(p+q) out **four times** -- (k*p)+(k*q), (p*k)+(k*q), (k*p)+(q*k), (p*k)+(q*k) -- because a C# pattern cannot say "either way round". One commutative pattern says it. **Matching had to start enumerating solutions rather than returning one, and that is not a refinement.** b*a + c*a shares a, and a matcher that commits to the first way the left product matches binds k = b, fails on the right, and gives up -- in both orders of the sum. Only backtracking finds it. So every pattern now yields *every* way it can match, lazily, and a rule takes the first that also satisfies its side condition. Bindings are copied rather than mutated for the same reason: a branch that fails must leave nothing behind for the branch tried next, and sharing one dictionary across attempts is how a matcher silently starts accepting things it should not. **And a finding that matters more than the rule.** The one commutative rule fires in exactly the cases the four arms fire, over all 256 sums of products the test generates -- but it does not always build the same tree. Where *two* factors are shared the two pick different ones: a*b + b*a gives b*(a+a) from the arms and a*(b+b) from the rule, both 2ab. That tie-break is fixed in the switch by the order its arms happen to be written in and was never chosen deliberately. **Migrating this rule is therefore not purely mechanical** -- it needs a tie-break convention, or the printed answer moves for expressions with two shared factors. The test asserts firing-agreement exactly, and asserts the values agree where the trees do not, so the difference is pinned rather than discovered later. It is also the first rule here that is Soundness.Sound. Distributivity holds for every complex k, p and q with no condition and no branch, so the tier finally says something its neighbours' does not -- which is the argument for a tier belonging to a rule rather than to a set. Not here: matching across a flattened chain, a + b + c against x + y. That is the n-ary half, it wants the operands gathered first, and it is a larger change than the commutative one. Measured: suite 7100 passed / 0 failed, 30 in this file. #248 Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
|
Two more commits, and the second one is #248's actual subject. A rule-level guard over two bindings
Comparing it to the Commutative matching — and it needs backtracking
Getting there meant matching has to enumerate solutions rather than return one. The finding that matters more than the ruleThe commutative rule fires in exactly the cases the four arms fire — all 256 sums of products the test generates — but it does not always build the same tree. Where two factors are shared they pick different ones: That tie-break is fixed in the It is also the first rule here that is Not here: matching across a flattened chain ( Suite 7100 / 0, 30 tests in this file. |
#746 v1.0 asks for "pattern matching as a data structure, not a
switch: matchable, enumerable, testable".Every rewrite in the library is currently an arm of a
switchexpression, and three separate things tier 2 wants turn out to be blocked on that one fact:What this adds
MatchPatternMatchedRuleSoundnessMatchedRuleSetThree properties the
switchcannot have, each with a test:when any1 == any1a. Here it falls out of the matcher.The load-bearing part is the equivalence test, not the types
DivisionPreparingis expressed as data beside theswitchthat already expresses it, and both are run over every expression a small grammar produces — 500+ — and required to agree on all of them. The test also asserts the rules actually fire on a fair number, so agreement cannot be two things both doing nothing.That makes replacing a
switcha mechanical step, provable one set at a time, rather than a wholesale leap.Deliberately not here
Associativity and commutativity in the matcher —
a + bdoes not matchb + a. That is what #248 is actually about, and guessing at its shape before the plain case is proven is how a matcher gets built twice. Everything isinternalfor the same reason: the shape should survive a few more sets before it becomes public surface.Measured
Suite 7075 passed / 0 failed, 5 new; casbench 116/119 with 0 wrong, 0 error, 0 timeout. No
BREAKING-CHANGES.mdentry — nothing existing changes behaviour, and no public surface is added.🤖 Generated with Claude Code