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
38 changes: 38 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ read first.

| Silent? | What | Was | Is |
|---|---|---|---|
| **Silent** | `"sum(k, k, 1, n)".ToEntity().FreeVariables`, and `product` | `{ k, n }` — the bound index counted as free | `{ n }` |
| **Silent** | `"integral(t * b, t, 0, 1)".ToEntity().FreeVariables`, and every integral with limits | `{ b, t }` | `{ b }` |
| | `"x ^ 3 - x > 0".ToEntity().Solve("x")`, and every polynomial inequality of degree three or more | `NotSufficientlySupportedException: Only linear and quadratic polynomial inequalities are supported` | `(-1; 0) \/ (1; +oo)` — the solution set |
| **Silent** | `"a implies (b implies c)".ToEntity().Stringize()` | `a implies b implies c`, which reads back as `(a implies b) implies c` | `a implies (b implies c)` |
| | `"(a implies b) implies c".ToEntity().Stringize()` | `(a implies b) implies c` | `a implies b implies c` |
Expand All @@ -35,6 +37,42 @@ read first.
| | `Compile` to a nullable integral return type in a NativeAOT app | `AngouriBugException: IsNaN method expected for type System.Double`, which took the process down | the compiled function |
| **Silent** | an app publishing with `PublishTrimmed` or NativeAOT | `AngouriMath.dll` was copied in whole, being unmarked | it is trimmed with the rest, since the assembly now declares `IsTrimmable` |

### A bound index is not a free variable

`FreeVariables` knew about two binders — `Lambda` and the set builder — and about no others. A
summation and a product bind their index, so `sum(k, k, 1, n)` is a function of `n` alone, and a
definite integral binds its variable between its limits. Both reported the bound name as free.

Measured on a build of 2.3.0 and a build of this branch:

| input | 2.3.0 | now |
|---|---|---|
| `sum(k, k, 1, n)` | `{ k, n }` | `{ n }` |
| `product(k, k, 1, n)` | `{ k, n }` | `{ n }` |
| `integral(t * b, t, 0, 1)` | `{ b, t }` | `{ b }` |
| `integral(t * b, t)` | `{ b, t }` | `{ b, t }` |
| `derivative(t * b, t)` | `{ b, t }` | `{ b, t }` |
| `lambda(x, x + y)` | `{ y }` | `{ y }` |
| `{ k : k > a }` | `{ a }` | `{ a }` |

The index is bound over the **bounds** as well as the body, which is what
[`Binding`](Sources/AngouriMath/Core/Binding.cs) already says of itself: the name a binder is handed
is honoured throughout it, through the summand and the bounds. So `sum(k, k, k, n)` is `{ n }` too.

**The last three rows are unchanged on purpose, and the distinction is the interesting part.** An
indefinite integral does not bind: the antiderivative of `t * b` over `t` is `b * t ^ 2 / 2 + C`,
which is still a function of `t`. Neither does a derivative — `d/dt` denotes a function of `t`. They
look like the same shape as a summation and are not, and a later sweep that completes the binder list
by adding them would make them wrong. `FreeVariablesTest` pins all of it, including the two that must
stay put.

**`Vars` and `VarsAndConsts` do not change.** They mean every name *occurring*, bound ones included —
`"sum(k, k, 1, n)".ToEntity().Vars` is still `{ k, n }` — which is what their own XML example has
always documented, listing a lambda's parameter under *variables and constants*. Occurring and free
are different questions and the three properties answer them separately.

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

### A polynomial inequality of degree three or more is answered

**Was** — every univariate polynomial inequality above degree two was refused outright, whatever its
Expand Down
33 changes: 33 additions & 0 deletions Sources/AngouriMath/Core/Entity/Entity.Definition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -616,13 +616,46 @@ public IReadOnlyList<Variable> Vars
// https://github.com/asc-community/AngouriMath/issues/989
Set.ConditionalSet(var bound, var predicate)
=> predicate.FreeVariables.Where(v => !bound.VarsAndConsts.Contains(v)).ToList(),
// A summation and a product bind their index, so sum(k, k, 1, n) is a
// function of n alone. The index is bound over the bounds as well as the
// summand, which is what Binding says of itself: the name a binder is
// handed is honoured throughout it, through the summand and the bounds.
// https://github.com/asc-community/AngouriMath/issues/1019
Summationf(var body, var index, var from, var to)
=> BoundBy(index, body, from, to),
Productf(var body, var index, var from, var to)
=> BoundBy(index, body, from, to),
// An integral binds its variable only when it has limits to bind it
// between. The indefinite one does not: the antiderivative of t * b over
// t is b * t ^ 2 / 2 + C, which is still a function of t. Nor does a
// derivative -- d/dt denotes a function of t. Their variable stays free
// on purpose, and a sweep that "fixes" that makes them wrong.
Integralf { Range: { } limits } integral
=> BoundBy(integral.Var, integral.Expression, limits.from, limits.to),
_ => new HashSet<Variable>(@this.DirectChildren.SelectMany(c => c.FreeVariables))
}
,
this
);
private LazyPropertyA<IReadOnlyCollection<Variable>> freeVariables;

