diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index f1e3890..275c5ef 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -15,7 +15,7 @@ jobs: version: - '1.6' - '1' -# - 'nightly' + - 'nightly' os: - ubuntu-latest - macOS-latest diff --git a/Project.toml b/Project.toml index 7046d0b..9ef7f60 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "FixedPointDecimals" uuid = "fb4d412d-6eee-574d-9565-ede6634db7b0" authors = ["Fengyang Wang ", "Curtis Vogt "] -version = "0.4.4" +version = "0.5.0" [deps] Parsers = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" diff --git a/README.md b/README.md index 698f202..3ca5e2e 100644 --- a/README.md +++ b/README.md @@ -44,3 +44,64 @@ julia> 0.1 + 0.2 julia> FixedDecimal{Int,1}(0.1) + FixedDecimal{Int,1}(0.2) FixedDecimal{Int64,1}(0.3) ``` + +### Arithmetic details: Overflow and checked math + +_NOTE: This section applies to FixedPointDecimals v0.5+._ + +By default, all arithmetic operations on FixedDecimals, except division, **will silently overflow** following the standard behavior for bit integer types in Julia. For example: +```julia +julia> FixedDecimal{Int8,2}(1.0) + FixedDecimal{Int8,2}(1.0) +FixedDecimal{Int8,2}(-0.56) + +julia> -FixedDecimal{Int8,2}(-1.28) # negative typemin wraps to typemin again +FixedDecimal{Int8,2}(-1.28) + +julia> abs(FixedDecimal{Int8,2}(-1.28)) # negative typemin wraps to typemin again +FixedDecimal{Int8,2}(-1.28) +``` + +*Note that **division** on FixedDecimals will throw OverflowErrors on overflow, and will not wrap. This decision may be reevaluated in a future breaking version change release of FixedDecimals. Please keep this in mind.* + +In most applications dealing with `FixedDecimals`, you will likely want to use the **checked arithmetic** operations instead. These operations will _throw an OverflowError_ on overflow or underflow, rather than silently wrapping. For example: +```julia +julia> Base.checked_mul(FixedDecimal{Int8,2}(1.2), FixedDecimal{Int8,2}(1.2)) +ERROR: OverflowError: 1.20 * 1.20 overflowed for type FixedDecimal{Int8, 2} + +julia> Base.checked_add(FixedDecimal{Int8,2}(1.2), 1) +ERROR: OverflowError: 1.20 + 1.00 overflowed for type FixedDecimal{Int8, 2} + +julia> Base.checked_div(Int8(1), FixedDecimal{Int8,2}(0.5)) +ERROR: OverflowError: 1.00 ÷ 0.50 overflowed for type FixedDecimal{Int8, 2} +``` + +**Checked division:** Note that `checked_div` performs truncating, integer division. Julia Base does not provide a function to perform checked *decimal* division (`/`), so we provide one in this package, `FixedPointDecimals.checked_rdiv`. However, as noted above, the default division arithmetic operators will throw on overflow anyway. + +Here are all the checked arithmetic operations supported by `FixedDecimal`s: +- `Base.checked_add(x,y)` +- `Base.checked_sub(x,y)` +- `Base.checked_mul(x,y)` +- `Base.checked_div(x,y)` +- `FixedPointDecimals.checked_rdiv(x,y)` +- `Base.checked_cld(x,y)` +- `Base.checked_fld(x,y)` +- `Base.checked_rem(x,y)` +- `Base.checked_mod(x,y)` +- `Base.checked_neg(x)` +- `Base.checked_abs(x)` + +### Conversions, Promotions, and Inexact Errors. + +Note that arithmetic operations will _promote_ all arguments to the same FixedDecimal type +before performing the operation. If you are promoting a non-FixedDecimal _number_ to a FixedDecimal, there is always a chance that the Number will not fit in the FD type. In that case, the conversion will throw an exception. Here are some examples: +```julia +julia> FixedDecimal{Int8,2}(2) # 200 doesn't fit in Int8 +ERROR: InexactError: convert(FixedDecimal{Int8, 2}, 2) + +julia> FixedDecimal{Int8,2}(1) + 2 # Same here: 2 is promoted to FD{Int8,2}(2) +ERROR: InexactError: convert(FixedDecimal{Int8, 2}, 2) + +julia> FixedDecimal{Int8,2}(1) + FixedDecimal{Int8,1}(2) # Promote to the higher-precision type again throws. +ERROR: InexactError: convert(FixedDecimal{Int8, 2}, 2.0) +``` + diff --git a/src/FixedPointDecimals.jl b/src/FixedPointDecimals.jl index 31de943..89389c4 100644 --- a/src/FixedPointDecimals.jl +++ b/src/FixedPointDecimals.jl @@ -27,6 +27,13 @@ module FixedPointDecimals export FixedDecimal, RoundThrows +# (Re)export checked_* arithmetic functions +# - Defined in this package: +export checked_rdiv +# - Reexported from Base: +export checked_abs, checked_add, checked_cld, checked_div, checked_fld, + checked_mod, checked_mul, checked_neg, checked_rem, checked_sub + using Base: decompose, BitInteger import Parsers @@ -187,28 +194,12 @@ end # these functions are needed to avoid InexactError when converting from the # integer type -Base.:*(x::Integer, y::FD{T, f}) where {T, f} = reinterpret(FD{T, f}, T(x * y.i)) -Base.:*(x::FD{T, f}, y::Integer) where {T, f} = reinterpret(FD{T, f}, T(x.i * y)) +Base.:*(x::Integer, y::FD{T, f}) where {T, f} = reinterpret(FD{T, f}, x * y.i) +Base.:*(x::FD{T, f}, y::Integer) where {T, f} = reinterpret(FD{T, f}, x.i * y) -function Base.:/(x::FD{T, f}, y::FD{T, f}) where {T, f} - powt = coefficient(FD{T, f}) - quotient, remainder = fldmod(widemul(x.i, powt), y.i) - reinterpret(FD{T, f}, T(_round_to_nearest(quotient, remainder, y.i))) -end - -# These functions allow us to perform division with integers outside of the range of the -# FixedDecimal. -function Base.:/(x::Integer, y::FD{T, f}) where {T, f} - powt = coefficient(FD{T, f}) - powtsq = widemul(powt, powt) - quotient, remainder = fldmod(widemul(x, powtsq), y.i) - reinterpret(FD{T, f}, T(_round_to_nearest(quotient, remainder, y.i))) -end - -function Base.:/(x::FD{T, f}, y::Integer) where {T, f} - quotient, remainder = fldmod(x.i, y) - reinterpret(FD{T, f}, T(_round_to_nearest(quotient, remainder, y))) -end +Base.:/(x::FD, y::FD) = checked_rdiv(x, y) +Base.:/(x::Integer, y::FD) = checked_rdiv(x, y) +Base.:/(x::FD, y::Integer) = checked_rdiv(x, y) # integerification Base.trunc(x::FD{T, f}) where {T, f} = FD{T, f}(div(x.i, coefficient(FD{T, f}))) @@ -359,17 +350,154 @@ for remfn in [:rem, :mod, :mod1, :min, :max] end # TODO: When we upgrade to a min julia version >=1.4 (i.e Julia 2.0), this block can be # dropped in favor of three-argument `div`, below. -for divfn in [:div, :fld, :fld1, :cld] - # div(x.i, y.i) eliminates the scaling coefficient, so we call the FD constructor. - # We don't need any widening logic, since we won't be multiplying by the coefficient. - @eval Base.$divfn(x::T, y::T) where {T <: FD} = T($divfn(x.i, y.i)) +# The division functions all default to *throwing OverflowError* rather than +# wrapping on integer overflow. +# This decision may be changed in a future release of FixedPointDecimals. +Base.div(x::FD, y::FD) = Base.checked_div(x, y) +Base.fld(x::FD, y::FD) = Base.checked_fld(x, y) +Base.cld(x::FD, y::FD) = Base.checked_cld(x, y) +# There is no checked_fld1, so this is implemented here: +function Base.fld1(x::FD{T,f}, y::FD{T,f}) where {T, f} + C = coefficient(FD{T, f}) + # Note: fld1() will already throw for divide-by-zero and typemin(T) ÷ -1. + v, b = Base.Checked.mul_with_overflow(C, fld1(x.i, y.i)) + b && _throw_overflowerr_op(:fld1, x, y) + return reinterpret(FD{T, f}, v) end if VERSION >= v"1.4.0-" # div(x.i, y.i) eliminates the scaling coefficient, so we call the FD constructor. # We don't need any widening logic, since we won't be multiplying by the coefficient. - Base.div(x::T, y::T, r::RoundingMode) where {T <: FD} = T(div(x.i, y.i, r)) + @eval function Base.div(x::FD{T, f}, y::FD{T, f}, r::RoundingMode) where {T<:Integer, f} + C = coefficient(FD{T, f}) + # Note: The div() will already throw for divide-by-zero and typemin(T) ÷ -1. + v, b = Base.Checked.mul_with_overflow(C, div(x.i, y.i, r)) + b && _throw_overflowerr_op(:div, x, y) + return reinterpret(FD{T, f}, v) + end +end + +# --- Checked arithmetic --- + +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)...) +Base.checked_div(x::FD, y::FD) = Base.checked_div(promote(x, y)...) +Base.checked_cld(x::FD, y::FD) = Base.checked_cld(promote(x, y)...) +Base.checked_fld(x::FD, y::FD) = Base.checked_fld(promote(x, y)...) +Base.checked_rem(x::FD, y::FD) = Base.checked_rem(promote(x, y)...) +Base.checked_mod(x::FD, y::FD) = Base.checked_mod(promote(x, y)...) + +Base.checked_add(x::FD, y) = Base.checked_add(promote(x, y)...) +Base.checked_add(x, y::FD) = Base.checked_add(promote(x, y)...) +Base.checked_sub(x::FD, y) = Base.checked_sub(promote(x, y)...) +Base.checked_sub(x, y::FD) = Base.checked_sub(promote(x, y)...) +Base.checked_mul(x::FD, y) = Base.checked_mul(promote(x, y)...) +Base.checked_mul(x, y::FD) = Base.checked_mul(promote(x, y)...) +Base.checked_div(x::FD, y) = Base.checked_div(promote(x, y)...) +Base.checked_div(x, y::FD) = Base.checked_div(promote(x, y)...) +Base.checked_cld(x::FD, y) = Base.checked_cld(promote(x, y)...) +Base.checked_cld(x, y::FD) = Base.checked_cld(promote(x, y)...) +Base.checked_fld(x::FD, y) = Base.checked_fld(promote(x, y)...) +Base.checked_fld(x, y::FD) = Base.checked_fld(promote(x, y)...) +Base.checked_rem(x::FD, y) = Base.checked_rem(promote(x, y)...) +Base.checked_rem(x, y::FD) = Base.checked_rem(promote(x, y)...) +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) + b && Base.Checked.throw_overflowerr_binaryop(:+, x, y) + return reinterpret(T, z) +end +function Base.checked_sub(x::T, y::T) where {T<:FD} + z, b = Base.sub_with_overflow(x.i, y.i) + b && Base.Checked.throw_overflowerr_binaryop(:-, x, y) + return reinterpret(T, 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)) +end +# Checked division functions +for divfn in [:div, :fld, :cld] + @eval function Base.$(Symbol("checked_$divfn"))(x::FD{T,f}, y::FD{T,f}) where {T<:Integer,f} + C = coefficient(FD{T, f}) + # Note: The div() will already throw for divide-by-zero and typemin(T) ÷ -1. + v, b = Base.Checked.mul_with_overflow(C, $divfn(x.i, y.i)) + b && _throw_overflowerr_op($(QuoteNode(divfn)), x, y) + return reinterpret(FD{T, f}, v) + end +end +for remfn in [:rem, :mod] + # rem and mod already check for divide-by-zero and typemin(T) ÷ -1, so nothing to do. + @eval Base.$(Symbol("checked_$remfn"))(x::T, y::T) where {T <: FD} = $remfn(x, y) +end + +@noinline _throw_overflowerr_op(op, x::T, y::T) where T = throw(OverflowError("$op($x, $y) overflowed for type $T")) + +function Base.checked_neg(x::T) where {T<:FD} + r = -x + (x<0) & (r<0) && Base.Checked.throw_overflowerr_negation(x) + return r +end +function Base.checked_abs(x::FD) + r = ifelse(x<0, -x, x) + r<0 || return r + _throw_overflow_abs(x) +end +if VERSION >= v"1.8.0-" + @noinline _throw_overflow_abs(x) = + throw(OverflowError(LazyString("checked arithmetic: cannot compute |x| for x = ", x, "::", typeof(x)))) +else + @noinline _throw_overflow_abs(x) = + throw(OverflowError("checked arithmetic: cannot compute |x| for x = $x")) +end + +# We introduce a new function for this since Base.Checked only supports integers, and ints +# don't have a decimal division operation. +""" + FixedPointDecimals.checked_rdiv(x::FD, y::FD) -> FD + +Calculates `x / y`, checking for overflow errors where applicable. + +The overflow protection may impose a perceptible performance penalty. + +See also: +- `Base.checked_div` for truncating division. +""" +checked_rdiv(x::FD, y::FD) = checked_rdiv(promote(x, y)...) + +function checked_rdiv(x::FD{T,f}, y::FD{T,f}) where {T<:Integer,f} + powt = coefficient(FD{T, f}) + quotient, remainder = fldmod(widemul(x.i, powt), y.i) + v = _round_to_nearest(quotient, remainder, y.i) + typemin(T) <= v <= typemax(T) || Base.Checked.throw_overflowerr_binaryop(:/, x, y) + return reinterpret(FD{T, f}, v) end +# These functions allow us to perform division with integers outside of the range of the +# FixedDecimal. +function checked_rdiv(x::Integer, y::FD{T, f}) where {T<:Integer, f} + powt = coefficient(FD{T, f}) + powtsq = widemul(powt, powt) + quotient, remainder = fldmod(widemul(x, powtsq), y.i) + v = _round_to_nearest(quotient, remainder, y.i) + typemin(T) <= v <= typemax(T) || Base.Checked.throw_overflowerr_binaryop(:/, x, y) + reinterpret(FD{T, f}, v) +end +function checked_rdiv(x::FD{T, f}, y::Integer) where {T<:Integer, f} + quotient, remainder = fldmod(x.i, y) + v = _round_to_nearest(quotient, remainder, y) + typemin(T) <= v <= typemax(T) || Base.Checked.throw_overflowerr_binaryop(:/, x, y) + reinterpret(FD{T, f}, v) +end + + +# -------------------------- + Base.convert(::Type{AbstractFloat}, x::FD) = convert(floattype(typeof(x)), x) function Base.convert(::Type{TF}, x::FD{T, f}) where {TF <: AbstractFloat, T, f} convert(TF, x.i / coefficient(FD{T, f}))::TF diff --git a/test/FixedDecimal.jl b/test/FixedDecimal.jl index 723f6ba..0b42cfa 100644 --- a/test/FixedDecimal.jl +++ b/test/FixedDecimal.jl @@ -539,7 +539,7 @@ end # signed integers using two's complement have one additional negative value if x < 0 && x == typemin(x) - @test_throws InexactError x / -one(x) + @test_throws OverflowError x / -one(x) else @test x / -one(x) == -x end @@ -624,9 +624,113 @@ end @test FD{Int8,1}(2) / Int8(20) == FD{Int8,1}(0.1) end - @testset "limits" begin - @test_throws InexactError Int8(1) / FD{Int8,2}(0.4) - @test_throws InexactError FD{Int8,2}(1) / FD{Int8,2}(0.4) + @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)) + @test_throws OverflowError Base.checked_add(FD{Int8,2}(1), 1) + @test_throws OverflowError Base.checked_add(FD{Int8,2}(1), FD{Int8,2}(0.4)) + + @test_throws OverflowError Base.checked_sub(FD{Int8,2}(1), FD{Int8,2}(-1)) + @test_throws OverflowError Base.checked_sub(1, FD{Int8,2}(-1)) + @test_throws OverflowError Base.checked_sub(FD{Int8,2}(-1), FD{Int8,2}(0.4)) + + @test_throws OverflowError Base.checked_mul(FD{Int8,2}(1.2), FD{Int8,2}(1.2)) + @test_throws OverflowError Base.checked_mul(FD{Int8,1}(12), 2) + @test_throws OverflowError Base.checked_mul(FD{Int8,0}(120), 2) + @test_throws OverflowError Base.checked_mul(120, FD{Int8,0}(2)) + + @test_throws OverflowError Base.checked_div(FD{Int8,2}(1), FD{Int8,2}(0.5)) + @test_throws OverflowError Base.checked_div(1, FD{Int8,2}(0.5)) + @test_throws OverflowError Base.checked_div(FD{Int8,2}(1), FD{Int8,2}(0.4)) + + @testset "checked_rdiv" begin + using FixedPointDecimals: checked_rdiv + + @test checked_rdiv(Int8(1), FD{Int8,2}(0.8)) == FD{Int8,2}(1.25) + @test_throws OverflowError checked_rdiv(Int8(1), FD{Int8,2}(0.7)) + end + + # Rounds down to -2 + @test_throws OverflowError Base.checked_fld(FD{Int8,2}(-1), FD{Int8,2}(0.9)) + # Rounds up to 2 + @test_throws OverflowError Base.checked_cld(FD{Int8,2}(1), FD{Int8,2}(0.9)) + + # Rem and Mod only throw DivideError and nothing more. They can't overflow, since + # they can only return smaller values than the arguments. + @test_throws DivideError Base.checked_rem(FD{Int8,2}(-1), FD{Int8,2}(0)) + @test_throws DivideError Base.checked_mod(FD{Int8,2}(-1), FD{Int8,2}(0)) + + @test_throws OverflowError Base.checked_abs(typemin(FD{Int8,2})) + @test_throws OverflowError Base.checked_neg(typemin(FD{Int8,2})) + @test Base.checked_abs(typemax(FD{Int8,2})) == FD{Int8,2}(1.27) + @test Base.checked_neg(typemax(FD{Int8,2})) == FD{Int8,2}(-1.27) + + @testset "checked 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_throws OverflowError Base.checked_add(typemax(T), eps(T)) + issigned(I) && @test_throws OverflowError Base.checked_add(typemin(T), -eps(T)) + @test_throws OverflowError Base.checked_add(typemax(T), 1) + @test_throws OverflowError Base.checked_add(1, typemax(T)) + + @test_throws OverflowError Base.checked_sub(typemin(T), eps(T)) + issigned(I) && @test_throws OverflowError Base.checked_sub(typemax(T), -eps(T)) + @test_throws OverflowError Base.checked_sub(typemin(T), 1) + if issigned(I) && 2.0 <= typemax(T) + @test_throws OverflowError Base.checked_sub(-2, typemax(T)) + end + + @test_throws OverflowError Base.checked_mul(typemax(T), typemax(T)) + issigned(I) && @test_throws OverflowError Base.checked_mul(typemin(T), typemax(T)) + if 2.0 <= typemax(T) + @test_throws OverflowError Base.checked_mul(typemax(T), 2) + @test_throws OverflowError Base.checked_mul(2, typemax(T)) + issigned(I) && @test_throws OverflowError Base.checked_mul(typemin(T), 2) + issigned(I) && @test_throws OverflowError Base.checked_mul(2, typemin(T)) + end + + if f > 0 + @test_throws OverflowError Base.checked_div(typemax(T), eps(T)) + issigned(I) && @test_throws OverflowError Base.checked_div(typemin(T), eps(T)) + issigned(I) && @test_throws OverflowError Base.checked_div(typemax(T), -eps(T)) + + issigned(I) && @test_throws DivideError Base.checked_div(typemax(T), T(0)) + issigned(I) && @test_throws DivideError Base.checked_div(typemin(T), T(0)) + issigned(I) && @test_throws DivideError Base.checked_div(typemin(T), -eps(T)) + end + + if f > 0 + @test_throws OverflowError Base.checked_fld(typemax(T), eps(T)) + issigned(I) && @test_throws OverflowError Base.checked_fld(typemin(T), eps(T)) + issigned(I) && @test_throws OverflowError Base.checked_fld(typemax(T), -eps(T)) + + @test_throws OverflowError Base.checked_cld(typemax(T), eps(T)) + issigned(I) && @test_throws OverflowError Base.checked_cld(typemin(T), eps(T)) + issigned(I) && @test_throws OverflowError Base.checked_cld(typemax(T), -eps(T)) + end + + issigned(I) && @test_throws OverflowError Base.checked_abs(typemin(T)) + issigned(I) && @test_throws OverflowError Base.checked_neg(typemin(T)) + end + end + + @testset "checked math promotions" begin + x = FD{Int8,1}(1) + y = FD{Int64,1}(2) + @testset for op in ( + Base.checked_add, Base.checked_sub, Base.checked_mul, Base.checked_div, + Base.checked_cld, Base.checked_fld, Base.checked_rem, Base.checked_mod, + FixedPointDecimals.checked_rdiv, + ) + @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 end @testset "limits of $T" for T in CONTAINER_TYPES @@ -712,6 +816,63 @@ end end end +@testset "overflow" begin + T = FD{Int8, 1} + @testset "addition" begin + @test typemax(T) + eps(T) == typemin(T) + @test typemin(T) + (-eps(T)) == typemax(T) + end + + @testset "subtraction" begin + @test typemin(T) - eps(T) == typemax(T) + @test typemax(T) - (-eps(T)) == typemin(T) + end + + @testset "multiplication" begin + @test typemax(T) * 2 == T(-0.2) + @test typemin(T) * 2 == T(0) + end + + @testset "division" begin + @test_throws OverflowError typemax(T) / T(0.5) + @test_throws OverflowError typemin(T) / T(0.5) + end + + @testset "truncating division" begin + @test_throws OverflowError typemax(T) ÷ T(0.5) + @test_throws OverflowError typemin(T) ÷ T(0.5) + @test_throws OverflowError typemax(T) ÷ eps(T) + @test_throws OverflowError typemin(T) ÷ eps(T) + + @test_throws OverflowError div(typemax(T), T(0.5), RoundUp) + @test_throws OverflowError div(typemin(T), T(0.5), RoundUp) + @test_throws OverflowError div(typemax(T), eps(T), RoundUp) + @test_throws OverflowError div(typemin(T), eps(T), RoundUp) + end + + @testset "fld / cld" begin + @test_throws OverflowError fld(typemax(T), T(0.5)) + @test_throws OverflowError fld(typemin(T), T(0.5)) + @test_throws OverflowError fld(typemax(T), eps(T)) + @test_throws OverflowError fld(typemin(T), eps(T)) + + @test_throws OverflowError fld1(typemax(T), T(0.5)) + @test_throws OverflowError fld1(typemin(T), T(0.5)) + @test_throws OverflowError fld1(typemax(T), eps(T)) + @test_throws OverflowError fld1(typemin(T), eps(T)) + + @test_throws OverflowError cld(typemax(T), T(0.5)) + @test_throws OverflowError cld(typemin(T), T(0.5)) + @test_throws OverflowError cld(typemax(T), eps(T)) + @test_throws OverflowError cld(typemin(T), eps(T)) + end + + @testset "abs / neg" begin + @test abs(typemin(T)) == typemin(T) + @test -(typemin(T)) == typemin(T) + end +end + @testset "isinteger" begin # Note: Test cannot be used unless we can construct `FD{Int8,6}` # @testset "overflow" begin