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 01/18] 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 02/18] 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 03/18] 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 04/18] 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() From c2c927b24eaf6d8d488be1a5d7d82b4f41cd2c1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4cker?= Date: Wed, 2 Sep 2026 06:33:01 +0200 Subject: [PATCH 05/18] Give `ComplexInfinity` its `abs`, `sign`, `conj` and negation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 returned an `Int` and are what `AllRealInfinities` relies on. --- src/Infinities.jl | 8 ++++++-- src/algebra.jl | 1 + test/runtests.jl | 17 +++++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/Infinities.jl b/src/Infinities.jl index 872af42..7becaef 100644 --- a/src/Infinities.jl +++ b/src/Infinities.jl @@ -2,7 +2,7 @@ module Infinities import Base: angle, isone, iszero, isinf, isfinite, abs, one, oneunit, zero, isless, inv, +, -, *, ^, ==, <, ≤, >, ≥, fld, cld, div, mod, min, max, sign, signbit, - string, show, promote_rule, convert, getindex, tryparse, + string, show, promote_rule, convert, getindex, tryparse, conj, Bool, Integer export ∞, ℵ₀, ℵ₁, RealInfinity, ComplexInfinity, InfiniteCardinal, NotANumber, PositiveInfinity, NegativeInfinity @@ -117,8 +117,12 @@ convert(::Type{ComplexInfinity{T}}, x::RealInfinity) where T = ComplexInfinity{T convert(::Type{ComplexInfinity}, x::RealInfinity) = ComplexInfinity(x) -sign(y::ComplexInfinity{<:Integer}) = mod(y.signbit,2) == 0 ? 1 : -1 +sign(y::ComplexInfinity{<:Integer}) = mod(y.signbit, 2) == 0 ? 1 : -1 +sign(y::ComplexInfinity) = cispi(y.signbit) angle(x::ComplexInfinity) = π*x.signbit +abs(::ComplexInfinity) = ∞ +conj(y::ComplexInfinity{<:Integer}) = y # an integer factor points along the real axis +conj(y::ComplexInfinity) = ComplexInfinity(mod(-y.signbit, 2)) show(io::IO, x::ComplexInfinity) = print(io, "exp($(x.signbit)*im*π)∞") diff --git a/src/algebra.jl b/src/algebra.jl index 4b6b498..928660a 100644 --- a/src/algebra.jl +++ b/src/algebra.jl @@ -16,6 +16,7 @@ -(::Infinity) = RealInfinity(true) -(y::RealInfinity) = RealInfinity(!signbit(y)) -(y::ComplexInfinity{B}) where B<:Integer = sign(y) == 1 ? ComplexInfinity(one(B)) : ComplexInfinity(zero(B)) +-(y::ComplexInfinity) = ComplexInfinity(mod(y.signbit + 1, 2)) +(x::InfiniteCardinal) = x -(::InfiniteCardinal) = -∞ diff --git a/test/runtests.jl b/test/runtests.jl index 2668832..82167b2 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -329,6 +329,23 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], # `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 + + @testset "abs/sign/conj/-" begin + @test -ComplexInfinity(0.5) ≡ ComplexInfinity(1.5) + @test -(-ComplexInfinity(0.5)) ≡ ComplexInfinity(0.5) + @test -ComplexInfinity() ≡ ComplexInfinity(true) + @test abs(ComplexInfinity()) ≡ abs(ComplexInfinity(0.5)) ≡ ∞ + @test sign(ComplexInfinity(0.5)) ≡ complex(0.0, 1.0) + @test sign(ComplexInfinity(0.0)) ≡ complex(1.0, 0.0) + @test sign(ComplexInfinity(1.0)) ≡ complex(-1.0, 0.0) + # an integer angle stays on the real line, where the sign is a real ±1 + @test sign(ComplexInfinity(false)) ≡ 1 + @test sign(ComplexInfinity(true)) ≡ -1 + # off the axes conjugation and negation part company: `-ComplexInfinity(0.25)` is `ComplexInfinity(1.25)` + @test conj(ComplexInfinity(0.25)) ≡ ComplexInfinity(1.75) + @test conj(conj(ComplexInfinity(0.25))) ≡ ComplexInfinity(0.25) + @test conj(ComplexInfinity(true)) ≡ ComplexInfinity(true) # the narrow type survives + end end @testset "Set" begin From aea57149e6b2e6b5b240e63faf6ee026e8b21bae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4cker?= Date: Mon, 7 Sep 2026 05:03:13 +0200 Subject: [PATCH 06/18] Give an infinite complex summand the direction it points in MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ComplexInfinity` stores its direction in half turns, but `toinf` filled the field with the radians of `angle(x)`. The direction of an infinite complex summand was therefore off by a factor of π: `angle(toinf(complex(0, Inf)))` gave 4.93 rather than `π/2`. `_infadd` compares those angles, so a sum whose parts point the same way threw although `==` called them equal: both `complex(-Inf, 0.0) + -∞` and `complex(0.0, Inf) + im*∞` raised an ArgumentError. Only the positive real axis escaped, angle `0` being the fixed point of the missing scaling. `_sb` is the conversion the multiplication already uses, so it moves above the addition and both sections share it. --- src/algebra.jl | 11 ++++++----- test/runtests.jl | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/algebra.jl b/src/algebra.jl index 928660a..252ce5a 100644 --- a/src/algebra.jl +++ b/src/algebra.jl @@ -22,8 +22,13 @@ # addition +@inline _sb(x) = signbit(x) +@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 toinf(x) = RealInfinity(signbit(x)) -@inline toinf(x::Complex) = ComplexInfinity(angle(x)) +# The field counts half turns, so the radians of `angle` have to be scaled. +@inline toinf(x::Complex) = ComplexInfinity(_sb(x)) @inline toinf(x::ComplexInfinity) = x @inline _infadd(x, y) = angle(x) == angle(y) ? y : throw(ArgumentError("Angles must be the same to add ∞")) @@ -49,10 +54,6 @@ # multiplication -@inline _sb(x) = signbit(x) -@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)) @inline __mul(x, y::ComplexInfinity{Bool}) = ComplexInfinity(_sb(x) ⊻ _sb(y)) diff --git a/test/runtests.jl b/test/runtests.jl index 82167b2..e40389a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -279,11 +279,26 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], @test ComplexInfinity(true)+1 == ComplexInfinity(true) @test ComplexInfinity(false)+1 == ComplexInfinity(false) + # An infinite summand reaches `_infadd` through `toinf`, which has to give half turns + @test complex(Inf, 0.0) + ∞ ≡ ComplexInfinity() + @test complex(-Inf, 0.0) + (-∞) ≡ -ComplexInfinity() + @test complex(0.0, Inf) + im*∞ ≡ im*∞ + @test complex(0.0, -Inf) + (-im*∞) ≡ -im*∞ + @test_throws ArgumentError complex(0.0, Inf) + ∞ + # two infinite parts are the only way an infinite `Complex` points off the axes + for (z, inf) in ((complex(Inf, Inf), (1+im)*∞), (complex(-Inf, Inf), (-1+im)*∞), + (complex(-Inf, -Inf), (-1-im)*∞), (complex(Inf, -Inf), (1-im)*∞)) + @test z + inf ≡ inf + end + @test ∞ * ComplexInfinity() ≡ RealInfinity() * ComplexInfinity() ≡ ComplexInfinity() * ∞ ≡ ComplexInfinity() * RealInfinity() ≡ ComplexInfinity() @test 2.0im*∞ ≡ ∞*2.0im ≡ 2.0im * RealInfinity() ≡ RealInfinity() * 2.0im ≡ ComplexInfinity(1/2) @test 2ComplexInfinity() ≡ ComplexInfinity()*2 ≡ ComplexInfinity() + # a factor gives the direction it actually has, so rescaling moves it once it rounds + @test 4*(0.3+0.1im)*∞ ≡ (0.3+0.1im)*∞ + @test 3*(0.3+0.1im)*∞ ≢ (0.3+0.1im)*∞ @test exp(im*π/4)*∞ == Inf+im*Inf @test exp(im*π/4)+∞ == ∞ From abb52c490964730db488a617b147c21bba78d8b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4cker?= Date: Wed, 2 Sep 2026 06:34:35 +0200 Subject: [PATCH 07/18] Add `isinteger` and the rounding functions for an infinity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 does the same and returns itself. `InfiniteCardinal` is left out of both: it is an `Integer`, for which `Base` already returns `true` and the value unchanged. --- src/Infinities.jl | 1 + src/interface.jl | 6 ++++++ test/runtests.jl | 9 +++++++++ 3 files changed, 16 insertions(+) diff --git a/src/Infinities.jl b/src/Infinities.jl index 7becaef..65a57cf 100644 --- a/src/Infinities.jl +++ b/src/Infinities.jl @@ -3,6 +3,7 @@ module Infinities import Base: angle, isone, iszero, isinf, isfinite, abs, one, oneunit, zero, isless, inv, +, -, *, ^, ==, <, ≤, >, ≥, fld, cld, div, mod, min, max, sign, signbit, string, show, promote_rule, convert, getindex, tryparse, conj, + isinteger, round, floor, ceil, trunc, Bool, Integer export ∞, ℵ₀, ℵ₁, RealInfinity, ComplexInfinity, InfiniteCardinal, NotANumber, PositiveInfinity, NegativeInfinity diff --git a/src/interface.jl b/src/interface.jl index a8a9d97..8de6ab1 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -7,6 +7,12 @@ iszero(::AllInfinities) = false isinf(::AllInfinities) = true isfinite(::AllInfinities) = false +# `InfiniteCardinal` is an `Integer`, which `Base` already covers. +isinteger(::Union{Infinity,RealInfinity}) = false +for f in (:round, :floor, :ceil, :trunc) + @eval $f(x::Union{Infinity,RealInfinity}) = x +end + # `Infinity` is positive, so it has no common type with `NegativeInfinity` (as is already the case for `PositiveInfinity`). promote_rule(::Type{Infinity}, ::Type{PositiveInfinity}) = PositiveInfinity promote_rule(::Type{Infinity}, ::Type{ComplexInfinity{T}}) where T = ComplexInfinity{T} diff --git a/test/runtests.jl b/test/runtests.jl index e40389a..f0e27a8 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -429,6 +429,15 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], @test zero(exp(0.1im)∞) ≡ zero(ComplexInfinity) ≡ 0.0+0.0im end + @testset "isinteger/round" begin + @test !isinteger(∞) && !isinteger(+∞) && !isinteger(-∞) + @test isinteger(ℵ₀) # an `InfiniteCardinal` is an `Integer` + @test ∞ ∉ 1:5 # `in` asks a range for `isinteger` before comparing + for f in (round, floor, ceil, trunc) + @test f(∞) ≡ ∞ && f(+∞) ≡ +∞ && f(-∞) ≡ -∞ && f(ℵ₀) ≡ ℵ₀ + end + end + @testset "float precisions" begin for T in (Float16, Float32, Float64, BigFloat) for inf in (∞, +∞, ComplexInfinity(), ℵ₀) From 7fb84d5ef509e867ec760f6f9262f965a45aa30b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4cker?= Date: Wed, 2 Sep 2026 06:51:01 +0200 Subject: [PATCH 08/18] Divide by and into an infinity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `∞ / 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`. `∞ / ∞` returns `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. --- src/Infinities.jl | 2 +- src/algebra.jl | 8 ++++++++ src/ambiguities.jl | 15 +++++++++------ test/runtests.jl | 19 +++++++++++++++++++ 4 files changed, 37 insertions(+), 7 deletions(-) diff --git a/src/Infinities.jl b/src/Infinities.jl index 65a57cf..d7a1417 100644 --- a/src/Infinities.jl +++ b/src/Infinities.jl @@ -1,7 +1,7 @@ module Infinities import Base: angle, isone, iszero, isinf, isfinite, abs, one, oneunit, zero, isless, inv, - +, -, *, ^, ==, <, ≤, >, ≥, fld, cld, div, mod, min, max, sign, signbit, + +, -, *, /, ^, ==, <, ≤, >, ≥, fld, cld, div, mod, min, max, sign, signbit, string, show, promote_rule, convert, getindex, tryparse, conj, isinteger, round, floor, ceil, trunc, Bool, Integer diff --git a/src/algebra.jl b/src/algebra.jl index 252ce5a..5a704b3 100644 --- a/src/algebra.jl +++ b/src/algebra.jl @@ -77,6 +77,14 @@ end *(::Infinity, ::Infinity) = ∞ +# division +# `\` needs nothing of its own, `Base` defining it as `y / x`. +@inline _div(x, y) = x * inv(y) + +/(x::AllInfinities, y::Number) = _div(x, y) +/(x::Number, y::AllInfinities) = _div(x, y) +/(x::AllInfinities, y::AllInfinities) = NotANumber() + # mod @inline function _mod(x::Real, y::IntegerInfinities) isnan(x) && return x diff --git a/src/ambiguities.jl b/src/ambiguities.jl index cf3e10a..15e23ff 100644 --- a/src/ambiguities.jl +++ b/src/ambiguities.jl @@ -19,12 +19,15 @@ for Typ in (Rational, BigInt, BigFloat, Complex, AbstractIrrational) end for Typ in (Complex, Rational, Complex{Bool}, Integer) - @eval +(x::AllInfinities, y::$Typ) = _add(y, x) - @eval +(x::$Typ, y::AllInfinities) = _add(x, y) - @eval -(x::AllInfinities, y::$Typ) = _sub(x, y) - @eval -(x::$Typ, y::AllInfinities) = _sub(x, y) - @eval *(x::AllInfinities, y::$Typ) = _mul(y, x) - @eval *(x::$Typ, y::AllInfinities) = _mul(x, y) + # `_add` and `_mul` dispatch on the infinity being second; `_sub` and `_div` delegate to them. + for (op, fop) in ((:+, :_add), (:*, :_mul)) + @eval $op(x::AllInfinities, y::$Typ) = $fop(y, x) + @eval $op(x::$Typ, y::AllInfinities) = $fop(x, y) + end + for (op, fop) in ((:-, :_sub), (:/, :_div)) + @eval $op(x::AllInfinities, y::$Typ) = $fop(x, y) + @eval $op(x::$Typ, y::AllInfinities) = $fop(x, y) + end end ^(x::RealInfinity, y::Rational) = _infpow(infpromote(x, y)...) diff --git a/test/runtests.jl b/test/runtests.jl index f0e27a8..b91e101 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -438,6 +438,25 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], end end + @testset "division" begin + @test ∞ / 2 ≡ 2 \ ∞ ≡ +∞ + @test (-∞) / 2 ≡ ∞ / -2 ≡ -∞ + # a zero divisor keeps the direction, and its own sign is the one that counts + @test ∞ / 0 ≡ ∞ / 0.0 ≡ (-∞) / (-0.0) ≡ +∞ + @test (-∞) / 0 ≡ (-∞) / 0.0 ≡ ∞ / (-0.0) ≡ -∞ + @test ComplexInfinity(0.5) / 2 ≡ ComplexInfinity(0.5) + # dividing by a complex turns the direction by its angle + @test (+∞) / (1+im) ≡ (1-im)*∞ + @test 2 / -∞ ≡ -0.0 + @test 2 / ∞ == ∞ \ 2 == 2 / ℵ₀ == 0 # the type follows `inv`, which returns an `Int` for `∞` + # `∞` is positive, so the quotient keeps the dividend exact; a signed infinity + # needs a float to carry `-0.0` + @test (2//3) / ∞ ≡ (2//3) / ℵ₀ ≡ 0//1 + @test (2//3) / (+∞) ≡ 0.0 + @test ∞ / ∞ isa NotANumber + @test isnan(NaN / ∞) && isnan(∞ / NaN) + end + @testset "float precisions" begin for T in (Float16, Float32, Float64, BigFloat) for inf in (∞, +∞, ComplexInfinity(), ℵ₀) From 9d8406aba3270bce72659df63e999d474acbaf99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4cker?= Date: Wed, 2 Sep 2026 07:14:09 +0200 Subject: [PATCH 09/18] Take the remainder against an infinity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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 throws. `divrem` follows from the two. The other direction returns `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. --- src/Infinities.jl | 3 ++- src/algebra.jl | 10 ++++++++++ src/ambiguities.jl | 6 +++++- test/runtests.jl | 16 ++++++++++++++++ 4 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/Infinities.jl b/src/Infinities.jl index d7a1417..025e6d0 100644 --- a/src/Infinities.jl +++ b/src/Infinities.jl @@ -1,7 +1,8 @@ module Infinities import Base: angle, isone, iszero, isinf, isfinite, abs, one, oneunit, zero, isless, inv, - +, -, *, /, ^, ==, <, ≤, >, ≥, fld, cld, div, mod, min, max, sign, signbit, + +, -, *, /, ^, ==, <, ≤, >, ≥, fld, cld, div, mod, rem, divrem, min, max, + sign, signbit, string, show, promote_rule, convert, getindex, tryparse, conj, isinteger, round, floor, ceil, trunc, Bool, Integer diff --git a/src/algebra.jl b/src/algebra.jl index 5a704b3..aef2ffb 100644 --- a/src/algebra.jl +++ b/src/algebra.jl @@ -95,6 +95,16 @@ mod(x::Real, y::IntegerInfinities) = _mod(x, y) mod(::IntegerInfinities, ::Real) = NotANumber() mod(::IntegerInfinities, ::IntegerInfinities) = NotANumber() +# rem, divrem +# `rem` keeps the sign of the dividend, so unlike `mod` it stays bounded either way. +rem(x::Real, ::IntegerInfinities) = x +rem(::IntegerInfinities, ::Real) = NotANumber() +rem(::IntegerInfinities, ::IntegerInfinities) = NotANumber() +# `Base` computes the remainder of two `Integer`s as `a - div(a,b)*b`, which an `InfiniteCardinal` cannot evaluate. +divrem(x::Real, y::IntegerInfinities) = (div(x, y), rem(x, y)) +divrem(x::IntegerInfinities, y::Real) = (div(x, y), rem(x, y)) +divrem(x::IntegerInfinities, y::IntegerInfinities) = (div(x, y), rem(x, y)) + # fld, cld, div _divinf(x) = isnan(x) ? x : zero(x) _fldinf(x) = isnan(x) ? x : signbit(x) ? -one(x) : zero(x) diff --git a/src/ambiguities.jl b/src/ambiguities.jl index 15e23ff..c619418 100644 --- a/src/ambiguities.jl +++ b/src/ambiguities.jl @@ -35,10 +35,14 @@ end for Typ in (Rational, ) @eval mod(::IntegerInfinities, ::$Typ) = NotANumber() @eval mod(x::$Typ, y::IntegerInfinities) = _mod(x, y) + @eval rem(::InfiniteCardinal, ::$Typ) = NotANumber() + @eval rem(x::$Typ, ::IntegerInfinities) = x for op in (:fld, :cld, :div) @eval $op(x::InfiniteCardinal, y::$Typ) = _inffcd(x, y) end @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 +end + +divrem(x::BigInt, y::IntegerInfinities) = (div(x, y), rem(x, y)) \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index b91e101..c5c2aca 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -457,6 +457,22 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], @test isnan(NaN / ∞) && isnan(∞ / NaN) end + @testset "rem/divrem" begin + @test 3 % ∞ ≡ rem(3, -∞) ≡ 3 % ℵ₀ ≡ 3 + @test -3 % ∞ ≡ -3 # `rem` keeps the sign of the dividend, where `mod(-3, ∞)` is unbounded + @test rem(∞, 3) isa NotANumber && rem(∞, ∞) isa NotANumber + @test isnan(rem(NaN, ∞)) + @test divrem(3, ∞) ≡ (0, 3) && divrem(-3, ∞) ≡ (0, -3) + # `Base` has an `Integer`-only `divrem` that avoids `rem`, and `ℵ₀` is an `Integer` + @test divrem(3, ℵ₀) ≡ (0, 3) + @test divrem(ℵ₀, 3) ≡ (ℵ₀, NotANumber()) + @test divrem(ℵ₀, ℵ₀) ≡ (NotANumber(), NotANumber()) + # `Rational` and `BigInt` bring their own `Base` methods, which need methods of their own + @test rem(1//2, ∞) ≡ rem(1//2, ℵ₀) ≡ 1//2 + @test rem(ℵ₀, 1//2) isa NotANumber + @test divrem(big(3), ∞) == divrem(big(3), ℵ₀) == (0, 3) + end + @testset "float precisions" begin for T in (Float16, Float32, Float64, BigFloat) for inf in (∞, +∞, ComplexInfinity(), ℵ₀) From 17c3c4b6033ccc06019c7185258b14ddab544315 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4cker?= Date: Wed, 2 Sep 2026 07:19:23 +0200 Subject: [PATCH 10/18] Let `isapprox` compare an infinity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `∞ ≈ 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 do too, so approximate equality is exact equality and the keywords have nothing to loosen. --- src/Infinities.jl | 2 +- src/ambiguities.jl | 7 ++++++- src/compare.jl | 6 ++++++ test/runtests.jl | 21 +++++++++++++++++++++ 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/Infinities.jl b/src/Infinities.jl index 025e6d0..87c2453 100644 --- a/src/Infinities.jl +++ b/src/Infinities.jl @@ -2,7 +2,7 @@ module Infinities import Base: angle, isone, iszero, isinf, isfinite, abs, one, oneunit, zero, isless, inv, +, -, *, /, ^, ==, <, ≤, >, ≥, fld, cld, div, mod, rem, divrem, min, max, - sign, signbit, + sign, signbit, isapprox, string, show, promote_rule, convert, getindex, tryparse, conj, isinteger, round, floor, ceil, trunc, Bool, Integer diff --git a/src/ambiguities.jl b/src/ambiguities.jl index c619418..4a80a6b 100644 --- a/src/ambiguities.jl +++ b/src/ambiguities.jl @@ -45,4 +45,9 @@ for Typ in (Rational, ) @eval cld(x::$Typ, ::IntegerInfinities) = _cldinf(x) end -divrem(x::BigInt, y::IntegerInfinities) = (div(x, y), rem(x, y)) \ No newline at end of file +divrem(x::BigInt, y::IntegerInfinities) = (div(x, y), rem(x, y)) + +# an `InfiniteCardinal` is an `Integer`, for which `Base` has its own `isapprox` +isapprox(x::InfiniteCardinal, y::Integer; kwargs...) = x == y +isapprox(x::Integer, y::InfiniteCardinal; kwargs...) = x == y +isapprox(x::InfiniteCardinal, y::InfiniteCardinal; kwargs...) = x == y \ No newline at end of file diff --git a/src/compare.jl b/src/compare.jl index 0faada0..b117798 100644 --- a/src/compare.jl +++ b/src/compare.jl @@ -27,6 +27,12 @@ _isinf(x::Real, y::AllRealInfinities) = isinf(x) && (signbit(y) ? x < zero(x) : ==(y::Number, x::AllInfinities) = _eq(y, x) ==(x::AllInfinities, y::AllInfinities) = _infeq(x, y) +# isapprox +# `Base` compares only after promoting, which fails for an infinity and a number. +isapprox(x::AllInfinities, y::Number; kwargs...) = x == y +isapprox(x::Number, y::AllInfinities; kwargs...) = x == y +isapprox(x::AllInfinities, y::AllInfinities; kwargs...) = x == y + # isless # `isless` is the sort order. `NaN` sorts after every other value, infinities included. isless(x::AllRealInfinities, y::AllRealInfinities) = signbit(x) && !signbit(y) diff --git a/test/runtests.jl b/test/runtests.jl index c5c2aca..4fd40b7 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -473,6 +473,27 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], @test divrem(big(3), ∞) == divrem(big(3), ℵ₀) == (0, 3) end + @testset "isapprox" begin + @test ∞ ≈ Inf && -∞ ≈ -Inf32 && ℵ₀ ≈ ∞ && ∞ ≈ ∞ + @test !(∞ ≈ 1e300) && !(∞ ≈ -∞) + @test Inf ≈ ∞ && -Inf32 ≈ -∞ && !(1e300 ≈ ∞) # the infinity may stand on either side + # an `InfiniteCardinal` is an `Integer`, for which `Base` has its own `isapprox` + @test ℵ₀ ≈ ℵ₀ && !(ℵ₀ ≈ ℵ₁) + @test !(ℵ₀ ≈ 3) && !(3 ≈ ℵ₀) + @test isapprox(∞, Inf; atol=1) # the keywords are accepted, but nothing is near an infinity + @test !isapprox(∞, 1; atol=∞) + + @testset "infinite tolerance" begin + values = (0, 1, -2, 1.5, 0.0, NaN, Inf, -Inf) + for x in values, y in values, (inf, flt) in ((∞, Inf), (+∞, Inf), (-∞, -Inf), (ℵ₀, Inf)) + @test isapprox(x, y; atol=inf) == isapprox(x, y; atol=flt) + # Skipped over a `Base` bug: its `Integer` method evaluates `rtol * 0` for two zeros, so `isapprox(0, 0; rtol=Inf)` is `false`. + x isa Integer && y isa Integer && iszero(x) && iszero(y) && continue + @test isapprox(x, y; rtol=inf) == isapprox(x, y; rtol=flt) + end + end + end + @testset "float precisions" begin for T in (Float16, Float32, Float64, BigFloat) for inf in (∞, +∞, ComplexInfinity(), ℵ₀) From f13c3ebbf5639233aab0bb7d2a7b8891fa9aee80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4cker?= Date: Fri, 4 Sep 2026 06:19:53 +0200 Subject: [PATCH 11/18] Complete `float` and the rounding forms for an infinity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gaps that #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 were also defined for `Infinity` and `RealInfinity` alone, so a `ComplexInfinity` raised a MethodError where `Base` returns `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. #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. --- src/Infinities.jl | 10 +++++++++- src/interface.jl | 7 ++++--- test/runtests.jl | 18 ++++++++++++++++-- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/Infinities.jl b/src/Infinities.jl index 87c2453..ecca7c6 100644 --- a/src/Infinities.jl +++ b/src/Infinities.jl @@ -4,7 +4,7 @@ import Base: angle, isone, iszero, isinf, isfinite, abs, one, oneunit, zero, isl +, -, *, /, ^, ==, <, ≤, >, ≥, fld, cld, div, mod, rem, divrem, min, max, sign, signbit, isapprox, string, show, promote_rule, convert, getindex, tryparse, conj, - isinteger, round, floor, ceil, trunc, + isinteger, round, floor, ceil, trunc, float, Bool, Integer export ∞, ℵ₀, ℵ₁, RealInfinity, ComplexInfinity, InfiniteCardinal, NotANumber, PositiveInfinity, NegativeInfinity @@ -126,6 +126,14 @@ abs(::ComplexInfinity) = ∞ conj(y::ComplexInfinity{<:Integer}) = y # an integer factor points along the real axis conj(y::ComplexInfinity) = ComplexInfinity(mod(-y.signbit, 2)) +# An exact zero has to stay finite, `Inf * 0` being a `NaN`. +@inline _ray(c) = iszero(c) ? c : copysign(Inf, c) +# `Complex` reaches only the eight rays of its two saturating parts, so the direction lands on the nearest of them. +function float(x::ComplexInfinity) + s, c = sincospi(x.signbit) + complex(_ray(c), _ray(s)) +end + show(io::IO, x::ComplexInfinity) = print(io, "exp($(x.signbit)*im*π)∞") one(::Type{<:ComplexInfinity}) = one(ComplexF64) diff --git a/src/interface.jl b/src/interface.jl index 8de6ab1..958883f 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -7,11 +7,12 @@ iszero(::AllInfinities) = false isinf(::AllInfinities) = true isfinite(::AllInfinities) = false -# `InfiniteCardinal` is an `Integer`, which `Base` already covers. -isinteger(::Union{Infinity,RealInfinity}) = false +# `InfiniteCardinal` is absent because `Base` already returns `true` for it through `Integer`. +isinteger(::Union{Infinity, RealInfinity, ComplexInfinity}) = false for f in (:round, :floor, :ceil, :trunc) - @eval $f(x::Union{Infinity,RealInfinity}) = x + @eval $f(x::AllInfinities; kwargs...) = x end +round(x::AllInfinities, ::RoundingMode; kwargs...) = x # `Infinity` is positive, so it has no common type with `NegativeInfinity` (as is already the case for `PositiveInfinity`). promote_rule(::Type{Infinity}, ::Type{PositiveInfinity}) = PositiveInfinity diff --git a/test/runtests.jl b/test/runtests.jl index 4fd40b7..3504469 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -361,6 +361,15 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], @test conj(conj(ComplexInfinity(0.25))) ≡ ComplexInfinity(0.25) @test conj(ComplexInfinity(true)) ≡ ComplexInfinity(true) # the narrow type survives end + + @testset "float" begin + @test float(ComplexInfinity()) ≡ float(ComplexInfinity(0.0)) ≡ complex(Inf, 0.0) + @test float(ComplexInfinity(1/2)) ≡ complex(0.0, Inf) + @test float(ComplexInfinity(1.0)) ≡ float(ComplexInfinity(true)) ≡ complex(-Inf, 0.0) + @test float(ComplexInfinity(-1/2)) ≡ float(ComplexInfinity(3/2)) ≡ complex(0.0, -Inf) + # `Complex` points along eight rays only, so every other angle collapses onto the nearest + @test float(ComplexInfinity(1/4)) ≡ float(ComplexInfinity(0.3)) ≡ complex(Inf, Inf) + end end @testset "Set" begin @@ -430,11 +439,16 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], end @testset "isinteger/round" begin + infinities = (∞, +∞, -∞, ℵ₀, ComplexInfinity(), ComplexInfinity(1/4)) @test !isinteger(∞) && !isinteger(+∞) && !isinteger(-∞) + @test !isinteger(ComplexInfinity()) && !isinteger(ComplexInfinity(1/4)) @test isinteger(ℵ₀) # an `InfiniteCardinal` is an `Integer` @test ∞ ∉ 1:5 # `in` asks a range for `isinteger` before comparing - for f in (round, floor, ceil, trunc) - @test f(∞) ≡ ∞ && f(+∞) ≡ +∞ && f(-∞) ≡ -∞ && f(ℵ₀) ≡ ℵ₀ + for f in (round, floor, ceil, trunc), x in infinities + @test f(x) ≡ f(x; digits=2) ≡ x + end + for r in (RoundNearest, RoundUp, RoundDown, RoundToZero), x in infinities + @test round(x, r) ≡ round(x, r; digits=2) ≡ x end end From 4a3f32a63676a691075a63087299dac4367e5a92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4cker?= Date: Wed, 2 Sep 2026 14:20:44 +0200 Subject: [PATCH 12/18] Make `NotANumber` behave like a `NaN` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It satisfies `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 also becomes a `Real`. Dividing two real numbers has to give a real number, and `+∞/+∞` is a `NotANumber`, so as a `Number` it made `isreal(+∞/+∞)` false. The supertype is also what lets `Complex{NotANumber}` exist, since `Complex` takes a `Real`. The price is dispatch. A `Real` matches Base's own methods for `Real`, which are exactly as specific, so every such slot has to be filled here. `NotANumberRivals` lists the types that need one. --- src/Infinities.jl | 14 ++++++++++---- src/compare.jl | 25 +++++++++++++++++++++++++ src/interface.jl | 23 +++++++++++++++++++++-- test/runtests.jl | 36 ++++++++++++++++++++++++++++++++++++ 4 files changed, 92 insertions(+), 6 deletions(-) diff --git a/src/Infinities.jl b/src/Infinities.jl index ecca7c6..27bc2d8 100644 --- a/src/Infinities.jl +++ b/src/Infinities.jl @@ -1,6 +1,6 @@ module Infinities -import Base: angle, isone, iszero, isinf, isfinite, abs, one, oneunit, zero, isless, inv, +import Base: angle, isone, iszero, isinf, isfinite, isnan, isreal, abs, one, oneunit, zero, isless, isequal, inv, +, -, *, /, ^, ==, <, ≤, >, ≥, fld, cld, div, mod, rem, divrem, min, max, sign, signbit, isapprox, string, show, promote_rule, convert, getindex, tryparse, conj, @@ -12,11 +12,17 @@ export ∞, ℵ₀, ℵ₁, RealInfinity, ComplexInfinity, InfiniteCardinal, N # export Infinity """ -NotANumber() + NotANumber() -represents something that is undefined, for example, `0 * ∞`. +Construct the undefined value, for example the result of `0 * ∞`. + +Every float type has a `NaN` of its own. This one belongs to none of them. """ -struct NotANumber <: Number end +struct NotANumber <: Real end + +(::Type{T})(::NotANumber) where {T<:AbstractFloat} = T(NaN) +float(::NotANumber) = NaN +Base.hash(::NotANumber, h::UInt)::UInt = hash(NaN, h) """ diff --git a/src/compare.jl b/src/compare.jl index b117798..7f77384 100644 --- a/src/compare.jl +++ b/src/compare.jl @@ -17,6 +17,31 @@ _isinf(x::Number, y::AllInfinities) = isinf(x) && _angle(x) == angle(y) # `signbit(y)` is constant, so the branch folds away and the check becomes a single instruction. _isinf(x::Real, y::AllRealInfinities) = isinf(x) && (signbit(y) ? x < zero(x) : x > zero(x)) +# NotANumber +# Undefined compares false against everything, itself included, as `NaN` does. +for op in (:(==), :<, :≤, :>, :≥) + for Typ in (NotANumberRivals..., NotANumberComplexRivals...) + @eval $op(::NotANumber, ::$Typ) = false + @eval $op(::$Typ, ::NotANumber) = false + end + @eval $op(::NotANumber, ::NotANumber) = false +end + +# `isequal` and `hash` still have to identify it, so that a container can hold one. +isequal(::NotANumber, ::NotANumber) = true +for Typ in (NotANumberRivals..., NotANumberComplexRivals...) + @eval isequal(::NotANumber, y::$Typ) = isnan(y) + @eval isequal(x::$Typ, ::NotANumber) = isnan(x) +end + +# The sort order puts it after every value, `NaN` included, so that sorting stays total. +# `isless` is the only operator for which `AllRealInfinities` is not covered by a wider slot. +isless(::NotANumber, ::NotANumber) = false +for Typ in (NotANumberRivals..., NotANumberComplexRivals..., AllRealInfinities) + @eval isless(::NotANumber, ::$Typ) = false + @eval isless(x::$Typ, ::NotANumber) = !isnan(x) +end + # == @inline _eq(x, y::InfiniteCardinal) = x == ∞ && y == ℵ₀ @inline _eq(x, y::AllInfinities) = _isinf(x, y) diff --git a/src/interface.jl b/src/interface.jl index 958883f..7bcabbf 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -7,12 +7,31 @@ iszero(::AllInfinities) = false isinf(::AllInfinities) = true isfinite(::AllInfinities) = false +# `NotANumber` is no value at all, so it is neither finite nor infinite. +isnan(::NotANumber) = true +isinf(::NotANumber) = false +isfinite(::NotANumber) = false +iszero(::NotANumber) = false +isone(::NotANumber) = false +isinteger(::NotANumber) = false +signbit(::NotANumber) = false + +# Undefined wins against every second argument, so it needs a method wherever `Base` or this +# package owns a slot of its own. A narrower slot simply beats a wider one. `Number` looks +# redundant against the types here, but without it a foreign `Number` reaches `Base`'s +# promoting fallback instead. +const NotANumberRivals = (Number, Real, AbstractFloat, AbstractIrrational, AllInfinities, + IntegerInfinities, RealInfinity, + InfiniteCardinal) +# A complex operand makes the undefined result complex, as it does over the floats. +const NotANumberComplexRivals = (Complex, Complex{Bool}, ComplexInfinity) + # `InfiniteCardinal` is absent because `Base` already returns `true` for it through `Integer`. isinteger(::Union{Infinity, RealInfinity, ComplexInfinity}) = false for f in (:round, :floor, :ceil, :trunc) - @eval $f(x::AllInfinities; kwargs...) = x + @eval $f(x::Union{AllInfinities, NotANumber}; kwargs...) = x end -round(x::AllInfinities, ::RoundingMode; kwargs...) = x +round(x::Union{AllInfinities, NotANumber}, ::RoundingMode; kwargs...) = x # `Infinity` is positive, so it has no common type with `NegativeInfinity` (as is already the case for `PositiveInfinity`). promote_rule(::Type{Infinity}, ::Type{PositiveInfinity}) = PositiveInfinity diff --git a/test/runtests.jl b/test/runtests.jl index 3504469..fde50cd 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -578,6 +578,42 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], end end + @testset "NotANumber" begin + nan = NotANumber() + # every operand it can meet, itself included + operands = (nan, 0, 1.5, ∞, +∞, -∞, ℵ₀, ComplexInfinity(), NaN, NaN32) + @test isnan(nan) && !isinf(nan) && !isfinite(nan) && !iszero(nan) && !isone(nan) && !signbit(nan) + @test !isinteger(nan) + # a `NaN` of any real type is real, and this is the type-independent one + @test isreal(nan) + for f in (round, floor, ceil, trunc) + @test f(nan) ≡ f(nan; digits=2) ≡ nan + end + for r in (RoundNearest, RoundUp, RoundDown, RoundToZero) + @test round(nan, r) ≡ round(nan, r; digits=2) ≡ nan + end + + # a numeric comparison is false in every direction, against itself included + for op in (==, <, ≤, >, ≥), x in operands + @test !op(nan, x) && !op(x, nan) + end + + # `isequal` and `hash` still identify it, as they do for `NaN` + @test isequal(nan, nan) && isequal(nan, NaN) && isequal(NaN32, nan) + @test hash(nan) == hash(NaN) + + # the sort order puts it last, alongside `NaN` + @test sort([1.0, nan, ∞, -∞])[end] ≡ nan + for x in operands + @test !isless(nan, x) && isless(x, nan) == !isnan(x) + end + + # it converts to the `NaN` of whichever float type is asked for + @test Float64(nan) ≡ float(nan) ≡ NaN + @test Float32(nan) ≡ NaN32 && Float16(nan) ≡ NaN16 + @test isnan(BigFloat(nan)) + end + @testset "ordinary values" begin for inf in (∞, +∞, ℵ₀) @test 1.0 < inf && !(inf < 1.0) && 1.0 ≤ inf && inf ≥ 1.0 From 51de456487fd8991cbeda40ef0a3ed6cc6aeba60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4cker?= Date: Wed, 2 Sep 2026 14:37:03 +0200 Subject: [PATCH 13/18] Propagate `NotANumber` through the arithmetic Every operation on it was a MethodError or an ErrorException, so an undefined result could not be carried any further. --- src/algebra.jl | 30 +++++++++++++++++++++++++++++- src/compare.jl | 5 +++++ test/runtests.jl | 16 +++++++++++++++- 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/algebra.jl b/src/algebra.jl index aef2ffb..85aa74c 100644 --- a/src/algebra.jl +++ b/src/algebra.jl @@ -138,4 +138,32 @@ end # inv inv(::Union{Infinity,InfiniteCardinal}) = 0 inv(x::RealInfinity) = inv(float(x)) -inv(x::ComplexInfinity) = zero(ComplexF64) \ No newline at end of file +inv(x::ComplexInfinity) = zero(ComplexF64) + + +# NotANumber +# Anything computed from an undefined value is undefined again, as it is for `NaN`. +for op in (:+, :-, :*, :/, :^, :div, :fld, :cld, :mod, :rem, :min, :max) + for Typ in NotANumberRivals + @eval $op(x::NotANumber, ::$Typ) = x + @eval $op(::$Typ, y::NotANumber) = y + end + for Typ in NotANumberComplexRivals + @eval $op(::NotANumber, ::$Typ) = complex(NotANumber(), NotANumber()) + @eval $op(::$Typ, ::NotANumber) = complex(NotANumber(), NotANumber()) + end + @eval $op(x::NotANumber, ::NotANumber) = x +end +for Typ in NotANumberRivals + @eval divrem(x::NotANumber, ::$Typ) = (x, x) + @eval divrem(::$Typ, y::NotANumber) = (y, y) +end +divrem(x::NotANumber, ::NotANumber) = (x, x) +# `Base` has its own `^(::Number, ::Integer)`, which a literal exponent also routes through. +^(x::NotANumber, ::Integer) = x +^(::Integer, y::NotANumber) = y +^(x::NotANumber, ::Rational) = x +^(::Irrational{:ℯ}, y::NotANumber) = y +for f in (:+, :-, :abs, :inv, :sign, :conj) + @eval $f(x::NotANumber) = x +end \ No newline at end of file diff --git a/src/compare.jl b/src/compare.jl index 7f77384..a3bb812 100644 --- a/src/compare.jl +++ b/src/compare.jl @@ -57,6 +57,11 @@ end isapprox(x::AllInfinities, y::Number; kwargs...) = x == y isapprox(x::Number, y::AllInfinities; kwargs...) = x == y isapprox(x::AllInfinities, y::AllInfinities; kwargs...) = x == y +for Typ in (Number, AllInfinities) + @eval isapprox(::NotANumber, ::$Typ; kwargs...) = false + @eval isapprox(::$Typ, ::NotANumber; kwargs...) = false +end +isapprox(::NotANumber, ::NotANumber; kwargs...) = false # isless # `isless` is the sort order. `NaN` sorts after every other value, infinities included. diff --git a/test/runtests.jl b/test/runtests.jl index fde50cd..b44591f 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -594,7 +594,7 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], end # a numeric comparison is false in every direction, against itself included - for op in (==, <, ≤, >, ≥), x in operands + for op in (==, <, ≤, >, ≥, isapprox), x in operands @test !op(nan, x) && !op(x, nan) end @@ -612,6 +612,20 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], @test Float64(nan) ≡ float(nan) ≡ NaN @test Float32(nan) ≡ NaN32 && Float16(nan) ≡ NaN16 @test isnan(BigFloat(nan)) + + # anything computed from it is undefined again + for op in (+, -, *, /, ^, div, fld, cld, mod, rem, min, max), x in operands + @test op(nan, x) ≡ op(x, nan) ≡ nan + end + # a complex operand makes the undefined result complex, as it does over the floats + for op in (+, -, *, /, ^, div, fld, cld, mod, rem, min, max), x in (complex(1.0, 2.0), complex(true, false)) + @test op(nan, x) ≡ op(x, nan) ≡ complex(nan, nan) + end + for x in operands + @test divrem(nan, x) ≡ divrem(x, nan) ≡ (nan, nan) + end + @test nan^(1//2) ≡ (1//2)^nan ≡ ℯ^nan ≡ nan + @test -nan ≡ +nan ≡ abs(nan) ≡ inv(nan) ≡ sign(nan) ≡ conj(nan) ≡ nan end @testset "ordinary values" begin From 3db34a197d1cd9cb23cd617bc9e1e4301ce976bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4cker?= Date: Wed, 2 Sep 2026 16:50:54 +0200 Subject: [PATCH 14/18] Return `NotANumber()` where the sum or product is undefined 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 returns -2.0 for mod(-2.0, Inf) rather than NaN. --- src/algebra.jl | 4 ++-- test/runtests.jl | 20 +++++++------------- test/test_cardinality.jl | 2 +- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/src/algebra.jl b/src/algebra.jl index 85aa74c..063ef08 100644 --- a/src/algebra.jl +++ b/src/algebra.jl @@ -31,7 +31,7 @@ @inline toinf(x::Complex) = ComplexInfinity(_sb(x)) @inline toinf(x::ComplexInfinity) = x -@inline _infadd(x, y) = angle(x) == angle(y) ? y : throw(ArgumentError("Angles must be the same to add ∞")) +@inline _infadd(x, y) = angle(x) == angle(y) ? y : NotANumber() @inline __add(x, y::AllInfinities) = isinf(x) ? _infadd(toinf(x), y) : y @inline __add(x::Integer, y::InfiniteCardinal) = max(x, y) @@ -62,7 +62,7 @@ @inline function _mul(x, y) isnan(x) && return x - iszero(x) && throw(ArgumentError("Cannot multiply $x * $y")) + iszero(x) && return NotANumber() __mul(infpromote(x, y)...) end diff --git a/test/runtests.jl b/test/runtests.jl index b44591f..e434fd1 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -44,7 +44,7 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], @test ∞ - 1 ≡ ∞ - 1.0 ≡ ∞ @test *(∞) ≡ ∞ @test ∞*∞ ≡ ∞ - @test_throws ArgumentError ∞ - ∞ + @test ∞ - ∞ ≡ NotANumber() @test one(∞) ≡ one(Infinity) ≡ oneunit(∞) ≡ oneunit(Infinity) ≡ 1 @test zero(∞) ≡ 0 @@ -170,23 +170,17 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], @test (1∞) + (1∞) ≡ 1∞ @test ∞ + (1∞) ≡ (1∞) + ∞ ≡ 1∞ - @test_throws ArgumentError ∞ + (-∞) - @test_throws ArgumentError (1∞) + (-∞) - @test_throws ArgumentError (-∞) + ∞ + @test ∞ + (-∞) ≡ (1∞) + (-∞) ≡ (-∞) + ∞ ≡ NotANumber() @test ∞ - (-∞) ≡ +∞ @test (-∞) - ∞ ≡ -∞ @test (1∞) - (-∞) ≡ 1∞ @test (-∞) - (1∞) ≡ -∞ - @test_throws ArgumentError ∞ - (1∞) - @test_throws ArgumentError (1∞) - ∞ - @test_throws ArgumentError (1∞) - (1∞) - @test_throws ArgumentError (-∞) - (-∞) - @test_throws ArgumentError 0*∞ - @test_throws ArgumentError 0*(-∞) - @test_throws ArgumentError Inf - RealInfinity() - @test_throws ArgumentError RealInfinity() - Inf + # summing opposite directions is undefined, as it is over the floats + @test ∞ - (1∞) ≡ (1∞) - ∞ ≡ (1∞) - (1∞) ≡ (-∞) - (-∞) ≡ NotANumber() + @test Inf - RealInfinity() ≡ RealInfinity() - Inf ≡ NotANumber() + @test 0*∞ ≡ 0*(-∞) ≡ NotANumber() @test (-∞)*2 ≡ 2*(-∞) ≡ -2 * ∞ ≡ ∞ * (-2) ≡ (-2) * RealInfinity() ≡ -∞ @test (-∞)*2.3 ≡ 2.3*(-∞) ≡ -2.3 * ∞ ≡ ∞ * (-2.3) ≡ (-2.3) * RealInfinity() ≡ -∞ @@ -284,7 +278,7 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], @test complex(-Inf, 0.0) + (-∞) ≡ -ComplexInfinity() @test complex(0.0, Inf) + im*∞ ≡ im*∞ @test complex(0.0, -Inf) + (-im*∞) ≡ -im*∞ - @test_throws ArgumentError complex(0.0, Inf) + ∞ + @test complex(0.0, Inf) + ∞ ≡ NotANumber() # two infinite parts are the only way an infinite `Complex` points off the axes for (z, inf) in ((complex(Inf, Inf), (1+im)*∞), (complex(-Inf, Inf), (-1+im)*∞), (complex(-Inf, -Inf), (-1-im)*∞), (complex(Inf, -Inf), (1-im)*∞)) diff --git a/test/test_cardinality.jl b/test/test_cardinality.jl index 16fb680..db7a516 100644 --- a/test/test_cardinality.jl +++ b/test/test_cardinality.jl @@ -130,7 +130,7 @@ Base.getindex(::InfVector, ::InfiniteCardinal{0}) = 42 @test 5 + ℵ₀ ≡ ℵ₀ + 5 ≡ ℵ₀ @test ℵ₀ - 5 ≡ ℵ₀ @test 5 - ℵ₀ ≡ -∞ - @test_throws ArgumentError ℵ₀ - ℵ₀ + @test ℵ₀ - ℵ₀ ≡ checked_sub(ℵ₀, ℵ₀) ≡ NotANumber() @test -ℵ₀ ≡ -∞ @test *(ℵ₀) ≡ ℵ₀ From e8091062e29e5cf521200a15a1cc64f2236eb7a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4cker?= Date: Fri, 4 Sep 2026 14:14:28 +0200 Subject: [PATCH 15/18] Return `NotANumber()` when a `NaN` meets an infinity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/algebra.jl | 22 +++++++++++----------- test/runtests.jl | 8 +++++--- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/algebra.jl b/src/algebra.jl index 063ef08..ba07fe1 100644 --- a/src/algebra.jl +++ b/src/algebra.jl @@ -36,8 +36,8 @@ @inline __add(x, y::AllInfinities) = isinf(x) ? _infadd(toinf(x), y) : y @inline __add(x::Integer, y::InfiniteCardinal) = max(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)...) +# A `NaN` argument makes the result undefined. Types with no `NaN` fold the test away. +@inline _add(x, y) = isnan(x) ? NotANumber() : __add(infpromote(x, y)...) +(x::Number, y::AllInfinities) = _add(x, y) +(x::AllInfinities, y::Number) = _add(y, x) @@ -61,7 +61,7 @@ @inline __mul(x::Integer, y::InfiniteCardinal) = x > 0 ? y : throw(ArgumentError("Cannot multiply $x * $y")) @inline function _mul(x, y) - isnan(x) && return x + isnan(x) && return NotANumber() iszero(x) && return NotANumber() __mul(infpromote(x, y)...) end @@ -87,7 +87,7 @@ end # mod @inline function _mod(x::Real, y::IntegerInfinities) - isnan(x) && return x + isnan(x) && return NotANumber() signbit(x) == signbit(y) || throw(ArgumentError("mod($x,$y) is unbounded")) x end @@ -97,7 +97,7 @@ mod(::IntegerInfinities, ::IntegerInfinities) = NotANumber() # rem, divrem # `rem` keeps the sign of the dividend, so unlike `mod` it stays bounded either way. -rem(x::Real, ::IntegerInfinities) = x +rem(x::Real, ::IntegerInfinities) = isnan(x) ? NotANumber() : x rem(::IntegerInfinities, ::Real) = NotANumber() rem(::IntegerInfinities, ::IntegerInfinities) = NotANumber() # `Base` computes the remainder of two `Integer`s as `a - div(a,b)*b`, which an `InfiniteCardinal` cannot evaluate. @@ -106,14 +106,14 @@ divrem(x::IntegerInfinities, y::Real) = (div(x, y), rem(x, y)) divrem(x::IntegerInfinities, y::IntegerInfinities) = (div(x, y), rem(x, y)) # fld, cld, div -_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) +_divinf(x) = isnan(x) ? NotANumber() : zero(x) +_fldinf(x) = isnan(x) ? NotANumber() : signbit(x) ? -one(x) : zero(x) +_cldinf(x) = isnan(x) ? NotANumber() : 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) = isnan(y) ? y : signbit(y) ? -x : x +_inffcd(x, y) = isnan(y) ? NotANumber() : signbit(y) ? -x : x for OP in (:fld,:cld,:div) @eval begin $OP(x::IntegerInfinities, y::Real) = _inffcd(x, y) @@ -124,9 +124,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) = isnan(p) ? p : ifelse(iszero(p), one(p), ifelse(p > 0, +∞, +zero(p))) +_infpow(::PositiveInfinity, p) = isnan(p) ? NotANumber() : ifelse(iszero(p), one(p), ifelse(p > 0, +∞, +zero(p))) function _infpow(x::NegativeInfinity, p) - isnan(p) && return p + isnan(p) && return NotANumber() !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/test/runtests.jl b/test/runtests.jl index e434fd1..d71a2ce 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -559,16 +559,18 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], end @testset "NaN arithmetic" begin + # the result is the package's own undefined value, as `Inf + ∞` is its own infinity 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)) + @test op(nan, inf) ≡ op(inf, nan) ≡ NotANumber() end - @test isnan(mod(nan, inf)) # the other direction is `NotANumber` for every argument + # `mod(inf, x)` discards `x`, so only one order is needed + @test mod(nan, inf) ≡ NotANumber() end for nan in (NaN, NaN32, NaN16, big(NaN)), inf in (+∞, -∞) - @test isnan(inf^nan) + @test inf^nan ≡ NotANumber() end end From 9722dbffa312a5e3283f53812a79c0f0e392b694 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4cker?= Date: Sun, 6 Sep 2026 12:28:05 +0200 Subject: [PATCH 16/18] Hold the direction of a `ComplexInfinity` as a fixed-point turn count MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A direction is an angle modulo a full turn, so the field now counts turns in units of 2^-64 and wraps where the circle does. Every `UInt64` names a direction and every direction has exactly one count, which the old half-turn field did not manage: half turns of 0.5 and 2.5 pointed the same way yet compared unequal and hashed apart. Wrapping also makes the group operation a machine add, and the resolution is uniform instead of thinning out towards a full turn. The count is what the constructor takes. An angle has to be rounded to reach it, so that step is now named at the call site with the `halfturns` keyword, and the old `ComplexInfinity(0.5)` is a `MethodError` rather than a silent reinterpretation. Most code needs neither form, since multiplying by `∞` takes the direction from the other operand: `im*∞` and `(1+im)*∞`. The element type carried no information about the value, only about how the direction had been spelled, so it is gone. It had been standing in for "this infinity lies on the real axis", and that was never what it meant: a zero angle written as a float pointed along the positive real axis just as `ComplexInfinity()` does, yet only the latter could be ordered or divided. The union types that used the parameter as a proxy now leave a `ComplexInfinity` out entirely, so the complex plane carries no order and takes no integer operation, exactly as `Base` treats a `Complex`. The count has 64 bits and an angle in a `Float64` has 53, so the two are kept apart wherever the difference shows. Equality and `hash` read the count, never the angle. `angle` reports on `Base`'s branch of `(-π, π]`. And `show` gives the readable `cispi(h)∞` only where that reads back, and the count itself otherwise, so every printed form evaluates to the value it came from. --- src/Infinities.jl | 91 ++++++++++++++-------- src/algebra.jl | 22 +++--- src/ambiguities.jl | 9 +-- src/cardinality.jl | 3 + src/compare.jl | 4 + src/interface.jl | 12 ++- test/runtests.jl | 173 ++++++++++++++++++++++++----------------- test/test_ambiguity.jl | 13 +++- 8 files changed, 197 insertions(+), 130 deletions(-) diff --git a/src/Infinities.jl b/src/Infinities.jl index 27bc2d8..62a1b7a 100644 --- a/src/Infinities.jl +++ b/src/Infinities.jl @@ -96,57 +96,85 @@ zero(::Type{RealInfinity}) = 0.0 # ComplexInfinity ####### -# angle is π*a where a is (false==0) and (true==1) - """ -ComplexInfinity(signbit) + ComplexInfinity(turns::UInt64) + ComplexInfinity(; halfturns::Real = 0) + +Construct an infinity in the complex plane, pointing in a direction held as a count of +`2^-64` turns. + +The count wraps at a full turn, so the stored `UInt64` and the directions are bijective. +`0x0` points along the positive real axis. Values increase counterclockwise. +`0x8000000000000000` points along the negative real axis. Use `reinterpret(UInt64, x)` to +read out the exact value. -represents an infinity in the complex plane with the angle -specified by `π * signbit`. The use of the name `signbit` is -for consistency with `RealInfinity`. +Multiplying by `∞` takes the direction from the other operand, which usually reads better +than naming an angle: + + im*∞ # cispi(0.5)∞ + (1+im)*∞ # cispi(0.25)∞ + exp(im*π/4)*∞ # the same direction again + +Those forms and the `halfturns` keyword go through `angle`, so they round. It is exact on +the axes and at a quarter turn, but `exp(im*π/8)*∞` lands 256 counts past an eighth turn. +Provide the `UInt64` when you have an off-axis value where accuracy matters. """ -struct ComplexInfinity{T<:Real} <: Number - signbit::T +struct ComplexInfinity <: Number + turns::UInt64 + ComplexInfinity(turns::UInt64) = new(turns) end -ComplexInfinity{T}() where T = ComplexInfinity(zero(T)) -ComplexInfinity() = ComplexInfinity{Bool}() -ComplexInfinity{T}(::Infinity) where T<:Real = ComplexInfinity{T}() +# A full turn fills the `UInt64` range, so a half turn is 2^63 units. +const _HALFTURN = UInt64(2)^63 # the negative real axis +# `_turns` and `_halfturns` are inverse: half turns in, count out, and back again. +# `mod` returns 2 itself for a tiny negative angle, since 2 + x rounds back to 2. A full +# turn is the direction zero. Testing `== 2` rather than `< 2` still lets `NaN` throw. +@inline _turns(halfturns::Real) = round(UInt64, (h = mod(halfturns, 2); h == 2 ? zero(h) : h) * 0x1p63) +# Scaling by 2^63 needs 63 bits beyond the numerator, so `Int128` has room for any `Int64`. +_turns(halfturns::Rational) = round(BigInt, mod(halfturns, 2) * big(2)^63) % UInt64 +_turns(halfturns::Rational{<:Base.BitInteger64}) = + round(Int128, mod(halfturns, 2) * Int128(2)^63) % UInt64 +# `Base` puts an angle in `(-π, π]`, so past the half turn the count reads as negative. +@inline _halfturns(x::ComplexInfinity) = + x.turns == _HALFTURN ? 1.0 : reinterpret(Int64, x.turns) / 0x1p63 + +ComplexInfinity(; halfturns::Real = 0) = ComplexInfinity(_turns(halfturns)) 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(x.signbit)) # ambiguity fix +ComplexInfinity(x::RealInfinity) = ComplexInfinity(_directionof(x)) +ComplexInfinity(x::ComplexInfinity) = x -signbit(y::ComplexInfinity) = mod(y.signbit, 2) == 1 +signbit(y::ComplexInfinity) = y.turns == _HALFTURN -convert(::Type{ComplexInfinity{T}}, ::Infinity) where T = ComplexInfinity{T}() convert(::Type{ComplexInfinity}, ::Infinity) = ComplexInfinity() -convert(::Type{ComplexInfinity{T}}, x::RealInfinity) where T = ComplexInfinity{T}(x) convert(::Type{ComplexInfinity}, x::RealInfinity) = ComplexInfinity(x) -sign(y::ComplexInfinity{<:Integer}) = mod(y.signbit, 2) == 0 ? 1 : -1 -sign(y::ComplexInfinity) = cispi(y.signbit) -angle(x::ComplexInfinity) = π*x.signbit +sign(y::ComplexInfinity) = cispi(_halfturns(y)) +angle(x::ComplexInfinity) = _halfturns(x) * π abs(::ComplexInfinity) = ∞ -conj(y::ComplexInfinity{<:Integer}) = y # an integer factor points along the real axis -conj(y::ComplexInfinity) = ComplexInfinity(mod(-y.signbit, 2)) +conj(y::ComplexInfinity) = ComplexInfinity(-y.turns) # An exact zero has to stay finite, `Inf * 0` being a `NaN`. @inline _ray(c) = iszero(c) ? c : copysign(Inf, c) # `Complex` reaches only the eight rays of its two saturating parts, so the direction lands on the nearest of them. function float(x::ComplexInfinity) - s, c = sincospi(x.signbit) + s, c = sincospi(_halfturns(x)) complex(_ray(c), _ray(s)) end -show(io::IO, x::ComplexInfinity) = print(io, "exp($(x.signbit)*im*π)∞") +# The readable form names an angle, which recovers most counts but not all, so it is used +# only where reading it back gives the same direction. +function show(io::IO, x::ComplexInfinity) + h = _halfturns(x) + _directionof(cispi(h)) == x.turns ? print(io, "cispi($h)∞") : + print(io, "ComplexInfinity(", repr(x.turns), ")") +end -one(::Type{<:ComplexInfinity}) = one(ComplexF64) -oneunit(::Type{<:ComplexInfinity}) = oneunit(ComplexF64) +one(::Type{ComplexInfinity}) = one(ComplexF64) +oneunit(::Type{ComplexInfinity}) = oneunit(ComplexF64) oneunit(::ComplexInfinity) = oneunit(ComplexF64) zero(::ComplexInfinity) = zero(ComplexF64) -zero(::Type{<:ComplexInfinity}) = zero(ComplexF64) +zero(::Type{ComplexInfinity}) = zero(ComplexF64) # `isequal` implies equal hashes, so the infinities have to hash like the float @@ -156,12 +184,11 @@ Base.hash(::Infinity, h::UInt)::UInt = hash(Inf, h) Base.hash(::PositiveInfinity, h::UInt)::UInt = hash(Inf, h) Base.hash(::NegativeInfinity, h::UInt)::UInt = hash(-Inf, h) -# Equality of ComplexInfinity is equality of the angle, hence so is the hash. +# The two real directions have to hash like the real infinities they compare equal to. function Base.hash(x::ComplexInfinity, h::UInt)::UInt - θ = angle(x) - θ == angle(PositiveInfinity()) && return hash(Inf, h) - θ == angle(NegativeInfinity()) && return hash(-Inf, h) - hash(ComplexInfinity, hash(θ, h)) + iszero(x.turns) && return hash(Inf, h) + x.turns == _HALFTURN && return hash(-Inf, h) + hash(ComplexInfinity, hash(x.turns, h)) end diff --git a/src/algebra.jl b/src/algebra.jl index ba07fe1..7d21527 100644 --- a/src/algebra.jl +++ b/src/algebra.jl @@ -15,20 +15,14 @@ +(::Infinity) = RealInfinity() -(::Infinity) = RealInfinity(true) -(y::RealInfinity) = RealInfinity(!signbit(y)) --(y::ComplexInfinity{B}) where B<:Integer = sign(y) == 1 ? ComplexInfinity(one(B)) : ComplexInfinity(zero(B)) --(y::ComplexInfinity) = ComplexInfinity(mod(y.signbit + 1, 2)) +-(y::ComplexInfinity) = ComplexInfinity(y.turns ⊻ _HALFTURN) +(x::InfiniteCardinal) = x -(::InfiniteCardinal) = -∞ # addition -@inline _sb(x) = signbit(x) -@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 toinf(x) = RealInfinity(signbit(x)) -# The field counts half turns, so the radians of `angle` have to be scaled. -@inline toinf(x::Complex) = ComplexInfinity(_sb(x)) +@inline toinf(x::Complex) = ComplexInfinity(_directionof(x)) @inline toinf(x::ComplexInfinity) = x @inline _infadd(x, y) = angle(x) == angle(y) ? y : NotANumber() @@ -54,10 +48,14 @@ # multiplication -@inline __mul(x, y::AllInfinities) = RealInfinity(_sb(x) ⊻ _sb(y)) -@inline __mul(x, y::ComplexInfinity) = ComplexInfinity(_sb(x) + _sb(y)) -@inline __mul(x, y::ComplexInfinity{Bool}) = ComplexInfinity(_sb(x) ⊻ _sb(y)) -@inline __mul(x::Complex, y::ComplexInfinity{Bool}) = ComplexInfinity(_sb(x) + _sb(y)) +# The count of the direction a value points in. `_turns` instead reads its argument as a +# number of half turns. +@inline _directionof(x::Real) = signbit(x) ? _HALFTURN : zero(UInt64) +@inline _directionof(x::Complex) = _turns(angle(x) / π) # overloading `signbit` causes type piracy +@inline _directionof(x::ComplexInfinity) = x.turns + +@inline __mul(x, y::AllInfinities) = RealInfinity(signbit(x) ⊻ signbit(y)) +@inline __mul(x, y::ComplexInfinity) = ComplexInfinity(_directionof(x) + _directionof(y)) @inline __mul(x::Integer, y::InfiniteCardinal) = x > 0 ? y : throw(ArgumentError("Cannot multiply $x * $y")) @inline function _mul(x, y) diff --git a/src/ambiguities.jl b/src/ambiguities.jl index 4a80a6b..10b8ed0 100644 --- a/src/ambiguities.jl +++ b/src/ambiguities.jl @@ -1,10 +1,9 @@ for Typ in (Base.TwicePrecision, AbstractChar, Complex) - @eval begin - RealInfinity(x::$Typ) = throw(MethodError(RealInfinity, x)) - ComplexInfinity{T}(x::$Typ) where T<:Real = ComplexInfinity(T(x)) - end + @eval RealInfinity(x::$Typ) = throw(MethodError(RealInfinity, x)) end -ComplexInfinity{T}(x::ComplexInfinity{T}) where T<:Real = x +# `Base` builds a `TwicePrecision` of any type by adding its two halves, which for two opposed +# directions is undefined. +ComplexInfinity(x::Base.TwicePrecision) = ComplexInfinity(halfturns = Float64(x)) for Typ in (Rational, BigInt, BigFloat) for (op, fop) in ((:<, :_lt), (:≤, :_le)) diff --git a/src/cardinality.jl b/src/cardinality.jl index bd71de2..f18277b 100644 --- a/src/cardinality.jl +++ b/src/cardinality.jl @@ -41,6 +41,9 @@ function Integer(x::ComplexInfinity) ℵ₀ end +# Every cardinal is a positive real infinity, so it points along the positive real axis. +ComplexInfinity(::InfiniteCardinal) = ComplexInfinity() + Base.to_index(::Union{Infinity,InfiniteCardinal{0}}) = ℵ₀ Base.to_shape(::Union{Infinity,InfiniteCardinal{0}}) = ℵ₀ diff --git a/src/compare.jl b/src/compare.jl index a3bb812..42a5b56 100644 --- a/src/compare.jl +++ b/src/compare.jl @@ -16,6 +16,10 @@ _isinf(x::Number, y::AllInfinities) = isinf(x) && _angle(x) == angle(y) # On the real line the direction is a comparison against zero. # `signbit(y)` is constant, so the branch folds away and the check becomes a single instruction. _isinf(x::Real, y::AllRealInfinities) = isinf(x) && (signbit(y) ? x < zero(x) : x > zero(x)) +# A direction in the plane is decided by the count, which is exact where an angle in a +# `Float64` is not: the count has 64 bits and the angle has 53. +_isinf(x::Number, y::ComplexInfinity) = isinf(x) && _directionof(x) == y.turns +_isinf(x::ComplexInfinity, y::AllRealInfinities) = x.turns == _directionof(y) # NotANumber # Undefined compares false against everything, itself included, as `NaN` does. diff --git a/src/interface.jl b/src/interface.jl index 7bcabbf..ba25548 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -1,7 +1,7 @@ const AllInfinities = Union{Infinity, RealInfinity, ComplexInfinity, InfiniteCardinal} -const AllRealInfinities = Union{Infinity, RealInfinity, ComplexInfinity{<:Integer}} -const IntegerInfinities = Union{Infinity, RealInfinity, ComplexInfinity{<:Integer}, InfiniteCardinal} -const ExtendedComplex{T} = Union{Complex{T}, ComplexInfinity{T}} +const AllRealInfinities = Union{Infinity, RealInfinity} +const IntegerInfinities = Union{Infinity, RealInfinity, InfiniteCardinal} +const ExtendedComplex = Union{Complex, ComplexInfinity} iszero(::AllInfinities) = false isinf(::AllInfinities) = true @@ -35,10 +35,8 @@ round(x::Union{AllInfinities, NotANumber}, ::RoundingMode; kwargs...) = x # `Infinity` is positive, so it has no common type with `NegativeInfinity` (as is already the case for `PositiveInfinity`). promote_rule(::Type{Infinity}, ::Type{PositiveInfinity}) = PositiveInfinity -promote_rule(::Type{Infinity}, ::Type{ComplexInfinity{T}}) where T = ComplexInfinity{T} -promote_rule(::Type{<:RealInfinity}, ::Type{ComplexInfinity{T}}) where T = ComplexInfinity{T} -promote_rule(::Type{ComplexInfinity{T}}, ::Type{<:RealInfinity}) where T<:Integer = ComplexInfinity{T} -promote_rule(::Type{ComplexInfinity{T}}, ::Type{ComplexInfinity{S}}) where {T, S} = ComplexInfinity{promote_type(T, S)} +promote_rule(::Type{Infinity}, ::Type{ComplexInfinity}) = ComplexInfinity +promote_rule(::Type{<:RealInfinity}, ::Type{ComplexInfinity}) = ComplexInfinity function tryparse(::Type{NegativeInfinity}, s::AbstractString) i = findfirst(!isspace, s) diff --git a/test/runtests.jl b/test/runtests.jl index d71a2ce..90f4306 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -240,17 +240,45 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], end @testset "ComplexInfinity" begin + # every spelling of the positive real axis is the same value, a cardinal included @test ComplexInfinity(∞) ≡ convert(ComplexInfinity, ∞) ≡ ComplexInfinity() ≡ - ComplexInfinity(false) ≡ ComplexInfinity{Bool}(∞) ≡ ComplexInfinity{Bool}(RealInfinity()) ≡ ComplexInfinity{Bool}(ComplexInfinity()) - - @test convert(ComplexInfinity{Bool}, ∞) ≡ convert(ComplexInfinity, ∞) ≡ ComplexInfinity() - @test convert(ComplexInfinity{Bool}, -∞) ≡ convert(ComplexInfinity, -∞) ≡ -ComplexInfinity() + ComplexInfinity(0x0000000000000000) ≡ ComplexInfinity(RealInfinity()) ≡ + ComplexInfinity(ComplexInfinity()) ≡ ComplexInfinity(ℵ₀) + + @test convert(ComplexInfinity, -∞) ≡ -ComplexInfinity() + # one direction is one value, however it is spelled + @test ComplexInfinity(halfturns = -0.5) ≡ ComplexInfinity(halfturns = 1.5) ≡ -im*∞ + @test ComplexInfinity(halfturns = 1) ≡ ComplexInfinity(halfturns = 3) ≡ ComplexInfinity(-∞) + # a rational direction converts without a float step + @test reinterpret(UInt64, ComplexInfinity(halfturns = 2//3)) ≡ 0x5555555555555555 + # a numerator too wide for `Int128` takes the `BigInt` route to the same count + @test ComplexInfinity(halfturns = big(1)//3) ≡ ComplexInfinity(halfturns = 1//3) + @test ComplexInfinity(0x4000000000000000) ≡ ComplexInfinity(halfturns = 1//2) ≡ im*∞ + # `mod` rounds a hair below the axis up to a full turn, which has no count of its own + @test ComplexInfinity(halfturns = -1e-300) ≡ ComplexInfinity(halfturns = 2.0) ≡ ComplexInfinity() + @test complex(1.0, -1e-17)*∞ ≡ ComplexInfinity() + # no count names a direction that is not one, so the conversion has to refuse + for h in (NaN, Inf, -Inf) + @test_throws InexactError ComplexInfinity(halfturns = h) + end + # the count runs forwards, `angle` reports it on `Base`'s branch of `(-π, π]` + for h in (0.0, 0.25, 0.5, 1.0, -0.25, -0.5, -0.75) + @test angle(ComplexInfinity(halfturns = h)) ≡ h*π + @test complex(cospi(h), sinpi(h))*∞ == ComplexInfinity(halfturns = h) + end + # off the axes the angle has to be rounded to reach a count + @test reinterpret(UInt64, exp(im*π/8)*∞) - reinterpret(UInt64, ComplexInfinity(halfturns = 1//8)) ≡ + 0x0000000000000100 + @test angle(exp(im*0.3)*∞) ≈ angle(∞*exp(im*0.3)) ≈ 0.3 + # the count is finer than an angle in a `Float64`, so equality has to read the count + @test ComplexInfinity(0x7fffffffffffffff) ≠ -ComplexInfinity() + @test im*∞ * ComplexInfinity(0x0000000000000001) ≠ im*∞ @test isinf(ComplexInfinity()) @test !isfinite(ComplexInfinity()) @test promote(∞, RealInfinity(), ComplexInfinity()) ≡ ntuple(_ -> ComplexInfinity(), 3) - @test promote_type(Infinity, ComplexInfinity{Bool}) == promote_type(RealInfinity, ComplexInfinity{Bool}) == ComplexInfinity{Bool} + @test promote_type(Infinity, ComplexInfinity) == promote_type(RealInfinity, ComplexInfinity) == ComplexInfinity @test ComplexInfinity(∞) == ∞ @@ -268,10 +296,10 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], @test ComplexInfinity() + ∞ ≡ ComplexInfinity() + RealInfinity() ≡ ∞ + ComplexInfinity() ≡ RealInfinity() + ComplexInfinity() ≡ ComplexInfinity() - @test ComplexInfinity(true) + ComplexInfinity(true) == ComplexInfinity(true) - @test ComplexInfinity(false) + ComplexInfinity(false) == ComplexInfinity(false) - @test ComplexInfinity(true)+1 == ComplexInfinity(true) - @test ComplexInfinity(false)+1 == ComplexInfinity(false) + @test ComplexInfinity(-∞) + ComplexInfinity(-∞) == ComplexInfinity(-∞) + @test ComplexInfinity() + ComplexInfinity() == ComplexInfinity() + @test ComplexInfinity(-∞)+1 == ComplexInfinity(-∞) + @test ComplexInfinity()+1 == ComplexInfinity() # An infinite summand reaches `_infadd` through `toinf`, which has to give half turns @test complex(Inf, 0.0) + ∞ ≡ ComplexInfinity() @@ -288,7 +316,7 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], @test ∞ * ComplexInfinity() ≡ RealInfinity() * ComplexInfinity() ≡ ComplexInfinity() * ∞ ≡ ComplexInfinity() * RealInfinity() ≡ ComplexInfinity() - @test 2.0im*∞ ≡ ∞*2.0im ≡ 2.0im * RealInfinity() ≡ RealInfinity() * 2.0im ≡ ComplexInfinity(1/2) + @test 2.0im*∞ ≡ ∞*2.0im ≡ 2.0im * RealInfinity() ≡ RealInfinity() * 2.0im ≡ im*∞ @test 2ComplexInfinity() ≡ ComplexInfinity()*2 ≡ ComplexInfinity() # a factor gives the direction it actually has, so rescaling moves it once it rounds @test 4*(0.3+0.1im)*∞ ≡ (0.3+0.1im)*∞ @@ -301,68 +329,61 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], @test Inf == ComplexInfinity() @test ComplexInfinity() == Inf - @test isless(-ComplexInfinity(), ComplexInfinity()) - @test isless(5, ComplexInfinity()) - @test !isless(ComplexInfinity(), 5) - - @test 5 < ComplexInfinity() && 5 ≤ ComplexInfinity() - @test !(ComplexInfinity() < 5) && !(ComplexInfinity() ≤ 5) - @test 5 > -ComplexInfinity() && 5 ≥ -ComplexInfinity() - @test ComplexInfinity() > 5 && ComplexInfinity() ≥ 5 + # the complex plane carries no order, so these are undefined as they are for `Complex` + for op in (isless, <, ≤, >, ≥, min, max), y in (5, ComplexInfinity(), (1+im)*∞) + @test_throws MethodError op(ComplexInfinity(), y) + @test_throws MethodError op(y, ComplexInfinity()) + end @test 1 + ComplexInfinity() ≡ 1.0 + ComplexInfinity() ≡ ComplexInfinity() + 1 ≡ ComplexInfinity() + 1.0 ≡ ComplexInfinity() @test 5 * ComplexInfinity() ≡ ComplexInfinity() @test (-5) * ComplexInfinity() ≡ -ComplexInfinity() - @test ComplexInfinity(0.25) * ComplexInfinity(0.5) ≡ ComplexInfinity(0.75) - @test ComplexInfinity(0.0) + ComplexInfinity() ≡ ComplexInfinity() + ComplexInfinity(0.0) ≡ ComplexInfinity(0.0) - - @test mod(ComplexInfinity(), 5) ≡ NotANumber() + @test (1+im)*∞ * (im*∞) ≡ (-1+im)*∞ + @test (2.0+0.0im)*∞ + ComplexInfinity() ≡ ComplexInfinity() + (2.0+0.0im)*∞ ≡ ComplexInfinity() - @test stringmime("text/plain", ComplexInfinity()) == "exp(false*im*π)∞" - - @testset "min/max" begin - @test min(ComplexInfinity(), -ComplexInfinity()) ≡ -ComplexInfinity() - @test max(ComplexInfinity(), -ComplexInfinity()) ≡ ComplexInfinity() - @test min(ComplexInfinity(), 5) ≡ min(5,ComplexInfinity()) ≡ 5 - @test max(ComplexInfinity(), 5) ≡ max(5,ComplexInfinity()) ≡ ComplexInfinity() + @test stringmime("text/plain", ComplexInfinity()) == "cispi(0.0)∞" + # a count an angle cannot name is shown as itself, so every form reads back + @test sprint(show, ComplexInfinity(0x5555555555555555)) == "ComplexInfinity(0x5555555555555555)" + for u in (0x0000000000000000, 0x4000000000000000, 0x5555555555555555, 0xdeadbeefdeadbeef) + @test Core.eval(@__MODULE__, Meta.parse(sprint(show, ComplexInfinity(u)))) ≡ ComplexInfinity(u) end - @testset "fld/cld/div" begin - @test div(ComplexInfinity(), 5) ≡ fld(ComplexInfinity(), 5) ≡ ComplexInfinity() - @test div(-ComplexInfinity(),2) ≡ -ComplexInfinity() + @testset "integer operations" begin + # an integer operation needs a real, and `Base` defines none of these for a `Complex` + for op in (div, fld, cld, mod, rem), x in (ComplexInfinity(), (1+im)*∞) + @test_throws MethodError op(x, 5) + @test_throws MethodError op(5, x) + end end - @test signbit(ComplexInfinity(3)) - @test !signbit(ComplexInfinity(100)) + @test signbit(ComplexInfinity(halfturns = 3)) + @test !signbit(ComplexInfinity(halfturns = 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 + @test signbit(ComplexInfinity(-∞)) === signbit(-ComplexInfinity()) === true + @test signbit(im*∞) === signbit(ComplexInfinity()) === false @testset "abs/sign/conj/-" begin - @test -ComplexInfinity(0.5) ≡ ComplexInfinity(1.5) - @test -(-ComplexInfinity(0.5)) ≡ ComplexInfinity(0.5) - @test -ComplexInfinity() ≡ ComplexInfinity(true) - @test abs(ComplexInfinity()) ≡ abs(ComplexInfinity(0.5)) ≡ ∞ - @test sign(ComplexInfinity(0.5)) ≡ complex(0.0, 1.0) - @test sign(ComplexInfinity(0.0)) ≡ complex(1.0, 0.0) - @test sign(ComplexInfinity(1.0)) ≡ complex(-1.0, 0.0) - # an integer angle stays on the real line, where the sign is a real ±1 - @test sign(ComplexInfinity(false)) ≡ 1 - @test sign(ComplexInfinity(true)) ≡ -1 - # off the axes conjugation and negation part company: `-ComplexInfinity(0.25)` is `ComplexInfinity(1.25)` - @test conj(ComplexInfinity(0.25)) ≡ ComplexInfinity(1.75) - @test conj(conj(ComplexInfinity(0.25))) ≡ ComplexInfinity(0.25) - @test conj(ComplexInfinity(true)) ≡ ComplexInfinity(true) # the narrow type survives + @test -(im*∞) ≡ -im*∞ + @test -(-(im*∞)) ≡ im*∞ + @test -ComplexInfinity() ≡ ComplexInfinity(-∞) + @test abs(ComplexInfinity()) ≡ abs(im*∞) ≡ ∞ + @test sign(im*∞) ≡ complex(0.0, 1.0) + @test sign(ComplexInfinity()) ≡ complex(1.0, 0.0) + @test sign(ComplexInfinity(-∞)) ≡ complex(-1.0, 0.0) + # conjugation negates the direction, so on the real axis it changes nothing + @test conj((1+im)*∞) ≡ (1-im)*∞ + @test conj(conj((1+im)*∞)) ≡ (1+im)*∞ + @test conj(ComplexInfinity(-∞)) ≡ ComplexInfinity(-∞) end @testset "float" begin - @test float(ComplexInfinity()) ≡ float(ComplexInfinity(0.0)) ≡ complex(Inf, 0.0) - @test float(ComplexInfinity(1/2)) ≡ complex(0.0, Inf) - @test float(ComplexInfinity(1.0)) ≡ float(ComplexInfinity(true)) ≡ complex(-Inf, 0.0) - @test float(ComplexInfinity(-1/2)) ≡ float(ComplexInfinity(3/2)) ≡ complex(0.0, -Inf) + @test float(ComplexInfinity()) ≡ float((2.0+0.0im)*∞) ≡ complex(Inf, 0.0) + @test float(im*∞) ≡ complex(0.0, Inf) + @test float(ComplexInfinity(-∞)) ≡ complex(-Inf, 0.0) + @test float(-im*∞) ≡ float(ComplexInfinity(halfturns = 3/2)) ≡ complex(0.0, -Inf) # `Complex` points along eight rays only, so every other angle collapses onto the nearest - @test float(ComplexInfinity(1/4)) ≡ float(ComplexInfinity(0.3)) ≡ complex(Inf, Inf) + @test float((1+im)*∞) ≡ float(ComplexInfinity(0x1000000000000000)) ≡ complex(Inf, Inf) end end @@ -375,8 +396,11 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], @testset "hash" begin infinities = (∞, +∞, -∞, Inf, -Inf, Inf32, -Inf32, Inf16, -Inf16, big(Inf), -big(Inf), - InfiniteCardinal{0}(), ComplexInfinity(false), - ComplexInfinity(true), ComplexInfinity(0.1)) + InfiniteCardinal{0}(), ComplexInfinity(), + ComplexInfinity(-∞), ComplexInfinity(0x1000000000000000), + # counts that an angle in a `Float64` cannot tell apart + ComplexInfinity(0x7fffffffffffffff), + im*∞ * ComplexInfinity(0x0000000000000001)) # isequal must imply equal hashes for a in infinities, b in infinities @@ -417,9 +441,9 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], @test Base.literal_pow(^, -∞, Val(2)) ≡ (-∞)^2 ≡ +∞ @test Base.literal_pow(^, -∞, Val(-2)) ≡ (-∞)^(-2) ≡ 0.0 - @test Base.literal_pow(^, ComplexInfinity(0.1), Val(0)) ≡ ComplexInfinity(0.1)^0 ≡ 1.0+0.0im - @test Base.literal_pow(^, ComplexInfinity(0.1), Val(1)) ≡ (ComplexInfinity(0.1))^1 ≡ ComplexInfinity(0.1) - @test Base.literal_pow(^, ComplexInfinity(0.1), Val(-1)) ≡ (ComplexInfinity(0.1))^(-1) ≡ 0.0+0.0im + @test Base.literal_pow(^, ComplexInfinity(0x1000000000000000), Val(0)) ≡ ComplexInfinity(0x1000000000000000)^0 ≡ 1.0+0.0im + @test Base.literal_pow(^, ComplexInfinity(0x1000000000000000), Val(1)) ≡ (ComplexInfinity(0x1000000000000000))^1 ≡ ComplexInfinity(0x1000000000000000) + @test Base.literal_pow(^, ComplexInfinity(0x1000000000000000), Val(-1)) ≡ (ComplexInfinity(0x1000000000000000))^(-1) ≡ 0.0+0.0im end @testset "one/zero/oneunit" begin @@ -433,9 +457,9 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], end @testset "isinteger/round" begin - infinities = (∞, +∞, -∞, ℵ₀, ComplexInfinity(), ComplexInfinity(1/4)) + infinities = (∞, +∞, -∞, ℵ₀, ComplexInfinity(), (1+im)*∞) @test !isinteger(∞) && !isinteger(+∞) && !isinteger(-∞) - @test !isinteger(ComplexInfinity()) && !isinteger(ComplexInfinity(1/4)) + @test !isinteger(ComplexInfinity()) && !isinteger((1+im)*∞) @test isinteger(ℵ₀) # an `InfiniteCardinal` is an `Integer` @test ∞ ∉ 1:5 # `in` asks a range for `isinteger` before comparing for f in (round, floor, ceil, trunc), x in infinities @@ -452,7 +476,7 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], # a zero divisor keeps the direction, and its own sign is the one that counts @test ∞ / 0 ≡ ∞ / 0.0 ≡ (-∞) / (-0.0) ≡ +∞ @test (-∞) / 0 ≡ (-∞) / 0.0 ≡ ∞ / (-0.0) ≡ -∞ - @test ComplexInfinity(0.5) / 2 ≡ ComplexInfinity(0.5) + @test im*∞ / 2 ≡ im*∞ # dividing by a complex turns the direction by its angle @test (+∞) / (1+im) ≡ (1-im)*∞ @test 2 / -∞ ≡ -0.0 @@ -521,7 +545,7 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], # ℵ₁ points in the same direction as ∞, even though `ℵ₁ == ∞` is false positive = (∞, +∞, ℵ₀, ℵ₁, ComplexInfinity(), Inf, Inf32, Inf16, big(Inf)) negative = (-∞, -ComplexInfinity(), -Inf, -Inf32, -Inf16, -big(Inf)) - imaginary = (ComplexInfinity(0.5), complex(0.0, Inf)) + imaginary = (im*∞, complex(0.0, Inf)) others = (0, 1.5, -2, -1.5, 0.0, -0.0, NaN, NaN32, prevfloat(Inf), nextfloat(-Inf), nextfloat(0.0), prevfloat(-0.0), "∞", "-∞") @@ -560,15 +584,22 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], @testset "NaN arithmetic" begin # the result is the package's own undefined value, as `Inf + ∞` is its own infinity - for nan in (NaN, NaN32, NaN16, big(NaN)), - inf in (∞, +∞, -∞, ℵ₀, ComplexInfinity(), -ComplexInfinity()) - + for nan in (NaN, NaN32, NaN16, big(NaN)), inf in (∞, +∞, -∞, ℵ₀) for op in (+, -, *, div, fld, cld) @test op(nan, inf) ≡ op(inf, nan) ≡ NotANumber() end # `mod(inf, x)` discards `x`, so only one order is needed @test mod(nan, inf) ≡ NotANumber() end + for nan in (NaN, NaN32, NaN16, big(NaN)), inf in (ComplexInfinity(), -ComplexInfinity()) + for op in (+, -, *) + @test op(nan, inf) ≡ op(inf, nan) ≡ NotANumber() + end + # `Base` defines no integer operation for a `Complex`, and neither do we + for op in (div, fld, cld, mod, rem) + @test_throws MethodError op(nan, inf) + end + end for nan in (NaN, NaN32, NaN16, big(NaN)), inf in (+∞, -∞) @test inf^nan ≡ NotANumber() end @@ -577,7 +608,9 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], @testset "NotANumber" begin nan = NotANumber() # every operand it can meet, itself included - operands = (nan, 0, 1.5, ∞, +∞, -∞, ℵ₀, ComplexInfinity(), NaN, NaN32) + reals = (nan, 0, 1.5, ∞, +∞, -∞, ℵ₀, NaN, NaN32) + complexes = (ComplexInfinity(), (1+im)*∞, complex(1.0, 2.0), complex(true, false)) + operands = (reals..., complexes...) @test isnan(nan) && !isinf(nan) && !isfinite(nan) && !iszero(nan) && !isone(nan) && !signbit(nan) @test !isinteger(nan) # a `NaN` of any real type is real, and this is the type-independent one @@ -610,11 +643,11 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], @test isnan(BigFloat(nan)) # anything computed from it is undefined again - for op in (+, -, *, /, ^, div, fld, cld, mod, rem, min, max), x in operands + for op in (+, -, *, /, ^, div, fld, cld, mod, rem, min, max), x in reals @test op(nan, x) ≡ op(x, nan) ≡ nan end # a complex operand makes the undefined result complex, as it does over the floats - for op in (+, -, *, /, ^, div, fld, cld, mod, rem, min, max), x in (complex(1.0, 2.0), complex(true, false)) + for op in (+, -, *, /, ^, div, fld, cld, mod, rem, min, max), x in complexes @test op(nan, x) ≡ op(x, nan) ≡ complex(nan, nan) end for x in operands diff --git a/test/test_ambiguity.jl b/test/test_ambiguity.jl index 03e2119..b8e0a82 100644 --- a/test/test_ambiguity.jl +++ b/test/test_ambiguity.jl @@ -5,17 +5,22 @@ @test_throws MethodError RealInfinity(Base.TwicePrecision(1.0)) @test_throws MethodError RealInfinity(im) - @test ComplexInfinity{Float64}(Base.TwicePrecision(1.0)) == ComplexInfinity(1) + @test ComplexInfinity(Base.TwicePrecision(1.0)) ≡ ComplexInfinity(-∞) @test_throws MethodError ComplexInfinity(im) - for inf in (∞,+∞,ℵ₀,ComplexInfinity()) + for inf in (∞,+∞,ℵ₀) @test mod(inf, 1//2) ≡ NotANumber() @test mod(1//2, inf) ≡ 1//2 @test fld(1//2, inf) == 0 @test cld(1//2, inf) == 1 @test div(1//2, inf) == 0 - @test fld(inf, 1//2) ≡ cld(inf, 1//2) ≡ div(inf, 1//2) ≡ inf - @test fld(inf, ∞) ≡ fld(inf, +∞) ≡ fld(inf, ℵ₀) ≡ fld(inf, ComplexInfinity()) ≡ NotANumber() + @test fld(inf, 1//2) ≡ cld(inf, 1//2) ≡ div(inf, 1//2) == inf + @test fld(inf, ∞) ≡ fld(inf, +∞) ≡ fld(inf, ℵ₀) ≡ NotANumber() + end + # `Base` defines no integer operation for a `Complex`, so neither does a `ComplexInfinity` take one + for op in (mod, fld, cld, div) + @test_throws MethodError op(ComplexInfinity(), 1//2) + @test_throws MethodError op(1//2, ComplexInfinity()) end @testset "rational power" begin From e439695bb6dd08769e7679846f86e3e95eadc685 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4cker?= Date: Sun, 6 Sep 2026 12:30:02 +0200 Subject: [PATCH 17/18] Add `isreal` for a complex infinity Dropping the element type removed the only way to ask a `ComplexInfinity` whether it points along the real axis, and that question was worth keeping: it just belongs to the value rather than to the type. `isreal` takes it over, and `RealInfinity` converts a direction that lies on the axis and throws an `InexactError` otherwise, which is what `Real` does with a `Complex`. So an order or an integer operation is still reachable, by naming the conversion, and an infinity off the axis throws instead of quietly behaving as if the imaginary part were not there. --- src/Infinities.jl | 5 +++++ test/runtests.jl | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/src/Infinities.jl b/src/Infinities.jl index 62a1b7a..c5e4e11 100644 --- a/src/Infinities.jl +++ b/src/Infinities.jl @@ -144,6 +144,11 @@ ComplexInfinity(x::RealInfinity) = ComplexInfinity(_directionof(x)) ComplexInfinity(x::ComplexInfinity) = x signbit(y::ComplexInfinity) = y.turns == _HALFTURN +isreal(y::ComplexInfinity) = iszero(y.turns) || signbit(y) + +# `Base` converts a `Complex` to a `Real` the same way, and throws the same error off the axis. +RealInfinity(x::ComplexInfinity) = isreal(x) ? RealInfinity(signbit(x)) : + throw(InexactError(:RealInfinity, RealInfinity, x)) convert(::Type{ComplexInfinity}, ::Infinity) = ComplexInfinity() convert(::Type{ComplexInfinity}, x::RealInfinity) = ComplexInfinity(x) diff --git a/test/runtests.jl b/test/runtests.jl index 90f4306..4267a7a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -249,6 +249,7 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], # one direction is one value, however it is spelled @test ComplexInfinity(halfturns = -0.5) ≡ ComplexInfinity(halfturns = 1.5) ≡ -im*∞ @test ComplexInfinity(halfturns = 1) ≡ ComplexInfinity(halfturns = 3) ≡ ComplexInfinity(-∞) + @test isreal(ComplexInfinity()) && isreal(-ComplexInfinity()) && !isreal((1+im)*∞) # a rational direction converts without a float step @test reinterpret(UInt64, ComplexInfinity(halfturns = 2//3)) ≡ 0x5555555555555555 # a numerator too wide for `Int128` takes the `BigInt` route to the same count @@ -334,6 +335,11 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], @test_throws MethodError op(ComplexInfinity(), y) @test_throws MethodError op(y, ComplexInfinity()) end + # a direction on the axis converts, as `Real(::Complex)` does + @test RealInfinity(ComplexInfinity()) ≡ +∞ + @test RealInfinity(-ComplexInfinity()) ≡ -∞ + @test_throws InexactError RealInfinity((1+im)*∞) + @test 5 < RealInfinity(ComplexInfinity()) @test 1 + ComplexInfinity() ≡ 1.0 + ComplexInfinity() ≡ ComplexInfinity() + 1 ≡ ComplexInfinity() + 1.0 ≡ ComplexInfinity() @test 5 * ComplexInfinity() ≡ ComplexInfinity() @@ -355,6 +361,7 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], @test_throws MethodError op(x, 5) @test_throws MethodError op(5, x) end + @test div(RealInfinity(ComplexInfinity()), 5) ≡ +∞ end @test signbit(ComplexInfinity(halfturns = 3)) From 421714b5e3b3a58004e079e96f9cdb78590d965a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4cker?= Date: Sun, 6 Sep 2026 12:32:44 +0200 Subject: [PATCH 18/18] Match an undefined result to the operands it came from `Base` returns `NaN` where two reals have no result and `NaN + NaN*im` where either operand is complex, and this package only did half of that. Anything computed *from* a `NotANumber` already came back complex against a complex operand, but every place that *produced* one returned the real value, so `NaN * ComplexInfinity()` and `NotANumber() * ComplexInfinity()` disagreed. `_undefined` picks the value from the two operands and now stands wherever an undefined result is made: a `NaN` argument, a zero times an infinity, two infinities divided, and two infinities added in different directions. Only five of those sites can see a complex operand at all; for the rest the choice is decided at compile time and costs nothing. --- src/algebra.jl | 22 +++++++++++++++------- src/interface.jl | 1 + test/runtests.jl | 13 ++++++++++--- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/algebra.jl b/src/algebra.jl index 7d21527..534d908 100644 --- a/src/algebra.jl +++ b/src/algebra.jl @@ -25,13 +25,17 @@ @inline toinf(x::Complex) = ComplexInfinity(_directionof(x)) @inline toinf(x::ComplexInfinity) = x -@inline _infadd(x, y) = angle(x) == angle(y) ? y : NotANumber() +# The undefined value that matches the operands, as `Base` returns `NaN` or `NaN + NaN*im`. +@inline _undefined(x, y) = + x isa ExtendedComplex || y isa ExtendedComplex ? ComplexNotANumber : NotANumber() + +@inline _infadd(x, y) = angle(x) == angle(y) ? y : _undefined(x, y) @inline __add(x, y::AllInfinities) = isinf(x) ? _infadd(toinf(x), y) : y @inline __add(x::Integer, y::InfiniteCardinal) = max(x, y) # A `NaN` argument makes the result undefined. Types with no `NaN` fold the test away. -@inline _add(x, y) = isnan(x) ? NotANumber() : __add(infpromote(x, y)...) +@inline _add(x, y) = isnan(x) ? _undefined(x, y) : __add(infpromote(x, y)...) +(x::Number, y::AllInfinities) = _add(x, y) +(x::AllInfinities, y::Number) = _add(y, x) @@ -59,8 +63,8 @@ @inline __mul(x::Integer, y::InfiniteCardinal) = x > 0 ? y : throw(ArgumentError("Cannot multiply $x * $y")) @inline function _mul(x, y) - isnan(x) && return NotANumber() - iszero(x) && return NotANumber() + isnan(x) && return _undefined(x, y) + iszero(x) && return _undefined(x, y) __mul(infpromote(x, y)...) end @@ -81,7 +85,7 @@ end /(x::AllInfinities, y::Number) = _div(x, y) /(x::Number, y::AllInfinities) = _div(x, y) -/(x::AllInfinities, y::AllInfinities) = NotANumber() +/(x::AllInfinities, y::AllInfinities) = _undefined(x, y) # mod @inline function _mod(x::Real, y::IntegerInfinities) @@ -147,8 +151,8 @@ for op in (:+, :-, :*, :/, :^, :div, :fld, :cld, :mod, :rem, :min, :max) @eval $op(::$Typ, y::NotANumber) = y end for Typ in NotANumberComplexRivals - @eval $op(::NotANumber, ::$Typ) = complex(NotANumber(), NotANumber()) - @eval $op(::$Typ, ::NotANumber) = complex(NotANumber(), NotANumber()) + @eval $op(::NotANumber, ::$Typ) = ComplexNotANumber + @eval $op(::$Typ, ::NotANumber) = ComplexNotANumber end @eval $op(x::NotANumber, ::NotANumber) = x end @@ -156,6 +160,10 @@ for Typ in NotANumberRivals @eval divrem(x::NotANumber, ::$Typ) = (x, x) @eval divrem(::$Typ, y::NotANumber) = (y, y) end +for Typ in NotANumberComplexRivals + @eval divrem(::NotANumber, ::$Typ) = (ComplexNotANumber, ComplexNotANumber) + @eval divrem(::$Typ, ::NotANumber) = (ComplexNotANumber, ComplexNotANumber) +end divrem(x::NotANumber, ::NotANumber) = (x, x) # `Base` has its own `^(::Number, ::Integer)`, which a literal exponent also routes through. ^(x::NotANumber, ::Integer) = x diff --git a/src/interface.jl b/src/interface.jl index ba25548..04094d7 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -25,6 +25,7 @@ const NotANumberRivals = (Number, Real, AbstractFloat, AbstractIrrational, AllIn InfiniteCardinal) # A complex operand makes the undefined result complex, as it does over the floats. const NotANumberComplexRivals = (Complex, Complex{Bool}, ComplexInfinity) +const ComplexNotANumber = complex(NotANumber(), NotANumber()) # `InfiniteCardinal` is absent because `Base` already returns `true` for it through `Integer`. isinteger(::Union{Infinity, RealInfinity, ComplexInfinity}) = false diff --git a/test/runtests.jl b/test/runtests.jl index 4267a7a..ee24e73 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -307,7 +307,7 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], @test complex(-Inf, 0.0) + (-∞) ≡ -ComplexInfinity() @test complex(0.0, Inf) + im*∞ ≡ im*∞ @test complex(0.0, -Inf) + (-im*∞) ≡ -im*∞ - @test complex(0.0, Inf) + ∞ ≡ NotANumber() + @test complex(0.0, Inf) + ∞ ≡ complex(NotANumber(), NotANumber()) # two infinite parts are the only way an infinite `Complex` points off the axes for (z, inf) in ((complex(Inf, Inf), (1+im)*∞), (complex(-Inf, Inf), (-1+im)*∞), (complex(-Inf, -Inf), (-1-im)*∞), (complex(Inf, -Inf), (1-im)*∞)) @@ -493,6 +493,9 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], @test (2//3) / ∞ ≡ (2//3) / ℵ₀ ≡ 0//1 @test (2//3) / (+∞) ≡ 0.0 @test ∞ / ∞ isa NotANumber + # a complex operand on either side makes the undefined quotient complex + @test ComplexInfinity() / ∞ ≡ ∞ / ComplexInfinity() ≡ + ComplexInfinity() / ComplexInfinity() ≡ complex(NotANumber(), NotANumber()) @test isnan(NaN / ∞) && isnan(∞ / NaN) end @@ -599,8 +602,9 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], @test mod(nan, inf) ≡ NotANumber() end for nan in (NaN, NaN32, NaN16, big(NaN)), inf in (ComplexInfinity(), -ComplexInfinity()) + # arithmetic is defined for a complex operand, so the undefined result is complex for op in (+, -, *) - @test op(nan, inf) ≡ op(inf, nan) ≡ NotANumber() + @test op(nan, inf) ≡ op(inf, nan) ≡ complex(NotANumber(), NotANumber()) end # `Base` defines no integer operation for a `Complex`, and neither do we for op in (div, fld, cld, mod, rem) @@ -657,9 +661,12 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], for op in (+, -, *, /, ^, div, fld, cld, mod, rem, min, max), x in complexes @test op(nan, x) ≡ op(x, nan) ≡ complex(nan, nan) end - for x in operands + for x in reals @test divrem(nan, x) ≡ divrem(x, nan) ≡ (nan, nan) end + for x in complexes + @test divrem(nan, x) ≡ divrem(x, nan) ≡ (complex(nan, nan), complex(nan, nan)) + end @test nan^(1//2) ≡ (1//2)^nan ≡ ℯ^nan ≡ nan @test -nan ≡ +nan ≡ abs(nan) ≡ inv(nan) ≡ sign(nan) ≡ conj(nan) ≡ nan end