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); + } +}