From a6caacce0dd332ab7db68310d3874157724e9257 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4cker?= Date: Tue, 1 Sep 2026 17:04:24 +0200 Subject: [PATCH 1/4] Keep `infpromote` from converting an infinity to `BigFloat` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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. --- src/algebra.jl | 3 +++ test/runtests.jl | 2 ++ 2 files changed, 5 insertions(+) diff --git a/src/algebra.jl b/src/algebra.jl index 669cc4a..5c4708b 100644 --- a/src/algebra.jl +++ b/src/algebra.jl @@ -6,6 +6,9 @@ @inline infpromote(x::RealInfinity, y::Union{Integer, Rational}) = (x, float(y)) @inline infpromote(x::Union{Integer, Rational}, y::RealInfinity) = (float(x), y) @inline infpromote(x::RealInfinity, ::InfiniteCardinal) = (x, ∞) +# `Base` promotes every `Real` to `BigFloat`, which would convert the infinity away. +@inline infpromote(x::BigFloat, y::Union{Infinity,RealInfinity}) = (x, y) +@inline infpromote(x::Union{Infinity,RealInfinity}, y::BigFloat) = (x, y) # sign diff --git a/test/runtests.jl b/test/runtests.jl index a1d5069..293c873 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -404,6 +404,8 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], @test T(-Inf) == inf == T(-Inf) @test T(Inf) ≠ inf end + @test T(2) + ∞ ≡ ∞ + T(2) ≡ ∞ + @test T(2) * +∞ ≡ (+∞)^T(2) ≡ +∞ end end From ad04aaff8ad0c65795a6dd041eac95814242b35b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4cker?= Date: Tue, 1 Sep 2026 17:06:29 +0200 Subject: [PATCH 2/4] Propagate `NaN` through the arithmetic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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` is defined 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. --- src/algebra.jl | 23 +++++++++++++++-------- src/ambiguities.jl | 2 +- test/runtests.jl | 14 ++++++++++++++ 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/src/algebra.jl b/src/algebra.jl index 5c4708b..a3b2810 100644 --- a/src/algebra.jl +++ b/src/algebra.jl @@ -30,7 +30,8 @@ @inline __add(x, y::AllInfinities) = isinf(x) ? _infadd(toinf(x), y) : y @inline __add(x::Integer, y::InfiniteCardinal) = max(x, y) -@inline _add(x, y) = __add(infpromote(x, y)...) +# A `NaN` argument is returned unchanged, as it is over the floats. Types with no `NaN` fold the test away. +@inline _add(x, y) = isnan(x) ? x : __add(infpromote(x, y)...) +(x::Number, y::AllInfinities) = _add(x, y) +(x::AllInfinities, y::Number) = _add(y, x) @@ -56,7 +57,11 @@ @inline __mul(x::Complex, y::ComplexInfinity{Bool}) = ComplexInfinity(_sb(x) + _sb(y)) @inline __mul(x::Integer, y::InfiniteCardinal) = x > 0 ? y : throw(ArgumentError("Cannot multiply $x * $y")) -@inline _mul(x, y) = iszero(x) ? throw(ArgumentError("Cannot multiply $x * $y")) : __mul(infpromote(x, y)...) +@inline function _mul(x, y) + isnan(x) && return x + iszero(x) && throw(ArgumentError("Cannot multiply $x * $y")) + __mul(infpromote(x, y)...) +end *(x::Number, y::AllInfinities) = _mul(x, y) *(x::AllInfinities, y::Number) = _mul(y, x) @@ -71,6 +76,7 @@ # mod @inline function _mod(x::Real, y::IntegerInfinities) + isnan(x) && return x signbit(x) == signbit(y) || throw(ArgumentError("mod($x,$y) is unbounded")) x end @@ -79,14 +85,14 @@ mod(::IntegerInfinities, ::Real) = NotANumber() mod(::IntegerInfinities, ::IntegerInfinities) = NotANumber() # fld, cld, div -_divinf(T) = zero(T) -_fldinf(x) = signbit(x) ? -one(x) : zero(x) -_cldinf(x) = signbit(x) ? zero(x) : one(x) -div(::T, ::IntegerInfinities) where T <: Real = _divinf(T) +_divinf(x) = isnan(x) ? x : zero(x) +_fldinf(x) = isnan(x) ? x : signbit(x) ? -one(x) : zero(x) +_cldinf(x) = isnan(x) ? x : signbit(x) ? zero(x) : one(x) +div(x::Real, ::IntegerInfinities) = _divinf(x) fld(x::Real, ::IntegerInfinities) = _fldinf(x) cld(x::Real, ::IntegerInfinities) = _cldinf(x) -_inffcd(x, y) = signbit(y) ? -x : x +_inffcd(x, y) = isnan(y) ? y : signbit(y) ? -x : x for OP in (:fld,:cld,:div) @eval begin $OP(x::IntegerInfinities, y::Real) = _inffcd(x, y) @@ -97,8 +103,9 @@ end # power # Although the base implementation can cover these cases, it can change overtime and yield inconsistent results. # ref: https://github.com/JuliaMath/Infinities.jl/actions/runs/19993302836/ -_infpow(::PositiveInfinity, p) = ifelse(iszero(p), one(p), ifelse(p > 0, +∞, +zero(p))) +_infpow(::PositiveInfinity, p) = isnan(p) ? p : ifelse(iszero(p), one(p), ifelse(p > 0, +∞, +zero(p))) function _infpow(x::NegativeInfinity, p) + isnan(p) && return p !isinteger(p) && throw(Base.Math.throw_exp_domainerror(x)) iszero(p) && return one(p) isodd(p) && return ifelse(p > 0, -∞, -zero(p)) diff --git a/src/ambiguities.jl b/src/ambiguities.jl index 371dd26..cf3e10a 100644 --- a/src/ambiguities.jl +++ b/src/ambiguities.jl @@ -35,7 +35,7 @@ for Typ in (Rational, ) for op in (:fld, :cld, :div) @eval $op(x::InfiniteCardinal, y::$Typ) = _inffcd(x, y) end - @eval div(::T, ::IntegerInfinities) where T <: $Typ = _divinf(T) + @eval div(x::$Typ, ::IntegerInfinities) = _divinf(x) @eval fld(x::$Typ, ::IntegerInfinities) = _fldinf(x) @eval cld(x::$Typ, ::IntegerInfinities) = _cldinf(x) end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 293c873..fc1d363 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -450,6 +450,20 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], @test sorted[1] === -∞ && sorted[2] === 1.0 && sorted[3] === ∞ && isnan(sorted[4]) end + @testset "NaN arithmetic" begin + for nan in (NaN, NaN32, NaN16, big(NaN)), + inf in (∞, +∞, -∞, ℵ₀, ComplexInfinity(), -ComplexInfinity()) + + for op in (+, -, *, div, fld, cld) + @test isnan(op(nan, inf)) && isnan(op(inf, nan)) + end + @test isnan(mod(nan, inf)) # the other direction is `NotANumber` for every argument + end + for nan in (NaN, NaN32, NaN16, big(NaN)), inf in (+∞, -∞) + @test isnan(inf^nan) + end + end + @testset "ordinary values" begin for inf in (∞, +∞, ℵ₀) @test 1.0 < inf && !(inf < 1.0) && 1.0 ≤ inf && inf ≥ 1.0 From 2d4604f0d8e392ec0bffc97079365ffa4ed23a3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4cker?= Date: Tue, 1 Sep 2026 17:08:03 +0200 Subject: [PATCH 3/4] Return a `Bool` from `signbit(::ComplexInfinity)` 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 returns 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. --- src/Infinities.jl | 6 ++---- src/algebra.jl | 3 ++- test/runtests.jl | 3 +++ 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Infinities.jl b/src/Infinities.jl index 95273de..872af42 100644 --- a/src/Infinities.jl +++ b/src/Infinities.jl @@ -107,11 +107,9 @@ ComplexInfinity{T}(::Infinity) where T<:Real = ComplexInfinity{T}() ComplexInfinity(::Infinity) = ComplexInfinity() ComplexInfinity{T}(x::RealInfinity) where T<:Real = ComplexInfinity{T}(signbit(x)) ComplexInfinity(x::RealInfinity) = ComplexInfinity(signbit(x)) -ComplexInfinity{T}(x::ComplexInfinity) where T<:Real = ComplexInfinity(T(signbit(x))) # ambiguity fix +ComplexInfinity{T}(x::ComplexInfinity) where T<:Real = ComplexInfinity(T(x.signbit)) # ambiguity fix -signbit(y::ComplexInfinity{Bool}) = y.signbit -signbit(y::ComplexInfinity{<:Integer}) = !(mod(y.signbit,2) == 0) -signbit(y::ComplexInfinity) = y.signbit +signbit(y::ComplexInfinity) = mod(y.signbit, 2) == 1 convert(::Type{ComplexInfinity{T}}, ::Infinity) where T = ComplexInfinity{T}() convert(::Type{ComplexInfinity}, ::Infinity) = ComplexInfinity() diff --git a/src/algebra.jl b/src/algebra.jl index a3b2810..4b6b498 100644 --- a/src/algebra.jl +++ b/src/algebra.jl @@ -49,7 +49,8 @@ # multiplication @inline _sb(x) = signbit(x) -@inline _sb(x::Complex) = angle(x)/π # overloading `signbit` causes type piracy +@inline _sb(x::Complex) = angle(x)/π # overloading `signbit` causes type piracy +@inline _sb(x::ComplexInfinity) = x.signbit # the whole angle, not just its sign @inline __mul(x, y::AllInfinities) = RealInfinity(_sb(x) ⊻ _sb(y)) @inline __mul(x, y::ComplexInfinity) = ComplexInfinity(_sb(x) + _sb(y)) diff --git a/test/runtests.jl b/test/runtests.jl index fc1d363..0fb06a9 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -326,6 +326,9 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], @test signbit(ComplexInfinity(3)) @test !signbit(ComplexInfinity(100)) + # `signbit` returns a `Bool` for every angle, as it does over the reals + @test signbit(ComplexInfinity(1.0)) === signbit(-ComplexInfinity()) === true + @test signbit(ComplexInfinity(0.5)) === signbit(ComplexInfinity()) === false end @testset "Set" begin From 2b74b7883d91a9f01b308e9365e1170bf0f6a569 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4cker?= Date: Tue, 1 Sep 2026 17:16:09 +0200 Subject: [PATCH 4/4] Compare every ordering against the float result 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 result as the matching float infinity, over a list of values that includes both zeros, both `NaN` precisions, the subnormal and the largest finite float. --- test/runtests.jl | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/runtests.jl b/test/runtests.jl index 0fb06a9..2668832 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -479,6 +479,20 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], @test max(-∞, ∞) === ∞ && min(-∞, ∞) === -∞ end + @testset "against the floats" begin + values = (0, 1, -2, 1.5, -1.5, 0.0, -0.0, NaN, NaN32, Inf, -Inf, + prevfloat(Inf), nextfloat(-Inf), nextfloat(0.0)) + for x in values, (inf, flt) in ((∞, Inf), (+∞, Inf), (-∞, -Inf), (ℵ₀, Inf)) + for op in (<, ≤, >, ≥, ==, isless, isequal) + @test op(x, inf) == op(x, flt) + @test op(inf, x) == op(flt, x) + end + for op in (max, min) + @test isequal(op(x, inf), op(x, flt)) && isequal(op(inf, x), op(flt, x)) + end + end + end + @testset "parsing" begin @test tryparse(NegativeInfinity, "-∞") == NegativeInfinity() @test tryparse(NegativeInfinity, " - ∞ ") == NegativeInfinity()