From 3143ff9a0cb959ef0de11a3d7d7d7e94c38c1ea9 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Mon, 10 Aug 2026 13:05:04 +0000 Subject: [PATCH 1/2] Drop the implicit Entity[] to Entity conversion as well Closes #861. #853 removed the conversion from List because it made three params overloads uncallable, and kept the one from Entity[] on the narrow ground that it never produced that ambiguity -- an array binds to the params overload in its normal form by an identity conversion, which wins outright. True, and beside the point, as Happypig375 pointed out on the issue: 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. That disqualifies it on its own. The question to ask of an implicit conversion is what it loses, not whether it breaks an overload today, and set types are built explicitly nearly everywhere for this reason. Measured before writing anything: removing it broke exactly one place in the repository, which was the test pinning it. Nothing in the library, no other test, no sample. That test now says the opposite, and still earns its keep the same way -- the file compiling is what says the conversion has not come back. Verified: 6084 C# tests and 130 F# tests pass. Co-Authored-By: Claude Opus 5 (1M context) --- BREAKING-CHANGES.md | 25 ++++++++++++++----- .../Core/Entity/Omni/Entity.Set.cs | 1 - .../Common/ListArgumentOverloadTest.cs | 18 +++++++++---- Sources/Tests/UnitTests/Common/PublicApi.txt | 1 - 4 files changed, 32 insertions(+), 13 deletions(-) diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index b6be1bfaa..9b693a182 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -58,6 +58,7 @@ read first. | 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 | | **silent** | a `MathS.Settings` scope across an `await`, or inside a task | lost, or somebody else's | follows the call | --- @@ -101,12 +102,12 @@ the built assemblies: present in `net8.0` and `net10.0`, absent from `netstandar ## Types and members -### The implicit conversion from `List` is removed +### 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); +public static implicit operator Entity(Entity[] elements); // removed public static implicit operator Entity(List elements); // removed ``` @@ -144,12 +145,24 @@ Write one of these instead: ```csharp Entity set = new FiniteSet(new List { 1, 2, 3 }); Entity set = new List { 1, 2, 3 }.ToSet(); -Entity set = new Entity[] { 1, 2, 3 }; // the array conversion is unchanged ``` -The `Entity[]` conversion is kept: an array argument binds to the `params` overload in its normal -form by an identity conversion, which beats the alternatives outright, so it never produced the -ambiguity. +**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. + ### A settings scope belongs to the call, not to the thread `MathS.Settings` values were held in `[ThreadStatic]` fields — fourteen of them. A scope diff --git a/Sources/AngouriMath/Core/Entity/Omni/Entity.Set.cs b/Sources/AngouriMath/Core/Entity/Omni/Entity.Set.cs index ba2bb62c9..2a266bba2 100644 --- a/Sources/AngouriMath/Core/Entity/Omni/Entity.Set.cs +++ b/Sources/AngouriMath/Core/Entity/Omni/Entity.Set.cs @@ -82,7 +82,6 @@ public bool Contains(Entity entity) #pragma warning disable CS1591 public static implicit operator Entity(Domain domain) => Set.SpecialSet.Create(domain); public static implicit operator Entity((Entity left, Entity right) interval) => new Interval(interval.left, true, interval.right, true); - public static implicit operator Entity(Entity[] elements) => new FiniteSet(elements); #pragma warning restore CS1591 } diff --git a/Sources/Tests/UnitTests/Common/ListArgumentOverloadTest.cs b/Sources/Tests/UnitTests/Common/ListArgumentOverloadTest.cs index cb6854b17..ec111f9df 100644 --- a/Sources/Tests/UnitTests/Common/ListArgumentOverloadTest.cs +++ b/Sources/Tests/UnitTests/Common/ListArgumentOverloadTest.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using AngouriMath; using AngouriMath.Core; +using AngouriMath.Extensions; using Xunit; namespace AngouriMath.Tests.Common @@ -59,14 +60,21 @@ public void ArrayAndVariadicFormsStillBind() } /// - /// The conversion from an array is deliberately kept, so an array in an - /// Entity position is still a set. + /// An array in an Entity position is no longer a set. Both conversions are + /// gone: an array carries an order and repeats, a set has neither, so the conversion + /// silently discarded part of what it was handed — and it is the one that made the + /// overloads above ambiguous in the first place. Building a set says so instead. /// + /// + /// This compiling is what says the conversion has not come back; the assertion is + /// only that the explicit forms mean what they used to. + /// [Fact] - public void ArrayStillConvertsToASet() + public void AnArrayIsBuiltIntoASetExplicitly() { - Entity set = new Entity[] { 1, 2, 3 }; - Assert.Equal(new Entity.Set.FiniteSet(1, 2, 3), set); + var elements = new Entity[] { 1, 2, 3 }; + Assert.Equal(new Entity.Set.FiniteSet(1, 2, 3), new Entity.Set.FiniteSet(elements)); + Assert.Equal(new Entity.Set.FiniteSet(1, 2, 3), elements.ToSet()); } } } diff --git a/Sources/Tests/UnitTests/Common/PublicApi.txt b/Sources/Tests/UnitTests/Common/PublicApi.txt index 33e85f487..60378c567 100644 --- a/Sources/Tests/UnitTests/Common/PublicApi.txt +++ b/Sources/Tests/UnitTests/Common/PublicApi.txt @@ -2126,7 +2126,6 @@ AngouriMath.Entity.op_ExclusiveOr(AngouriMath.Entity, AngouriMath.Entity) : Ango AngouriMath.Entity.op_GreaterThan(AngouriMath.Entity, AngouriMath.Entity) : AngouriMath.Entity AngouriMath.Entity.op_GreaterThanOrEqual(AngouriMath.Entity, AngouriMath.Entity) : AngouriMath.Entity AngouriMath.Entity.op_Implicit(AngouriMath.Core.Domain) : AngouriMath.Entity -AngouriMath.Entity.op_Implicit(AngouriMath.Entity[]) : AngouriMath.Entity AngouriMath.Entity.op_Implicit(PeterO.Numbers.EDecimal) : AngouriMath.Entity AngouriMath.Entity.op_Implicit(PeterO.Numbers.EInteger) : AngouriMath.Entity AngouriMath.Entity.op_Implicit(PeterO.Numbers.ERational) : AngouriMath.Entity From 19d576260285208f2f5df61f6dc298fc7fa91a43 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Mon, 10 Aug 2026 13:52:06 +0000 Subject: [PATCH 2/2] Drop the implicit pair to Interval conversion Closes #867. A two-element tuple became an Interval with both endpoints included. A tuple says nothing about whether either endpoint is in, so the conversion had to supply that, and it chose the reading opposite to the notation: (1, 5) is the open interval in ordinary mathematical writing, and this produced the closed one. A caller writing what looks like an open interval got a closed one, with 1 a member of it, and nothing said so. Where the array conversion removed in #866 dropped information that was there, this one produced information that was not. Same fault from the other side, and the same remedy -- ask for the interval by name, which makes the choice visible instead of assumed: MathS.Interval(1, 5) closed MathS.Interval(1, false, 5, false) open Measured before writing anything: nothing in the library, the tests, the F# wrapper or the utilities used it. Zero compile errors on removal, where #866 had one. The other pairs in the API are left alone, and checked rather than assumed: an integration Range, the arguments of Substitute, the cases of MathS.Piecewise and ToProvided are all ordered pairs whose halves have distinct stated roles. None of them has to guess what it was handed. Verified: 6085 C# tests and 130 F# tests pass. Co-Authored-By: Claude Opus 5 (1M context) --- BREAKING-CHANGES.md | 27 +++++++++++++++++++ .../Core/Entity/Omni/Entity.Set.cs | 1 - .../UnitTests/Common/ImplicitOperators.cs | 21 +++++++++++++++ Sources/Tests/UnitTests/Common/PublicApi.txt | 1 - 4 files changed, 48 insertions(+), 2 deletions(-) diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 9b693a182..d8ed2410d 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -59,6 +59,7 @@ read first. | **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 | --- @@ -163,6 +164,32 @@ 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. + ### A settings scope belongs to the call, not to the thread `MathS.Settings` values were held in `[ThreadStatic]` fields — fourteen of them. A scope diff --git a/Sources/AngouriMath/Core/Entity/Omni/Entity.Set.cs b/Sources/AngouriMath/Core/Entity/Omni/Entity.Set.cs index 2a266bba2..0b52c47cb 100644 --- a/Sources/AngouriMath/Core/Entity/Omni/Entity.Set.cs +++ b/Sources/AngouriMath/Core/Entity/Omni/Entity.Set.cs @@ -81,7 +81,6 @@ public bool Contains(Entity entity) #pragma warning disable CS1591 public static implicit operator Entity(Domain domain) => Set.SpecialSet.Create(domain); - public static implicit operator Entity((Entity left, Entity right) interval) => new Interval(interval.left, true, interval.right, true); #pragma warning restore CS1591 } diff --git a/Sources/Tests/UnitTests/Common/ImplicitOperators.cs b/Sources/Tests/UnitTests/Common/ImplicitOperators.cs index 6aaaea3d7..cb589ee9c 100644 --- a/Sources/Tests/UnitTests/Common/ImplicitOperators.cs +++ b/Sources/Tests/UnitTests/Common/ImplicitOperators.cs @@ -33,5 +33,26 @@ public sealed class ImplicitOperators [Fact] public void FromEInteger() => Test(EInteger.FromString("32324"), "32324"); [Fact] public void FromERational() => Test(ERational.Create(EInteger.FromString("32324"), EInteger.FromString("243244")), ((Entity)"32324/243244").InnerSimplified); [Fact] public void FromEDecimal() => Test(EDecimal.FromString("3.4"), "3.4"); + + /// + /// The conversions that used to exist from a collection and from a pair are gone, + /// because each had to supply something its input did not carry — an array has an + /// order and repeats where a set has neither, and a pair says nothing about whether + /// its endpoints are included. An interval is asked for by name instead, which also + /// makes the choice visible rather than assumed. + /// + /// + /// This test compiling is the part that matters: bring either conversion back and the + /// assertions below start passing for the wrong reason, but the ones in + /// ListArgumentOverloadTest stop building. + /// + [Fact] + public void AnIntervalIsAskedForByName() + { + Test(MathS.Interval(1, 5), new Entity.Set.Interval(1, true, 5, true)); + Assert.NotEqual( + (Entity)MathS.Interval(1, true, 5, true), + (Entity)MathS.Interval(1, false, 5, false)); + } } } diff --git a/Sources/Tests/UnitTests/Common/PublicApi.txt b/Sources/Tests/UnitTests/Common/PublicApi.txt index 60378c567..538ff42d1 100644 --- a/Sources/Tests/UnitTests/Common/PublicApi.txt +++ b/Sources/Tests/UnitTests/Common/PublicApi.txt @@ -2144,7 +2144,6 @@ AngouriMath.Entity.op_Implicit(System.String) : AngouriMath.Entity AngouriMath.Entity.op_Implicit(System.UInt16) : AngouriMath.Entity AngouriMath.Entity.op_Implicit(System.UInt32) : AngouriMath.Entity AngouriMath.Entity.op_Implicit(System.UInt64) : AngouriMath.Entity -AngouriMath.Entity.op_Implicit(System.ValueTuple) : AngouriMath.Entity AngouriMath.Entity.op_Inequality(AngouriMath.Entity, AngouriMath.Entity) : System.Boolean AngouriMath.Entity.op_LessThan(AngouriMath.Entity, AngouriMath.Entity) : AngouriMath.Entity AngouriMath.Entity.op_LessThanOrEqual(AngouriMath.Entity, AngouriMath.Entity) : AngouriMath.Entity