Skip to content

A step renders as a sentence - #1110

Merged
Rafael-SOWNet merged 1 commit into
masterfrom
step-as-a-sentence
Aug 30, 2026
Merged

A step renders as a sentence#1110
Rafael-SOWNet merged 1 commit into
masterfrom
step-as-a-sentence

Conversation

@Rafael-SOWNet

Copy link
Copy Markdown
Collaborator

#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.

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 — dividing-by-a-quotient-multiplies-by-its-reciprocal
is a clause, not an identifier. There are 293 of them and EveryRuleWrittenAsDataIsNamedInProse
holds 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 on RewriteStep, DerivationStep and DerivationPath — three additive methods, the
whole 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 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 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 better
than 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 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. 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.Soundness read RuleSet.Soundness and nothing else, so every rewrite of a set
reported 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
SoundUnderAssumptions while 181 of the 322 rules written as data are Sound.

DerivationStep.Soundness changes for the same reason and one more — it 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 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, …" because Trigonometric still reads
its arms from the switch. When it is repointed — the work #1108 and #1109 have been doing set by
set — 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.

#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
Rafael-SOWNet merged commit 01e00a5 into master Aug 30, 2026
31 checks passed
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant