diff --git a/Sources/AngouriMath/Docs/Usage/Syntax.md b/Sources/AngouriMath/Docs/Usage/Syntax.md index dfbbadbcd..6a501166b 100644 --- a/Sources/AngouriMath/Docs/Usage/Syntax.md +++ b/Sources/AngouriMath/Docs/Usage/Syntax.md @@ -13,6 +13,9 @@ notation is not in the grammar prints as its function call instead: a lambda pri `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. +One thing the printed form does not carry: a **codomain**. `domain(x, ZZ)` prints as `x`, and the +narrowing is lost on the way back ([#1022](https://github.com/asc-community/AngouriMath/issues/1022)). + **LaTeX output has a round trip too, and it is checked in someone else's repository.** [CSharpMath.Evaluation](https://github.com/verybadcat/CSharpMath/blob/master/CSharpMath.Evaluation/Evaluation.cs) reads LaTeX back into an `Entity` and says so in its own source — *"CSharpMath must handle all @@ -33,7 +36,7 @@ prints as `x + y + z` and reads back as `(x + y) + z` — the same number, a dif | | operators | notes | |---|---|---| | 1 | `provided` | **groups to the right**: `a provided b provided c` is `a provided (b provided c)` | -| 2 | `implies` | | +| 2 | `implies` `->` | | | 3 | `or` `\|` | | | 4 | `xor` | | | 5 | `and` `&` | | @@ -55,18 +58,61 @@ prints as `x + y + z` and reads back as `(x + y) + z` — the same number, a dif 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. +## Whitespace and comments + +Newlines are skipped, so an expression may be written over several lines. `//` runs to the end of +the line and `/* … */` spans lines; both are dropped before parsing. A `//` comment needs a newline +after it: it is the newline that ends the comment, so `x + 1 // done` is a parse error where +`x + 1 /* done */` is `x + 1` ([#1039](https://github.com/asc-community/AngouriMath/issues/1039)). + ## Numbers and constants -`1`, `1.5`, `1/2`, `1e-9`, `i`, `e`, `pi`, `+oo`, `-oo`, `NaN`, `true`, `false`. +`1`, `1.5`, `.5`, `1.`, `1/2`, `1e-9`, `i`, `e`, `pi`, `+oo`, `-oo`, `NaN`, `true`, `false`. + +A trailing `i` makes the literal imaginary, exponent and all: `3i`, `1.5e3i`. `i` on its own is the +imaginary unit, and it is a **number** rather than a name — which is why `x i` is `x ^ i` and not a +product (below), why `ix` is an ordinary variable called `ix`, and why an operator that declares +`i` as an index has to say so. + +Booleans are written `true` or `false`; `True` and `False` are read as well, because that is what +`Stringize` prints. `NaN` is the value an undefined computation gives — `0/0` and `1/0` both reach it — and it is spelled exactly that way, since only the exact word is reserved. A longer name containing it, such as `NaNx` or `NaN_1`, is still a variable, because the lexer takes the longest match. -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. +## Names + +A name is **one or more letters**, optionally followed by `_` and one or more letters or digits: +`x`, `xy`, `x_1`, `x_a`, `α`, `ω_1`, `Θ_1`, `альфа`, `x_ω`. Letters are `a`–`z`, `A`–`Z`, Greek +(U+0370–U+03FF and U+1F00–U+1FFF) and Cyrillic (U+0400–U+04FF); no other script is one, so `fi` is +a lexer error. + +Four things that look like names and are not: + +| written | what it is | +|---|---| +| `_x`, `x_` | a lexer error. `_` may neither begin a name nor end one | +| `x_1_2` | a lexer error. At most one `_`, so subscripts do not nest ([#524](https://github.com/asc-community/AngouriMath/issues/524)) | +| `x1` | `x ^ 1`. A digit is not a letter, and a name beside a number is a power — see below | +| `sinx` | one name, spelled `sinx`. Letters written together are a single name, never a product of one-letter ones | + +## Juxtaposition + +Two things written next to each other have an operator inserted between them, and **which +operator depends on what comes second**: + +| | inserted | | +|---|---|---| +| number, name or `)`, then a **name**, a function or `(` | `*` | `2x` is `2 * x`, `a(b + c)` is `a * (b + c)`, `x sin(x)` is `x * sin(x)`, `x(2)` is `x * 2` | +| number, name or `)`, then a **number** | `^` | `x2` is `x ^ 2`, `(x + 1)2` is `(x + 1) ^ 2`, `3 2` is `3 ^ 2` | + +So `x2` is a square and `x(2)` is a product, and since `i` is a number, `x i` is `x ^ i` while +`2i x` is `2i * x`. + +`MathS.Settings.ExplicitParsingOnly` turns the insertion off. Under it every row above is a +`MissingOperatorParseException` naming the two tokens it will not join, and only what is written +with an operator parses. ## Sets @@ -74,13 +120,17 @@ of the names below. |---|---| | finite | `{ 1, 2, 3 }`, `{}` | | interval | `[a; b]` closed, `(a; b)` open, `[a; b)` and `(a; b]` half-open | -| conditional | `{ x : x > 0 }` | +| conditional | `{ x : x > 0 }` — the name before the `:` is declared, as under `sum` below | | 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. +`[1, 2, 3]` is a column vector (3 by 1), `[[1, 2], [3, 4]]` a matrix, `[1, 2, 3]T` a transpose +(1 by 3). There is no empty vector: `[]` is not a value this library has +([#1028](https://github.com/asc-community/AngouriMath/issues/1028)), while the empty set `{}` is. + +`(|x|)` is the absolute value of `x`, and prints back as `abs(x)`. ## Functions @@ -94,17 +144,31 @@ Everything below is written `name(argument, ...)`. `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. +`ar-` and not `arc-`. Each has a long spelling, an `a-` spelling and at least one short one: + +| | | | +|---|---|---| +| sine | `arsinh` `asinh` `arsh` | `arcsinh` is **refused** | +| cosine | `arcosh` `acosh` `arch` | `arccosh` is **refused** | +| tangent | `artanh` `atanh` `arth` | `arctanh` is **refused** | +| cotangent | `arcotanh` `acotanh` `arcoth` `acoth` `arcth` | `arccotanh` is **refused** | +| secant | `arsech` `asech` `arsch` | `arcsech` is **refused** | +| cosecant | `arcosech` `acosech` `arcsch` `acsch` | `arccosech` is **refused** | + +A refused spelling raises a parse error that names the accepted ones. It is refused rather than +ignored because an unknown name followed by a bracket is a product (above), so `arcsinh(x)` left +alone would quietly be a variable times `x`. 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`. +The same goes for `cbrt(x)`, which is `x ^ (1/3)`, `sqr(x)`, which is `x ^ 2`, and `exp(x)`, +which is `e ^ x`. + +**Powers and logarithms** — `sqrt` `cbrt` `sqr` `pow(a, b)` `exp` `ln` `log(base, x)`. `log` with +one argument is base 10, so `log(100)` is 2; `log10` and `log2` say it in the name. -**Other** — `sqrt` `cbrt` `sqr` `pow(a, b)` `ln` `log(base, x)` `abs` `signum` `sgn` `sign` -`phi` `gamma` `factorial` (or postfix `!`). +**Other** — `abs` `signum` `sgn` `sign` `phi` `gamma` `factorial` (or postfix `!`). **Rounding** — `floor` `ceil` (`ceiling` is accepted on the way in and prints as `ceil`) `round`. All three round a complex argument componentwise. `floor` and `ceil` go toward the infinities @@ -131,16 +195,45 @@ range in [#657](https://github.com/asc-community/AngouriMath/pull/657). **Refused by name** — `trunc` `lcm` `erf` `conjugate`. AngouriMath has none of these, and each is what some other CAS calls a function, so a caller reaches for it. Left alone they would be read as -products under the rule below and answer silently and wrongly; they raise a parse error naming the +products under the rule above and answer silently and wrongly; they raise a parse error naming the function instead. `re` and `im` are the same case and are *not* refused, being short enough to be somebody's variable. +## `sum` and `product`, which declare a name + +`sum(body, name, from, to)` and `product(body, name, from, to)`. The **second** argument is the +name being declared and the first is the body it runs over, so `sum(i, i, 1, 10)` is +1 + 2 + … + 10 = 55, and `sum(k, k, 1, 10)` is the same number written over a different name. + +Both bounds are inclusive and the step is one. An empty range is the operator's identity — +`sum(k, k, 5, 1)` is `0` and `product(k, k, 5, 1)` is `1` — and the range is written out only when +both bounds are integers, so `sum(k, k, 1, n)` and `sum(k, k, 1, 5/2)` stay as they are written. + +**The declared name means the name, and only inside the operator that declares it.** Two names +show this, because both mean something else in this language: + +- `i` is the imaginary unit, so `sum(i, i, 1, 10)` is `55` and not a sum of imaginary units. + Outside, it is the unit again: `sum(i, i, 1, 3) + i` is `6 + i`, and `sum(sqrt(-1) * i, i, 1, 10)` + is `55i` — the factor is the unit, the index is the name. `2i` is a single number token, so + `sum(2i, i, 1, 3)` is `12`: two times the index, three times over. +- `pi` and `e` are constants, so `product(pi, pi, 1, 4)` is `24` and `sum(e, e, 1, 3)` is `6`. + +A declared name that outlives the operator declaring it is renamed, since `i` and `pi` are not +names the parser can produce: `derivative(pi ^ 2, pi)` is `2 * pi_1`. + +Every binder in the language reads its name this way — `integral`, `derivative`, `limit`, `lambda` +and the set builder `{ x : … }` alike. `lambda` is the one that will not take a number: its +parameter is typed as a name, so `lambda(2, x)` is a parse error where `sum(k, 2, 1, 3)` is not. + ## 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`, and `f(x)` for an - unknown `f` is `f * x`. That is what makes `a(b + c)` work, and it is why the names above are - refused individually rather than by a general rule. +- **A name beside a number is a power.** `x2` is `x ^ 2`; `x(2)` is `x * 2`. +- **Letters run together are one name.** `sinx` is a variable called `sinx`, not `sin(x)` and not + a product. Write the bracket. +- **An unknown name with a bracket is a product**, not an error: `f(x)` for an unknown `f` is + `f * x`. That is what makes `a(b + c)` work, and it is why the names above are refused + individually rather than by a general rule. - **`%` 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/Tests/UnitTests/Convenience/SyntaxDocumentedTest.cs b/Sources/Tests/UnitTests/Convenience/SyntaxDocumentedTest.cs new file mode 100644 index 000000000..70a50921d --- /dev/null +++ b/Sources/Tests/UnitTests/Convenience/SyntaxDocumentedTest.cs @@ -0,0 +1,346 @@ +// +// 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.Core.Exceptions; +using AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.Convenience +{ + /// + /// Every example printed in Docs/Usage/Syntax.md, run. That page is the only statement + /// of the language other than the grammar itself, and a rule nothing executes is a rule + /// nothing keeps true. + /// + /// + /// Only what that page states and nothing else pins. The function synonyms are + /// SynonymFunctionTest, the refused arc- spellings ArcHyperbolicRefusedTest, + /// sum and product in general SummationProductTest, and the codomain the + /// printed form drops EntitySerializationTest. + /// + [Trait("Area", "Convenience")] + public sealed class SyntaxDocumentedTest + { + // ------------------------------------------------------------ operators, loosest first + + /// + /// The precedence table, one row per line: what is written without brackets against the + /// same thing written with them. + /// + [Theory] + [InlineData("a provided b provided c", "a provided (b provided c)")] + [InlineData("a implies b implies c", "(a implies b) implies c")] + [InlineData("a or b xor c", "a or (b xor c)")] + [InlineData("a xor b and c", "a xor (b and c)")] + [InlineData("not a and b", "(not a) and b")] + [InlineData("a and b implies c", "(a and b) implies c")] + [InlineData("not x = y", "not (x = y)")] + [InlineData("x + y in RR", "(x + y) in RR")] + [InlineData("x in RR and y in ZZ", "(x in RR) and (y in ZZ)")] + [InlineData("A \\/ B \\ C", "(A \\/ B) \\ C")] + [InlineData("A /\\ B \\/ C", "(A /\\ B) \\/ C")] + [InlineData("a - b - c", "(a - b) - c")] + [InlineData("x * y mod z", "(x * y) mod z")] + [InlineData("a / b / c", "(a / b) / c")] + [InlineData("-x ^ 2", "-(x ^ 2)")] + [InlineData("2 ^ 3 ^ 2", "2 ^ (3 ^ 2)")] + [InlineData("x! + 1", "(x!) + 1")] + [InlineData("2 ^ x!", "2 ^ (x!)")] + public void AnOperatorGroupsWhereTheTableSaysItDoes(string written, string bracketed) => + Assert.Equal(bracketed.ToEntity(), written.ToEntity()); + + /// a < b < c is a conjunction and not a comparison of a boolean. + [Theory] + [InlineData("2 <= x < 5", "2 <= x and x < 5")] + [InlineData("a = b = c", "a = b and b = c")] + public void AComparisonChains(string written, string meaning) => + Assert.Equal(meaning.ToEntity(), written.ToEntity()); + + /// % is left free to mean percent, so it is not a token at all. + [Fact] + public void PercentIsNotAnOperator() => + Assert.Throws(() => "5 % 2".ToEntity()); + + // ------------------------------------------------------------ whitespace and comments + + [Theory] + [InlineData("x + /* dropped */ y")] + [InlineData("x + // dropped\n y")] + [InlineData("x +\n y")] + public void ACommentOrANewlineIsDropped(string written) => + Assert.Equal("x + y".ToEntity(), written.ToEntity()); + + /// + /// It is the newline that ends a // comment, so one at the end of the input never + /// matches and its / reaches the parser + /// (#1039). The + /// block form has no such condition. + /// + [Fact] + public void ALineCommentNeedsItsNewline() + { + Assert.Throws(() => "x + 1 // done".ToEntity()); + Assert.Equal("x + 1".ToEntity(), "x + 1 /* done */".ToEntity()); + } + + // ------------------------------------------------------------ numbers and names + + [Theory] + [InlineData(".5", "1/2")] + [InlineData("1.", "1")] + [InlineData("1.5e3", "1500")] + [InlineData("1e-9", "0.000000001")] + [InlineData("true", "True")] + [InlineData("false", "False")] + [InlineData("True", "true")] + [InlineData("False", "false")] + public void ANumberOrABooleanIsWrittenTheseWays(string written, string same) => + Assert.Equal(same.ToEntity(), written.ToEntity()); + + /// A trailing i makes the literal imaginary, exponent and all. + [Theory] + [InlineData("3i", 3)] + [InlineData("1.5e3i", 1500)] + public void ATrailingIMakesTheLiteralImaginary(string written, int magnitude) => + Assert.Equal((magnitude * MathS.i).Evaled, written.ToEntity()); + + /// The lexer takes the longest match, so only the exact word is the value. + [Theory] + [InlineData("NaNx")] + [InlineData("NaN_1")] + public void ALongerNameContainingNaNIsAVariable(string written) => + Assert.IsType(written.ToEntity()); + + [Theory] + [InlineData("x")] + [InlineData("xy")] + [InlineData("x_1")] + [InlineData("x_a")] + [InlineData("α")] + [InlineData("ω_1")] + [InlineData("Θ_1")] + [InlineData("альфа")] + [InlineData("x_ω")] + [InlineData("sinx")] + public void AName(string written) + { + var variable = Assert.IsType(written.ToEntity()); + Assert.Equal(written, variable.Name); + } + + /// + /// The four shapes the page lists as looking like names and not being them. The first two + /// are lexer errors; the last two are expressions, which is what makes them worth writing + /// down — nothing tells the caller they did not get the name they typed. + /// + [Theory] + [InlineData("_x")] + [InlineData("x_")] + [InlineData("x_1_2")] + [InlineData("fi")] + public void WhatIsNotAName(string written) => + Assert.Throws(() => written.ToEntity()); + + [Fact] + public void ADigitEndsAName() => Assert.Equal("x ^ 1".ToEntity(), "x1".ToEntity()); + + // ------------------------------------------------------------ juxtaposition + + /// Second is a name, a function or a bracket: the inserted operator is a product. + [Theory] + [InlineData("2x", "2 * x")] + [InlineData("x y", "x * y")] + [InlineData("a(b + c)", "a * (b + c)")] + [InlineData("x sin(x)", "x * sin(x)")] + [InlineData("x(2)", "x * 2")] + [InlineData("(x + 1)(x + 2)", "(x + 1) * (x + 2)")] + [InlineData("2i x", "2i * x")] + public void JuxtapositionBeforeANameIsAProduct(string written, string meaning) => + Assert.Equal(meaning.ToEntity(), written.ToEntity()); + + /// Second is a number: the inserted operator is a power, which is the surprise. + [Theory] + [InlineData("x2", "x ^ 2")] + [InlineData("3 2", "3 ^ 2")] + [InlineData("(x + 1)2", "(x + 1) ^ 2")] + [InlineData("x i", "x ^ i")] + public void JuxtapositionBeforeANumberIsAPower(string written, string meaning) => + Assert.Equal(meaning.ToEntity(), written.ToEntity()); + + /// + /// refuses to insert either one, and says + /// which two tokens it will not join. + /// + [Theory] + [InlineData("2x")] + [InlineData("x2")] + [InlineData("a(b + c)")] + public void ExplicitParsingOnlyInsertsNothing(string written) => + MathS.Settings.ExplicitParsingOnly.As(true, () => + Assert.Throws(() => written.ToEntity())); + + [Fact] + public void ExplicitParsingOnlyStillTakesWhatIsWrittenOut() => + MathS.Settings.ExplicitParsingOnly.As(true, () => + Assert.Equal(2 * MathS.Var("x"), "2 * x".ToEntity())); + + // ------------------------------------------------------------ sets, vectors, abs + + /// + /// A column vector and its transpose, with the shapes the page gives. The empty set is a + /// value and the empty vector is not + /// (#1028); + /// asserted as "does not answer" rather than by exception type, since that issue is about + /// which exception it ought to be. + /// + [Fact] + public void AVectorAndItsTranspose() + { + var column = Assert.IsType("[1, 2, 3]".ToEntity()); + Assert.Equal((3, 1), (column.RowCount, column.ColumnCount)); + var row = Assert.IsType("[1, 2, 3]T".ToEntity()); + Assert.Equal((1, 3), (row.RowCount, row.ColumnCount)); + + Assert.Empty(Assert.IsType("{}".ToEntity())); + Assert.ThrowsAny(() => "[]".ToEntity()); + } + + [Fact] + public void AbsoluteValueBrackets() + { + Assert.Equal(MathS.Abs("x - 5".ToEntity()), "(|x - 5|)".ToEntity()); + Assert.Equal("abs(x)", "(|x|)".ToEntity().Stringize()); + } + + // ------------------------------------------------------------ functions + + /// One argument to log is base 10, and two more names say a base outright. + [Theory] + [InlineData("log(100)", "2")] + [InlineData("log10(100)", "2")] + [InlineData("log2(1024)", "10")] + [InlineData("exp(2)", "e ^ 2")] + public void ALogarithmOrAnExponential(string written, string expected) => + Assert.Equal(expected.ToEntity(), written.ToEntity().Simplify()); + + /// + /// Each row of the inverse hyperbolic table: every accepted spelling of one function is + /// the same expression. They are rewritten as they are parsed, so this compares what each + /// one became and not a node named after it. + /// + [Theory] + [InlineData("arsinh(x)", "asinh(x)", "arsh(x)")] + [InlineData("arcosh(x)", "acosh(x)", "arch(x)")] + [InlineData("artanh(x)", "atanh(x)", "arth(x)")] + [InlineData("arcotanh(x)", "acotanh(x)", "arcoth(x)", "acoth(x)", "arcth(x)")] + [InlineData("arsech(x)", "asech(x)", "arsch(x)")] + [InlineData("arcosech(x)", "acosech(x)", "arcsch(x)", "acsch(x)")] + public void OneInverseHyperbolicFunctionUnderEverySpelling(params string[] spellings) + { + var first = spellings[0].ToEntity(); + foreach (var spelling in spellings) + Assert.Equal(first, spelling.ToEntity()); + } + + /// Rewritten on the way in, so none of these is a node of its own. + [Theory] + [InlineData("sinh(x)", "(e ^ x - e ^ (-x)) / 2")] + [InlineData("arsinh(x)", "ln(x + sqrt(x ^ 2 + 1))")] + [InlineData("cbrt(x)", "x ^ (1/3)")] + [InlineData("sqr(x)", "x ^ 2")] + [InlineData("exp(x)", "e ^ x")] + public void AFunctionThatIsRewrittenAsItIsParsed(string written, string what) => + Assert.Equal(what.ToEntity(), written.ToEntity()); + + /// + /// Three arguments name neither form of integral, where they are an order for + /// derivative. + /// + [Fact] + public void IntegralTakesNoOrder() + { + Assert.Throws(() => "integral(f, x, 2)".ToEntity()); + Assert.IsType("derivative(f, x, 2)".ToEntity()); + } + + /// Short enough to be somebody's variable, so read as a product and not refused. + [Theory] + [InlineData("re(x)")] + [InlineData("im(x)")] + public void ANameTheLibraryDoesNotRefuseIsAProduct(string written) => + Assert.IsType(written.ToEntity()); + + // ------------------------------------------------------------ sum and product + + /// + /// The second argument is the name and the first is the body, which is the whole + /// difference between 55 and something else. + /// + [Theory] + [InlineData("sum(i, i, 1, 10)", "55")] + [InlineData("sum(k, k, 1, 10)", "55")] + [InlineData("product(k, k, 1, 5)", "120")] + public void TheSecondArgumentIsTheDeclaredName(string written, string expected) => + Assert.Equal(expected.ToEntity(), written.ToEntity().Simplify()); + + /// Inclusive bounds, step one, and the identity of the operator on an empty range. + [Theory] + [InlineData("sum(k, k, 5, 1)", "0")] + [InlineData("product(k, k, 5, 1)", "1")] + [InlineData("sum(k, k, 1, 1)", "1")] + [InlineData("sum(k, k, -2, 2)", "0")] + public void AnInclusiveRangeAndItsIdentity(string written, string expected) => + Assert.Equal(expected.ToEntity(), written.ToEntity().Simplify()); + + /// Written out only when both bounds are integers. + [Theory] + [InlineData("sum(k, k, 1, n)")] + [InlineData("sum(k, k, 1, 5/2)")] + [InlineData("sum(k, k, 1, +oo)")] + public void ARangeThatIsNotAnIntegerOneStaysAsWritten(string written) => + Assert.Equal(written.ToEntity(), written.ToEntity().Simplify()); + + /// + /// The declared name means the name inside the operator, and what it usually means + /// outside it. Both halves matter: the first alone would be a name that leaks. + /// + [Theory] + [InlineData("sum(2i, i, 1, 3)", "12")] + [InlineData("product(pi, pi, 1, 4)", "24")] + [InlineData("sum(e, e, 1, 3)", "6")] + [InlineData("sum(i, i, 1, 3) + i", "6 + i")] + [InlineData("sum(sqrt(-1) * i, i, 1, 10)", "55i")] + public void ADeclaredNameShadowsWhatItWouldOtherwiseMean(string written, string expected) => + Assert.Equal(expected.ToEntity().Evaled, written.ToEntity().Simplify().Evaled); + + /// + /// A name that outlives the operator that declared it is renamed, since the parser cannot + /// produce a variable called pi. + /// + [Fact] + public void ANameThatOutlivesItsOperatorIsRenamed() => + Assert.Equal("2 * pi_1".ToEntity(), "derivative(pi ^ 2, pi)".ToEntity().Simplify()); + + /// lambda is the one binder whose parameter has to be a name. + [Fact] + public void OnlyALambdaInsistsOnAName() + { + Assert.IsType("sum(k, 2, 1, 3)".ToEntity()); + Assert.Throws(() => "lambda(2, x)".ToEntity()); + } + + /// A set builder declares the name before its colon, the same way. + [Fact] + public void ASetBuilderDeclaresItsNameToo() + { + var set = Assert.IsType("{ i : i > 0 }".ToEntity()); + Assert.True(set.Contains(5)); + Assert.False(set.Contains(-5)); + } + } +}