Skip to content

Commit 4efe927

Browse files
The growth corpus reaches 194 rules instead of 84 (#1160)
A declaration is only checked if the corpus makes the rule fire, and the arithmetic grammar reached 84 of the 324. Six whole sets -- trigonometry, booleans, sets, comparisons and both factorial ones -- were never exercised at all. Rules are now tried at every node rather than only at the root, and the shapes the generated grammar does not build are supplied on top of it: the trigonometric identities, logarithms, factorials, the boolean laws, set operations, the polynomial shapes, and the comparison shapes that key on zero or a number being on the left. Also a comparison standing inside a comparison. The rules about comparisons bind their operands with Any, so one can be a comparison itself, and building the replacement with < or > then chains: (x > y) < 0 is x > y and y < 0, seven nodes rather than three. Without those shapes a rule whose replacement chains looks like a clean shrink at every point the corpus reaches and is not one. 194 rules now fire, 59 of them carrying a declaration, against 84 and 18 before. No declaration is contradicted by any of the new coverage. Full suite 9552 passed, 0 failed. Part of #825. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura
1 parent 7c642ce commit 4efe927

1 file changed

Lines changed: 102 additions & 9 deletions

File tree

‎Sources/Tests/UnitTests/Core/Transformations/RuleGrowthAgreesWithTheCorpusTest.cs‎

Lines changed: 102 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,98 @@ public sealed class RuleGrowthAgreesWithTheCorpusTest
5050
{ "({0}) + ({1})", "({0}) - ({1})", "({0}) * ({1})", "({0}) / ({1})", "({0}) ^ ({1})" };
5151

5252
/// <summary>
53-
/// The same shape as the corpus <c>MatchedRulesAgreeWithTheSwitchTest</c> builds, and for
54-
/// the same reason: generated inputs reach arrangements nobody would think to write down.
53+
/// Shapes the arithmetic grammar does not build. Without them the corpus reaches the sets
54+
/// about quotients and powers and none of the six about trigonometry, booleans, sets,
55+
/// comparisons or factorials — so those rules' declarations would be checked by a test
56+
/// that never ran them.
5557
/// </summary>
58+
private static readonly string[] Reached =
59+
{
60+
// trigonometric, and the arguments the multiple-angle and collapse rules want
61+
"sin(x)", "cos(x)", "tan(x)", "cotan(x)", "sec(x)", "cosec(x)",
62+
"sin(2 * x)", "cos(2 * x)", "sin(x + y)", "cos(x + y)", "sin(3 * x)", "cos(3 * x)",
63+
"sin(x) * cos(x)", "cos(x) * sin(x)", "sin(x) ^ 2 + cos(x) ^ 2", "cos(x) ^ 2 + sin(x) ^ 2",
64+
"sin(x) / cos(x)", "cos(x) / sin(x)", "1 / sin(x)", "1 / cos(x)", "1 / tan(x)",
65+
"tan(x) * cotan(x)", "sin(x) ^ 2", "cos(x) ^ 2", "sin(x) ^ 2 - cos(x) ^ 2",
66+
"arcsin(x)", "arccos(x)", "arctan(x)", "arccotan(x)",
67+
"arcsin(x) + arccos(x)", "arctan(x) + arccotan(x)", "sin(arcsin(x))", "cos(arccos(x))",
68+
69+
// logarithms and exponentials
70+
"ln(x)", "log(2, x)", "ln(x * y)", "ln(x ^ 2)", "e ^ ln(x)", "ln(e ^ x)",
71+
"log(x, x)", "2 ^ x * 2 ^ y", "e ^ x * e ^ y",
72+
73+
// factorials, both sets
74+
"x!", "(x + 1)!", "(x + 1)! / x!", "x! / (x + 1)!", "x! * (x + 1)", "(x + 1) * x!",
75+
"(x + 2)! / x!",
76+
77+
// boolean
78+
"a and b", "a or b", "not a", "not (not a)", "a implies b", "a xor b",
79+
"a and not a", "a or not a", "a and a", "a or a", "a and (a or b)", "a or (a and b)",
80+
"not (a and b)", "not (a or b)", "a and (b or c)", "(a and b) or (a and c)",
81+
"true and a", "false or a", "true or a", "false and a",
82+
83+
// comparisons and the equality set
84+
"x > y", "x < y", "x >= y", "x <= y", "x = y", "(x < y) or (x = y)", "(x > y) or (x = y)",
85+
"not (x > y)", "not (x = y)",
86+
87+
// sets
88+
"x in [1; 2]", "[1; 2] \\/ [3; 4]", "[1; 2] /\\ [3; 4]", "[1; 2] \\ [3; 4]",
89+
"A \\/ A", "A /\\ A", "A \\/ {}", "A /\\ {}", "{ t : t > 0 }",
90+
91+
// powers and roots the arithmetic grammar reaches only shallowly
92+
"sqrt(x) * sqrt(x)", "sqrt(x ^ 2)", "(x ^ 2) ^ 3", "x ^ 2 * x ^ 3", "x ^ 3 / x ^ 2",
93+
"x ^ 2 * y ^ 2", "(x / y) ^ 2", "x ^ (1/2) * x ^ (1/2)", "(-x) ^ 2",
94+
"1 / x ^ (-2)", "x ^ 0", "0 ^ x",
95+
96+
// polynomial shapes for the division, gcd and factorisation sets
97+
"(x ^ 2 - 1) / (x - 1)", "(x ^ 2 + 2 * x + 1) / (x + 1)", "x ^ 2 - 1",
98+
"x ^ 2 + 2 * x + 1", "x ^ 3 - 1", "x ^ 2 - y ^ 2", "x ^ 4 - y ^ 4",
99+
"x ^ 2 + 2 * x * y + y ^ 2", "(x + y) ^ 2", "(x + y) * (x - y)",
100+
"1 / (1 + sqrt(2))", "1 / (sqrt(3) - 1)", "2 / (1 + sqrt(x))",
101+
102+
// Comparisons. The largest block of rules the arithmetic grammar cannot reach at all:
103+
// they key on zero or a number being on the left, on both sides being the same thing,
104+
// and on a negative factor or divisor standing beside a zero.
105+
"0 > x", "0 < x", "0 >= x", "0 <= x", "0 = x",
106+
"2 > x", "2 < x", "2 >= x", "2 <= x", "2 = x",
107+
"x > x", "x < x", "x >= x", "x <= x",
108+
"x / (-2) > 0", "x / (-2) < 0", "x / (-2) >= 0", "x / (-2) <= 0", "x / (-2) = 0",
109+
"(-2) * x > 0", "(-2) * x < 0", "(-2) * x >= 0", "(-2) * x <= 0", "(-2) * x = 0",
110+
"x * (-2) > 0", "x * (-2) < 0", "x * (-2) >= 0", "x * (-2) <= 0", "x * (-2) = 0",
111+
"(x > y) and (y > z)", "(x < y) and (y < z)",
112+
"not (x >= y)", "not (x <= y)", "not (x < y)", "y > x", "y >= x",
113+
114+
// Boolean, both directions of De Morgan and the absorptions with a negation.
115+
"not a and not b", "not a or not b", "a and not b", "a or not b",
116+
"not a or b", "not a and b", "a and (b and c)", "a or (b or c)",
117+
"(a and b) and c", "(a or b) or c", "(a or b) and (a or c)", "(a and b) or c",
118+
119+
// The trigonometric reciprocal pairs and the inverses the shapes above miss.
120+
"cosec(x) * sin(x)", "sin(x) * cosec(x)", "sec(x) * cos(x)", "cos(x) * sec(x)",
121+
"cotan(arccotan(x))", "tan(arctan(x))", "arcsin(sin(x))", "arctan(tan(x))",
122+
"sin(2 * x) * cosec(x)", "cos(2 * x) * sec(x)", "cotan(x) * tan(x)",
123+
124+
// Logarithms in and of reciprocals.
125+
"log(1/2, x)", "log(2, 1/x)", "log(1/2, 1/x)", "log(1/x, y)", "log(x, 1/y)",
126+
127+
// A comparison standing where a number would. The rules about comparisons bind their
128+
// operands with `Any`, so one of them can be a comparison itself -- and building the
129+
// replacement with `<` or `>` then *chains*, where the same rule written with
130+
// `EqualTo` would not: `(x > y) < 0` is `x > y and y < 0`, seven nodes rather than
131+
// three. Without these a rule whose replacement chains looks like a clean shrink at
132+
// every point the corpus reaches and is not one.
133+
"(x > y) < 0", "(x > y) > 0", "(x > y) <= 0", "(x > y) >= 0",
134+
"(x > y) / (-2) > 0", "(x > y) / (-2) < 0", "(x < y) / (-2) >= 0",
135+
"(-2) * (x > y) > 0", "(x > y) * (-2) < 0", "0 > (x > y)", "0 < (x < y)",
136+
"2 > (x > y)", "(x > y) = 0",
137+
138+
// A factorial beside a zero, and the differences and products that contain their own
139+
// operand a second time.
140+
"x! = 0", "(x - y) / (y - x)", "x - (x - y)", "(x - y) - x",
141+
"x * y - x", "x - x * y", "x * x", "x * x * y", "x * (x * y)",
142+
"sin(x) * 2", "2 * sin(x)",
143+
};
144+
56145
private static List<Entity> Corpus()
57146
{
58147
var level1 = new List<string>(Leaves);
@@ -71,7 +160,7 @@ private static List<Entity> Corpus()
71160
level3.Add(string.Format(shape, left, right));
72161

73162
var parsed = new List<Entity>();
74-
foreach (var source in level1.Concat(level2).Concat(level3))
163+
foreach (var source in level1.Concat(level2).Concat(level3).Concat(Reached))
75164
{
76165
try { parsed.Add(source.ToEntity()); }
77166
catch { /* the generator makes some strings the parser declines */ }
@@ -88,7 +177,9 @@ private static List<Entity> Corpus()
88177
/// </summary>
89178
private static Dictionary<MatchedRule, List<(Entity Before, Entity After)>> Firings()
90179
{
91-
var corpus = Corpus();
180+
// Every node, not only the root: a rule matches where its shape sits, and asking
181+
// only about whole corpus expressions leaves most rules never firing at all.
182+
var corpus = Corpus().SelectMany(expr => expr.Nodes).Distinct().ToList();
92183
var firings = new Dictionary<MatchedRule, List<(Entity, Entity)>>();
93184
foreach (var set in MatchedRules.All)
94185
foreach (var rule in set.Rules)
@@ -148,11 +239,13 @@ public void TheCorpusExercisesTheDeclarationsItChecks()
148239
var fired = Firings().Where(pair => pair.Value.Count > 0).ToList();
149240
var declared = fired.Count(pair => pair.Key.Growth is not RewriteRuleGrowth.Unknown);
150241

151-
// Named as counts that move rather than as "most of them". 84 rules reach the corpus,
152-
// 18 of them carrying a declaration -- so the check above is exercised, and the other
153-
// 66 are the measured evidence for declaring more of them.
154-
Assert.True(fired.Count >= 80, $"only {fired.Count} rules fired at all");
155-
Assert.True(declared >= 15, $"only {declared} rules with a declared growth fired");
242+
// Named as counts that move rather than as "most of them". 194 of the 324 rules reach
243+
// the corpus and 59 of those carry a declaration, so the check above is exercised
244+
// rather than passing by never running; the rest are the measured evidence for
245+
// declaring more of them. It was 84 and 18 before the shapes in `Reached` were added
246+
// and before rules were tried at every node rather than only at the root.
247+
Assert.True(fired.Count >= 190, $"only {fired.Count} rules fired at all");
248+
Assert.True(declared >= 56, $"only {declared} rules with a declared growth fired");
156249
}
157250
}
158251
}

0 commit comments

Comments
 (0)