Skip to content

Implement promotion among UFixed, make convert errors more informative #53

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Sep 24, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/FixedPointNumbers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,13 @@ end
const _log2_10 = 3.321928094887362
showcompact{T,f}(io::IO, x::FixedPoint{T,f}) = show(io, round(convert(Float64,x), ceil(Int,f/_log2_10)))

@noinline function throw_converterror{T<:FixedPoint}(::Type{T}, x)
n = 2^(8*sizeof(T))
bitstring = sizeof(T) == 1 ? "an 8-bit" : "a $(8*sizeof(T))-bit"
io = IOBuffer()
showcompact(io, typemin(T)); Tmin = takebuf_string(io)
showcompact(io, typemax(T)); Tmax = takebuf_string(io)
throw(ArgumentError("$T is $bitstring type representing $n values from $Tmin to $Tmax; cannot represent $x"))
end

end # module
56 changes: 44 additions & 12 deletions src/ufixed.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ immutable UFixed{T<:Unsigned,f} <: FixedPoint{T,f}
end

rawtype{T,f}(::Type{UFixed{T,f}}) = T
rawtype(x::Number) = rawtype(typeof(x))
nbitsfrac{T,f}(::Type{UFixed{T,f}}) = f
nbitsfrac(x::Number) = nbitsfract(typeof(x))

typealias UFixed8 UFixed{UInt8,8}
typealias UFixed10 UFixed{UInt16,10}
Expand All @@ -31,29 +33,40 @@ const uf14 = UFixedConstructor{UInt16,14}()
const uf16 = UFixedConstructor{UInt16,16}()

zero{T,f}(::Type{UFixed{T,f}}) = UFixed{T,f}(zero(T),0)
@generated function one{T<:UFixed}(::Type{T})
f = 2^nbitsfrac(T)-1
:( T($f,0) )
function one{T<:UFixed}(::Type{T})
T(typemax(rawtype(T)) >> (8*sizeof(T)-nbitsfrac(T)), 0)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the generated version might be faster. see #44 (comment). or does the use of >> instead of ^ close the gap?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LLVM inlines this new version to a constant:

julia> @code_llvm one(UFixed{UInt64, 51})

define %UFixed @julia_one_70810(%jl_value_t*) #0 {
top:
  ret %UFixed { i64 2251799813685247 }
}

The bigger issue was that for UInt128 (or even for UInt64 on 32-bit machines), 2^f was returning 0 for f bigger than the WORD_SIZE. This version is guaranteed to be safe no matter what kind of machine or what kind of precision we're using.

end
zero(x::UFixed) = zero(typeof(x))
one(x::UFixed) = one(typeof(x))
rawone(v) = reinterpret(one(v))

# Conversions
convert{T<:UFixed}(::Type{T}, x::T) = x
convert{T1<:UFixed}(::Type{T1}, x::UFixed) = reinterpret(T1, round(rawtype(T1), (rawone(T1)/rawone(x))*reinterpret(x)))
function convert{T<:UFixed}(::Type{T}, x::UFixed)
y = round((rawone(T)/rawone(x))*reinterpret(x))
(0 <= y) & (y <= typemax(rawtype(T))) || throw_converterror(T, x)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not just say (0 <= y <= typemax(rawtype(T))) || ... ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance. Currently a <= x <= b gets lowered to (a <= x) && (x <= b) rather than (a <= x) & (x <= b), which means there are two branches rather than one.

This is something that should probably be changed in Julia, but neither I nor anyone else has gotten to it yet.

reinterpret(T, _unsafe_trunc(rawtype(T), y))
end
convert(::Type{UFixed16}, x::UFixed8) = reinterpret(UFixed16, convert(UInt16, 0x0101*reinterpret(x)))
convert{U<:UFixed}(::Type{U}, x::Real) = _convert(U, rawtype(U), x)
_convert{U<:UFixed,T}(::Type{U}, ::Type{T}, x) = U(round(T, widen1(rawone(U))*x), 0)
_convert{U<:UFixed }(::Type{U}, ::Type{UInt128}, x) = U(round(UInt128, rawone(U)*x), 0)
function _convert{U<:UFixed,T}(::Type{U}, ::Type{T}, x)
y = round(widen1(rawone(U))*x)
(0 <= y) & (y <= typemax(T)) || throw_converterror(U, x)
U(_unsafe_trunc(T, y), 0)
end
function _convert{U<:UFixed}(::Type{U}, ::Type{UInt128}, x)
y = round(rawone(U)*x) # for UInt128, we can't widen
Copy link
Collaborator

@vchuravy vchuravy Sep 23, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we definedwiden1(::Type{UInt128}) = UInt128 we wouldn't need to duplicate the code path and we would be forward compatible for the glorious day when we have UInt256.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about that. The flip side is that if one is counting on widen1 to actually protect you, then you'll be mistaken.

