Complete NotANumber - #86
Draft
PatrickHaecker wants to merge 14 commits into
Draft
Conversation
added 10 commits
September 3, 2026 17:13
`Base` promotes every `Real` to `BigFloat`, so `Base._promote` handed the arithmetic two `BigFloat`s and `__add`, `__mul` and `_infpow` no longer matched: `big(2.0) + ∞`, `big(2.0) * ∞` and `(+∞)^big(2.0)` were all `MethodError`s. `BigInt` was unaffected, the `Integer` rule catching it first, and so were `ℵ₀` and `ComplexInfinity`, neither of which promotes to a float.
`NaN + ∞` gave `∞`, `NaN * ∞` gave `+∞`, `div(NaN, ∞)` gave `0.0` and `(+∞)^NaN` gave `0.0`, where the same expressions over the floats all give `NaN`. An argument that was not an infinity was treated as negligible, so a `NaN` marking a failed computation silently became a plausible infinity. Each entry point now returns its `NaN` argument unchanged, which keeps the precision as well: `NaN32 * ∞ === NaN32`. `isnan` answers for every `Number` and folds to `false` for the types that carry no `NaN`, so nothing changes for them. A float argument widens the inferred return type by one union member and still allocates nothing.
For a float parameter it returned the field itself, so `signbit(ComplexInfinity(0.5))` was `0.5` and any caller branching on it hit a `TypeError`. It now answers whether the infinity points along the negative real axis, for every parameter type, which is what the `Bool` and `Integer` methods already did and what `Base` guarantees. One method replaces the three, as `mod(signbit, 2) == 1` covers them all. The two places that wanted the whole angle rather than its sign take the field directly.
The three bugs found so far were all the same shape: an infinity behaving differently from `Inf` in a case nobody had enumerated. The table asks each comparison and each of `max` and `min` for the same answer as the matching float infinity, over a list of values that includes both zeros, both `NaN` precisions, the subnormal and the largest finite float.
All four were `MethodError`s for an angle that is not a multiple of π: negation existed only for an integer factor, and the other three not at all. Negation and conjugation rotate and reflect the angle, reduced so that both stay involutions; `abs` is `∞` whichever way the infinity points; `sign` is the unit vector, `cispi` giving it exactly on the axes. The integer factor keeps its own methods, which already answered in `Int` and are what `AllRealInfinities` relies on.
Both were `MethodError`s, which also made `∞ in 1:5` fail, a range asking `isinteger` before it compares. `Inf` is not an integer and rounding leaves it alone, so an infinity answers the same way and returns itself. `InfiniteCardinal` is left out of both: it is an `Integer`, for which `Base` already answers `true` and returns the value unchanged.
`∞ / 2` and `2 / ∞` were promotion errors, though `inv` was already there to build them from: division is multiplication by the inverse, which brings the sign and the `NaN` handling of `*` with it. `\` needs nothing of its own, `Base` defining it as `y / x`. `∞ / ∞` answers `NotANumber`, as `div(∞, ∞)` and `mod(∞, ∞)` already do, rather than the `NaN` of the floats. `2 / ∞` inherits the `Int` zero of `inv(∞)` where the floats give `0.0`, which fixing `inv` will settle in one place. `Rational` and `Complex` need the same explicit pairs in `ambiguities.jl` as the other operators.
`3 % ∞` was a promotion error, though `mod` and `div` were both already there. `rem` keeps the sign of the dividend, so unlike `mod` it needs no bound: `-3 % ∞` is `-3`, where `mod(-3, ∞)` is unbounded and says so. `divrem` follows from the two. The other direction answers `NotANumber`, as `mod(∞, x)` and `div(∞, ∞)` do. `Rational` and `BigInt` need the explicit pairs in `ambiguities.jl`, an `InfiniteCardinal` being an `Integer` that `Base` has its own methods for.
`∞ ≈ Inf` threw, `Base` promoting its arguments before it compares them and an infinity having no common type with a number. Nothing is near an infinity but an equal one, which is what the floats say too, so approximate equality is exact equality and the keywords have nothing to loosen.
Gaps that JuliaMath#68 covers and this branch did not, plus the types they missed. `round(x, ::RoundingMode)` was a MethodError, and `round(x; digits)` fell through to `Base` and returned `Inf` rather than the infinity, disagreeing with the plain `round(x)` next to it. `isinteger` and the four rounding functions also answered for `Infinity` and `RealInfinity` alone, so a `ComplexInfinity` raised a MethodError where `Base` answers `false` and the value itself for the matching `Complex`, and `ℵ₀` took a rounding mode but not the keywords. `float(::ComplexInfinity)` was a MethodError, the real infinities having got theirs from the `AbstractFloat` conversion. JuliaMath#68 proposes `exp(im*angle(x))*Inf`, which is unsound: `0 * Inf` is a `NaN`, so `float(ComplexInfinity())` gives `Inf + NaN*im`, and the imaginary and negative real axes come back as diagonals. `cospi`/`sinpi` are exact at the half-integers, so building the parts from them keeps the axes exact. Two saturating parts can express only eight rays, so an angle off them lands on the nearest one, which the test pins.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #86 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 6 6
Lines 266 340 +74
=========================================
+ Hits 266 340 +74 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
added 4 commits
September 4, 2026 15:33
It answers isnan, compares false against everything including itself, keeps isequal and hash so a container can hold one, sorts last, and converts to the NaN of any float type. It stays a Number rather than a Real, since it is also the undefined result of a ComplexInfinity.
Every operation on it was a MethodError or an ErrorException, so an undefined result could not be carried any further.
Adding infinities of opposite direction and multiplying an infinity by zero threw an ArgumentError, where the floats give NaN. The mod case is left alone, since Base answers mod(-2.0, Inf) with -2.0 rather than NaN.
The float NaN was returned unchanged, keeping its precision, where every other float special value loses it: Inf32 + ∞ is already ∞. This reverses a tested line of the NaN arithmetic PR.
PatrickHaecker
force-pushed
the
not_a_number
branch
from
September 4, 2026 13:48
01b5ce1 to
65682f0
Compare
Contributor
Author
|
Test coverage is now 100%. |
Contributor
Author
|
Working at |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This builds on #82, so I make it a draft until it is based purely on
master🤖 summary with some editing applied:
NotANumberis the package's type-independent counterpart ofNaN, but it did not act like one:isnanwas false, it compared equal to itself, and nearly every operation on it was aMethodError. This PR makes it a real not-a-number and routes the undefined results into it.What changes
NotANumberanswers the same predicates, comparisons, sort order, hashing and float conversions as a floatNaN.NotANumberagain instead of erroring.∞ - ∞and0 * ∞returnNotANumberinstead of throwing anArgumentError.NaNcombined with an infinity returnsNotANumber.Breaking changes
∞ - ∞and0 * ∞no longer throw, so code that catches thatArgumentErrorneeds updating. The docstring example0 * ∞now works as documented.NaN + ∞wasNaNand is nowNotANumber(), which reverses a line tested in #81. No existing assertion changes meaning, becauseisnan(::NotANumber)is true. Only the type of the result differs.Notes
modkeeps its current answers, sinceBasegivesmod(-2.0, Inf) == -2.0rather thanNaN.NotANumberstays<: Numberrather than<: Real, because it is also the undefined result of aComplexInfinity.