Skip to content

Commit

Permalink
Add with_overflow for add, sub, and mul (#102)
Browse files Browse the repository at this point in the history
* Fix ambiguities.

* Add tests.

* Comment

* Simplify logic.
  • Loading branch information
mbarbar authored Jan 16, 2025
1 parent 27c92b2 commit 6c6df97
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 9 deletions.
42 changes: 33 additions & 9 deletions src/FixedPointDecimals.jl
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,32 @@ end

# --- Checked arithmetic ---

function Base.add_with_overflow(x::T, y::T) where {T<:FD}
z, b = Base.add_with_overflow(x.i, y.i)
return (reinterpret(T, z), b)
end
Base.Checked.add_with_overflow(x::FD, y::FD) = Base.Checked.add_with_overflow(promote(x, y)...)
Base.Checked.add_with_overflow(x::FD, y) = Base.Checked.add_with_overflow(promote(x, y)...)
Base.Checked.add_with_overflow(x, y::FD) = Base.Checked.add_with_overflow(promote(x, y)...)

function Base.sub_with_overflow(x::T, y::T) where {T<:FD}
z, b = Base.sub_with_overflow(x.i, y.i)
return (reinterpret(T, z), b)
end
Base.Checked.sub_with_overflow(x::FD, y::FD) = Base.Checked.sub_with_overflow(promote(x, y)...)
Base.Checked.sub_with_overflow(x::FD, y) = Base.Checked.sub_with_overflow(promote(x, y)...)
Base.Checked.sub_with_overflow(x, y::FD) = Base.Checked.sub_with_overflow(promote(x, y)...)

function Base.Checked.mul_with_overflow(x::FD{T,f}, y::FD{T,f}) where {T<:Integer,f}
powt = coefficient(FD{T, f})
quotient, remainder = fldmodinline(_widemul(x.i, y.i), powt)
v = _round_to_nearest(quotient, remainder, powt)
return (reinterpret(FD{T,f}, Base.trunc_int(T, v)), v < typemin(T) || v > typemax(T))
end
Base.Checked.mul_with_overflow(x::FD, y::FD) = Base.Checked.mul_with_overflow(promote(x, y)...)
Base.Checked.mul_with_overflow(x::FD, y) = Base.Checked.mul_with_overflow(promote(x, y)...)
Base.Checked.mul_with_overflow(x, y::FD) = Base.Checked.mul_with_overflow(promote(x, y)...)

Base.checked_add(x::FD, y::FD) = Base.checked_add(promote(x, y)...)
Base.checked_sub(x::FD, y::FD) = Base.checked_sub(promote(x, y)...)
Base.checked_mul(x::FD, y::FD) = Base.checked_mul(promote(x, y)...)
Expand Down Expand Up @@ -424,21 +450,19 @@ Base.checked_mod(x::FD, y) = Base.checked_mod(promote(x, y)...)
Base.checked_mod(x, y::FD) = Base.checked_mod(promote(x, y)...)

function Base.checked_add(x::T, y::T) where {T<:FD}
z, b = Base.add_with_overflow(x.i, y.i)
z, b = Base.Checked.add_with_overflow(x, y)
b && Base.Checked.throw_overflowerr_binaryop(:+, x, y)
return reinterpret(T, z)
return z
end
function Base.checked_sub(x::T, y::T) where {T<:FD}
z, b = Base.sub_with_overflow(x.i, y.i)
z, b = Base.Checked.sub_with_overflow(x, y)
b && Base.Checked.throw_overflowerr_binaryop(:-, x, y)
return reinterpret(T, z)
return z
end
function Base.checked_mul(x::FD{T,f}, y::FD{T,f}) where {T<:Integer,f}
powt = coefficient(FD{T, f})
quotient, remainder = fldmodinline(_widemul(x.i, y.i), powt)
v = _round_to_nearest(quotient, remainder, powt)
typemin(T) <= v <= typemax(T) || Base.Checked.throw_overflowerr_binaryop(:*, x, y)
return reinterpret(FD{T, f}, T(v))
z, b = Base.Checked.mul_with_overflow(x, y)
b && Base.Checked.throw_overflowerr_binaryop(:*, x, y)
return z
end
# Checked division functions
for divfn in [:div, :fld, :cld]
Expand Down
76 changes: 76 additions & 0 deletions test/FixedDecimal.jl
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,82 @@ end
@test FD{Int8,1}(2) / Int8(20) == FD{Int8,1}(0.1)
end

@testset "limits: with_overflow math" begin
# Easy to reason about cases of overflow:
@test Base.Checked.add_with_overflow(FD{Int8,2}(1), FD{Int8,2}(1)) == (FD{Int8,2}(-0.56), true)
@test Base.Checked.add_with_overflow(FD{Int8,2}(1), 1) == (FD{Int8,2}(-0.56), true)
@test Base.Checked.add_with_overflow(FD{Int8,2}(1), FD{Int8,2}(0.4)) == (FD{Int8,2}(-1.16), true)

@test Base.Checked.sub_with_overflow(FD{Int8,2}(1), FD{Int8,2}(-1)) == (FD{Int8,2}(-0.56), true)
@test Base.Checked.sub_with_overflow(1, FD{Int8,2}(-1)) == (FD{Int8,2}(-0.56), true)
@test Base.Checked.sub_with_overflow(FD{Int8,2}(-1), FD{Int8,2}(0.4)) == (FD{Int8,2}(1.16), true)

@test Base.Checked.mul_with_overflow(FD{Int8,2}(1.2), FD{Int8,2}(1.2)) == (FD{Int8,2}(-1.12), true)
@test Base.Checked.mul_with_overflow(FD{Int8,1}(12), 2) == (FD{Int8,1}(-1.6), true)
@test Base.Checked.mul_with_overflow(FD{Int8,0}(120), 2) == (FD{Int8,0}(-16), true)
@test Base.Checked.mul_with_overflow(120, FD{Int8,0}(2)) == (FD{Int8,0}(-16), true)

@testset "with_overflow math corner cases" begin
@testset for I in (Int128, UInt128, Int8, UInt8), f in (0,2)
T = FD{I, f}
issigned(I) = signed(I) === I

@test Base.Checked.add_with_overflow(typemax(T), eps(T)) == (typemax(T) + eps(T), true)
issigned(I) && @test Base.Checked.add_with_overflow(typemin(T), -eps(T)) == (typemin(T) + -eps(T), true)
@test Base.Checked.add_with_overflow(typemax(T), 1) == (typemax(T) + 1, true)
@test Base.Checked.add_with_overflow(1, typemax(T)) == (typemax(T) + 1, true)

@test Base.Checked.sub_with_overflow(typemin(T), eps(T)) == (typemin(T) - eps(T), true)
issigned(I) && @test Base.Checked.sub_with_overflow(typemax(T), -eps(T)) == (typemax(T) - -eps(T), true)
@test Base.Checked.sub_with_overflow(typemin(T), 1) == (typemin(T) - 1, true)
if issigned(I) && 2.0 <= typemax(T)
@test Base.Checked.sub_with_overflow(-2, typemax(T)) == (-2 -typemax(T), true)
end

@test Base.Checked.mul_with_overflow(typemax(T), typemax(T)) == (typemax(T) * typemax(T), true)
issigned(I) && @test Base.Checked.mul_with_overflow(typemin(T), typemax(T)) == (typemin(T) * typemax(T), true)
if 2.0 <= typemax(T)
@test Base.Checked.mul_with_overflow(typemax(T), 2) == (typemax(T) * 2, true)
@test Base.Checked.mul_with_overflow(2, typemax(T)) == (2 * typemax(T), true)
issigned(I) && @test Base.Checked.mul_with_overflow(typemin(T), 2) == (typemin(T) * 2, true)
issigned(I) && @test Base.Checked.mul_with_overflow(2, typemin(T)) == (2 * typemin(T), true)
end
end
end

@testset "with_overflow math promotions" begin
x = FD{Int8,1}(1)
y = FD{Int64,1}(2)
@testset for op in (
Base.Checked.add_with_overflow, Base.Checked.sub_with_overflow,
Base.Checked.mul_with_overflow,
)
@test op(x, y) === op(FD{Int64,1}(1), y)
@test op(y, x) === op(y, FD{Int64,1}(1))

@test op(x, 2) === op(x, FD{Int8,1}(2))
@test op(2, x) === op(FD{Int8,1}(2), x)
end
end

@testset "non-overflowing with_overflow math" begin
@test Base.Checked.add_with_overflow(1, FD{Int8,1}(1.1)) == (FD{Int8,1}(2.1), false)
@test Base.Checked.add_with_overflow(FD{Int8,1}(1.1), 1) == (FD{Int8,1}(2.1), false)
@test Base.Checked.add_with_overflow(FD{Int64,8}(30.123), FD{Int64,8}(30)) == (FD{Int64,8}(60.123), false)
@test Base.Checked.add_with_overflow(FD{Int64,8}(30.123), FD{Int64,5}(30)) == (FD{Int64,8}(60.123), false)

@test Base.Checked.sub_with_overflow(3, FD{Int16,2}(2.5)) == (FD{Int16,1}(0.5), false)
@test Base.Checked.sub_with_overflow(FD{Int16,2}(2.5), 3) == (FD{Int16,1}(-0.5), false)
@test Base.Checked.sub_with_overflow(FD{Int32,4}(10.11), FD{Int32,4}(2)) == (FD{Int32,4}(8.11), false)
@test Base.Checked.sub_with_overflow(FD{Int32,4}(10.11), FD{Int32,3}(2)) == (FD{Int32,4}(8.11), false)

@test Base.Checked.mul_with_overflow(4, FD{Int64,6}(2.22)) == (FD{Int64,6}(8.88), false)
@test Base.Checked.mul_with_overflow(FD{Int64,6}(2.22), 4) == (FD{Int64,6}(8.88), false)
@test Base.Checked.mul_with_overflow(FD{Int128,14}(10), FD{Int128,14}(20.1)) == (FD{Int128,14}(201), false)
@test Base.Checked.mul_with_overflow(FD{Int128,30}(10.11), FD{Int128,0}(1)) == (FD{Int128,30}(10.11), false)
end
end

@testset "limits: overflow checked math" begin
# Easy to reason about cases of overflow:
@test_throws OverflowError Base.checked_add(FD{Int8,2}(1), FD{Int8,2}(1))
Expand Down

0 comments on commit 6c6df97

Please sign in to comment.