Make each rewrite rule addressable, generated from the switch that defines it (#825, #746 item 50) - #951
Merged
Merged
Conversation
…the name (#825) RewriteRule.Pattern/Guard/Replacement hold the C# the switch arm is written in, not anything a matcher can run. #746 tier 1 is pattern matching as data, and when a pattern becomes a value it should be able to be called Pattern without breaking somebody first. Renamed to PatternSource/GuardSource/ReplacementSource before any of this is public in a release, which is the only moment it is free. The generator emits the constructor call by named argument, so the two cannot drift apart -- a rename on one side stops the generated code compiling rather than silently binding elsewhere. PublicApi.txt regenerated: three members renamed, nothing else moved.
This was referenced Aug 16, 2026
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.
Implements the design in #825 — item 50 of #746, "the rule registry: turn the pattern set into enumerable, attributable data without regressing
Simplifyperformance".#825 is still labelled
Design documentand has noAcceptedlabel, so this is offered for review rather than on an assumed go-ahead — henceEarly PR. If the design is wanted in a different shape, the generator is the part to argue about and it is one file.What it does
A source generator (
Sources/Analyzers/RuleRegistryGenerator) turns each arm of a rewrite-ruleswitchinto an individually addressableRewriteRule. 347 rules across 16 rule sets. ARewriteStepnow names which rewrite fired, not just which set, which is the grain #28 asks for.The
switchis untouched as the thing a human edits and the thing the simplifier calls, so nothing on the hot path changes.Why a generator and not forty objects
#825's measurement disposed of the performance objection but explicitly did not dispose of this one:
The generator addresses it by construction: each arm's pattern,
whenclause and result expression are copied verbatim as syntax into the generatedapplylambda. Nothing is retyped, nothing is re-derived, and the registry is rebuilt from theswitchon every compile, so the two cannot drift. If a copied arm did not mean the same thing in the generated context it would fail to compile rather than diverge silently.Only two things are derived, and both are documented as what they are:
NodeTypesis a necessary, not sufficient condition (empty where the pattern's shape does not say), andGrowthis an explicitly crude syntactic proxy. A method carrying the attribute that is not an expression-bodiedparameter switch { ... }emits#error, not an empty list — a rule set that silently has no rules reads exactly like one that has been checked.The part to review most carefully: four arms are deleted
Patterns.Common.csloses three andPatterns.Power.csone. These are unreachable duplicates, and removing them changes no answer. Each appeared exactly twice with byte-identical pattern, guard and result; under first-match-wins the second could never fire. Verified mechanically rather than by eye — each survivor is the earlier occurrence:Minusf(Mulf(any1, any2), Mulf(any1a, any3))Patterns.Common.cs, was line 132Minusf(any1, Mulf(any2, any1a))Patterns.Common.cs, was line 137Minusf(any1, Mulf(any1a, any2))Patterns.Common.cs, was line 136Divf(Powf(any1, any2), Powf(any1a, any3))Patterns.Power.cs, was line 59The compiler cannot warn about these: a guarded arm never marks a later arm unreachable, so
CS8510does not fire. The generator found them because it names rules by pattern and suffixes repeats — andNoArmIsShadowedByAnIdenticalOneAboveItnow fails the build if another appears. There are currently zero suffixed names, so these four were all of them.Behaviour
No answer changes, so there is no
BREAKING-CHANGES.mdentry. The four removed arms were unreachable; everything else is additive. Measured rather than reasoned:casbench116/119, 0 wrong / 0 error / 0 timeout — byte-identical to its committed baseline once the timing column is normalisedrulecheck30 sets, 1365 applications, 0 never-settle, 0 value changes — unchangedThe new tests are not a "how many fail without this" count, because they test API that does not exist without it — they would not compile. What binds instead is that the four harnesses above are unchanged against their baselines.
Public API
21 new members, which is a promise for the rest of 2.x. Two things done deliberately:
PatternSource/GuardSource/ReplacementSource. They hold the C# the arm is written in, not anything a matcher can run. Goal: Math OS — a ten-year vision for AngouriMath as an open mathematical reasoning platform #746 tier 1 is pattern matching as data, and when a pattern becomes a value it should be able to be calledPatternwithout breaking someone first. Renaming was free before release and a major-version cost after.PublicApi.txtis regenerated, not hand-edited.What it does not do
Soundness. A rule's tier is a claim somebody has to argue for and there is no honest way to derive one from syntax, soRewriteRuleSet.Soundnessstays the declared tier. What being addressable buys is that a finer tier now has somewhere to live once the argument is made.switchover the expression, so there are no arms to generate from.RewriteRuleSet.Rulesis empty for them, which is documented as "empty is not the same as no rewrites", andTheRegistryIsAddressableAsFarAsItSaysItIspins the list by name so making one addressable is a change to that list rather than a silent improvement.Follow-on, measured separately
work/egraphin the analysis workspace re-ran #746 item 51's honest memory measurement now that rules carry a direction. Withholding the 60 rules that expand ends the saturation blow-up inside the addressable half — every expression saturates, none above 19 e-nodes, where the same configuration with all rules reaches thousands — while still findingsin(x)^2 + cos(x)^2 = 1. Item 51's first stated precondition was this issue, and it is now met. The remaining obstacle is the 14 sets that cannot declare a direction.That measurement lives outside this repository and nothing here depends on it.
🤖 Generated with Claude Code