Repository navigation
Make each rewrite rule addressable, generated from the switch that defines it (#825, #746 item 50) - #951
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 was referenced Aug 23, 2026
Open
Rafael-SOWNet
added a commit
that referenced
this pull request
Aug 23, 2026
…1012) * Make the derivation a path from the input to the answer (#28, #273) A recording held every rewrite that fired, across every candidate the simplifier generated including the ones that lost, each on the subexpression it matched. Read in order that does not walk from the input to the answer, and the type's own documentation said so in three places. `RewriteRecording.PathFrom(input, result)` walks it. Every step is a whole expression turning into another: `Steps[i].After` is `Steps[i + 1].Before`, the first starts at the input, and the last lands on what `Simplify` returned. `DerivationPath.OfSimplifying(expression)` is the same in one call, and prints as the stages with the identity that produced each one beside it. Two grains in two types, because a rewrite pass walks bottom-up and no partly-rewritten whole expression exists inside one: `DerivationStep` is the pass, and its `Rewrites` are the `RewriteStep`s that fired in it, each naming the rule that did it. What was missing was attribution. A rule-set pass now records what it turned into as well as what it matched, and `Simplificator` records the stages that are not rewrite passes -- inner simplification, the boolean minimiser, the polynomial rearrangement, expansion, factoring -- since the chain otherwise has a hole wherever the simplifier tidies up. Losing candidates are absent rather than marked: nothing leads from one to the answer, so none can be on a path to it, and what the search produced and did not keep is reported as `ExpressionsExplored`. Free when nobody is recording, which is what #746 requires of anything above the tree: one ambient read per stage against a tree walk per stage, and no allocation. `RewriteAllocationTest` guards the new fast path the way it already guarded `ApplyOnce`. `Simplify` returns what it returned before, and the raw recording is unchanged -- 270 rewrites on `x^(-1)/(y/z)`, 251 of them normalisation, measured on both builds. Corrected in passing, each re-measured: `Common` has 100 arms and not 103; `Derivation` on that expression is 5 rewrites and not 6; and `Transformations.md` said a source generator over the switch bodies was still for #746 item 50 to decide, which #951 shipped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011emtnRT6EWTrxXNtqDVK3e * Say per flow and ambient read, which is what the recording has been since #863 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011emtnRT6EWTrxXNtqDVK3e --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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