Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 11 additions & 6 deletions Sources/AngouriMath/Docs/Usage/Syntax.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -22,23 +22,28 @@ 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` | |
| 5 | `and` `&` | |
| 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 |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
17 changes: 13 additions & 4 deletions Sources/AngouriMath/Functions/Output/Latex/Latex.Omni.Classes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,12 @@ public override string Latexize()
partial record Unionf
{
/// <inheritdoc/>
// 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
Expand All @@ -64,22 +68,27 @@ public override string Latexize()
partial record SetMinusf
{
/// <inheritdoc/>
// 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
{
/// <inheritdoc/>
// \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
{
/// <inheritdoc/>
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,14 @@ public override string Stringize() =>
public partial record Mulf
{
/// <inheritdoc/>
// 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);
/// <inheritdoc/>
public override string ToString() => Stringize();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,13 @@ public override string Stringize()
partial record Impliesf
{
/// <inheritdoc/>
// 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)}";
/// <inheritdoc/>
public override string ToString() => Stringize();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,13 @@ partial record Complexes
partial record Unionf
{
/// <inheritdoc/>
// 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)}";
/// <inheritdoc/>
public override string ToString() => Stringize();
}
Expand All @@ -106,17 +111,23 @@ public override string Stringize()
partial record SetMinusf
{
/// <inheritdoc/>
// 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)}";
/// <inheritdoc/>
public override string ToString() => Stringize();
}

partial record Inf
{
/// <inheritdoc/>
// `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)}";
/// <inheritdoc/>
public override string ToString() => Stringize();
}
Expand All @@ -127,7 +138,14 @@ public override string Stringize()
partial record Providedf
{
/// <inheritdoc/>
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)}";
/// <inheritdoc/>
public override string ToString() => Stringize();
}
Expand Down
15 changes: 14 additions & 1 deletion Sources/Tests/UnitTests/Convenience/FromStringTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down
Loading
Loading