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
47 changes: 47 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ read first.
| **Silent** | a rewritten expression under `domain(...)` — including via `Substitute` | the constraint was dropped, so it answered | it refuses, as it did before the rewrite |
| **Silent** | `"x!".Differentiate("x")`, and anything containing it | `NaN` | the unevaluated `derivative(x!, x)` |
| **Silent** | `"7/2".ToEntity()` and any quotient of two integer literals | a `Divf` | the `Rational` it denotes |
| **Silent** | `sum(i, i, 1, 10)`, and every binder given `i` as the name it binds | the imaginary unit in the name position, so nothing was bound | `i` is the bound name, and `55` |
| **Silent** | `sum(2i, i, 1, 3)` | `6i` | `12` |
| **Silent** | `derivative(i ^ 2, i)` | `0` | `2 * i` |
| **Silent** | `{ i : i > 0 }` | `NaN` | the set it describes |
| **Silent** | `limit(i, i, 0)` | unevaluated | `0` |
| **Silent** | `integral(i, i)` | `-1/2 + C` | `i ^ 2 / 2 + C` |
| | `lambda(i, i + 1)` | `InvalidArgumentParseException` | the lambda |

### A rewritten node keeps its `Codomain`, so a domain constraint no longer disappears

Expand Down Expand Up @@ -203,6 +210,46 @@ Found while documenting the members a `#pragma warning disable CS1591` was cover
conversions sit in different files and had to be read side by side to look wrong.
[#585](https://github.com/asc-community/AngouriMath/issues/585).

### A binder given `i` as the name it binds reads it as that name

`i` is the imaginary unit, and the lexer decides it — `NUMBER: ... | 'i'` — so it never reaches the
rule that makes variables and could not be a bound name anywhere in the language. Every binder
handed one therefore did something other than bind: a sum bound nothing and stayed unevaluated, a
set builder answered `NaN`, a lambda threw. Naming `i` is now read as the declaration it plainly
is, throughout that binder and nowhere else.

```csharp
"sum(i, i, 1, 10)".ToEntity().Simplify() // was: unevaluated now: 55
"sum(2i, i, 1, 3)".ToEntity().Simplify() // was: 6i now: 12
"integral(i, i)".ToEntity().Simplify() // was: -1/2 + C now: i ^ 2 / 2 + C
"limit(i, i, 0)".ToEntity().Simplify() // was: unevaluated now: 0
"derivative(i ^ 2, i)".ToEntity().Simplify() // was: 0 now: 2 * i
"{ i : i > 0 }".ToEntity().Simplify() // was: NaN now: { i : i > 0 }
"lambda(i, i + 1)".ToEntity() // was: threw now: the lambda
```

`2i` is one token, so a written coefficient on the bound name arrives as a single number with
nothing in it to rename; under a binder that names `i` it is read as the product it would be with
any other name, which is what turns `6i` into `12` above.

**Only inside the binder that declares it**, which is what makes this a fix and not a new defect:

```csharp
"sum(i * k, k, 1, 3)".ToEntity().Simplify() // 6i, unchanged
"sum(i, i, 1, 3) + i".ToEntity().Simplify() // 6 + i, unchanged
"sum(sqrt(-1) * i, i, 1, 10)".ToEntity().Simplify() // 55i — the sum's index, times the unit
```

The last is what SymPy answers for `Sum(sqrt(-1) * i, (i, 1, 10))`, which resolves the same
collision by naming the constant `I` and refusing to bind it.

`e` and `pi` are unaffected and needed nothing here: they are variables that carry a value, so a
binder has always taken those names. They are also not fully fixed by anything in this entry — a
bound `e` still means `2.718…`, since the bound name and the constant are one object.
[#984](https://github.com/asc-community/AngouriMath/issues/984) has the measurements.

[#976](https://github.com/asc-community/AngouriMath/issues/976).

---

## 2.2.0 — since 2.1.0
Expand Down
6 changes: 6 additions & 0 deletions Sources/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,15 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed
[AngouriMath/Core/CostModel.cs]
file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n

[AngouriMath/Core/Binding.cs]
file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n

[Tests/UnitTests/Core/CostModelTest.cs]
file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n

[Tests/UnitTests/Core/BinderShadowingTest.cs]
file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n

[Tests/UnitTests/Common/ListArgumentOverloadTest.cs]
file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n

Expand Down
44 changes: 4 additions & 40 deletions Sources/AngouriMath/Convenience/MathS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6846,8 +6846,8 @@ public static Entity Limit(Entity expr, Entity var, Entity dest, ApproachFrom ap
/// <param name="var">
/// The index, which this <b>binds</b> — it is not a free variable of the result. Naming
/// <c>i</c> works: it is the imaginary unit everywhere else, but declaring it as the index
/// is taken as meaning it, throughout this operator and nowhere outside it. See
/// <see cref="Iterated"/>.
/// is taken as meaning it, throughout this operator and nowhere outside it — and the
/// same holds of every other binder in the language. See <see cref="Core.Binding"/>.
/// </param>
/// <param name="from">The first value of the index.</param>
/// <param name="to">The last value of the index, inclusive.</param>
Expand Down Expand Up @@ -6876,7 +6876,7 @@ public static Entity Limit(Entity expr, Entity var, Entity dest, ApproachFrom ap
/// </code>
/// </example>
public static Entity Sum(Entity expr, Entity var, Entity from, Entity to)
=> Iterated(static (e, v, f, t) => new Summationf(e, v, f, t), expr, var, from, to);
=> new Summationf(expr, var, from, to);

/// <summary>
/// A product of <paramref name="expr"/> as <paramref name="var"/> runs from
Expand All @@ -6903,43 +6903,7 @@ public static Entity Sum(Entity expr, Entity var, Entity from, Entity to)
/// </code>
/// </example>
public static Entity Product(Entity expr, Entity var, Entity from, Entity to)
=> Iterated(static (e, v, f, t) => new Productf(e, v, f, t), expr, var, from, to);

/// <summary>
/// Reads <c>i</c> as the loop variable where it is named as one, through the summand and
/// the bounds as well as in the index position.
/// </summary>
/// <remarks>
/// <para>
/// <c>i</c> is the imaginary unit, and that is decided in the lexer — <c>NUMBER: ... |</c>
/// <c>'i'</c> — so it never reaches the rule that makes variables and cannot be one
/// anywhere in the language. That left <c>sum(i, i, 1, 10)</c> quietly summing nothing:
/// the index was a number, so the operator had no variable to bind.
/// <a href="https://github.com/asc-community/AngouriMath/issues/976">#976</a>
/// </para>
/// <para>
/// Naming <c>i</c> as the index says something about the whole operator, so it is honoured
/// throughout it. Doing it in the index position alone would be worse than not doing it at
/// all: the index would become a variable while every <c>i</c> in the summand stayed the
/// imaginary unit, nothing would substitute, and <c>sum(i, i, 1, 10)</c> would answer
/// <c>10i</c> instead of 55 — a wrong answer in place of an unevaluated one.
/// </para>
/// <para>
/// Only inside the operator that declares it. <c>sum(i * k, k, 1, 3)</c> is <c>6i</c>, and
/// the <c>i</c> in <c>sum(i, i, 1, 3) + i</c> outside the sum is still the imaginary unit.
/// </para>
/// </remarks>
private static Entity Iterated(
Func<Entity, Entity, Entity, Entity, Entity> build,
Entity expr, Entity var, Entity from, Entity to)
{
if (var != i)
return build(expr, var, from, to);
var index = Entity.Variable.CreateVariableUnchecked("i");
Entity Rename(Entity subject) => subject.Replace(node => node == i ? index : node);
return build(Rename(expr), index, Rename(from), Rename(to));
}

=> new Productf(expr, var, from, to);

/// <summary>Some non-symbolic constants</summary>
[SuppressMessage("Style", "IDE1006:Naming Styles",
Expand Down
9 changes: 7 additions & 2 deletions Sources/AngouriMath/Core/Antlr/AngouriMath.g
Original file line number Diff line number Diff line change
Expand Up @@ -461,8 +461,13 @@ atom returns[Entity value]
var body = $args.list.Last();
foreach (var x in ((IEnumerable<Entity>)$args.list).Reverse().Skip(1))
{
if (x is not Variable v) throw new InvalidArgumentParseException($"Lambda is expected to have valid parameters, {x} encountered instead");
body = body.LambdaOver(v);
/* Lambda's parameter is typed Variable, so unlike every other binder it cannot
be handed the imaginary unit and read it there. i is what a lambda over an
index is called, so it is read here instead.
https://github.com/asc-community/AngouriMath/issues/976 */
var bound = AngouriMath.Core.Binding.Of(x);
if (bound.Name is not Variable v) throw new InvalidArgumentParseException($"Lambda is expected to have valid parameters, {x} encountered instead");
body = bound.In(body).LambdaOver(v);
}
$value = body;
}
Expand Down
9 changes: 7 additions & 2 deletions Sources/AngouriMath/Core/Antlr/AngouriMathParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3290,8 +3290,13 @@ public AtomContext atom() {
var body = _localctx.args.list.Last();
foreach (var x in ((IEnumerable<Entity>)_localctx.args.list).Reverse().Skip(1))
{
if (x is not Variable v) throw new InvalidArgumentParseException($"Lambda is expected to have valid parameters, {x} encountered instead");
body = body.LambdaOver(v);
/* Lambda's parameter is typed Variable, so unlike every other binder it cannot
be handed the imaginary unit and read it there. i is what a lambda over an
index is called, so it is read here instead.
https://github.com/asc-community/AngouriMath/issues/976 */
var bound = AngouriMath.Core.Binding.Of(x);
if (bound.Name is not Variable v) throw new InvalidArgumentParseException($"Lambda is expected to have valid parameters, {x} encountered instead");
body = bound.In(body).LambdaOver(v);
}
_localctx.value = body;

Expand Down
97 changes: 97 additions & 0 deletions Sources/AngouriMath/Core/Binding.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
//
// Copyright (c) 2019-2026 Angouri.
// AngouriMath is licensed under MIT.
// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.
// Website: https://am.angouri.org.
//

namespace AngouriMath.Core
{
using static Entity;
using static Entity.Number;

/// <summary>
/// The name a binder is handed, and what that name means inside it.
/// </summary>
/// <remarks>
/// <para>
/// There is one name in the language a binder cannot otherwise bind. <c>i</c> is the
/// imaginary unit and that is decided in the lexer — <c>NUMBER: ... | 'i'</c> — so it never
/// reaches the rule that makes variables and cannot be one anywhere. <c>e</c> and <c>pi</c>
/// are the other way round: they are <see cref="Variable"/>s that carry a value, so a binder
/// takes the name without trouble — and cannot then stop it meaning the constant, since a
/// bound <c>e</c> and the constant <c>e</c> are one object. That is why this is about the one
/// named constant that is not a variable.
/// <a href="https://github.com/asc-community/AngouriMath/issues/976">#976</a>,
/// <a href="https://github.com/asc-community/AngouriMath/issues/984">#984</a>
/// </para>
/// <para>
/// Naming <c>i</c> as the bound name says something about the whole binder, so it is
/// honoured throughout it — through the summand and the bounds, not only in the name
/// position. Doing it in the name position alone would be worse than not doing it at all:
/// the index would become a variable while every <c>i</c> beside it stayed the imaginary
/// unit, nothing would substitute, and <c>sum(i, i, 1, 10)</c> would answer <c>10i</c>
/// instead of 55 — a wrong answer in place of an unevaluated one.
/// </para>
/// <para>
/// Only inside the binder that declares it. <c>sum(i * k, k, 1, 3)</c> is <c>6i</c>, and the
/// <c>i</c> outside the sum in <c>sum(i, i, 1, 3) + i</c> is still the imaginary unit.
/// </para>
/// <para>
/// This is a <see langword="struct"/> holding two fields and it is the whole cost of the
/// feature where nothing is shadowed: <see cref="Of(Entity)"/> is one equality test and
/// <see cref="In(Entity)"/> hands the expression straight back without walking it.
/// </para>
/// </remarks>
internal readonly struct Binding
{
/// <summary>
/// What <c>i</c> becomes where it is bound. Unchecked, because the checked factory
/// parses and the parser reads <c>i</c> as the imaginary unit — the very thing that
/// makes this type necessary.
/// </summary>
[ConstantField]
private static readonly Variable index = Variable.CreateVariableUnchecked("i");

private readonly Entity given;
private readonly bool shadowed;

private Binding(Entity given, bool shadowed) => (this.given, this.shadowed) = (given, shadowed);

/// <summary>Reads a bound name as the binder was given it.</summary>
/// <param name="name">Whatever arrived in the name position.</param>
/// <remarks>
/// Every binder node runs this on the way in, so it is written to cost nothing in the
/// case that is nearly all of them: a bound name is a <see cref="Variable"/>, the type
/// test fails, and the equality is never reached. Measured at 2.2ns as an equality and
/// 0.5ns this way, against about 10ns to construct the node it guards.
/// </remarks>
internal static Binding Of(Entity name) => new(name, name is Complex && name == MathS.i);

/// <summary>The name to bind: what was given, unless that was the imaginary unit.</summary>
internal Entity Name => shadowed ? index : given;

/// <summary>
/// An expression that is inside this binder, with the bound name meaning what it means
/// here. Identity unless something is shadowed.
/// </summary>
internal Entity In(Entity scope) => shadowed ? scope.Replace(Rename) : scope;

/// <remarks>
/// <c>2i</c> is one token — the lexer's <c>NUMBER</c> ends <c>'i'?</c> — so a written
/// <c>2i</c> arrives as a single number and not as a product with anything to rename.
/// Under a binder that names <c>i</c> it is nevertheless the writer's <c>2</c> beside
/// the writer's <c>i</c>, and <c>2e</c> is a product there, so this reads it as one:
/// <c>sum(2i, i, 1, 3)</c> is 12 rather than <c>6i</c>.
/// </remarks>
private static Entity Rename(Entity node)
{
// Real derives from Complex, so the second test is the one that says "and it has an
// imaginary part". NaN and the infinities are Reals and so are left alone.
if (node is not Complex complex || node is Real)
return node;
Entity scaled = complex.ImaginaryPart == Integer.One ? index : complex.ImaginaryPart * index;
return complex.RealPart.IsZero ? scaled : complex.RealPart + scaled;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

using System;
using AngouriMath.Core;

namespace AngouriMath
{
Expand Down Expand Up @@ -35,6 +36,11 @@ public override Entity Replace(Func<Entity, Entity> func) =>
/// </summary>
public sealed partial record Integralf(Entity Expression, Entity Var, (Entity from, Entity to)? Range) : CalculusOperator(Expression, Var)
{
/// <summary>The interval integrated over, or <see langword="null"/> for an indefinite integral.</summary>
public (Entity from, Entity to)? Range { get; init; } = Range is var (from, to)
? (Binding.Of(Var).In(from), Binding.Of(Var).In(to))
: Range;

/// <summary>Reuse the cache by returning the same object if possible</summary>
private Integralf New(Entity expression, Entity var, (Entity from, Entity to)? range) =>
ReferenceEquals(Expression, expression) && ReferenceEquals(Var, var)
Expand Down Expand Up @@ -70,6 +76,12 @@ public override Entity Replace(Func<Entity, Entity> func) =>
/// </remarks>
public sealed partial record Summationf(Entity Expression, Entity Var, Entity From, Entity To) : CalculusOperator(Expression, Var)
{
/// <summary>The first value of the index.</summary>
public Entity From { get; init; } = Binding.Of(Var).In(From);

/// <summary>The last value of the index, inclusive.</summary>
public Entity To { get; init; } = Binding.Of(Var).In(To);

/// <summary>Reuse the cache by returning the same object if possible</summary>
private Summationf New(Entity expression, Entity var, Entity from, Entity to) =>
ReferenceEquals(Expression, expression) && ReferenceEquals(Var, var)
Expand All @@ -89,6 +101,12 @@ public override Entity Replace(Func<Entity, Entity> func) =>
/// <remarks><a href="https://github.com/asc-community/AngouriMath/issues/248">#248</a></remarks>
public sealed partial record Productf(Entity Expression, Entity Var, Entity From, Entity To) : CalculusOperator(Expression, Var)
{
/// <summary>The first value of the index.</summary>
public Entity From { get; init; } = Binding.Of(Var).In(From);

/// <summary>The last value of the index, inclusive.</summary>
public Entity To { get; init; } = Binding.Of(Var).In(To);

/// <summary>Reuse the cache by returning the same object if possible</summary>
private Productf New(Entity expression, Entity var, Entity from, Entity to) =>
ReferenceEquals(Expression, expression) && ReferenceEquals(Var, var)
Expand All @@ -106,6 +124,9 @@ public override Entity Replace(Func<Entity, Entity> func) =>
/// </summary>
public sealed partial record Limitf(Entity Expression, Entity Var, Entity Destination, ApproachFrom ApproachFrom) : CalculusOperator(Expression, Var)
{
/// <summary>What the variable approaches.</summary>
public Entity Destination { get; init; } = Binding.Of(Var).In(Destination);

/// <summary>Reuse the cache by returning the same object if possible</summary>
private Limitf New(Entity expression, Entity var, Entity destination, ApproachFrom approachFrom) =>
ReferenceEquals(Expression, expression) && ReferenceEquals(Var, var) && ReferenceEquals(Destination, destination)
Expand Down
Loading
Loading