But perhaps that's at too low of a level to matter, and that the only issue that matters is whether convert successfully catches the problem. Which refusing to defining widen1 doesn't actually help with. I will change it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, we had a number of problems with high-precision UFixed types.

(0 <= y) & (y <= typemax(UInt128)) & (x <= Float64(typemax(U))) || throw_converterror(U, x)
U(_unsafe_trunc(UInt128, y), 0)
end

rem{T<:UFixed}(x::T, ::Type{T}) = x
rem{T<:UFixed}(x::UFixed, ::Type{T}) = reinterpret(T, unsafe_trunc(rawtype(T), round((rawone(T)/rawone(x))*reinterpret(x))))
rem{T<:UFixed}(x::Real, ::Type{T}) = reinterpret(T, unsafe_trunc(rawtype(T), round(rawone(T)*x)))
rem{T<:UFixed}(x::UFixed, ::Type{T}) = reinterpret(T, _unsafe_trunc(rawtype(T), round((rawone(T)/rawone(x))*reinterpret(x))))
rem{T<:UFixed}(x::Real, ::Type{T}) = reinterpret(T, _unsafe_trunc(rawtype(T), round(rawone(T)*x)))

convert(::Type{BigFloat}, x::UFixed) = reinterpret(x)*(1/BigFloat(rawone(x)))
function convert{T<:AbstractFloat}(::Type{T}, x::UFixed)
y = reinterpret(x)*(1/convert(T, rawone(x)))
y = reinterpret(x)*(one(rawtype(x))/convert(T, rawone(x)))
convert(T, y) # needed for types like Float16 which promote arithmetic to Float32
end
convert(::Type{Bool}, x::UFixed) = x == zero(x) ? false : true
Expand All @@ -68,17 +81,21 @@ sizeof{T<:UFixed}(::Type{T}) = sizeof(rawtype(T))
abs(x::UFixed) = x

# Arithmetic
@generated function floattype{U<:UFixed}(::Type{U})
eps(U) < eps(Float32) ? :(Float64) : :(Float32)
end

(-){T<:UFixed}(x::T) = T(-reinterpret(x), 0)
(~){T<:UFixed}(x::T) = T(~reinterpret(x), 0)

+{T,f}(x::UFixed{T,f}, y::UFixed{T,f}) = UFixed{T,f}(convert(T, x.i+y.i),0)
-{T,f}(x::UFixed{T,f}, y::UFixed{T,f}) = UFixed{T,f}(convert(T, x.i-y.i),0)
*{T<:UFixed}(x::T, y::T) = convert(T,convert(Float32, x)*convert(Float32, y))
/{T<:UFixed}(x::T, y::T) = convert(T,convert(Float32, x)/convert(Float32, y))
*{T<:UFixed}(x::T, y::T) = convert(T,convert(floattype(T), x)*convert(floattype(T), y))
/{T<:UFixed}(x::T, y::T) = convert(T,convert(floattype(T), x)/convert(floattype(T), y))

# Comparisons
<{T<:UFixed}(x::T, y::T) = reinterpret(x) < reinterpret(y)
<={T<:UFixed}(x::T, y::T) = reinterpret(x) < reinterpret(y)
<={T<:UFixed}(x::T, y::T) = reinterpret(x) <= reinterpret(y)

# Functions
trunc{T<:UFixed}(x::T) = T(div(reinterpret(x), rawone(T))*rawone(T),0)
Expand Down Expand Up @@ -150,3 +167,18 @@ promote_rule{T<:UFixed, R<:Rational}(::Type{T}, ::Type{R}) = R
Tp = eps(convert(Float32, typemax(Ti))) > eps(T) ? Float64 : Float32
:( $Tp )
end
@generated function promote_rule{T1,T2,f1,f2}(::Type{UFixed{T1,f1}}, ::Type{UFixed{T2,f2}})
f = max(f1, f2) # ensure we have enough precision
T = promote_type(T1, T2)
# make sure we have enough integer bits
i1, i2 = 8*sizeof(T1)-f1, 8*sizeof(T2)-f2 # number of integer bits for each
i = 8*sizeof(T)-f
while i < max(i1, i2)
T = widen1(T)
i = 8*sizeof(T)-f
end
:(UFixed{$T,$f})
end

_unsafe_trunc{T}(::Type{T}, x::Integer) = x % T
_unsafe_trunc{T}(::Type{T}, x) = unsafe_trunc(T, x)
44 changes: 31 additions & 13 deletions test/ufixed.jl
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ v = @compat UFixed12.([2])
@test v == UFixed12[0x1ffeuf12]
@test isa(v, Vector{UFixed12})

UF2 = (UFixed{UInt32,16}, UFixed{UInt64,3}, UFixed{UInt128,7})
UF2 = (UFixed{UInt32,16}, UFixed{UInt64,3}, UFixed{UInt64,51}, UFixed{UInt128,7}, UFixed{UInt128,51})

