From 21ce1b716291b3a259be5d176dfc8b331ff3c743 Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Thu, 6 Aug 2026 05:47:24 -0500 Subject: [PATCH] Fast paths for `ExactReal` with `BareInterval` An `ExactReal` is a point, so `+`, `-`, `*`, `/` and `\` against a `BareInterval` of the same number type apply the operation to the two endpoints directly, rather than promoting the point to a thin interval and running the interval-interval operator on it. Each method takes that route only where it is bitwise identical to the promotion, and delegates the rest: a non-finite point, whose thin interval is ill-formed; an unbounded interval under `*`, where `0 * Inf` is flavor dependent; and division by a zero point. Rounding still goes through `@round`, so the results are unchanged under every rounding mode. Mixed number types keep the promotion, which is where the point is rounded to the interval's number type. The type parameter is bounded by `NumTypes` so that these methods are more specific than the generic `ExactReal`/`BareInterval` methods, whose own parameters are bounded by `Real` and `NumTypes`; the tests assert that dispatch selects them. Point times interval costs 3.2 ns against the promotion route's 6.4-8.2 ns with rounding `:none`, and 6.7 ns against 9.1-10.2 ns with `:correct`. Assisted-by: Claude Opus 5 (claude-opus-5) --- src/intervals/exact_literals.jl | 51 ++++++++++++++++++++++++ src/intervals/intervals.jl | 7 +++- test/interval_tests/exact_literals.jl | 56 +++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 2 deletions(-) diff --git a/src/intervals/exact_literals.jl b/src/intervals/exact_literals.jl index 013f0eea..d240db3f 100644 --- a/src/intervals/exact_literals.jl +++ b/src/intervals/exact_literals.jl @@ -261,6 +261,57 @@ for f ∈ (:+, :-, :*, :/, :\, :^) @eval Base.$f(x::ExactReal, y::BareInterval) = $f(promote(x, y)...) end +# These specializations improve efficiency, but always agree precisely with +# the promoting fallbacks above. Several circumstances fall back to promotion: a +# non-finite point (whose thin interval is not even valid), an unbounded interval +# times a point (`0 * ∞` is flavor dependent), and division by zero (likewise). +# The type parameter is bounded so that these methods are more specific than the +# generic `Base.$f(::ExactReal, ::BareInterval)`, whose own parameters are bounded +# by `Real` and `NumTypes`. + +function Base.:+(x::BareInterval{T}, y::ExactReal{T}) where {T<:NumTypes} + v = y.value + isfinite(v) || return +(promote(x, y)...) + isempty_interval(x) && return x + return @round(T, inf(x) + v, sup(x) + v) +end +Base.:+(x::ExactReal{T}, y::BareInterval{T}) where {T<:NumTypes} = y + x + +function Base.:-(x::BareInterval{T}, y::ExactReal{T}) where {T<:NumTypes} + v = y.value + isfinite(v) || return -(promote(x, y)...) + isempty_interval(x) && return x + return @round(T, inf(x) - v, sup(x) - v) +end + +function Base.:-(x::ExactReal{T}, y::BareInterval{T}) where {T<:NumTypes} + v = x.value + isfinite(v) || return -(promote(x, y)...) + isempty_interval(y) && return y + return @round(T, v - sup(y), v - inf(y)) +end + +function Base.:*(x::BareInterval{T}, y::ExactReal{T}) where {T<:NumTypes} + v = y.value + isempty_interval(x) && return x # `isbounded` is true for the empty interval + (isfinite(v) & isbounded(x)) || return *(promote(x, y)...) + signbit(v) && return @round(T, v * sup(x), v * inf(x)) + return @round(T, v * inf(x), v * sup(x)) +end +Base.:*(x::ExactReal{T}, y::BareInterval{T}) where {T<:NumTypes} = y * x + +function Base.:/(x::BareInterval{T}, y::ExactReal{T}) where {T<:NumTypes} + v = y.value + isempty_interval(x) && return x + (isfinite(v) & !iszero(v)) || return /(promote(x, y)...) + signbit(v) && return @round(T, sup(x) / v, inf(x) / v) + return @round(T, inf(x) / v, sup(x) / v) +end + +# `x \ y` is `y / x`; the reverse case is a point divided by an interval, which +# has no shortcut — the zero-crossing tree of `/` is the whole computation. +Base.:\(x::ExactReal{T}, y::BareInterval{T}) where {T<:NumTypes} = y / x + """ has_exact_display(x::Real) diff --git a/src/intervals/intervals.jl b/src/intervals/intervals.jl index 280002ea..053952a1 100644 --- a/src/intervals/intervals.jl +++ b/src/intervals/intervals.jl @@ -5,12 +5,15 @@ include("construction.jl") include("parsing.jl") include("real_interface.jl") export numtype -include("exact_literals.jl") - export ExactReal, exact, @exact, has_exact_display # Rounding include("rounding.jl") +# after `rounding.jl`: the interval operations on `ExactReal` round their +# endpoints with `@round` +include("exact_literals.jl") + export ExactReal, exact, @exact, has_exact_display + # Flavor include("flavor.jl") diff --git a/test/interval_tests/exact_literals.jl b/test/interval_tests/exact_literals.jl index 5c76832b..b740aa68 100644 --- a/test/interval_tests/exact_literals.jl +++ b/test/interval_tests/exact_literals.jl @@ -98,3 +98,59 @@ @test val2 == 3.0 end end + +@testset "Exact literals with bare intervals" begin + # `+`, `-`, `*`, `/` and `\` on a matching number type bypass the promotion to + # a thin interval, so they must be more specific than the generic methods and + # must return exactly what those return. + specialized(f, S, R) = which(f, Tuple{S,R}).sig isa UnionAll + @test all(f -> specialized(f, BareInterval{Float64}, ExactReal{Float64}), (+, -, *, /)) + @test all(f -> specialized(f, ExactReal{Float64}, BareInterval{Float64}), (+, -, *, \)) + + # ... and the mixed number types must not, since the promotion is where the + # `ExactReal` gets rounded to the interval's number type. + @test !specialized(*, BareInterval{Float64}, ExactReal{Int}) + + viapromotion(f, x, y) = f(promote(x, y)...) + samebits(x::BareInterval, y::BareInterval) = (inf(x) === inf(y)) & (sup(x) === sup(y)) + + for T ∈ (Float64, Float32, Rational{Int}) + vals = T <: Rational ? + (zero(T), one(T), -one(T), T(1//2), T(-5//3)) : + (zero(T), -zero(T), one(T), -one(T), T(0.5), T(-2.5), T(0.1), T(-0.1), + floatmax(T), floatmin(T)) + ivs = (bareinterval(T, 1, 2), bareinterval(T, -2, -1), bareinterval(T, -1, 2), + bareinterval(T, 0, 1), bareinterval(T, -1, 0), bareinterval(T, 0, 0), + bareinterval(T(1//10), T(1//10)), bareinterval(T, -Inf, 2), + bareinterval(T, 3, Inf), entireinterval(BareInterval{T}), + emptyinterval(BareInterval{T})) + for v ∈ vals, x ∈ ivs + k = exact(v) + @test samebits(x + k, viapromotion(+, x, k)) + @test samebits(k + x, viapromotion(+, k, x)) + @test samebits(x - k, viapromotion(-, x, k)) + @test samebits(k - x, viapromotion(-, k, x)) + @test samebits(x * k, viapromotion(*, x, k)) + @test samebits(k * x, viapromotion(*, k, x)) + @test samebits(x / k, viapromotion(/, x, k)) + @test samebits(k \ x, viapromotion(\, k, x)) + end + end + + # A non-finite `ExactReal` has no thin interval; both routes warn and return + # the empty interval. + let x = bareinterval(1.0, 2.0) + for v ∈ (Inf, -Inf, NaN) + @test isempty_interval(@test_logs (:warn,) x + exact(v)) + @test isempty_interval(@test_logs (:warn,) x - exact(v)) + @test isempty_interval(@test_logs (:warn,) exact(v) - x) + @test isempty_interval(@test_logs (:warn,) x * exact(v)) + @test isempty_interval(@test_logs (:warn,) x / exact(v)) + end + end + + # Mixed number types still round through the promotion. + @test isequal_interval(bareinterval(1.0, 2.0) * exact(2), bareinterval(2.0, 4.0)) + @test isequal_interval(bareinterval(1.0, 2.0) + exact(1//3), + bareinterval(1.0, 2.0) + bareinterval(Float64, 1//3)) +end