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
108 changes: 108 additions & 0 deletions Sources/AngouriMath/Docs/Usage/Syntax.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
/// <inheritdoc/>
public override string ToString() => Stringize();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,15 @@ partial record Providedf
partial record Piecewise
{
/// <inheritdoc/>
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))) + ")";
/// <inheritdoc/>
public override string ToString() => Stringize();
}
Expand All @@ -159,9 +167,11 @@ public override string Stringize()
partial record Application
{
/// <inheritdoc/>
// 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())) + ")";

/// <inheritdoc/>
public override string ToString() => Stringize();
Expand All @@ -170,8 +180,11 @@ public override string Stringize()
partial record Lambda
{
/// <inheritdoc/>
// 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() + ")";

/// <inheritdoc/>
public override string ToString() => Stringize();
Expand Down
165 changes: 165 additions & 0 deletions Sources/Tests/UnitTests/Convenience/StringizeRoundTripTest.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// What <see cref="Entity.Stringize()"/> 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.
/// </summary>
public sealed class StringizeRoundTripTest
{
private static void AssertRoundTrip(string source)
{
var original = source.ToEntity();
var printed = original.Stringize();
Assert.Equal(original, printed.ToEntity());
}

/// <summary>
/// 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.
/// </summary>
[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);
}

/// <summary>
/// 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.
/// </summary>
[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);
}
}
Loading