diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md
index c6e798182..6da345a0d 100644
--- a/BREAKING-CHANGES.md
+++ b/BREAKING-CHANGES.md
@@ -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
@@ -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
diff --git a/Sources/.editorconfig b/Sources/.editorconfig
index eff7e1b3e..0b281a7ad 100644
--- a/Sources/.editorconfig
+++ b/Sources/.editorconfig
@@ -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
diff --git a/Sources/AngouriMath/Convenience/MathS.cs b/Sources/AngouriMath/Convenience/MathS.cs
index 7204c4a04..22d70b21c 100644
--- a/Sources/AngouriMath/Convenience/MathS.cs
+++ b/Sources/AngouriMath/Convenience/MathS.cs
@@ -6846,8 +6846,8 @@ public static Entity Limit(Entity expr, Entity var, Entity dest, ApproachFrom ap
///
/// The index, which this binds — it is not a free variable of the result. Naming
/// i 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
- /// .
+ /// is taken as meaning it, throughout this operator and nowhere outside it — and the
+ /// same holds of every other binder in the language. See .
///
/// The first value of the index.
/// The last value of the index, inclusive.
@@ -6876,7 +6876,7 @@ public static Entity Limit(Entity expr, Entity var, Entity dest, ApproachFrom ap
///
///
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);
///
/// A product of as runs from
@@ -6903,43 +6903,7 @@ public static Entity Sum(Entity expr, Entity var, Entity from, Entity to)
///
///
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);
-
- ///
- /// Reads i as the loop variable where it is named as one, through the summand and
- /// the bounds as well as in the index position.
- ///
- ///
- ///
- /// i is the imaginary unit, and that is decided in the lexer — NUMBER: ... |
- /// 'i' — so it never reaches the rule that makes variables and cannot be one
- /// anywhere in the language. That left sum(i, i, 1, 10) quietly summing nothing:
- /// the index was a number, so the operator had no variable to bind.
- /// #976
- ///
- ///
- /// Naming i 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 i in the summand stayed the
- /// imaginary unit, nothing would substitute, and sum(i, i, 1, 10) would answer
- /// 10i instead of 55 — a wrong answer in place of an unevaluated one.
- ///
- ///
- /// Only inside the operator that declares it. sum(i * k, k, 1, 3) is 6i, and
- /// the i in sum(i, i, 1, 3) + i outside the sum is still the imaginary unit.
- ///
- ///
- private static Entity Iterated(
- Func 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);
/// Some non-symbolic constants
[SuppressMessage("Style", "IDE1006:Naming Styles",
diff --git a/Sources/AngouriMath/Core/Antlr/AngouriMath.g b/Sources/AngouriMath/Core/Antlr/AngouriMath.g
index 13df9ab7f..816ff2492 100644
--- a/Sources/AngouriMath/Core/Antlr/AngouriMath.g
+++ b/Sources/AngouriMath/Core/Antlr/AngouriMath.g
@@ -461,8 +461,13 @@ atom returns[Entity value]
var body = $args.list.Last();
foreach (var x in ((IEnumerable)$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;
}
diff --git a/Sources/AngouriMath/Core/Antlr/AngouriMathParser.cs b/Sources/AngouriMath/Core/Antlr/AngouriMathParser.cs
index e5426d7a9..9deb85251 100644
--- a/Sources/AngouriMath/Core/Antlr/AngouriMathParser.cs
+++ b/Sources/AngouriMath/Core/Antlr/AngouriMathParser.cs
@@ -3290,8 +3290,13 @@ public AtomContext atom() {
var body = _localctx.args.list.Last();
foreach (var x in ((IEnumerable)_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;
diff --git a/Sources/AngouriMath/Core/Binding.cs b/Sources/AngouriMath/Core/Binding.cs
new file mode 100644
index 000000000..999f52a1b
--- /dev/null
+++ b/Sources/AngouriMath/Core/Binding.cs
@@ -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;
+
+ ///
+ /// The name a binder is handed, and what that name means inside it.
+ ///
+ ///
+ ///
+ /// There is one name in the language a binder cannot otherwise bind. i is the
+ /// imaginary unit and that is decided in the lexer — NUMBER: ... | 'i' — so it never
+ /// reaches the rule that makes variables and cannot be one anywhere. e and pi
+ /// are the other way round: they are s that carry a value, so a binder
+ /// takes the name without trouble — and cannot then stop it meaning the constant, since a
+ /// bound e and the constant e are one object. That is why this is about the one
+ /// named constant that is not a variable.
+ /// #976,
+ /// #984
+ ///
+ ///
+ /// Naming i 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 i beside it stayed the imaginary
+ /// unit, nothing would substitute, and sum(i, i, 1, 10) would answer 10i
+ /// instead of 55 — a wrong answer in place of an unevaluated one.
+ ///
+ ///
+ /// Only inside the binder that declares it. sum(i * k, k, 1, 3) is 6i, and the
+ /// i outside the sum in sum(i, i, 1, 3) + i is still the imaginary unit.
+ ///
+ ///
+ /// This is a holding two fields and it is the whole cost of the
+ /// feature where nothing is shadowed: is one equality test and
+ /// hands the expression straight back without walking it.
+ ///
+ ///
+ internal readonly struct Binding
+ {
+ ///
+ /// What i becomes where it is bound. Unchecked, because the checked factory
+ /// parses and the parser reads i as the imaginary unit — the very thing that
+ /// makes this type necessary.
+ ///
+ [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);
+
+ /// Reads a bound name as the binder was given it.
+ /// Whatever arrived in the name position.
+ ///
+ /// 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 , 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.
+ ///
+ internal static Binding Of(Entity name) => new(name, name is Complex && name == MathS.i);
+
+ /// The name to bind: what was given, unless that was the imaginary unit.
+ internal Entity Name => shadowed ? index : given;
+
+ ///
+ /// An expression that is inside this binder, with the bound name meaning what it means
+ /// here. Identity unless something is shadowed.
+ ///
+ internal Entity In(Entity scope) => shadowed ? scope.Replace(Rename) : scope;
+
+ ///
+ /// 2i is one token — the lexer's NUMBER ends 'i'? — so a written
+ /// 2i arrives as a single number and not as a product with anything to rename.
+ /// Under a binder that names i it is nevertheless the writer's 2 beside
+ /// the writer's i, and 2e is a product there, so this reads it as one:
+ /// sum(2i, i, 1, 3) is 12 rather than 6i.
+ ///
+ 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;
+ }
+ }
+}
diff --git a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Calculus.Classes.cs b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Calculus.Classes.cs
index 3775903e0..847da70dd 100644
--- a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Calculus.Classes.cs
+++ b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Calculus.Classes.cs
@@ -6,6 +6,7 @@
//
using System;
+using AngouriMath.Core;
namespace AngouriMath
{
@@ -35,6 +36,11 @@ public override Entity Replace(Func func) =>
///
public sealed partial record Integralf(Entity Expression, Entity Var, (Entity from, Entity to)? Range) : CalculusOperator(Expression, Var)
{
+ /// The interval integrated over, or for an indefinite integral.
+ public (Entity from, Entity to)? Range { get; init; } = Range is var (from, to)
+ ? (Binding.Of(Var).In(from), Binding.Of(Var).In(to))
+ : Range;
+
/// Reuse the cache by returning the same object if possible
private Integralf New(Entity expression, Entity var, (Entity from, Entity to)? range) =>
ReferenceEquals(Expression, expression) && ReferenceEquals(Var, var)
@@ -70,6 +76,12 @@ public override Entity Replace(Func func) =>
///
public sealed partial record Summationf(Entity Expression, Entity Var, Entity From, Entity To) : CalculusOperator(Expression, Var)
{
+ /// The first value of the index.
+ public Entity From { get; init; } = Binding.Of(Var).In(From);
+
+ /// The last value of the index, inclusive.
+ public Entity To { get; init; } = Binding.Of(Var).In(To);
+
/// Reuse the cache by returning the same object if possible
private Summationf New(Entity expression, Entity var, Entity from, Entity to) =>
ReferenceEquals(Expression, expression) && ReferenceEquals(Var, var)
@@ -89,6 +101,12 @@ public override Entity Replace(Func func) =>
/// #248
public sealed partial record Productf(Entity Expression, Entity Var, Entity From, Entity To) : CalculusOperator(Expression, Var)
{
+ /// The first value of the index.
+ public Entity From { get; init; } = Binding.Of(Var).In(From);
+
+ /// The last value of the index, inclusive.
+ public Entity To { get; init; } = Binding.Of(Var).In(To);
+
/// Reuse the cache by returning the same object if possible
private Productf New(Entity expression, Entity var, Entity from, Entity to) =>
ReferenceEquals(Expression, expression) && ReferenceEquals(Var, var)
@@ -106,6 +124,9 @@ public override Entity Replace(Func func) =>
///
public sealed partial record Limitf(Entity Expression, Entity Var, Entity Destination, ApproachFrom ApproachFrom) : CalculusOperator(Expression, Var)
{
+ /// What the variable approaches.
+ public Entity Destination { get; init; } = Binding.Of(Var).In(Destination);
+
/// Reuse the cache by returning the same object if possible
private Limitf New(Entity expression, Entity var, Entity destination, ApproachFrom approachFrom) =>
ReferenceEquals(Expression, expression) && ReferenceEquals(Var, var) && ReferenceEquals(Destination, destination)
diff --git a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Definition.cs b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Definition.cs
index 29f579e42..944020777 100644
--- a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Definition.cs
+++ b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Definition.cs
@@ -5,6 +5,8 @@
// Website: https://am.angouri.org.
//
+using AngouriMath.Core;
+
namespace AngouriMath
{
partial record Entity
@@ -36,8 +38,18 @@ public abstract record TrigonometricFunction : Function
///
/// Describes any calculus operator
///
+ ///
+ /// Every one of these binds , so this is where a bound name is
+ /// read — see for the one name in the language that needs
+ /// reading rather than taking as it arrives.
+ ///
public abstract partial record CalculusOperator(Entity Expression, Entity Var) : Function
{
+ /// The name this operator binds.
+ public Entity Var { get; init; } = Binding.Of(Var).Name;
+
+ /// The expression the operator is applied to, which may mention .
+ public Entity Expression { get; init; } = Binding.Of(Var).In(Expression);
}
///
diff --git a/Sources/AngouriMath/Core/Entity/Omni/Entity.Omni.Classes.cs b/Sources/AngouriMath/Core/Entity/Omni/Entity.Omni.Classes.cs
index 66a722c97..5bc911925 100644
--- a/Sources/AngouriMath/Core/Entity/Omni/Entity.Omni.Classes.cs
+++ b/Sources/AngouriMath/Core/Entity/Omni/Entity.Omni.Classes.cs
@@ -6,6 +6,7 @@
//
using System;
+using AngouriMath.Core;
using System.Collections;
using AngouriMath.Core.HashCode;
using AngouriMath.Core.Exceptions;
@@ -431,6 +432,12 @@ public override int GetHashCode()
///
public sealed partial record ConditionalSet(Entity Var, Entity Predicate) : Set, IEquatable
{
+ /// The name this set builder binds. See .
+ public Entity Var { get; init; } = Binding.Of(Var).Name;
+
+ /// What holds of exactly for the members of this set.
+ public Entity Predicate { get; init; } = Binding.Of(Var).In(Predicate);
+
///
public override Entity Replace(Func func)
=> func(New(Var, Predicate.Replace(func)));
diff --git a/Sources/Tests/UnitTests/Core/BinderShadowingTest.cs b/Sources/Tests/UnitTests/Core/BinderShadowingTest.cs
new file mode 100644
index 000000000..4e9b5a8f5
--- /dev/null
+++ b/Sources/Tests/UnitTests/Core/BinderShadowingTest.cs
@@ -0,0 +1,170 @@
+//
+// 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.
+//
+
+using AngouriMath;
+using AngouriMath.Extensions;
+using Xunit;
+
+namespace AngouriMath.Tests.Core
+{
+ ///
+ /// A binder handed i as the name it binds reads it as that name, and every binder in
+ /// the language does it the same way.
+ /// #976
+ ///
+ ///
+ /// e and pi need none of this and are here to say why: they are
+ /// s that carry a value, so a binder shadows them by
+ /// construction. i is a number — the lexer decides it, NUMBER: ... | 'i' — so
+ /// without this it is the one name in the language that no binder can bind.
+ ///
+ [Trait("Area", "Core")]
+ public sealed class BinderShadowingTest
+ {
+ ///
+ /// Every binder, given the same expression written over i and over an ordinary
+ /// name, answers the same thing.
+ ///
+ [Theory]
+ [InlineData("sum({0}, {0}, 1, 10)")]
+ [InlineData("product({0}, {0}, 1, 5)")]
+ [InlineData("integral({0}, {0})")]
+ [InlineData("integral({0}, {0}, 0, 1)")]
+ [InlineData("integral({0} ^ 2 + 1, {0}, 0, 2)")]
+ [InlineData("limit({0}, {0}, 0)")]
+ [InlineData("limit(sin({0}) / {0}, {0}, 0)")]
+ [InlineData("derivative({0} ^ 2, {0})")]
+ [InlineData("apply(lambda({0}, {0} + 1), 3)")]
+ public void ABinderOverIAnswersWhatTheSameBinderOverKAnswers(string shape)
+ {
+ var overK = string.Format(shape, "k").ToEntity().Simplify();
+ var overI = string.Format(shape, "i").ToEntity().Simplify();
+ // Renamed rather than compared as they stand: the two answers are the same
+ // expression written over different names wherever the name survives into it.
+ Assert.Equal(overK, overI.Substitute(Entity.Variable.CreateVariableUnchecked("i"), "k"));
+ }
+
+ ///
+ /// 2i is one token — the lexer's NUMBER ends 'i'? — so it arrives as a
+ /// single number with nothing in it to rename. Under a binder that names i it is
+ /// the writer's 2 beside the writer's i, and 2k there is a product.
+ ///
+ [Theory]
+ [InlineData("sum(2i, i, 1, 3)", "12")]
+ [InlineData("product(2i, i, 1, 3)", "48")]
+ [InlineData("sum(3 + 2i, i, 1, 3)", "21")]
+ [InlineData("integral(2i, i, 0, 1)", "1")]
+ public void AWrittenCoefficientOnTheBoundNameIsAProduct(string expression, string expected) =>
+ Assert.Equal(expected.ToEntity().Evaled, expression.ToEntity().Simplify().Evaled);
+
+ ///
+ /// The half that makes the above a fix rather than a new defect: i is read as a
+ /// name only inside the binder that declares it.
+ ///
+ ///
+ /// The last case is the one Happypig375 asked about on
+ /// #979, and it is
+ /// what SymPy answers for Sum(sqrt(-1) * i, (i, 1, 10)).
+ ///
+ [Theory]
+ [InlineData("sum(i * k, k, 1, 3)", "6i")]
+ [InlineData("integral(i * k, k, 0, 2)", "2i")]
+ [InlineData("limit(i * k, k, 2)", "2i")]
+ [InlineData("sum(i, i, 1, 3) + i", "6 + i")]
+ [InlineData("sum(sqrt(-1) * i, i, 1, 10)", "55i")]
+ public void ElsewhereItIsStillTheImaginaryUnit(string expression, string expected) =>
+ Assert.Equal(expected.ToEntity().Evaled, expression.ToEntity().Simplify().Evaled);
+
+ /// A set builder binds its name too, and used to answer NaN over this one.
+ [Fact]
+ public void ASetBuilderOverIIsTheSetItDescribes()
+ {
+ var set = (Entity.Set)"{ i : i > 0 }".ToEntity();
+ Assert.True(set.Contains(5));
+ Assert.False(set.Contains(-5));
+ }
+
+ ///
+ /// A lambda's parameter is typed , so it is the one binder
+ /// that cannot be handed the imaginary unit at all — it threw rather than answering.
+ ///
+ [Fact]
+ public void ALambdaOverIBindsIt()
+ {
+ var lambda = Assert.IsType("lambda(i, i + 1)".ToEntity());
+ Assert.IsType(lambda.Parameter);
+ Assert.Equal(4, "apply(lambda(i, i + 1), 3)".ToEntity().Simplify().EvalNumerical());
+ }
+
+ ///
+ /// Read at the node and not at the way in, so the constructor gets it as well as
+ /// and the parser.
+ ///
+ [Fact]
+ public void TheNodeItselfReadsTheName()
+ {
+ Assert.Equal(55, new Entity.Summationf(MathS.i, MathS.i, 1, 10).Simplify().EvalNumerical());
+ Assert.Equal(120, new Entity.Productf(MathS.i, MathS.i, 1, 5).Simplify().EvalNumerical());
+ Assert.Equal(55, MathS.Sum(MathS.i, MathS.i, 1, 10).Simplify().EvalNumerical());
+ }
+
+ ///
+ /// One reading for the whole binder, not one per position: the bounds are inside it too.
+ ///
+ [Fact]
+ public void TheWholeBinderReadsTheNameTheSameWay()
+ {
+ var sum = Assert.IsType(MathS.Sum(MathS.i, MathS.i, 1, MathS.i));
+ Assert.IsType(sum.Var);
+ Assert.Equal(sum.Var, sum.Expression);
+ Assert.Equal(sum.Var, sum.To);
+ }
+
+ ///
+ /// Nothing was added for e and pi because a binder already shadows them:
+ /// they are variables that carry a value, so the name in the index position binds, and
+ /// these have always worked.
+ ///
+ [Theory]
+ [InlineData("sum(e, e, 1, 3)", "6")]
+ [InlineData("product(pi, pi, 1, 4)", "24")]
+ [InlineData("integral(e, e, 0, 1)", "1/2")]
+ public void ANamedConstantThatIsAVariableNeededNothing(string expression, string expected) =>
+ Assert.Equal(expected.ToEntity(), expression.ToEntity().Simplify());
+
+ ///
+ /// And where they do not work, which is recorded here rather than believed away.
+ ///
+ ///
+ /// A bound e keeps the constant's value, because the bound name and the constant
+ /// are the same object: Variable.ConstantList is keyed by name, so a variable
+ /// called e is 2.718… wherever it is read, binder or no binder. Nothing
+ /// short of a representation that tells a bound name from a constant one fixes these,
+ /// which is why this change is about the one constant that is not a variable — for
+ /// i the bound name and the unit are distinguishable, and that is the whole reason
+ /// the fix above is possible at all.
+ /// #984
+ ///
+ [Theory]
+ [InlineData("derivative(e ^ 2, e)", "0")] // 2 * e
+ [InlineData("derivative(pi ^ 2, pi)", "0")] // 2 * pi
+ [InlineData("{ e : e > 0 }", "{ e : True }")] // { e : e > 0 }
+ public void ABoundNamedConstantStillCarriesItsValue(string expression, string expected) =>
+ Assert.Equal(expected.ToEntity(), expression.ToEntity().Simplify());
+
+ /// The same, on the path that shows it most plainly.
+ [Fact]
+ public void ABoundNamedConstantEvaluatesToTheConstant()
+ {
+ // Simplify gets this right -- it is 0 -- and Evaled does not, which is the shape of
+ // the defect: the bound name is only a constant once something reads its value.
+ Assert.Equal("limit(e, e, 0)".ToEntity().Simplify(), (Entity)0);
+ Assert.Equal(MathS.DecimalConst.e, "limit(e, e, 0)".ToEntity().Evaled.EvalNumerical().RealPart.EDecimal);
+ }
+
+ }
+}