From ae6fa99a77656de5f0530fc409afb500deddf32d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4cker?= Date: Sun, 6 Sep 2026 07:01:55 +0200 Subject: [PATCH 1/3] Rename `_sb` to `_halfturns` `_sb` was short for `signbit`. The function returns a count of half turns. For a real number that count is the sign bit, which is where the old name came from. The new name says which unit the caller gets. --- src/algebra.jl | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/algebra.jl b/src/algebra.jl index 669cc4a..5e01508 100644 --- a/src/algebra.jl +++ b/src/algebra.jl @@ -44,13 +44,14 @@ # multiplication -@inline _sb(x) = signbit(x) -@inline _sb(x::Complex) = angle(x)/π # overloading `signbit` causes type piracy - -@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 direction of an infinity in half turns, which for a real is its sign bit. +@inline _halfturns(x) = signbit(x) +@inline _halfturns(x::Complex) = angle(x)/π + +@inline __mul(x, y::AllInfinities) = RealInfinity(_halfturns(x) ⊻ _halfturns(y)) +@inline __mul(x, y::ComplexInfinity) = ComplexInfinity(_halfturns(x) + _halfturns(y)) +@inline __mul(x, y::ComplexInfinity{Bool}) = ComplexInfinity(_halfturns(x) ⊻ _halfturns(y)) +@inline __mul(x::Complex, y::ComplexInfinity{Bool}) = ComplexInfinity(_halfturns(x) + _halfturns(y)) @inline __mul(x::Integer, y::InfiniteCardinal) = x > 0 ? y : throw(ArgumentError("Cannot multiply $x * $y")) @inline _mul(x, y) = iszero(x) ? throw(ArgumentError("Cannot multiply $x * $y")) : __mul(infpromote(x, y)...) From bd4545c158c08a465d0ca15fcda55561270b6097 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4cker?= Date: Sun, 6 Sep 2026 07:04:11 +0200 Subject: [PATCH 2/3] Give `toinf` the half turns that `ComplexInfinity` counts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ComplexInfinity` stores its direction in half turns, but `toinf` filled the field with the radian `angle(x)`. The direction of an infinite complex summand was therefore off by a factor of π: `angle(toinf(complex(0,Inf)))` gave 4.93 instead of `π/2`. `_infadd` compares those angles, so a sum whose parts point the same way threw although `==` called them equal. Both `complex(-Inf,0.0) + -∞` and `complex(0,Inf) + ComplexInfinity(0.5)` raised an ArgumentError. Only the positive real axis escaped, angle `0` being the fixed point of the missing scaling. `_halfturns` is the conversion the multiplication already uses. It moves above the addition section, because both sections use it now. --- src/algebra.jl | 11 ++++++----- test/runtests.jl | 12 ++++++++++++ 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/algebra.jl b/src/algebra.jl index 5e01508..7cdd946 100644 --- a/src/algebra.jl +++ b/src/algebra.jl @@ -17,9 +17,14 @@ -(::InfiniteCardinal) = -∞ +# The direction of an infinity in half turns, which for a real is its sign bit. +@inline _halfturns(x) = signbit(x) +@inline _halfturns(x::Complex) = angle(x)/π + + # addition @inline toinf(x) = RealInfinity(signbit(x)) -@inline toinf(x::Complex) = ComplexInfinity(angle(x)) +@inline toinf(x::Complex) = ComplexInfinity(_halfturns(x)) @inline toinf(x::ComplexInfinity) = x @inline _infadd(x, y) = angle(x) == angle(y) ? y : throw(ArgumentError("Angles must be the same to add ∞")) @@ -44,10 +49,6 @@ # multiplication -# The direction of an infinity in half turns, which for a real is its sign bit. -@inline _halfturns(x) = signbit(x) -@inline _halfturns(x::Complex) = angle(x)/π - @inline __mul(x, y::AllInfinities) = RealInfinity(_halfturns(x) ⊻ _halfturns(y)) @inline __mul(x, y::ComplexInfinity) = ComplexInfinity(_halfturns(x) + _halfturns(y)) @inline __mul(x, y::ComplexInfinity{Bool}) = ComplexInfinity(_halfturns(x) ⊻ _halfturns(y)) diff --git a/test/runtests.jl b/test/runtests.jl index a1d5069..e41282c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -289,6 +289,18 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], @test exp(im*π/4)+∞ == ∞ @test Inf + im + ∞ ≡ ComplexInfinity() + # An infinite summand reaches `_infadd` through `toinf`, which has to answer in half turns. + @test complex(Inf, 0.0) + ∞ ≡ ComplexInfinity() + @test complex(-Inf, 0.0) + (-∞) ≡ -ComplexInfinity() + @test complex(0.0, Inf) + ComplexInfinity(1/2) ≡ ComplexInfinity(1/2) + @test complex(0.0, -Inf) + ComplexInfinity(-1/2) ≡ ComplexInfinity(-1/2) + @test_throws ArgumentError complex(0.0, Inf) + ∞ + + # Two infinite parts are the only way an infinite `Complex` points off the axes. + for (x, y, s) in ((Inf, Inf, 1/4), (-Inf, Inf, 3/4), (-Inf, -Inf, -3/4), (Inf, -Inf, -1/4)) + @test complex(x, y) + ComplexInfinity(s) ≡ ComplexInfinity(s) + end + @test Inf == ComplexInfinity() @test ComplexInfinity() == Inf From 7d555d3c9763d9d175326a78a3291fbda35cb651 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4cker?= Date: Sun, 6 Sep 2026 07:06:46 +0200 Subject: [PATCH 3/3] Test how exact the direction of a complex factor is MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `exp(im*π/4)*∞ == Inf+im*Inf` held by coincidence. `angle(x)/π` round-trips exactly only on the axes, where `angle` returns exactly `0`, `±π/2` or `π`. Off the axes `π/4` happens to come back unchanged, while the adjacent `π/8` gives 0.12500000000000003 and a sweep of `k/2^12` misses 2000 of 8192 angles. That assertion now compares the angle with `≈`. The four axes go through one loop, which also compares the infinity with the infinite `Complex` itself. That comparison is the only test in the suite reaching `==(::AllInfinities, ::Complex)`, and on an axis it is exact. Two assertions say what a rescaling does. `angle` is scale invariant wherever the scaling itself is exact, so a direction only moves when the rescaled operand has to be rounded. --- test/runtests.jl | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index e41282c..e51b1e5 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -283,9 +283,21 @@ Base.iterate(s::CharString, i::Integer=1) = i ≤ length(s.chars) ? (s.chars[i], ComplexInfinity() * ∞ ≡ ComplexInfinity() * RealInfinity() ≡ ComplexInfinity() @test 2.0im*∞ ≡ ∞*2.0im ≡ 2.0im * RealInfinity() ≡ RealInfinity() * 2.0im ≡ ComplexInfinity(1/2) + # On an axis `angle(x)/π` is exact, `angle` giving exactly `0`, `±π/2` or `π` and halving being exact. + for (z, s, w) in ((2.0+0.0im, 0.0, complex(Inf, 0.0)), (2.0im, 0.5, complex(0.0, Inf)), + (-2.0+0.0im, 1.0, complex(-Inf, 0.0)), (-2.0im, -0.5, complex(0.0, -Inf))) + @test z*∞ ≡ ComplexInfinity(s) + @test z*∞ == w + end @test 2ComplexInfinity() ≡ ComplexInfinity()*2 ≡ ComplexInfinity() - @test exp(im*π/4)*∞ == Inf+im*Inf + # Off the axes it is not exact in general: `angle(exp(im*π/8))/π` gives 0.12500000000000003. + for θ in (π/8, π/4, 0.3, -2.4) + @test angle(exp(im*θ)*∞) ≈ angle(∞*exp(im*θ)) ≈ θ + end + # The direction is the one the operand actually has, so rescaling keeps it only while it is exact. + @test 4*(0.3+0.1im)*∞ ≡ (0.3+0.1im)*∞ + @test 3*(0.3+0.1im)*∞ ≢ (0.3+0.1im)*∞ @test exp(im*π/4)+∞ == ∞ @test Inf + im + ∞ ≡ ComplexInfinity()