Skip to content

Commit 28e72f8

Browse files
authored
Add checked, wrapping and saturating arithmetic for rem/mod (#230)
The default arithmetic for `rem` is still checked arithmetic. This also adds the support for 3-arg `rem`. In `RoundUp` mode, `rem` causes `OverflowError` with unsigned (i.e. `Normed`) values. Otherwise, `rem`/`mod` does not cause an overflow. This also adds dummy checked, wrapping and saturating rem for type modulus to prevent `MethodError` due to lexical replacement of `x % X`.
1 parent c6b8a9c commit 28e72f8

File tree

7 files changed

+157
-19
lines changed

7 files changed

+157
-19
lines changed

src/FixedPointNumbers.jl

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import Statistics # for _mean_promote
1313
import Random: Random, AbstractRNG, SamplerType, rand!
1414

1515
import Base.Checked: checked_neg, checked_abs, checked_add, checked_sub, checked_mul,
16-
checked_div, checked_fld, checked_cld
16+
checked_div, checked_fld, checked_cld, checked_rem, checked_mod
1717

1818
using Base: @pure
1919

@@ -38,10 +38,10 @@ export
3838
# Functions
3939
scaledual,
4040
wrapping_neg, wrapping_abs, wrapping_add, wrapping_sub, wrapping_mul,
41-
wrapping_fdiv, wrapping_div, wrapping_fld, wrapping_cld,
41+
wrapping_div, wrapping_fld, wrapping_cld, wrapping_rem, wrapping_mod,
4242
saturating_neg, saturating_abs, saturating_add, saturating_sub, saturating_mul,
43-
saturating_fdiv, saturating_div, saturating_fld, saturating_cld,
44-
checked_fdiv
43+
saturating_div, saturating_fld, saturating_cld, saturating_rem, saturating_mod,
44+
wrapping_fdiv, saturating_fdiv, checked_fdiv
4545

4646
include("utilities.jl")
4747

@@ -64,6 +64,12 @@ wrapping_mul(x::Real, ::Type{X}) where {X <: FixedPoint} = x % X
6464
saturating_mul(x::Real, ::Type{X}) where {X <: FixedPoint} = clamp(x, X)
6565
checked_mul(x::Real, ::Type{X}) where {X <: FixedPoint} = _convert(X, x)
6666

67+
# type modulus
68+
rem(x::Real, ::Type{X}) where {X <: FixedPoint} = _rem(x, X)
69+
wrapping_rem(x::Real, ::Type{X}) where {X <: FixedPoint} = _rem(x, X)
70+
saturating_rem(x::Real, ::Type{X}) where {X <: FixedPoint} = _rem(x, X)
71+
checked_rem(x::Real, ::Type{X}) where {X <: FixedPoint} = _rem(x, X)
72+
6773
# constructor-style conversions
6874
(::Type{X})(x::X) where {X <: FixedPoint} = x
6975
(::Type{X})(x::Number) where {X <: FixedPoint} = _convert(X, x)
@@ -225,6 +231,9 @@ function wrapping_div(x::X, y::X, r::RoundingMode = RoundToZero) where {T, X <:
225231
end
226232
wrapping_fld(x::X, y::X) where {X <: FixedPoint} = wrapping_div(x, y, RoundDown)
227233
wrapping_cld(x::X, y::X) where {X <: FixedPoint} = wrapping_div(x, y, RoundUp)
234+
wrapping_rem(x::X, y::X, r::RoundingMode = RoundToZero) where {T, X <: FixedPoint{T}} =
235+
X(x.i - wrapping_div(x, y, r) * y.i, 0)
236+
wrapping_mod(x::X, y::X) where {X <: FixedPoint} = wrapping_rem(x, y, RoundDown)
228237

229238
# saturating arithmetic
230239
saturating_neg(x::X) where {X <: FixedPoint} = X(~min(x.i - true, x.i), 0)
@@ -257,6 +266,11 @@ function saturating_div(x::X, y::X, r::RoundingMode = RoundToZero) where {T, X <
257266
end
258267
saturating_fld(x::X, y::X) where {X <: FixedPoint} = saturating_div(x, y, RoundDown)
259268
saturating_cld(x::X, y::X) where {X <: FixedPoint} = saturating_div(x, y, RoundUp)
269+
function saturating_rem(x::X, y::X, r::RoundingMode = RoundToZero) where {T, X <: FixedPoint{T}}
270+
T <: Unsigned && r isa RoundingMode{:Up} && return zero(X)
271+
X(x.i - saturating_div(x, y, r) * y.i, 0)
272+
end
273+
saturating_mod(x::X, y::X) where {X <: FixedPoint} = saturating_rem(x, y, RoundDown)
260274

261275
# checked arithmetic
262276
checked_neg(x::X) where {X <: FixedPoint} = checked_sub(zero(X), x)
@@ -301,6 +315,16 @@ function checked_div(x::X, y::X, r::RoundingMode = RoundToZero) where {T, X <: F
301315
end
302316
checked_fld(x::X, y::X) where {X <: FixedPoint} = checked_div(x, y, RoundDown)
303317
checked_cld(x::X, y::X) where {X <: FixedPoint} = checked_div(x, y, RoundUp)
318+
function checked_rem(x::X, y::X, r::RoundingMode = RoundToZero) where {T, X <: FixedPoint{T}}
319+
y === zero(X) && throw(DivideError())
320+
fx, fy = floattype(X)(x.i), floattype(X)(y.i)
321+
z = fx - round(fx / fy, r) * fy
322+
if T <: Unsigned && r isa RoundingMode{:Up}
323+
z >= zero(z) || throw_overflowerror_rem(r, x, y)
324+
end
325+
X(_unsafe_trunc(T, z), 0)
326+
end
327+
checked_mod(x::X, y::X) where {X <: FixedPoint} = checked_rem(x, y, RoundDown)
304328

305329
# default arithmetic
306330
const DEFAULT_ARITHMETIC = :wrapping
@@ -322,6 +346,10 @@ end
322346
div(x::X, y::X, r::RoundingMode = RoundToZero) where {X <: FixedPoint} = checked_div(x, y, r)
323347
fld(x::X, y::X) where {X <: FixedPoint} = checked_div(x, y, RoundDown)
324348
cld(x::X, y::X) where {X <: FixedPoint} = checked_div(x, y, RoundUp)
349+
rem(x::X, y::X) where {X <: FixedPoint} = checked_rem(x, y, RoundToZero)
350+
rem(x::X, y::X, ::RoundingMode{:Down}) where {X <: FixedPoint} = checked_rem(x, y, RoundDown)
351+
rem(x::X, y::X, ::RoundingMode{:Up}) where {X <: FixedPoint} = checked_rem(x, y, RoundUp)
352+
mod(x::X, y::X) where {X <: FixedPoint} = checked_rem(x, y, RoundDown)
325353

326354
function minmax(x::X, y::X) where {X <: FixedPoint}
327355
a, b = minmax(reinterpret(x), reinterpret(y))
@@ -377,7 +405,7 @@ for f in (:~, )
377405
$f(x::X) where {X <: FixedPoint} = X($f(x.i), 0)
378406
end
379407
end
380-
for f in (:rem, :mod, :mod1, :min, :max)
408+
for f in (:mod1, :min, :max)
381409
@eval begin
382410
$f(x::X, y::X) where {X <: FixedPoint} = X($f(x.i, y.i), 0)
383411
end
@@ -544,6 +572,11 @@ end
544572
print(io, op, x, ", ", y, ") overflowed for type ", rawtype(x))
545573
throw(OverflowError(String(take!(io))))
546574
end
575+
@noinline function throw_overflowerror_rem(r::RoundingMode, @nospecialize(x), @nospecialize(y))
576+
io = IOBuffer()
577+
print(io, "rem(", x, ", ", y, ", ", r, ") overflowed for type ", typeof(x))
578+
throw(OverflowError(String(take!(io))))
579+
end
547580

548581
function Random.rand(r::AbstractRNG, ::SamplerType{X}) where X <: FixedPoint
549582
X(rand(r, rawtype(X)), 0)

src/fixed.jl

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,14 +95,14 @@ function _convert(::Type{F}, x::Rational) where {T, f, F <: Fixed{T,f}}
9595
end
9696
end
9797

98-
rem(x::F, ::Type{F}) where {F <: Fixed} = x
99-
function rem(x::Fixed, ::Type{F}) where {T, f, F <: Fixed{T,f}}
98+
_rem(x::F, ::Type{F}) where {F <: Fixed} = x
99+
function _rem(x::Fixed, ::Type{F}) where {T, f, F <: Fixed{T,f}}
100100
f2 = nbitsfrac(typeof(x))
101101
y = round(@exp2(f - f2) * reinterpret(x))
102102
reinterpret(F, _unsafe_trunc(T, y))
103103
end
104-
rem(x::Integer, ::Type{F}) where {T, f, F <: Fixed{T,f}} = F(_unsafe_trunc(T, x) << f, 0)
105-
function rem(x::Real, ::Type{F}) where {T, f, F <: Fixed{T,f}}
104+
_rem(x::Integer, ::Type{F}) where {T, f, F <: Fixed{T,f}} = F(_unsafe_trunc(T, x) << f, 0)
105+
function _rem(x::Real, ::Type{F}) where {T, f, F <: Fixed{T,f}}
106106
if bitwidth(T) < 32
107107
Ti = T
108108
else
@@ -113,7 +113,7 @@ function rem(x::Real, ::Type{F}) where {T, f, F <: Fixed{T,f}}
113113
y = _unsafe_trunc(Ti, round(x * Tf(@exp2(f))))
114114
reinterpret(F, _unsafe_trunc(T, y))
115115
end
116-
function rem(x::BigFloat, ::Type{F}) where {T, f, F <: Fixed{T,f}}
116+
function _rem(x::BigFloat, ::Type{F}) where {T, f, F <: Fixed{T,f}}
117117
isfinite(x) || return zero(F)
118118
reinterpret(F, _unsafe_trunc(T, round(x * @exp2(f))))
119119
end

src/normed.jl

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -108,24 +108,25 @@ function _convert(::Type{N}, x::Rational) where {T, f, N <: Normed{T,f}}
108108
end
109109
end
110110

111-
rem(x::N, ::Type{N}) where {N <: Normed} = x
112-
rem(x::Normed, ::Type{N}) where {T, N <: Normed{T}} = reinterpret(N, _unsafe_trunc(T, round((rawone(N)/rawone(x))*reinterpret(x))))
113-
function rem(x::Real, ::Type{N}) where {T, N <: Normed{T}}
111+
_rem(x::N, ::Type{N}) where {N <: Normed} = x
112+
_rem(x::Normed, ::Type{N}) where {T, N <: Normed{T}} =
113+
reinterpret(N, _unsafe_trunc(T, round((rawone(N)/rawone(x))*reinterpret(x))))
114+
function _rem(x::Real, ::Type{N}) where {T, N <: Normed{T}}
114115
bitwidth(T) < 32 || isfinite(x) || return zero(N)
115116
reinterpret(N, _unsafe_trunc(T, round(rawone(N) * x)))
116117
end
117-
rem(x::Float16, ::Type{N}) where {N <: Normed} = rem(Float32(x), N) # avoid overflow
118+
_rem(x::Float16, ::Type{X}) where {X <: Normed} = _rem(Float32(x), X) # avoid overflow
118119
# Float32 and Float64 cannot exactly represent `rawone(N)` with `f` greater than
119120
# the number of their significand bits, resulting in rounding errors (issue #150).
120121
# So, we use another strategy for the large `f`s explained in:
121122
# https://github.com/JuliaMath/FixedPointNumbers.jl/pull/166#issuecomment-574135643
122-
function rem(x::Float32, ::Type{N}) where {f, N <: Normed{UInt32,f}}
123+
function _rem(x::Float32, ::Type{N}) where {f, N <: Normed{UInt32,f}}
123124
isfinite(x) || return zero(N)
124125
f <= 24 && return reinterpret(N, _unsafe_trunc(UInt32, round(rawone(N) * x)))
125126
r = _unsafe_trunc(UInt32, round(x * @f32(0x1p24)))
126127
reinterpret(N, r << UInt8(f - 24) - unsigned(signed(r) >> 0x18))
127128
end
128-
function rem(x::Float64, ::Type{N}) where {f, N <: Normed{UInt64,f}}
129+
function _rem(x::Float64, ::Type{N}) where {f, N <: Normed{UInt64,f}}
129130
isfinite(x) || return zero(N)
130131
f <= 53 && return reinterpret(N, _unsafe_trunc(UInt64, round(rawone(N) * x)))
131132
r = _unsafe_trunc(UInt64, round(x * 0x1p53))

test/common.jl

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ function test_rem_type(TX::Type)
157157
@testset "% $X" for X in target(TX, :i8, :i16; ex = :thin)
158158
xs = typemin(X):0.1:typemax(X)
159159
@test all(x -> x % X === X(x), xs)
160+
@test wrapping_rem(2, X) === saturating_rem(2, X) === checked_rem(2, X) === 2 % X
160161
end
161162
end
162163

@@ -271,6 +272,33 @@ function test_div_3arg(TX::Type)
271272
end
272273
end
273274

275+
function test_rem(TX::Type)
276+
for X in target(TX, :i8; ex = :thin)
277+
T = rawtype(X)
278+
xys = xypairs(X)
279+
frem(x, y) = y === zero(y) ? float(x) : x - float(wrapping_div(x, y)) * y
280+
fmod(x, y) = y === zero(y) ? float(x) : x - float(wrapping_fld(x, y)) * y
281+
frems(x, y) = y === zero(y) ? float(x) : x - float(saturating_div(x, y)) * y
282+
fmods(x, y) = y === zero(y) ? float(x) : x - float(saturating_fld(x, y)) * y
283+
@test all(((x, y),) -> wrapping_rem(x, y) === frem(x, y) % X, xys)
284+
@test all(((x, y),) -> wrapping_mod(x, y) === fmod(x, y) % X, xys)
285+
@test all(((x, y),) -> saturating_rem(x, y) === frems(x, y) % X, xys)
286+
@test all(((x, y),) -> saturating_mod(x, y) === fmods(x, y) % X, xys)
287+
@test all(((x, y),) -> y === zero(y) ||
288+
wrapping_rem(x, y) === checked_rem(x, y), xys)
289+
@test all(((x, y),) -> y === zero(y) ||
290+
wrapping_mod(x, y) === checked_mod(x, y), xys)
291+
end
292+
end
293+
294+
function test_rem_3arg(TX::Type)
295+
for X in target(TX; ex = :thin)
296+
@test rem(eps(X), typemax(X), RoundToZero) === rem(eps(X), typemax(X))
297+
@test rem(eps(X), typemax(X), RoundDown) === mod(eps(X), typemax(X))
298+
@test rem(eps(X), eps(X), RoundUp) === zero(X)
299+
end
300+
end
301+
274302
function test_fld1_mod1(TX::Type)
275303
for X in target(TX, :i8, :i16; ex = :thin)
276304
T = rawtype(X)

test/fixed.jl

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,51 @@ end
448448
test_div_3arg(Fixed)
449449
end
450450

451+
@testset "rem/mod" begin
452+
for F in target(Fixed; ex = :thin)
453+
fm, fn, fz, fe = typemax(F), typemin(F), zero(F), eps(F)
454+
T = rawtype(F)
455+
@test wrapping_rem(fm, fm) === wrapping_mod(fm, fm) === fz
456+
@test saturating_rem(fm, fm) === saturating_mod(fm, fm) === fz
457+
@test checked_rem(fm, fm) === checked_mod(fm, fm) === fz
458+
459+
@test wrapping_rem(fz, fe) === wrapping_mod(fz, fe) === fz
460+
@test saturating_rem(fz, fe) === saturating_mod(fz, fe) === fz
461+
@test checked_rem(fz, fe) === checked_mod(fz, fe) === fz
462+
463+
@test wrapping_rem(fm, fe) === wrapping_mod(fm, fe) === fz
464+
@test saturating_rem(fm, fe) === saturating_mod(fm, fe) === fz
465+
@test checked_rem(fm, fe) === checked_mod(fm, fe) === fz
466+
467+
@test wrapping_rem(fz, fz) === wrapping_mod(fz, fz) === fz
468+
@test saturating_rem(fz, fz) === saturating_mod(fz, fz) === fz
469+
@test_throws DivideError checked_rem(fz, fz)
470+
@test_throws DivideError checked_mod(fz, fz)
471+
472+
@test wrapping_rem(fe, fz) === wrapping_mod(fe, fz) === fe
473+
@test saturating_rem(fe, fz) === saturating_mod(fe, fz) === fe
474+
@test_throws DivideError checked_rem(fe, fz)
475+
@test_throws DivideError checked_mod(fe, fz)
476+
477+
@test wrapping_rem(fn, -fe) === wrapping_mod(fn, -fe) === fz
478+
@test saturating_rem(fn, -fe) === saturating_mod(fn, -fe) === -fe
479+
@test checked_rem(fn, -fe) === checked_mod(fn, -fe) === fz
480+
481+
@test wrapping_rem(fe, fm) === saturating_rem(fe, fm) === checked_rem(fe, fm) === fe
482+
@test wrapping_mod(fe, fm) === saturating_mod(fe, fm) === checked_mod(fe, fm) === fe
483+
484+
@test wrapping_rem(fe, fn) === saturating_rem(fe, fn) === checked_rem(fe, fn) === fe
485+
@test wrapping_mod(fe, fn) === saturating_mod(fe, fn) === checked_mod(fe, fn) === fn + fe
486+
487+
@test wrapping_rem(fn, fm) === saturating_rem(fn, fm) === checked_rem(fn, fm) === -fe
488+
@test wrapping_mod(fn, fm) === saturating_mod(fn, fm) === checked_mod(fn, fm) === fm - fe
489+
end
490+
test_rem(Fixed)
491+
test_rem_3arg(Fixed)
492+
493+
@test rem(0.5Q0f7, 0.75Q0f7, RoundUp) === -0.25Q0f7
494+
end
495+
451496
@testset "fld1/mod1" begin
452497
test_fld1_mod1(Fixed)
453498
end

test/normed.jl

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -442,8 +442,39 @@ end
442442
end
443443

444444
@testset "rem/mod" begin
445-
@test mod(reinterpret(N0f8, 0x10), reinterpret(N0f8, 0x02)) == rem(reinterpret(N0f8, 0x10), reinterpret(N0f8, 0x02)) == 0
446-
@test mod(reinterpret(N0f8, 0x0f), reinterpret(N0f8, 0x02)) == rem(reinterpret(N0f8, 0x0f), reinterpret(N0f8, 0x02)) == reinterpret(N0f8, 0x01)
445+
for N in target(Normed; ex = :thin)
446+
nm, nz, ne = typemax(N), zero(N), eps(N)
447+
T = rawtype(N)
448+
@test wrapping_rem(nm, nm) === wrapping_mod(nm, nm) === nz
449+
@test saturating_rem(nm, nm) === saturating_mod(nm, nm) === nz
450+
@test checked_rem(nm, nm) === checked_mod(nm, nm) === nz
451+
452+
@test wrapping_rem(nz, ne) === wrapping_mod(nz, ne) === nz
453+
@test saturating_rem(nz, ne) === saturating_mod(nz, ne) === nz
454+
@test checked_rem(nz, ne) === checked_mod(nz, ne) === nz
455+
456+
@test wrapping_rem(nm, ne) === wrapping_mod(nm, ne) === nz
457+
@test saturating_rem(nm, ne) === saturating_mod(nm, ne) === nz
458+
@test checked_rem(nm, ne) === checked_mod(nm, ne) === nz
459+
460+
@test wrapping_rem(nz, nz) === wrapping_mod(nz, nz) === nz
461+
@test saturating_rem(nz, nz) === saturating_mod(nz, nz) === nz
462+
@test_throws DivideError checked_rem(nz, nz)
463+
@test_throws DivideError checked_mod(nz, nz)
464+
465+
@test wrapping_rem(ne, nz) === wrapping_mod(ne, nz) === ne
466+
@test saturating_rem(ne, nz) === saturating_mod(ne, nz) === ne
467+
@test_throws DivideError checked_rem(ne, nz)
468+
@test_throws DivideError checked_mod(ne, nz)
469+
470+
@test wrapping_rem(ne, nm) === saturating_rem(ne, nm) === checked_rem(ne, nm) === ne
471+
@test wrapping_mod(ne, nm) === saturating_mod(ne, nm) === checked_mod(ne, nm) === ne
472+
end
473+
test_rem(Normed)
474+
test_rem_3arg(Normed)
475+
476+
@test_throws OverflowError rem(0.5N0f8, 1N0f8, RoundUp)
477+
@test saturating_rem(0.5N0f8, 1N0f8, RoundUp) === zero(N0f8)
447478
end
448479

449480
@testset "fld1/mod1" begin

test/runtests.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
using FixedPointNumbers, Test
22

3-
if VERSION >= v"1.6.0-DEV.816" # JuliaLang/julia #36962
3+
if VERSION >= v"1.6.0-DEV.816" # JuliaLang/julia #36962 # FIXME
44
@test isempty(detect_ambiguities(FixedPointNumbers))
55
else
66
@test isempty(detect_ambiguities(FixedPointNumbers, Base, Core))

0 commit comments

Comments
 (0)