diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index c194d9725..b139f4ba3 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -15,6 +15,87 @@ read first. --- +## Unreleased — since 2.3.0 + +### At a glance + +| Silent? | What | Was | Is | +|---|---|---|---| +| **Silent** | `"a implies (b implies c)".ToEntity().Stringize()` | `a implies b implies c`, which reads back as `(a implies b) implies c` | `a implies (b implies c)` | +| | `"(a implies b) implies c".ToEntity().Stringize()` | `(a implies b) implies c` | `a implies b implies c` | +| **Silent** | `@"A \ (B \ C)".ToEntity().Stringize()`, and `Latexize` | `A \ B \ C` / `A \setminus B \setminus C` | `A \ (B \ C)` / `A \setminus \left(B \setminus C\right)` | +| **Silent** | `@"A \ (B \/ C)".ToEntity().Stringize()`, and `Latexize` | `A \ B \/ C` / `A \setminus B \cup C` | `A \ (B \/ C)` / `A \setminus \left(B \cup C\right)` | +| **Silent** | `@"A \/ (B \ C)".ToEntity().Stringize()`, and `Latexize` | `A \/ B \ C` / `A \cup B \setminus C` | `A \/ (B \ C)` / `A \cup \left(B \setminus C\right)` | +| **Silent** | `"(x provided p) provided q".ToEntity().Stringize()`, and `Latexize` | `x provided p provided q`, which reads back as `x provided (p provided q)` | `(x provided p) provided q` | +| **Silent** | `"a in (b in c)".ToEntity().Stringize()`, and `Latexize` | `a in b in c` / `a \in b \in c` | `a in (b in c)` / `a \in \left(b \in c\right)` | +| **Silent** | `"x * (y mod z)".ToEntity().Stringize()`, and `Latexize` | `x * y mod z` / `x y \bmod z` | `x * (y mod z)` / `x \left(y \bmod z\right)` | +| **Silent** | `"-1 * (y mod z)".ToEntity().Stringize()` | `-y mod z` | `-(y mod z)` | + +### A printed operator that is not associative keeps its brackets + +`Stringize` is the library's own input format: parsing what it prints has to give back the +expression it printed, and that is what [`StringizeRoundTripTest`](Sources/Tests/UnitTests/Convenience/StringizeRoundTripTest.cs) +enforces. Six operators broke it, five of them in the same way — the printer left the **right** +operand unbracketed at its own precedence level, while the grammar folds that level to the left, so +the printed text came back re-associated. The sixth, `provided`, is the mirror: it is the one infix +operator the grammar folds to the **right**, so there it is the *left* operand that mis-associates. + +Four of the six are not associative, so the re-association changed the answer and not merely +the shape. Measured on a build of 2.3.0 and a build of this branch: + +| input | 2.3.0 printed | 2.3.0 read that back as | value moved | +|---|---|---|---| +| `false implies (true implies false)` | `False implies True implies False` | `(False implies True) implies False` | `True` → `False` | +| `{ 1, 2, 3 } \ ({ 2, 3 } \ { 3 })` | `{ 1, 2, 3 } \ { 2, 3 } \ { 3 }` | `({ 1, 2, 3 } \ { 2, 3 }) \ { 3 }` | `{ 1, 3 }` → `{ 1 }` | +| `{ 1, 2 } \/ ({ 3 } \ { 1, 2 })` | `{ 1, 2 } \/ { 3 } \ { 1, 2 }` | `({ 1, 2 } \/ { 3 }) \ { 1, 2 }` | `{ 1, 2, 3 }` → `{ 3 }` | +| `{ 1, 2 } \ ({ 2 } \/ { 3 })` | `{ 1, 2 } \ { 2 } \/ { 3 }` | `({ 1, 2 } \ { 2 }) \/ { 3 }` | `{ 1 }` → `{ 1, 3 }` | +| `2 * (3 mod 2)` | `2 * 3 mod 2` | `(2 * 3) mod 2` | `2` → `0` | + +On this branch each of those prints its brackets and reads back as itself, so the value column +no longer moves. + +`implies` also changes in the other direction, because its printer had the rule the wrong way +round rather than missing: it bracketed the **assumption** — the operand that never needs it under +a left fold — and not the conclusion. So `(a implies b) implies c`, which used to print as +`(a implies b) implies c`, now prints flat as `a implies b implies c`. Both read back as the same +expression; the brackets were redundant, and printing them is what made the genuinely ambiguous +case look no different. + +`\/` and `mod` are bracketed only against the operator that makes them ambiguous, because both +share a precedence level with an operator that is not associative while being associative +themselves: + +- `A \/ (B \/ C)` still prints `A \/ B \/ C`; `A \/ (B \ C)` now prints `A \/ (B \ C)`. +- `x * (y * z)` still prints `x * y * z` and `x * (y / z)` still prints `x * y / z`; + `x * (y mod z)` now prints `x * (y mod z)`. +- `-1 * (y mod z)` printed `-y mod z` and now prints `-(y mod z)`. + +`provided` is bracketed on the other side, and it is the case where the value is *not* the test. +`(x provided p) provided q` and `x provided (p provided q)` are both `x` exactly when `p` and `q` +hold, so no answer moves — and they are different expressions, which is what the round trip is +about. It printed flat and read back as the right-nested one. + +**What has not changed, deliberately.** An operator that *is* associative still prints flat, so +`x + (y + z)` still prints `x + y + z`, and likewise for `*`, `and`, `or`, `xor`, `unite` and +`intersect`. Those come back as a different `Entity` — a left-nested tree instead of +a right-nested one — and as the same number, because the bracketing carries no mathematics. The +alternative would print every expanded polynomial as a right-nested pile of parentheses: +`"(a + b + c + d) ^ 2".ToEntity().Expand()` prints, on both versions, +`d ^ 2 + 2 * c * d + c ^ 2 + 2 * b * d + 2 * b * c + b ^ 2 + 2 * a * d + 2 * a * c + 2 * a * b + a ^ 2`, +and its sum is right-nested throughout. `StringizeRoundTripTest` pins that decision so that it +stays one. + +**LaTeX.** `Latexize` moves for the four set and `mod` cases above and **not** for `implies`. +[CSharpMath.Evaluation](https://github.com/verybadcat/CSharpMath/blob/master/CSharpMath.Evaluation/Evaluation.cs), +which reads our LaTeX back, folds `\cup`, `\setminus`, `\in`, `\cdot` and `\bmod` to the left at +the same precedences this grammar uses — so those needed the same brackets — but folds `\to` to the +**right**, which is the usual convention for implication and is what `Latexize` was already +bracketing for. The change only ever adds `\left(`/`\right)` groups, which CSharpMath already +parses, so nothing downstream needs a matching change +([#822](https://github.com/asc-community/AngouriMath/issues/822)). + +--- + ## 2.3.0 — since 2.2.0 ### At a glance diff --git a/Sources/AngouriMath/Docs/Usage/Syntax.md b/Sources/AngouriMath/Docs/Usage/Syntax.md index af2d04094..dfbbadbcd 100644 --- a/Sources/AngouriMath/Docs/Usage/Syntax.md +++ b/Sources/AngouriMath/Docs/Usage/Syntax.md @@ -1,4 +1,4 @@ -# Expression syntax +# Expression syntax What `MathS.FromString` accepts, and what `Entity.Stringize` produces. The authority is [`Core/Antlr/AngouriMath.g`](../../Core/Antlr/AngouriMath.g); this page is a reading of it. To @@ -22,12 +22,17 @@ will say so ([#822](https://github.com/asc-community/AngouriMath/issues/822)). ## Operators, loosest first -Everything on one line has the same precedence and groups to the left, except `^`, which groups -to the right. +Everything on one line has the same precedence and groups to the left, except `^` and `provided`, +which group to the right. + +Grouping is not decoration: an operator that is not associative prints the brackets it has, so +`a implies (b implies c)` prints with them and `(a implies b) implies c` prints without. Where the +operator *is* associative the brackets carry no mathematics and are not printed, so `x + (y + z)` +prints as `x + y + z` and reads back as `(x + y) + z` — the same number, a differently shaped tree. | | operators | notes | |---|---|---| -| 1 | `provided` | | +| 1 | `provided` | **groups to the right**: `a provided b provided c` is `a provided (b provided c)` | | 2 | `implies` | | | 3 | `or` `\|` | | | 4 | `xor` | | @@ -35,10 +40,10 @@ to the right. | 6 | `not` | prefix | | 7 | `=` `<>` `>` `>=` `<` `<=` | chained: `a < b < c` means `a < b and b < c` | | 8 | `in` | | -| 9 | `unite` `\/`, `setsubtract` `\` | | +| 9 | `unite` `\/`, `setsubtract` `\` | one level, so `A \/ B \ C` is `(A \/ B) \ C` | | 10 | `intersect` `/\` | | | 11 | `+` `-` | | -| 12 | `*` `/` `mod` | | +| 12 | `*` `/` `mod` | one level, so `x * y mod z` is `(x * y) mod z` | | 13 | `+` `-` | prefix | | 14 | `^` | **groups to the right**: `a ^ b ^ c` is `a ^ (b ^ c)` | | 15 | `!` | postfix factorial | diff --git a/Sources/AngouriMath/Functions/Output/Latex/Latex.Arithmetics.Classes.cs b/Sources/AngouriMath/Functions/Output/Latex/Latex.Arithmetics.Classes.cs index 7e0ccc40e..2257437bd 100644 --- a/Sources/AngouriMath/Functions/Output/Latex/Latex.Arithmetics.Classes.cs +++ b/Sources/AngouriMath/Functions/Output/Latex/Latex.Arithmetics.Classes.cs @@ -49,10 +49,13 @@ public override string Latexize() }; case 1: if (longArray[index - 1] is Integer(-1)) - return $"-{currIn.Latexize(currIn.LatexPriority < LatexPriority)}"; // display "-1 * x * y" as "-x \cdot y", only for the first -1 + return $"-{currIn.Latexize(currIn.LatexPriority < LatexPriority || currIn is Modf)}"; // display "-1 * x * y" as "-x \cdot y", only for the first -1 break; } - var currOut = currIn.Latexize(currIn.LatexPriority < LatexPriority); + // A factor other than the first that is a `mod` needs brackets: \bmod sits + // at this precedence level and is not associative, so "x \cdot y \bmod z" + // reads as "(x y) mod z". 2 * (3 mod 2) is 2 and (2 * 3) mod 2 is 0. + var currOut = currIn.Latexize(currIn.LatexPriority < LatexPriority || currIn is Modf); return (longArray[index - 1], currIn) switch // whether we use juxtaposition and omit \cdot { diff --git a/Sources/AngouriMath/Functions/Output/Latex/Latex.Omni.Classes.cs b/Sources/AngouriMath/Functions/Output/Latex/Latex.Omni.Classes.cs index ad97a0428..00a9f5f9f 100644 --- a/Sources/AngouriMath/Functions/Output/Latex/Latex.Omni.Classes.cs +++ b/Sources/AngouriMath/Functions/Output/Latex/Latex.Omni.Classes.cs @@ -50,8 +50,12 @@ public override string Latexize() partial record Unionf { /// + // CSharpMath.Evaluation reads \cup and \setminus at one precedence level and folds + // them to the left, exactly as the grammar here does, so a \setminus on the right + // needs bracketing or it comes back as the outer operator. Union with union is + // associative and stays flat. public override string Latexize() - => $@"{Left.Latexize(Left.LatexPriority < LatexPriority)} \cup {Right.Latexize(Right.LatexPriority < LatexPriority)}"; + => $@"{Left.Latexize(Left.LatexPriority < LatexPriority)} \cup {Right.Latexize(Right.LatexPriority < LatexPriority || Right is SetMinusf)}"; } partial record Intersectionf @@ -64,22 +68,27 @@ public override string Latexize() partial record SetMinusf { /// + // Not associative, and sharing a precedence level with union: anything at that + // level on the right is bracketed, the same rule \frac's text form follows. public override string Latexize() - => $@"{Left.Latexize(Left.LatexPriority < LatexPriority)} \setminus {Right.Latexize(Right.LatexPriority < LatexPriority)}"; + => $@"{Left.Latexize(Left.LatexPriority < LatexPriority)} \setminus {Right.Latexize(Right.LatexPriority <= LatexPriority)}"; } partial record Inf { /// + // \in is left-folded by CSharpMath too, and membership is not associative. public override string Latexize() - => $@"{Element.Latexize(Element.LatexPriority < LatexPriority)} \in {SupSet.Latexize(SupSet.LatexPriority < LatexPriority)}"; + => $@"{Element.Latexize(Element.LatexPriority < LatexPriority)} \in {SupSet.Latexize(SupSet.LatexPriority <= LatexPriority)}"; } } partial record Providedf { /// - public override string Latexize() => $@"{Expression.Latexize(Expression.LatexPriority < LatexPriority)} \quad \text{{for}} \quad {Predicate.Latexize(Predicate.LatexPriority < LatexPriority)}"; + // Right-folding, so the left operand is the one that needs bracketing -- see the + // remark on Stringize. + public override string Latexize() => $@"{Expression.Latexize(Expression.LatexPriority <= LatexPriority)} \quad \text{{for}} \quad {Predicate.Latexize(Predicate.LatexPriority < LatexPriority)}"; } partial record Piecewise diff --git a/Sources/AngouriMath/Functions/Output/ToString/ToString.Arithmetics.Classes.cs b/Sources/AngouriMath/Functions/Output/ToString/ToString.Arithmetics.Classes.cs index be2782930..f549f2769 100644 --- a/Sources/AngouriMath/Functions/Output/ToString/ToString.Arithmetics.Classes.cs +++ b/Sources/AngouriMath/Functions/Output/ToString/ToString.Arithmetics.Classes.cs @@ -30,10 +30,14 @@ public override string Stringize() => public partial record Mulf { /// + // Multiplication is associative, so a product or a quotient on the right may stay + // unbracketed -- but `mod` shares this precedence level and is not associative, so a + // `mod` on the right must be bracketed or it is re-read as the outer operator: + // `2 * (3 mod 2)` is 2 and `(2 * 3) mod 2` is 0. public override string Stringize() => (Multiplier is Integer(-1) && !MathS.Diagnostic.OutputExplicit ? "-" : Multiplier.Stringize(Multiplier.Priority < Priority) + " * ") - + Multiplicand.Stringize(Multiplicand.Priority < Priority); + + Multiplicand.Stringize(Multiplicand.Priority < Priority || Multiplicand is Modf); /// public override string ToString() => Stringize(); } diff --git a/Sources/AngouriMath/Functions/Output/ToString/ToString.Discrete.Classes.cs b/Sources/AngouriMath/Functions/Output/ToString/ToString.Discrete.Classes.cs index 6c952b0dc..9f305e7bf 100644 --- a/Sources/AngouriMath/Functions/Output/ToString/ToString.Discrete.Classes.cs +++ b/Sources/AngouriMath/Functions/Output/ToString/ToString.Discrete.Classes.cs @@ -55,8 +55,13 @@ public override string Stringize() partial record Impliesf { /// + // The grammar folds `implies` to the left, like everything but `^` and `provided`, so + // it is the *conclusion* that needs bracketing when it is an implication of its own. + // The two are not interchangeable: `false implies (true implies false)` is true, and + // `(false implies true) implies false` is false, so printing the first without its + // brackets said the opposite of what it meant. public override string Stringize() - => $"{Assumption.Stringize(Assumption.Priority <= Priority)} implies {Conclusion.Stringize(Conclusion.Priority < Priority)}"; + => $"{Assumption.Stringize(Assumption.Priority < Priority)} implies {Conclusion.Stringize(Conclusion.Priority <= Priority)}"; /// public override string ToString() => Stringize(); } diff --git a/Sources/AngouriMath/Functions/Output/ToString/ToString.Omni.Classes.cs b/Sources/AngouriMath/Functions/Output/ToString/ToString.Omni.Classes.cs index a69393263..8cd273c52 100644 --- a/Sources/AngouriMath/Functions/Output/ToString/ToString.Omni.Classes.cs +++ b/Sources/AngouriMath/Functions/Output/ToString/ToString.Omni.Classes.cs @@ -88,8 +88,13 @@ partial record Complexes partial record Unionf { /// + // Union is associative, so a union on the right may stay unbracketed -- but `\/` + // and `\` share one precedence level and are folded by one loop in the grammar, so + // a *set difference* on the right must be bracketed or it is re-read as the outer + // operator: `{ 1, 2 } \/ ({ 3 } \ { 1, 2 })` is { 1, 2, 3 } and + // `({ 1, 2 } \/ { 3 }) \ { 1, 2 }` is { 3 }. public override string Stringize() - => $@"{Left.Stringize(Left.Priority < Priority)} \/ {Right.Stringize(Right.Priority < Priority)}"; + => $@"{Left.Stringize(Left.Priority < Priority)} \/ {Right.Stringize(Right.Priority < Priority || Right is SetMinusf)}"; /// public override string ToString() => Stringize(); } @@ -106,8 +111,12 @@ public override string Stringize() partial record SetMinusf { /// + // Set difference is not associative, and it shares its precedence level with + // union, so anything at that level on the right needs bracketing -- the same rule + // `-` and `/` follow. `{1,2,3} \ ({2,3} \ {3})` is { 1, 3 } where + // `({1,2,3} \ {2,3}) \ {3}` is { 1 }. public override string Stringize() - => $@"{Left.Stringize(Left.Priority < Priority)} \ {Right.Stringize(Right.Priority < Priority)}"; + => $@"{Left.Stringize(Left.Priority < Priority)} \ {Right.Stringize(Right.Priority <= Priority)}"; /// public override string ToString() => Stringize(); } @@ -115,8 +124,10 @@ public override string Stringize() partial record Inf { /// + // `in` is folded to the left and is not associative: `(a in b) in c` asks whether + // a truth value is an element of c, `a in (b in c)` whether a is an element of one. public override string Stringize() - => $@"{Element.Stringize(Element.Priority < Priority)} in {SupSet.Stringize(SupSet.Priority < Priority)}"; + => $@"{Element.Stringize(Element.Priority < Priority)} in {SupSet.Stringize(SupSet.Priority <= Priority)}"; /// public override string ToString() => Stringize(); } @@ -127,7 +138,14 @@ public override string Stringize() partial record Providedf { /// - public override string Stringize() => $@"{Expression.Stringize(Expression.Priority < Priority.Provided)} provided {Predicate.Stringize(Predicate.Priority < Priority.Provided)}"; + // `provided` is the one infix operator the grammar folds to the *right*, so this is + // the mirror of the `implies` rule: the flat form is read as + // `a provided (b provided c)`, and it is the left operand that has to say when it is + // an attached condition of its own. `(x provided p) provided q` printed flat came + // back as `x provided (p provided q)` -- the same value, since both are `x` exactly + // when `p` and `q` hold, and a different expression, which is what the round trip is + // about. + public override string Stringize() => $@"{Expression.Stringize(Expression.Priority <= Priority.Provided)} provided {Predicate.Stringize(Predicate.Priority < Priority.Provided)}"; /// public override string ToString() => Stringize(); } diff --git a/Sources/Tests/UnitTests/Convenience/FromStringTest.cs b/Sources/Tests/UnitTests/Convenience/FromStringTest.cs index 08f80817f..ad8d5a331 100644 --- a/Sources/Tests/UnitTests/Convenience/FromStringTest.cs +++ b/Sources/Tests/UnitTests/Convenience/FromStringTest.cs @@ -216,7 +216,20 @@ public void TestFormula8() [Fact] public void TestProvided1() => Assert.Equal(MathS.Provided("a", "b"), FromString("a provided b")); [Fact] public void TestProvided2() => Assert.Equal(MathS.Provided("a", MathS.Provided("b", "c")), FromString("a provided b provided c")); [Fact] public void TestProvided3() => Assert.Equal(MathS.Provided(MathS.Provided("a", "b"), "c"), FromString("(a provided b) provided c")); - [Fact] public void TestProvided4() => Assert.Equal(FromString("a provided (b provided c)").Stringize(), FromString("(a provided b) provided c").Stringize()); + // These two are different expressions -- `provided` folds to the right, so only the first + // is what the flat spelling means -- and this used to assert that they print alike. That + // is the round trip failing, written down as an expectation: the printed form of the + // second came back as the first. They must print differently, and each must read back as + // itself. + [Fact] public void TestProvided4() + { + var rightNested = FromString("a provided (b provided c)"); + var leftNested = FromString("(a provided b) provided c"); + Assert.NotEqual(rightNested, leftNested); + Assert.NotEqual(rightNested.Stringize(), leftNested.Stringize()); + Assert.Equal(rightNested, FromString(rightNested.Stringize())); + Assert.Equal(leftNested, FromString(leftNested.Stringize())); + } [Theory] [InlineData("sh", "Sinh")] diff --git a/Sources/Tests/UnitTests/Convenience/LatexTest.cs b/Sources/Tests/UnitTests/Convenience/LatexTest.cs index 8179e86ad..fd9b43aed 100644 --- a/Sources/Tests/UnitTests/Convenience/LatexTest.cs +++ b/Sources/Tests/UnitTests/Convenience/LatexTest.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) 2019-2022 Angouri. // AngouriMath is licensed under MIT. // Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md. @@ -312,10 +312,13 @@ [Fact] public void Provided1() => Test(@"x \quad \text{for} \quad y", MathS.Provided("x", "y")); [Fact] public void Provided2() => Test(@"x+1 \quad \text{for} \quad y > 0", MathS.Provided("x + 1", "y > 0")); + // Both of these are left-nested, and `provided` folds to the right -- so the flat form + // these used to expect is read back as the *right*-nested tree, which is a different + // expression. The grouping is the assertion; the two used to pin its absence. [Fact] public void Provided3() - => Test(@"a \quad \text{for} \quad b \quad \text{for} \quad c", MathS.Provided(MathS.Provided("a", "b"), "c")); + => Test(@"\left(a \quad \text{for} \quad b\right) \quad \text{for} \quad c", MathS.Provided(MathS.Provided("a", "b"), "c")); [Fact] public void Provided4() - => Test(@"\left(a \quad \text{for} \quad b \quad \text{for} \quad c \quad \text{for} \quad d\right) \to \top ", MathS.Provided(MathS.Provided("a", "b"), MathS.Provided("c", "d")).Implies(Entity.Boolean.True)); + => Test(@"\left(\left(a \quad \text{for} \quad b\right) \quad \text{for} \quad c \quad \text{for} \quad d\right) \to \top ", MathS.Provided(MathS.Provided("a", "b"), MathS.Provided("c", "d")).Implies(Entity.Boolean.True)); // Juxtaposition tests [Fact] public void M1InTheMiddle() => Test(@"x \left(-1\right) \cdot x", (x * (-1)) * x); [Fact] public void MultiplyNumberWithPower() => Test(@"2 \cdot {3}^{4}", 2 * ((Entity)3).Pow(4)); @@ -532,6 +535,37 @@ [Fact] public void SetMinus() => Test(@"\left\{ 1, 2, 3 \right\} \setminus \left\{ 2 \right\}", MathS.Sets.Finite(1, 2, 3).SetSubtract(MathS.Sets.Finite(2))); [Fact] public void SetIn() => Test(@"x \in \left\{ 1, 2, 3 \right\}", x.In(MathS.Sets.Finite(1, 2, 3))); + + // Associativity, which LaTeX has to express too: CSharpMath.Evaluation reads the LaTeX + // back, and it folds \cup, \setminus and \in to the left at the precedences the grammar + // here uses, so a right operand at the same level must be bracketed or it comes back as + // the outer operator. \to is the exception -- CSharpMath folds it to the *right*, the + // usual convention for implication, so it is the assumption that gets the brackets. + private static readonly Entity A = MathS.Sets.Finite(1, 2), B = MathS.Sets.Finite(2, 3), C = MathS.Sets.Finite(3); + private static readonly string As = @"\left\{ 1, 2 \right\}", Bs = @"\left\{ 2, 3 \right\}", Cs = @"\left\{ 3 \right\}"; + + [Fact] public void SetMinusOfSetMinusOnTheRight() + => Test($@"{As} \setminus \left({Bs} \setminus {Cs}\right)", A.SetSubtract(B.SetSubtract(C))); + [Fact] public void SetMinusOfSetMinusOnTheLeft() + => Test($@"{As} \setminus {Bs} \setminus {Cs}", A.SetSubtract(B).SetSubtract(C)); + [Fact] public void SetMinusOfUnionOnTheRight() + => Test($@"{As} \setminus \left({Bs} \cup {Cs}\right)", A.SetSubtract(B.Unite(C))); + [Fact] public void UnionOfSetMinusOnTheRight() + => Test($@"{As} \cup \left({Bs} \setminus {Cs}\right)", A.Unite(B.SetSubtract(C))); + [Fact] public void UnionOfUnionOnTheRightStaysFlat() + => Test($@"{As} \cup {Bs} \cup {Cs}", A.Unite(B.Unite(C))); + [Fact] public void IntersectionOfIntersectionOnTheRightStaysFlat() + => Test($@"{As} \cap {Bs} \cap {Cs}", A.Intersect(B.Intersect(C))); + [Fact] public void InOfInOnTheRight() + => Test(@"x \in \left(y \in z\right)", x.In(MathS.Var("y").In(MathS.Var("z")))); + [Fact] public void ImpliesOfImpliesOnTheRightStaysFlat() + => Test(@"x \to y \to z", x.Implies(MathS.Var("y").Implies(MathS.Var("z")))); + [Fact] public void ImpliesOfImpliesOnTheLeft() + => Test(@"\left(x \to y\right) \to z", x.Implies(MathS.Var("y")).Implies(MathS.Var("z"))); + [Fact] public void ModAsANonLeadingFactor() + => Test(@"x \left(y \bmod z\right)", x * (MathS.Var("y") % MathS.Var("z"))); + [Fact] public void ModAsTheLeadingFactorStaysFlat() + => Test(@"x \bmod y \cdot z", (x % MathS.Var("y")) * MathS.Var("z")); // Special sets [Fact] public void SetBooleans() diff --git a/Sources/Tests/UnitTests/Convenience/PriorityTest.cs b/Sources/Tests/UnitTests/Convenience/PriorityTest.cs index eff56bb0b..c2797df6b 100644 --- a/Sources/Tests/UnitTests/Convenience/PriorityTest.cs +++ b/Sources/Tests/UnitTests/Convenience/PriorityTest.cs @@ -49,6 +49,20 @@ public class PriorityTest [InlineData("not a = b", "a <> b")] [InlineData("a = b and not b = c and c < d", "a = b <> c < d")] [InlineData("not a = b and not b = c", "a <> b <> c")] + // The right operand of a left-folded operator that is not associative has to keep its + // brackets, or the normalized form is a different expression. + [InlineData("a implies (b implies c)", "a implies (b implies c)")] + [InlineData("a implies b implies c", "(a implies b) implies c")] + [InlineData("a implies (b implies c) implies d", "(a implies (b implies c)) implies d")] + [InlineData(@"A \ (B \ C)", @"A \ (B \ C)")] + [InlineData(@"A \ B \ C", @"(A \ B) \ C")] + [InlineData(@"A \/ (B \ C)", @"A \/ (B \ C)")] + [InlineData(@"A \ (B \/ C)", @"A \ (B \/ C)")] + [InlineData(@"A \/ B \ C", @"(A \/ B) \ C")] + [InlineData("a in (b in c)", "a in (b in c)")] + [InlineData("a in b in c", "(a in b) in c")] + [InlineData("a * (b mod c)", "a * (b mod c)")] + [InlineData("a * b mod c", "(a * b) mod c")] public void Test(string normalizedForm, string alternateForm) { Assert.Equal(alternateForm.ToEntity(), normalizedForm.ToEntity()); diff --git a/Sources/Tests/UnitTests/Convenience/StringizeRoundTripTest.cs b/Sources/Tests/UnitTests/Convenience/StringizeRoundTripTest.cs index f2507b5ec..25de960a1 100644 --- a/Sources/Tests/UnitTests/Convenience/StringizeRoundTripTest.cs +++ b/Sources/Tests/UnitTests/Convenience/StringizeRoundTripTest.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) 2019-2022 Angouri. // AngouriMath is licensed under MIT. // Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md. @@ -136,6 +136,92 @@ public void TheNodesWithoutAnOperatorSpellingPrintAsTheirFunction(string source) [InlineData("x provided y")] public void BooleansRoundTrip(string source) => AssertRoundTrip(source); + /// + /// An operator that is not associative has to print the bracketing it has, because + /// the flat form is read the way the grammar folds -- to the left for everything but + /// ^ and provided. Printing a implies (b implies c) as + /// a implies b implies c did not merely look wrong: it comes back as + /// (a implies b) implies c, and the two do not agree + /// (false implies (true implies false) is true, the other is false). + /// + /// + /// \/ and \ share one precedence level and are folded by the same loop, so a + /// \ on the right of a \/ mis-associates too even though union on its own is + /// associative; and mod shares a level with * and /, so a mod on + /// the right of a * does the same. + /// + [Theory] + [InlineData("a implies (b implies c)")] + [InlineData("(a implies b) implies c")] + [InlineData("a implies b implies c")] + [InlineData("a implies (b implies (c implies d))")] + [InlineData("(a implies (b implies c)) implies d")] + [InlineData("a implies (b or c)")] + [InlineData(@"A \ (B \ C)")] + [InlineData(@"(A \ B) \ C")] + [InlineData(@"A \/ (B \ C)")] + [InlineData(@"A \ (B \/ C)")] + [InlineData(@"(A \/ B) \ C")] + [InlineData(@"A \ (B /\ C)")] + [InlineData(@"A /\ (B \ C)")] + [InlineData("a in (b in c)")] + [InlineData("(a in b) in c")] + [InlineData("x * (y mod z)")] + [InlineData("(x * y) mod z")] + [InlineData("x mod (y * z)")] + [InlineData("x / (y mod z)")] + [InlineData("x mod (y mod z)")] + // `provided` is the mirror case: it folds to the *right*, so it is the left operand that + // mis-associates. Its value survives either reading -- `(x provided p) provided q` and + // `x provided (p provided q)` are both `x` exactly when `p` and `q` hold -- which is why + // a check on the value alone passes it and a check on the expression does not. + [InlineData("(x provided p) provided q")] + [InlineData("x provided (p provided q)")] + [InlineData("x provided p provided q")] + [InlineData("((x provided p) provided q) provided r")] + public void ANonAssociativeOperatorKeepsItsGrouping(string source) => AssertRoundTrip(source); + + /// + /// The same defect stated in values rather than in trees: each of these is a case where + /// reading the printed form back the way the grammar folds gives a different answer. + /// + [Theory] + [InlineData("false implies (true implies false)")] + [InlineData(@"{ 1, 2, 3 } \ ({ 2, 3 } \ { 3 })")] + [InlineData(@"{ 1, 2 } \ ({ 2 } \/ { 3 })")] + [InlineData(@"{ 1, 2 } \/ ({ 3 } \ { 1, 2 })")] + [InlineData("2 * (3 mod 2)")] + [InlineData("(3 in { 3 }) in BB")] + public void AMisreadGroupingWouldChangeTheValue(string source) + { + var original = source.ToEntity(); + Assert.Equal(original.Evaled, original.Stringize().ToEntity().Evaled); + } + + /// + /// The other half of the rule, pinned so that it is a decision rather than an oversight: + /// an operator that is associative prints flat, because the bracketing then + /// carries no mathematics. x + (y + z) comes back as (x + y) + z, which is a + /// different and the same number -- and bracketing it instead would + /// turn every expanded polynomial into a right-nested pile of parentheses. + /// + [Theory] + [InlineData("1 + (2 + 3)", "1 + 2 + 3")] + [InlineData("1 + (2 - 3)", "1 + 2 - 3")] + [InlineData("2 * (3 * 4)", "2 * 3 * 4")] + [InlineData("2 * (6 / 3)", "2 * 6 / 3")] + [InlineData("true and (false and true)", "True and False and True")] + [InlineData("true or (false or true)", "True or False or True")] + [InlineData("true xor (false xor true)", "True xor False xor True")] + [InlineData(@"{ 1 } \/ ({ 2 } \/ { 3 })", @"{ 1 } \/ { 2 } \/ { 3 }")] + [InlineData(@"{ 1, 2 } /\ ({ 2, 3 } /\ { 2 })", @"{ 1, 2 } /\ { 2, 3 } /\ { 2 }")] + public void AnAssociativeOperatorPrintsFlatAndKeepsItsValue(string source, string expectedFlat) + { + var original = source.ToEntity(); + Assert.Equal(expectedFlat, original.Stringize()); + Assert.Equal(original.Evaled, original.Stringize().ToEntity().Evaled); + } + [Theory] [InlineData("{ 1, 2, 3 }")] [InlineData("[1; 2]")]