diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index a791c04b9..2e78fbd59 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -15,63 +15,28 @@ read first. --- -## 2.0.0 — since 1.4.0 +## 2.1.0 — since 2.0.0 + +Everything here landed **after** the 2.0.0 tag, so it is not in the package a reader of that +section already has. Almost all of it is a wrong answer becoming a right one, found by the +boundary and crash harnesses rather than reported; two answers are withdrawn deliberately and say +so, and one word — `NaN` — is reserved. + +Performance against 2.0.0 is measured and published as a pair in +[`Docs/WhatsNew/version_performance_control.md`](Sources/AngouriMath/Docs/WhatsNew/version_performance_control.md): +no regression, the largest real move being `SolveMedium` at +4.6%. ### At a glance | Silent? | What | Was | Is | |---|---|---|---| -| loud | `Minusf.Minuend` / `.Subtrahend` | named for the wrong operand | named for the right one | -| **silent** | `exp(x)`, `log10(x)`, `log2(x)` | products of undeclared variables | the functions | -| loud | `arcsinh(x)` and its five relatives | a product | `UnrecognizedFunctionParseException` | -| loud | `floor(x)` and ten other names the library lacks | a product, silently | `UnrecognizedFunctionParseException` | -| **silent** | `sqrt(x^2)`, `sqrt(-x)` and their kind | `x`, `i*sqrt(x)` — wrong for negative x | left as written | -| loud | `mod` as a variable name | a variable | a keyword, so a parse error | | loud | `NaN` as a variable name | a variable | a keyword, so the NaN value | | loud | `MathS.ToSympyCode` of any non-integer rational | `SyntaxError` — a parenthesis was never closed | code that runs | | **silent** | `MathS.ToSympyCode` of `1/2`, `2^(-1)` | ran, and gave the float `0.5` | `1/2`, exact | | loud | `MathS.ToSympyCode` of `NaN`, `+oo`, `-oo` | `NameError` — the name is never bound | `sympy.nan`, `sympy.oo`, `-sympy.oo` | | **silent** | `NaN` printed and read back | a variable of that name, which cancels and collects | the NaN value | -| **silent** | `Stringize` of powers, lambdas, applications, piecewises | did not parse back | parses back | -| **silent** | `Stringize` of a complex number with a fractional imaginary part | read back as its negation, or as a power | parses back | -| **silent** | numbers below `1e-16` | rounded to `0` | kept | -| **silent** | `Real` `%` | `-7 % 3` was `-1` | `2` | -| **silent** | `Simplify` of a cancelling quotient | the quotient | a `Providedf` | -| **silent** | radicals, everywhere | `sqrt(12)` | `2 * sqrt(3)` | -| **silent** | `Expand` | left like terms uncollected | collects them | -| **silent** | an identity equation | `{ }` or `{ 0 }` | all of `CC` | -| **silent** | a quadratic inequality | wrong for one sign of a symbolic coefficient | a case split on that sign | -| **silent** | numeric root sets | one root per starting point | one root per root | -| **silent** | many limits | `NaN`, or unevaluated | a value | -| **silent** | a vanishing sine behind a constant factor | `NaN` | a value — but `sin(x)*ln(x)*2` loses its `0` | -| **silent** | a limit assembling `oo^0` or `1^oo` | that form's value, `1` | the real answer, or unevaluated | -| **silent** | a factorial under a vanishing exponent | unevaluated | a value, by Stirling | -| **silent** | `ln(x!)` in a limit | unevaluated, and `ln(x!)/ln(x)` was `NaN` | a value | -| **silent** | a boolean expression | factored, not minimised | minimised where that is shorter | -| **silent** | some integrals | not antiderivatives | closed forms | -| **silent** | two integrals | answered correctly | unevaluated — a deliberate loss | -| **silent** | `k/(a x^2 + c)` and `k/sqrt(a x^2 + c)` for symbolic `a` | `NaN` | a piecewise on the sign of the discriminant | -| loud | `Compile` over a missing variable | `KeyNotFoundException` | `UncompilableNodeException` | | loud | `Compile` of `floor`, `ceil`, `round`, `phi`, `gamma`, `!` | `AngouriBugException`, asking to be reported | `UncompilableNodeException` | | loud | `Compile` of a boolean or lambda node as `double` | `InvalidOperationException` from Linq | `UncompilableNodeException` | -| loud | `Expand` of a quotient of factorials | `AngouriBugException` | the expanded polynomial | -| loud | parsing a `provided` in a parenthesised comma list | `NullReferenceException` | `UnhandledParseException` | -| loud | `floor(x)`, `ceil(x)`, `ceiling(x)` | `UnrecognizedFunctionParseException` | the functions | -| loud | `round(x)`, `min(a, b)`, `max(a, b)`, `gcd(a, b)` | `UnrecognizedFunctionParseException` | the functions | -| loud | 28 members deprecated since 1.x | obsolete but present | removed | -| loud | `Latexise`, `ILatexiseable`, `entity_latexise` | the British spelling | `Latexize`, `ILatexizeable`, `entity_latexize` | -| loud | `MathS.Quantum.Factorise` | one letter from the unrelated `Entity.Factorize` | `MathS.Quantum.TensorFactorize` | -| loud | `MathS.Quantum.IsNormalised` | the British spelling | `MathS.Quantum.IsNormalized` | -| loud | the target frameworks | `net7.0;netstandard2.0` | `netstandard2.0;net8.0;net10.0` | -| **silent** | `abs(x) = c` for a negative `c` | a set of non-solutions | the empty set | -| loud | implicit `List` to `Entity` | made a `FiniteSet`, and made three `params` overloads uncallable | removed | -| loud | implicit `Entity[]` to `Entity` | made a `FiniteSet`, discarding order and repeats | removed | -| loud | implicit `(Entity, Entity)` to `Entity` | made a **closed** interval, though `(a, b)` reads as the open one | removed | -| **silent** | a `MathS.Settings` scope across an `await`, or inside a task | lost, or somebody else's | follows the call | -| **silent** | a `RewriteRecording` across an `await`, or work started under it | lost, or somebody else's | follows the call | -| loud | a polynomial system with more equations than unknowns | `WrongNumberOfArgumentsException` | solved | -| loud | a known gap, e.g. a cubic inequality | `AngouriBugException`, asking to be reported | `NotSufficientlySupportedException` | -| **silent** | `arcsin(sin(x))` and three siblings | `x`, wrong wherever `x` leaves the principal interval | left as written unless `x` is a real in that interval | | **silent** | `abs(sgn(x))` and `sgn(abs(x))` | `1`, wrong at `x = 0` where both are `0` | left as written unless the argument's value can be read | | **silent** | `ln(e^x)`, `log(2, 2^x)`, `ln(x^2)` | `x`, `x`, `2 * ln(x)` — wrong off the real line | left as written unless the argument is decidable | | **silent** | two limits over `(x^2)^x` and `x^x` | answered correctly | unevaluated — a deliberate loss | @@ -86,180 +51,6 @@ read first. | **silent** | `log(1/2, 0)` and any base below 1 | `-oo` | `+oo` | | **silent** | `abs(-sqrt(6))`, `abs(-pi)`, `abs(1 - sqrt(2))` | left as written | `sqrt(6)`, `pi`, `sqrt(2) - 1` | ---- - -## Target frameworks - -`net7.0` is replaced by `net8.0` and `net10.0`. `netstandard2.0` is unchanged, so consumers on -.NET Framework and the older runtimes are unaffected. - -| | was | is | -|---|---|---| -| `TargetFrameworks` | `net7.0;netstandard2.0` | `netstandard2.0;net8.0;net10.0` | - -.NET 7 left support in May 2024, so the old list named one framework nobody should still be -targeting and no supported modern one. A consumer on net8.0 or net10.0 previously resolved the -`netstandard2.0` asset and silently lost the generic-math surface with it; they now get a real -target. - -**What breaks.** Anything that pins the framework of its reference: - -```xml - - TargetFramework=net7.0 - -``` - -fails with `NETSDK1005: Assets file ... doesn't have a target for 'net7.0'`. Change it to `net10.0` -or `net8.0`. This is not hypothetical -- every one of the nine measurement harnesses in this -workspace pinned `net7.0` and had to be repointed. - -A consumer that references the NuGet package rather than the project picks its asset by its own -framework and needs no change, unless it targets `net7.0` itself, in which case it now resolves -`netstandard2.0` and loses the generic-math types. - -**Generic math is unchanged in reach.** `Core/Entity/GenericMath` needs `INumber` and so is -still compiled only into the net7.0-and-later targets -- now written as a compatibility test -rather than a literal `!= 'net7.0'`, so adding a framework does not silently drop it. Verified on -the built assemblies: present in `net8.0` and `net10.0`, absent from `netstandard2.0`. - ---- - -## Types and members - -### The implicit conversions from a collection are removed - -`Entity` had two implicit conversions from a collection, both building a `FiniteSet`: - -```csharp -public static implicit operator Entity(Entity[] elements); // removed -public static implicit operator Entity(List elements); // removed -``` - -The second one made three public members impossible to call with a `List`: - -```csharp -var equations = new List { "x - 1", "y - 2" }; -new EquationSystem(equations); // error CS0121: the call is ambiguous -new FiniteSet(equations); // error CS0121 -MathS.Equations(equations); // error CS0121 -``` - -Each of those has both an `IEnumerable` overload and a `params Entity[]` overload. With -the conversion in place a `List` also converts to a single `Entity`, so the `params` -overload becomes applicable in its expanded form, neither candidate is better than the other, -and the call does not compile. The array, `IEnumerable`, and variadic forms were always -fine — only a concrete `List` broke, which is why it survived to a 2.0 preview. - -This was not three separate defects. One conversion made *every* `params Entity[]` overload in -the library uncallable with a list, including any added later. - -**This one moves between previews.** The conversion is in `2.0.0-preview.1` and -`2.0.0-preview.2`, both published, and is gone in `2.0.0`. It is the only entry here that -changes between two previews rather than between releases: a reader coming from 1.3.0 or -1.4.0 can ignore the distinction, one already on a preview cannot. - -**What breaks.** Assigning a list where an `Entity` is expected: - -```csharp -Entity set = new List { 1, 2, 3 }; // no longer compiles -``` - -Write one of these instead: - -```csharp -Entity set = new FiniteSet(new List { 1, 2, 3 }); -Entity set = new List { 1, 2, 3 }.ToSet(); -``` - -**The conversion from `Entity[]` is gone as well.** It was kept at first, on the narrow -ground that it never produced the ambiguity — an array binds to the `params` overload in its -normal form by an identity conversion, which wins outright. That was true and beside the -point. An array carries an order and can repeat an element; a set has neither, so the -conversion silently discarded part of what it was handed, and an implicit conversion that -loses information is the wrong shape regardless of which overloads it happens to break. -Set types are built explicitly nearly everywhere for this reason. - -```csharp -Entity set = new Entity[] { 1, 2, 3 }; // no longer compiles either -Entity set = new FiniteSet(1, 2, 3); // say it -Entity set = new Entity[] { 1, 2, 3 }.ToSet(); -``` - -Only one place in the whole repository relied on it, which was the test pinning it. - -**And the conversion from a pair, which had the same fault from the other side.** A -two-element tuple became an `Interval`, and since a tuple says nothing about whether its -endpoints are included, the conversion had to supply that: - -```csharp -Entity e = ((Entity)1, (Entity)5); -// was: [1; 5] — both endpoints included, and 1 is a member -``` - -Where the array conversion dropped information that was there, this one produced -information that was not — and chose the reading opposite to the notation, since `(1, 5)` -is the open interval in ordinary mathematical writing and `[1, 5]` the closed one. A caller -writing what looks like an open interval got a closed one, silently. - -```csharp -Entity e = MathS.Interval(1, 5); // closed, said out loud -Entity e = MathS.Interval(1, false, 5, false); // open -Entity e = new Interval(1, true, 5, true); -``` - -Nothing in the library, the tests, the F# wrapper or the utilities used it. - -The pairs elsewhere in the API are unaffected, because none of them has to guess: an -integration `Range`, the arguments of `Substitute`, the cases of `MathS.Piecewise` and -`ToProvided` are all ordered pairs whose two halves have distinct, stated roles. - -### An inverse trigonometric function no longer cancels its own function off the principal branch - -`arcsin` is a left inverse of `sin` only on `[-pi/2, pi/2]`, and the three siblings likewise only on -their own intervals. The rewrite was unconditional, so `Simplify` returned a value that is wrong at -every real point outside the interval: - -| | was | is | the value at that point | -|---|---|---|---| -| `arcsin(sin(x))` | `x` | left as written | `pi - x` for `x` in `[pi/2, 3pi/2]`, and so on | -| `arccos(cos(x))` | `x` | left as written | `2*pi - x` for `x` in `[pi, 2pi]` | -| `arctan(tan(x))` | `x` | left as written | `x - pi` for `x` in `(pi/2, 3pi/2)` | -| `arccotan(cotan(x))` | `x` | left as written | `x - pi` for `x` in `(pi, 2pi)` | - -Measured on a build of `35f4ae5a` before this change and after it. The four rules are older than -1.4.0 and untouched since, so 1.4.0 answers the same way, but the numbers below are from the two -2.0.0 builds rather than from a 1.4.0 one: - -``` -"arcsin(sin(x))".Simplify().Substitute("x", 3).EvalNumerical() - was 3 - is 0.14159... which is pi - 3, and is what arcsin(sin(3)) equals - -"arctan(tan(x))".Simplify().Substitute("x", 2).EvalNumerical() - was 2 - is -1.14159... = 2 - pi - -"arccos(cos(x))".Simplify().Substitute("x", 4).EvalNumerical() - was 4 - is 2.28318... = 2*pi - 4 -``` - -**Where the argument is a real number inside the interval, the cancellation still happens and stays -exact**: `arcsin(sin(1/2))` is `1/2`, not a decimal. Where it is symbolic, the expression is left as -written, which is what SymPy and Mathematica both answer. - -A condition was deliberately *not* attached. `arcsin(sin(x))` is defined for every real `x`, so -`x provided x >= -pi/2 and x <= pi/2` would say the expression is undefined outside the interval -when in fact it merely has another value — trading a wrong value for a wrong domain. - -**The other direction is unchanged**, and needs no assumption: `sin(arcsin(z))`, `cos(arccos(z))`, -`tan(arctan(z))` and `cotan(arccotan(z))` are all `z`, because they compose the *right* inverse. - -`CircleTest.Test8` asserted `arccotan(cotan(3x)) == 3x` and was pinning the wrong answer; it now -asserts that the expression is left alone. Issue -[#884](https://github.com/asc-community/AngouriMath/issues/884). - ### `Compile` fails with its own exception rather than someone else's Two families, both reported as `UncompilableNodeException` — the exception @@ -522,114 +313,434 @@ an exponent, a function's argument or a matrix. Written as a rule instead — a `(-1) * x`, so `(-1) * (a - b)` becomes `b - a`, which is five nodes for three — it runs wherever the shape occurs, since a rule walks the tree. -`Expand` of a matrix is the same story from the other end: it read its argument as a sum, and a matrix -is not one, so a matrix left through the "too complicated, return what came in" exit. It now expands -entry by entry. `Factorize` and `Differentiate` both already descended, being built out of rewrite -rules, so this was `Expand` being the odd one out rather than matrices being held back deliberately. +`Expand` of a matrix is the same story from the other end: it read its argument as a sum, and a matrix +is not one, so a matrix left through the "too complicated, return what came in" exit. It now expands +entry by entry. `Factorize` and `Differentiate` both already descended, being built out of rewrite +rules, so this was `Expand` being the odd one out rather than matrices being held back deliberately. + +**Where a caller meets it.** `EquationSystem.Solve` returns a `Matrix`, so a solved system's entries +were left in whatever form the solver built them in: + +``` +MathS.Equations("x2 + y", "y - x - 3").Solve("x", "y").Simplify() + +was: [[-(-(5 - sqrt(-11)) / 2 + 3), (5 - sqrt(-11)) / 2], [-(-(5 + sqrt(-11)) / 2 + 3), (5 + sqrt(-11)) / 2]] +is: [[-1/2 + -1/2 * sqrt(-11), 5/2 + -1/2 * sqrt(-11)], [1/2 * sqrt(-11) + -1/2, 1/2 * sqrt(-11) + 5/2]] +``` + +Issue [#882](https://github.com/asc-community/AngouriMath/issues/882), which +[#497](https://github.com/asc-community/AngouriMath/issues/497) names as the shape of defect to hunt: +the same input simplifying or not depending on its parent. + +### A conditional set's bound variable is renamed to a temporary + +`DirectChildren` of `{ x : P(x) }` renames the binder, so that the bound `x` is not read as an `x` +that may be free outside the set. The replacement name was four lowercase letters derived from the +predicate's hash code, and it was produced through `MathS.Var`, which **parses** — so a name that the +parser reads as something other than a variable did not come back as one. + +| | was | is | +|---|---|---| +| `new ConditionalSet("x", "x > 0").DirectChildren[0]` | `abcd > 0`, a different name in every process | `%1 > 0` | +| one predicate in `26^4` | `CannotParseInstanceException: Cannot parse an instance of Variable from `true`` | `%1 > 0` | + +Four lowercase letters can spell `true`. They can also spell a variable the predicate already uses, in +which case the rename captures it and the set means something else — silently, with no exception to +say so. The name now comes from `Variable.CreateTemp`, which reads the predicate's variables rather +than its hash and hands back `%1` upward, skipping what is taken: fresh by construction, and with no +reading as anything but a variable, since the parser does not accept `%`. + +**Printed output does not change.** `Stringize` reads the node's own fields, so `{ x : x > 0 }` still +prints with its own binder, and `Solve` answers containing a conditional set are unaffected. Nothing +could have depended on the old name either, since string hash codes are randomised per process and it +was therefore different on every run — which is also why this surfaced as a CI failure that would not +reproduce. + +`Variable.CreateRandom`, which had this one caller, is removed. It was `internal`. + +Issue [#891](https://github.com/asc-community/AngouriMath/issues/891). + +### `abs(sgn(x))` and `sgn(abs(x))` are not `1` at zero + +`|sgn(z)|` and `sgn(|z|)` are `1` for every `z` except `0`, where both are `0`, because `sgn(0)` is +`0`. Both rewrites answered `1` for any argument. + +| | was | is | +|---|---|---| +| `abs(sgn(x))`, `sgn(abs(x))` | `1` | left as written | +| the same at `x = 0` | `1` | `0` — which is the value | +| `abs(sgn(0))`, `sgn(abs(0))` | `0`, unchanged | | +| `abs(sgn(2))`, `sgn(abs(-3))` | `1`, unchanged | | +| `signum(abs(x/x))`, `abs(signum(x/x))` | `1 provided not x = 0` | left as written | + +The rules consulted the argument's `DomainCondition`, which answers a different question from the one +they needed: a bare `x` is defined everywhere and can *be* zero, while `x / x` is defined only away +from zero and is nonzero throughout. Reading the domain conflated the two. + +**A condition would not have been the fix**, for the same reason as in the two entries above: the +expression is defined at zero and equal to `0` there, so `1 provided not z = 0` would replace a wrong +value with a wrong domain. There is no closed form for "1 away from zero, 0 at it" other than the +expressions themselves, so the rules decide where the argument's value can be read and decline +otherwise. + +**The last row is a coverage loss, not a correction.** `1 provided not x = 0` was right for `x / x`. +Separating it from the `x` case needs "nonzero throughout its domain", and neither test available in +`InnerSimplify` can express it — `Evaled` leaves `x / x` as `x / x`, and `DomainCondition` is what +conflated them; `Simplify` would answer it and cannot be called from there (#403). + +Found by `boundcheck` after adding `x = 0` and `x = 1` to its sample points: 366 shapes had been +passing at 23 points chosen for branch cuts and principal intervals, none of which was the place +where a rule's own arithmetic degenerates. Issue +[#892](https://github.com/asc-community/AngouriMath/issues/892). + +### `abs` folds where the sign of its argument is known + +`|x|` is `x` for a non-negative real `x` and `-x` for a negative one. That is the definition of the +function rather than an identity with a side condition, and it was applied only when the argument +was a *number*. An argument whose value is a known real without its node being a number was left +alone, so a radical or a constant kept its `abs`: + +| | was | is | +|---|---|---| +| `abs(-sqrt(6))` | left as written | `sqrt(6)` | +| `abs(-pi)`, `abs(-e)` | left as written | `pi`, `e` | +| `abs(1 - sqrt(2))` | left as written | `sqrt(2) - 1` | +| `abs(-2)` | `2` | `2`, unchanged | +| `abs(sqrt(-4))` | `2` | `2`, unchanged — the magnitude of `2i` | +| `abs(-a)` for symbolic `a` | left as written | left as written | + +Where this shows up is in an answer built out of radicals. `(2x^2 - 3 > 0) and (x > 0)` solved to +`(abs(-sqrt(6)) / 2; +oo)` and now solves to `(sqrt(6) / 2; +oo)`; the endpoint was always the same +number, printed in a form that looked like unfinished work. The +[Solvers wiki page](https://github.com/asc-community/AngouriMath/wiki/Solvers) shows the old output +and wants updating with the release. + +**Nothing is assumed about a symbol**, and an argument off the real line is declined rather than +guessed at: `sqrt(-4)` evaluates to `2i`, whose absolute value is `2` — neither the argument nor its +negation, so a rule that read "negative, therefore negate" would be wrong there. The sign is read +off the value, and a value that is not a finite real does not answer the question. + +Issue [#881](https://github.com/asc-community/AngouriMath/issues/881). + +### `MathS.ToSympyCode` emits Python that runs + +Its documented purpose is code you can run in SymPy, and for two whole classes of expression it +emitted code that did not run at all. + +| expression | was | is | +|---|---|---| +| `1/2`, `1/3 + 1/6` | `sympy.Rational(1, 2` — `SyntaxError: '(' was never closed` | `sympy.Rational(1, 2)` | +| `0/0`, `1/0` | `NaN` — `NameError: name 'NaN' is not defined` | `sympy.nan` | +| `+oo`, `-oo` | `+oo`, `-oo` — `NameError` | `sympy.oo`, `-sympy.oo` | + +The first is a missing parenthesis, and it broke **every** expression carrying a non-integer rational, +which is most of what a computer algebra system hands back. The second is a value with no binding: the +generated preamble declares a `sympy.Symbol` for each free *variable*, and a `NaN` or an infinity is +neither a variable nor something SymPy names the same way this library does. + +Both were checked by running the emitted programs against SymPy 1.14 rather than by reading them, which +is also how it was established that they now come back **exact** — `1/2` arrives as SymPy's `Half` and +not as the float `0.5`. + +Nothing else about the exporter changes. `pi`, `e` and `i` were already emitted as `sympy.pi`, +`sympy.E` and `sympy.I`, and `sqrt` as `sympy.sqrt`. + +Two tests now hold the properties that failed, without needing an interpreter in the suite: the +parentheses balance, and every name in the emitted body is either declared in the preamble or reached +through `sympy.`. 17 of their 23 cases fail against the old exporter. + +Issue [#909](https://github.com/asc-community/AngouriMath/issues/909). + +### `MathS.ToSympyCode` keeps an exact value exact + +The generated program ran, and then quietly gave a different number. Python's `/`, and its `**` with a +negative exponent, are float operations on two integers: + +| expression | emitted | SymPy read it as | now emitted | and reads as | +|---|---|---|---|---| +| `1/2` | `1 / 2` | `0.500000000000000`, a `Float` | `sympy.Integer(1) / 2` | `1/2`, a `Rational` | +| `2^(-1)` | `2 ** (-1)` | `0.5` | `sympy.Integer(2) ** (-1)` | `1/2` | +| `2^(-3)` | `2 ** (-3)` | `0.125` | `sympy.Integer(2) ** (-3)` | `1/8` | +| `x + 1/2` | `x + 1 / 2` | `x + 0.5` | `x + sympy.Integer(1) / 2` | `x + 1/2` | + +Making one operand a SymPy integer hands the arithmetic to SymPy, which keeps it exact. **Only a pair of +integers is rewritten**, and the rest of the emitted code is unchanged: with a symbol anywhere in the +shape SymPy's own operators already take over (`x / 2`, `1 / x`, `x ** (-1)`), and `+`, `-`, `*` and a +non-negative `**` are exact on Python integers, whose precision is unbounded — `2 ** 70` was always +right. + +It bit only the **unsimplified** form, which is the one a caller writes: `"1/2".ToEntity()` is a `Divf` +of two integers, because a printed rational parses back as a division +([#873](https://github.com/asc-community/AngouriMath/issues/873)), while a simplified `1/2` is a +`Rational` node and already emitted `sympy.Rational(1, 2)`. + +Checked by running the emitted programs against SymPy 1.14, which is the only way this class of defect +shows itself — the code was always valid, so the earlier tests could not have caught it, and the two +added here assert the property instead: no two plain integer literals are combined with `/` or with a +negative `**`. + +Issue [#911](https://github.com/asc-community/AngouriMath/issues/911). + +### `NaN` is now a keyword, and the printed form of NaN reads back + +`Stringize` prints the NaN value as `NaN`, and the grammar had no such token, so reading it back gave a +**variable of that name**. A variable behaves like any symbol — it cancels, collects and compares — and +nothing on the page distinguished the two, since a variable named `NaN` also prints as `NaN`: + +| expression | was | is | +|---|---|---| +| `"NaN - NaN".Simplify()` | `0` | `NaN` | +| `"NaN / NaN".Simplify()` | `1 provided not NaN = 0` | `NaN` | +| `"NaN * 0".Evaled` | `0` | `NaN` | +| `"NaN * 2".Evaled`, `"NaN + NaN".Evaled` | left as written | `NaN` | +| `NaN` as a variable name | a variable | a parse of the NaN value | +| `NaNx`, `NaN_1`, `aNaN` | variables | variables, unchanged | + +This is the same trade `mod` took above: a word that was an identifier becomes a keyword, so an +expression using it as a variable name now means something else. It is the narrower half of the trade — +only the exact spelling is reserved, because the lexer takes the longest match, so any longer name that +merely contains it is still a variable. + +Only `NaN` is affected. Its two siblings already had tokens: `+oo` and `-oo` both print and parse, and +`Latexize` has had `\mathrm{undefined}` for all along — which is the token +[CSharpMath](https://github.com/verybadcat/CSharpMath) decodes back to `MathS.NaN`, so the LaTeX round +trip was closed already and is untouched here. + +**The round-trip test now runs in both directions.** Every case in `StringizeRoundTripTest` began from a +*string*, so it could only reach expressions the parser already produces, and a value with no source +form was invisible to all of them however many cases were added. It now also enumerates the library's +named constants by reflection — `MathS` and `Entity.Number.Real` — prints each and reads it back, so a +constant added later is covered without anyone remembering the file exists. Those two cases are what +fail against the old grammar. + +Issue [#906](https://github.com/asc-community/AngouriMath/issues/906). + +--- + +## 2.0.0 — since 1.4.0 + +### At a glance + +| Silent? | What | Was | Is | +|---|---|---|---| +| loud | `Minusf.Minuend` / `.Subtrahend` | named for the wrong operand | named for the right one | +| **silent** | `exp(x)`, `log10(x)`, `log2(x)` | products of undeclared variables | the functions | +| loud | `arcsinh(x)` and its five relatives | a product | `UnrecognizedFunctionParseException` | +| loud | `floor(x)` and ten other names the library lacks | a product, silently | `UnrecognizedFunctionParseException` | +| **silent** | `sqrt(x^2)`, `sqrt(-x)` and their kind | `x`, `i*sqrt(x)` — wrong for negative x | left as written | +| loud | `mod` as a variable name | a variable | a keyword, so a parse error | +| **silent** | `Stringize` of powers, lambdas, applications, piecewises | did not parse back | parses back | +| **silent** | `Stringize` of a complex number with a fractional imaginary part | read back as its negation, or as a power | parses back | +| **silent** | numbers below `1e-16` | rounded to `0` | kept | +| **silent** | `Real` `%` | `-7 % 3` was `-1` | `2` | +| **silent** | `Simplify` of a cancelling quotient | the quotient | a `Providedf` | +| **silent** | radicals, everywhere | `sqrt(12)` | `2 * sqrt(3)` | +| **silent** | `Expand` | left like terms uncollected | collects them | +| **silent** | an identity equation | `{ }` or `{ 0 }` | all of `CC` | +| **silent** | a quadratic inequality | wrong for one sign of a symbolic coefficient | a case split on that sign | +| **silent** | numeric root sets | one root per starting point | one root per root | +| **silent** | many limits | `NaN`, or unevaluated | a value | +| **silent** | a vanishing sine behind a constant factor | `NaN` | a value — but `sin(x)*ln(x)*2` loses its `0` | +| **silent** | a limit assembling `oo^0` or `1^oo` | that form's value, `1` | the real answer, or unevaluated | +| **silent** | a factorial under a vanishing exponent | unevaluated | a value, by Stirling | +| **silent** | `ln(x!)` in a limit | unevaluated, and `ln(x!)/ln(x)` was `NaN` | a value | +| **silent** | a boolean expression | factored, not minimised | minimised where that is shorter | +| **silent** | some integrals | not antiderivatives | closed forms | +| **silent** | two integrals | answered correctly | unevaluated — a deliberate loss | +| **silent** | `k/(a x^2 + c)` and `k/sqrt(a x^2 + c)` for symbolic `a` | `NaN` | a piecewise on the sign of the discriminant | +| loud | `Compile` over a missing variable | `KeyNotFoundException` | `UncompilableNodeException` | +| loud | `Expand` of a quotient of factorials | `AngouriBugException` | the expanded polynomial | +| loud | parsing a `provided` in a parenthesised comma list | `NullReferenceException` | `UnhandledParseException` | +| loud | `floor(x)`, `ceil(x)`, `ceiling(x)` | `UnrecognizedFunctionParseException` | the functions | +| loud | `round(x)`, `min(a, b)`, `max(a, b)`, `gcd(a, b)` | `UnrecognizedFunctionParseException` | the functions | +| loud | 28 members deprecated since 1.x | obsolete but present | removed | +| loud | `Latexise`, `ILatexiseable`, `entity_latexise` | the British spelling | `Latexize`, `ILatexizeable`, `entity_latexize` | +| loud | `MathS.Quantum.Factorise` | one letter from the unrelated `Entity.Factorize` | `MathS.Quantum.TensorFactorize` | +| loud | `MathS.Quantum.IsNormalised` | the British spelling | `MathS.Quantum.IsNormalized` | +| loud | the target frameworks | `net7.0;netstandard2.0` | `netstandard2.0;net8.0;net10.0` | +| **silent** | `abs(x) = c` for a negative `c` | a set of non-solutions | the empty set | +| loud | implicit `List` to `Entity` | made a `FiniteSet`, and made three `params` overloads uncallable | removed | +| loud | implicit `Entity[]` to `Entity` | made a `FiniteSet`, discarding order and repeats | removed | +| loud | implicit `(Entity, Entity)` to `Entity` | made a **closed** interval, though `(a, b)` reads as the open one | removed | +| **silent** | a `MathS.Settings` scope across an `await`, or inside a task | lost, or somebody else's | follows the call | +| **silent** | a `RewriteRecording` across an `await`, or work started under it | lost, or somebody else's | follows the call | +| loud | a polynomial system with more equations than unknowns | `WrongNumberOfArgumentsException` | solved | +| loud | a known gap, e.g. a cubic inequality | `AngouriBugException`, asking to be reported | `NotSufficientlySupportedException` | +| **silent** | `arcsin(sin(x))` and three siblings | `x`, wrong wherever `x` leaves the principal interval | left as written unless `x` is a real in that interval | + +--- + +## Target frameworks + +`net7.0` is replaced by `net8.0` and `net10.0`. `netstandard2.0` is unchanged, so consumers on +.NET Framework and the older runtimes are unaffected. + +| | was | is | +|---|---|---| +| `TargetFrameworks` | `net7.0;netstandard2.0` | `netstandard2.0;net8.0;net10.0` | + +.NET 7 left support in May 2024, so the old list named one framework nobody should still be +targeting and no supported modern one. A consumer on net8.0 or net10.0 previously resolved the +`netstandard2.0` asset and silently lost the generic-math surface with it; they now get a real +target. + +**What breaks.** Anything that pins the framework of its reference: + +```xml + + TargetFramework=net7.0 + +``` + +fails with `NETSDK1005: Assets file ... doesn't have a target for 'net7.0'`. Change it to `net10.0` +or `net8.0`. This is not hypothetical -- every one of the nine measurement harnesses in this +workspace pinned `net7.0` and had to be repointed. + +A consumer that references the NuGet package rather than the project picks its asset by its own +framework and needs no change, unless it targets `net7.0` itself, in which case it now resolves +`netstandard2.0` and loses the generic-math types. + +**Generic math is unchanged in reach.** `Core/Entity/GenericMath` needs `INumber` and so is +still compiled only into the net7.0-and-later targets -- now written as a compatibility test +rather than a literal `!= 'net7.0'`, so adding a framework does not silently drop it. Verified on +the built assemblies: present in `net8.0` and `net10.0`, absent from `netstandard2.0`. + +--- + +## Types and members + +### The implicit conversions from a collection are removed + +`Entity` had two implicit conversions from a collection, both building a `FiniteSet`: + +```csharp +public static implicit operator Entity(Entity[] elements); // removed +public static implicit operator Entity(List elements); // removed +``` + +The second one made three public members impossible to call with a `List`: + +```csharp +var equations = new List { "x - 1", "y - 2" }; +new EquationSystem(equations); // error CS0121: the call is ambiguous +new FiniteSet(equations); // error CS0121 +MathS.Equations(equations); // error CS0121 +``` + +Each of those has both an `IEnumerable` overload and a `params Entity[]` overload. With +the conversion in place a `List` also converts to a single `Entity`, so the `params` +overload becomes applicable in its expanded form, neither candidate is better than the other, +and the call does not compile. The array, `IEnumerable`, and variadic forms were always +fine — only a concrete `List` broke, which is why it survived to a 2.0 preview. + +This was not three separate defects. One conversion made *every* `params Entity[]` overload in +the library uncallable with a list, including any added later. + +**This one moves between previews.** The conversion is in `2.0.0-preview.1` and +`2.0.0-preview.2`, both published, and is gone in `2.0.0`. It is the only entry here that +changes between two previews rather than between releases: a reader coming from 1.3.0 or +1.4.0 can ignore the distinction, one already on a preview cannot. -**Where a caller meets it.** `EquationSystem.Solve` returns a `Matrix`, so a solved system's entries -were left in whatever form the solver built them in: +**What breaks.** Assigning a list where an `Entity` is expected: +```csharp +Entity set = new List { 1, 2, 3 }; // no longer compiles ``` -MathS.Equations("x2 + y", "y - x - 3").Solve("x", "y").Simplify() -was: [[-(-(5 - sqrt(-11)) / 2 + 3), (5 - sqrt(-11)) / 2], [-(-(5 + sqrt(-11)) / 2 + 3), (5 + sqrt(-11)) / 2]] -is: [[-1/2 + -1/2 * sqrt(-11), 5/2 + -1/2 * sqrt(-11)], [1/2 * sqrt(-11) + -1/2, 1/2 * sqrt(-11) + 5/2]] -``` +Write one of these instead: -Issue [#882](https://github.com/asc-community/AngouriMath/issues/882), which -[#497](https://github.com/asc-community/AngouriMath/issues/497) names as the shape of defect to hunt: -the same input simplifying or not depending on its parent. +```csharp +Entity set = new FiniteSet(new List { 1, 2, 3 }); +Entity set = new List { 1, 2, 3 }.ToSet(); +``` -### A conditional set's bound variable is renamed to a temporary +**The conversion from `Entity[]` is gone as well.** It was kept at first, on the narrow +ground that it never produced the ambiguity — an array binds to the `params` overload in its +normal form by an identity conversion, which wins outright. That was true and beside the +point. An array carries an order and can repeat an element; a set has neither, so the +conversion silently discarded part of what it was handed, and an implicit conversion that +loses information is the wrong shape regardless of which overloads it happens to break. +Set types are built explicitly nearly everywhere for this reason. -`DirectChildren` of `{ x : P(x) }` renames the binder, so that the bound `x` is not read as an `x` -that may be free outside the set. The replacement name was four lowercase letters derived from the -predicate's hash code, and it was produced through `MathS.Var`, which **parses** — so a name that the -parser reads as something other than a variable did not come back as one. +```csharp +Entity set = new Entity[] { 1, 2, 3 }; // no longer compiles either +Entity set = new FiniteSet(1, 2, 3); // say it +Entity set = new Entity[] { 1, 2, 3 }.ToSet(); +``` -| | was | is | -|---|---|---| -| `new ConditionalSet("x", "x > 0").DirectChildren[0]` | `abcd > 0`, a different name in every process | `%1 > 0` | -| one predicate in `26^4` | `CannotParseInstanceException: Cannot parse an instance of Variable from `true`` | `%1 > 0` | +Only one place in the whole repository relied on it, which was the test pinning it. -Four lowercase letters can spell `true`. They can also spell a variable the predicate already uses, in -which case the rename captures it and the set means something else — silently, with no exception to -say so. The name now comes from `Variable.CreateTemp`, which reads the predicate's variables rather -than its hash and hands back `%1` upward, skipping what is taken: fresh by construction, and with no -reading as anything but a variable, since the parser does not accept `%`. +**And the conversion from a pair, which had the same fault from the other side.** A +two-element tuple became an `Interval`, and since a tuple says nothing about whether its +endpoints are included, the conversion had to supply that: -**Printed output does not change.** `Stringize` reads the node's own fields, so `{ x : x > 0 }` still -prints with its own binder, and `Solve` answers containing a conditional set are unaffected. Nothing -could have depended on the old name either, since string hash codes are randomised per process and it -was therefore different on every run — which is also why this surfaced as a CI failure that would not -reproduce. +```csharp +Entity e = ((Entity)1, (Entity)5); +// was: [1; 5] — both endpoints included, and 1 is a member +``` -`Variable.CreateRandom`, which had this one caller, is removed. It was `internal`. +Where the array conversion dropped information that was there, this one produced +information that was not — and chose the reading opposite to the notation, since `(1, 5)` +is the open interval in ordinary mathematical writing and `[1, 5]` the closed one. A caller +writing what looks like an open interval got a closed one, silently. -Issue [#891](https://github.com/asc-community/AngouriMath/issues/891). +```csharp +Entity e = MathS.Interval(1, 5); // closed, said out loud +Entity e = MathS.Interval(1, false, 5, false); // open +Entity e = new Interval(1, true, 5, true); +``` -### `abs(sgn(x))` and `sgn(abs(x))` are not `1` at zero +Nothing in the library, the tests, the F# wrapper or the utilities used it. -`|sgn(z)|` and `sgn(|z|)` are `1` for every `z` except `0`, where both are `0`, because `sgn(0)` is -`0`. Both rewrites answered `1` for any argument. +The pairs elsewhere in the API are unaffected, because none of them has to guess: an +integration `Range`, the arguments of `Substitute`, the cases of `MathS.Piecewise` and +`ToProvided` are all ordered pairs whose two halves have distinct, stated roles. -| | was | is | -|---|---|---| -| `abs(sgn(x))`, `sgn(abs(x))` | `1` | left as written | -| the same at `x = 0` | `1` | `0` — which is the value | -| `abs(sgn(0))`, `sgn(abs(0))` | `0`, unchanged | | -| `abs(sgn(2))`, `sgn(abs(-3))` | `1`, unchanged | | -| `signum(abs(x/x))`, `abs(signum(x/x))` | `1 provided not x = 0` | left as written | +### An inverse trigonometric function no longer cancels its own function off the principal branch -The rules consulted the argument's `DomainCondition`, which answers a different question from the one -they needed: a bare `x` is defined everywhere and can *be* zero, while `x / x` is defined only away -from zero and is nonzero throughout. Reading the domain conflated the two. +`arcsin` is a left inverse of `sin` only on `[-pi/2, pi/2]`, and the three siblings likewise only on +their own intervals. The rewrite was unconditional, so `Simplify` returned a value that is wrong at +every real point outside the interval: -**A condition would not have been the fix**, for the same reason as in the two entries above: the -expression is defined at zero and equal to `0` there, so `1 provided not z = 0` would replace a wrong -value with a wrong domain. There is no closed form for "1 away from zero, 0 at it" other than the -expressions themselves, so the rules decide where the argument's value can be read and decline -otherwise. +| | was | is | the value at that point | +|---|---|---|---| +| `arcsin(sin(x))` | `x` | left as written | `pi - x` for `x` in `[pi/2, 3pi/2]`, and so on | +| `arccos(cos(x))` | `x` | left as written | `2*pi - x` for `x` in `[pi, 2pi]` | +| `arctan(tan(x))` | `x` | left as written | `x - pi` for `x` in `(pi/2, 3pi/2)` | +| `arccotan(cotan(x))` | `x` | left as written | `x - pi` for `x` in `(pi, 2pi)` | -**The last row is a coverage loss, not a correction.** `1 provided not x = 0` was right for `x / x`. -Separating it from the `x` case needs "nonzero throughout its domain", and neither test available in -`InnerSimplify` can express it — `Evaled` leaves `x / x` as `x / x`, and `DomainCondition` is what -conflated them; `Simplify` would answer it and cannot be called from there (#403). +Measured on a build of `35f4ae5a` before this change and after it. The four rules are older than +1.4.0 and untouched since, so 1.4.0 answers the same way, but the numbers below are from the two +2.0.0 builds rather than from a 1.4.0 one: -Found by `boundcheck` after adding `x = 0` and `x = 1` to its sample points: 366 shapes had been -passing at 23 points chosen for branch cuts and principal intervals, none of which was the place -where a rule's own arithmetic degenerates. Issue -[#892](https://github.com/asc-community/AngouriMath/issues/892). +``` +"arcsin(sin(x))".Simplify().Substitute("x", 3).EvalNumerical() + was 3 + is 0.14159... which is pi - 3, and is what arcsin(sin(3)) equals -### `abs` folds where the sign of its argument is known +"arctan(tan(x))".Simplify().Substitute("x", 2).EvalNumerical() + was 2 + is -1.14159... = 2 - pi -`|x|` is `x` for a non-negative real `x` and `-x` for a negative one. That is the definition of the -function rather than an identity with a side condition, and it was applied only when the argument -was a *number*. An argument whose value is a known real without its node being a number was left -alone, so a radical or a constant kept its `abs`: +"arccos(cos(x))".Simplify().Substitute("x", 4).EvalNumerical() + was 4 + is 2.28318... = 2*pi - 4 +``` -| | was | is | -|---|---|---| -| `abs(-sqrt(6))` | left as written | `sqrt(6)` | -| `abs(-pi)`, `abs(-e)` | left as written | `pi`, `e` | -| `abs(1 - sqrt(2))` | left as written | `sqrt(2) - 1` | -| `abs(-2)` | `2` | `2`, unchanged | -| `abs(sqrt(-4))` | `2` | `2`, unchanged — the magnitude of `2i` | -| `abs(-a)` for symbolic `a` | left as written | left as written | +**Where the argument is a real number inside the interval, the cancellation still happens and stays +exact**: `arcsin(sin(1/2))` is `1/2`, not a decimal. Where it is symbolic, the expression is left as +written, which is what SymPy and Mathematica both answer. -Where this shows up is in an answer built out of radicals. `(2x^2 - 3 > 0) and (x > 0)` solved to -`(abs(-sqrt(6)) / 2; +oo)` and now solves to `(sqrt(6) / 2; +oo)`; the endpoint was always the same -number, printed in a form that looked like unfinished work. The -[Solvers wiki page](https://github.com/asc-community/AngouriMath/wiki/Solvers) shows the old output -and wants updating with the release. +A condition was deliberately *not* attached. `arcsin(sin(x))` is defined for every real `x`, so +`x provided x >= -pi/2 and x <= pi/2` would say the expression is undefined outside the interval +when in fact it merely has another value — trading a wrong value for a wrong domain. -**Nothing is assumed about a symbol**, and an argument off the real line is declined rather than -guessed at: `sqrt(-4)` evaluates to `2i`, whose absolute value is `2` — neither the argument nor its -negation, so a rule that read "negative, therefore negate" would be wrong there. The sign is read -off the value, and a value that is not a finite real does not answer the question. +**The other direction is unchanged**, and needs no assumption: `sin(arcsin(z))`, `cos(arccos(z))`, +`tan(arctan(z))` and `cotan(arccotan(z))` are all `z`, because they compose the *right* inverse. -Issue [#881](https://github.com/asc-community/AngouriMath/issues/881). +`CircleTest.Test8` asserted `arccotan(cotan(3x)) == 3x` and was pinning the wrong answer; it now +asserts that the expression is left alone. Issue +[#884](https://github.com/asc-community/AngouriMath/issues/884). ### A known gap no longer presents as a bug @@ -1012,99 +1123,6 @@ disagreements over 10463 expressions goes **30 to 0**. [#752](https://github.com/asc-community/AngouriMath/issues/752), PR [#758](https://github.com/asc-community/AngouriMath/pull/758). -### `MathS.ToSympyCode` emits Python that runs - -Its documented purpose is code you can run in SymPy, and for two whole classes of expression it -emitted code that did not run at all. - -| expression | was | is | -|---|---|---| -| `1/2`, `1/3 + 1/6` | `sympy.Rational(1, 2` — `SyntaxError: '(' was never closed` | `sympy.Rational(1, 2)` | -| `0/0`, `1/0` | `NaN` — `NameError: name 'NaN' is not defined` | `sympy.nan` | -| `+oo`, `-oo` | `+oo`, `-oo` — `NameError` | `sympy.oo`, `-sympy.oo` | - -The first is a missing parenthesis, and it broke **every** expression carrying a non-integer rational, -which is most of what a computer algebra system hands back. The second is a value with no binding: the -generated preamble declares a `sympy.Symbol` for each free *variable*, and a `NaN` or an infinity is -neither a variable nor something SymPy names the same way this library does. - -Both were checked by running the emitted programs against SymPy 1.14 rather than by reading them, which -is also how it was established that they now come back **exact** — `1/2` arrives as SymPy's `Half` and -not as the float `0.5`. - -Nothing else about the exporter changes. `pi`, `e` and `i` were already emitted as `sympy.pi`, -`sympy.E` and `sympy.I`, and `sqrt` as `sympy.sqrt`. - -Two tests now hold the properties that failed, without needing an interpreter in the suite: the -parentheses balance, and every name in the emitted body is either declared in the preamble or reached -through `sympy.`. 17 of their 23 cases fail against the old exporter. - -Issue [#909](https://github.com/asc-community/AngouriMath/issues/909). - -### `MathS.ToSympyCode` keeps an exact value exact - -The generated program ran, and then quietly gave a different number. Python's `/`, and its `**` with a -negative exponent, are float operations on two integers: - -| expression | emitted | SymPy read it as | now emitted | and reads as | -|---|---|---|---|---| -| `1/2` | `1 / 2` | `0.500000000000000`, a `Float` | `sympy.Integer(1) / 2` | `1/2`, a `Rational` | -| `2^(-1)` | `2 ** (-1)` | `0.5` | `sympy.Integer(2) ** (-1)` | `1/2` | -| `2^(-3)` | `2 ** (-3)` | `0.125` | `sympy.Integer(2) ** (-3)` | `1/8` | -| `x + 1/2` | `x + 1 / 2` | `x + 0.5` | `x + sympy.Integer(1) / 2` | `x + 1/2` | - -Making one operand a SymPy integer hands the arithmetic to SymPy, which keeps it exact. **Only a pair of -integers is rewritten**, and the rest of the emitted code is unchanged: with a symbol anywhere in the -shape SymPy's own operators already take over (`x / 2`, `1 / x`, `x ** (-1)`), and `+`, `-`, `*` and a -non-negative `**` are exact on Python integers, whose precision is unbounded — `2 ** 70` was always -right. - -It bit only the **unsimplified** form, which is the one a caller writes: `"1/2".ToEntity()` is a `Divf` -of two integers, because a printed rational parses back as a division -([#873](https://github.com/asc-community/AngouriMath/issues/873)), while a simplified `1/2` is a -`Rational` node and already emitted `sympy.Rational(1, 2)`. - -Checked by running the emitted programs against SymPy 1.14, which is the only way this class of defect -shows itself — the code was always valid, so the earlier tests could not have caught it, and the two -added here assert the property instead: no two plain integer literals are combined with `/` or with a -negative `**`. - -Issue [#911](https://github.com/asc-community/AngouriMath/issues/911). - -### `NaN` is now a keyword, and the printed form of NaN reads back - -`Stringize` prints the NaN value as `NaN`, and the grammar had no such token, so reading it back gave a -**variable of that name**. A variable behaves like any symbol — it cancels, collects and compares — and -nothing on the page distinguished the two, since a variable named `NaN` also prints as `NaN`: - -| expression | was | is | -|---|---|---| -| `"NaN - NaN".Simplify()` | `0` | `NaN` | -| `"NaN / NaN".Simplify()` | `1 provided not NaN = 0` | `NaN` | -| `"NaN * 0".Evaled` | `0` | `NaN` | -| `"NaN * 2".Evaled`, `"NaN + NaN".Evaled` | left as written | `NaN` | -| `NaN` as a variable name | a variable | a parse of the NaN value | -| `NaNx`, `NaN_1`, `aNaN` | variables | variables, unchanged | - -This is the same trade `mod` took above: a word that was an identifier becomes a keyword, so an -expression using it as a variable name now means something else. It is the narrower half of the trade — -only the exact spelling is reserved, because the lexer takes the longest match, so any longer name that -merely contains it is still a variable. - -Only `NaN` is affected. Its two siblings already had tokens: `+oo` and `-oo` both print and parse, and -`Latexize` has had `\mathrm{undefined}` for all along — which is the token -[CSharpMath](https://github.com/verybadcat/CSharpMath) decodes back to `MathS.NaN`, so the LaTeX round -trip was closed already and is untouched here. - -**The round-trip test now runs in both directions.** Every case in `StringizeRoundTripTest` began from a -*string*, so it could only reach expressions the parser already produces, and a value with no source -form was invisible to all of them however many cases were added. It now also enumerates the library's -named constants by reflection — `MathS` and `Entity.Number.Real` — prints each and reads it back, so a -constant added later is covered without anyone remembering the file exists. Those two cases are what -fail against the old grammar. - -Issue [#906](https://github.com/asc-community/AngouriMath/issues/906). - ### `mod` is now a keyword | | | diff --git a/Sources/AngouriMath/Docs/WhatsNew/version_performance_control.md b/Sources/AngouriMath/Docs/WhatsNew/version_performance_control.md index fac6275e7..97f7df79c 100644 --- a/Sources/AngouriMath/Docs/WhatsNew/version_performance_control.md +++ b/Sources/AngouriMath/Docs/WhatsNew/version_performance_control.md @@ -157,3 +157,64 @@ and it is on the hot path. Worth a measurement of its own before more guards are `EvalEasy` is not a real movement in either direction. It measures a cached lookup rather than arithmetic — see the note above the main table — and at one nanosecond the figure is dominated by whatever the harness itself costs. + +## The 1691st and the 1707th, measured together on one machine — the pair for 2.1.0 + +`v2.0.0` itself against the 2.1.0 candidate: the 1691st is +[`f2594259`](https://github.com/asc-community/AngouriMath/commit/f2594259), tagged `v2.0.0`, and the +1707th is [`9446c762`](https://github.com/asc-community/AngouriMath/commit/9446c762), sixteen commits +later. Both measured minutes apart in one session on one machine, so **only these two columns may be +read against each other**; neither may be compared with anything above. + +This is the question a release needs answered — what the version users have costs against the version +they are being offered — which the pair above could not answer, since it took the 1697th rather than +the tag. + +| Method | [1691st](https://github.com/asc-community/AngouriMath/commit/f2594259) | [1707th](https://github.com/asc-community/AngouriMath/commit/9446c762) | change | +|---|--:|--:|--:| +| ParseEasy | 5,920 ns | 5,703 ns | −3.7% | +| ParseHard | 1,343,362 ns | 1,339,656 ns | −0.3% | +| SimplifyEasy | 81,620 ns | 83,340 ns | +2.1% | +| SimplifyHard | 1,574,777,032 ns | 1,614,280,250 ns | +2.5% | +| Derivate | 13,510 ns | 13,741 ns | +1.7% | +| SolveEasy | 11,139,199 ns | 11,383,594 ns | +2.2% | +| SolveEasyMedium | 23,437 ns | 23,733 ns | +1.3% | +| SolveMedium | 397,908 ns | 416,180 ns | +4.6% | +| SolveMediumHard | 71,646,943 ns | 71,756,037 ns | +0.2% | +| SolveHard | 770,417,841 ns | 761,848,527 ns | −1.1% | +| EvalEasy | 1.211 ns | 1.336 ns | +10.3% | +| EvalTrig | 701,704 ns | 703,612 ns | +0.3% | +| EvalTrigPrecise | 22,673,032 ns | 22,585,377 ns | −0.4% | +| CompileEasy | 186,889 ns | 187,557 ns | +0.4% | +| CompileHard | 319,797 ns | 319,365 ns | −0.1% | +| RunEasy | 20.36 ns | 19.99 ns | −1.8% | +| RunMedium | 163.9 ns | 168.6 ns | +2.9% | +| RunHard | 293.0 ns | 294.6 ns | +0.5% | + +Allocation over the same pair: `SimplifyEasy` 127,964 → 128,564 B (+0.5%), `CompileEasy` +16,207 → 16,408 B (+1.2%), `ParseHard` 3,486,001 → 3,498,137 B (+0.3%), `SimplifyHard` +3,737,107,808 → 3,733,175,840 B (−0.1%). `Derivate`, `EvalTrig` and `EvalTrigPrecise` are +**byte-identical**, and the remaining rows move by tens of bytes on totals in the millions. + +**Read: no regression.** The largest real move is `SolveMedium` at +4.6%, which by the standard at the +top of this file says nothing on its own; the rest sits inside ±3%. `EvalEasy` at +10.3% is 0.125 ns and +is the cached-lookup row again. + +### It also corrects the section above + +That pair reported `SimplifyEasy` at **+8.7%**, attributed it to guards added in that range calling +`Evaled` inside a pattern's `when` clause, and asked for "a measurement of its own before more guards +are written that way". Since then roughly six more guards of exactly that shape were added — the +interval tests of [#887](https://github.com/asc-community/AngouriMath/issues/887), the zero test of +[#892](https://github.com/asc-community/AngouriMath/issues/892), the sign read of +[#881](https://github.com/asc-community/AngouriMath/issues/881), the positivity check of +[#902](https://github.com/asc-community/AngouriMath/issues/902) and the sort guard of +[#897](https://github.com/asc-community/AngouriMath/issues/897). + +`SimplifyEasy` is **+2.1%** here, and its allocation moves 0.5%. If the guards were the cause, that row +should have moved further and allocation should have followed, since `Evaled` allocates on its first +call per node. Neither happened. **So the +8.7% was mostly the machine rather than the guards, and the +attribution in the section above was wrong** — which is the same error that section itself was written +to catch, made one level down: a single number read as a cause without a second measurement to hold it +against. The action item it raised is discharged, and the answer is that this pattern is not +measurably expensive at this scale. diff --git a/Sources/Package.Build.props b/Sources/Package.Build.props index 5617e4782..bfe553ae2 100644 --- a/Sources/Package.Build.props +++ b/Sources/Package.Build.props @@ -17,7 +17,7 @@ release tag, so what ships is named by the tag; this is what a local build and the assembly metadata carry. --> - 2.0.0 + 2.1.0 2.0.0.0 - 2.0.0.0 + 2.1.0.0 WhiteBlackGoose and contributors Angouri