Skip to content

Commit d401eed

Browse files
Rafael-SOWNetdarkfaderclaude
authored
Print the brackets an operator that is not associative has (#1009)
* Print the brackets an operator that is not associative has Stringize is the library's own input format, so parsing what it prints has to give back the expression it printed. Five operators broke that in one way: the printer left the *right* operand unbracketed at its own precedence level, while the grammar folds that level to the left, so the text came back re-associated. Four of the five are not associative, so the answer moved and not merely the shape. Measured on a build of 2.3.0 and a build of this branch: false implies (true implies false) printed False implies True implies False read back as (False implies True) implies False: True -> False { 1, 2, 3 } \ ({ 2, 3 } \ { 3 }) printed without its brackets: { 1, 3 } -> { 1 } { 1, 2 } \/ ({ 3 } \ { 1, 2 }) \/ and \ share one precedence level: { 1, 2, 3 } -> { 3 } 2 * (3 mod 2) mod shares a level with * and /: 2 -> 0 implies had the rule the wrong way round rather than missing: it bracketed the assumption, which a left fold never needs, and not the conclusion. So (a implies b) implies c loses its redundant brackets here, which is what made the genuinely ambiguous case look no different from it. \/ and * are bracketed only against the operator that makes them ambiguous, since both are associative themselves and share a level with one that is not: A \/ (B \/ C) still prints flat, A \/ (B \ C) does not; x * (y * z) and x * (y / z) still print flat, x * (y mod z) does not. What does not change, deliberately: an operator that *is* associative still prints flat. x + (y + z) prints x + y + z and comes back as (x + y) + z -- a differently shaped tree and the same number, because the bracketing carries no mathematics. Bracketing those would print every expanded polynomial as a right-nested pile of parentheses. StringizeRoundTripTest pins both halves so the flat half stays a decision rather than an oversight. Latexize moves for the set and mod cases and not for implies. CSharpMath.Evaluation, which reads our LaTeX back, folds \cup, \setminus, \in, \cdot and \bmod to the left at the same precedences this grammar uses, and folds \to to the right -- the usual convention for implication, and what Latexize was already bracketing for. The change only adds \left( \right) groups, which CSharpMath already parses, so nothing is owed downstream (#822). Syntax.md said everything but ^ groups to the left; provided groups to the right, and has since it was written. Suite 7591 passed, 1 failed, 14 skipped. The failure is OneSidedLimitTest.ADifferenceOfReciprocalLogarithms, a Task.Wait timeout guard that a different limit test tripped on the previous run and that passes when run alone -- contention, not this change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011emtnRT6EWTrxXNtqDVK3e * Bracket the operand `provided` mis-associates, which is the other one `provided` is the one infix operator the grammar folds to the right, so it is the *left* operand that has to say when it is an attached condition of its own. `(x provided p) provided q` printed as `x provided p provided q` and read back as `x provided (p provided q)`. It was classified as safe here on the strength of a value test, and the value is genuinely safe: both readings are `x` exactly when `p` and `q` hold. The contract is about the expression, not the number, and asking the expression is what catches it -- so the case moves from the flat-and-keeps-its-value theory to the keeps-its-grouping one. LatexTests.Provided3 and Provided4 asserted the flat form. They were pinning the ambiguity rather than a behaviour: both build a left-nested Providedf and expect it to print without the grouping that distinguishes it. Updated, with the reason beside them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011emtnRT6EWTrxXNtqDVK3e * Assert that two different provided expressions print differently FromStringTest.TestProvided4 asserted that `a provided (b provided c)` and `(a provided b) provided c` produce the same string. They are different expressions -- the parse is right-associative, which the two tests above it pin -- so that assertion was the round trip failing, written down as an expectation. It now asserts what the contract asks: the two print differently, and each reads back as itself. Found by CI rather than locally, because the run before this one was filtered to the printer tests and this one lives with the parser. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011emtnRT6EWTrxXNtqDVK3e --------- Co-authored-by: Rafael <darkfader@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 0fa4af8 commit d401eed

11 files changed

Lines changed: 295 additions & 23 deletions

File tree

‎BREAKING-CHANGES.md‎

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,87 @@ read first.
1515

1616
---
1717

18+
## Unreleased — since 2.3.0
19+
20+
### At a glance
21+
22+
| Silent? | What | Was | Is |
23+
|---|---|---|---|
24+
| **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)` |
25+
| | `"(a implies b) implies c".ToEntity().Stringize()` | `(a implies b) implies c` | `a implies b implies c` |
26+
| **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)` |
27+
| **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)` |
28+
| **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)` |
29+
| **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` |
30+
| **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)` |
31+
| **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)` |
32+
| **Silent** | `"-1 * (y mod z)".ToEntity().Stringize()` | `-y mod z` | `-(y mod z)` |
33+
34+
### A printed operator that is not associative keeps its brackets
35+
36+
`Stringize` is the library's own input format: parsing what it prints has to give back the
37+
expression it printed, and that is what [`StringizeRoundTripTest`](Sources/Tests/UnitTests/Convenience/StringizeRoundTripTest.cs)
38+
enforces. Six operators broke it, five of them in the same way — the printer left the **right**
39+
operand unbracketed at its own precedence level, while the grammar folds that level to the left, so
40+
the printed text came back re-associated. The sixth, `provided`, is the mirror: it is the one infix
41+
operator the grammar folds to the **right**, so there it is the *left* operand that mis-associates.
42+
43+
Four of the six are not associative, so the re-association changed the answer and not merely
44+
the shape. Measured on a build of 2.3.0 and a build of this branch:
45+
46+
| input | 2.3.0 printed | 2.3.0 read that back as | value moved |
47+
|---|---|---|---|
48+
| `false implies (true implies false)` | `False implies True implies False` | `(False implies True) implies False` | `True` → `False` |
49+
| `{ 1, 2, 3 } \ ({ 2, 3 } \ { 3 })` | `{ 1, 2, 3 } \ { 2, 3 } \ { 3 }` | `({ 1, 2, 3 } \ { 2, 3 }) \ { 3 }` | `{ 1, 3 }` → `{ 1 }` |
50+
| `{ 1, 2 } \/ ({ 3 } \ { 1, 2 })` | `{ 1, 2 } \/ { 3 } \ { 1, 2 }` | `({ 1, 2 } \/ { 3 }) \ { 1, 2 }` | `{ 1, 2, 3 }` → `{ 3 }` |
51+
| `{ 1, 2 } \ ({ 2 } \/ { 3 })` | `{ 1, 2 } \ { 2 } \/ { 3 }` | `({ 1, 2 } \ { 2 }) \/ { 3 }` | `{ 1 }` → `{ 1, 3 }` |
52+
| `2 * (3 mod 2)` | `2 * 3 mod 2` | `(2 * 3) mod 2` | `2` → `0` |
53+
54+
On this branch each of those prints its brackets and reads back as itself, so the value column
55+
no longer moves.
56+
57+
`implies` also changes in the other direction, because its printer had the rule the wrong way
58+
round rather than missing: it bracketed the **assumption** — the operand that never needs it under
59+
a left fold — and not the conclusion. So `(a implies b) implies c`, which used to print as
60+
`(a implies b) implies c`, now prints flat as `a implies b implies c`. Both read back as the same
61+
expression; the brackets were redundant, and printing them is what made the genuinely ambiguous
62+
case look no different.
63+
64+
`\/` and `mod` are bracketed only against the operator that makes them ambiguous, because both
65+
share a precedence level with an operator that is not associative while being associative
66+
themselves:
67+
68+
- `A \/ (B \/ C)` still prints `A \/ B \/ C`; `A \/ (B \ C)` now prints `A \/ (B \ C)`.
69+
- `x * (y * z)` still prints `x * y * z` and `x * (y / z)` still prints `x * y / z`;
70+
`x * (y mod z)` now prints `x * (y mod z)`.
71+
- `-1 * (y mod z)` printed `-y mod z` and now prints `-(y mod z)`.
72+
73+
`provided` is bracketed on the other side, and it is the case where the value is *not* the test.
74+
`(x provided p) provided q` and `x provided (p provided q)` are both `x` exactly when `p` and `q`
75+
hold, so no answer moves — and they are different expressions, which is what the round trip is
76+
about. It printed flat and read back as the right-nested one.
77+
78+
**What has not changed, deliberately.** An operator that *is* associative still prints flat, so
79+
`x + (y + z)` still prints `x + y + z`, and likewise for `*`, `and`, `or`, `xor`, `unite` and
80+
`intersect`. Those come back as a different `Entity` — a left-nested tree instead of
81+
a right-nested one — and as the same number, because the bracketing carries no mathematics. The
82+
alternative would print every expanded polynomial as a right-nested pile of parentheses:
83+
`"(a + b + c + d) ^ 2".ToEntity().Expand()` prints, on both versions,
84+
`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`,
85+
and its sum is right-nested throughout. `StringizeRoundTripTest` pins that decision so that it
86+
stays one.
87+
88+
**LaTeX.** `Latexize` moves for the four set and `mod` cases above and **not** for `implies`.
89+
[CSharpMath.Evaluation](https://github.com/verybadcat/CSharpMath/blob/master/CSharpMath.Evaluation/Evaluation.cs),
90+
which reads our LaTeX back, folds `\cup`, `\setminus`, `\in`, `\cdot` and `\bmod` to the left at
91+
the same precedences this grammar uses — so those needed the same brackets — but folds `\to` to the
92+
**right**, which is the usual convention for implication and is what `Latexize` was already
93+
bracketing for. The change only ever adds `\left(`/`\right)` groups, which CSharpMath already
94+
parses, so nothing downstream needs a matching change
95+
([#822](https://github.com/asc-community/AngouriMath/issues/822)).
96+
97+
---
98+
1899
## 2.3.0 — since 2.2.0
19100

20101
### At a glance

‎Sources/AngouriMath/Docs/Usage/Syntax.md‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Expression syntax
1+
# Expression syntax
22

33
What `MathS.FromString` accepts, and what `Entity.Stringize` produces. The authority is
44
[`Core/Antlr/AngouriMath.g`](../../Core/Antlr/AngouriMath.g); this page is a reading of it. To
@@ -22,23 +22,28 @@ will say so ([#822](https://github.com/asc-community/AngouriMath/issues/822)).
2222

2323
## Operators, loosest first
2424

25-
Everything on one line has the same precedence and groups to the left, except `^`, which groups
26-
to the right.
25+
Everything on one line has the same precedence and groups to the left, except `^` and `provided`,
26+
which group to the right.
27+
28+
Grouping is not decoration: an operator that is not associative prints the brackets it has, so
29+
`a implies (b implies c)` prints with them and `(a implies b) implies c` prints without. Where the
30+
operator *is* associative the brackets carry no mathematics and are not printed, so `x + (y + z)`
31+
prints as `x + y + z` and reads back as `(x + y) + z` — the same number, a differently shaped tree.
2732

2833
| | operators | notes |
2934
|---|---|---|
30-
| 1 | `provided` | |
35+
| 1 | `provided` | **groups to the right**: `a provided b provided c` is `a provided (b provided c)` |
3136
| 2 | `implies` | |
3237
| 3 | `or` `\|` | |
3338
| 4 | `xor` | |
3439
| 5 | `and` `&` | |
3540
| 6 | `not` | prefix |
3641
| 7 | `=` `<>` `>` `>=` `<` `<=` | chained: `a < b < c` means `a < b and b < c` |
3742
| 8 | `in` | |
38-
| 9 | `unite` `\/`, `setsubtract` `\` | |
43+
| 9 | `unite` `\/`, `setsubtract` `\` | one level, so `A \/ B \ C` is `(A \/ B) \ C` |
3944
| 10 | `intersect` `/\` | |
4045
| 11 | `+` `-` | |
41-
| 12 | `*` `/` `mod` | |
46+
| 12 | `*` `/` `mod` | one level, so `x * y mod z` is `(x * y) mod z` |
4247
| 13 | `+` `-` | prefix |
4348
| 14 | `^` | **groups to the right**: `a ^ b ^ c` is `a ^ (b ^ c)` |
4449
| 15 | `!` | postfix factorial |

‎Sources/AngouriMath/Functions/Output/Latex/Latex.Arithmetics.Classes.cs‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,13 @@ public override string Latexize()
4949
};
5050
case 1:
5151
if (longArray[index - 1] is Integer(-1))
52-
return $"-{currIn.Latexize(currIn.LatexPriority < LatexPriority)}"; // display "-1 * x * y" as "-x \cdot y", only for the first -1
52+
return $"-{currIn.Latexize(currIn.LatexPriority < LatexPriority || currIn is Modf)}"; // display "-1 * x * y" as "-x \cdot y", only for the first -1
5353
break;
5454
}
55-
var currOut = currIn.Latexize(currIn.LatexPriority < LatexPriority);
55+
// A factor other than the first that is a `mod` needs brackets: \bmod sits
56+
// at this precedence level and is not associative, so "x \cdot y \bmod z"
57+
// reads as "(x y) mod z". 2 * (3 mod 2) is 2 and (2 * 3) mod 2 is 0.
58+
var currOut = currIn.Latexize(currIn.LatexPriority < LatexPriority || currIn is Modf);
5659

5760
return (longArray[index - 1], currIn) switch // whether we use juxtaposition and omit \cdot
5861
{

‎Sources/AngouriMath/Functions/Output/Latex/Latex.Omni.Classes.cs‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,12 @@ public override string Latexize()
5050
partial record Unionf
5151
{
5252
/// <inheritdoc/>
53+
// CSharpMath.Evaluation reads \cup and \setminus at one precedence level and folds
54+
// them to the left, exactly as the grammar here does, so a \setminus on the right
55+
// needs bracketing or it comes back as the outer operator. Union with union is
56+
// associative and stays flat.
5357
public override string Latexize()
54-
=> $@"{Left.Latexize(Left.LatexPriority < LatexPriority)} \cup {Right.Latexize(Right.LatexPriority < LatexPriority)}";
58+
=> $@"{Left.Latexize(Left.LatexPriority < LatexPriority)} \cup {Right.Latexize(Right.LatexPriority < LatexPriority || Right is SetMinusf)}";
5559
}
5660

5761
partial record Intersectionf
@@ -64,22 +68,27 @@ public override string Latexize()
6468
partial record SetMinusf
6569
{
6670
/// <inheritdoc/>
71+
// Not associative, and sharing a precedence level with union: anything at that
72+
// level on the right is bracketed, the same rule \frac's text form follows.
6773
public override string Latexize()
68-
=> $@"{Left.Latexize(Left.LatexPriority < LatexPriority)} \setminus {Right.Latexize(Right.LatexPriority < LatexPriority)}";
74+
=> $@"{Left.Latexize(Left.LatexPriority < LatexPriority)} \setminus {Right.Latexize(Right.LatexPriority <= LatexPriority)}";
6975
}
7076

7177
partial record Inf
7278
{
7379
/// <inheritdoc/>
80+
// \in is left-folded by CSharpMath too, and membership is not associative.
7481
public override string Latexize()
75-
=> $@"{Element.Latexize(Element.LatexPriority < LatexPriority)} \in {SupSet.Latexize(SupSet.LatexPriority < LatexPriority)}";
82+
=> $@"{Element.Latexize(Element.LatexPriority < LatexPriority)} \in {SupSet.Latexize(SupSet.LatexPriority <= LatexPriority)}";
7683
}
7784
}
7885

7986
partial record Providedf
8087
{
8188
/// <inheritdoc/>
82-
public override string Latexize() => $@"{Expression.Latexize(Expression.LatexPriority < LatexPriority)} \quad \text{{for}} \quad {Predicate.Latexize(Predicate.LatexPriority < LatexPriority)}";
89+
// Right-folding, so the left operand is the one that needs bracketing -- see the
90+
// remark on Stringize.
91+
public override string Latexize() => $@"{Expression.Latexize(Expression.LatexPriority <= LatexPriority)} \quad \text{{for}} \quad {Predicate.Latexize(Predicate.LatexPriority < LatexPriority)}";
8392
}
8493

8594
partial record Piecewise

‎Sources/AngouriMath/Functions/Output/ToString/ToString.Arithmetics.Classes.cs‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,14 @@ public override string Stringize() =>
3030
public partial record Mulf
3131
{
3232
/// <inheritdoc/>
33+
// Multiplication is associative, so a product or a quotient on the right may stay
34+
// unbracketed -- but `mod` shares this precedence level and is not associative, so a
35+
// `mod` on the right must be bracketed or it is re-read as the outer operator:
36+
// `2 * (3 mod 2)` is 2 and `(2 * 3) mod 2` is 0.
3337
public override string Stringize() =>
3438
(Multiplier is Integer(-1) && !MathS.Diagnostic.OutputExplicit ? "-"
3539
: Multiplier.Stringize(Multiplier.Priority < Priority) + " * ")
36-
+ Multiplicand.Stringize(Multiplicand.Priority < Priority);
40+
+ Multiplicand.Stringize(Multiplicand.Priority < Priority || Multiplicand is Modf);
3741
/// <inheritdoc/>
3842
public override string ToString() => Stringize();
3943
}

‎Sources/AngouriMath/Functions/Output/ToString/ToString.Discrete.Classes.cs‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,13 @@ public override string Stringize()
5555
partial record Impliesf
5656
{
5757
/// <inheritdoc/>
58+
// The grammar folds `implies` to the left, like everything but `^` and `provided`, so
59+
// it is the *conclusion* that needs bracketing when it is an implication of its own.
60+
// The two are not interchangeable: `false implies (true implies false)` is true, and
61+
// `(false implies true) implies false` is false, so printing the first without its
62+
// brackets said the opposite of what it meant.
5863
public override string Stringize()
59-
=> $"{Assumption.Stringize(Assumption.Priority <= Priority)} implies {Conclusion.Stringize(Conclusion.Priority < Priority)}";
64+
=> $"{Assumption.Stringize(Assumption.Priority < Priority)} implies {Conclusion.Stringize(Conclusion.Priority <= Priority)}";
6065
/// <inheritdoc/>
6166
public override string ToString() => Stringize();
6267
}

‎Sources/AngouriMath/Functions/Output/ToString/ToString.Omni.Classes.cs‎

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,13 @@ partial record Complexes
8888
partial record Unionf
8989
{
9090
/// <inheritdoc/>
91+
// Union is associative, so a union on the right may stay unbracketed -- but `\/`
92+
// and `\` share one precedence level and are folded by one loop in the grammar, so
93+
// a *set difference* on the right must be bracketed or it is re-read as the outer
94+
// operator: `{ 1, 2 } \/ ({ 3 } \ { 1, 2 })` is { 1, 2, 3 } and
95+
// `({ 1, 2 } \/ { 3 }) \ { 1, 2 }` is { 3 }.
9196
public override string Stringize()
92-
=> $@"{Left.Stringize(Left.Priority < Priority)} \/ {Right.Stringize(Right.Priority < Priority)}";
97+
=> $@"{Left.Stringize(Left.Priority < Priority)} \/ {Right.Stringize(Right.Priority < Priority || Right is SetMinusf)}";
9398
/// <inheritdoc/>
9499
public override string ToString() => Stringize();
95100
}
@@ -106,17 +111,23 @@ public override string Stringize()
106111
partial record SetMinusf
107112
{
108113
/// <inheritdoc/>
114+
// Set difference is not associative, and it shares its precedence level with
115+
// union, so anything at that level on the right needs bracketing -- the same rule
116+
// `-` and `/` follow. `{1,2,3} \ ({2,3} \ {3})` is { 1, 3 } where
117+
// `({1,2,3} \ {2,3}) \ {3}` is { 1 }.
109118
public override string Stringize()
110-
=> $@"{Left.Stringize(Left.Priority < Priority)} \ {Right.Stringize(Right.Priority < Priority)}";
119+
=> $@"{Left.Stringize(Left.Priority < Priority)} \ {Right.Stringize(Right.Priority <= Priority)}";
111120
/// <inheritdoc/>
112121
public override string ToString() => Stringize();
113122
}
114123

115124
partial record Inf
116125
{
117126
/// <inheritdoc/>
127+
// `in` is folded to the left and is not associative: `(a in b) in c` asks whether
128+
// a truth value is an element of c, `a in (b in c)` whether a is an element of one.
118129
public override string Stringize()
119-
=> $@"{Element.Stringize(Element.Priority < Priority)} in {SupSet.Stringize(SupSet.Priority < Priority)}";
130+
=> $@"{Element.Stringize(Element.Priority < Priority)} in {SupSet.Stringize(SupSet.Priority <= Priority)}";
120131
/// <inheritdoc/>
121132
public override string ToString() => Stringize();
122133
}
@@ -127,7 +138,14 @@ public override string Stringize()
127138
partial record Providedf
128139
{
129140
/// <inheritdoc/>
130-
public override string Stringize() => $@"{Expression.Stringize(Expression.Priority < Priority.Provided)} provided {Predicate.Stringize(Predicate.Priority < Priority.Provided)}";
141+
// `provided` is the one infix operator the grammar folds to the *right*, so this is
142+
// the mirror of the `implies` rule: the flat form is read as
143+
// `a provided (b provided c)`, and it is the left operand that has to say when it is
144+
// an attached condition of its own. `(x provided p) provided q` printed flat came
145+
// back as `x provided (p provided q)` -- the same value, since both are `x` exactly
146+
// when `p` and `q` hold, and a different expression, which is what the round trip is
147+
// about.
148+
public override string Stringize() => $@"{Expression.Stringize(Expression.Priority <= Priority.Provided)} provided {Predicate.Stringize(Predicate.Priority < Priority.Provided)}";
131149
/// <inheritdoc/>
132150
public override string ToString() => Stringize();
133151
}

‎Sources/Tests/UnitTests/Convenience/FromStringTest.cs‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,20 @@ public void TestFormula8()
216216
[Fact] public void TestProvided1() => Assert.Equal(MathS.Provided("a", "b"), FromString("a provided b"));
217217
[Fact] public void TestProvided2() => Assert.Equal(MathS.Provided("a", MathS.Provided("b", "c")), FromString("a provided b provided c"));
218218
[Fact] public void TestProvided3() => Assert.Equal(MathS.Provided(MathS.Provided("a", "b"), "c"), FromString("(a provided b) provided c"));
219-
[Fact] public void TestProvided4() => Assert.Equal(FromString("a provided (b provided c)").Stringize(), FromString("(a provided b) provided c").Stringize());
219+
// These two are different expressions -- `provided` folds to the right, so only the first
220+
// is what the flat spelling means -- and this used to assert that they print alike. That
221+
// is the round trip failing, written down as an expectation: the printed form of the
222+
// second came back as the first. They must print differently, and each must read back as
223+
// itself.
224+
[Fact] public void TestProvided4()
225+
{
226+
var rightNested = FromString("a provided (b provided c)");
227+
var leftNested = FromString("(a provided b) provided c");
228+
Assert.NotEqual(rightNested, leftNested);
229+
Assert.NotEqual(rightNested.Stringize(), leftNested.Stringize());
230+
Assert.Equal(rightNested, FromString(rightNested.Stringize()));
231+
Assert.Equal(leftNested, FromString(leftNested.Stringize()));
232+
}
220233

221234
[Theory]
222235
[InlineData("sh", "Sinh")]

0 commit comments

Comments
 (0)