From 608f6ff08a1b23c7f432a21294cb617cd4d34eed Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sun, 16 Aug 2026 00:53:46 +0000 Subject: [PATCH] Keep a node's codomain when a rewrite rebuilds it (#955) Entity.Replace rebuilds every node on the path to a change, and each rebuild went through a New(...) helper whose `new(...)` starts from the type's default codomain. So any rewrite dropped a domain(...) annotation, Substitute included, since that is built on Replace: "domain(sqrt(x), ZZ)".ToEntity().Substitute("x", "4/9") evaluated to 2/3 2/3 is not an integer and the constraint existed to refuse it. The failure was silent and one-sided -- constraints were only ever weakened, so a rewrite could make an undefined expression look defined and never the reverse. 47 rebuild sites, all of the same shape: 41 target-typed `? this : new(...)` and 6 written with an explicit type in Piecewise, Interval, ConditionalSet and Matrix. Each now carries the original's codomain. The blast radius was the open question on the issue and it is zero: the suite passes 7263 with the fix in, unchanged, so nothing depended on the annotation being lost. Three tests pin it -- through Replace, through Substitute, and one level deeper -- and all three fail without the change. Found while implementing #873, whose parser-side fix is blocked behind this: any post-parse rewrite has the same hole. --- BREAKING-CHANGES.md | 19 ++++++++++ .../Entity.Continuous.AbsSignum.Classes.cs | 4 +- ...tity.Continuous.ArcTrigonometry.Classes.cs | 12 +++--- .../Entity.Continuous.Calculus.Classes.cs | 6 +-- .../Entity.Continuous.Exponential.Classes.cs | 4 +- .../Entity.Continuous.Factorial.Classes.cs | 2 +- .../Entity.Continuous.Floors.Classes.cs | 6 +-- .../Entity.Continuous.Operators.Classes.cs | 10 ++--- .../Entity.Continuous.Rounding.Classes.cs | 10 ++--- .../Entity.Continuous.Trigonometry.Classes.cs | 12 +++--- .../Discrete/Entity.Discrete.Classes.cs | 22 +++++------ .../Core/Entity/Omni/Entity.Matrix.cs | 2 +- .../Core/Entity/Omni/Entity.Omni.Classes.cs | 6 +-- .../Core/Entity/Omni/Entity.Piecewise.cs | 4 +- Sources/Tests/UnitTests/Core/Domains.cs | 37 +++++++++++++++++++ 15 files changed, 106 insertions(+), 50 deletions(-) diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 1d512c595..d0ad7ee87 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -25,6 +25,25 @@ read first. | **Silent** | `"1/x".Integrate("x")`, and every antiderivative with a logarithm | `ln(abs(x)) + C` | `ln(x) + C` | | **Silent** | `CostModel.FewestDivisions.Cost(y ^ (1 * (-1)) * x)` | `0.007`, cheaper than `x / y` | `1.007` | | **Silent** | `Real.NaN > (Real)1`, and the other three operators | `true` | `false` | +| **Silent** | a rewritten expression under `domain(...)` — including via `Substitute` | the constraint was dropped, so it answered | it refuses, as it did before the rewrite | + +### A rewritten node keeps its `Codomain`, so a domain constraint no longer disappears + +`Entity.Replace` rebuilds every node on the path to a change, and a rebuilt node started from its +type's default codomain rather than the one the original carried. Any rewrite therefore dropped a +`domain(...)` annotation — including `Substitute`, which is built on `Replace`. + +```csharp +"domain(sqrt(x), ZZ)".ToEntity().Substitute("x", "4/9".ToEntity()).EvalNumerical() +// was: 2/3 now: NaN — 2/3 is not an integer, so the constraint refuses it +``` + +The old behaviour was **silent** and one-sided: constraints were only ever weakened, never +strengthened, so a rewrite could make an undefined expression look defined but never the reverse. +An expression that answered a value where it should have refused now refuses. + +Measured: the whole suite passes unchanged with the fix in, so nothing in the library depended on +the annotation being lost. [#955](https://github.com/asc-community/AngouriMath/issues/955). ### `Real`'s comparison operators refuse `NaN` instead of ordering it diff --git a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.AbsSignum.Classes.cs b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.AbsSignum.Classes.cs index 3fbe93d9c..20f2ff4b7 100644 --- a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.AbsSignum.Classes.cs +++ b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.AbsSignum.Classes.cs @@ -20,7 +20,7 @@ public sealed partial record Signumf(Entity Argument) : Function, IUnaryNode public Entity NodeChild => Argument; private Signumf New(Entity arg) => - ReferenceEquals(Argument, arg) ? this : new(arg); + ReferenceEquals(Argument, arg) ? this : new(arg) { Codomain = Codomain }; /// public override Entity Replace(Func func) => func(New(Argument.Replace(func))); /// @@ -36,7 +36,7 @@ public sealed partial record Absf(Entity Argument) : Function, IUnaryNode public Entity NodeChild => Argument; private Absf New(Entity arg) => - ReferenceEquals(Argument, arg) ? this : new(arg); + ReferenceEquals(Argument, arg) ? this : new(arg) { Codomain = Codomain }; /// public override Entity Replace(Func func) => func(New(Argument.Replace(func))); /// diff --git a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.ArcTrigonometry.Classes.cs b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.ArcTrigonometry.Classes.cs index 239903e16..d6b7e110b 100644 --- a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.ArcTrigonometry.Classes.cs +++ b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.ArcTrigonometry.Classes.cs @@ -20,7 +20,7 @@ public sealed partial record Arcsinf(Entity Argument) : TrigonometricFunction, I public Entity NodeChild => Argument; /// Reuse the cache by returning the same object if possible - private Arcsinf New(Entity argument) => ReferenceEquals(Argument, argument) ? this : new(argument); + private Arcsinf New(Entity argument) => ReferenceEquals(Argument, argument) ? this : new(argument) { Codomain = Codomain }; /// public override Entity Replace(Func func) => func(New(Argument.Replace(func))); /// @@ -36,7 +36,7 @@ public sealed partial record Arccosf(Entity Argument) : TrigonometricFunction, I public Entity NodeChild => Argument; /// Reuse the cache by returning the same object if possible - private Arccosf New(Entity argument) => ReferenceEquals(Argument, argument) ? this : new(argument); + private Arccosf New(Entity argument) => ReferenceEquals(Argument, argument) ? this : new(argument) { Codomain = Codomain }; /// public override Entity Replace(Func func) => func(New(Argument.Replace(func))); /// @@ -52,7 +52,7 @@ public sealed partial record Arctanf(Entity Argument) : TrigonometricFunction, I public Entity NodeChild => Argument; /// Reuse the cache by returning the same object if possible - private Arctanf New(Entity argument) => ReferenceEquals(Argument, argument) ? this : new(argument); + private Arctanf New(Entity argument) => ReferenceEquals(Argument, argument) ? this : new(argument) { Codomain = Codomain }; /// public override Entity Replace(Func func) => func(New(Argument.Replace(func))); /// @@ -68,7 +68,7 @@ public sealed partial record Arccotanf(Entity Argument) : TrigonometricFunction, public Entity NodeChild => Argument; /// Reuse the cache by returning the same object if possible - Arccotanf New(Entity argument) => ReferenceEquals(Argument, argument) ? this : new(argument); + Arccotanf New(Entity argument) => ReferenceEquals(Argument, argument) ? this : new(argument) { Codomain = Codomain }; /// public override Entity Replace(Func func) => func(New(Argument.Replace(func))); /// @@ -84,7 +84,7 @@ public sealed partial record Arcsecantf(Entity Argument) : TrigonometricFunction public Entity NodeChild => Argument; /// Reuse the cache by returning the same object if possible - private Arcsecantf New(Entity argument) => ReferenceEquals(Argument, argument) ? this : new(argument); + private Arcsecantf New(Entity argument) => ReferenceEquals(Argument, argument) ? this : new(argument) { Codomain = Codomain }; /// public override Entity Replace(Func func) => func(New(Argument.Replace(func))); /// @@ -100,7 +100,7 @@ public sealed partial record Arccosecantf(Entity Argument) : TrigonometricFuncti public Entity NodeChild => Argument; /// Reuse the cache by returning the same object if possible - private Arccosecantf New(Entity argument) => ReferenceEquals(Argument, argument) ? this : new(argument); + private Arccosecantf New(Entity argument) => ReferenceEquals(Argument, argument) ? this : new(argument) { Codomain = Codomain }; /// public override Entity Replace(Func func) => func(New(Argument.Replace(func))); /// 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 08896cf82..d53e4d973 100644 --- a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Calculus.Classes.cs +++ b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Calculus.Classes.cs @@ -22,7 +22,7 @@ public sealed partial record Derivativef(Entity Expression, Entity Var, int Iter /// Reuse the cache by returning the same object if possible private Derivativef New(Entity expression, Entity var) => ReferenceEquals(Expression, expression) && ReferenceEquals(Var, var) - ? this : new(expression, var, Iterations); + ? this : new(expression, var, Iterations) { Codomain = Codomain }; /// public override Entity Replace(Func func) => func(New(Expression.Replace(func), Var.Replace(func))); @@ -40,7 +40,7 @@ private Integralf New(Entity expression, Entity var, (Entity from, Entity to)? r ReferenceEquals(Expression, expression) && ReferenceEquals(Var, var) && (range is null && Range is null || range is var (newFrom, newTo) && Range is var (oldFrom, oldTo) && ReferenceEquals(newFrom, oldFrom) && ReferenceEquals(newTo, oldTo)) - ? this : new(expression, var, range); + ? this : new(expression, var, range) { Codomain = Codomain }; /// public override Entity Replace(Func func) => func(New(Expression.Replace(func), Var, Range is var (from, to) ? (from.Replace(func), to.Replace(func)) : null)); @@ -56,7 +56,7 @@ public sealed partial record Limitf(Entity Expression, Entity Var, Entity Destin /// 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) - && ApproachFrom == approachFrom ? this : new(expression, var, destination, approachFrom); + && ApproachFrom == approachFrom ? this : new(expression, var, destination, approachFrom) { Codomain = Codomain }; /// public override Entity Replace(Func func) => func(New(Expression.Replace(func), Var.Replace(func), Destination.Replace(func), ApproachFrom)); diff --git a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Exponential.Classes.cs b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Exponential.Classes.cs index 08e5081ec..56ec3c40e 100644 --- a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Exponential.Classes.cs +++ b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Exponential.Classes.cs @@ -18,7 +18,7 @@ public sealed partial record Powf(Entity Base, Entity Exponent) : Function, IBin { /// Reuse the cache by returning the same object if possible private Powf New(Entity @base, Entity exponent) => - ReferenceEquals(Base, @base) && ReferenceEquals(Exponent, exponent) ? this : new(@base, exponent); + ReferenceEquals(Base, @base) && ReferenceEquals(Exponent, exponent) ? this : new(@base, exponent) { Codomain = Codomain }; internal override Priority Priority => Priority.Pow; /// @@ -40,7 +40,7 @@ public sealed partial record Logf(Entity Base, Entity Antilogarithm) : Function, { /// Reuse the cache by returning the same object if possible private Logf New(Entity @base, Entity antilogarithm) => - ReferenceEquals(Base, @base) && ReferenceEquals(Antilogarithm, antilogarithm) ? this : new(@base, antilogarithm); + ReferenceEquals(Base, @base) && ReferenceEquals(Antilogarithm, antilogarithm) ? this : new(@base, antilogarithm) { Codomain = Codomain }; /// public Entity NodeFirstChild => Base; diff --git a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Factorial.Classes.cs b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Factorial.Classes.cs index 4d4b9280f..ce3ec5c3e 100644 --- a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Factorial.Classes.cs +++ b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Factorial.Classes.cs @@ -17,7 +17,7 @@ partial record Entity public sealed partial record Factorialf(Entity Argument) : Function, IUnaryNode { /// Reuse the cache by returning the same object if possible - private Factorialf New(Entity argument) => ReferenceEquals(Argument, argument) ? this : new(argument); + private Factorialf New(Entity argument) => ReferenceEquals(Argument, argument) ? this : new(argument) { Codomain = Codomain }; // This is still a function for pattern replacement internal override Priority Priority => Priority.Factorial; diff --git a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Floors.Classes.cs b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Floors.Classes.cs index de146b873..d624b2564 100644 --- a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Floors.Classes.cs +++ b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Floors.Classes.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) 2019-2026 Angouri. // AngouriMath is licensed under MIT. // Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md. @@ -26,7 +26,7 @@ public sealed partial record Floorf(Entity Argument) : Function, IUnaryNode public Entity NodeChild => Argument; private Floorf New(Entity arg) => - ReferenceEquals(Argument, arg) ? this : new(arg); + ReferenceEquals(Argument, arg) ? this : new(arg) { Codomain = Codomain }; /// public override Entity Replace(Func func) => func(New(Argument.Replace(func))); /// @@ -46,7 +46,7 @@ public sealed partial record Ceilf(Entity Argument) : Function, IUnaryNode public Entity NodeChild => Argument; private Ceilf New(Entity arg) => - ReferenceEquals(Argument, arg) ? this : new(arg); + ReferenceEquals(Argument, arg) ? this : new(arg) { Codomain = Codomain }; /// public override Entity Replace(Func func) => func(New(Argument.Replace(func))); /// diff --git a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Operators.Classes.cs b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Operators.Classes.cs index fa216a635..0cdad7e8a 100644 --- a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Operators.Classes.cs +++ b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Operators.Classes.cs @@ -30,7 +30,7 @@ public static Entity Sum(IEnumerable terms) /// Reuse the cache by returning the same object if possible private Sumf New(Entity augend, Entity addend) => - ReferenceEquals(Augend, augend) && ReferenceEquals(Addend, addend) ? this : new(augend, addend); + ReferenceEquals(Augend, augend) && ReferenceEquals(Addend, addend) ? this : new(augend, addend) { Codomain = Codomain }; internal override Priority Priority => Priority.Sum; /// @@ -65,7 +65,7 @@ public sealed partial record Minusf(Entity Minuend, Entity Subtrahend) : Continu { /// Reuse the cache by returning the same object if possible private Minusf New(Entity minuend, Entity subtrahend) => - ReferenceEquals(Minuend, minuend) && ReferenceEquals(Subtrahend, subtrahend) ? this : new(minuend, subtrahend); + ReferenceEquals(Minuend, minuend) && ReferenceEquals(Subtrahend, subtrahend) ? this : new(minuend, subtrahend) { Codomain = Codomain }; internal override Priority Priority => Priority.Minus; /// @@ -99,7 +99,7 @@ public static Entity Multiply(IEnumerable terms) /// Reuse the cache by returning the same object if possible private Mulf New(Entity multiplier, Entity multiplicand) => - ReferenceEquals(Multiplier, multiplier) && ReferenceEquals(Multiplicand, multiplicand) ? this : new(multiplier, multiplicand); + ReferenceEquals(Multiplier, multiplier) && ReferenceEquals(Multiplicand, multiplicand) ? this : new(multiplier, multiplicand) { Codomain = Codomain }; internal override Priority Priority => Priority.Mul; /// @@ -134,7 +134,7 @@ public sealed partial record Divf(Entity Dividend, Entity Divisor) : ContinuousN { /// Reuse the cache by returning the same object if possible internal Divf New(Entity dividend, Entity divisor) => - ReferenceEquals(Dividend, dividend) && ReferenceEquals(Divisor, divisor) ? this : new(dividend, divisor); + ReferenceEquals(Dividend, dividend) && ReferenceEquals(Divisor, divisor) ? this : new(dividend, divisor) { Codomain = Codomain }; internal override Priority Priority => Priority.Div; /// @@ -159,7 +159,7 @@ public sealed partial record Modf(Entity Dividend, Entity Divisor) : ContinuousN { /// Reuse the cache by returning the same object if possible internal Modf New(Entity dividend, Entity divisor) => - ReferenceEquals(Dividend, dividend) && ReferenceEquals(Divisor, divisor) ? this : new(dividend, divisor); + ReferenceEquals(Dividend, dividend) && ReferenceEquals(Divisor, divisor) ? this : new(dividend, divisor) { Codomain = Codomain }; internal override Priority Priority => Priority.Mul; /// diff --git a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Rounding.Classes.cs b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Rounding.Classes.cs index b6376a30d..c0e53ad41 100644 --- a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Rounding.Classes.cs +++ b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Rounding.Classes.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) 2019-2026 Angouri. // AngouriMath is licensed under MIT. // Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md. @@ -26,7 +26,7 @@ public sealed partial record Roundf(Entity Argument) : Function, IUnaryNode public Entity NodeChild => Argument; private Roundf New(Entity arg) => - ReferenceEquals(Argument, arg) ? this : new(arg); + ReferenceEquals(Argument, arg) ? this : new(arg) { Codomain = Codomain }; /// public override Entity Replace(Func func) => func(New(Argument.Replace(func))); /// @@ -53,7 +53,7 @@ public sealed partial record Minf(Entity Left, Entity Right) : Function, IBinary public Entity NodeSecondChild => Right; private Minf New(Entity left, Entity right) => - ReferenceEquals(Left, left) && ReferenceEquals(Right, right) ? this : new(left, right); + ReferenceEquals(Left, left) && ReferenceEquals(Right, right) ? this : new(left, right) { Codomain = Codomain }; /// public override Entity Replace(Func func) => func(New(Left.Replace(func), Right.Replace(func))); /// @@ -73,7 +73,7 @@ public sealed partial record Maxf(Entity Left, Entity Right) : Function, IBinary public Entity NodeSecondChild => Right; private Maxf New(Entity left, Entity right) => - ReferenceEquals(Left, left) && ReferenceEquals(Right, right) ? this : new(left, right); + ReferenceEquals(Left, left) && ReferenceEquals(Right, right) ? this : new(left, right) { Codomain = Codomain }; /// public override Entity Replace(Func func) => func(New(Left.Replace(func), Right.Replace(func))); /// @@ -99,7 +99,7 @@ public sealed partial record Gcdf(Entity Left, Entity Right) : Function, IBinary public Entity NodeSecondChild => Right; private Gcdf New(Entity left, Entity right) => - ReferenceEquals(Left, left) && ReferenceEquals(Right, right) ? this : new(left, right); + ReferenceEquals(Left, left) && ReferenceEquals(Right, right) ? this : new(left, right) { Codomain = Codomain }; /// public override Entity Replace(Func func) => func(New(Left.Replace(func), Right.Replace(func))); /// diff --git a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Trigonometry.Classes.cs b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Trigonometry.Classes.cs index 49b8b2ea7..5f5423990 100644 --- a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Trigonometry.Classes.cs +++ b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Trigonometry.Classes.cs @@ -20,7 +20,7 @@ public sealed partial record Sinf(Entity Argument) : TrigonometricFunction, IUna public Entity NodeChild => Argument; /// Reuse the cache by returning the same object if possible - private Sinf New(Entity argument) => ReferenceEquals(Argument, argument) ? this : new(argument); + private Sinf New(Entity argument) => ReferenceEquals(Argument, argument) ? this : new(argument) { Codomain = Codomain }; /// public override Entity Replace(Func func) => func(New(Argument.Replace(func))); /// @@ -36,7 +36,7 @@ public sealed partial record Cosf(Entity Argument) : TrigonometricFunction, IUna public Entity NodeChild => Argument; /// Reuse the cache by returning the same object if possible - private Cosf New(Entity argument) => ReferenceEquals(Argument, argument) ? this : new(argument); + private Cosf New(Entity argument) => ReferenceEquals(Argument, argument) ? this : new(argument) { Codomain = Codomain }; /// public override Entity Replace(Func func) => func(New(Argument.Replace(func))); /// @@ -52,7 +52,7 @@ public sealed partial record Tanf(Entity Argument) : TrigonometricFunction, IUna public Entity NodeChild => Argument; /// Reuse the cache by returning the same object if possible - private Tanf New(Entity argument) => ReferenceEquals(Argument, argument) ? this : new(argument); + private Tanf New(Entity argument) => ReferenceEquals(Argument, argument) ? this : new(argument) { Codomain = Codomain }; /// public override Entity Replace(Func func) => func(New(Argument.Replace(func))); /// @@ -68,7 +68,7 @@ public sealed partial record Cotanf(Entity Argument) : TrigonometricFunction, IU public Entity NodeChild => Argument; /// Reuse the cache by returning the same object if possible - private Cotanf New(Entity argument) => ReferenceEquals(Argument, argument) ? this : new(argument); + private Cotanf New(Entity argument) => ReferenceEquals(Argument, argument) ? this : new(argument) { Codomain = Codomain }; /// public override Entity Replace(Func func) => func(New(Argument.Replace(func))); /// @@ -84,7 +84,7 @@ public sealed partial record Secantf(Entity Argument) : TrigonometricFunction, I public Entity NodeChild => Argument; /// Reuse the cache by returning the same object if possible - private Secantf New(Entity argument) => ReferenceEquals(Argument, argument) ? this : new(argument); + private Secantf New(Entity argument) => ReferenceEquals(Argument, argument) ? this : new(argument) { Codomain = Codomain }; /// public override Entity Replace(Func func) => func(New(Argument.Replace(func))); /// @@ -100,7 +100,7 @@ public sealed partial record Cosecantf(Entity Argument) : TrigonometricFunction, public Entity NodeChild => Argument; /// Reuse the cache by returning the same object if possible - private Cosecantf New(Entity argument) => ReferenceEquals(Argument, argument) ? this : new(argument); + private Cosecantf New(Entity argument) => ReferenceEquals(Argument, argument) ? this : new(argument) { Codomain = Codomain }; /// public override Entity Replace(Func func) => func(New(Argument.Replace(func))); /// diff --git a/Sources/AngouriMath/Core/Entity/Discrete/Entity.Discrete.Classes.cs b/Sources/AngouriMath/Core/Entity/Discrete/Entity.Discrete.Classes.cs index e5651f2d8..4b58d85e4 100644 --- a/Sources/AngouriMath/Core/Entity/Discrete/Entity.Discrete.Classes.cs +++ b/Sources/AngouriMath/Core/Entity/Discrete/Entity.Discrete.Classes.cs @@ -98,7 +98,7 @@ public sealed partial record Notf(Entity Argument) : Statement, IUnaryNode public Entity NodeChild => Argument; private Notf New(Entity negated) => - ReferenceEquals(Argument, negated) ? this : new(negated); + ReferenceEquals(Argument, negated) ? this : new(negated) { Codomain = Codomain }; /// public override Entity Replace(Func func) => func(New(Argument.Replace(func))); /// @@ -120,7 +120,7 @@ public sealed partial record Andf(Entity Left, Entity Right) : Statement, IBinar public Entity NodeSecondChild => Right; private Andf New(Entity left, Entity right) => - ReferenceEquals(Left, left) && ReferenceEquals(Right, right) ? this : new(left, right); + ReferenceEquals(Left, left) && ReferenceEquals(Right, right) ? this : new(left, right) { Codomain = Codomain }; /// public override Entity Replace(Func func) => func(New(Left.Replace(func), Right.Replace(func))); @@ -142,7 +142,7 @@ public sealed partial record Orf(Entity Left, Entity Right) : Statement, IBinary public Entity NodeSecondChild => Right; private Orf New(Entity left, Entity right) => - ReferenceEquals(Left, left) && ReferenceEquals(Right, right) ? this : new(left, right); + ReferenceEquals(Left, left) && ReferenceEquals(Right, right) ? this : new(left, right) { Codomain = Codomain }; /// public override Entity Replace(Func func) => func(New(Left.Replace(func), Right.Replace(func))); @@ -164,7 +164,7 @@ public sealed partial record Xorf(Entity Left, Entity Right) : Statement, IBinar public Entity NodeSecondChild => Right; private Xorf New(Entity left, Entity right) => - ReferenceEquals(Left, left) && ReferenceEquals(Right, right) ? this : new(left, right); + ReferenceEquals(Left, left) && ReferenceEquals(Right, right) ? this : new(left, right) { Codomain = Codomain }; /// public override Entity Replace(Func func) => func(New(Left.Replace(func), Right.Replace(func))); @@ -186,7 +186,7 @@ public sealed partial record Impliesf(Entity Assumption, Entity Conclusion) : St public Entity NodeSecondChild => Conclusion; private Impliesf New(Entity assumption, Entity conclusion) => - ReferenceEquals(Assumption, assumption) && ReferenceEquals(Conclusion, conclusion) ? this : new(assumption, conclusion); + ReferenceEquals(Assumption, assumption) && ReferenceEquals(Conclusion, conclusion) ? this : new(assumption, conclusion) { Codomain = Codomain }; /// public override Entity Replace(Func func) => func(New(Assumption.Replace(func), Conclusion.Replace(func))); @@ -238,7 +238,7 @@ public sealed partial record Greaterf(Entity Left, Entity Right) : ComparisonSig public Entity NodeSecondChild => Right; internal Greaterf New(Entity left, Entity right) - => ReferenceEquals(Left, left) && ReferenceEquals(Right, right) ? this : new(left, right); + => ReferenceEquals(Left, left) && ReferenceEquals(Right, right) ? this : new(left, right) { Codomain = Codomain }; /// public override Entity Replace(Func func) => func(New(Left.Replace(func), Right.Replace(func))); @@ -264,7 +264,7 @@ public sealed partial record GreaterOrEqualf(Entity Left, Entity Right) : Compar public Entity NodeSecondChild => Right; internal GreaterOrEqualf New(Entity left, Entity right) - => ReferenceEquals(Left, left) && ReferenceEquals(Right, right) ? this : new(left, right); + => ReferenceEquals(Left, left) && ReferenceEquals(Right, right) ? this : new(left, right) { Codomain = Codomain }; /// public override Entity Replace(Func func) => func(New(Left.Replace(func), Right.Replace(func))); @@ -290,7 +290,7 @@ public sealed partial record Lessf(Entity Left, Entity Right) : ComparisonSign, public Entity NodeSecondChild => Right; internal Lessf New(Entity left, Entity right) - => ReferenceEquals(Left, left) && ReferenceEquals(Right, right) ? this : new(left, right); + => ReferenceEquals(Left, left) && ReferenceEquals(Right, right) ? this : new(left, right) { Codomain = Codomain }; /// public override Entity Replace(Func func) => func(New(Left.Replace(func), Right.Replace(func))); @@ -316,7 +316,7 @@ public sealed partial record LessOrEqualf(Entity Left, Entity Right) : Compariso public Entity NodeSecondChild => Right; internal LessOrEqualf New(Entity left, Entity right) - => ReferenceEquals(Left, left) && ReferenceEquals(Right, right) ? this : new(left, right); + => ReferenceEquals(Left, left) && ReferenceEquals(Right, right) ? this : new(left, right) { Codomain = Codomain }; /// public override Entity Replace(Func func) => func(New(Left.Replace(func), Right.Replace(func))); @@ -343,7 +343,7 @@ public sealed partial record Inf(Entity Element, Entity SupSet) : Statement, IBi public Entity NodeSecondChild => SupSet; internal Inf New(Entity element, Entity supSet) - => ReferenceEquals(Element, element) && ReferenceEquals(SupSet, supSet) ? this : new(element, supSet); + => ReferenceEquals(Element, element) && ReferenceEquals(SupSet, supSet) ? this : new(element, supSet) { Codomain = Codomain }; /// public override Entity Replace(Func func) => func(New(Element.Replace(func), SupSet.Replace(func))); @@ -363,7 +363,7 @@ public sealed partial record Phif(Entity Argument) : Function, IUnaryNode public Entity NodeChild => Argument; internal Phif New(Entity argument) - => ReferenceEquals(argument, Argument) ? this : new(argument); + => ReferenceEquals(argument, Argument) ? this : new(argument) { Codomain = Codomain }; /// public override Entity Replace(Func func) => func(New(Argument.Replace(func))); diff --git a/Sources/AngouriMath/Core/Entity/Omni/Entity.Matrix.cs b/Sources/AngouriMath/Core/Entity/Omni/Entity.Matrix.cs index c8f2ec06a..4c917bb9e 100644 --- a/Sources/AngouriMath/Core/Entity/Omni/Entity.Matrix.cs +++ b/Sources/AngouriMath/Core/Entity/Omni/Entity.Matrix.cs @@ -57,7 +57,7 @@ private Matrix(GenTensor innerMatrix, bool toCopy) Matrix New(GenTensor innerMatrix) => innerMatrix.Iterate().All(tup => ReferenceEquals(InnerMatrix.GetValueNoCheck(tup.Index), tup.Value)) ? this - : new Matrix(innerMatrix); + : new Matrix(innerMatrix) { Codomain = Codomain }; internal override Priority Priority => Priority.Leaf; internal Matrix Elementwise(Func operation) => New(GenTensor.CreateTensor(InnerMatrix.Shape, indices => operation(InnerMatrix.GetValueNoCheck(indices)))); diff --git a/Sources/AngouriMath/Core/Entity/Omni/Entity.Omni.Classes.cs b/Sources/AngouriMath/Core/Entity/Omni/Entity.Omni.Classes.cs index e5ae575be..66a722c97 100644 --- a/Sources/AngouriMath/Core/Entity/Omni/Entity.Omni.Classes.cs +++ b/Sources/AngouriMath/Core/Entity/Omni/Entity.Omni.Classes.cs @@ -247,12 +247,12 @@ private static bool IsALessThanB(Real A, Real B, bool closed) internal Interval New(Entity left, Entity right) => ReferenceEquals(Left, left) && ReferenceEquals(Right, right) - ? this : new Interval(left, LeftClosed, right, RightClosed); + ? this : new Interval(left, LeftClosed, right, RightClosed) { Codomain = Codomain }; internal Interval New(Entity left, bool leftClosed, Entity right, bool rightClosed) => ReferenceEquals(Left, left) && ReferenceEquals(Right, right) && LeftClosed == leftClosed && RightClosed == rightClosed - ? this : new Interval(left, leftClosed, right, rightClosed); + ? this : new Interval(left, leftClosed, right, rightClosed) { Codomain = Codomain }; /// public override Entity Replace(Func func) @@ -463,7 +463,7 @@ public override bool TryContains(Entity entity, out bool contains) internal Entity New(Entity var, Entity predicate) => ReferenceEquals(Var, var) && ReferenceEquals(Predicate, predicate) ? - this : new ConditionalSet(var, predicate); + this : new ConditionalSet(var, predicate) { Codomain = Codomain }; internal override Priority Priority => Priority.Leaf; diff --git a/Sources/AngouriMath/Core/Entity/Omni/Entity.Piecewise.cs b/Sources/AngouriMath/Core/Entity/Omni/Entity.Piecewise.cs index 94cbd5c3e..3f759ad86 100644 --- a/Sources/AngouriMath/Core/Entity/Omni/Entity.Piecewise.cs +++ b/Sources/AngouriMath/Core/Entity/Omni/Entity.Piecewise.cs @@ -26,7 +26,7 @@ public sealed partial record Providedf(Entity Expression, Entity Predicate) : En internal Providedf New(Entity expression, Entity predicate) => ReferenceEquals(expression, Expression) && ReferenceEquals(predicate, Predicate) ? this : - new Providedf(expression, predicate); + new Providedf(expression, predicate) { Codomain = Codomain }; /// public override Entity Replace(Func func) @@ -63,7 +63,7 @@ public sealed partial record Piecewise : Entity, IEquatable protected override Entity[] InitDirectChildren() => Cases.Select(c => (c.Expression, c.Predicate)).ConcatTuples().ToArray(); private Piecewise New(IEnumerable newCases) - => (Cases, newCases).SequencesAreEqualReferences() ? this : new Piecewise(newCases); + => (Cases, newCases).SequencesAreEqualReferences() ? this : new Piecewise(newCases) { Codomain = Codomain }; /// /// Creates an instance of Piecewise diff --git a/Sources/Tests/UnitTests/Core/Domains.cs b/Sources/Tests/UnitTests/Core/Domains.cs index 2b84bc7a2..871789491 100644 --- a/Sources/Tests/UnitTests/Core/Domains.cs +++ b/Sources/Tests/UnitTests/Core/Domains.cs @@ -6,6 +6,7 @@ // using AngouriMath; +using AngouriMath.Core; using AngouriMath.Extensions; using Xunit; @@ -26,6 +27,42 @@ public sealed class Domains public void CheckNaN(string expr) => Assert.Equal(MathS.NaN, expr.EvalNumerical()); + /// + /// A rewritten node keeps the codomain the original carried. + /// https://github.com/asc-community/AngouriMath/issues/955 -- `Replace` rebuilds every + /// node on the path to a change, and the rebuilt node used to start from its type's + /// default, so a domain constraint silently disappeared and the expression began + /// answering where it had refused. + /// + [Fact] + public void ARewrittenNodeKeepsItsCodomain() + { + var original = "domain(sqrt(x), ZZ)".ToEntity(); + Assert.Equal(Domain.Integer, original.Codomain); + + var rewritten = original.Replace(node => + node is Entity.Variable { Name: "x" } ? "y".ToEntity() : node); + Assert.Equal(Domain.Integer, rewritten.Codomain); + } + + /// + /// The same through `Substitute`, which is what a caller actually reaches for and is + /// built on `Replace`. sqrt(4/9) is 2/3, which is not an integer, so this must refuse. + /// + [Fact] + public void SubstitutingIntoADomainKeepsTheConstraint() + => Assert.Equal(MathS.NaN, + "domain(sqrt(x), ZZ)".ToEntity().Substitute("x", "4/9".ToEntity()).EvalNumerical()); + + /// And a constraint deeper than one level survives too. + [Fact] + public void ANestedRewriteKeepsTheCodomain() + { + var rewritten = "domain(sin(x) + 1, ZZ)".ToEntity() + .Replace(node => node is Entity.Variable { Name: "x" } ? "y".ToEntity() : node); + Assert.Equal(Domain.Integer, rewritten.Codomain); + } + [Theory] [InlineData("domain(sqrt(4), RR)")] [InlineData("domain(sqrt(4), QQ)")]