for T in (FixedPointNumbers.UF..., UF2...)
@test zero(T) == 0
Expand All @@ -43,15 +43,19 @@ end
@test typemax(UFixed{UInt32,16}) == typemax(UInt32) // (2^16-1)
@test typemax(UFixed{UInt64,3}) == typemax(UInt64) // (2^3-1)
@test typemax(UFixed{UInt128,7}) == typemax(UInt128) // (2^7-1)

@test_throws InexactError UFixed8(2)
@test_throws InexactError UFixed8(255)
@test_throws InexactError UFixed8(0xff)
@test_throws InexactError UFixed16(2)
@test_throws InexactError UFixed16(0xff)
@test_throws InexactError UFixed16(0xffff)
@test_throws InexactError convert(UFixed8, typemax(UFixed10))
@test_throws InexactError convert(UFixed16, typemax(UFixed10))
@test typemax(UFixed{UInt128,100}) == typemax(UInt128) // (UInt128(2)^100-1)

# TODO: change back to InexactError when it allows message strings
@test_throws ArgumentError UFixed8(2)
@test_throws ArgumentError UFixed8(255)
@test_throws ArgumentError UFixed8(0xff)
@test_throws ArgumentError UFixed16(2)
@test_throws ArgumentError UFixed16(0xff)
@test_throws ArgumentError UFixed16(0xffff)
@test_throws ArgumentError convert(UFixed8, typemax(UFixed10))
@test_throws ArgumentError convert(UFixed16, typemax(UFixed10))
@test_throws ArgumentError convert(UFixed{UInt128,100}, 10^9)
@test_throws ArgumentError convert(UFixed{UInt128,100}, 10.0^9)

x = UFixed8(0.5)
@test isfinite(x) == true
Expand All @@ -64,7 +68,7 @@ x = UFixed8(0.5)
@test convert(UFixed14, 1.1/typemax(UInt16)*4) == eps(UFixed14)
@test convert(UFixed16, 1.1/typemax(UInt16)) == eps(UFixed16)
@test convert(UFixed{UInt32,16}, 1.1/typemax(UInt32)*2^16) == eps(UFixed{UInt32,16})
@test convert(UFixed{UInt64,3}, 1.1/typemax(UInt64)*2^61) == eps(UFixed{UInt64,3})
@test convert(UFixed{UInt64,3}, 1.1/typemax(UInt64)*UInt64(2)^61) == eps(UFixed{UInt64,3})
@test convert(UFixed{UInt128,7}, 1.1/typemax(UInt128)*UInt128(2)^121) == eps(UFixed{UInt128,7})

@test convert(UFixed8, 1.1f0/typemax(UInt8)) == eps(UFixed8)
Expand Down Expand Up @@ -93,15 +97,18 @@ end
@test (65.2 % UFixed10).i == round(Int, 65.2*1023) % UInt16
@test (-0.3 % UFixed10).i == round(Int, -0.3*1023) % UInt16

@test 1 % UFixed8 == 1
@test 2 % UFixed8 == UFixed8(0.996)

x = UFixed8(0b01010001, 0)
@test ~x == UFixed8(0b10101110, 0)
@test -x == 0xafuf8

for T in (FixedPointNumbers.UF..., UF2...)
x = T(0x10,0)
y = T(0x25,0)
fx = convert(Float32, x)
fy = convert(Float32, y)
fx = convert(FixedPointNumbers.floattype(T), x)
fy = convert(FixedPointNumbers.floattype(T), y)
@test y > x
@test y != x
@test typeof(x+y) == T
Expand Down Expand Up @@ -154,6 +161,9 @@ for T in (FixedPointNumbers.UF..., UF2...)
testtrunc(eps(T))
end

@test !(UFixed8(0.5) < UFixed8(0.5))
@test UFixed8(0.5) <= UFixed8(0.5)

@test div(0x10uf8, 0x02uf8) == fld(0x10uf8, 0x02uf8) == 8
@test div(0x0fuf8, 0x02uf8) == fld(0x0fuf8, 0x02uf8) == 7
@test Base.fld1(0x10uf8, 0x02uf8) == 8
Expand All @@ -166,6 +176,14 @@ end
r = 1uf8:1uf8:48uf8
@test length(r) == 48

# Promotion within UFixed
@test @inferred(promote(UFixed8(0.2), UFixed8(0.8))) ===
(UFixed8(0.2), UFixed8(0.8))
@test @inferred(promote(UFixed{UInt16,3}(0.2), UFixed{UInt8,3}(0.86))) ===
(UFixed{UInt16,3}(0.2), UFixed{UInt16,3}(0.86))
@test @inferred(promote(UFixed{UInt8,7}(0.197), UFixed{UInt8,4}(0.8))) ===
(UFixed{UInt16,7}(0.197), UFixed{UInt16,7}(0.8))

# Show
x = 0xaauf8
iob = IOBuffer()
Expand Down