diff --git a/Sources/AngouriMath/Docs/Usage/Syntax.md b/Sources/AngouriMath/Docs/Usage/Syntax.md
new file mode 100644
index 000000000..df228dae3
--- /dev/null
+++ b/Sources/AngouriMath/Docs/Usage/Syntax.md
@@ -0,0 +1,108 @@
+# 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
+change the language, change the grammar and regenerate — see
+[`ImproveParser.md`](../Contributing/ImproveParser.md).
+
+## The contract
+
+**Parsing what `Stringize` prints gives back the expression it printed.** That is the whole
+point of the printed form, and it is enforced by `StringizeRoundTripTest`. A node whose usual
+notation is not in the grammar prints as its function call instead: a lambda prints as
+`lambda(x, x + 1)` and not `x -> x + 1`, because `->` is the implication operator and the arrow
+form would silently come back as something else.
+
+LaTeX output is under no such obligation — nothing parses LaTeX — so `Latexise` is free to use
+`\frac`, `\bmod` and the rest.
+
+## Operators, loosest first
+
+Everything on one line has the same precedence and groups to the left, except `^`, which groups
+to the right.
+
+| | operators | notes |
+|---|---|---|
+| 1 | `provided` | |
+| 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` `\` | |
+| 10 | `intersect` `/\` | |
+| 11 | `+` `-` | |
+| 12 | `*` `/` `mod` | |
+| 13 | `+` `-` | prefix |
+| 14 | `^` | **groups to the right**: `a ^ b ^ c` is `a ^ (b ^ c)` |
+| 15 | `!` | postfix factorial |
+
+`%` is **not** an operator. It is left free to mean percent; the remainder is written `mod`.
+
+`mod` is the floored remainder, `a - b * floor(a / b)`, which takes the sign of the **divisor**:
+`(-7) mod 3` is 2 and `7 mod (-3)` is -2. This is the convention SymPy, Mathematica and Maxima
+use, and the one under which the residues modulo n are the numbers from 0 to n - 1. C's `%`
+truncates instead; it is a different operation and the library does not inherit it.
+
+## Numbers and constants
+
+`1`, `1.5`, `1/2`, `1e-9`, `i`, `e`, `pi`, `+oo`, `-oo`, `true`, `false`.
+
+A variable is a letter or `_` followed by letters, digits or `_`, and Greek letters are letters.
+Juxtaposition is multiplication, so `2x` is `2 * x` — which is also why an unknown function name
+parses as a product rather than as an error: `foo(x)` is `foo * x`, silently, unless `foo` is one
+of the names below.
+
+## Sets
+
+| | |
+|---|---|
+| finite | `{ 1, 2, 3 }`, `{}` |
+| interval | `[a; b]` closed, `(a; b)` open, `[a; b)` and `(a; b]` half-open |
+| conditional | `{ x : x > 0 }` |
+| special | `RR` `CC` `ZZ` `QQ` `BB` |
+| operations | `unite` `/\` … see the table above |
+
+## Matrices and vectors
+
+`[1, 2, 3]` is a column vector, `[[1, 2], [3, 4]]` a matrix, `[1, 2, 3]T` a transpose.
+
+## Functions
+
+Everything below is written `name(argument, ...)`.
+
+**Trigonometric** — `sin` `cos` `tan` `cotan` `cot` `sec` `cosec` `csc`, and the inverses
+`arcsin` `arccos` `arctan` `arccotan` `arccot` `arcsec` `arccosec` `arccsc`, each also spelled
+`asin` `acos` `atan` `acotan` `acot` `asec` `acosec` `acsc`.
+
+**Hyperbolic** — `sinh` `sh` `cosh` `ch` `tanh` `th` `cotanh` `coth` `cth` `sech` `sch`
+`cosech` `csch`.
+
+**Inverse hyperbolic** — the inverse of a hyperbolic function is an *area*, not an arc, so it is
+`arsinh` (also `asinh`, `arsh`; `arcsinh` is **refused**), `arcosh`, `artanh`, `arcotanh`, and
+their short forms `arsh` `arch` `arth` `arcth` `arsch` `arcsch`. The `arc-` spellings raise a
+parse error rather than being silently read as a product.
+
+Both hyperbolic families are **rewritten as they are parsed** and are not nodes of their own:
+`sinh(x)` becomes `(e ^ x - e ^ (-x)) / 2` and `arsinh(x)` becomes `ln(x + sqrt(x ^ 2 + 1))`.
+So they do not print back as themselves — what round-trips is the expression, not the spelling.
+The same goes for `cbrt(x)`, which is `x ^ (1/3)`, and `sqr(x)`, which is `x ^ 2`.
+
+**Other** — `sqrt` `cbrt` `sqr` `pow(a, b)` `ln` `log(base, x)` `abs` `signum` `sgn` `sign`
+`phi` `gamma` `factorial` (or postfix `!`).
+
+**Calculus** — `derivative(expr, var, order)`, `integral(expr, var)`, `limit(expr, var, dest)`,
+`limitleft(...)`, `limitright(...)`.
+
+**Structural** — `piecewise(a provided p, b provided q)`, `lambda(param, body)`,
+`apply(f, arg, ...)`, `domain(expr, set)`.
+
+## Where it is easy to be caught out
+
+- **`^` groups to the right.** `2 ^ 2 ^ 3` is 256, not 64. Write `(2 ^ 2) ^ 3` for the other.
+- **An unknown name is a product**, not an error. `sinx` is `s * i * n * x`.
+- **`%` is not the remainder.** Write `mod`.
+- **Intervals use `;`**, not `,`: `[1; 2]`. `[1, 2]` is a vector.
+- **`->` is implication**, not a lambda arrow.
diff --git a/Sources/AngouriMath/Functions/Output/ToString/ToString.Arithmetics.Classes.cs b/Sources/AngouriMath/Functions/Output/ToString/ToString.Arithmetics.Classes.cs
index 1e472e229..52f84eaa6 100644
--- a/Sources/AngouriMath/Functions/Output/ToString/ToString.Arithmetics.Classes.cs
+++ b/Sources/AngouriMath/Functions/Output/ToString/ToString.Arithmetics.Classes.cs
@@ -66,7 +66,12 @@ public partial record Powf
public override string Stringize() =>
Exponent == 0.5m
? "sqrt(" + Base.Stringize() + ")"
- : Base.Stringize(Base.Priority < Priority) + " ^ " + Exponent.Stringize(Exponent.Priority < Priority);
+ // The base takes <=, the exponent takes <, which is the mirror of the rule the
+ // left-associative operators above use: ^ groups to the right, so it is the
+ // *left* operand that needs bracketing when it is a power of its own.
+ // (2 ^ 3) ^ 2 printed as 2 ^ 3 ^ 2 before, which is 512 where the expression
+ // printed is 64.
+ : Base.Stringize(Base.Priority <= Priority) + " ^ " + Exponent.Stringize(Exponent.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 cac355347..a69393263 100644
--- a/Sources/AngouriMath/Functions/Output/ToString/ToString.Omni.Classes.cs
+++ b/Sources/AngouriMath/Functions/Output/ToString/ToString.Omni.Classes.cs
@@ -135,7 +135,15 @@ partial record Providedf
partial record Piecewise
{
///
- public override string Stringize() => $"({string.Join(", ", Cases.Select(n => $"{n.Expression} if {n.Predicate}"))})";
+ // piecewise(a provided p, b provided q). The `if` spelling is not in the grammar,
+ // and neither is a bare comma-separated list, so a printed piecewise came back
+ // either as a product with `if` read as an undeclared variable, or as nothing at
+ // all once there was more than one case.
+ public override string Stringize()
+ => "piecewise(" + string.Join(", ",
+ Cases.Select(n => n.Expression.Stringize(n.Expression.Priority < Priority)
+ + " provided "
+ + n.Predicate.Stringize(n.Predicate.Priority < Priority))) + ")";
///
public override string ToString() => Stringize();
}
@@ -159,9 +167,11 @@ public override string Stringize()
partial record Application
{
///
+ // apply(f, a, b), for the same reason: juxtaposition is not application in the
+ // grammar, and `(x -> x + 1) 2` came back as a power.
public override string Stringize()
- => Expression.Stringize(Expression.Priority < Priority) + " " +
- " ".Join(Arguments.Select(arg => arg.Stringize(arg.Priority <= Priority)));
+ => "apply(" + Expression.Stringize() + ", " +
+ string.Join(", ", Arguments.Select(arg => arg.Stringize())) + ")";
///
public override string ToString() => Stringize();
@@ -170,8 +180,11 @@ public override string Stringize()
partial record Lambda
{
///
+ // lambda(p, body), because that is what the parser reads. The arrow spelling is
+ // not in the grammar at all, and `->` there is the implication operator, so a
+ // printed lambda used to come back as an implication.
public override string Stringize()
- => Parameter.Stringize() + " -> " + Body.Stringize(Body.Priority < Priority);
+ => "lambda(" + Parameter.Stringize() + ", " + Body.Stringize() + ")";
///
public override string ToString() => Stringize();
diff --git a/Sources/Tests/UnitTests/Convenience/StringizeRoundTripTest.cs b/Sources/Tests/UnitTests/Convenience/StringizeRoundTripTest.cs
new file mode 100644
index 000000000..01dc180a1
--- /dev/null
+++ b/Sources/Tests/UnitTests/Convenience/StringizeRoundTripTest.cs
@@ -0,0 +1,165 @@
+//
+// Copyright (c) 2019-2022 Angouri.
+// AngouriMath is licensed under MIT.
+// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.
+// Website: https://am.angouri.org.
+//
+
+using AngouriMath;
+using AngouriMath.Extensions;
+using Xunit;
+
+namespace AngouriMath.Tests.Common
+{
+ ///
+ /// What is for: parsing what it prints gives back the
+ /// expression it printed. Anything else makes the printed form a lie, and a silent one,
+ /// since a wrong reading is still a valid expression.
+ ///
+ public sealed class StringizeRoundTripTest
+ {
+ private static void AssertRoundTrip(string source)
+ {
+ var original = source.ToEntity();
+ var printed = original.Stringize();
+ Assert.Equal(original, printed.ToEntity());
+ }
+
+ ///
+ /// Powers group to the right, so it is the base that needs bracketing when it is a power
+ /// of its own -- the mirror of what the left-associative operators need. Printing
+ /// (2 ^ 3) ^ 2 as 2 ^ 3 ^ 2 did not merely look wrong: the first is 64 and the second is
+ /// 512, so the printed form was a different expression.
+ ///
+ [Theory]
+ [InlineData("(x ^ y) ^ z")]
+ [InlineData("((x ^ y) ^ z) ^ w")]
+ [InlineData("x ^ (y ^ z)")]
+ [InlineData("x ^ y ^ z")]
+ [InlineData("(2 ^ 2) ^ 3")]
+ [InlineData("2 ^ 2 ^ 3")]
+ [InlineData("(x + 1) ^ (y + 1)")]
+ [InlineData("(-x) ^ 2")]
+ [InlineData("2 ^ (-x)")]
+ public void PowersKeepTheirGrouping(string source) => AssertRoundTrip(source);
+
+ [Fact]
+ public void ANestedPowerKeepsItsValue()
+ {
+ var original = "(2 ^ 3) ^ 2".ToEntity();
+ Assert.Equal(original.Evaled, original.Stringize().ToEntity().Evaled);
+ }
+
+ ///
+ /// These three used to be printed in spellings the parser does not have. A lambda came
+ /// back as an implication, an application as a power, and a piecewise as a product with
+ /// "if" read as an undeclared variable -- or, with more than one case, as a parse error.
+ ///
+ [Theory]
+ [InlineData("lambda(x, x + 1)")]
+ [InlineData("lambda(x, lambda(y, x + y))")]
+ [InlineData("apply(lambda(x, x + 1), 2)")]
+ [InlineData("apply(lambda(x, lambda(y, x + y)), 1, 2)")]
+ [InlineData("piecewise(1 provided x > 0)")]
+ [InlineData("piecewise(1 provided x > 0, 2 provided x < 0)")]
+ [InlineData("piecewise(x + 1 provided x > 0, x - 1 provided x < 0)")]
+ public void TheNodesWithoutAnOperatorSpellingPrintAsTheirFunction(string source) =>
+ AssertRoundTrip(source);
+
+ [Theory]
+ [InlineData("x + y")]
+ [InlineData("x - y")]
+ [InlineData("x * y")]
+ [InlineData("x / y")]
+ [InlineData("-x")]
+ [InlineData("x!")]
+ [InlineData("(x + y) * z")]
+ [InlineData("x + y * z")]
+ [InlineData("(x - y) - z")]
+ [InlineData("x - (y - z)")]
+ [InlineData("x / y / z")]
+ [InlineData("x / (y / z)")]
+ [InlineData("-(x + y)")]
+ public void ArithmeticKeepsItsGrouping(string source) => AssertRoundTrip(source);
+
+ [Theory]
+ [InlineData("sin(x)")]
+ [InlineData("cos(x)")]
+ [InlineData("tan(x)")]
+ [InlineData("cotan(x)")]
+ [InlineData("sec(x)")]
+ [InlineData("cosec(x)")]
+ [InlineData("arcsin(x)")]
+ [InlineData("arccos(x)")]
+ [InlineData("arctan(x)")]
+ [InlineData("arccotan(x)")]
+ [InlineData("arcsec(x)")]
+ [InlineData("arccosec(x)")]
+ [InlineData("sinh(x)")]
+ [InlineData("cosh(x)")]
+ [InlineData("tanh(x)")]
+ [InlineData("cotanh(x)")]
+ [InlineData("sech(x)")]
+ [InlineData("cosech(x)")]
+ [InlineData("ln(x)")]
+ [InlineData("log(2, x)")]
+ [InlineData("sqrt(x)")]
+ [InlineData("abs(x)")]
+ [InlineData("signum(x)")]
+ [InlineData("phi(x)")]
+ [InlineData("gamma(x)")]
+ [InlineData("derivative(x ^ 2, x, 1)")]
+ [InlineData("limit(x, x, 0)")]
+ [InlineData("limitleft(x, x, 0)")]
+ [InlineData("limitright(x, x, 0)")]
+ public void FunctionsRoundTrip(string source) => AssertRoundTrip(source);
+
+ [Theory]
+ [InlineData("a and b")]
+ [InlineData("a or b")]
+ [InlineData("a xor b")]
+ [InlineData("not a")]
+ [InlineData("a implies b")]
+ [InlineData("a and b or c")]
+ [InlineData("a or b and c")]
+ [InlineData("not (a and b)")]
+ [InlineData("x > y")]
+ [InlineData("x < y")]
+ [InlineData("x >= y")]
+ [InlineData("x <= y")]
+ [InlineData("x = y")]
+ [InlineData("x <> y")]
+ [InlineData("x provided y")]
+ public void BooleansRoundTrip(string source) => AssertRoundTrip(source);
+
+ [Theory]
+ [InlineData("{ 1, 2, 3 }")]
+ [InlineData("[1; 2]")]
+ [InlineData("(1; 2)")]
+ [InlineData("[1; 2)")]
+ [InlineData("A unite B")]
+ [InlineData("A intersect B")]
+ [InlineData("A setsubtract B")]
+ [InlineData("x in A")]
+ [InlineData("RR")]
+ [InlineData("CC")]
+ [InlineData("ZZ")]
+ [InlineData("QQ")]
+ [InlineData("BB")]
+ public void SetsRoundTrip(string source) => AssertRoundTrip(source);
+
+ [Theory]
+ [InlineData("[[1, 2], [3, 4]]")]
+ [InlineData("[1, 2, 3]")]
+ [InlineData("1/2")]
+ [InlineData("-1/2")]
+ [InlineData("i")]
+ [InlineData("-i")]
+ [InlineData("2 + 3 * i")]
+ [InlineData("+oo")]
+ [InlineData("-oo")]
+ [InlineData("e")]
+ [InlineData("pi")]
+ public void MatricesAndNumbersRoundTrip(string source) => AssertRoundTrip(source);
+ }
+}