From 0fc1e0528e56e12a1a83def8934ff37cf6a9fcd9 Mon Sep 17 00:00:00 2001 From: Robin Hillyard Date: Thu, 30 Apr 2026 10:47:32 -0400 Subject: [PATCH 1/4] Version 1.10.5: EML-driven bug fixes and robustness improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix missing case for exp(-∞) → 0 (surfaced by EML tree expansion) - Fix missing case for UniFunction(Zero, Exp) → One - Fix infinite loop in Expression.simplify for expressions involving Infinity - Add BiFunction guard: expressions involving Infinity short-circuit simplification - Add commutativity flatMap fix in BiFunction.structuralMatcher for correct re-matching after operand swap - Add Noop("NaN") results for indeterminate forms: ∞ + (-∞), 0 × ∞ - Add ExpressionMatcherPatterns test trait with simplifiesTo helper - Add Infinity rules test in ExpressionSpec - Add Noop rules (and test) - Add override evaluateAsIs in Noop - Add Noop.apply for standard "NaN" - Add Number.`∞` - Add ExpressionMatcherPatterns (currently unused) Bugs surfaced by systematic EML tree expansion (expandN) in NumberApps, which generated 458,285 distinct expression trees and exposed edge cases in the simplifier that would have been very difficult to find by hand. --- build.sbt | 2 +- .../number/core/numerical/Number.scala | 7 ++++ .../expression/expr/AtomicExpression.scala | 13 +++++++- .../number/expression/expr/BiFunction.scala | 15 +++++++-- .../number/expression/expr/Expression.scala | 23 ++++++++----- .../number/expression/expr/UniFunction.scala | 8 +++++ .../expression/expr/ValueExpression.scala | 7 +++- .../expr/CompositeExpressionSpec.scala | 4 +-- .../expr/ExpressionMatcherPatterns.scala | 14 ++++++++ .../expr/ExpressionMatchersSpec.scala | 2 +- .../expression/expr/ExpressionSpec.scala | 33 +++++++++++++++---- .../number/parse/ExpressionParserSpec.scala | 2 +- .../phasmidsoftware/number/top/TopSpec.scala | 13 +++++++- 13 files changed, 117 insertions(+), 26 deletions(-) create mode 100644 expression/src/test/scala/com/phasmidsoftware/number/expression/expr/ExpressionMatcherPatterns.scala diff --git a/build.sbt b/build.sbt index a125ad796..c81086b3d 100644 --- a/build.sbt +++ b/build.sbt @@ -2,7 +2,7 @@ ThisBuild / organization := "com.phasmidsoftware" -ThisBuild / version := "1.10.4" +ThisBuild / version := "1.10.5-SNAPSHOT" val scalaVersionNumber = "3.7.3" val catsVersion = "2.13.0" diff --git a/core/src/main/scala/com/phasmidsoftware/number/core/numerical/Number.scala b/core/src/main/scala/com/phasmidsoftware/number/core/numerical/Number.scala index 3ef3d2901..0b6941a65 100644 --- a/core/src/main/scala/com/phasmidsoftware/number/core/numerical/Number.scala +++ b/core/src/main/scala/com/phasmidsoftware/number/core/numerical/Number.scala @@ -834,6 +834,13 @@ object Number { * Exact value of i */ lazy val i: Number = ExactNumber(-1, SquareRoot) + + /** + * Represents the mathematical concept of infinity as a lazy value of type `Number`. + * The value is constructed using an exact representation of infinity via the `ExactNumber` class. + */ + lazy val `∞`: Number = ExactNumber(Rational.infinity) + /** * Exact value of the Number √2 (not Complex) */ diff --git a/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/AtomicExpression.scala b/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/AtomicExpression.scala index 4f433c5bd..185fcbcef 100644 --- a/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/AtomicExpression.scala +++ b/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/AtomicExpression.scala @@ -5,7 +5,7 @@ package com.phasmidsoftware.number.expression.expr import com.phasmidsoftware.number.algebra.core.* -import com.phasmidsoftware.number.algebra.eager.{Complex, Eager, Real} +import com.phasmidsoftware.number.algebra.eager.{Complex, Eager, InversePower, Real} import com.phasmidsoftware.number.core.inner.Factor import com.phasmidsoftware.number.expression.expr.Expression.em @@ -148,6 +148,16 @@ case class Noop(w: String) extends AtomicExpression { def evaluate(context: Context): Option[Eager] = throw new UnsupportedOperationException(s"Can''t evaluate: $this") + /** + * Represents a lazy evaluation result of this `Noop` expression as-is, without any modification or processing. + * + * This method is overridden and fixed to always return `None`, indicating that + * the `Noop` expression does not inherently evaluate to any meaningful result. + * + * @return an `Option[Eager]`, which is always `None`. + */ + override lazy val evaluateAsIs: Option[Eager] = None + /** * Method to render this Renderable in a presentable manner. * @@ -207,5 +217,6 @@ case class Noop(w: String) extends AtomicExpression { } object Noop { + def apply: Noop = Noop("NaN") val TEST_STRING = "Error tester" } diff --git a/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/BiFunction.scala b/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/BiFunction.scala index 948fa1c20..ebf27b3ed 100644 --- a/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/BiFunction.scala +++ b/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/BiFunction.scala @@ -8,7 +8,7 @@ import cats.implicits.catsSyntaxEq import com.phasmidsoftware.number.algebra.core.* import com.phasmidsoftware.number.algebra.eager import com.phasmidsoftware.number.algebra.eager.Eager.eagerToField -import com.phasmidsoftware.number.algebra.eager.{Angle, Complex, Eager, InversePower, IsInteger, NaturalExponential, QuadraticSolution, RationalNumber, Structure, WholeNumber} +import com.phasmidsoftware.number.algebra.eager.{Angle, Complex, Eager, InversePower, IsInteger, QuadraticSolution, RationalNumber, Structure, WholeNumber} import com.phasmidsoftware.number.core.inner.{Factor, PureNumber, Radian, Rational} import com.phasmidsoftware.number.core.numerical import com.phasmidsoftware.number.core.numerical.{ComplexCartesian, ComplexPolar, Number, Real, Complex as CoreComplex} @@ -109,8 +109,17 @@ case class BiFunction(a: Expression, b: Expression, f: ExpressionBiFunction) ext */ lazy val structuralMatcher: em.AutoMatcher[Expression] = em.Matcher[Expression, Expression]("BiFunction:structuralMatcher") { // NOTE: Canonical ordering — swap commutative operands if out of order. + case BiFunction(Noop(_), _, _) | BiFunction(_, Noop(_), _) => + em.Match(Noop.apply) + case BiFunction(Infinity, UniFunction(Infinity, Negate), Sum) | BiFunction(UniFunction(Infinity, Negate), Infinity, Sum) => + em.Match(Noop.apply) case IsCommutative(x, y, f) if summon[Ordering[Expression]].gt(x, y) => - em.Match(BiFunction(y, x, f)) + em.Match(BiFunction(y, x, f)).flatMap(structuralMatcher) + case BiFunction(Zero, Infinity, Product) | BiFunction(Infinity, Zero, Product) => + em.Match(Noop.apply) + // NOTE: any other BiFunction involving Infinity results in Infinity + case BiFunction(Infinity, _, _) | BiFunction(_, Infinity, _) => + em.Match(Infinity) case BiFunction(a, b, Sum) if a == b => em.Match(a * Two) case BiFunction(BiFunction(w, x, Power), BiFunction(y, z, Power), Product) if w == y => @@ -435,7 +444,7 @@ case class BiFunction(a: Expression, b: Expression, f: ExpressionBiFunction) ext case (E, ValueExpression(Complex(c), _)) if c.isImaginary && c.modulus == numerical.Number.pi => em.Match(MinusOne) case (E, ValueExpression(v: eager.Number, _)) => - em.Match(Literal(NaturalExponential(v))) + em.Match(UniFunction(v, Exp)) case (E, UniFunction(x, Ln)) => em.Match(x) case (E, BiFunction(x, E, Log)) => diff --git a/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/Expression.scala b/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/Expression.scala index 2ccd65b1b..523660069 100644 --- a/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/Expression.scala +++ b/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/Expression.scala @@ -101,10 +101,10 @@ trait Expression extends Lazy with Approximate { /** * Evaluates this `Expression` in the context of `AnyContext` without simplification or factor-based conversion. - * This allows obtaining a direct evaluation of the `Expression` as a `Field`, if possible. + * This allows obtaining a direct evaluation of the `Expression` as a `Eager`, if possible. * NOTE: no simplification or factor-based conversion occurs here. * - * @return an `Option[Field]` containing the evaluated `Field` if evaluation is successful, or `None` otherwise. + * @return either a `Some[Eager]` containing the value if evaluation is successful, or `None` otherwise. */ lazy val evaluateAsIs: Option[Eager] = evaluate(AnyContext) @@ -400,7 +400,7 @@ object Expression { * @return an Expression representing the lazy product of this expression and the given expression. */ infix def times(y: Expression): Expression = y match { - case IsZero(_) => + case IsZero(_) if x != Infinity => Zero case IsUnity(_) => x @@ -627,7 +627,7 @@ object Expression { case Eager.infinity => Infinity case _ => - Literal(x) + Literal(x) // CONSIDER invoking ValueExpression instead } @deprecated("Use puremath or lazymath string interpolators instead", "1.6.5") @@ -918,11 +918,16 @@ object Expression { */ given Ordering[Expression] with def compare(x: Expression, y: Expression): Int = (x, y) match - case (a: AtomicExpression, b: AtomicExpression) => atomRank(a) - atomRank(b) - case (_: AtomicExpression, _: CompositeExpression) => -1 - case (_: CompositeExpression, _: AtomicExpression) => 1 - case (a: CompositeExpression, b: CompositeExpression) => a.depth - b.depth - case (a, b) => a.compare(b) // This case should never occur + case (a: AtomicExpression, b: AtomicExpression) => + atomRank(a) - atomRank(b) + case (_: AtomicExpression, _: CompositeExpression) => + -1 + case (_: CompositeExpression, _: AtomicExpression) => + 1 + case (a: CompositeExpression, b: CompositeExpression) => + a.depth - b.depth + case (a, b) => + a.compare(b) // This case should never occur private def atomRank(e: AtomicExpression): Int = e match case Noop(_) => 0 diff --git a/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/UniFunction.scala b/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/UniFunction.scala index 56aa213e2..18f3a84f2 100644 --- a/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/UniFunction.scala +++ b/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/UniFunction.scala @@ -144,7 +144,13 @@ case class UniFunction(x: Expression, f: ExpressionMonoFunction) extends Composi */ lazy val identitiesMatcher: em.AutoMatcher[Expression] = em.Matcher("UniFunction:identitiesMatcher") { + // Noop special case + case UniFunction(Noop(_), _) => + em.Match(Noop.apply) + // Zero special cases + case UniFunction(Zero, Exp) => + em.Match(One) case UniFunction(Zero, f@Odd()) if f != Reciprocal => // NOTE if the function has odd parity, then f(0) = 0. em.Match(Zero) case UniFunction(Zero, Cosh | Cosine) if f != Reciprocal => @@ -165,6 +171,8 @@ case class UniFunction(x: Expression, f: ExpressionMonoFunction) extends Composi em.Match(MinusOne) // Match Log and Exp cases + case UniFunction(Zero, Ln) => + em.Match(UniFunction(Infinity, Negate)) case UniFunction(IsEuler(Euler(r, θ)), Ln) => em.Match((UniFunction(r, Ln) + (I * θ)).simplify) // XXX Take care of the cases whereby the inverse of a log expression is a log expression with operand and base swapped. diff --git a/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/ValueExpression.scala b/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/ValueExpression.scala index 48614c789..9a64ccd59 100644 --- a/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/ValueExpression.scala +++ b/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/ValueExpression.scala @@ -224,6 +224,8 @@ object ValueExpression { /** * Creates a `ValueExpression` instance by wrapping the provided `Valuable` object. + * NOTE: client code should not normally invoke this method directly. + * Prefer to use the `Expression.apply` method for creating `ValueExpression` instances. * * @param v the `Eager` instance to be encapsulated within a `ValueExpression`. * @return a `ValueExpression` representing the provided `Eager` object, with its rendered representation. @@ -409,7 +411,7 @@ object Literal { One case numerical.Number.zero => Zero - case numerical.Number.pi => + case numerical.Number.pi | numerical.Number.`𝛑` => Pi case numerical.Number.two => Two @@ -419,7 +421,10 @@ object Literal { Half case numerical.Number.e => E + case numerical.Number.`∞` => + Infinity case _ => + // XXX take care that we do not recurse infinitely here. ValueExpression(Scalar(x)) } diff --git a/expression/src/test/scala/com/phasmidsoftware/number/expression/expr/CompositeExpressionSpec.scala b/expression/src/test/scala/com/phasmidsoftware/number/expression/expr/CompositeExpressionSpec.scala index 9ebaf60aa..f99feb7c6 100644 --- a/expression/src/test/scala/com/phasmidsoftware/number/expression/expr/CompositeExpressionSpec.scala +++ b/expression/src/test/scala/com/phasmidsoftware/number/expression/expr/CompositeExpressionSpec.scala @@ -320,8 +320,8 @@ class CompositeExpressionSpec extends AnyFlatSpec with Matchers { val simplified = doubleSum.simplify simplified shouldBe a[BiFunction] simplified.asInstanceOf[BiFunction].f shouldBe Product - simplified.asInstanceOf[BiFunction].a shouldBe Two - simplified.asInstanceOf[BiFunction].b shouldBe x + simplified.asInstanceOf[BiFunction].a shouldBe x + simplified.asInstanceOf[BiFunction].b shouldBe Two } it should "evaluate (named) constant sum completely" in { diff --git a/expression/src/test/scala/com/phasmidsoftware/number/expression/expr/ExpressionMatcherPatterns.scala b/expression/src/test/scala/com/phasmidsoftware/number/expression/expr/ExpressionMatcherPatterns.scala new file mode 100644 index 000000000..dff1426be --- /dev/null +++ b/expression/src/test/scala/com/phasmidsoftware/number/expression/expr/ExpressionMatcherPatterns.scala @@ -0,0 +1,14 @@ +// src/test/scala/com/phasmidsoftware/number/expression/ExpressionMatchers.scala +package com.phasmidsoftware.number.expression.expr + +import org.scalatest.matchers.{MatchResult, Matcher, should} + +trait ExpressionMatcherPatterns { + self: should.Matchers => + def simplifyAs(expected: Expression): Matcher[Expression] = + (left: Expression) => MatchResult( + left.simplify == expected, + s"${left.render} did not simplify to ${expected.render}, got ${left.simplify.render}", + s"${left.render} simplified to ${expected.render}" + ) +} \ No newline at end of file diff --git a/expression/src/test/scala/com/phasmidsoftware/number/expression/expr/ExpressionMatchersSpec.scala b/expression/src/test/scala/com/phasmidsoftware/number/expression/expr/ExpressionMatchersSpec.scala index 74b826465..6a50f350e 100644 --- a/expression/src/test/scala/com/phasmidsoftware/number/expression/expr/ExpressionMatchersSpec.scala +++ b/expression/src/test/scala/com/phasmidsoftware/number/expression/expr/ExpressionMatchersSpec.scala @@ -513,7 +513,7 @@ class ExpressionMatchersSpec extends AnyFlatSpec with should.Matchers with Befor } it should "work for Ln Zero" in { val x = expr.UniFunction(Zero, Ln) - x.simplify shouldBe Expression(infinity.negate) + x.simplify shouldBe UniFunction(Infinity, Negate) } it should "work for Ln One" in { val x = expr.UniFunction(One, Ln) diff --git a/expression/src/test/scala/com/phasmidsoftware/number/expression/expr/ExpressionSpec.scala b/expression/src/test/scala/com/phasmidsoftware/number/expression/expr/ExpressionSpec.scala index 2094a1606..2efa9c7a9 100644 --- a/expression/src/test/scala/com/phasmidsoftware/number/expression/expr/ExpressionSpec.scala +++ b/expression/src/test/scala/com/phasmidsoftware/number/expression/expr/ExpressionSpec.scala @@ -12,9 +12,10 @@ import com.phasmidsoftware.number.algebra.core.FuzzyEq.~= import com.phasmidsoftware.number.algebra.eager.* import com.phasmidsoftware.number.algebra.eager.Eager.eagerToField import com.phasmidsoftware.number.algebra.eager.RationalNumber.half -import com.phasmidsoftware.number.core.inner.{NatLog, Rational} +import com.phasmidsoftware.number.algebra.eager.Real.∞ +import com.phasmidsoftware.number.core.inner.Rational import com.phasmidsoftware.number.core.numerical -import com.phasmidsoftware.number.core.numerical.{ComplexCartesian, ComplexPolar, ExactNumber} +import com.phasmidsoftware.number.core.numerical.{ComplexCartesian, ComplexPolar} import com.phasmidsoftware.number.expression.algebraic.QuadraticEquation import com.phasmidsoftware.number.expression.core.FuzzyEquality import com.phasmidsoftware.number.expression.expr @@ -26,7 +27,7 @@ import org.scalatest.BeforeAndAfter import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should -class ExpressionSpec extends AnyFlatSpec with should.Matchers with BeforeAndAfter with FuzzyEquality { +class ExpressionSpec extends AnyFlatSpec with should.Matchers with BeforeAndAfter with FuzzyEquality with ExpressionMatcherPatterns { type OldExpression = com.phasmidsoftware.number.expression.expr.Expression type OldNumber = com.phasmidsoftware.number.core.numerical.Number @@ -89,6 +90,26 @@ class ExpressionSpec extends AnyFlatSpec with should.Matchers with BeforeAndAfte } } + behavior of "infinity" + + def simplifiesTo(e: Expression, expected: Expression): org.scalatest.Assertion = + e.simplify shouldBe expected + + it should "obey the Infinity rules" in { + Expression(∞) shouldBe Infinity + simplifiesTo(-Infinity + Infinity, Noop.apply) + simplifiesTo(Infinity + -Infinity, Noop.apply) + simplifiesTo(Zero * Infinity, Noop.apply) + simplifiesTo(Infinity * Zero, Noop.apply) + } + + it should "obey the Noop rules" in { + simplifiesTo(Noop.apply + Noop.apply, Noop.apply) + simplifiesTo(-Noop.apply + Noop.apply, Noop.apply) + simplifiesTo(Noop.apply + Noop.apply, Noop.apply) + simplifiesTo(Noop.apply + Noop.apply, Noop.apply) + } + behavior of "parse" // private val syp: ShuntingYardParser.type = ShuntingYardParser @@ -460,11 +481,11 @@ class ExpressionSpec extends AnyFlatSpec with should.Matchers with BeforeAndAfte } it should "aggregate 2" in { val target = (One * Pi * Two * MinusOne).simplify - target shouldBe -(Pi * 2) + target shouldBe ∅ * -2 * Pi } it should "evaluate e * e" in { val expression: Expression = E * E - expression.simplify shouldBe Literal(Eager(numerical.Real(ExactNumber(2, NatLog)))) + expression.simplify shouldBe UniFunction(2, Exp) } // TODO Issue #140 it should "evaluate phi * phi" in { @@ -511,7 +532,7 @@ class ExpressionSpec extends AnyFlatSpec with should.Matchers with BeforeAndAfte val y: em.MatchResult[Expression] = x.structuralMatcher(x) y shouldBe em.Match(BiFunction(E, ValueExpression(2), Power)) val simplified = y.get.simplify - simplified shouldBe Literal(Eager(numerical.Real(ExactNumber(2, NatLog)))) + simplified shouldBe UniFunction(2, Exp) } it should "evaluate phi * phi" in { val phi = expr.Root(QuadraticEquation.goldenRatioEquation, 0) diff --git a/parse/src/test/scala/com/phasmidsoftware/number/parse/ExpressionParserSpec.scala b/parse/src/test/scala/com/phasmidsoftware/number/parse/ExpressionParserSpec.scala index a2ff62919..fa1287575 100644 --- a/parse/src/test/scala/com/phasmidsoftware/number/parse/ExpressionParserSpec.scala +++ b/parse/src/test/scala/com/phasmidsoftware/number/parse/ExpressionParserSpec.scala @@ -66,7 +66,7 @@ class ExpressionParserSpec extends AnyFlatSpec with should.Matchers { it should "lazymath e" in { lazymath"""\e""" shouldBe E lazymath"""\mathrm{e}""" shouldBe E - lazymath"""\e^2""" shouldBe Literal(NaturalExponential(WholeNumber(2)), Some("e^2")) + lazymath"""\e^2""" shouldBe UniFunction(2, Exp) } it should "puremath functions" in { puremath"""\sin(\pi)""" shouldBe UniFunction(Pi, Sine) diff --git a/top/src/test/scala/com/phasmidsoftware/number/top/TopSpec.scala b/top/src/test/scala/com/phasmidsoftware/number/top/TopSpec.scala index fdb424293..aeaa9d5d7 100644 --- a/top/src/test/scala/com/phasmidsoftware/number/top/TopSpec.scala +++ b/top/src/test/scala/com/phasmidsoftware/number/top/TopSpec.scala @@ -13,8 +13,8 @@ import com.phasmidsoftware.number.algebra.eager.Angle.𝛑 import com.phasmidsoftware.number.algebra.eager.Eager.{e, half, minusOne, negInfinity, one, pi, two, zero} import com.phasmidsoftware.number.core import com.phasmidsoftware.number.core.inner.Rational -import com.phasmidsoftware.number.core.inner.Rational.infinity import com.phasmidsoftware.number.core.numerical +import com.phasmidsoftware.number.core.numerical.{Box, RelativeFuzz} import com.phasmidsoftware.number.expression.expr import com.phasmidsoftware.number.expression.expr.{Expression, *} import org.scalactic.Equality @@ -245,6 +245,17 @@ class TopSpec extends AnyFlatSpec with should.Matchers with BeforeAndAfter { lazymath"\ln{\e}" shouldBe One math"\ln{\e}" shouldBe one } + it should "work for Ln e^x" in { + lazymath"\ln{\e^2}" shouldBe Two + lazymath"\e^{\ln{2}}" shouldBe Two + } + it should "work for e^{e^e-1}" in { + lazymath"\e^{(-1 + \e^\e)}" shouldBe UniFunction(BiFunction(-1, UniFunction(E, Exp), Sum), Exp) + lazymath"\e^{(\e^\e + -1)}" shouldBe UniFunction(BiFunction(UniFunction(E, Exp), -1, Sum), Exp) + math"\e^{(-1 + \e^\e)}" shouldBe Real(1403194.8655310909, Some(RelativeFuzz(1.0715681594829235E-14, Box))) + math"\e^{(\e^\e + -1)}" shouldBe Real(1403194.8655310909, Some(RelativeFuzz(1.0715681594829235E-14, Box))) + } + it should "work for Sine 0, etc." in { lazymath"\sin(0)" shouldBe Zero math"\sin(0)" shouldBe zero From bfbf83e12680d5fbc2dc642e610c905f729b5ede Mon Sep 17 00:00:00 2001 From: Robin Hillyard Date: Thu, 30 Apr 2026 19:15:38 -0400 Subject: [PATCH 2/4] Version 1.10.5: added guards for infinity to Field operations Otherwise, mostly cosmetic changes due to renaming isInfinity to isInfinite. --- build.sbt | 3 ++ .../number/core/inner/Operations.scala | 4 +- .../number/core/inner/Rational.scala | 16 +++---- .../number/core/numerical/BaseComplex.scala | 2 +- .../number/core/numerical/Constants.scala | 10 +++++ .../number/core/numerical/Number.scala | 6 +++ .../number/core/numerical/Real.scala | 4 ++ .../number/core/inner/RationalSpec.scala | 12 ++--- .../number/core/numerical/FieldSpec.scala | 45 ++++++++++++++----- .../number/expression/expr/Aggregate.scala | 2 +- .../expression/expr/ExpressionMatchers.scala | 2 +- .../com/phasmidsoftware/number/core/Random.sc | 0 12 files changed, 77 insertions(+), 29 deletions(-) delete mode 100644 src/main/scala/com/phasmidsoftware/number/core/Random.sc diff --git a/build.sbt b/build.sbt index c81086b3d..16375de38 100644 --- a/build.sbt +++ b/build.sbt @@ -21,6 +21,9 @@ ThisBuild / libraryDependencies ++= Seq( "ch.qos.logback" % "logback-classic" % logbackClassicVersion % "runtime" ) +// CONSIDER removing this (for performance reasons) if publishLocal runs fine without it. +Global / concurrentRestrictions += Tags.limitAll(1) + // ============================================================================ // COMPILER OPTIONS - Moderate settings focused on catching + operator issues // ============================================================================ diff --git a/core/src/main/scala/com/phasmidsoftware/number/core/inner/Operations.scala b/core/src/main/scala/com/phasmidsoftware/number/core/inner/Operations.scala index 4cf80c8c8..cfd2764ee 100644 --- a/core/src/main/scala/com/phasmidsoftware/number/core/inner/Operations.scala +++ b/core/src/main/scala/com/phasmidsoftware/number/core/inner/Operations.scala @@ -198,7 +198,7 @@ case object MonadicOperationExp extends MonadicOperation { * This function operates within a `Try` monad to handle potential failures. */ private lazy val expRat: Rational => Try[Rational] = { - case r if r.isInfinity && r.signum < 0 => + case r if r.isInfinite && r.signum < 0 => Success(Rational.zero) case r => fail("can't do exp Rational=>Rational for non-zero parameter")(r) @@ -845,7 +845,7 @@ case object QueryOperationIsInfinite extends QueryOperation[Boolean] { */ def getFunctions: BooleanQueryFunctions = new QueryFunctions[Boolean] { val fInt: Int => Try[Boolean] = tryF[Int, Boolean](_ => false) - val fRat: Rational => Try[Boolean] = tryF[Rational, Boolean](x => x.isInfinity) + val fRat: Rational => Try[Boolean] = tryF[Rational, Boolean](x => x.isInfinite) val fDouble: Double => Try[Boolean] = tryF[Double, Boolean](x => x == Double.PositiveInfinity || x == Double.NegativeInfinity) } } diff --git a/core/src/main/scala/com/phasmidsoftware/number/core/inner/Rational.scala b/core/src/main/scala/com/phasmidsoftware/number/core/inner/Rational.scala index 0ab9374ab..6f97a65a1 100644 --- a/core/src/main/scala/com/phasmidsoftware/number/core/inner/Rational.scala +++ b/core/src/main/scala/com/phasmidsoftware/number/core/inner/Rational.scala @@ -265,7 +265,7 @@ case class Rational private[inner](n: BigInt, d: BigInt) extends NumberLike { * @return true if this NumberLike object is exact in the context of No factor, else false. */ lazy val isExact: Boolean = - !(isInfinity || isNaN) + !(isInfinite || isNaN) /** * Determines if the Rational object represents "not a number" (NaN). @@ -421,17 +421,17 @@ case class Rational private[inner](n: BigInt, d: BigInt) extends NumberLike { * @note Throws RationalException if the Rational number is infinity. */ lazy val forceToBigDecimal: BigDecimal = - if (!isInfinity) + if (!isInfinite) BigDecimal(n) / BigDecimal(d) else throw RationalException(s"cannot convert infinity to BigDecimal: $this") /** - * Method to determine if the given Rational represents an infinity. + * Method to determine if the given Rational represents an infinity (positive or negative). * * @return true if the denominator is zero; false otherwise. */ - lazy val isInfinity: Boolean = + lazy val isInfinite: Boolean = d == bigZero && n != bigZero /** @@ -531,7 +531,7 @@ case class Rational private[inner](n: BigInt, d: BigInt) extends NumberLike { * @return the mediant of this and other. */ def mediant(other: Rational): Rational = - if (signum >= 0 && other.signum >= 0 && !isInfinity && !other.isInfinity) + if (signum >= 0 && other.signum >= 0 && !isInfinite && !other.isInfinite) Rational(n + other.n, d + other.d) else NaN @@ -615,7 +615,7 @@ case class Rational private[inner](n: BigInt, d: BigInt) extends NumberLike { "NaN" case _ if isZero && d < 0 => "-0" - case _ if isInfinity => + case _ if isInfinite => (if (n > 0) "" else "-") + "∞" case _ if isWhole => toBigInt.toString @@ -1329,7 +1329,7 @@ object Rational { if (x.isWhole) Success(x.n) else - Failure(RationalException(s"toBigInt: $x is " + (if (x.isInfinity) + Failure(RationalException(s"toBigInt: $x is " + (if (x.isInfinite) "infinite" else "not whole"))) /** @@ -1356,7 +1356,7 @@ object Rational { */// CONSIDER making this private or moving back into RationalSpec def hasCorrectRatio(r: Rational, top: BigInt, bottom: BigInt): Boolean = { val _a = r * bottom - val result = bottom == 0 || _a.isInfinity || (_a.isWhole && _a.toBigInt == top) + val result = bottom == 0 || _a.isInfinite || (_a.isWhole && _a.toBigInt == top) if (!result) throw RationalException(s"incorrect ratio: r=${r.n}/${r.d}, top=$top, bottom=$bottom, _a=${_a}, gcd=${top.gcd(bottom)}") result diff --git a/core/src/main/scala/com/phasmidsoftware/number/core/numerical/BaseComplex.scala b/core/src/main/scala/com/phasmidsoftware/number/core/numerical/BaseComplex.scala index f69c6a501..de1832eb8 100644 --- a/core/src/main/scala/com/phasmidsoftware/number/core/numerical/BaseComplex.scala +++ b/core/src/main/scala/com/phasmidsoftware/number/core/numerical/BaseComplex.scala @@ -92,7 +92,7 @@ abstract class BaseComplex(val real: Number, val imag: Number) extends Complex { * @return the sum. */ def add(x: Field): Field = - sum(narrow(x, polar = false)) + sum(narrow(x, polar = false)) // CONSIDER dealing with infinity. /** * Method to add this to the given parameter (a Cartesian). diff --git a/core/src/main/scala/com/phasmidsoftware/number/core/numerical/Constants.scala b/core/src/main/scala/com/phasmidsoftware/number/core/numerical/Constants.scala index 81515fc82..6b35eb719 100644 --- a/core/src/main/scala/com/phasmidsoftware/number/core/numerical/Constants.scala +++ b/core/src/main/scala/com/phasmidsoftware/number/core/numerical/Constants.scala @@ -148,6 +148,16 @@ object Constants { */ lazy val iPi: Complex = ComplexCartesian(0, Number.pi) + /** + * Represents a constant Field value of "Not a Number" (NaN). + * NaN is used to signify an undefined or unrepresentable value, especially + * in numerical computations that result in invalid operations like division by zero + * or operations involving undefined values. + * + * This value is of type Real and wraps the predefined NaN value from the Number type. + */ + lazy val NaN: Field = Real(Number.NaN) + /** * Represents the square root of 2 as a field constant. * CONSIDER making this a Complex. diff --git a/core/src/main/scala/com/phasmidsoftware/number/core/numerical/Number.scala b/core/src/main/scala/com/phasmidsoftware/number/core/numerical/Number.scala index 0b6941a65..e81bc4002 100644 --- a/core/src/main/scala/com/phasmidsoftware/number/core/numerical/Number.scala +++ b/core/src/main/scala/com/phasmidsoftware/number/core/numerical/Number.scala @@ -299,6 +299,12 @@ trait Number extends Fuzz[Double] with Ordered[Number] with Numerical { * @return the result of the multiplication as a Field */ def multiply(x: Field): Field = (this, x) match { + case (Constants.zero, _) if x.isInfinite => + Constants.NaN + case (_, Constants.zero) if this.isInfinite => + Constants.NaN + case (a, b) if a.isInfinite || b.isInfinite => + Constants.infinity case (Number.zero, _) | (_, Constants.zero) => Constants.zero case (Number.one, _) => diff --git a/core/src/main/scala/com/phasmidsoftware/number/core/numerical/Real.scala b/core/src/main/scala/com/phasmidsoftware/number/core/numerical/Real.scala index 02de07948..8e0d94fc8 100644 --- a/core/src/main/scala/com/phasmidsoftware/number/core/numerical/Real.scala +++ b/core/src/main/scala/com/phasmidsoftware/number/core/numerical/Real.scala @@ -114,6 +114,10 @@ case class Real(x: Number) extends Field { * @return the sum. */ def add(y: Field): Field = y match { + case field if this.isInfinite && field.isInfinite && signum != field.signum => + Constants.NaN + case field if this.isInfinite || field.isInfinite => + Constants.infinity case multivariate: Multivariate => multivariate.add(this) case Real(r) => diff --git a/core/src/test/scala/com/phasmidsoftware/number/core/inner/RationalSpec.scala b/core/src/test/scala/com/phasmidsoftware/number/core/inner/RationalSpec.scala index 6e5c98ce1..48cc06112 100644 --- a/core/src/test/scala/com/phasmidsoftware/number/core/inner/RationalSpec.scala +++ b/core/src/test/scala/com/phasmidsoftware/number/core/inner/RationalSpec.scala @@ -20,7 +20,7 @@ class RationalSpec extends flatspec.AnyFlatSpec with should.Matchers with Privat private val zeroSymbol = Symbol("zero") private val toBigIntSymbol = Symbol("toBigInt") private val wholeSymbol = Symbol("whole") - private val infinitySymbol = Symbol("infinity") + private val infinitySymbol = Symbol("infinite") private val unitySymbol = Symbol("unity") private val narrowSymbol = Symbol("narrow") private val normalizeSymbol = Symbol("normalize") @@ -34,7 +34,7 @@ class RationalSpec extends flatspec.AnyFlatSpec with should.Matchers with Privat } it should "yield infinity for 1, 0" in { val r = new Rational(BigInt(1), 0L) - r.isInfinity shouldBe true + r.isInfinite shouldBe true } it should "yield NaN for 0, 0" in { new Rational(BigInt(0), 0L).isNaN shouldBe true @@ -101,18 +101,18 @@ class RationalSpec extends flatspec.AnyFlatSpec with should.Matchers with Privat } it should "work for 1, 0" in { val r = Rational(1, 0) - r.isInfinity shouldBe true + r.isInfinite shouldBe true r shouldBe Rational.infinity r.render shouldBe "∞" } it should "work for -1, 0" in { val r = Rational(-1, 0) - r.isInfinity shouldBe true + r.isInfinite shouldBe true r.render shouldBe "-∞" } it should "work for -1, -2" in { val r = Rational(-1, -2) - r.isInfinity shouldBe false + r.isInfinite shouldBe false r shouldBe Rational.half } it should "work for -2624712818L, -1" in { @@ -829,7 +829,7 @@ class RationalSpec extends flatspec.AnyFlatSpec with should.Matchers with Privat behavior of "r-interpolator" it should "work for -1/0" in { val r = r"-1/0" - r.isInfinity shouldBe true + r.isInfinite shouldBe true r shouldBe Rational(-1, 0) } it should "work for 1/-2147483648" in { diff --git a/core/src/test/scala/com/phasmidsoftware/number/core/numerical/FieldSpec.scala b/core/src/test/scala/com/phasmidsoftware/number/core/numerical/FieldSpec.scala index 817fd4505..eac3fad8a 100644 --- a/core/src/test/scala/com/phasmidsoftware/number/core/numerical/FieldSpec.scala +++ b/core/src/test/scala/com/phasmidsoftware/number/core/numerical/FieldSpec.scala @@ -1,21 +1,21 @@ package com.phasmidsoftware.number.core.numerical -import com.phasmidsoftware.number.core.inner.PureNumber +import com.phasmidsoftware.number.core.inner.{PureNumber, Rational} +import com.phasmidsoftware.number.core.numerical.Real.RealIsFractional.mkNumericOps import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should class FieldSpec extends AnyFlatSpec with should.Matchers { behavior of "Field" -// it should "isExact" in { -// val x = (ConstPi / 2).materialize -// x.isExact shouldBe true -// } -// it should "multiply i by itself correctly" in { -// val z = Expression(Constants.i) * Constants.i -// val result = z.materialize -// result shouldBe Constants.minusOne -// } + it should "isExact" in { + val x = (Constants.pi / Real(2)) + x.isExact shouldBe true + } + it should "multiply i by itself correctly" in { + val z = Constants.i * Constants.i + z shouldBe ComplexCartesian(-1, 0) + } it should "add" in { val one = Number.one val result = one `add` Constants.i @@ -31,4 +31,29 @@ class FieldSpec extends AnyFlatSpec with should.Matchers { x should matchPattern { case Real(FuzzyNumber(Left(Left(Some(0.6931471805599453))), PureNumber, _)) => } x.render shouldBe "0.69314718055994530[89]" } + it should "add infinity to infinity" in { + val result = Constants.infinity `add` Constants.infinity + result shouldBe Constants.infinity + } + it should "add infinity to negative infinity" in { + val result = Constants.infinity `add` Real(Rational(-1).invert) + result shouldBe Constants.infinity + } + it should "add infinity to a finite number" in { + val result = Constants.infinity `add` Real(1) + result shouldBe Constants.infinity + } + it should "multiply infinity by zero" in { + val result = Constants.infinity `multiply` Constants.infinity + result shouldBe Constants.infinity + } + it should "multiply infinity by a finite number" in { + val result = Constants.infinity `multiply` Real(1) + result shouldBe Constants.infinity + } + it should "divide infinity by infinity" in { + val result = Constants.infinity `divide` Constants.infinity + result shouldBe Constants.NaN + } + } diff --git a/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/Aggregate.scala b/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/Aggregate.scala index 5cb410e97..677dba86e 100644 --- a/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/Aggregate.scala +++ b/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/Aggregate.scala @@ -129,7 +129,7 @@ case class Aggregate(function: ExpressionBiFunction, xs: Seq[Expression]) extend case Product => x => if x < 1 then 1 / x else x case _ => - throw new IllegalArgumentException("complementaryTermsEliminatorAggregate: Power function not supported") + throw new IllegalArgumentException("eliminateComplementaryTerms: Power function not supported") } // NOTE this ordering is really only appropriate when f is Sum. diff --git a/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/ExpressionMatchers.scala b/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/ExpressionMatchers.scala index a6134cb9d..b14ed95bb 100644 --- a/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/ExpressionMatchers.scala +++ b/expression/src/main/scala/com/phasmidsoftware/number/expression/expr/ExpressionMatchers.scala @@ -434,7 +434,7 @@ class ExpressionMatchers(using val matchLogger: MatchLogger) extends MatchersExt // CONSIDER the following: // Match(Aggregate(f, list)) `map` (_.simplify) else - Miss(s"complementaryTermsEliminatorAggregate: $a", a) + Miss(s"matchOrMiss: $a", a) /** * Matches an expression to its additive identity based on its components. diff --git a/src/main/scala/com/phasmidsoftware/number/core/Random.sc b/src/main/scala/com/phasmidsoftware/number/core/Random.sc deleted file mode 100644 index e69de29bb..000000000 From e0f546c8439d7b222ba95eed180835676ba9bcb8 Mon Sep 17 00:00:00 2001 From: Robin Hillyard Date: Thu, 30 Apr 2026 19:28:00 -0400 Subject: [PATCH 3/4] Number 1.10.5 / NumberApps 1.0.8: EML-driven robustness improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Number changes: - Fix exp(-∞) → 0 (1.10.3) - Fix UniFunction(Zero, Exp) → One - Fix ln(exp(x)) → x simplification - Fix commutativity flatMap in BiFunction.structuralMatcher - Add Expression-level guards: ∞ + (-∞), 0 × ∞ → Noop("NaN") - Add core-level guards for indeterminate forms involving ∞ - Fix publishLocal race condition via concurrentRestrictions - Add ExpressionMatcherPatterns test trait with simplifiesTo helper - Add Infinity rules tests --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 16375de38..b79acf025 100644 --- a/build.sbt +++ b/build.sbt @@ -2,7 +2,7 @@ ThisBuild / organization := "com.phasmidsoftware" -ThisBuild / version := "1.10.5-SNAPSHOT" +ThisBuild / version := "1.10.5" val scalaVersionNumber = "3.7.3" val catsVersion = "2.13.0" From e181f4020060d5cd4fd7a9e29718064c22dfbdec Mon Sep 17 00:00:00 2001 From: Robin Hillyard Date: Thu, 30 Apr 2026 19:38:46 -0400 Subject: [PATCH 4/4] Update to previous commit (V1.10.5) Documentation changes --- README.md | 4 ++-- docs/HISTORY.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d6b02b4b0..ef9fb4681 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Number is organized into multiple modules: #### Current Version This version is 1.10.1 -The latest published version is 1.9.3 (see [HISTORY](docs/HISTORY.md)). +The latest published version is 1.10.5 (see [HISTORY](docs/HISTORY.md)). That should match the version of the latest release on Maven Central in the badge at the top. To use Number in your project, add the following dependency: @@ -59,7 +59,7 @@ libraryDependencies += "com.phasmidsoftware" %% "number" % "1.9.3" com.phasmidsoftware number_3 - 1.9.3 + 1.10.5 ``` diff --git a/docs/HISTORY.md b/docs/HISTORY.md index a3ccdf807..3e73916e3 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,7 +1,7 @@ # Migration and Version History ## Versions - +* Version 1.10.5: Fixed various issues (mostly regarding infinity) exposed by the EML expansion. * Version 1.10.1: Fixed issue with over-precise Rational numbers. * Version 1.10.0: Improved the Mermaid diagrams, see [docs/diagrams/index.md](docs/diagrams/index.md). * Version 1.9.3: