From cfc26415a87d908457acaa756d0ee321c31f70dd 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 01/14] 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 ae0dee3aaa2c88d5ef7619182ec1a578d334d16c 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 02/14] 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 3411c1bddeb309d9365e05d8cee744b993caf142 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 03/14] 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 From c5a58b80437c0d73d138de02e9631caae8f6426b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4cker?= Date: Fri, 25 Sep 2026 14:00:00 +0200 Subject: [PATCH 04/14] `angle` needs to return a `Float64` by default --- src/Infinities.jl | 2 +- src/cardinality.jl | 2 +- test/runtests.jl | 2 +- test/test_cardinality.jl | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Infinities.jl b/src/Infinities.jl index c5e4e11..cdf1cae 100644 --- a/src/Infinities.jl +++ b/src/Infinities.jl @@ -44,7 +44,7 @@ _convert(::Type{T}, ::Infinity) where {T<:Real} = convert(T, Inf)::T (::Type{T})(x::Infinity) where {T<:Real} = _convert(T, x) sign(y::Infinity) = 1 -angle(x::Infinity) = 0 +angle(x::Infinity) = 0.0 signbit(::Infinity) = false one(::Type{Infinity}) = 1 diff --git a/src/cardinality.jl b/src/cardinality.jl index f18277b..acad061 100644 --- a/src/cardinality.jl +++ b/src/cardinality.jl @@ -23,7 +23,7 @@ isone(::InfiniteCardinal) = false signbit(::InfiniteCardinal) = false sign(::InfiniteCardinal) = 1 -angle(::InfiniteCardinal) = 0 +angle(::InfiniteCardinal) = 0.0 abs(a::InfiniteCardinal) = a zero(::InfiniteCardinal) = 0 zero(::Type{<:InfiniteCardinal}) = 0 diff --git a/test/runtests.jl b/test/runtests.jl index ee24e73..e26f639 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -54,7 +54,7 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], @test !signbit(∞) @test sign(∞) ≡ 1 - @test angle(∞) ≡ 0 + @test angle(∞) ≡ 0.0 @test string(∞) == stringmime("text/plain", ∞) == "∞" diff --git a/test/test_cardinality.jl b/test/test_cardinality.jl index db7a516..4df0252 100644 --- a/test/test_cardinality.jl +++ b/test/test_cardinality.jl @@ -12,7 +12,7 @@ Base.getindex(::InfVector, ::InfiniteCardinal{0}) = 42 @test !isone(ℵ₀) @test !iszero(ℵ₀) @test sign(ℵ₀) ≡ 1 && !signbit(ℵ₀) - @test angle(ℵ₀) ≡ 0 + @test angle(ℵ₀) ≡ 0.0 @test Integer(∞) ≡ convert(Integer,∞) ≡ Integer(ℵ₀) ≡ convert(Integer, ℵ₀) ≡ ℵ₀ @test abs(ℵ₀) ≡ ℵ₀ @test zero(ℵ₀) ≡ zero(InfiniteCardinal{0}) ≡ 0 From 1d36d39cceb231391946822b791a7e432dfe546d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4cker?= Date: Fri, 25 Sep 2026 16:08:32 +0200 Subject: [PATCH 05/14] Simplify handling of complex infinities and make them more consistent with `Base` --- src/Infinities.jl | 23 ++++++++++++++++------- src/algebra.jl | 11 ++++++++++- src/interface.jl | 9 +++++++++ test/runtests.jl | 40 ++++++++++++++++++++++++++++++++++++---- 4 files changed, 71 insertions(+), 12 deletions(-) diff --git a/src/Infinities.jl b/src/Infinities.jl index cdf1cae..c2c48e8 100644 --- a/src/Infinities.jl +++ b/src/Infinities.jl @@ -3,7 +3,7 @@ module Infinities 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, + string, show, promote_rule, convert, getindex, tryparse, conj, complex, isinteger, round, floor, ceil, trunc, float, Bool, Integer @@ -111,8 +111,8 @@ read out the exact value. 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)∞ + im*∞ # 0 + ∞*im + (1+im)*∞ # ∞ + ∞*im exp(im*π/4)*∞ # the same direction again Those forms and the `halfturns` keyword go through `angle`, so they round. It is exact on @@ -124,8 +124,11 @@ struct ComplexInfinity <: Number ComplexInfinity(turns::UInt64) = new(turns) end -# A full turn fills the `UInt64` range, so a half turn is 2^63 units. -const _HALFTURN = UInt64(2)^63 # the negative real axis +# Half of the `typemax(UInt64) + 1` counts of a full turn, a number that would overflow. +const _HALFTURN = typemax(UInt64) ÷ 2 + 1 # the negative real axis +# The signs of the parts along each of the eight rays, the axes and diagonals. +const _EIGHTH = _HALFTURN ÷ 4 +const _RAYPARTS = ((1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0), (-1, -1), (0, -1), (1, -1)) # `_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. @@ -167,9 +170,15 @@ function float(x::ComplexInfinity) complex(_ray(c), _ray(s)) end -# 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. +# The rays print as `Base` prints an infinite `Complex`. Off the rays 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) + k, offset = divrem(x.turns, _EIGHTH) + if iszero(offset) + r, i = _RAYPARTS[k + 1] + return print(io, ("-∞", "0", "∞")[r + 2], (" - ∞*im", " + 0im", " + ∞*im")[i + 2]) + end h = _halfturns(x) _directionof(cispi(h)) == x.turns ? print(io, "cispi($h)∞") : print(io, "ComplexInfinity(", repr(x.turns), ")") diff --git a/src/algebra.jl b/src/algebra.jl index 534d908..0beec47 100644 --- a/src/algebra.jl +++ b/src/algebra.jl @@ -29,7 +29,16 @@ @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 _infadd(x, y) = _directionof(x) == _directionof(y) ? y : + x isa ComplexInfinity || y isa ComplexInfinity ? _rayadd(x, y) : _undefined(x, y) + +# On the eight rays each part is infinite or exactly zero, so `Base` adds part by part. +@inline function _rayadd(x, y) + (kx, ox), (ky, oy) = divrem(_directionof(x), _EIGHTH), divrem(_directionof(y), _EIGHTH) + iszero(ox | oy) || return _undefined(x, y) + (rx, ix), (ry, iy) = _RAYPARTS[kx + 1], _RAYPARTS[ky + 1] + rx * ry < 0 || ix * iy < 0 ? _undefined(x, y) : toinf(complex(sign(rx + ry), sign(ix + iy))) +end @inline __add(x, y::AllInfinities) = isinf(x) ? _infadd(toinf(x), y) : y @inline __add(x::Integer, y::InfiniteCardinal) = max(x, y) diff --git a/src/interface.jl b/src/interface.jl index 04094d7..b2de55a 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -39,6 +39,15 @@ promote_rule(::Type{Infinity}, ::Type{PositiveInfinity}) = PositiveInfinity promote_rule(::Type{Infinity}, ::Type{ComplexInfinity}) = ComplexInfinity promote_rule(::Type{<:RealInfinity}, ::Type{ComplexInfinity}) = ComplexInfinity +# An infinite part makes the whole number infinite, so `complex` gives the direction it points in. +complex(x::IntegerInfinities) = ComplexInfinity(x) +complex(x::ComplexInfinity) = x +complex(::Type{<:IntegerInfinities}) = ComplexInfinity +complex(::Type{ComplexInfinity}) = ComplexInfinity +complex(x::IntegerInfinities, y::Real) = x + im*y +complex(x::Real, y::IntegerInfinities) = x + im*y +complex(x::IntegerInfinities, y::IntegerInfinities) = x + im*y + function tryparse(::Type{NegativeInfinity}, s::AbstractString) i = findfirst(!isspace, s) (isnothing(i) || s[i] != '-') && return nothing diff --git a/test/runtests.jl b/test/runtests.jl index e26f639..37fedbb 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -307,13 +307,40 @@ 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) + ∞ ≡ complex(NotANumber(), NotANumber()) + @test complex(0.0, Inf) + ∞ ≡ (1+im)*∞ + # on the eight rays the sum goes part by part, as the `Complex` sum does + rays = (∞, (1+im)*∞, im*∞, (-1+im)*∞, -∞, (-1-im)*∞, -im*∞, (1-im)*∞) + for x in rays, y in rays + z = float(x) + float(y) + @test isequal(x + y, !isnan(z) ? z*∞ : + z isa Complex ? complex(NotANumber(), NotANumber()) : NotANumber()) + end + @test ∞ + im*∞ ≡ im*∞ + ∞ ≡ complex(∞, ∞) ≡ (1+im)*∞ + @test exp(0.1im)*∞ + ∞ ≡ ComplexInfinity(0x7fffffffffffffff) + (-ComplexInfinity()) ≡ + 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)*∞)) @test z + inf ≡ inf end + # an infinite part makes the whole number infinite, pointing where the `Complex` would + @test complex(∞) ≡ complex(+∞) ≡ complex(ℵ₀) ≡ complex(∞, 0) ≡ complex(+∞, -2.5) ≡ ComplexInfinity() + @test complex(-∞) ≡ complex(-∞, 0) ≡ complex(-∞, -0.0) ≡ -ComplexInfinity() + @test complex(0, ∞) ≡ complex(-3, +∞) ≡ im*∞ + @test complex(0.0, -∞) ≡ -im*∞ + for (x, y, inf) in ((∞, ∞, (1+im)*∞), (0.5∞, ∞, (1+im)*∞), (-∞, +∞, (-1+im)*∞), + (-∞, -∞, (-1-im)*∞), (ℵ₀, -∞, (1-im)*∞), (∞, Inf, (1+im)*∞), + (-Inf, 0.5∞, (-1+im)*∞)) + @test complex(x, y) ≡ inf + end + for nan in (NaN, NotANumber()) + @test complex(∞, nan) ≡ complex(nan, -∞) ≡ complex(NotANumber(), NotANumber()) + end + @test complex(ComplexInfinity()) ≡ ComplexInfinity() && complex(im*∞) ≡ im*∞ + @test complex(Infinity) ≡ complex(RealInfinity) ≡ complex(PositiveInfinity) ≡ + complex(InfiniteCardinal{0}) ≡ complex(ComplexInfinity) ≡ ComplexInfinity + @test ∞ * ComplexInfinity() ≡ RealInfinity() * ComplexInfinity() ≡ ComplexInfinity() * ∞ ≡ ComplexInfinity() * RealInfinity() ≡ ComplexInfinity() @@ -348,11 +375,16 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], @test (1+im)*∞ * (im*∞) ≡ (-1+im)*∞ @test (2.0+0.0im)*∞ + ComplexInfinity() ≡ ComplexInfinity() + (2.0+0.0im)*∞ ≡ ComplexInfinity() - @test stringmime("text/plain", ComplexInfinity()) == "cispi(0.0)∞" + @test stringmime("text/plain", ComplexInfinity()) == "∞ + 0im" + @test map(x -> sprint(show, ComplexInfinity(x)), rays) == + ("∞ + 0im", "∞ + ∞*im", "0 + ∞*im", "-∞ + ∞*im", + "-∞ + 0im", "-∞ - ∞*im", "0 - ∞*im", "∞ - ∞*im") + @test sprint(show, ComplexInfinity(halfturns = 0.1)) == "cispi(0.1)∞" # 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) + for x in (ComplexInfinity.(rays)..., ComplexInfinity(0x0ccccccccccccd00), + ComplexInfinity(0x5555555555555555), ComplexInfinity(0xdeadbeefdeadbeef)) + @test Core.eval(@__MODULE__, Meta.parse(sprint(show, x))) ≡ x end @testset "integer operations" begin From 1b5459eee716b2595396e7152a7d427836ffbbbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4cker?= Date: Wed, 23 Sep 2026 13:02:16 +0200 Subject: [PATCH 06/14] Implement package extension for Static.jl This takes over ideas from #20 created by @cscherrer. So thanks for that contribution! As the PR is 5 years old, quite some things are now obsolete. A nice side-effect was finding and fixing a missing `infpromote` for `Bool`. --- Project.toml | 10 +++++++- README.md | 25 ++++++++++++++++++ ext/InfinitiesStaticExt.jl | 37 +++++++++++++++++++++++++++ src/algebra.jl | 1 + test/runtests.jl | 10 ++++++++ test/test_static.jl | 52 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 ext/InfinitiesStaticExt.jl create mode 100644 test/test_static.jl diff --git a/Project.toml b/Project.toml index c4f1857..2632de2 100644 --- a/Project.toml +++ b/Project.toml @@ -3,10 +3,17 @@ uuid = "e1ba4f0e-776d-440f-acd9-e1d2e9742647" authors = ["Sheehan Olver "] version = "0.1.13" +[weakdeps] +Static = "aedffcd0-7271-4cad-89d0-dc628f76c6d3" + +[extensions] +InfinitiesStaticExt = "Static" + [compat] Aqua = "0.8" Base64 = "1" JET = "0.9, 0.10, 0.11, 0.12" +Static = "1.4" Test = "1" julia = "1.10" @@ -14,7 +21,8 @@ julia = "1.10" Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" +Static = "aedffcd0-7271-4cad-89d0-dc628f76c6d3" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Aqua", "Base64", "JET", "Test"] +test = ["Aqua", "Base64", "JET", "Static", "Test"] diff --git a/README.md b/README.md index 2fca520..7ba3686 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,31 @@ This Julia package is used to represent infinities, including: Note that we subtype based on interfaces, rather than strict mathematical definitions. For example, `ℵ₀ isa Integer` as `Integer` is often used to represent the size of a set or vector. Similarly, `∞ isa Real`. +## Static.jl integration + +Loading [Static.jl](https://github.com/SciML/Static.jl) enables an optional package extension. +The values `∞`, `+∞`, `-∞`, `InfiniteCardinal{k}()`, and `NotANumber()` are already +fully determined by their types, so they need no separate static representation: + +```julia +using Infinities, Static + +static(∞) === ∞ # true +is_static(typeof(-∞)) === True() # true +known(typeof(ℵ₀)) === ℵ₀ # true +Static.lt(static(2), ∞) === True() # true +static(2) + ℵ₀ === ℵ₀ # true +``` + +Mixed arithmetic and comparisons use the same rules as the corresponding ordinary +numbers, including undefined results and NaN propagation. Base operations retain their +ordinary return types: `one(∞)` is `1` and `isinf(∞)` is `true`. Use `static` on a result +or Static.jl's comparison functions when a static result type is needed. + +`ComplexInfinity` stores its direction as a value, not in its type. Its arithmetic +accepts static operands, but `is_static(ComplexInfinity)` is `False()` and +`static(im*∞)` is unsupported. + ## Similar packages This package is meant to eventually replace [Infinity.jl](https://github.com/cjdoris/Infinity.jl) and the definitions of `∞` in [InfiniteArrays.jl](https://github.com/JuliaArrays/InfiniteArrays.jl). We do not yet support Infinity.jl's notions of `InfExtendedReal` but we hope to add this soon. diff --git a/ext/InfinitiesStaticExt.jl b/ext/InfinitiesStaticExt.jl new file mode 100644 index 0000000..8cc0337 --- /dev/null +++ b/ext/InfinitiesStaticExt.jl @@ -0,0 +1,37 @@ +module InfinitiesStaticExt + +using Infinities: Infinity, PositiveInfinity, NegativeInfinity, InfiniteCardinal, NotANumber +using Infinities: AllInfinities, AllRealInfinities, IntegerInfinities, ComplexInfinity, RealInfinity +using Static: Static, dynamic, StaticNumber, StaticInteger, StaticFloat64, StaticInt, True + +for Typ in (Infinity, PositiveInfinity, NegativeInfinity, NotANumber) + @eval begin + Static.static(x::$Typ) = x + Static.is_static(::Type{$Typ}) = True() + Static.known(::Type{$Typ}) = $Typ() + end +end + +Static.static(x::InfiniteCardinal) = x +Static.is_static(::Type{InfiniteCardinal{N}}) where {N} = True() +Static.known(::Type{InfiniteCardinal{N}}) where {N} = InfiniteCardinal{N}() + +for (ops, Types, StaticTypes) in ( + ((:+, :-, :*, :/, :(==)), (AllInfinities,), (StaticNumber,)), + ((:isequal,), (NotANumber,), (StaticNumber,)), + ((:div, :fld, :cld, :divrem), (IntegerInfinities,), (StaticNumber,)), + ((:<, :<=, :>, :>=), (AllInfinities,), (StaticInteger, StaticFloat64)), + ((:isless,), (AllRealInfinities, InfiniteCardinal, NotANumber), (StaticInteger, StaticFloat64)), + ((:min, :max), (AllInfinities, NotANumber), (StaticInteger,)), + ((:min, :max), (IntegerInfinities, ComplexInfinity, NotANumber), (StaticFloat64,)), + ((:mod,), (IntegerInfinities, NotANumber), (StaticNumber,)), + ((:rem,), (IntegerInfinities, NotANumber), (StaticInteger, StaticFloat64)), + ((:*,), (InfiniteCardinal,), (StaticInt{0},)), +), op in ops, Typ in Types, StaticTyp in StaticTypes + @eval Base.$op(x::$Typ, y::$StaticTyp) = $op(x, dynamic(y)) + @eval Base.$op(x::$StaticTyp, y::$Typ) = $op(dynamic(x), y) +end + +Base.:^(x::RealInfinity, y::StaticNumber) = x^dynamic(y) + +end \ No newline at end of file diff --git a/src/algebra.jl b/src/algebra.jl index 0beec47..18b9265 100644 --- a/src/algebra.jl +++ b/src/algebra.jl @@ -1,4 +1,5 @@ @inline infpromote(x, y) = Base._promote(x, y) +@inline infpromote(x::Bool, y::Union{Infinity, ComplexInfinity}) = (x, y) @inline infpromote(x::ExtendedComplex, y::AllInfinities) = (x, ComplexInfinity(y)) @inline infpromote(x::ExtendedComplex, y::ComplexInfinity) = Base._promote(x, y) @inline infpromote(x::Real, ::InfiniteCardinal) = (x, ∞) diff --git a/test/runtests.jl b/test/runtests.jl index 37fedbb..736cef4 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2,6 +2,7 @@ using Infinities, Base64, Test import Infinities: Infinity, AllInfinities, _isinf using Aqua, JET +using Static: Static "An `AbstractString` indexed by character position, so that byte arithmetic on indices is invalid." struct CharString <: AbstractString @@ -15,6 +16,14 @@ Base.isvalid(s::CharString, i::Integer) = 1 ≤ i ≤ ncodeunits(s) Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], i + 1) : nothing @testset "∞" begin + @testset "Boolean arithmetic" begin + for inf in (∞, +∞, -∞, ℵ₀, ℵ₁, NotANumber(), ComplexInfinity(), im*∞), value in (false, true), + (args, expected) in (((inf, value), (inf, Int(value))), ((value, inf), (Int(value), inf))), + op in (+, -, *) + + @test isequal(op(args...), op(expected...)) + end + end @testset "∞" begin @test ∞ ≠ 1 @test 1 ≠ ∞ @@ -779,6 +788,7 @@ end include("test_cardinality.jl") include("test_ambiguity.jl") +include("test_static.jl") @testset "Project quality" begin Aqua.test_all(Infinities) diff --git a/test/test_static.jl b/test/test_static.jl new file mode 100644 index 0000000..26a9f7b --- /dev/null +++ b/test/test_static.jl @@ -0,0 +1,52 @@ +using Static: Static, static, dynamic, is_static, known, eq, lt, True, False + +@testset "Static extension" begin + @test !isnothing(Base.get_extension(Infinities, :InfinitiesStaticExt)) + @test isempty(Test.detect_ambiguities(Infinities, Static, Base.get_extension(Infinities, :InfinitiesStaticExt))) + singletons = (∞, +∞, -∞, ℵ₀, ℵ₁, InfiniteCardinal{2}(), NotANumber()) + for value in (singletons..., (∞, -∞, ℵ₀)) + for op in (static, dynamic) + @test @inferred(op(value)) === value + end + for input in (value, typeof(value)) + @test @inferred(is_static(input)) === True() + @test @inferred(known(input)) === value + end + end + for (op, reference) in ((eq, ==), (lt, <), (+, +), (*, *)), + first in (∞, -∞, NotANumber(), static(0), static(2), static(NaN), static(Inf)), second in singletons + + @test @inferred(op(first, second)) === static(reference(dynamic(first), second)) + end + @test @inferred((-∞)^static(2)) === +∞ + for Typ in (RealInfinity, ComplexInfinity) + @test is_static(Typ) === False() + @test isnothing(known(Typ)) + end + @test_throws ErrorException static(ComplexInfinity()) + + operators = (+, -, *, /, div, fld, cld, mod, rem, divrem, min, max, isless, ==, <, <=, >, >=, isequal) + for inf in (∞, +∞, -∞, ℵ₀, ℵ₁, NotANumber(), ComplexInfinity(), im*∞), + value in (false, true, 0, 2, -2, 0.0, -0.0, 1.5, -1.5, NaN, Inf, -Inf), + args in ((inf, static(value)), (static(value), inf)) + + unsupported = inf isa ComplexInfinity ? + (div, fld, cld, mod, rem, divrem, isless, (isnan(value) ? () : (min, max, <, <=, >, >=))...) : () + unbounded = args[2] === inf && inf isa Union{Infinities.Infinity, RealInfinity, InfiniteCardinal} && + !isnan(value) && signbit(value) != signbit(inf) ? (mod,) : () + invalid = inf isa InfiniteCardinal && value isa Integer && value < 0 ? (unbounded..., *) : unbounded + for op in setdiff(operators, unsupported, invalid) + @test isequal(op(args...), op(map(dynamic, args)...)) + end + for (exception, ops) in ((MethodError, unsupported), (ArgumentError, invalid)), op in ops + @test_throws exception op(args...) + end + end + for inf in (+∞, -∞), exponent in (false, true, -2, 0, 2, 3, -1.5, 1.5, NaN, Inf) + if inf isa NegativeInfinity && !isnan(exponent) && !isinteger(exponent) + @test_throws DomainError inf^static(exponent) + else + @test isequal(inf^static(exponent), inf^exponent) + end + end +end \ No newline at end of file From 4f7ba7f0c60b20fa7eb8fa2b30e734087a09cd8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4cker?= Date: Wed, 23 Sep 2026 15:03:38 +0200 Subject: [PATCH 07/14] Add a ForwardDiff.jl extension This would benefit from someone looking at it who knows ForwardDiff better than I do. Fixes: #13 --- Project.toml | 6 +++- README.md | 28 ++++++++++++++++++ ext/InfinitiesForwardDiffExt.jl | 51 +++++++++++++++++++++++++++++++++ test/runtests.jl | 2 ++ test/test_forwarddiff.jl | 49 +++++++++++++++++++++++++++++++ 5 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 ext/InfinitiesForwardDiffExt.jl create mode 100644 test/test_forwarddiff.jl diff --git a/Project.toml b/Project.toml index 2632de2..d1b6578 100644 --- a/Project.toml +++ b/Project.toml @@ -4,14 +4,17 @@ authors = ["Sheehan Olver "] version = "0.1.13" [weakdeps] +ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" Static = "aedffcd0-7271-4cad-89d0-dc628f76c6d3" [extensions] +InfinitiesForwardDiffExt = "ForwardDiff" InfinitiesStaticExt = "Static" [compat] Aqua = "0.8" Base64 = "1" +ForwardDiff = "1" JET = "0.9, 0.10, 0.11, 0.12" Static = "1.4" Test = "1" @@ -20,9 +23,10 @@ julia = "1.10" [extras] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" +ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" Static = "aedffcd0-7271-4cad-89d0-dc628f76c6d3" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Aqua", "Base64", "JET", "Static", "Test"] +test = ["Aqua", "Base64", "ForwardDiff", "JET", "Static", "Test"] diff --git a/README.md b/README.md index 7ba3686..05d49ba 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,34 @@ or Static.jl's comparison functions when a static result type is needed. accepts static operands, but `is_static(ComplexInfinity)` is `False()` and `static(im*∞)` is unsupported. +## ForwardDiff.jl integration + +Loading [ForwardDiff.jl](https://github.com/JuliaDiff/ForwardDiff.jl) enables an optional +extension for mixed `+`, `-`, `*`, `/`, `mod`, `rem`, `min`, `max`, and comparisons +of dual numbers with `∞`, `+∞`, `-∞`, and `NotANumber()`. Primal values retain +Infinities' scalar results and exceptions, without converting infinities to floats. +Derivative rules use the same scalar arithmetic, so a zero tangent multiplied by an +infinity becomes `NotANumber()`. Bounded `mod` and `rem` preserve the dividend's +tangents, while an undefined remainder has undefined tangents. + +```julia +using Infinities, ForwardDiff + +ForwardDiff.derivative(input -> input * ∞, 2.0) # +∞ +ForwardDiff.derivative(input -> input + ∞, 2.0) # 1.0 +ForwardDiff.derivative(input -> input / ∞, 2.0) # 0.0 +``` + +`Dual(∞)` preserves the infinity. Explicitly requesting a floating scalar type, such +as `Dual{Nothing, Float64}(∞)`, converts it. Mixed symbolic results can use `Real` +as the dual's scalar parameter, with a corresponding loss of type specialization. + +This is not general support for arbitrary symbolic differentiation: ForwardDiff +operations such as unary negation can reject heterogeneous symbolic partials, and +powers remain subject to the existing scalar and ForwardDiff limitations. There is +no floating-point fallback. The extension does not define mixed dual-number operations +for `InfiniteCardinal` or `ComplexInfinity`. + ## Similar packages This package is meant to eventually replace [Infinity.jl](https://github.com/cjdoris/Infinity.jl) and the definitions of `∞` in [InfiniteArrays.jl](https://github.com/JuliaArrays/InfiniteArrays.jl). We do not yet support Infinity.jl's notions of `InfExtendedReal` but we hope to add this soon. diff --git a/ext/InfinitiesForwardDiffExt.jl b/ext/InfinitiesForwardDiffExt.jl new file mode 100644 index 0000000..971ec2e --- /dev/null +++ b/ext/InfinitiesForwardDiffExt.jl @@ -0,0 +1,51 @@ +module InfinitiesForwardDiffExt + +using Infinities: Infinity, RealInfinity, NotANumber +import ForwardDiff: Dual, Partials, value, partials + +function symbolic_dual(::Dual{Tag}, primal, tangents::NTuple{N, Any}) where {Tag, N} + Dual{Tag, Real, N}(primal, Partials{N, Real}(tangents)) +end + +for Typ in (Infinity, RealInfinity, NotANumber) + for op in (:^, :min, :max, :(==), :isequal, :<, :<=, :isless) + @eval Base.$op(dual::Dual, inf::$Typ) = invoke($op, Tuple{Dual, Real}, dual, inf) + @eval Base.$op(inf::$Typ, dual::Dual) = invoke($op, Tuple{Real, Dual}, inf, dual) + end + + for (op, forward, reverse) in ((:+, :tangent, :tangent), + (:-, :tangent, :(-tangent)), (:*, :(tangent * inf), :(tangent * inf)), + (:/, :(tangent / inf), :(-(primal / value(dual)) * tangent))) + @eval function Base.$op(dual::Dual, inf::$Typ) + primal = $op(value(dual), inf) + symbolic_dual(dual, primal, map(tangent -> $forward, Tuple(partials(dual)))) + end + @eval function Base.$op(inf::$Typ, dual::Dual) + primal = $op(inf, value(dual)) + symbolic_dual(dual, primal, map(tangent -> $reverse, Tuple(partials(dual)))) + end + end + + for op in (:mod, :rem) + @eval function Base.$op(dual::Dual, inf::$Typ) + primal = $op(value(dual), inf) + symbolic_dual(dual, primal, map(tangent -> isnan(primal) ? NotANumber() : tangent, Tuple(partials(dual)))) + end + @eval Base.$op(inf::$Typ, dual::Dual) = + symbolic_dual(dual, $op(inf, value(dual)), map(_ -> NotANumber(), Tuple(partials(dual)))) + end + + @eval begin + Dual(inf::$Typ) = Dual{Nothing}(inf, ()) + Dual{Tag}(inf::$Typ) where {Tag} = Dual{Tag}(inf, ()) + Dual{Tag, Value}(inf::$Typ) where {Tag, Value} = Dual{Tag, Value, 0}(inf) + Dual{Tag, Value, N}(inf::$Typ) where {Tag, Value, N} = + Dual{Tag, Value, N}(convert(Value, inf), zero(Partials{N, Value})) + Dual{Tag}(inf::$Typ, tangents::Partials{N, Value}) where {Tag, N, Value} = + Dual{Tag, Real, N}(inf, Partials{N, Real}(Tuple(tangents))) + Dual{Tag}(inf::Value, tangents::Partials{N, Value}) where {Tag, N, Value<:$Typ} = + Dual{Tag, Real, N}(inf, Partials{N, Real}(Tuple(tangents))) + end +end + +end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 736cef4..71c6359 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -794,3 +794,5 @@ include("test_static.jl") Aqua.test_all(Infinities) test_package(Infinities) end + +include("test_forwarddiff.jl") diff --git a/test/test_forwarddiff.jl b/test/test_forwarddiff.jl new file mode 100644 index 0000000..44e66fb --- /dev/null +++ b/test/test_forwarddiff.jl @@ -0,0 +1,49 @@ +using ForwardDiff: ForwardDiff, Dual, value, partials + +@testset "ForwardDiff extension" begin + @test isempty(Test.detect_ambiguities(Base.get_extension(Infinities, :InfinitiesForwardDiffExt))) + same = (actual, expected) -> typeof(actual) === typeof(expected) && isequal(actual, expected) + for inf in (∞, +∞, -∞, NotANumber()), Scalar in (Float32, Float64, BigFloat) + for input in (0, 2, -2, Inf, -Inf, NaN), reverse in (false, true), + op in (+, -, *, /, mod, rem, min, max) + primal, tangents = Scalar(input), (Scalar(0), Scalar(1), Scalar(-1)) + dual = Dual(primal, tangents...) + args = reverse ? (inf, dual) : (dual, inf) + if op === mod && !reverse && !isnan(primal) && !isnan(inf) && signbit(primal) != signbit(inf) + @test_throws ArgumentError mod(primal, inf) + @test_throws ArgumentError op(args...) + continue + end + expected = op((reverse ? (inf, primal) : (primal, inf))...) + result = op(args...) + @test same(value(result), expected) + expected_partials = map(tangents) do tangent + op === (+) && return tangent + op === (-) && return reverse ? -tangent : tangent + op === (*) && return tangent * inf + op === (/) && return reverse ? -(expected / primal) * tangent : tangent / inf + op in (mod, rem) && return reverse || isnan(expected) ? NotANumber() : tangent + reference = op((reverse ? (Scalar(inf), dual) : (dual, Scalar(inf)))...) + return partials(reference)[findfirst(isequal(tangent), tangents)] + end + @test all(same.(Tuple(partials(result)), expected_partials)) + end + for op in (isless, isequal, ==, <, <=, >, >=), input in (2, Inf, -Inf, NaN), tangent in (0, 1) + dual = Dual(Scalar(input), Scalar(tangent)) + @test op(dual, inf) === op(dual, float(inf)) + @test op(inf, dual) === op(float(inf), dual) + end + for constructor in (Dual, Dual{Nothing}, Dual{Nothing, Real}, Dual{Nothing, Real, 2}) + @test value(constructor(inf)) === inf + end + for constructor in (Dual{Nothing, Scalar}, Dual{Nothing, Scalar, 2}) + @test same(value(constructor(inf)), Scalar(inf)) && iszero(partials(constructor(inf))) + end + end + for (op, expected) in ((*, +∞), (+, 1.0), (/, 0.0), (min, 1.0), (max, 0.0), (mod, 1.0), (rem, 1.0)) + @test ForwardDiff.derivative(input -> op(input, ∞), 2.0) === expected + end + @test value((Dual(2.0, 1.0) + ∞) + 3.0) === ∞ + @test ForwardDiff.derivative(input -> ForwardDiff.derivative(inner -> inner^2 + ∞, input), 2.0) === 2.0 + @test isequal(ForwardDiff.gradient(input -> input[1] * ∞, [2.0, 3.0]), [+∞, NotANumber()]) +end \ No newline at end of file From df8feffde8fa343f3ac3c7a38d8452292c147bd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4cker?= Date: Wed, 23 Sep 2026 16:51:54 +0200 Subject: [PATCH 08/14] Define a clear `RealInfinity` interface There is only a negative or a positive infinity in the real space. However, there can still be an arbitrary number of Julia implementations of both values. So keep `RealInfinity` as an abstract type and let it have a proper interface. Fixes #78 --- README.md | 27 +++++++++++++ src/Infinities.jl | 25 +++++++++--- src/algebra.jl | 1 + test/runtests.jl | 2 + test/test_real_infinity.jl | 79 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 129 insertions(+), 5 deletions(-) create mode 100644 test/test_real_infinity.jl diff --git a/README.md b/README.md index 05d49ba..10d445e 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,33 @@ This Julia package is used to represent infinities, including: Note that we subtype based on interfaces, rather than strict mathematical definitions. For example, `ℵ₀ isa Integer` as `Integer` is often used to represent the size of a set or vector. Similarly, `∞ isa Real`. +## Extending `RealInfinity` + +To add another representation of positive or negative infinity, subtype `RealInfinity` +and define `Base.signbit`: return `false` for positive infinity and `true` for negative infinity. + +```julia +struct SignedInfinity <: RealInfinity + negative::Bool +end +Base.signbit(inf::SignedInfinity) = inf.negative + +SignedInfinity(true) == -∞ # true +SignedInfinity(true)^2 === +∞ # true +``` + +Read the sign from your representation in `signbit`. Do not define it as `x < 0`, +because the inherited comparisons call `signbit` and would cause infinite recursion. +You do not need to implement arithmetic, comparisons, floating-point conversion, or +hashing: your subtype inherits these operations from `RealInfinity`. Any additional +fields you store are ignored when comparing or hashing values. Calling `zero` on your +type or an instance returns `0.0`. Calling `one` or `oneunit` returns `1.0`. + +A subtype may represent only one sign. Results need not retain its concrete type or +metadata: negation and powers may return `PositiveInfinity` or `NegativeInfinity`. +Constructors and representation-preserving conversions are the subtype's responsibility. +Specialize standard Base operations when representation preservation is needed. + ## Static.jl integration Loading [Static.jl](https://github.com/SciML/Static.jl) enables an optional package extension. diff --git a/src/Infinities.jl b/src/Infinities.jl index c2c48e8..3764777 100644 --- a/src/Infinities.jl +++ b/src/Infinities.jl @@ -53,10 +53,26 @@ oneunit(::Infinity) = 1 zero(::Infinity) = 0 zero(::Type{Infinity}) = 0 +""" + RealInfinity <: Real + +Represent a signed real infinity by subtyping `RealInfinity` and implementing `Base.signbit`. + +Every instance must represent exactly positive or negative infinity. Define +`Base.signbit(x::YourInfinity)::Bool` directly, without relying on comparisons that +use `signbit`. Additional fields do not affect numeric equality or hashing. + +Inherited operations follow the built-in signed infinities' value semantics, but +need not preserve the concrete type or its metadata. A subtype need not represent +both signs. Finite identities are `0.0` and `1.0`, including `zero`, `one`, and +`oneunit` called on the type. Define constructors and specialize Base operations +separately when representation preservation is needed. +""" abstract type RealInfinity <: Real end struct PositiveInfinity <: RealInfinity end struct NegativeInfinity <: RealInfinity end +signbit(x::RealInfinity) = throw(ArgumentError("$(typeof(x)) must implement Base.signbit")) signbit(::PositiveInfinity) = false signbit(::NegativeInfinity) = true one(::RealInfinity) = 1.0 @@ -85,11 +101,11 @@ show(io::IO, y::RealInfinity) = print(io, string(y)) Base.to_index(i::RealInfinity) = convert(Integer, i) -one(::Type{RealInfinity}) = 1.0 -oneunit(::Type{RealInfinity}) = 1.0 +one(::Type{<:RealInfinity}) = 1.0 +oneunit(::Type{<:RealInfinity}) = 1.0 oneunit(::RealInfinity) = 1.0 zero(::RealInfinity) = 0.0 -zero(::Type{RealInfinity}) = 0.0 +zero(::Type{<:RealInfinity}) = 0.0 ####### @@ -195,8 +211,7 @@ zero(::Type{ComplexInfinity}) = zero(ComplexF64) # infinities they compare equal to. The interface requires implementing `hash(x, h::UInt)`. 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) +Base.hash(x::RealInfinity, h::UInt)::UInt = hash(signbit(x) ? -Inf : Inf, h) # The two real directions have to hash like the real infinities they compare equal to. function Base.hash(x::ComplexInfinity, h::UInt)::UInt diff --git a/src/algebra.jl b/src/algebra.jl index 18b9265..66486df 100644 --- a/src/algebra.jl +++ b/src/algebra.jl @@ -136,6 +136,7 @@ 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(x::RealInfinity, p) = _infpow(RealInfinity(signbit(x)), 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 NotANumber() diff --git a/test/runtests.jl b/test/runtests.jl index 71c6359..a473df3 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -790,6 +790,8 @@ include("test_cardinality.jl") include("test_ambiguity.jl") include("test_static.jl") +include("test_real_infinity.jl") + @testset "Project quality" begin Aqua.test_all(Infinities) test_package(Infinities) diff --git a/test/test_real_infinity.jl b/test/test_real_infinity.jl new file mode 100644 index 0000000..54056d2 --- /dev/null +++ b/test/test_real_infinity.jl @@ -0,0 +1,79 @@ +using Infinities, Test + +struct SignedInfinity <: RealInfinity + negative::Bool +end +Base.signbit(inf::SignedInfinity) = inf.negative + +struct FixedInfinity{Negative} <: RealInfinity end +Base.signbit(::FixedInfinity{Negative}) where {Negative} = Negative + +struct MissingSignInfinity <: RealInfinity end + +@testset "RealInfinity interface" begin + for negative in (false, true), custom in (SignedInfinity(negative), FixedInfinity{negative}()) + canonical = RealInfinity(negative) + for operation in (+, -, signbit, sign, angle, abs, abs2, inv, float, + Float16, Float32, Float64, BigFloat, ComplexInfinity, + isinf, isfinite, isnan, iszero, isone, isinteger, isreal, + real, imag, conj, zero, one, oneunit, round, floor, ceil, trunc, repr) + @test isequal(operation(custom), operation(canonical)) + end + for InfinityType in (typeof(custom), typeof(canonical), RealInfinity) + @test zero(InfinityType) === 0.0 + @test one(InfinityType) === oneunit(InfinityType) === 1.0 + @test float(InfinityType) === Float64 + end + @test RealInfinity(custom) === custom + for InfinityType in (typeof(custom), RealInfinity, Real) + @test convert(InfinityType, custom) === custom + end + for seed in (UInt(0), UInt(123)) + @test hash(custom, seed) == hash(canonical, seed) + end + @test length(Set((custom, canonical, Float64(canonical)))) == 1 + @test Dict(canonical => :found)[custom] === :found + + for scalar in (-2, -0.0, 0, 2, 2.0, big(2.0), 2//1, Inf, -Inf, NaN, + +∞, -∞, SignedInfinity(!negative), FixedInfinity{!negative}(), NotANumber()), + operation in (+, -, *, /, ==, isequal, isless, <, <=, min, max, copysign, flipsign, + div, fld, cld, rem, divrem) + @test isequal(operation(custom, scalar), operation(canonical, scalar)) + @test isequal(operation(scalar, custom), operation(scalar, canonical)) + end + for scalar in (-2, -0.0, 0, 2, NaN) + @test isequal(mod(custom, scalar), mod(canonical, scalar)) + if !isnan(scalar) && signbit(scalar) != negative + @test_throws ArgumentError mod(scalar, custom) + else + @test isequal(mod(scalar, custom), mod(scalar, canonical)) + end + end + for exponent in (-3, -2, 0, 2, 3, 2.0, 2//1, big(2.0), Inf, -Inf, NaN, NotANumber()) + if negative && isinf(exponent) + @test_throws DomainError custom^exponent + else + @test isequal(custom^exponent, canonical^exponent) + end + end + for exponent in (0.5, 1//2) + if negative + @test_throws DomainError custom^exponent + else + @test custom^exponent === canonical^exponent + end + end + for exponent in (-2, -1, 0, 1, 2, 3) + @test isequal(Base.literal_pow(^, custom, Val(exponent)), + Base.literal_pow(^, canonical, Val(exponent))) + end + for scalar in (1+im, im*∞), operation in (+, -, *, /, ==, isequal) + @test isequal(operation(custom, scalar), operation(canonical, scalar)) + @test isequal(operation(scalar, custom), operation(scalar, canonical)) + end + end + for operation in (signbit, Float64, hash, repr, inf -> inf < 0) + @test_throws ArgumentError operation(MissingSignInfinity()) + end + @test_throws "MissingSignInfinity must implement Base.signbit" signbit(MissingSignInfinity()) +end \ No newline at end of file From 35d6f4be38454eb127fff9feb8c0ee21ea63bc8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4cker?= Date: Fri, 25 Sep 2026 10:41:11 +0200 Subject: [PATCH 09/14] Try to avoid accidental misuse I am not fully convinced, that these are a good idea. This is addressing item 2 in #7. If we merge the current change, #7, item 2 can be considered done. Otherwise we should reject it there and also consider this item done --- src/Infinities.jl | 8 ++++++++ test/runtests.jl | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/Infinities.jl b/src/Infinities.jl index 3764777..bb23f24 100644 --- a/src/Infinities.jl +++ b/src/Infinities.jl @@ -67,6 +67,9 @@ need not preserve the concrete type or its metadata. A subtype need not represen both signs. Finite identities are `0.0` and `1.0`, including `zero`, `one`, and `oneunit` called on the type. Define constructors and specialize Base operations separately when representation preservation is needed. + +Use `RealInfinity(negative::Bool)` to construct from a sign bit. This is not a numeric +conversion: `convert(RealInfinity, negative)` throws `InexactError`. """ abstract type RealInfinity <: Real end struct PositiveInfinity <: RealInfinity end @@ -81,6 +84,7 @@ RealInfinity() = PositiveInfinity() RealInfinity(::Infinity) = PositiveInfinity() RealInfinity(x::RealInfinity) = x RealInfinity(x::Bool) = ifelse(x, NegativeInfinity(), PositiveInfinity()) +convert(::Type{RealInfinity}, x::Bool) = throw(InexactError(:convert, RealInfinity, x)) PositiveInfinity(::Infinity) = PositiveInfinity() # otherwise the generic `(::Type{T})(::Infinity) where T<:Real` would route through `Inf` _convert(::Type{Float16}, x::RealInfinity) = sign(x)*Inf16 @@ -124,6 +128,9 @@ The count wraps at a full turn, so the stored `UInt64` and the directions are bi `0x8000000000000000` points along the negative real axis. Use `reinterpret(UInt64, x)` to read out the exact value. +Pass a `UInt64` directly to construct from a direction count. `convert(ComplexInfinity, count)` +throws `InexactError`, because the finite numeric value of the count is not an infinity. + Multiplying by `∞` takes the direction from the other operand, which usually reads better than naming an angle: @@ -171,6 +178,7 @@ RealInfinity(x::ComplexInfinity) = isreal(x) ? RealInfinity(signbit(x)) : convert(::Type{ComplexInfinity}, ::Infinity) = ComplexInfinity() convert(::Type{ComplexInfinity}, x::RealInfinity) = ComplexInfinity(x) +convert(::Type{ComplexInfinity}, x::UInt64) = throw(InexactError(:convert, ComplexInfinity, x)) sign(y::ComplexInfinity) = cispi(_halfturns(y)) diff --git a/test/runtests.jl b/test/runtests.jl index a473df3..286ed99 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -243,6 +243,15 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], @test convert(Float32, -∞) ≡ Float32(-∞) ≡ -Inf32 @test convert(Float16, -∞) ≡ Float16(-∞) ≡ -Inf16 @test convert(BigFloat, -∞)::BigFloat == BigFloat(-∞)::BigFloat == -BigFloat(Inf) + for negative in (false, true) + inf = RealInfinity(negative) + @test signbit(inf) === negative + @test convert(RealInfinity, inf) === inf + values = RealInfinity[inf] + @test_throws InexactError convert(RealInfinity, negative) + @test_throws InexactError values[1] = negative + @test values[1] === inf + end end @test Base.to_index(RealInfinity()) ≡ ℵ₀ @@ -255,6 +264,15 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], ComplexInfinity(ComplexInfinity()) ≡ ComplexInfinity(ℵ₀) @test convert(ComplexInfinity, -∞) ≡ -ComplexInfinity() + for turns in (UInt64(0), UInt64(1), 0x8000000000000000, typemax(UInt64)) + inf = ComplexInfinity(turns) + @test reinterpret(UInt64, inf) === turns + @test convert(ComplexInfinity, inf) === inf + values = ComplexInfinity[inf] + @test_throws InexactError convert(ComplexInfinity, turns) + @test_throws InexactError values[1] = turns + @test values[1] === inf + end # 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(-∞) From 1bcbfe3e44ad13af600e44dd8ed1bdcbf773060b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4cker?= Date: Fri, 25 Sep 2026 11:36:21 +0200 Subject: [PATCH 10/14] Give `ComplexInfinity` no ordering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The complex plane carries no order, so `Base` does not defines `isless`, `<`, `≤`, `min` and `max` for `Complex`. This package ordered a `ComplexInfinity` against a real one anyway, which was also inconsistent: `isless(im*∞, +∞)` was `true` while `isless(0, im*∞)` was a `MethodError`. Restrict the ordering to the real infinities that lie on the real line. Convert a `ComplexInfinity` with `RealInfinity` to compare it. Addresses #7, item 5 --- ext/InfinitiesStaticExt.jl | 8 ++++---- src/Infinities.jl | 4 ++++ src/compare.jl | 15 +++++++-------- src/interface.jl | 2 ++ test/runtests.jl | 4 +++- test/test_static.jl | 2 +- 6 files changed, 21 insertions(+), 14 deletions(-) diff --git a/ext/InfinitiesStaticExt.jl b/ext/InfinitiesStaticExt.jl index 8cc0337..32acfb0 100644 --- a/ext/InfinitiesStaticExt.jl +++ b/ext/InfinitiesStaticExt.jl @@ -1,7 +1,8 @@ module InfinitiesStaticExt using Infinities: Infinity, PositiveInfinity, NegativeInfinity, InfiniteCardinal, NotANumber -using Infinities: AllInfinities, AllRealInfinities, IntegerInfinities, ComplexInfinity, RealInfinity +using Infinities: AllInfinities, AllRealInfinities, IntegerInfinities, OrderedInfinities +using Infinities: ComplexInfinity, RealInfinity using Static: Static, dynamic, StaticNumber, StaticInteger, StaticFloat64, StaticInt, True for Typ in (Infinity, PositiveInfinity, NegativeInfinity, NotANumber) @@ -20,10 +21,9 @@ for (ops, Types, StaticTypes) in ( ((:+, :-, :*, :/, :(==)), (AllInfinities,), (StaticNumber,)), ((:isequal,), (NotANumber,), (StaticNumber,)), ((:div, :fld, :cld, :divrem), (IntegerInfinities,), (StaticNumber,)), - ((:<, :<=, :>, :>=), (AllInfinities,), (StaticInteger, StaticFloat64)), + ((:<, :<=, :>, :>=), (OrderedInfinities,), (StaticInteger, StaticFloat64)), ((:isless,), (AllRealInfinities, InfiniteCardinal, NotANumber), (StaticInteger, StaticFloat64)), - ((:min, :max), (AllInfinities, NotANumber), (StaticInteger,)), - ((:min, :max), (IntegerInfinities, ComplexInfinity, NotANumber), (StaticFloat64,)), + ((:min, :max), (OrderedInfinities, NotANumber), (StaticInteger, StaticFloat64)), ((:mod,), (IntegerInfinities, NotANumber), (StaticNumber,)), ((:rem,), (IntegerInfinities, NotANumber), (StaticInteger, StaticFloat64)), ((:*,), (InfiniteCardinal,), (StaticInt{0},)), diff --git a/src/Infinities.jl b/src/Infinities.jl index bb23f24..6179c26 100644 --- a/src/Infinities.jl +++ b/src/Infinities.jl @@ -131,6 +131,10 @@ read out the exact value. Pass a `UInt64` directly to construct from a direction count. `convert(ComplexInfinity, count)` throws `InexactError`, because the finite numeric value of the count is not an infinity. +The complex plane carries no order, so `isless`, `<`, `≤`, `min` and `max` have no method +here, just as they have none for `Complex`. A direction along the real axis is no exception. +Convert it with `RealInfinity` to compare it. + Multiplying by `∞` takes the direction from the other operand, which usually reads better than naming an angle: diff --git a/src/compare.jl b/src/compare.jl index 42a5b56..601ccb5 100644 --- a/src/compare.jl +++ b/src/compare.jl @@ -71,14 +71,13 @@ isapprox(::NotANumber, ::NotANumber; kwargs...) = false # `isless` is the sort order. `NaN` sorts after every other value, infinities included. isless(x::AllRealInfinities, y::AllRealInfinities) = signbit(x) && !signbit(y) @generated isless(::InfiniteCardinal{N}, ::InfiniteCardinal{M}) where {N,M} = :($(isless(N, M))) -# The leading `signbit` call discards its result. It is there to reject a non-real `Number`. -for Typ in (Number, Real, AbstractFloat) +for Typ in (Real, AbstractFloat) @eval begin - isless(x::AllRealInfinities, y::$Typ) = (signbit(y); isnan(y) || signbit(x) && y ≠ -∞) - isless(x::$Typ, y::AllRealInfinities) = (signbit(x); !isnan(x) && !signbit(y) && x ≠ ∞) + isless(x::AllRealInfinities, y::$Typ) = isnan(y) || signbit(x) && y ≠ -∞ + isless(x::$Typ, y::AllRealInfinities) = !isnan(x) && !signbit(y) && x ≠ ∞ end end -for Typ in (Number, Real, AbstractFloat, AllRealInfinities) +for Typ in (Real, AbstractFloat, AllRealInfinities) @eval begin isless(::InfiniteCardinal, x::$Typ) = isnan(x) isless(x::$Typ, y::InfiniteCardinal) = isless(x, ∞) || isless(ℵ₀, y) @@ -96,9 +95,9 @@ isless(::InfiniteCardinal{0}, ::InfiniteCardinal{0}) = false for (op, fop) in ((:max, :_max), (:min, :_min), (:<, :_lt), (:≤, :_le)) for Typ in (Real, ) @eval begin - $op(x::AllInfinities, y::$Typ) = $fop(x, y) - $op(x::$Typ, y::AllInfinities) = $fop(x, y) + $op(x::OrderedInfinities, y::$Typ) = $fop(x, y) + $op(x::$Typ, y::OrderedInfinities) = $fop(x, y) end end - @eval $op(x::AllInfinities, y::AllInfinities) = $fop(x, y) + @eval $op(x::OrderedInfinities, y::OrderedInfinities) = $fop(x, y) end diff --git a/src/interface.jl b/src/interface.jl index b2de55a..1d670b7 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -1,6 +1,8 @@ const AllInfinities = Union{Infinity, RealInfinity, ComplexInfinity, InfiniteCardinal} const AllRealInfinities = Union{Infinity, RealInfinity} const IntegerInfinities = Union{Infinity, RealInfinity, InfiniteCardinal} +# The infinities that lie on the real line and so have a place in the numeric ordering. +const OrderedInfinities = Union{Infinity, RealInfinity, InfiniteCardinal} const ExtendedComplex = Union{Complex, ComplexInfinity} iszero(::AllInfinities) = false diff --git a/test/runtests.jl b/test/runtests.jl index 286ed99..327322c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -385,7 +385,9 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], @test ComplexInfinity() == Inf # 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)*∞) + for op in (isless, <, ≤, >, ≥, min, max), + y in (5, Inf, -Inf, NaN, ∞, +∞, -∞, ℵ₀, ComplexInfinity(), -ComplexInfinity(), (1+im)*∞) + @test_throws MethodError op(ComplexInfinity(), y) @test_throws MethodError op(y, ComplexInfinity()) end diff --git a/test/test_static.jl b/test/test_static.jl index 26a9f7b..67bab8c 100644 --- a/test/test_static.jl +++ b/test/test_static.jl @@ -31,7 +31,7 @@ using Static: Static, static, dynamic, is_static, known, eq, lt, True, False args in ((inf, static(value)), (static(value), inf)) unsupported = inf isa ComplexInfinity ? - (div, fld, cld, mod, rem, divrem, isless, (isnan(value) ? () : (min, max, <, <=, >, >=))...) : () + (div, fld, cld, mod, rem, divrem, isless, min, max, <, <=, >, >=) : () unbounded = args[2] === inf && inf isa Union{Infinities.Infinity, RealInfinity, InfiniteCardinal} && !isnan(value) && signbit(value) != signbit(inf) ? (mod,) : () invalid = inf isa InfiniteCardinal && value isa Integer && value < 0 ? (unbounded..., *) : unbounded From aa976a7d410b81410030133b38579a1c6ca74946 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4cker?= Date: Fri, 25 Sep 2026 12:25:18 +0200 Subject: [PATCH 11/14] Round a finite numerator over an infinite divisor to zero MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `fld` and `cld` rounded away from zero and ignored the sign of the divisor, giving `cld(0, ∞) == 1`, `fld(-1, ∞) == -1` and `fld(1, -∞) == 0`. `Base` rounds such a quotient to zero in all three modes and signs the zero by the divisor. Follow it, so that an infinity behaves like the float infinity. An infinite or `NaN` numerator now gives `NotANumber`, as `div(Inf, Inf)` gives `NaN`. --- src/algebra.jl | 17 ++++++++--------- src/ambiguities.jl | 4 +--- test/runtests.jl | 32 ++++++++++++++++++++++++++------ test/test_ambiguity.jl | 2 +- test/test_cardinality.jl | 4 ++-- 5 files changed, 38 insertions(+), 21 deletions(-) diff --git a/src/algebra.jl b/src/algebra.jl index 66486df..3790aef 100644 --- a/src/algebra.jl +++ b/src/algebra.jl @@ -117,15 +117,14 @@ 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) ? 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) ? NotANumber() : signbit(y) ? -x : x +# A finite numerator over an infinite divisor rounds to zero in every mode. As in `Base`, that +# zero takes its sign from the divisor and its type from the numerator. +_fcdinf(x, y) = isnan(x) || isinf(x) ? NotANumber() : signbit(y) ? -zero(x) : zero(x) +div(x::Real, y::IntegerInfinities) = _fcdinf(x, y) +fld(x::Real, y::IntegerInfinities) = _fcdinf(x, y) +cld(x::Real, y::IntegerInfinities) = _fcdinf(x, y) + +_inffcd(x, y) = isnan(y) || isinf(y) ? NotANumber() : signbit(y) ? -x : x for OP in (:fld,:cld,:div) @eval begin $OP(x::IntegerInfinities, y::Real) = _inffcd(x, y) diff --git a/src/ambiguities.jl b/src/ambiguities.jl index 10b8ed0..a81a8f9 100644 --- a/src/ambiguities.jl +++ b/src/ambiguities.jl @@ -38,10 +38,8 @@ for Typ in (Rational, ) @eval rem(x::$Typ, ::IntegerInfinities) = x for op in (:fld, :cld, :div) @eval $op(x::InfiniteCardinal, y::$Typ) = _inffcd(x, y) + @eval $op(x::$Typ, y::IntegerInfinities) = _fcdinf(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 divrem(x::BigInt, y::IntegerInfinities) = (div(x, y), rem(x, y)) diff --git a/test/runtests.jl b/test/runtests.jl index 327322c..3f79774 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -88,12 +88,8 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], @test div(∞, 2) ≡ ∞ @test fld(∞, 2) ≡ ∞ @test cld(∞, 2) ≡ ∞ - @test div(2, ∞) ≡ 0 - @test fld(2, ∞) ≡ 0 - @test cld(2, ∞) ≡ 1 - @test div(-2, ∞) ≡ 0 - @test fld(-2, ∞) ≡ -1 - @test cld(-2, ∞) ≡ 0 + @test div(2, ∞) ≡ fld(2, ∞) ≡ cld(2, ∞) ≡ 0 + @test div(-2, ∞) ≡ fld(-2, ∞) ≡ cld(-2, ∞) ≡ 0 @test mod(2,∞) ≡ 2 @test div(∞,∞) isa NotANumber @test fld(∞,∞) isa NotANumber @@ -101,6 +97,30 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], @test mod(∞,∞) isa NotANumber @test mod(∞,2) isa NotANumber @test_throws ArgumentError mod(-2,∞) + + for op in (div, fld, cld), x in (0, 2, -2, 0.0, -0.0, 1.5, -1.5, 2//3, -2//3) + @test op(x, ∞) ≡ op(x, +∞) ≡ op(x, ℵ₀) ≡ zero(x) + @test op(x, -∞) ≡ -zero(x) + end + + for op in (div, fld, cld), x in (big(3), big(-3.0)) + @test op(x, ∞) == zero(x) && op(x, -∞) == zero(x) + end + + for op in (div, fld, cld), x in (Inf, -Inf, NaN), inf in (∞, +∞, -∞, ℵ₀) + @test op(x, inf) ≡ NotANumber() + @test op(inf, x) ≡ NotANumber() + end + + for op in (div, fld, cld), x in (0.0, -0.0, 1.5, -1.5), + (inf, flt) in ((∞, Inf), (+∞, Inf), (-∞, -Inf), (ℵ₀, Inf)) + + @test op(x, inf) ≡ op(x, flt) + end + + for op in (div, fld, cld), x in (0, 2, -2), inf in (∞, +∞, -∞, ℵ₀) + @test op(x, inf) ≡ 0 + end end @testset "convert" begin diff --git a/test/test_ambiguity.jl b/test/test_ambiguity.jl index b8e0a82..bda419b 100644 --- a/test/test_ambiguity.jl +++ b/test/test_ambiguity.jl @@ -12,7 +12,7 @@ @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 cld(1//2, inf) == 0 @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, ℵ₀) ≡ NotANumber() diff --git a/test/test_cardinality.jl b/test/test_cardinality.jl index 4df0252..85ce63c 100644 --- a/test/test_cardinality.jl +++ b/test/test_cardinality.jl @@ -156,8 +156,8 @@ Base.getindex(::InfVector, ::InfiniteCardinal{0}) = 42 @test ℵ₀ ÷ 5 ≡ ℵ₀ @test ℵ₀ ÷ ℵ₀ ≡ NotANumber() @test 5 ÷ ℵ₀ ≡ 0 - @test fld(-5, ℵ₀) ≡ -1 - @test cld(5, ℵ₀) ≡ 1 + @test fld(-5, ℵ₀) ≡ 0 + @test cld(5, ℵ₀) ≡ 0 @test mod(ℵ₀,ℵ₀) ≡ NotANumber() @test mod(ℵ₀,6) ≡ NotANumber() @test mod(5,ℵ₀) ≡ 5 From b7f934608c2dc08d1b1f8f7d75af085e845bc29f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4cker?= Date: Fri, 25 Sep 2026 13:25:03 +0200 Subject: [PATCH 12/14] Compare with the float division only where Base gets it right Before Julia 1.13, `Base` itself answers `fld(-1.5, Inf) === NaN` and `fld(-0.0, Inf) === -0.0`, so the comparison failed on the LTS runners. The expected zeros are still checked on every version by the loop above it. --- test/runtests.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index 3f79774..7be309a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -112,7 +112,8 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], @test op(inf, x) ≡ NotANumber() end - for op in (div, fld, cld), x in (0.0, -0.0, 1.5, -1.5), + # before 1.13, `Base` itself gives e.g. `fld(-1.5, Inf) === NaN` + VERSION ≥ v"1.13" && for op in (div, fld, cld), x in (0.0, -0.0, 1.5, -1.5), (inf, flt) in ((∞, Inf), (+∞, Inf), (-∞, -Inf), (ℵ₀, Inf)) @test op(x, inf) ≡ op(x, flt) From d254f44ece125c1daf4614b8a9f259e2a3e8556e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4cker?= Date: Fri, 25 Sep 2026 16:48:21 +0200 Subject: [PATCH 13/14] =?UTF-8?q?Divide=20an=20`Integer`=20by=20`=E2=84=B5?= =?UTF-8?q?=E2=82=80`=20in=20floating=20point?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ℵ₀` is an `Integer`, and `Base` divides two integers in floating point, so `2 / ℵ₀` is `0.0` rather than `0`. This also keeps the sign of a negative numerator, as `-2 / ℵ₀` is `-0.0`. --- src/algebra.jl | 2 ++ test/runtests.jl | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/algebra.jl b/src/algebra.jl index 3790aef..59f37a0 100644 --- a/src/algebra.jl +++ b/src/algebra.jl @@ -92,6 +92,8 @@ end # division # `\` needs nothing of its own, `Base` defining it as `y / x`. @inline _div(x, y) = x * inv(y) +# `Base` divides two `Integer`s in floating point. +@inline _div(x::Integer, y::InfiniteCardinal) = float(x) * inv(y) /(x::AllInfinities, y::Number) = _div(x, y) /(x::Number, y::AllInfinities) = _div(x, y) diff --git a/test/runtests.jl b/test/runtests.jl index 7be309a..f397823 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -569,7 +569,10 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], # 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 `∞` + @test 2 / ∞ ≡ ∞ \ 2 ≡ 0 # the type follows `inv`, which returns an `Int` for `∞` + # `ℵ₀` is an `Integer`, and `Base` divides two of them in floating point + @test 2 / ℵ₀ ≡ ℵ₀ \ 2 ≡ true / ℵ₀ ≡ 0.0 && -2 / ℵ₀ ≡ -0.0 + @test big(2) / ℵ₀ isa BigFloat && iszero(big(2) / ℵ₀) # `∞` 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 From e36bf71221b050f2a48e4bac5fda1d075c244be6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4cker?= Date: Sun, 27 Sep 2026 07:57:40 +0200 Subject: [PATCH 14/14] Opt-in for non-`Real` types to work with Infinities.jl [Infinity.jl](https://github.com/cjdoris/Infinity.jl) hs both `InfExtendedReal` and `InfExtendedTime`. In order to replace this package, Infinities.jl needs to support these use cases, too. I wanted to have one unified approach, because time is just one physical dimension and I don't think there is a reason why it should be treated separately. So I dug into the math and tried to find why intuitively we want to be able to support infinity for some types, but not for others. It seems like an [Archimedean group](https://en.wikipedia.org/wiki/Archimedean_group) is the relevant criterion. I implemented a macro so that a type can opt-in into enabling basic functionality with infinity. The suitable types from `Dates` now do that with a simple package extension. --- Project.toml | 6 +- README.md | 37 +++++++++++ ext/InfinitiesDatesExt.jl | 11 ++++ src/Infinities.jl | 2 + src/archimedean.jl | 125 ++++++++++++++++++++++++++++++++++++++ src/interface.jl | 1 + test/runtests.jl | 1 + test/test_archimedean.jl | 84 +++++++++++++++++++++++++ 8 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 ext/InfinitiesDatesExt.jl create mode 100644 src/archimedean.jl create mode 100644 test/test_archimedean.jl diff --git a/Project.toml b/Project.toml index d1b6578..8c0bfe6 100644 --- a/Project.toml +++ b/Project.toml @@ -4,16 +4,19 @@ authors = ["Sheehan Olver "] version = "0.1.13" [weakdeps] +Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" Static = "aedffcd0-7271-4cad-89d0-dc628f76c6d3" [extensions] +InfinitiesDatesExt = "Dates" InfinitiesForwardDiffExt = "ForwardDiff" InfinitiesStaticExt = "Static" [compat] Aqua = "0.8" Base64 = "1" +Dates = "1" ForwardDiff = "1" JET = "0.9, 0.10, 0.11, 0.12" Static = "1.4" @@ -23,10 +26,11 @@ julia = "1.10" [extras] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" +Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" Static = "aedffcd0-7271-4cad-89d0-dc628f76c6d3" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Aqua", "Base64", "ForwardDiff", "JET", "Static", "Test"] +test = ["Aqua", "Base64", "Dates", "ForwardDiff", "JET", "Static", "Test"] diff --git a/README.md b/README.md index 10d445e..6ad715a 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,43 @@ metadata: negation and powers may return `PositiveInfinity` or `NegativeInfinity Constructors and representation-preserving conversions are the subtype's responsibility. Specialize standard Base operations when representation preservation is needed. +## Comparing other types with infinity + +With `Infinities.@archimedean` you can opt-in a non-`Real` type to compare with `∞`, `+∞` +and `-∞`, and support `+` and `-` with them. The name refers to +[Archimedean groups](https://en.wikipedia.org/wiki/Archimedean_group), in which adding any +positive value often enough exceeds every other value. Declare a type whose values lie on an +unbounded additive scale, i.e. it _models_ something unbounded, such as lengths or dates, and +which defines `isless`: + +```julia +struct Meter + value::Rational{Int} +end +Base.isless(a::Meter, b::Meter) = isless(a.value, b.value) +Infinities.@archimedean Meter + +-∞ < Meter(3//2) < ∞ # true +Meter(3//2) - ∞ === -∞ # true +``` + +A scale bounded on one side, i.e. something which is bounded on one side and unbounded on the +other, names its unbounded end, as in `@archimedean +∞ Kelvin` where positive temperatures can +go to infinity, but negative temperatures aren't a thing (neglecting thermodynamic temperature). +Cyclic types such as a time of day and orders without a fixed step such as strings do not +qualify. The docstring of `@archimedean` explains when a declaration is valid. + +Loading Dates enables an extension that declares the periods, `Date` and `DateTime`: + +```julia +using Infinities, Dates + +Date(2026, 9, 27) < ∞ # true +Day(3) + ∞ === ∞ # true +``` + +`Dates.Time` is left out, because a time of day wraps at midnight. + ## Static.jl integration Loading [Static.jl](https://github.com/SciML/Static.jl) enables an optional package extension. diff --git a/ext/InfinitiesDatesExt.jl b/ext/InfinitiesDatesExt.jl new file mode 100644 index 0000000..5271693 --- /dev/null +++ b/ext/InfinitiesDatesExt.jl @@ -0,0 +1,11 @@ +module InfinitiesDatesExt + +using Infinities: @archimedean +using Dates: Year, Quarter, Month, Week, Day, Hour, Minute, Second, Millisecond, Microsecond, + Nanosecond, Date, DateTime + +# `Time` is left out, as it models a bounded quantity which wraps at midnight. +# Calendar periods are not comparable with fixed-duration periods. +@archimedean Year Quarter Month Week Day Hour Minute Second Millisecond Microsecond Nanosecond Date DateTime + +end diff --git a/src/Infinities.jl b/src/Infinities.jl index 6179c26..e3aaacf 100644 --- a/src/Infinities.jl +++ b/src/Infinities.jl @@ -10,6 +10,7 @@ import Base: angle, isone, iszero, isinf, isfinite, isnan, isreal, abs, one, one export ∞, ℵ₀, ℵ₁, RealInfinity, ComplexInfinity, InfiniteCardinal, NotANumber, PositiveInfinity, NegativeInfinity # The following is commented out for now to avoid conflicts with Infinity.jl # export Infinity +VERSION >= v"1.11.0-DEV.469" && eval(Meta.parse("public @archimedean")) """ NotANumber() @@ -237,5 +238,6 @@ include("cardinality.jl") include("interface.jl") include("compare.jl") include("algebra.jl") +include("archimedean.jl") include("ambiguities.jl") end # module diff --git a/src/archimedean.jl b/src/archimedean.jl new file mode 100644 index 0000000..6e371a1 --- /dev/null +++ b/src/archimedean.jl @@ -0,0 +1,125 @@ +""" + @archimedean T₁ T₂ ... + @archimedean ±∞ T₁ T₂ ... + @archimedean +∞ T₁ T₂ ... + @archimedean -∞ T₁ T₂ ... + +Declare each listed type `T` to model an Archimedean scale, so that its values lie +strictly between `-∞` and `+∞`. + +The declaration defines `isless`, `+` and `-` between `T` and the real infinities `∞`, `+∞` +and `-∞`. `Base` builds `<`, `≤`, `>`, `≥`, `min`, `max` and sorting on `isless`. +On a scale unbounded in both directions, an infinity absorbs every value of `T`, so a sum or +difference is the infinity itself, negated when it is subtracted: `x + ∞ == ∞ ± x == ∞` and +`x - ∞ == -∞ ± x == -∞`. +For a position such as a `Date`, the `∞` in `x + ∞` is an infinitely long step, and the `∞` +in `∞ - x` is the end of the scale. Multiplication and division are left out, because a +position cannot be meaningfully scaled. + +List types as separate macro arguments. The direction is optional. Without it, both ends are +unbounded. + +Each type must define `Base.isless(x::T, y::T)` as a strict total order consistent with `isequal`. + +# When to declare + +Declare `T` if its values are magnitudes or positions on a scale that is unbounded in both +directions and on which adding the same positive step repeatedly gets past any value. A +magnitude has a natural zero, like a `Period`. A position is measured from an arbitrary origin, +like a `Date`, and the difference of two positions is a magnitude, as the difference of two +`Date`s is a `Period`. In measurement theory, a magnitude scale is a ratio scale, and a position +scale is an interval scale. For magnitudes, the condition on steps is the Archimedean property: +for every `x > 0` and every `y`, some multiple `n*x` exceeds `y`. For positions, it holds for +their differences. Hölder's theorem shows that, up to the choice of unit and origin, such a +scale fits into the real line. So `-∞` lies below and `+∞` above all of its values in every unit. + +# Scales bounded on one side + +Some scales end at one of their own values on one side and go on without limit on the other. +Absolute temperature, for example, ends at absolute zero. Hölder's theorem also covers such +scales and fits them into a half-line. Then only one infinity lies on the unbounded side, and +the optional direction names it. Comparisons are the same as on a scale unbounded in both +directions. For `@archimedean +∞ T`, `-∞ < x` still holds for every value `x`. `+` and `-` are +defined only where the result is `+∞`: `x + ∞`, `∞ + x` and `∞ - x` give `∞`, and `x - (-∞)` +gives `+∞`. `x - ∞`, `x + (-∞)`, `-∞ + x` and `-∞ - x` have no method, because their result +`-∞` would lie below the end of the scale. `@archimedean -∞ T` mirrors this. + +# The model decides, not the representation + +Declare a type for what it models, even where its representation falls short. The same reasoning +lets every `Real` compare with `∞`. `Int8` arithmetic wraps from `127` to `-128`, yet `Base` +orders `Int8` like the integers and treats the wrap as a limit of the 8-bit representation. +`Float64` models the real numbers, although the gaps between its values grow with their size. +Likewise, a `Year` models a number of years without upper limit, although its count stops at +`typemax(Int64)`. + +Do not declare + +- a cyclic type, where wrapping around is part of the meaning. A time of day (`Dates.Time`) + returns to the same value after 24 hours, and a weekday after seven days. +- an order without a fixed step, such as strings in lexicographic order. Infinitely many + strings lie between `"a"` and `"b"`, and appending the same characters again and again + never gets from `"a"` past `"b"`. +- a partial order, in which some values are not comparable, such as sets ordered by inclusion. +- a type with infinite or unordered values of its own, such as `NaN`. The declaration would + still order them below `∞`. +- a subtype of `Real`, which already compares with the infinities. + +Call the macro at top level, either in the module that owns `T` or in a package extension that +adds support for `T`. Anywhere else, the new methods are type piracy. + +# Examples + +```julia +julia> struct Meter + value::Rational{Int} + end + +julia> Base.isless(a::Meter, b::Meter) = isless(a.value, b.value) + +julia> Infinities.@archimedean Meter + +julia> -∞ < Meter(3//2) < ∞ +true + +julia> max(Meter(3//2), ∞) +∞ + +julia> Meter(3//2) - ∞ +-∞ +``` +""" +macro archimedean(direction::Union{Symbol, Expr}, args::Union{Symbol, Expr}...) + direction === :∞ && throw(ArgumentError("the direction has to be `±∞`, `+∞` or `-∞`")) + unbounded, negated_unbounded = + direction == :(+∞) ? (PositiveRealInfinities, NegativeInfinity) : + direction == :(-∞) ? (NegativeInfinity, PositiveRealInfinities) : + (AllRealInfinities, AllRealInfinities) + types = direction in (:(±∞), :(+∞), :(-∞)) ? args : (direction, args...) + isempty(types) && throw(ArgumentError("`@archimedean` needs at least one type after the direction")) + any(type -> type in (:(±∞), :(+∞), :(-∞), :∞), types) && + throw(ArgumentError("an infinity selector can appear only as the first argument")) + declarations = map(type -> archimedean(esc(type), unbounded, negated_unbounded), types) + Expr(:block, declarations...) +end + +""" + archimedean(T::Expr, unbounded::Type, negated_unbounded::Type) -> Expr + +Build the expression defining comparison and arithmetic methods between the scale and +its infinities. + +`unbounded` is the type accepted for infinity operands at unbounded ends of the scale. +`negated_unbounded` is the type accepted for subtrahends whose negatives are those ends. +""" +function archimedean(T::Expr, unbounded::Type, negated_unbounded::Type) + quote + Base.isless(x::AllRealInfinities, ::$T) = signbit(x) + Base.isless(x::$T, y::AllRealInfinities) = !isless(y, x) + Base.:+(::$T, y::$unbounded) = y + Base.:+(x::$unbounded, ::$T) = x + Base.:-(x::$unbounded, ::$T) = x + Base.:-(::$T, y::$negated_unbounded) = -y + nothing + end +end diff --git a/src/interface.jl b/src/interface.jl index 1d670b7..88cb838 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -1,5 +1,6 @@ const AllInfinities = Union{Infinity, RealInfinity, ComplexInfinity, InfiniteCardinal} const AllRealInfinities = Union{Infinity, RealInfinity} +const PositiveRealInfinities = Union{Infinity, PositiveInfinity} const IntegerInfinities = Union{Infinity, RealInfinity, InfiniteCardinal} # The infinities that lie on the real line and so have a place in the numeric ordering. const OrderedInfinities = Union{Infinity, RealInfinity, InfiniteCardinal} diff --git a/test/runtests.jl b/test/runtests.jl index f397823..b2477c8 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -833,6 +833,7 @@ end include("test_cardinality.jl") include("test_ambiguity.jl") include("test_static.jl") +include("test_archimedean.jl") include("test_real_infinity.jl") diff --git a/test/test_archimedean.jl b/test/test_archimedean.jl new file mode 100644 index 0000000..3ea198e --- /dev/null +++ b/test/test_archimedean.jl @@ -0,0 +1,84 @@ +using Dates: Dates, Year, Quarter, Month, Week, Day, Hour, Minute, Second, + Millisecond, Microsecond, Nanosecond, Date, DateTime, Time + +struct Meters + value::Rational{Int} +end +Base.isless(a::Meters, b::Meters) = isless(a.value, b.value) +Infinities.@archimedean ±∞ Meters + +# bounded below by absolute zero +struct Kelvin + value::Rational{Int} +end +Base.isless(a::Kelvin, b::Kelvin) = isless(a.value, b.value) +Infinities.@archimedean +∞ Kelvin + +# bounded above by zero +struct Depth + value::Rational{Int} +end +Base.isless(a::Depth, b::Depth) = isless(a.value, b.value) +Infinities.@archimedean -∞ Depth + +@testset "Archimedean types" begin + @testset "declared with the macro" begin + @test Meters(1) < Meters(2) + @test -∞ < Meters(3) < ∞ && Meters(3) < +∞ && Meters(3) ≤ ∞ && ∞ ≥ Meters(3) + @test !(∞ < Meters(3)) && !(Meters(3) < -∞) && Meters(3) ≠ ∞ + @test max(Meters(3), ∞) ≡ ∞ && min(Meters(3), -∞) ≡ -∞ && min(Meters(3), ∞) ≡ Meters(3) + @test isequal(sort([∞, Meters(2), -∞, Meters(1)]), [-∞, Meters(1), Meters(2), ∞]) + @test Meters(3) + ∞ ≡ ∞ + Meters(3) ≡ ∞ - Meters(3) ≡ ∞ && Meters(3) - ∞ ≡ -∞ + @test Meters(3) - (-∞) ≡ +∞ && -∞ + Meters(3) ≡ Meters(3) + (-∞) ≡ -∞ + # a cardinal counts elements, it is no end of a scale + @test_throws MethodError Meters(3) < ℵ₀ + end + + @testset "bounded on one side" begin + for value in (Kelvin(300), Depth(-5)) + @test -∞ < value < ∞ + end + + for (value, end_infinity) in ((Kelvin(300), ∞), (Kelvin(300), +∞), (Depth(-5), -∞)) + @test value + end_infinity ≡ end_infinity + value ≡ end_infinity - value ≡ end_infinity + end + for (value, subtrahend, result) in ( + (Kelvin(300), -∞, +∞), (Depth(-5), ∞, -∞), (Depth(-5), +∞, -∞), + ) + @test value - subtrahend ≡ result + end + + # Operations producing the other infinity have no method. + for (value, end_infinity) in ((Kelvin(300), +∞), (Depth(-5), -∞)), + operation in ( + (scale_value, infinity) -> scale_value - infinity, + (scale_value, infinity) -> scale_value + (-infinity), + (scale_value, infinity) -> -infinity + scale_value, + (scale_value, infinity) -> -infinity - scale_value, + ) + + @test_throws MethodError operation(value, end_infinity) + end + @test_throws ArgumentError macroexpand(@__MODULE__, :(Infinities.@archimedean Kelvin +∞)) + @test_throws ArgumentError macroexpand(@__MODULE__, :(Infinities.@archimedean ∞ Kelvin)) + @test_throws ArgumentError macroexpand(@__MODULE__, :(Infinities.@archimedean +∞)) + end + + @testset "Dates extension" begin + ext = Base.get_extension(Infinities, :InfinitiesDatesExt) + @test !isnothing(ext) + @test isempty(Test.detect_ambiguities(Infinities, Dates, ext)) + @test Year(1) > Quarter(1) > Month(1) + @test Week(1) > Day(1) > Hour(1) + @test_throws MethodError Year(1) < Week(1) + for x in (Year(14), Quarter(2), Month(5), Week(2), Day(-3), Hour(14), Minute(30), + Second(1), Millisecond(5), Microsecond(2), Nanosecond(1), + Date(2026, 9, 25), DateTime(2026, 9, 25, 12)) + @test -∞ < x < ∞ && x < +∞ && !(∞ ≤ x) && !(x ≤ -∞) + @test max(x, ∞) ≡ ∞ && min(x, -∞) ≡ -∞ && isequal(min(x, ∞), x) + @test x + ∞ ≡ ∞ + x ≡ ∞ - x ≡ ∞ && x - ∞ ≡ -∞ + x ≡ -∞ + end + # a time of day wraps at midnight + @test_throws MethodError Time(12) < ∞ + end +end