diff --git a/Sources/AngouriMath/Core/Transformations/Matching/MatchedRules.cs b/Sources/AngouriMath/Core/Transformations/Matching/MatchedRules.cs index ecfcbfe74..2395e8747 100644 --- a/Sources/AngouriMath/Core/Transformations/Matching/MatchedRules.cs +++ b/Sources/AngouriMath/Core/Transformations/Matching/MatchedRules.cs @@ -2120,14 +2120,16 @@ is var (divided, remainder) MatchPattern.Any("x"), (node, _) => Functions.Patterns.SortAndGroup( node, Sumf.LinearChildren(node), level, (a, b) => a + b), - Soundness.SoundUnderAssumptions), + Soundness.SoundUnderAssumptions, + description: "the terms of a sum are put in canonical order and like terms are grouped"), new MatchedRule( "a-difference-chain-is-sorted-and-grouped", MatchPattern.Any("x"), (node, _) => Functions.Patterns.SortAndGroup( node, Sumf.LinearChildren(node), level, (a, b) => a + b), - Soundness.SoundUnderAssumptions), + Soundness.SoundUnderAssumptions, + description: "a difference is read as a sum, whose terms are put in canonical order and grouped"), new MatchedRule( "a-product-chain-is-sorted-and-grouped", @@ -2136,49 +2138,56 @@ is var (divided, remainder) node, Mulf.LinearChildren(node), level, (a, b) => a * b), // Regrouping reads a quotient as a product with a negative power, which is the // same value wherever the divisor is not zero. - Soundness.SoundUnderAssumptions), + Soundness.SoundUnderAssumptions, + description: "the factors of a product are put in canonical order and like factors are grouped"), new MatchedRule( "a-quotient-chain-is-sorted-and-grouped", MatchPattern.Any("x"), (node, _) => Functions.Patterns.SortAndGroup( node, Mulf.LinearChildren(node), level, (a, b) => a * b), - Soundness.SoundUnderAssumptions), + Soundness.SoundUnderAssumptions, + description: "a quotient is read as a product, whose factors are put in canonical order and grouped"), new MatchedRule( "a-conjunction-chain-is-sorted-and-grouped", MatchPattern.Any("x"), (node, _) => Functions.Patterns.SortAndGroup( node, Andf.LinearChildren(node), level, (a, b) => a & b), - Soundness.Sound), + Soundness.Sound, + description: "the operands of a conjunction are put in canonical order and repeats are grouped"), new MatchedRule( "a-disjunction-chain-is-sorted-and-grouped", MatchPattern.Any("x"), (node, _) => Functions.Patterns.SortAndGroup( node, Orf.LinearChildren(node), level, (a, b) => a | b), - Soundness.Sound), + Soundness.Sound, + description: "the operands of a disjunction are put in canonical order and repeats are grouped"), new MatchedRule( "a-union-chain-is-sorted-and-grouped", MatchPattern.Any("x"), (node, _) => Functions.Patterns.SortAndGroup( node, Set.Unionf.LinearChildren(node), level, (a, b) => a.Unite(b)), - Soundness.Sound), + Soundness.Sound, + description: "the operands of a union are put in canonical order and repeats are grouped"), new MatchedRule( "an-intersection-chain-is-sorted-and-grouped", MatchPattern.Any("x"), (node, _) => Functions.Patterns.SortAndGroup( node, Set.Intersectionf.LinearChildren(node), level, (a, b) => a.Intersect(b)), - Soundness.Sound), + Soundness.Sound, + description: "the operands of an intersection are put in canonical order and repeats are grouped"), new MatchedRule( "an-exclusive-disjunction-chain-is-sorted-and-grouped", MatchPattern.Any("x"), (node, _) => Functions.Patterns.SortAndGroup( node, Xorf.LinearChildren(node), level, (a, b) => a ^ b), - Soundness.Sound)); + Soundness.Sound, + description: "the operands of an exclusive disjunction are put in canonical order and repeats are grouped")); /// /// , as data — again one diff --git a/Sources/AngouriMath/Core/Transformations/RewriteRules.cs b/Sources/AngouriMath/Core/Transformations/RewriteRules.cs index 23ee1cb1a..211c2136a 100644 --- a/Sources/AngouriMath/Core/Transformations/RewriteRules.cs +++ b/Sources/AngouriMath/Core/Transformations/RewriteRules.cs @@ -50,8 +50,7 @@ public static class RewriteRules // Regrouping reads a quotient as a product with a negative power, which is the // same value wherever the divisor is not zero. Soundness.SoundUnderAssumptions, - Patterns.SortRules(TreeAnalyzer.SortLevel.HIGH_LEVEL), - Patterns.SortRulesArms(TreeAnalyzer.SortLevel.HIGH_LEVEL), + Matching.MatchedRules.Sort(TreeAnalyzer.SortLevel.HIGH_LEVEL), isNormalization: true); /// @@ -63,8 +62,7 @@ public static class RewriteRules "Sorts and groups commutative operands, distinguishing terms by their constants too.", TransformationRelation.Equivalence, Soundness.SoundUnderAssumptions, - Patterns.SortRules(TreeAnalyzer.SortLevel.MIDDLE_LEVEL), - Patterns.SortRulesArms(TreeAnalyzer.SortLevel.MIDDLE_LEVEL), + Matching.MatchedRules.Sort(TreeAnalyzer.SortLevel.MIDDLE_LEVEL), isNormalization: true); /// @@ -76,8 +74,7 @@ public static class RewriteRules "Sorts and groups commutative operands by the whole subtree.", TransformationRelation.Equivalence, Soundness.SoundUnderAssumptions, - Patterns.SortRules(TreeAnalyzer.SortLevel.LOW_LEVEL), - Patterns.SortRulesArms(TreeAnalyzer.SortLevel.LOW_LEVEL), + Matching.MatchedRules.Sort(TreeAnalyzer.SortLevel.LOW_LEVEL), isNormalization: true); /// diff --git a/Sources/AngouriMath/Docs/Contributing/WritingARule.md b/Sources/AngouriMath/Docs/Contributing/WritingARule.md index e6bc37c8f..1ded6800b 100644 --- a/Sources/AngouriMath/Docs/Contributing/WritingARule.md +++ b/Sources/AngouriMath/Docs/Contributing/WritingARule.md @@ -20,10 +20,12 @@ effect". This is how to supply those seven things. `Core/Transformations/Matching/MatchedRules.cs`, as a value in a `MatchedRuleSet`. **33** sets and **324** rules live there today. -The `switch` statements in `Functions/Simplification/Patterns` are the older form. Twenty-seven of -the thirty registered sets no longer run theirs, and **none of those twenty-seven describes it any -more either**; the three that remain still run theirs, so describing it is honest -([#825](https://github.com/asc-community/AngouriMath/issues/825)). +The `switch` statements in `Functions/Simplification/Patterns` are the older form. **All thirty +registered sets now run as data and describe what they run**; none executes its `switch` any more. +The last three to move were the `CanonicalOrder` family, repointed at `MatchedRules.Sort(level)` — +which had existed as data, held to the `switch` by `MatchedRulesAgreeWithTheSwitchTest` at a hundred +firings per level, and been run by nothing. The `switch` forms stay because that agreement test is +what keeps the two spellings honest ([#825](https://github.com/asc-community/AngouriMath/issues/825)). **Do not add a rule to a `switch`.** A `switch` arm cannot carry a name, a tier, an identity or a direction, and everything below is about those. @@ -89,7 +91,7 @@ debugged for an afternoon. | `Left.ToString()` | `Divf(var a, Divf(var b, var c))` — how the matcher spells it | Write the identity with `=`, not `->`: it is an equality, and the arrow belongs to the direction the -rule happens to be applied in. **294** rules carry one today; a new rule should. +rule happens to be applied in. **321** rules carry one today; a new rule should. ## The pattern language diff --git a/Sources/Tests/UnitTests/Core/Transformations/AddressableRulesTest.cs b/Sources/Tests/UnitTests/Core/Transformations/AddressableRulesTest.cs index 193c29c82..444e780ed 100644 --- a/Sources/Tests/UnitTests/Core/Transformations/AddressableRulesTest.cs +++ b/Sources/Tests/UnitTests/Core/Transformations/AddressableRulesTest.cs @@ -299,7 +299,7 @@ public void TheRegistryIsAddressableAsFarAsItSaysItIs() // are thirty-three; NumericNeat's sixteen are eleven, six of them being three rules // written once per side a negative factor can sit on; and the two factorial sets are // eight arms each written as three. Every other repointed set is one arm for one rule. - Assert.Equal(315, withRules.Sum(set => set.Rules.Count)); + Assert.Equal(321, withRules.Sum(set => set.Rules.Count)); } /// diff --git a/Sources/Tests/UnitTests/Core/Transformations/RuleAuthoringGuideTest.cs b/Sources/Tests/UnitTests/Core/Transformations/RuleAuthoringGuideTest.cs index f8d34ea28..ba6874d1e 100644 --- a/Sources/Tests/UnitTests/Core/Transformations/RuleAuthoringGuideTest.cs +++ b/Sources/Tests/UnitTests/Core/Transformations/RuleAuthoringGuideTest.cs @@ -45,11 +45,16 @@ public void WhereARuleGoes() Stated(33, MatchedRules.All.Count, "the number of rule sets written as data"); Stated(324, MatchedRules.All.Sum(set => set.Rules.Count), "the number of rules written as data"); Stated(30, RewriteRules.All.Count, "the number of registered sets"); - Stated(27, RewriteRules.All.Count(set => + // Two families register under one name and run a data set under another -- + // CommonDenominator over MatchedRules.CommonDenominator(level), CanonicalOrder over + // MatchedRules.Sort(level) -- so a name match alone undercounts them. This said 27 + // while the truth was 30 for exactly as long as the second family was unlisted here. + Stated(30, RewriteRules.All.Count(set => MatchedRules.All.Any(data => data.Name == set.Name) - || set.Name.StartsWith("CommonDenominator", StringComparison.Ordinal)), + || set.Name.StartsWith("CommonDenominator", StringComparison.Ordinal) + || set.Name.StartsWith("CanonicalOrder", StringComparison.Ordinal)), "how many registered sets run the matcher"); - Stated(27, RewriteRules.All.Count(set => + Stated(30, RewriteRules.All.Count(set => set.Rules.Count > 0 && set.Rules.All(rule => rule.Soundness is not null)), "how many registered sets describe what they run"); } @@ -68,7 +73,7 @@ public void TheNameIsASentence() [Fact] public void TheIdentityIsNotTheName() - => Stated(294, + => Stated(321, MatchedRules.All.SelectMany(set => set.Rules).Count(rule => rule.Description is not null), "how many rules carry an identity"); diff --git a/Sources/Tests/UnitTests/Core/Transformations/RuleMetadataTest.cs b/Sources/Tests/UnitTests/Core/Transformations/RuleMetadataTest.cs index 195864bc6..b18b1d4f8 100644 --- a/Sources/Tests/UnitTests/Core/Transformations/RuleMetadataTest.cs +++ b/Sources/Tests/UnitTests/Core/Transformations/RuleMetadataTest.cs @@ -22,11 +22,14 @@ namespace AngouriMath.Tests.Core.Transformations /// /// /// - /// The registry runs one thing and describes another. 27 of the 30 sets execute - /// , and 29 of them take their - /// from RuleRegistryGenerator reading the - /// switch those sets no longer run. That is - /// #825's open half. + /// The registry used to run one thing and describe another. When this was written, + /// 27 of the 30 sets executed while 29 took their + /// from RuleRegistryGenerator reading a + /// switch they no longer ran — #825's + /// open half. Both numbers are now 30 and 0: every registration passes its + /// to the constructor that takes one, which runs it and + /// describes it from the same object. The last three to move were the CanonicalOrder + /// family, and the counts below moved with them. /// /// /// This file is about the three things that had to be true of @@ -112,6 +115,9 @@ public void ARuleWrittenAsDataCarriesItsTierIntoTheRegistry() var fromData = new[] { nameof(RewriteRules.Boolean), + nameof(RewriteRules.CanonicalOrder), + nameof(RewriteRules.CanonicalOrderCountingConstants), + nameof(RewriteRules.CanonicalOrderExact), nameof(RewriteRules.CollapseMultipleFractions), nameof(RewriteRules.Common), nameof(RewriteRules.CollapseTrigonometricFunctions), @@ -168,7 +174,7 @@ public void EveryRuleOfARepointedSetCarriesItsIdentity() var repointed = RewriteRules.All .Where(set => set.Rules.Count > 0 && set.Rules.All(rule => rule.Soundness is not null)) .ToList(); - Assert.Equal(294, repointed.Sum(set => set.Rules.Count)); + Assert.Equal(321, repointed.Sum(set => set.Rules.Count)); Assert.All(repointed, set => Assert.All(set.Rules, rule => Assert.NotNull(rule.Description))); } diff --git a/Sources/Tests/UnitTests/Core/Transformations/StepAsASentenceTest.cs b/Sources/Tests/UnitTests/Core/Transformations/StepAsASentenceTest.cs index 53fa4905a..bcaa543c0 100644 --- a/Sources/Tests/UnitTests/Core/Transformations/StepAsASentenceTest.cs +++ b/Sources/Tests/UnitTests/Core/Transformations/StepAsASentenceTest.cs @@ -63,19 +63,20 @@ public void EveryRuleWrittenAsDataIsNamedInProse() Assert.All(names, name => Assert.True(Explanation.IsProse(name), $"'{name}' is a rule name that does not read as a phrase in English")); - // And a generated one is not mistaken for prose. Which sets those are moves as the - // registry is repointed set by set, so they are found rather than named: a set whose - // rules carry no tier of their own is one still described by `RuleRegistryGenerator`, - // and every name it gives is a rendered pattern. Naming one instead cost a failure the - // day Trigonometric was repointed, which is the argument for asking. + // And no set is described by `RuleRegistryGenerator` any more. A set whose rules + // carry no tier of their own was one still described by the generator, and every name + // it gave was a rendered pattern rather than prose; this used to assert that such + // sets existed and that their names did not pass for English. The last three -- + // the CanonicalOrder family -- were repointed at `MatchedRules.Sort`, so the + // population is empty, and that is asserted rather than the assertion being deleted: + // a set falling back to the generator would be a regression this should catch. var generated = RewriteRules.All .Where(set => set.Rules.Count > 0 && set.Rules.All(rule => rule.Soundness is null)) - .SelectMany(set => set.Rules) - .Select(rule => rule.Name) + .Select(set => set.Name) .ToList(); - Assert.NotEmpty(generated); - Assert.All(generated, name => Assert.False(Explanation.IsProse(name), - $"'{name}' is a rendered pattern and was taken for prose")); + Assert.True(generated.Count == 0, + "these sets are described by the generator again, and their rule names are " + + "rendered patterns rather than prose: " + string.Join(", ", generated)); } ///