/// <summary>
/// The free variables of <paramref name="parts"/> taken together, less whatever
/// <paramref name="binder"/> binds. <paramref name="binder"/> is an <see cref="Entity"/>
/// rather than a <see cref="Variable"/> because a binder's name position accepts one and
/// the parser is what settles which name it is.
/// </summary>
private static IReadOnlyCollection<Variable> BoundBy(Entity binder, params Entity[] parts)
{
var bound = binder.VarsAndConsts;
var free = new HashSet<Variable>();
foreach (var part in parts)
foreach (var v in part.FreeVariables)
if (!bound.Contains(v))
free.Add(v);
return free;
}

/// <summary>Checks if <paramref name="x"/> is a subnode inside this <see cref="Entity"/> tree.
/// Optimized for <see cref="Variable"/>.</summary>
/// <example>
Expand Down
60 changes: 60 additions & 0 deletions Sources/Tests/UnitTests/Common/FreeVariablesTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,5 +115,65 @@ public void TwoSetBuildersDifferingOnlyInTheBoundNameAreStillEqual()
=> Assert.Equal(
Entity.Boolean.True,
"{ x : x > 0 } = { y : y > 0 }".ToEntity().Simplify());

/// <summary>
/// A summation and a product bind their index: the value of sum(k, k, 1, n) depends on n
/// and not on any k outside it.
/// https://github.com/asc-community/AngouriMath/issues/1019
/// </summary>
[Theory]
[InlineData("sum(k, k, 1, n)")]
[InlineData("product(k, k, 1, n)")]
[InlineData("sum(k * 2, k, 1, n)")]
public void ARangeBinderBindsItsIndex(string exprRaw)
=> Assert.Equal(SeqVar("n"), exprRaw.ToEntity().FreeVariables);

/// <summary>
/// The index is bound over the bounds as well as the body, which is what
/// <see cref="AngouriMath.Core.Binding"/> says of itself.
/// </summary>
[Fact]
public void TheIndexIsBoundOverTheBoundsToo()
=> Assert.Equal(SeqVar("n"), "sum(k, k, k, n)".ToEntity().FreeVariables);

/// <summary>
/// A definite integral binds its variable between its limits.
/// </summary>
[Theory]
[InlineData("integral(t * b, t, 0, 1)")]
[InlineData("integral(t, t, 0, b)")]
public void ADefiniteIntegralBindsItsVariable(string exprRaw)
=> Assert.Equal(SeqVar("b"), exprRaw.ToEntity().FreeVariables);

/// <summary>
/// And the two that look like the same shape and are not. The antiderivative of
/// <c>t * b</c> over <c>t</c> is <c>b * t ^ 2 / 2 + C</c>, still a function of <c>t</c>;
/// <c>d/dt</c> denotes a function of <c>t</c> as well. Neither binds, and a sweep that
/// makes them bind makes them wrong.
/// </summary>
[Theory]
[InlineData("integral(t * b, t)")]
[InlineData("derivative(t * b, t)")]
public void AnIndefiniteIntegralAndADerivativeDoNotBind(string exprRaw)
=> Assert.Equal(
SeqVar("b", "t").OrderBy(v => v.Name),
exprRaw.ToEntity().FreeVariables.OrderBy(v => v.Name));

/// <summary>
/// Binding the index does not hide it from <see cref="Entity.Vars"/>, which means every
/// name occurring and says so.
/// </summary>
[Fact]
public void ABoundIndexStillOccurs()
=> Assert.Equal(
SeqVar("k", "n").OrderBy(v => v.Name),
"sum(k, k, 1, n)".ToEntity().Vars.OrderBy(v => v.Name));

/// <summary>Only inside the binder that declares it.</summary>
[Fact]
public void AnIndexOutsideItsBinderIsStillFree()
=> Assert.Equal(
SeqVar("k", "n").OrderBy(v => v.Name),
"sum(k, k, 1, n) + k".ToEntity().FreeVariables.OrderBy(v => v.Name));
}
}
Loading