@@ -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