Skip to content
Closed
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
82 changes: 52 additions & 30 deletions Sources/AngouriMath/Convenience/MathS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6844,10 +6844,11 @@ public static Entity Limit(Entity expr, Entity var, Entity dest, ApproachFrom ap
/// </summary>
/// <param name="expr">The summand. It may mention <paramref name="var"/>.</param>
/// <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"/>.
/// The index, which this <b>binds</b> — it is not a free variable of the result. Passing the
/// imaginary unit declares an index <i>named</i> <c>i</c>, which shadows the constant inside
/// <paramref name="expr"/>, so <c>sum(i, i, 1, 10)</c> is the sum a textbook means by it;
/// the bounds are outside the binder and keep the constant. Anything else that is not a
/// <see cref="Entity.Variable"/> binds nothing and is carried unevaluated.
/// </param>
/// <param name="from">The first value of the index.</param>
/// <param name="to">The last value of the index, inclusive.</param>
Expand All @@ -6868,15 +6869,20 @@ public static Entity Limit(Entity expr, Entity var, Entity dest, ApproachFrom ap
///
/// Console.WriteLine(Sum("k", "k", 1, 10).Simplify());
/// Console.WriteLine(Sum("k", "k", 1, "n"));
/// Console.WriteLine(Sum("i", "i", 1, 10).Simplify());
/// </code>
/// Prints
/// <code>
/// 55
/// sum(k, k, 1, n)
/// 55
/// </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);
{
(expr, var) = ShadowImaginaryUnitIndex(expr, var);
return new Summationf(expr, var, from, to);
}

/// <summary>
/// A product of <paramref name="expr"/> as <paramref name="var"/> runs from
Expand All @@ -6885,7 +6891,10 @@ public static Entity Sum(Entity expr, Entity var, Entity from, Entity to)
/// multiplying to <c>1</c>.
/// </summary>
/// <param name="expr">The factor. It may mention <paramref name="var"/>.</param>
/// <param name="var">The index, which this binds.</param>
/// <param name="var">
/// The index, which this binds; the imaginary unit declares an index named <c>i</c>, exactly
/// as in <see cref="Sum(Entity, Entity, Entity, Entity)"/>.
/// </param>
/// <param name="from">The first value of the index.</param>
/// <param name="to">The last value of the index, inclusive.</param>
/// <returns>The product written out where it can be, and an unevaluated node otherwise.</returns>
Expand All @@ -6903,43 +6912,56 @@ 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);
{
(expr, var) = ShadowImaginaryUnitIndex(expr, var);
return new Productf(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.
/// Reads a binder whose index is the imaginary unit as declaring an index <i>named</i>
/// <c>i</c>, and shadows the constant inside the body accordingly.
/// </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>
/// <c>i</c> lexes as the imaginary unit — see the <c>NUMBER</c> rule in
/// <c>AngouriMath.g</c> — so <c>sum(i, i, 1, 10)</c>, which is how every textbook writes
/// that sum, arrived here with a number in the index position, bound nothing, and was
/// carried unevaluated. An index is a name, and the imaginary unit is not one, so there is
/// no reading under which the caller meant the constant: the only informative thing to do
/// is to take the declaration seriously and let it shadow, as any bound name shadows an
/// outer one. <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.
/// It is the <i>name</i> that is shadowed, so what changes is every literal written with it:
/// the bare <c>i</c>, and <c>2i</c> too, since that is one number token rather than a
/// product. Reading <c>sum(2i, i, 1, 3)</c> as <c>6i</c> while <c>sum(2 * i, i, 1, 3)</c> is
/// <c>12</c> would answer one expression two ways. A number with a real part as well cannot
/// be written as a single token, so no spelling of it mentions the name and it is left alone
/// — as is <c>sqrt(-1)</c>, which denotes the constant without naming it.
/// </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.
/// The bounds are left alone: they lie outside the binder, in the scope the declaration is
/// made in, so the imaginary unit is still the imaginary unit there.
/// </para>
/// </remarks>
private static Entity Iterated(
Func<Entity, Entity, Entity, Entity, Entity> build,
Entity expr, Entity var, Entity from, Entity to)
private static (Entity Expression, Entity Var) ShadowImaginaryUnitIndex(Entity expr, Entity var)
{
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));
}
if (!IsImaginaryUnit(var))
return (expr, var);
var index = Variable.CreateVariableUnchecked("i");
// Real, Rational and Integer all derive from Complex, so `is not Real` is what keeps an
// ordinary 2 out of this: a pure-imaginary number is the one kind the lexer builds out
// of the letter being shadowed.
return (expr.Replace(node => node switch
{
Complex { RealPart.IsZero: true } written when written is not Real =>
IsImaginaryUnit(written) ? index : written.ImaginaryPart * index,
_ => node
}), index);

static bool IsImaginaryUnit(Entity entity) =>
entity is Complex { ImaginaryPart: Integer(1) } complex && complex.RealPart.IsZero;
}

