Thirty-five arms become thirty: Power is data (#746) - #1076
Merged
Conversation
This was referenced Aug 26, 2026
Rafael-SOWNet
added a commit
that referenced
this pull request
Aug 26, 2026
`MatchedRule.TryApply` had two paths: a pattern that `IsDeterministic` is asked for a single match and allocates nothing, and anything else is enumerated through `Match`, which is a chain of iterator state machines, one per pattern node, built at every node of the tree a pass visits. Between the two sits a case that is neither and is common. A `Commutative<T>` node of deterministic children offers the written order and the swapped one and nothing else -- two candidates, known before it is asked. Enumerating those two costs the whole iterator chain, and on a set that runs on every pass it is measurable: `RewriteRules.Power` as data with two commutative rules took `SolveMediumHard` from 165.05 MB to 171.37 MB, +3.8%, past the kernel gate's 3% band, and had to be written out as six node patterns to land (#1076). So a pattern now says how many candidates it can offer at most -- `ChoiceCount`, or `Unbounded` where it cannot say -- and can be asked for the nth of them without an iterator. A node's count is the product over its children, doubled when commutative; `Gathered` is unbounded, because how many ways k parts sit among n operands is a property of the expression rather than of the pattern, and anything containing one is unbounded with it. The index is a mixed radix over the children with the first the most significant digit, because `MatchInOrder` makes it the outermost loop -- so indexing yields the candidates in the order enumeration yields them, which matters: `TryApply` takes the first that satisfies the rule's `when`, so two implementations agreeing on the set of matches and differing on the order are two different rewriters. The count is an upper bound rather than a count, since a child whose name is already bound offers one candidate or none depending on what it is asked to match; an index that does not exist answers false and is skipped, exactly as an enumeration omits it. `BoundedMatchingAgreesWithEnumeration` holds the two implementations together over every rule in `MatchedRules` and the corpus already there, asserting count, order and bindings, and asserting that at least one pattern did match more than one way -- or it would be the deterministic test again under another name. Measured on master, with no new rule set: `SolveMediumHard` 165,054,016 B to 163,400,736 B, -1.00%. The sets already converted carry commutative patterns, so the saving is there to collect before anything else is exchanged. The set list in `DeterministicMatchingTest` was five names, written when there were five and still five while `MatchedRules` grew past twenty -- so the test whose subject is "every rule" was looking at a tenth of them. It reads the class now. Claude-Session: https://claude.ai/code/session_01Bjumi5K7fg8yx6UK1mZTQd Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Rafael-SOWNet
force-pushed
the
feat/power-is-data
branch
from
August 26, 2026 18:44
de0166d to
1e36b14
Compare
…e most guarded `Patterns.PowerRules` is the biggest `switch` in the transformation layer and the one carrying the most conditions: five separate issues -- #752, #801, #802, #902 and #721 -- each put a branch-cut guard on one rewrite in it. Written as data, its thirty-five arms are thirty rules, and every one of those guards is *asked of the same helper it was written in* rather than restated here. A branch cut copied into a second file is how the two copies come to disagree, and one of these has already been wrong in both directions at once. Two rules needed something a `Bindings` cannot carry. `log_b(b) = 1` holds wherever the node is defined at all, so it takes the overload handed the matched node and reads the node's own `DomainCondition`; and the radical reduction asks `ReduceRadical` in both its `when` and its replacement, because whether it applies and what it produces are one computation. Six `switch` arms collapse into two commutative patterns -- a power times its own base, and a power times a product containing its base -- which fire exactly where the pair and the quadruple did, since the `switch` writes every orientation out. **That collapse is affordable only because of #1080.** A commutative pattern is not `IsDeterministic`, so before bounded matching it enumerated its two candidates through an iterator state machine per pattern node, at every node of every pass. The kernel gate failed this branch on allocation for exactly that: `SolveHard` +4.4% and `SolveMediumHard` +5.0%, reproduced locally as 165,054,016 B to 171,367,976 B. Writing the six arms out fixed it at the cost of the collapse; walking the candidates by index fixes it without, and the same two rules now measure 164,959,936 B against master's 163,425,360 B, +0.94% and inside the band. `SimplifyEasy` is 129,648 B either way, to the byte. Two arms bind their operands as `Number` rather than as `Entity`, so `1 - c` folds to a literal in the `switch` where `Entity` arithmetic would build a `Minusf`. The agreement run over 3,464 generated expressions found both; the data form casts and folds the same way. This is the whole value of running the two forms over generated expressions rather than over cases someone thought of. `EveryDataRuleIsClassifiedAsWritten` takes a name's classification as distinct rather than merely distinct-by-pair: `Power` and `Factorization` deliberately carry the same guarded `a^b * c^b`, and one name standing for two classifications would have listed the rule twice below rather than failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bjumi5K7fg8yx6UK1mZTQd
Rafael-SOWNet
force-pushed
the
feat/power-is-data
branch
from
August 26, 2026 19:50
1e36b14 to
b66f72b
Compare
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.
The largest
switchin the transformation layer, and the one carrying the mostconditions.
Patterns.PowerRuleshas thirty-five arms; five separate issues — #752,#801, #802, #902 and #721 — each put a branch-cut guard on one rewrite in it. As data
it is thirty rules.
The guards are asked, not restated.
MayTakeLogOfPower,MayGatherLogarithmsandReduceRadicalstay inPatterns.Power.csand the rules call them. Copying a branch-cutcondition into a second file is how the two copies come to disagree, and one of these has
already been wrong in both directions at once (
log(-3, -3)is 1 where a written-outa > 0calls it undefined;log(1, 1)is NaN where that guard calls it 1).Two rules read what a
Bindingscannot carry.log_b(b) = 1takes the overloadhanded the matched node, so the condition it attaches is the node's own
DomainCondition.The radical reduction asks
ReduceRadicalin both itswhenand its replacement, becausewhether it applies and what it produces are one computation.
Six arms become two commutative patterns — a power times its own base, and a power
times a product containing its base. They fire exactly where the pair and the quadruple
did, since the
switchwrites every orientation out;firesWhereTheSwitchDoesNotis empty.What this PR cost, and what it produced
The kernel gate failed the first version of this branch on allocation —
SolveHard+4.4%,
SolveMediumHard+5.0%, past its 3% band. Not time, and not noise: allocationreproduces to 0.033%.
The cause was the two commutative patterns. A commutative pattern is not
IsDeterministic, soTryApplyenumerated its two candidates through an iterator statemachine per pattern node, at every node of every pass. My own pre-push measurement had
looked at
Simplify, whose allocation is identical to the byte on every build here — sothe regression was invisible to it. The gate measures eighteen entry points because one of
them is not the others.
Writing the six arms out as node patterns fixed it, at the cost of the collapse. That was
a bad trade for a rule set language, so it became #1079 → #1080: a bounded pattern is
now walked by index rather than enumerated. With that in, the commutative form is the cheap
one:
SolveMediumHard, allocated2c082da4)SimplifyEasyis 129,648 B on both, to the byte.What the differential run caught. Two arms bind their operands as
Number, so1 - cfolds to a literal in the
switchwhereEntityarithmetic builds aMinusf. Bothdisagreed on the first run of
PowerAsDataMatchesTheSwitchover 3,464 generatedexpressions, and neither is a case anyone would have written down by hand.
One test made stronger.
EveryDataRuleIsClassifiedAsWrittennow asserts that a rulename never stands for two classifications.
PowerandFactorizationdeliberately carrythe same guarded
a^b * c^b; without the assertion, a name meaning two things would havelisted the rule twice rather than failed.
Part of #746 tier 1.
🤖 Generated with Claude Code
https://claude.ai/code/session_01Bjumi5K7fg8yx6UK1mZTQd