A step renders as a sentence - #1110
Merged
Merged
Conversation
#746 tier 2's last piece of required infrastructure is "transformation metadata rich enough that v5.0 can render a step as a sentence". The only check on that is to render one: metadata is rich enough exactly when a sentence can be built from it. Console.WriteLine(DerivationPath.OfSimplifying("tan(x) * cos(x)")!.Explain()); tan(x) * cos(x) becomes sin(x) provided not cos(x) = 0, in 4 steps. 1. Tidying: tan(x) * cos(x) becomes cos(x) * tan(x). 2. Tangent is sine over cosine (tan(a) = sin(a) / cos(a)), so tan(x) becomes sin(x) / cos(x). 3. Product with a quotient on the right (a * (b / c) = (a * b) / c), so cos(x) * sin(x) / cos(x) is reshaped into an equal tree that prints the same way. 4. A quotient of symbolic parts is grouped pairwise (num / den = the quotient with shared factors paired off), so cos(x) * sin(x) / cos(x) becomes sin(x) provided not cos(x) = 0. All 4 steps hold under assumptions rather than universally. The search reached 6 expressions and kept these 4. **Every word comes off a rule.** The clause is `RewriteRule.Name` with its hyphens replaced; the identity in brackets is `RewriteRule.Description`. There is no table of phrasings and no per-rule English written a second time, so a rule that is renamed or re-described says the new thing without this being touched. That is only possible because the names were written as phrases: 293 of them, and `RuleProseTest` holds all 293 to it. **Three things the first version got wrong, each found by reading the output.** It rendered *every* name as a clause, including the ones `RuleRegistryGenerator` invents for a `switch` arm -- producing "Divf(Sinf(var any1), Cosf(var any1a)) when any1 == any1a, so sin(x) / cos(x) becomes tan(x)", a sentence that quotes a matcher at the reader while its own doc comment said not to. The two kinds of name are now told apart exactly rather than guessed at: a rendered pattern has capitals, brackets and spaces and cannot be mistaken for lower-case letters and hyphens. A name that is not prose falls back to the identity ("By sin / cos = tan, ...") and then to naming the set. It said "X becomes X" whenever two different trees printed alike -- `1 + (x + y)` against `(1 + x) + y` -- which happened on two of the first six derivations tried. Saying so is better than hiding it: a reader who sees a step reshaping a chain without the printed form moving has learnt something true, where a reader who sees `a becomes a` has learnt to distrust the feature. And the closing note said "The step holds under assumptions" over a path of four steps, one of which was a rule set. That was wrong twice over -- it hid three steps and implied the other three were justified. It now counts, and says which it is counting. A fourth came from the tests: a path of *zero* steps said "nothing was done to it" where the search had reached five expressions and kept none. Not the same fact, and the difference is the whole of what that sentence has to say. **And a step now reports the tier of the rule that fired.** `RewriteStep.Soundness` read its set's tier, which is the minimum over the set's rules -- all thirty sets declare `SoundUnderAssumptions` while 181 of the 322 data rules are `Sound`, so the answer was the same for every rewrite of a set and understated most of them. `DerivationStep.Soundness` is now the weakest tier any rewrite that actually fired inside the step holds at, rather than its set's minimum over rules the step never reached. Both recorded in BREAKING-CHANGES.md, measured on a build. Public surface: three additive methods. Full suite: 8979 passed, 14 skipped, 0 failed. Part of #746 tier 2, and closes tier 5's "no step renders as a sentence".
Rafael-SOWNet
added a commit
that referenced
this pull request
Aug 30, 2026
…ities are written out (#1114) The fourth tranche of the repoint. `Trigonometric`'s comments are notes on *soundness* -- which branch cut, which range, which issue -- rather than identities, so all thirty-three were written from the rules' own patterns and replacements. Two were checked by measurement rather than recalled, and one of those found a wrong answer elsewhere (fixed separately in #1113): - `arctan(a) + arccotan(a) = pi/2 for a >= 0, and -pi/2 for a < 0`, because this library's arccotan has range (-pi/2, pi/2] and not the textbook (0, pi); - `arccotan(cotan(a)) = a, for a in arccotan's own range` -- written that way deliberately, naming no interval, because the interval is the thing this codebase has shipped wrong before and the rule does not need it stated to be described. Forty-three arms become thirty-three rules. The registry total goes 407 -> 355 and described rules 95 -> 200, both measured. Three tests changed, and each was asserting something the repoint improves: - `StepAsASentenceTest` used Trigonometric as its example of a set still named by rendered patterns. It now *finds* those sets rather than naming one, since which they are moves with every tranche. - `RewriteRecordingTest.AOneStepRewriteIsOneStep` asserted the switch arm's replacement text for sin^2 + cos^2. It now asserts the rule's name and identity, which is what a derivation reports -- and the two cases now show the contrast directly, Common named by its rendered pattern against Trigonometric named in words. - `DerivationPathTest.EveryStepNamesWhatDidIt` asserted a step's tier equals its set's. It is the weakest of the rewrites that fired, per #1110, and sin^2 + cos^2 is now `Sound` where Trigonometric is `SoundUnderAssumptions` -- which is the finer grain doing its job. Twenty-four of the thirty sets now describe what they run. Three of the six left are the CanonicalOrder family, which still runs its switch. The three others are Common (33 descriptions to port), Power (22) and InequalityEquality (11). Full suite: 9010 passed, 14 skipped, 0 failed. No public API change. Part of #746 tier 2 and #825.
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.
#746 tier 2's last piece of required infrastructure is "transformation metadata rich enough that
v5.0 can render a step as a sentence". The only check on that is to render one: metadata is rich
enough exactly when a sentence can be built from it, and nothing else settles it.
Every word comes off a rule
The clause is
RewriteRule.Namewith its hyphens replaced; the identity in brackets isRewriteRule.Description. There is no table of phrasings and no per-rule English written a secondtime, so a rule that is renamed or re-described says the new thing without this being touched.
That is only possible because the names were written as phrases —
dividing-by-a-quotient-multiplies-by-its-reciprocalis a clause, not an identifier. There are 293 of them and
EveryRuleWrittenAsDataIsNamedInProseholds all 293 to it, so a rule whose name stops reading as English is a name to fix rather than a
case for an exceptions table here.
Explain()is onRewriteStep,DerivationStepandDerivationPath— three additive methods, thewhole public surface change.
Three things the first version got wrong, each found by reading the output
It rendered every name as a clause, including the ones
RuleRegistryGeneratorinvents for aswitcharm — producing "Divf(Sinf(var any1), Cosf(var any1a)) when any1 == any1a, so sin(x) /cos(x) becomes tan(x)": a sentence that quotes a matcher at the reader while its own doc comment
said not to. The two kinds of name are now told apart exactly rather than heuristically — a
rendered pattern has capitals, brackets and spaces and cannot be mistaken for lower-case letters and
hyphens. A name that is not prose falls back to the identity ("By sin / cos = tan, …") and then to
naming the set.
It said "X becomes X" whenever two different trees printed alike —
1 + (x + y)against(1 + x) + y— which happened on two of the first six derivations tried. Saying so is betterthan hiding it: a reader who sees a step reshaping a chain without the printed form moving has learnt
something true about the engine, where a reader who sees
a becomes ahas learnt to distrust thefeature.
And the closing note said "The step holds under assumptions" over a path of four steps, one of
which was a rule set. Wrong twice over: it hid three steps, and it implied the other three were
justified. It now counts, and says what it is counting — "One of the 4 steps is a registered rule
set, and it holds under assumptions rather than universally."
A fourth came from the tests rather than from reading: a path of zero steps said "nothing was
done to it" where the search had reached five expressions and kept none. Those are not the same
fact, and the difference is the whole of what that sentence has to say.
A step is justified by the rule that fired
RewriteStep.SoundnessreadRuleSet.Soundnessand nothing else, so every rewrite of a setreported the same tier. A set's tier is the minimum over its rules, and one conditional rule is
enough to make a set of a hundred report as conditional: all thirty sets declare
SoundUnderAssumptionswhile 181 of the 322 rules written as data areSound.DerivationStep.Soundnesschanges for the same reason and one more — it is now the weakest tier anyrewrite that actually fired inside the step holds at, rather than its set's minimum over rules
the step may never have reached. A pass of nine unconditional rewrites and one conditional one is
still a conditional pass; a pass of ten unconditional ones now says so.
Both are silent changes and both are in
BREAKING-CHANGES.md, measured on a build. This closes what#746 tier 5 records as "justification is per rule set, not per step" and "no step renders as a
sentence".
A property that improves on its own
sin(x) / cos(x)currently renders as "By sin / cos = tan, …" becauseTrigonometricstill readsits arms from the
switch. When it is repointed — the work #1108 and #1109 have been doing set byset — the same derivation will read "Sine over cosine is the tangent (sin(a) / cos(a) = tan(a)),
so …" with nothing here changing. The rendering gets better as the rules become data, which is the
right dependency direction.
State
Full suite: 8979 passed, 14 skipped, 0 failed.
Part of #746 tier 2.