/// <summary>Some non-symbolic constants</summary>
[SuppressMessage("Style", "IDE1006:Naming Styles",
Expand Down
92 changes: 70 additions & 22 deletions Sources/Tests/UnitTests/Core/SummationProductTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// Website: https://am.angouri.org.
//

using System.Linq;
using AngouriMath;
using AngouriMath.Extensions;
using Xunit;
Expand All @@ -17,11 +18,10 @@ namespace AngouriMath.Tests.Core
/// over a range. <a href="https://github.com/asc-community/AngouriMath/issues/248">#248</a>
/// </summary>
/// <remarks>
/// The index is mostly written <c>k</c>, but <c>i</c> works too and the cases below say so.
/// <c>i</c> is the imaginary unit — decided in the lexer, so it cannot be a variable anywhere
/// in the language — and naming it as an index used to bind nothing and sum nothing. It is now
/// read as the declaration it plainly is, and only inside the operator that declares it.
/// <a href="https://github.com/asc-community/AngouriMath/issues/976">#976</a>
/// The index is written <c>k</c> in most of what follows, but <c>i</c> works too: it lexes as
/// the imaginary unit, and declaring it as the index shadows the constant inside the body.
/// <a href="https://github.com/asc-community/AngouriMath/issues/976">#976</a> asked for that,
/// and the tests at the bottom of this file are what it means.
/// </remarks>
[Trait("Area", "Core")]
public sealed class SummationProductTest
Expand Down Expand Up @@ -80,33 +80,77 @@ public void SubstitutingABoundIsAnOrdinarySubstitution() =>
"sum(k, k, 1, n)".ToEntity().Substitute("n", 3).Simplify().Evaled);

/// <summary>
/// <c>i</c> is the imaginary unit, and the lexer decides that — so naming it as the index
/// used to sum nothing. Naming it is now taken as the declaration it obviously is.
/// <a href="https://github.com/asc-community/AngouriMath/issues/976">#976</a>
/// <c>i</c> lexes as the imaginary unit, and declaring it as the index shadows that: the
/// sum every textbook writes with <c>i</c> is the sum a reader means by it. #976
/// </summary>
[Theory]
[InlineData("sum(i, i, 1, 10)", "55")]
[InlineData("sum(i ^ 2, i, 1, 4)", "30")]
[InlineData("product(i, i, 1, 5)", "120")]
public void TheImaginaryUnitCanBeAnIndexWhenItIsDeclaredAsOne(string expression, string expected) =>
[InlineData("sum(i^2, i, 1, 4)", "30")]
[InlineData("product(i, i, 1, 4)", "24")]
public void AnIndexNamedIShadowsTheImaginaryUnit(string expression, string expected) =>
Assert.Equal(expected.ToEntity().Evaled, expression.ToEntity().Simplify().Evaled);

/// <summary>
/// The other half, and the one that would make the change above a wrong answer if it
/// failed: <c>i</c> is reinterpreted only where it is the index. Anywhere else — inside
/// the same sum, or outside it — it is still the imaginary unit.
/// The same through the C# surface, which is why the shadowing lives in <c>MathS.Sum</c>
/// rather than in the grammar action: <c>"i"</c> converts to an entity by being parsed, so
/// this call arrives exactly as the parsed text does, and the two must not disagree.
/// </summary>
[Fact]
public void TheApiShadowsItToo() =>
Assert.Equal(((Entity)55).Evaled, Sum("i", "i", 1, 10).Simplify().Evaled);

/// <summary>
/// The shadowing reaches an imaginary <i>literal</i> too, because <c>2i</c> is one number
/// token rather than a product: answering this <c>6i</c> while <c>sum(2 * i, i, 1, 3)</c>
/// is <c>12</c> would be one expression answered two ways.
/// </summary>
[Theory]
[InlineData("sum(i * k, k, 1, 3)", "6i")]
[InlineData("sum(k + i, k, 1, 2)", "3 + 2i")]
[InlineData("sum(i, i, 1, 3) + i", "6 + i")]
public void ElsewhereItIsStillTheImaginaryUnit(string expression, string expected) =>
Assert.Equal(expected.ToEntity().Evaled, expression.ToEntity().Simplify().Evaled);
[InlineData("sum(2i, i, 1, 3)")]
[InlineData("sum(2 * i, i, 1, 3)")]
public void AnImaginaryLiteralIsTwiceTheIndexToo(string expression) =>
Assert.Equal(((Entity)12).Evaled, expression.ToEntity().Simplify().Evaled);

/// <summary>
/// And nothing is shadowed where <c>i</c> was not declared: the constant is still the
/// constant when the index is something else, which is the half that could regress silently.
/// </summary>
[Fact]
public void TheImaginaryUnitSurvivesAnIndexNamedSomethingElse() =>
Assert.Equal((3 * MathS.i).Evaled, "sum(i, k, 1, 3)".ToEntity().Simplify().Evaled);

/// <summary>A declared index with a symbolic bound is still carried, not guessed at.</summary>
/// <summary>
/// What is shadowed is the <i>name</i>, so an expression that denotes the constant without
/// naming it still denotes it — the same way shadowing works anywhere else. This is the case
/// that says the rule is syntactic rather than a hunt for the value.
/// </summary>
[Fact]
public void ADeclaredImaginaryUnitIndexWithASymbolicBoundIsCarried() =>
Assert.IsType<Entity.Summationf>("sum(i, i, 1, n)".ToEntity().Simplify());
public void AnExpressionThatOnlyEqualsTheImaginaryUnitIsNotTheName() =>
Assert.Equal((3 * MathS.i).Evaled, "sum(sqrt(-1), i, 1, 3)".ToEntity().Simplify().Evaled);

/// <summary>
/// The index is a name once it is declared, so it is bound like any other index — the
/// property <see cref="SubstitutingTheIndexFromOutsideDoesNothing"/> pins for <c>k</c>.
/// </summary>
[Fact]
public void TheShadowedIndexIsBoundLikeAnyOther()
{
var summation = "sum(i, i, 1, n)".ToEntity();
var index = Assert.Single(summation.Vars.Where(variable => variable.Name == "i"));
Assert.Equal(summation, summation.Substitute(index, 5));
}

/// <summary>
/// The bounds are outside the binder — they are written in the scope the declaration is
/// made in — so the imaginary unit there is still the imaginary unit, and a range that ends
/// at it has no integer bound and is carried.
/// </summary>
[Fact]
public void TheBoundsAreOutsideTheBinder()
{
var summation = Assert.IsType<Entity.Summationf>("sum(i, i, 1, i)".ToEntity().Simplify());
Assert.Equal(MathS.i, summation.To);
Assert.IsType<Entity.Variable>(summation.Var);
}

/// <summary>
/// Too many terms is left unexpanded: a thousand-term sum is a correct expansion and a
Expand All @@ -119,6 +163,10 @@ public void AVeryLongRangeIsNotWrittenOut() =>
[Theory]
[InlineData("sum(k, k, 1, n)")]
[InlineData("product(k, k, 1, n)")]
// A shadowed index prints as `i`, which reads as the imaginary unit anywhere else -- so the
// round trip only closes because parsing the printed form shadows it again. #976
[InlineData("sum(i, i, 1, n)")]
[InlineData("product(i^2, i, 1, n)")]
public void ItPrintsAndParsesBackToItself(string expression)
{
var original = expression.ToEntity();
Expand Down
Loading