diff --git a/.github/workflows/TagBot.yml b/.github/workflows/TagBot.yml index d77d3a0c..eb6c8610 100644 --- a/.github/workflows/TagBot.yml +++ b/.github/workflows/TagBot.yml @@ -1,9 +1,17 @@ name: TagBot on: - schedule: - - cron: 0 * * * * + issue_comment: + types: + - created + workflow_dispatch: + inputs: + lookback: + default: 3 +permissions: + contents: write jobs: TagBot: + if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot' runs-on: ubuntu-latest steps: - uses: JuliaRegistries/TagBot@v1 diff --git a/.github/workflows/UnitTest.yml b/.github/workflows/UnitTest.yml index 24695405..0bcc5eb4 100644 --- a/.github/workflows/UnitTest.yml +++ b/.github/workflows/UnitTest.yml @@ -1,14 +1,13 @@ name: Unit test on: - create: - tags: push: branches: - master + - release-* + tags: ['*'] pull_request: - schedule: - - cron: '20 00 1 * *' + workflow_dispatch: jobs: test: @@ -16,40 +15,45 @@ jobs: strategy: fail-fast: false matrix: - julia-version: ['1.0', '1', 'nightly'] - os: [ubuntu-latest, windows-latest, macOS-latest] + julia-version: ['1.0', '1.6', '1', 'nightly'] + os: [ubuntu-latest, windows-latest, macos-13] julia-arch: [x64] - # only test one 32-bit job include: - - os: ubuntu-latest + - os: ubuntu-latest # only test one 32-bit job julia-version: '1' julia-arch: x86 + - os: macos-latest + julia-version: '1' + julia-arch: aarch64 + - os: macos-latest + julia-version: 'nightly' + julia-arch: aarch64 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: "Set up Julia" - uses: julia-actions/setup-julia@v1 + uses: julia-actions/setup-julia@v2 with: version: ${{ matrix.julia-version }} arch: ${{ matrix.julia-arch }} - name: Cache artifacts - uses: actions/cache@v1 + uses: actions/cache@v4 env: cache-name: cache-artifacts with: - path: ~/.julia/artifacts + path: ~/.julia/artifacts key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} restore-keys: | ${{ runner.os }}-test-${{ env.cache-name }}- ${{ runner.os }}-test- ${{ runner.os }}- - name: "Unit Test" - uses: julia-actions/julia-runtest@master + uses: julia-actions/julia-runtest@v1 - uses: julia-actions/julia-processcoverage@v1 - - uses: codecov/codecov-action@v1 + - uses: codecov/codecov-action@v4 with: + token: ${{ secrets.CODECOV_TOKEN }} # required + fail_ci_if_error: true file: lcov.info - - diff --git a/.github/workflows/UnitTestArm.yml b/.github/workflows/UnitTestArm.yml new file mode 100644 index 00000000..3af6d0a5 --- /dev/null +++ b/.github/workflows/UnitTestArm.yml @@ -0,0 +1,76 @@ +name: Unit test for Arm + +on: + push: + branches: + - master + - release-* + tags: ['*'] + pull_request: + workflow_dispatch: +permissions: + actions: write + contents: read +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + julia-version: ['1.0', '1.6', '1', 'nightly'] + os: [ubuntu-latest] + distro: [ubuntu_latest] + arch: [aarch64] + + steps: + - uses: actions/checkout@v4 + - uses: julia-actions/setup-julia@v2 + with: + version: ${{ matrix.julia-version }} + - uses: julia-actions/cache@v1 + - name: Download Julia Binary + run: > + julia -e ' + using Pkg; Pkg.add("JSON"); using JSON; + if "${{ matrix.julia-version }}" == "nightly"; + url = "https://julialangnightlies-s3.julialang.org/bin/linux/${{ matrix.arch }}/julia-latest-linux-${{ matrix.arch }}.tar.gz"; + else; + path = download("https://julialang-s3.julialang.org/bin/versions.json"); + json = JSON.parsefile(path); + try rm(path) catch end; + vspec = Pkg.Types.VersionSpec("${{ matrix.julia-version }}"); + a(f) = f["arch"] == "${{ matrix.arch }}" && f["os"] == "linux" && !occursin("musl", f["triplet"]); + m = filter(json) do v; vn = VersionNumber(v[1]); vn in vspec && isempty(vn.prerelease) && any(a, v[2]["files"]); end; + v = sort(VersionNumber.(keys(m)))[end]; + url = filter(a, json[string(v)]["files"])[1]["url"]; + end; + download(url, "/tmp/julia-aarch64.tar.gz");' + + - name: Extract Julia Files + run: | + mkdir -p /home/runner/work/julia/ + tar -xf /tmp/julia-aarch64.tar.gz --strip-components=1 -C /home/runner/work/julia/ + rm /tmp/julia-aarch64.tar.gz + + - uses: uraimo/run-on-arch-action@v2.7.2 + name: Unit Test + with: + arch: ${{ matrix.arch }} + distro: ${{ matrix.distro }} + dockerRunArgs: | + -v "/home/runner/work/julia:/home/runner/work/julia" + -v "/home/runner/.julia/registries:/root/.julia/registries" + --net=host + install: | + ln -s /home/runner/work/julia/bin/julia /usr/local/bin/julia + echo /home/runner/work/julia/lib > /etc/ld.so.conf.d/julia.conf + mkdir -p /root/.julia/registries/General + run: | + julia --compile=min -O0 -e 'using InteractiveUtils; versioninfo();' + julia --project=. --check-bounds=yes --color=yes -e 'using Pkg; Pkg.build(); Pkg.test(coverage=true)' + - uses: julia-actions/julia-processcoverage@v1 + - uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} # required + fail_ci_if_error: true + file: lcov.info diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index aff8557d..00000000 --- a/.travis.yml +++ /dev/null @@ -1,15 +0,0 @@ -language: julia -os: - - linux -julia: - - 1.0 - - 1 - - nightly -arch: - - arm64 -cache: - directories: - - $HOME/.julia/artifacts -notifications: - email: false -codecov: true diff --git a/Project.toml b/Project.toml index 31c836cc..d2c137aa 100644 --- a/Project.toml +++ b/Project.toml @@ -3,13 +3,32 @@ uuid = "53c48c17-4a7d-5ca2-90c5-79b7896eea93" version = "0.8.4" [deps] +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" +[weakdeps] +Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" + +[extensions] +FixedPointNumbersStatisticsExt = "Statistics" + [compat] +Aqua = "0.8" +Documenter = "0.27, 1" +Random = "<0.0.1, 1" +StableRNGs = "1" +# Update this version specifier when Statistics.jl v1.11.2 is released. +# https://github.com/JuliaStats/Statistics.jl/issues/165 +Statistics = "< 1.11.2" +Test = "1" julia = "1" [extras] +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" +Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Test"] +test = ["Aqua", "Documenter", "StableRNGs", "Statistics", "Test"] diff --git a/README.md b/README.md index 0366ace5..35f9e202 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![Build Status][action-img]][action-url] [![Build Status][pkgeval-img]][pkgeval-url] [![coverage][codecov-img]][codecov-url] +[![Aqua QA](https://raw.githubusercontent.com/JuliaTesting/Aqua.jl/master/badge.svg)](https://github.com/JuliaTesting/Aqua.jl) This library implements fixed-point number types. A [fixed-point number] represents a fractional, or diff --git a/ext/FixedPointNumbersStatisticsExt.jl b/ext/FixedPointNumbersStatisticsExt.jl new file mode 100644 index 00000000..5cf777fa --- /dev/null +++ b/ext/FixedPointNumbersStatisticsExt.jl @@ -0,0 +1,13 @@ +module FixedPointNumbersStatisticsExt + +using FixedPointNumbers +import FixedPointNumbers: Treduce +using Statistics + +import Statistics +if isdefined(Statistics, :_mean_promote) + # https://github.com/JuliaMath/FixedPointNumbers.jl/pull/183 + Statistics._mean_promote(x::Real, y::FixedPoint) = Treduce(y) +end + +end diff --git a/src/FixedPointNumbers.jl b/src/FixedPointNumbers.jl index 7e4ab7d3..f1368ae0 100644 --- a/src/FixedPointNumbers.jl +++ b/src/FixedPointNumbers.jl @@ -5,13 +5,14 @@ import Base: ==, <, <=, -, +, *, /, ~, isapprox, isnan, isinf, isfinite, isinteger, zero, oneunit, one, typemin, typemax, floatmin, floatmax, eps, reinterpret, big, rationalize, float, trunc, round, floor, ceil, bswap, clamp, - div, fld, rem, mod, mod1, fld1, min, max, minmax, + div, fld, cld, rem, mod, mod1, fld1, min, max, minmax, signed, unsigned, copysign, flipsign, signbit, - rand, length + length -import Statistics # for _mean_promote +import Random: Random, AbstractRNG, SamplerType, rand! -using Base.Checked: checked_add, checked_sub, checked_div +import Base.Checked: checked_mul, checked_div +using Base.Checked: checked_add, checked_sub using Base: @pure @@ -49,6 +50,7 @@ rawtype(::Type{X}) where {T, X <: FixedPoint{T}} = T # traits based on static parameters signbits(::Type{X}) where {T, X <: FixedPoint{T}} = T <: Unsigned ? 0 : 1 +nbitsint(::Type{X}) where {X <: FixedPoint} = bitwidth(X) - nbitsfrac(X) - signbits(X) # construction using the (approximate) intended value, i.e., N0f8 *(x::Real, ::Type{X}) where {X <: FixedPoint} = _convert(X, x) @@ -136,7 +138,7 @@ except in scenarios where they are guaranteed to have hardware support. A classic usage is to avoid overflow behavior by promoting `FixedPoint` to `AbstractFloat` -```jldoctest +```jldoctest; setup = :(using FixedPointNumbers) julia> x = N0f8(1.0) 1.0N0f8 @@ -178,6 +180,21 @@ floattype(::Type{Base.TwicePrecision{T}}) where T<:Union{Float16,Float32} = wide float(x::FixedPoint) = convert(floattype(x), x) +wrapping_mul(x::X, y::X) where {X <: FixedPoint} = (float(x) * float(y)) % X +*(x::X, y::X) where {X <: FixedPoint} = wrapping_mul(x, y) + +function checked_div(x::X, y::X, r::RoundingMode = RoundToZero) where {T, X <: FixedPoint{T}} + y === zero(X) && throw(DivideError()) + z = round(floattype(X)(x.i) / floattype(X)(y.i), r) + if T <: Signed + z <= typemax(T) || throw_overflowerror_div(r, x, y) + end + _unsafe_trunc(T, z) +end +div(x::X, y::X, r::RoundingMode = RoundToZero) where {X <: FixedPoint} = checked_div(x, y, r) +fld(x::X, y::X) where {X <: FixedPoint} = checked_div(x, y, RoundDown) +cld(x::X, y::X) where {X <: FixedPoint} = checked_div(x, y, RoundUp) + function minmax(x::X, y::X) where {X <: FixedPoint} a, b = minmax(reinterpret(x), reinterpret(y)) X(a,0), X(b,0) @@ -193,6 +210,12 @@ clamp(x::X, lo::X, hi::X) where {X <: FixedPoint} = X(clamp(x.i, lo.i, hi.i), 0) clamp(x, ::Type{X}) where {X <: FixedPoint} = clamp(x, typemin(X), typemax(X)) % X +# Workaround for poor promotion due to lack of PR #207 +function clamp(x::AbstractFloat, ::Type{X}) where {X <: FixedPoint} + Tf = promote_type(typeof(x), floattype(X)) + clamp(Tf(x), typemin(X), typemax(X)) % X +end + # Since `FixedPoint` is not an integer type, it is not clear in what type # `signed` and `unsigned` for `FixedPoint` should return values. They should # currently throw errors in case we support "unsigned Fixed" or "signed Normed" @@ -217,12 +240,15 @@ end signbit(x::X) where {X <: FixedPoint} = signbit(x.i) +trunc(x::X) where {X <: FixedPoint{<:Unsigned}} = floor(x) +trunc(::Type{Ti}, x::X) where {X <: FixedPoint{<:Unsigned}, Ti <: Integer} = floor(Ti, x) + for f in (:zero, :oneunit, :one, :eps, :rawone, :rawtype, :floattype) @eval begin $f(x::FixedPoint) = $f(typeof(x)) end end -for f in (:(==), :<, :<=, :div, :fld, :fld1) +for f in (:(==), :<, :<=, :fld1) @eval begin $f(x::X, y::X) where {X <: FixedPoint} = $f(x.i, y.i) end @@ -260,19 +286,45 @@ function length(r::StepRange{<:FixedPoint}) return div((stop - start) + step, step) end -# Printing. These are used to generate type-symbols, so we need them -# before we include any files. -function showtype(io::IO, ::Type{X}) where {X <: FixedPoint} - print(io, typechar(X)) - f = nbitsfrac(X) - m = bitwidth(X)-f-signbits(X) - print(io, m, 'f', f) - io +hasalias(::Type) = false +hasalias(::Type{X}) where {T<:NotBiggerThanInt64, f, X<:FixedPoint{T,f}} = f isa Int + +# `alias_symbol` is used to define type aliases, so we need this before we +# include "src/fixed.jl" / "src/normed.jl". +function alias_symbol(@nospecialize(X)) + Symbol(type_prefix(X), nbitsint(X), 'f', nbitsfrac(X)) +end + +function _alias_symbol(::Type{X}) where {X <: FixedPoint} + if @generated + return QuoteNode(alias_symbol(X)) + else + return alias_symbol(X) + end +end + +@inline function showtype(io::IO, ::Type{X}) where {X <: FixedPoint} + if hasalias(X) + write(io, _alias_symbol(X)) + else + print(io, X) + end + return nothing end + function show(io::IO, x::FixedPoint{T,f}) where {T,f} + compact = get(io, :compact, false)::Bool log10_2 = 0.3010299956639812 - show(io, round(convert(Float64,x), digits=ceil(Int, f * log10_2))) - get(io, :compact, false) || showtype(io, typeof(x)) + val = round(convert(Float64, x), digits=ceil(Int, f * log10_2)) + if compact + show(io, val) + elseif hasalias(typeof(x)) + show(io, val) + showtype(io, typeof(x)) + else + print(io, typeof(x), '(', val, ')') + end + return nothing end function Base.showarg(io::IO, a::Array{T}, toplevel) where {T<:FixedPoint} @@ -297,10 +349,6 @@ Base.mul_prod(x::FixedPoint, y::FixedPoint) = Treduce(x) * Treduce(y) Base.reduce_empty(::typeof(Base.mul_prod), ::Type{F}) where {F<:FixedPoint} = one(Treduce) Base.reduce_first(::typeof(Base.mul_prod), x::FixedPoint) = Treduce(x) -if isdefined(Statistics, :_mean_promote) - Statistics._mean_promote(x::Real, y::FixedPoint) = Treduce(y) -end - """ sd, ad = scaledual(s::Number, a) @@ -317,17 +365,45 @@ scaledual(::Type{Tdual}, x::FixedPoint) where Tdual = convert(Tdual, 1/rawone(x) scaledual(::Type{Tdual}, x::AbstractArray{T}) where {Tdual, T <: FixedPoint} = convert(Tdual, 1/rawone(T)), reinterpret(rawtype(T), x) -@noinline function throw_converterror(::Type{X}, x) where {X <: FixedPoint} - n = 2^bitwidth(X) - bitstring = bitwidth(X) == 8 ? "an 8-bit" : "a $(bitwidth(X))-bit" +@noinline function throw_converterror(::Type{X}, @nospecialize(x)) where X <: FixedPoint + nbits = bitwidth(rawtype(X)) + io = IOBuffer() + showtype(io, X) + print(io, " is ") + print(io, nbits == 8 ? "an " : "a ", nbits, "-bit type representing ") + print(io, nbits <= 16 ? string(2^nbits) : "2^$nbits", " values from ") + print(IOContext(io, :compact=>true), typemin(X), " to ") + print(IOContext(io, :compact=>true), typemax(X), "; ") + print(io, "cannot represent ", x) + throw(ArgumentError(String(take!(io)))) +end + +@noinline function throw_overflowerror(op::Symbol, @nospecialize(x), @nospecialize(y)) io = IOBuffer() - show(IOContext(io, :compact=>true), typemin(X)); Xmin = String(take!(io)) - show(IOContext(io, :compact=>true), typemax(X)); Xmax = String(take!(io)) - throw(ArgumentError("$X is $bitstring type representing $n values from $Xmin to $Xmax; cannot represent $x")) + print(io, x, ' ', op, ' ', y, " overflowed for type ") + showtype(io, typeof(x)) + throw(OverflowError(String(take!(io)))) +end +@noinline function throw_overflowerror_div(r::RoundingMode, @nospecialize(x), @nospecialize(y)) + io = IOBuffer() + op = r === RoundUp ? "cld(" : r === RoundDown ? "fld(" : "div(" + print(io, op, x, ", ", y, ") overflowed for type ", rawtype(x)) + throw(OverflowError(String(take!(io)))) +end + +function Random.rand(r::AbstractRNG, ::SamplerType{X}) where X <: FixedPoint + X(rand(r, rawtype(X)), 0) end -rand(::Type{T}) where {T <: FixedPoint} = reinterpret(T, rand(rawtype(T))) -rand(::Type{T}, sz::Dims) where {T <: FixedPoint} = reinterpret(T, rand(rawtype(T), sz)) +function rand!(r::AbstractRNG, A::Array{X}, ::SamplerType{X}) where {T, X <: FixedPoint{T}} + At = unsafe_wrap(Array, reinterpret(Ptr{T}, pointer(A)), size(A)) + Random.rand!(r, At, SamplerType{T}()) + A +end + +if !isdefined(Base, :get_extension) + include("../ext/FixedPointNumbersStatisticsExt.jl") +end if VERSION >= v"1.1" # work around https://github.com/JuliaLang/julia/issues/34121 include("precompile.jl") diff --git a/src/deprecations.jl b/src/deprecations.jl index 30d586a8..2441efd3 100644 --- a/src/deprecations.jl +++ b/src/deprecations.jl @@ -7,3 +7,11 @@ function floattype(::Type{T}) where {T <: Real} """, :floattype) return T end + +function typechar(::Type{X}) where {X} + Base.depwarn(""" + `typechar` was deprecated since the prefix may not be a single character in the future. + We recommend not using private functions, but if you need to, use `type_prefix` instead. + """, :typechar) + Char(string(type_prefix(X))[1]) +end diff --git a/src/fixed.jl b/src/fixed.jl index 5beb98df..af7fc1ce 100644 --- a/src/fixed.jl +++ b/src/fixed.jl @@ -24,14 +24,17 @@ struct Fixed{T <: Signed, f} <: FixedPoint{T, f} end end -typechar(::Type{X}) where {X <: Fixed} = 'Q' +# TODO: remove this +hasalias(::Type{F}) where {F <: Union{Fixed{Int8,8},Fixed{Int16,16},Fixed{Int32,32},Fixed{Int64,64}}} = false + +type_prefix(::Type{F}) where {F <: Fixed{<:Signed}} = :Q for T in (Int8, Int16, Int32, Int64) - io = IOBuffer() for f in 0:bitwidth(T)-1 - sym = Symbol(String(take!(showtype(io, Fixed{T,f})))) + F = Fixed{T,f} + sym = alias_symbol(F) @eval begin - const $sym = Fixed{$T,$f} + const $sym = $F export $sym end end @@ -92,19 +95,28 @@ function _convert(::Type{F}, x::Rational) where {T, f, F <: Fixed{T,f}} end end -# unchecked arithmetic - -# with truncation: -#*(x::Fixed{T,f}, y::Fixed{T,f}) = Fixed{T,f}(Base.widemul(x.i,y.i)>>f,0) -# with rounding up: -*(x::Fixed{T,f}, y::Fixed{T,f}) where {T,f} = Fixed{T,f}((Base.widemul(x.i,y.i) + (one(widen(T)) << (f-1)))>>f,0) - -/(x::Fixed{T,f}, y::Fixed{T,f}) where {T,f} = Fixed{T,f}(div(convert(widen(T), x.i) << f, y.i), 0) - - -rem(x::Integer, ::Type{Fixed{T,f}}) where {T,f} = Fixed{T,f}(rem(x,T)<>> (bitwidth(T) - f - 1)) + half = oneunit(T) << (f - 1) + c = half - (xf === half) + (x + c) >> f +end +div_2f(x::T, ::Val{0}) where {T} = x + +# wrapping arithmetic +function wrapping_mul(x::F, y::F) where {T <: Union{Int8,Int16,Int32,Int64}, f, F <: Fixed{T,f}} + z = widemul(x.i, y.i) + F(div_2f(z, Val(Int(f))) % T, 0) +end + +function mul_with_rounding(x::F, y::F, ::RoundingMode{:Nearest}) where {F <: Fixed} + wrapping_mul(x, y) +end +function mul_with_rounding(x::F, y::F, ::RoundingMode{:NearestTiesUp}) where + {T <: Union{Int8,Int16,Int32,Int64}, f, F <: Fixed{T, f}} + z = widemul(x.i, y.i) + F(((z + (oftype(z, 1) << f >>> 1)) >> f) % T, 0) +end +function mul_with_rounding(x::F, y::F, ::RoundingMode{:Down}) where + {T <: Union{Int8,Int16,Int32,Int64}, f, F <: Fixed{T, f}} + F((widemul(x.i, y.i) >> f) % T, 0) +end + +/(x::Fixed{T,f}, y::Fixed{T,f}) where {T,f} = Fixed{T,f}(div(convert(widen(T), x.i) << f, y.i), 0) + function trunc(x::Fixed{T,f}) where {T, f} f == 0 && return x f == bitwidth(T) && return zero(x) # TODO: remove this line diff --git a/src/normed.jl b/src/normed.jl index 5c2f6c4f..759f369f 100644 --- a/src/normed.jl +++ b/src/normed.jl @@ -19,14 +19,14 @@ struct Normed{T <: Unsigned, f} <: FixedPoint{T, f} end end -typechar(::Type{X}) where {X <: Normed} = 'N' +type_prefix(::Type{N}) where {N <: Normed{<:Unsigned}} = :N for T in (UInt8, UInt16, UInt32, UInt64) - io = IOBuffer() for f in 1:bitwidth(T) - sym = Symbol(String(take!(showtype(io, Normed{T,f})))) + N = Normed{T,f} + sym = alias_symbol(N) @eval begin - const $sym = Normed{$T,$f} + const $sym = $N export $sym end end @@ -110,24 +110,28 @@ end rem(x::N, ::Type{N}) where {N <: Normed} = x rem(x::Normed, ::Type{N}) where {T, N <: Normed{T}} = reinterpret(N, _unsafe_trunc(T, round((rawone(N)/rawone(x))*reinterpret(x)))) -rem(x::Real, ::Type{N}) where {T, N <: Normed{T}} = reinterpret(N, _unsafe_trunc(T, round(rawone(N)*x))) +function rem(x::Real, ::Type{N}) where {T, N <: Normed{T}} + bitwidth(T) < 32 || isfinite(x) || return zero(N) + reinterpret(N, _unsafe_trunc(T, round(rawone(N) * x))) +end rem(x::Float16, ::Type{N}) where {N <: Normed} = rem(Float32(x), N) # avoid overflow # Float32 and Float64 cannot exactly represent `rawone(N)` with `f` greater than # the number of their significand bits, resulting in rounding errors (issue #150). # So, we use another strategy for the large `f`s explained in: # https://github.com/JuliaMath/FixedPointNumbers.jl/pull/166#issuecomment-574135643 function rem(x::Float32, ::Type{N}) where {f, N <: Normed{UInt32,f}} + isfinite(x) || return zero(N) f <= 24 && return reinterpret(N, _unsafe_trunc(UInt32, round(rawone(N) * x))) r = _unsafe_trunc(UInt32, round(x * @f32(0x1p24))) reinterpret(N, r << UInt8(f - 24) - unsigned(signed(r) >> 0x18)) end function rem(x::Float64, ::Type{N}) where {f, N <: Normed{UInt64,f}} + isfinite(x) || return zero(N) f <= 53 && return reinterpret(N, _unsafe_trunc(UInt64, round(rawone(N) * x))) r = _unsafe_trunc(UInt64, round(x * 0x1p53)) reinterpret(N, r << UInt8(f - 53) - unsigned(signed(r) >> 0x35)) end - function (::Type{T})(x::Normed) where {T <: AbstractFloat} # The following optimization for constant division may cause rounding errors. # y = reinterpret(x)*(one(rawtype(x))/convert(T, rawone(x))) @@ -248,12 +252,40 @@ Base.BigFloat(x::Normed) = reinterpret(x) / BigFloat(rawone(x)) Base.Rational(x::Normed) = reinterpret(x)//rawone(x) -# unchecked arithmetic -*(x::T, y::T) where {T <: Normed} = convert(T,convert(floattype(T), x)*convert(floattype(T), y)) +# Division by `2^f-1` with RoundNearest. The result would be in the lower half bits. +div_2fm1(x::T, ::Val{f}) where {T, f} = (x + (T(1)<<(f - 1) - 0x1)) ÷ (T(1) << f - 0x1) +div_2fm1(x::T, ::Val{1}) where T = x +div_2fm1(x::UInt16, ::Val{8}) = (((x + 0x80) >> 0x8) + x + 0x80) >> 0x8 +div_2fm1(x::UInt32, ::Val{16}) = (((x + 0x8000) >> 0x10) + x + 0x8000) >> 0x10 +div_2fm1(x::UInt64, ::Val{32}) = (((x + 0x80000000) >> 0x20) + x + 0x80000000) >> 0x20 +div_2fm1(x::UInt128, ::Val{64}) = (((x + 0x8000000000000000) >> 0x40) + x + 0x8000000000000000) >> 0x40 + +# wrapping arithmetic +function wrapping_mul(x::N, y::N) where {T <: Union{UInt8,UInt16,UInt32,UInt64}, f, N <: Normed{T,f}} + z = widemul(x.i, y.i) + N(div_2fm1(z, Val(Int(f))) % T, 0) +end + +# checked arithmetic +function checked_mul(x::N, y::N) where {N <: Normed} + z = float(x) * float(y) + z < typemax(N) + eps(N)/2 || throw_overflowerror(:*, x, y) + z % N +end +function checked_mul(x::N, y::N) where {T <: Union{UInt8,UInt16,UInt32,UInt64}, f, N <: Normed{T,f}} + f == bitwidth(T) && return wrapping_mul(x, y) + z = widemul(x.i, y.i) + m = widemul(typemax(N).i, rawone(N)) + (rawone(N) >> 0x1) + z < m || throw_overflowerror(:*, x, y) + N(div_2fm1(z, Val(Int(f))) % T, 0) +end + +# Override the default arithmetic with `checked` for backward compatibility +*(x::N, y::N) where {N <: Normed} = checked_mul(x, y) + /(x::T, y::T) where {T <: Normed} = convert(T,convert(floattype(T), x)/convert(floattype(T), y)) # Functions -trunc(x::N) where {N <: Normed} = floor(x) floor(x::N) where {N <: Normed} = reinterpret(N, x.i - x.i % rawone(N)) function ceil(x::Normed{T,f}) where {T, f} f == 1 && return x @@ -270,7 +302,6 @@ function round(x::Normed{T,f}) where {T, f} reinterpret(Normed{T,f}, r > q ? x.i + q : x.i - r) end -trunc(::Type{Ti}, x::Normed) where {Ti <: Integer} = floor(Ti, x) function floor(::Type{Ti}, x::Normed) where {Ti <: Integer} convert(Ti, reinterpret(x) ÷ rawone(x)) end @@ -283,16 +314,6 @@ function round(::Type{Ti}, x::Normed) where {Ti <: Integer} convert(Ti, r > (rawone(x) >> 0x1) ? d + oneunit(rawtype(x)) : d) end -# Iteration -# The main subtlety here is that iterating over N0f8(0):N0f8(1) will wrap around -# unless we iterate using a wider type -@inline start(r::StepRange{T}) where {T <: Normed} = widen1(reinterpret(r.start)) -@inline next(r::StepRange{T}, i::Integer) where {T <: Normed} = (T(i,0), i+reinterpret(r.step)) -@inline function done(r::StepRange{T}, i::Integer) where {T <: Normed} - i1, i2 = reinterpret(r.start), reinterpret(r.stop) - isempty(r) | (i < min(i1, i2)) | (i > max(i1, i2)) -end - function decompose(x::Normed) g = gcd(reinterpret(x), rawone(x)) div(reinterpret(x),g), 0, div(rawone(x),g) diff --git a/src/precompile.jl b/src/precompile.jl index 178be83f..d945baf2 100644 --- a/src/precompile.jl +++ b/src/precompile.jl @@ -1,30 +1,32 @@ +using Random + function _precompile_() ccall(:jl_generating_output, Cint, ()) == 1 || return nothing normedtypes = (N0f8, N0f16) # precompiled Normed types realtypes = (Float16, Float32, Float64, Int) # types for mixed Normed/Real operations for T in normedtypes for f in (+, -, abs, eps, rand) # unary operations - @assert precompile(Tuple{typeof(f),T}) + precompile(Tuple{typeof(f),T}) end - @assert precompile(Tuple{typeof(rand),T,Tuple{Int}}) - @assert precompile(Tuple{typeof(rand),T,Tuple{Int,Int}}) + precompile(Tuple{typeof(rand),T,Tuple{Int}}) + precompile(Tuple{typeof(rand),T,Tuple{Int,Int}}) for f in (trunc, floor, ceil, round) # rounding operations - @assert precompile(Tuple{typeof(f),T}) - @assert precompile(Tuple{typeof(f),Type{Int},T}) + precompile(Tuple{typeof(f),T}) + precompile(Tuple{typeof(f),Type{Int},T}) end for f in (+, -, *, /, <, <=, ==) # binary operations - @assert precompile(Tuple{typeof(f),T,T}) + precompile(Tuple{typeof(f),T,T}) for S in realtypes - @assert precompile(Tuple{typeof(f),T,S}) - @assert precompile(Tuple{typeof(f),S,T}) + precompile(Tuple{typeof(f),T,S}) + precompile(Tuple{typeof(f),S,T}) end end # conversions for S in realtypes - @assert precompile(Tuple{Type{T},S}) - @assert precompile(Tuple{Type{S},T}) - @assert precompile(Tuple{typeof(convert),Type{T},S}) - @assert precompile(Tuple{typeof(convert),Type{S},T}) + precompile(Tuple{Type{T},S}) + precompile(Tuple{Type{S},T}) + precompile(Tuple{typeof(convert),Type{T},S}) + precompile(Tuple{typeof(convert),Type{S},T}) end end end diff --git a/src/utilities.jl b/src/utilities.jl index 2c685de8..60352e93 100644 --- a/src/utilities.jl +++ b/src/utilities.jl @@ -1,6 +1,7 @@ # utility functions and macros, which are independent of `FixedPoint` bitwidth(T::Type) = 8sizeof(T) +widen1(T::Type) = T # fallback widen1(::Type{Int8}) = Int16 widen1(::Type{UInt8}) = UInt16 widen1(::Type{Int16}) = Int32 @@ -9,8 +10,6 @@ widen1(::Type{Int32}) = Int64 widen1(::Type{UInt32}) = UInt64 widen1(::Type{Int64}) = Int128 widen1(::Type{UInt64}) = UInt128 -widen1(::Type{Int128}) = Int128 -widen1(::Type{UInt128}) = UInt128 widen1(x::Integer) = x % widen1(typeof(x)) signedtype(::Type{T}) where {T <: Integer} = typeof(signed(zero(T))) @@ -20,6 +19,7 @@ const LongInts = Union{Int64, UInt64, Int128, UInt128, BigInt} const ShorterThanInt = Int === Int32 ? ShortInts : Union{ShortInts, Int32, UInt32} const NotBiggerThanInt = Union{ShorterThanInt, Int, UInt} +const NotBiggerThanInt64 = Union{ShortInts, Int32, UInt32, Int64, UInt64} const SShorterThanInt = typeintersect(ShorterThanInt, Signed) const UShorterThanInt = typeintersect(ShorterThanInt, Unsigned) @@ -38,12 +38,17 @@ exponent_bias(::Type{Float32}) = 127 exponent_bias(::Type{Float64}) = 1023 _unsafe_trunc(::Type{T}, x::Integer) where {T} = x % T -_unsafe_trunc(::Type{T}, x) where {T} = unsafe_trunc(T, x) -if !signbit(signed(unsafe_trunc(UInt, -12.345))) - # a workaround for ARM (issue #134) - function _unsafe_trunc(::Type{T}, x::AbstractFloat) where {T <: Integer} - unsafe_trunc(T, unsafe_trunc(signedtype(T), x)) +_unsafe_trunc(::Type{T}, x) where {T} = unsafe_trunc(T, x) +# issue #202, #211 +_unsafe_trunc(::Type{T}, x::BigFloat) where {T <: Integer} = trunc(BigInt, x) % T + +# issue #288 +function _unsafe_trunc(::Type{T}, x::AbstractFloat) where {T <: Integer} + if T <: ShortInts + return unsafe_trunc(Int32, x) % T + elseif T <: Unsigned + return copysign(unsafe_trunc(T, abs(x)), x) + else + return unsafe_trunc(T, x) end - # exclude BigFloat (issue #202) - _unsafe_trunc(::Type{T}, x::BigFloat) where {T <: Integer} = unsafe_trunc(T, x) end diff --git a/test/common.jl b/test/common.jl new file mode 100644 index 00000000..f8b1295b --- /dev/null +++ b/test/common.jl @@ -0,0 +1,303 @@ +using FixedPointNumbers, Statistics, Random, StableRNGs, Test +using FixedPointNumbers: bitwidth, rawtype, nbitsfrac + +SP = VERSION >= v"1.6.0-DEV.771" ? " " : "" # JuliaLang/julia #37085 + +wrapping_neg = (-) +wrapping_abs = abs +wrapping_add = (+) +wrapping_sub = (-) +wrapping_mul = FixedPointNumbers.wrapping_mul +checked_mul = FixedPointNumbers.checked_mul +checked_fdiv = (/) # The current implementations are inconsistent (do not check properly). +checked_div = FixedPointNumbers.checked_div +checked_fld = fld +checked_cld = cld + +""" + target(X::Type, Ss...; ex = :default) + +Return a generator which enumerates the target types for testing. + +# Arguments +- `X`: target base type +- `Ss`: symbols for specifying the target raw types + - `:i*` : a `Signed` type if `X === Fixed`, or an `Unsigned` type if `X === Normed` + - `:s*` : a `Signed` type (not yet supported) + - `:u*` : an `Unsigned` type (not yet supported) +- `ex`: exhaustivity of `f`s (see also the [`target_f`](@ref) function) + - `:heavy`: all supported `f`s + - `:default`: same as `:heavy` for 8-/16-bit types, and same as `:light` otherwise + - `:light`: important `f`s for byte boundaries and floating point types + - `:thin`: maximum and half `f`s per type + +# Example +```julia +julia> collect(target(Normed, :i8, :i32; ex = :default)) +21-element Array{DataType,1}: + Normed{UInt8,1} + Normed{UInt8,2} + Normed{UInt8,3} + Normed{UInt8,4} + Normed{UInt8,5} + Normed{UInt8,6} + Normed{UInt8,7} + Normed{UInt8,8} + Normed{UInt32,1} + Normed{UInt32,7} + Normed{UInt32,8} + Normed{UInt32,9} + Normed{UInt32,10} + Normed{UInt32,11} + Normed{UInt32,15} + Normed{UInt32,16} + Normed{UInt32,17} + Normed{UInt32,23} + Normed{UInt32,24} + Normed{UInt32,31} + Normed{UInt32,32} +``` +""" +function target(X::Type, Ss...; ex = :default) + Ts = symbol_to_inttype.(X, Ss) + (X{T,f} for T in Ts for f in target_f(X, T; ex = ex)) +end +target(X::Type; ex = :default) = target(X, :i8, :i16, :i32, :i64, :i128; ex = ex) + +""" + target_f(X::Type, T::Type; ex = :default) + +Return a tuple or range of the number of fractional bits `f` to be tested. + +# Arguments +The `X` specifies the target base type, i.e. `Fixed` or `Normed`, and the `T` +specifies the target raw type. + +## `ex` keyword +The `ex` specifies the exhaustivity of `f`s. +The following are examples of `target_f(Normed, T)`. The marker `x` means the +target and the marker `-` means not the target. + +### `:heavy` -- all supported `f`s +``` + | 3 2 1 | + f |2 1 0 9 8 7 6 5:4 3 2 1 0 9 8 7:6 5 4 3 2 1 0 9:8 7 6 5 4 3 2 1| +T == UInt8 | : : :x x x x x x x x| +T == UInt16 | : :x x x x x x x x:x x x x x x x x| +T == UInt32 |x x x x x x x x:x x x x x x x x:x x x x x x x x:x x x x x x x x| +``` +### `:default` -- same as `:heavy` for 8-/16-bit types, and same as `:light` otherwise +``` + | 3 2 1 | + f |2 1 0 9 8 7 6 5:4 3 2 1 0 9 8 7:6 5 4 3 2 1 0 9:8 7 6 5 4 3 2 1| +T == UInt8 | : : :x x x x x x x x| +T == UInt16 | : :x x x x x x x x:x x x x x x x x| +T == UInt32 |x x - - - - - -:x x - - - - - x:x x - - - x x x:x x - - - - - x| +``` + +### `:light` -- important `f`s for byte boundaries and floating point types +``` + | 3 2 1 | + f |2 1 0 9 8 7 6 5:4 3 2 1 0 9 8 7:6 5 4 3 2 1 0 9:8 7 6 5 4 3 2 1| +T == UInt8 | : : :x x - - - - - x| +T == UInt16 | : :x x - - - x x x:x x - - - - - x| +T == UInt32 |x x - - - - - -:x x - - - - - x:x x - - - x x x:x x - - - - - x| + | | + +--precision(Float32) +--precision(Float16) +``` + +### `:thin` -- maximum and half `f`s per type +``` + | 3 2 1 | + f |2 1 0 9 8 7 6 5:4 3 2 1 0 9 8 7:6 5 4 3 2 1 0 9:8 7 6 5 4 3 2 1| +T == UInt8 | : : :x - - - x - - -| +T == UInt16 | : :x - - - - - - -:x - - - - - - -| +T == UInt32 |x - - - - - - -:- - - - - - - -:x - - - - - - -:- - - - - - - -| +``` +""" +function target_f(X::Type, T::Type{<:Integer}; ex = :default) + f_min = X === Fixed ? 0 : 1 + f_max = bitwidth(T) - (T <: Signed) + ex === :heavy && return f_min:f_max + ex === :default && bitwidth(T) <= 16 && return f_min:f_max + ex === :thin && return ((f_max + 1) ÷ 2, f_max) + if ex === :light || ex === :default + itr = Iterators.filter(x -> x <= f_max, target_f_series(X, T)) + return (itr...,) + end + error() +end + +target_f_series(::Type{Fixed}, T::Type{<:Integer}) = + (0, 1, 7, 8, 9, + 10, 11, 15, 16, 17, + 23, 24, 31, 32, 33, + 52, 53, 63, 64, 65, + 112, 113, 127) + +target_f_series(::Type{Normed}, T::Type{<:Integer}) = + (1, 7, 8, 9, + 10, 11, 15, 16, 17, + 23, 24, 31, 32, 33, + 52, 53, 63, 64, 65, + 112, 113, 127, 128) + +# generator for cartesian product +function xypairs(::Type{X}) where X + xs = typemin(X):eps(X):typemax(X) + ((x, y) for x in xs, y in xs) +end + + +function test_floattype(TX::Type) + @testset "floattype($X)" for X in target(TX, :i8, :i16, :i32, :i64; ex = :heavy) + @test typemax(X) <= maxintfloat(floattype(X)) + end +end + +function test_convert_from_nan(TX::Type) + @testset "$X(nan)" for X in target(TX; ex = :thin) + @test_throws ArgumentError X(Inf) + @test_throws ArgumentError X(-Inf32) + @test_throws ArgumentError X(NaN) + end +end + +function test_rem_type(TX::Type) + @testset "% $X" for X in target(TX, :i8, :i16; ex = :thin) + xs = typemin(X):0.1:typemax(X) + @test all(x -> x % X === X(x), xs) + end +end + +function test_rem_nan(TX::Type) + # TODO: avoid undefined behavior + @testset "nan % $X" for X in target(TX, :i8, :i16, :i32, :i64; ex = :thin) + @test NaN % X === NaN32 % X === NaN16 % X === zero(X) + end +end + +function test_neg(TX::Type) + for X in target(TX, :i8; ex = :thin) + xs = typemin(X):eps(X):typemax(X) + fneg(x) = -float(x) + @test all(x -> wrapping_neg(wrapping_neg(x)) === x, xs) + end +end + +function test_abs(TX::Type) + for X in target(TX, :i8; ex = :thin) + xs = typemin(X):eps(X):typemax(X) + fabs(x) = abs(float(x)) + @test all(x -> wrapping_abs(x) === (x > 0 ? x : wrapping_neg(x)), xs) + end +end + +function test_add(TX::Type) + for X in target(TX, :i8; ex = :thin) + xys = xypairs(X) + fadd(x, y) = float(x) + float(y) + @test all(((x, y),) -> wrapping_sub(wrapping_add(x, y), y) === x, xys) + end +end + +function test_sub(TX::Type) + for X in target(TX, :i8; ex = :thin) + xys = xypairs(X) + fsub(x, y) = float(x) - float(y) + @test all(((x, y),) -> wrapping_add(wrapping_sub(x, y), y) === x, xys) + end +end + +function test_mul(TX::Type) + for X in target(TX, :i8; ex = :thin) + xys = xypairs(X) + fmul(x, y) = float(x) * float(y) # note that precision(Float32) < 32 + @test all(((x, y),) -> wrapping_mul(x, y) === fmul(x, y) % X, xys) + end +end + +function test_fdiv(TX::Type) + for X in target(TX, :i8; ex = :thin) + xys = xypairs(X) + fdiv(x, y) = oftype(float(x), big(x) / big(y)) + fdivz(x, y) = y === zero(y) ? float(y) : fdiv(x, y) + @test_broken all(((x, y),) -> !(typemin(X) <= fdiv(x, y) <= typemax(X)) || + checked_fdiv(x, y) === fdivz(x, y) % X, xys) + end +end + +function test_div(TX::Type) + for X in target(TX, :i8; ex = :thin) + T = rawtype(X) + xys = xypairs(X) + fdiv(x, y) = oftype(float(x), big(x) / big(y)) + @test all(xys) do (x, y) + z = fdiv(x, y) + t = !(typemin(T) <= trunc(z) <= typemax(T)) || trunc(z) == checked_div(x, y) + f = !(typemin(T) <= floor(z) <= typemax(T)) || floor(z) == checked_fld(x, y) + c = !(typemin(T) <= ceil(z) <= typemax(T)) || ceil(z) == checked_cld(x, y) + return t & f & c + end + end +end + +function test_div_3arg(TX::Type) + for X in target(TX; ex = :thin) + @test div(eps(X), typemax(X), RoundToZero) === div(eps(X), typemax(X)) + @test div(eps(X), typemax(X), RoundDown) === fld(eps(X), typemax(X)) + @test div(eps(X), typemax(X), RoundUp) === cld(eps(X), typemax(X)) + end +end + +function test_fld1_mod1(TX::Type) + for X in target(TX, :i8, :i16; ex = :thin) + T = rawtype(X) + eps2 = eps(X) + eps(X) + xs = reinterpret.(X, T.((17, 16, 15, 14))) + @test all(fld1.(xs, eps2) .=== T.((9, 8, 8, 7))) + @test_throws DivideError fld1(eps(X), zero(X)) + + @test all(mod1.(xs, eps2) .=== reinterpret.(X, T.((1, 2, 1, 2)))) + @test_throws DivideError mod1(eps(X), zero(X)) + + d, r = fldmod1(typemin(X), eps2) + @test d isa T && r isa X && ((d - 1.0) * eps2 + r) % X === typemin(X) # use `1.0` instead of `1` + d, r = fldmod1(typemax(X), eps2) + @test d isa T && r isa X && ((d - 1.0) * eps2 + r) % X === typemax(X) # use `1.0` instead of `1` + end +end + +function test_isapprox(TX::Type) + @testset "approx $X" for X in target(TX, :i8, :i16; ex = :light) + xs = typemin(X):eps(X):typemax(X)-eps(X) + @test all(x -> x ≈ x + eps(X), xs) + @test all(x -> x + eps(X) ≈ x, xs) + @test !any(x -> x - eps(X) ≈ x + eps(X), xs) + end + +end + +function test_clamp_nan(TX::Type) + @testset "clamp(nan, $X)" for X in target(TX; ex = :thin) + @test clamp( Inf, X) === clamp( Inf32, X) === typemax(X) + @test clamp(-Inf, X) === clamp(-Inf32, X) === typemin(X) + @test clamp( NaN, X) === clamp( NaN32, X) === zero(X) + end +end + +function test_isinteger(TX::Type) + @testset "isinteger(::$X)" for X in target(TX, :i8, :i16) + xs = typemin(X):eps(X):typemax(X) + @test all(x -> isinteger(x) == isinteger(float(x)), xs) + end +end + +function test_rand(TX::Type) + @testset "rand(::$X)" for X in target(TX; ex = :thin) + @test isa(rand(X), X) + a = rand(X, (3, 5)) + @test ndims(a) == 2 && eltype(a) === X + @test size(a) == (3, 5) + end +end diff --git a/test/fixed.jl b/test/fixed.jl index 1c958a01..255f56d0 100644 --- a/test/fixed.jl +++ b/test/fixed.jl @@ -1,54 +1,89 @@ -using FixedPointNumbers, Statistics, Test -using FixedPointNumbers: bitwidth +if !isdefined(Main, :target) + include("common.jl") +end + +function symbol_to_inttype(::Type{Fixed}, s::Symbol) + d = Dict(:i8 => Int8, :i16 => Int16, :i32 => Int32, :i64 => Int64, :i128 => Int128) + d[s] +end + +# issue #288 +# The following needs to be outside of `@testset` to reproduce the issue. +_to_fixed(::Val, x) = x % Q0f7 +_to_fixed(::Val{:Q0f7}, x) = x % Q0f7 +_to_fixed(::Val{:Q0f15}, x) = x % Q0f15 +buf = IOBuffer() +# in range +for vs in ((:Q0f7, :Q0f15), (:Q0f15, :Q0f7)) + for v in vs + show(buf, _to_fixed(Val(v), -1.0)) + print(buf, " ") + end +end +issue288_in = String(take!(buf)) +# out of range +for vs in ((:Q0f7, :Q0f15), (:Q0f15, :Q0f7)) + for v in vs + show(buf, _to_fixed(Val(v), 1.0)) + print(buf, " ") + end +end +issue288_out = String(take!(buf)) -function test_op(fun::F, ::Type{T}, fx, fy, fxf, fyf, tol) where {F,T} - # Make sure that the result is representable - (typemin(T) <= fun(fxf, fyf) <= typemax(T)) || return nothing - @assert abs(fun(fx, fy) - convert(T, fun(fxf, fyf))) <= tol - @assert abs(convert(Float64, fun(fx, fy)) - fun(fxf, fyf)) <= tol +@testset "issue288" begin + @test issue288_in == "-1.0Q0f7 -1.0Q0f15 -1.0Q0f15 -1.0Q0f7 " + @test issue288_out == "-1.0Q0f7 -1.0Q0f15 -1.0Q0f15 -1.0Q0f7 " end -function test_fixed(::Type{T}, f) where {T} - values = [-10:0.01:10; -180:.01:-160; 160:.01:180] - tol = 2.0^-f +function test_op(fun::Fun, fx::F, fy::F, fxf, fyf, tol) where {Fun, F} + # Make sure that the result is representable + zf = fun(fxf, fyf) + typemin(F) <= zf <= typemax(F) || return nothing + z = fun(fx, fy) + @assert abs(z - convert(F, zf)) <= tol + @assert abs(convert(Float64, z) - zf) <= tol +end + +function test_fixed(::Type{F}) where {F} + tol = Float64(eps(F)) + v = [-10:0.01:10; -180:.01:-160; 160:.01:180] + # Ignore values outside the representable range + values = filter(x -> typemin(F) < x <= typemax(F), v) for x in values - # Ignore values outside the representable range - # typemin <, otherwise for -(-0.5) > typemax - if !(typemin(T) < x <= typemax(T)) - continue - end - fx = convert(T,x) - @test convert(T,convert(Float64, fx)) == fx - @test convert(T,convert(Float64, -fx)) == -fx - @test convert(Float64, -fx) == -convert(Float64, fx) - + fx = convert(F, x) fxf = convert(Float64, fx) - rx = convert(Rational{BigInt},fx) - @assert isequal(fx,rx) == isequal(hash(fx),hash(rx)) + @test convert(F, convert(Float64, fx)) === fx + fx != typemin(F) && @test convert(F, convert(Float64, -fx)) === -fx + fx != typemin(F) && @test convert(Float64, -fx) == -convert(Float64, fx) - for y in values - if !(typemin(T) < y <= typemax(T)) - continue - end + rx = convert(Rational{BigInt}, fx) + @assert isequal(fx, rx) == isequal(hash(fx), hash(rx)) - fy = convert(T,y) + for y in values + fy = convert(F, y) fyf = convert(Float64, fy) @assert fx==fy || x!=y - @assert fx=y + @assert fx=y @assert fx<=fy || x>y - test_op(+, T, fx, fy, fxf, fyf, tol) - test_op(-, T, fx, fy, fxf, fyf, tol) - test_op(*, T, fx, fy, fxf, fyf, tol) - fy != 0 && test_op(/, T, fx, fy, fxf, fyf, tol) + test_op(+, fx, fy, fxf, fyf, tol) + test_op(-, fx, fy, fxf, fyf, tol) + test_op(*, fx, fy, fxf, fyf, tol) + fy != 0 && test_op(/, fx, fy, fxf, fyf, tol) - @assert isequal(fx,fy) == isequal(hash(fx),hash(fy)) + @assert isequal(fx, fy) === isequal(hash(fx), hash(fy)) end end end +@testset "test_fixed" begin + for F in target(Fixed, :i8, :i16, :i32; ex = :thin) + test_fixed(F) + end +end + @testset "domain of f" begin # TODO: change the upper limit @test_logs (:warn, r"`f=8` with raw type `T=Int8` will be removed") zero(Fixed{Int8,8}) @@ -84,26 +119,57 @@ end @test FixedPointNumbers.fracmask(0Q0f7) === signed(0x7F) end +@testset "limits and identities" begin + @testset "$F" for F in target(Fixed) + T, f = rawtype(F), nbitsfrac(F) + @test zero(F) == 0 + f < bitwidth(T) - 1 && @test one(F) == 1 + f < bitwidth(T) - 1 && @test one(F) * oneunit(F) == oneunit(F) + @test typemin(F) == typemin(T) >> f + if T === Int128 + @test typemax(F) * big"2.0"^f == typemax(T) # force promotion to BigFloat due to lack of PR #207 + else + @test typemax(F) == typemax(T)//big"2"^f + end + @test floatmin(F) === eps(F) == 2.0^-f # issue #79 + @test floatmax(F) === typemax(F) + @test eps(zero(F)) === eps(typemax(F)) + @test sizeof(F) == sizeof(T) + end +end + @testset "inexactness" begin # TODO: change back to InexactError when it allows message strings @test_throws ArgumentError Q0f7(-2) @test_throws ArgumentError one(Q0f15) @test_throws ArgumentError oneunit(Q0f31) @test_throws ArgumentError one(Fixed{Int8,8}) # TODO: remove this at end of its support + + @test_throws ArgumentError convert(Q0f7, 0.999) + @test_throws ArgumentError convert(Q0f7, 1.0) + @test_throws ArgumentError convert(Q0f7, 1) + @test_throws ArgumentError convert(Q0f7, 2) + + ret = @test_throws ArgumentError Q0f7(127) + msg = ret.value.msg + @test occursin("Q0f7 is an 8-bit type representing 256 values from -1.0 to 0.992;", msg) + ret = @test_throws ArgumentError convert(Fixed{Int128,100}, 10.0^9) + msg = ret.value.msg + @test occursin("Fixed{Int128,$(SP)100} is a 128-bit type representing 2^128 values", msg) +end + +@testset "disambiguation constructors" begin + @test_throws ArgumentError Fixed{Int32,16}('a') + @test_throws InexactError Fixed{Int32,16}(complex(1.0, 1.0)) + @test Fixed{Int32,16}(complex(1.0, 0.0)) == 1 + @test Fixed{Int32,16}(Base.TwicePrecision(1.0, 0.0)) == 1 end @testset "conversion" begin @test isapprox(convert(Fixed{Int8,7}, 0.8), 0.797, atol=0.001) @test isapprox(convert(Fixed{Int8,7}, 0.9), 0.898, atol=0.001) - @test_throws ArgumentError convert(Fixed{Int8, 7}, 0.999) - @test_throws ArgumentError convert(Fixed{Int8, 7}, 1.0) - @test_throws ArgumentError convert(Fixed{Int8, 7}, 1) - @test_throws ArgumentError convert(Fixed{Int8, 7}, 2) - @test_throws ArgumentError convert(Fixed{Int8, 7}, 128) - @test_throws ArgumentError convert(Fixed{Int8, 7}, 1.0) @test convert(Q0f7, -128.5/128) == -1 - @test convert(Q0f7, -0.75f0) == -0.75 @test convert(Q0f7, Float16(-0.75)) == -0.75 @test convert(Q0f7, BigFloat(-0.75)) == -0.75 @@ -121,24 +187,240 @@ end @test convert(Q0f63, tp) === reinterpret(Q0f63, typemax(Int64)) end -@testset "test_fixed" begin - for (TI, f) in [(Int8, 7), (Int16, 8), (Int16, 10), (Int32, 16)] - T = Fixed{TI,f} - # println(" Testing $T") - test_fixed(T, f) +@testset "bool conversions" begin + @test convert(Bool, 0.0Q1f6) === false + @test convert(Bool, 1.0Q1f6) === true + @test_throws InexactError convert(Bool, 0.5Q1f6) + @test_throws InexactError convert(Bool, -1Q1f6) + @test_broken convert(Bool, Fixed{Int8,8}(0.2)) # TODO: remove this +end + +@testset "integer conversions" begin + @test convert(Int, Q1f6(1)) === 1 + @test convert(Integer, Q1f6(1)) === Int8(1) + @test convert(UInt, 1Q1f6) === UInt(1) + @test_throws InexactError convert(Integer, 0.5Q1f6) + @test_throws InexactError convert(Int8, 256Q9f6) +end + +@testset "rational conversions" begin + @test convert(Rational, -0.75Q1f6) === Rational{Int8}(-3//4) + @test convert(Rational, -0.75Q0f7) === Rational{Int16}(-3//4) + @test convert(Rational{Int}, -0.75Q0f7) === Rational{Int}(-3//4) + + @test rationalize(-0.75Q3f4) === Rational{Int}(-3//4) + @test rationalize(Int16, 0.81Q3f4) === Rational{Int16}(13//16) + @test rationalize(-0.81Q3f4, tol=0.02) === Rational{Int}(-13//16) + @test rationalize(Int8, -0.81Q3f4, tol=0.07) === Rational{Int8}(-3//4) +end + +@testset "BigFloat conversions" begin + @test convert(BigFloat, -0.75Q0f7)::BigFloat == big"-0.75" + + @test big(Q7f0) === BigFloat # !== BigInt + @test big(0.75Q3f4)::BigFloat == big"0.75" +end + +@testset "float/floattype" begin + @test float(0.75Q3f4) === 0.75f0 + @test float(0.75Q19f12) === 0.75 + @test float(0.75Q7f24) === 0.75 + @test float(0.75Q10f53)::BigFloat == big"0.75" + + test_floattype(Fixed) +end + +@testset "conversions from float" begin + test_convert_from_nan(Fixed) +end + +@testset "conversions to float" begin + for T in (Float16, Float32, Float64) + @test isa(convert(T, Q0f7(0.3)), T) + end + + for Tf in (Float16, Float32, Float64) + @testset "$Tf(::$F)" for F in target(Fixed, :i8, :i16) + T, exp2mf = rawtype(F), big(2.0^-nbitsfrac(F)) + float_err = zero(Tf) + for i = typemin(T):typemax(T) + f_expected = Tf(i * exp2mf) + f_actual = Tf(reinterpret(F, i)) + float_err += abs(f_actual - f_expected) + end + @test float_err == 0 + end + @testset "$Tf(::$F)" for F in target(Fixed, :i32, :i64, :i128) + T, exp2mf = rawtype(F), big(2.0^-nbitsfrac(F)) + error_count = 0 + for i in vcat(typemin(T):(typemin(T)+0xFF), + -T(0xFF):T(0xFF), + (typemax(T)-0xFF):typemax(T)) + f_expected = Tf(i * exp2mf) + isinf(f_expected) && break # for Float16() and Float32() + f_actual = Tf(reinterpret(F, i)) + f_actual == f_expected && continue + error_count += 1 + end + @test error_count == 0 + end end end +@testset "fractional fixed-point numbers" begin + # test all-fractional fixed-point numbers (issue #104) + for F in (Q0f7, Q0f15, Q0f31, Q0f63) + tmax = typemax(F) + tol = (tmax + BigFloat(1.0)) / bitwidth(F) + r = range(-1, stop=BigFloat(tmax)-tol, length=50) + @test all(x -> abs(F(x) - x) <= tol, r) + end +end + +@testset "type modulus" begin + test_rem_type(Fixed) + test_rem_nan(Fixed) + + @test Q0f7(0.2) % Q0f7 === Q0f7(0.2) + @test Q1f14(1.2) % Q0f15 === Q0f15(-0.8) + @test Q1f14(1.2) % Q0f7 === Q0f7(-0.8) + + @test ( 1.5 % Q0f7).i == round(Int, 1.5*128) % Int8 + @test (-0.3 % Q0f7).i == round(Int, -0.3*128) % Int8 + + @test ( 65.2 % Q6f9).i == round(Int, 65.2*512) % Int16 + @test (-67.2 % Q6f9).i == round(Int, -67.2*512) % Int16 + + @test -1 % Q0f7 === Q0f7(-1) + @test -2 % Q0f7 === Q0f7(0) +end + +@testset "neg" begin + for F in target(Fixed; ex = :thin) + @test wrapping_neg(typemin(F)) === typemin(F) + + @test wrapping_neg(typemax(F)) === typemin(F) + eps(F) + + @test wrapping_neg(eps(F)) === zero(F) - eps(F) + end + test_neg(Fixed) +end + +@testset "abs" begin + for F in target(Fixed; ex = :thin) + @test wrapping_abs(typemax(F)) === typemax(F) + + @test wrapping_abs(typemin(F)) === typemin(F) + end + test_abs(Fixed) +end + +@testset "add" begin + for F in target(Fixed; ex = :thin) + @test wrapping_add(typemin(F), typemin(F)) === zero(F) + + @test wrapping_add(typemax(F), eps(F)) === wrapping_add(eps(F), typemax(F)) === typemin(F) + + @test wrapping_add(zero(F), eps(F)) === wrapping_add(eps(F), zero(F)) === eps(F) + end + test_add(Fixed) +end + +@testset "sub" begin + for F in target(Fixed; ex = :thin) + @test wrapping_sub(typemin(F), typemin(F)) === zero(F) + + @test wrapping_sub(typemin(F), eps(F)) === typemax(F) + + @test wrapping_sub(eps(F), zero(F)) === eps(F) + end + test_sub(Fixed) +end + +@testset "mul" begin + for F in target(Fixed; ex = :thin) + @test wrapping_mul(typemax(F), zero(F)) === zero(F) + + @test wrapping_mul(F(-1), typemax(F)) === -typemax(F) + + @test wrapping_mul(typemin(F), typemax(F)) === big(typemin(F)) * big(typemax(F)) % F + + @test wrapping_mul(typemin(F), typemin(F)) === big(typemin(F))^2 % F + end + test_mul(Fixed) + + FixedPointNumbers.mul_with_rounding(1.5Q6f1, 0.5Q6f1, RoundNearest) === 1.0Q6f1 + FixedPointNumbers.mul_with_rounding(1.5Q6f1, -0.5Q6f1, RoundNearest) === -1.0Q6f1 + FixedPointNumbers.mul_with_rounding(1.5Q6f1, 0.5Q6f1, RoundNearestTiesUp) === 1.0Q6f1 + FixedPointNumbers.mul_with_rounding(1.5Q6f1, -0.5Q6f1, RoundNearestTiesUp) === -0.5Q6f1 + FixedPointNumbers.mul_with_rounding(1.5Q6f1, 0.5Q6f1, RoundDown) === 0.5Q6f1 + FixedPointNumbers.mul_with_rounding(1.5Q6f1, -0.5Q6f1, RoundDown) === -1.0Q6f1 +end + +@testset "fdiv" begin + for F in target(Fixed; ex = :thin) + @test checked_fdiv(typemax(F), -typemax(F)) === F(-1) + + @test checked_fdiv(zero(F), typemin(F)) === zero(F) + + # OverflowError on v0.9 (#222) + @test_broken (try; checked_fdiv(typemin(F), F(-1)); catch e; e; end) isa Exception + + @test_throws Exception checked_fdiv(zero(F), zero(F)) # DivideError on v0.9 (#222) + + @test_throws Exception checked_fdiv(-eps(F), zero(F)) # DivideError on v0.9 (#222) + end + test_fdiv(Fixed) +end + +@testset "div/cld/fld" begin + for F in target(Fixed; ex = :thin) + fm, fn, fz, fe = typemax(F), typemin(F), zero(F), eps(F) + T = rawtype(F) + @test checked_div(fm, fm) === checked_fld(fm, fm) === checked_cld(fm, fm) === one(T) + + @test checked_div(fz, fe) === checked_fld(fz, fe) === checked_cld(fz, fe) === zero(T) + + @test checked_div(fm, fe) === checked_fld(fm, fe) === checked_cld(fm, fe) === typemax(T) + + @test_throws DivideError checked_div(fz, fz) + @test_throws DivideError checked_fld(fz, fz) + @test_throws DivideError checked_cld(fz, fz) + + @test_throws DivideError checked_div(fe, fz) + @test_throws DivideError checked_fld(fe, fz) + @test_throws DivideError checked_cld(fe, fz) + + @test_throws OverflowError checked_div(fn, -fe) + @test_throws OverflowError checked_fld(fn, -fe) + @test_throws OverflowError checked_cld(fn, -fe) + + @test checked_div(fe, fm) === zero(T) + @test checked_fld(fe, fm) === zero(T) + @test checked_cld(fe, fm) === one(T) + + @test checked_div(fe, fn) === zero(T) + @test checked_fld(fe, fn) === -one(T) + @test checked_cld(fe, fn) === zero(T) + end + test_div(Fixed) + test_div_3arg(Fixed) +end + +@testset "fld1/mod1" begin + test_fld1_mod1(Fixed) +end + @testset "rounding" begin - for T in (Int8, Int16, Int32, Int64) + for sym in (:i8, :i16, :i32, :i64) + T = symbol_to_inttype(Fixed, sym) rs = vcat([ oneunit(T) << b - oneunit(T) for b = 0:bitwidth(T)-1], [ oneunit(T) << b for b = 1:bitwidth(T)-2], [ oneunit(T) << b + oneunit(T) for b = 2:bitwidth(T)-2], [-oneunit(T) << b - oneunit(T) for b = 2:bitwidth(T)-2], [-oneunit(T) << b for b = 1:bitwidth(T)-1], [-oneunit(T) << b + oneunit(T) for b = 1:bitwidth(T)-1]) - @testset "rounding Fixed{$T,$f}" for f = 0:bitwidth(T)-1 - F = Fixed{T,f} + @testset "rounding $F" for F in target(Fixed, sym) xs = (reinterpret(F, r) for r in rs) @test all(x -> trunc(x) == trunc(float(x)), xs) @test all(x -> floor(float(x)) < typemin(F) || floor(x) == floor(float(x)), xs) @@ -179,28 +461,77 @@ end @test_throws InexactError floor(UInt, -eps(Q0f7)) end -@testset "modulus" begin - T = Fixed{Int8,7} - for i = -1.0:0.1:typemax(T) - @test i % T === T(i) - end - @test ( 1.5 % T).i == round(Int, 1.5*128) % Int8 - @test (-0.3 % T).i == round(Int, -0.3*128) % Int8 +@testset "approx" begin + test_isapprox(Fixed) - T = Fixed{Int16,9} - for i = -64.0:0.1:typemax(T) - @test i % T === T(i) - end - @test ( 65.2 % T).i == round(Int, 65.2*512) % Int16 - @test (-67.2 % T).i == round(Int, -67.2*512) % Int16 + # PR #216 required + @test_broken isapprox(-0.5Q0f7, -1Q0f7, rtol=0.5, atol=0) # issue 209 + @test_broken isapprox(typemin(Q0f7), typemax(Q0f7), rtol=2.0) + @test !isapprox(zero(Q0f7), typemax(Q0f7), rtol=0.9) + @test isapprox(zero(Q0f7), eps(Q0f7), rtol=1e-6) # atol = eps(Q0f7) + @test !isapprox(eps(Q0f7), zero(Q0f7), rtol=1e-6, atol=1e-6) + @test_broken !isapprox(1.0Q6f1, 1.5Q6f1, rtol=0.3, atol=0) # 1.5 * 0.3 < eps(Q6f1) + + @test isapprox(eps(Q8f7), eps(Q0f7), rtol=1e-6) +end + +@testset "clamp" begin + @test clamp(0.5Q0f7, -0.8Q0f7, 0.8Q0f7) === 0.5Q0f7 + @test clamp(0.5Q0f7, 0.75Q0f7, 0.8Q0f7) === 0.75Q0f7 + @test clamp(0.5Q0f7, -0.8Q0f7, 0.25Q0f7) === 0.25Q0f7 + @test clamp(0.5, -0.8Q0f7, 0.8Q0f7) === 0.5 + @test clamp(0.5f0, 0.75Q0f7, 0.8Q0f7) === 0.75f0 + @test clamp(0.5Q0f15, -0.8Q0f7, 0.25Q0f7) === 0.25Q0f15 + @test clamp(0.5Q0f7, -Inf, Inf) === 0.5 + @test clamp(0.5, Q0f7) === 0.5Q0f7 + @test clamp(-1.5f0, Q0f7) === -1.0Q0f7 + @test clamp(1.5Q1f6, Q0f7) === 0.992Q0f7 + + test_clamp_nan(Fixed) +end + +@testset "sign-related functions" begin + @test_throws Exception signed(Q0f7) + @test_throws Exception signed(0.5Q0f7) + @test_throws Exception unsigned(Q0f7) + @test_throws Exception unsigned(0.5Q0f7) + @test copysign(0.5Q0f7, 0x1) === 0.5Q0f7 + @test copysign(0.5Q0f7, -1) === -0.5Q0f7 + @test flipsign(0.5Q0f7, 0x1) === 0.5Q0f7 + @test flipsign(0.5Q0f7, -1) === -0.5Q0f7 + @test_throws ArgumentError sign(0Q0f7) + @test sign(0Q1f6) === 0Q1f6 + @test sign(0.5Q1f6) === 1Q1f6 + @test sign(-0.5Q1f6) === -1Q1f6 + @test signbit(0.5Q0f7) === false + @test signbit(-0.5Q0f7) === true +end + +@testset "bitwise" begin + @test bswap(Q0f7(0.5)) === Q0f7(0.5) + @test bswap(Q0f15(0.5)) === reinterpret(Q0f15, signed(0x0040)) end -@testset "testapprox" begin - @testset "approx $T" for T in [Fixed{Int8,7}, Fixed{Int16,8}, Fixed{Int16,10}] - xs = typemin(T):eps(T):typemax(T)-eps(T) - @test all(x -> x ≈ x + eps(T), xs) - @test all(x -> x + eps(T) ≈ x, xs) - @test !any(x -> x - eps(T) ≈ x + eps(T), xs) +@testset "predicates" begin + @test isfinite(1Q7f8) + @test !isnan(1Q7f8) + @test !isinf(1Q7f8) + + @testset "isinteger" begin + test_isinteger(Fixed) + @testset "isinteger(::$F)" for F in target(Fixed, :i32, :i64, :i128) + fzero, fmax, fmin = zero(F), typemax(F), typemin(F) + if nbitsfrac(F) == 0 + @test isinteger(fzero) & isinteger(fmax) & isinteger(fmin) + else + @test isinteger(fzero) & !isinteger(fmax) & isinteger(fmin) + end + end + @testset "isinteger(::Fixed{Int8,8})" begin # TODO: remove this testset + @test !isinteger(Fixed{Int8,8}(-0.5)) + @test isinteger(Fixed{Int8,8}(0.0)) + @test !isinteger(Fixed{Int8,8}(127/256)) + end end end @@ -228,7 +559,7 @@ end @test length(r) == 256 QInt1 = Fixed{Int,1} @test length(QInt1(0):eps(QInt1):typemax(QInt1)-eps(QInt1)) == typemax(Int) - @test Base.unsafe_length(typemin(QInt1):eps(QInt1):typemax(QInt1)-eps(QInt1)) == -1 + @test_throws OverflowError length(typemin(QInt1):eps(QInt1):typemax(QInt1)-eps(QInt1)) @test_throws OverflowError length(QInt1(-1):eps(QInt1):typemax(QInt1)-eps(QInt1)) end @@ -256,184 +587,10 @@ end @test varm(a, m) === varm(af, m) end -@testset "bool conversions" begin - @test convert(Bool, 0.0Q1f6) === false - @test convert(Bool, 1.0Q1f6) === true - @test_throws InexactError convert(Bool, 0.5Q1f6) - @test_throws InexactError convert(Bool, -1Q1f6) - @test_broken convert(Bool, Fixed{Int8,8}(0.2)) # TODO: remove this -end - -@testset "integer conversions" begin - @test convert(Int, Q1f6(1)) === 1 - @test convert(Integer, Q1f6(1)) === Int8(1) - @test convert(UInt, 1Q1f6) === UInt(1) - @test_throws InexactError convert(Integer, 0.5Q1f6) - @test_throws InexactError convert(Int8, 256Q9f6) -end - -@testset "rational conversions" begin - @test convert(Rational, -0.75Q1f6) === Rational{Int8}(-3//4) - @test convert(Rational, -0.75Q0f7) === Rational{Int16}(-3//4) - @test convert(Rational{Int}, -0.75Q0f7) === Rational{Int}(-3//4) - - @test rationalize(-0.75Q3f4) === Rational{Int}(-3//4) - @test rationalize(Int16, 0.81Q3f4) === Rational{Int16}(13//16) - @test rationalize(-0.81Q3f4, tol=0.02) === Rational{Int}(-13//16) - @test rationalize(Int8, -0.81Q3f4, tol=0.07) === Rational{Int8}(-3//4) -end - -@testset "BigFloat conversions" begin - @test convert(BigFloat, -0.75Q0f7)::BigFloat == big"-0.75" - - @test big(Q7f0) === BigFloat # !== BigInt - @test big(0.75Q3f4)::BigFloat == big"0.75" -end - -@testset "Floating-point conversions" begin - @test isa(float(one(Fixed{Int8,6})), Float32) - @test isa(float(one(Fixed{Int32,18})), Float64) - @test isa(float(one(Fixed{Int32,25})), Float64) -end - -@testset "conversions to float" begin - for T in (Float16, Float32, Float64) - @test isa(convert(T, Q0f7(0.3)), T) - end - - for Tf in (Float16, Float32, Float64) - @testset "$Tf(::Fixed{$T})" for T in (Int8, Int16) - @testset "$Tf(::Fixed{$T,$f})" for f = 0:bitwidth(T)-1 - F = Fixed{T,f} - float_err = 0.0 - for i = typemin(T):typemax(T) - f_expected = Tf(i * BigFloat(2)^-f) - f_actual = Tf(reinterpret(F, i)) - float_err += abs(f_actual - f_expected) - end - @test float_err == 0.0 - end - end - @testset "$Tf(::Fixed{$T})" for T in (Int32, Int64, Int128) - @testset "$Tf(::Fixed{$T,$f})" for f = 0:bitwidth(T)-1 - F = Fixed{T,f} - error_count = 0 - for i in vcat(typemin(T):(typemin(T)+0xFF), - -T(0xFF):T(0xFF), - (typemax(T)-0xFF):typemax(T)) - f_expected = Tf(i * BigFloat(2)^-f) - isinf(f_expected) && break # for Float16() and Float32() - f_actual = Tf(reinterpret(F, i)) - f_actual == f_expected && continue - error_count += 1 - end - @test error_count == 0 - end - end - end -end - -@testset "predicates" begin - @test isfinite(1Q7f8) - @test !isnan(1Q7f8) - @test !isinf(1Q7f8) - - @testset "isinteger" begin - for T in (Int8, Int16) - @testset "isinteger(::Fixed{$T,$f})" for f = 0:bitwidth(T)-1 - F = Fixed{T,f} - xs = typemin(F):eps(F):typemax(F) - @test all(x -> isinteger(x) == isinteger(float(x)), xs) - end - end - for T in (Int32, Int64) - @testset "isinteger(::Fixed{$T,$f})" for f = 0:bitwidth(T)-1 - F = Fixed{T,f} - fzero, fmax, fmin = zero(F), typemax(F), typemin(F) - if f == 0 - @test isinteger(fzero) & isinteger(fmax) & isinteger(fmin) - else - @test isinteger(fzero) & !isinteger(fmax) & isinteger(fmin) - end - end - end - @testset "isinteger(::Fixed{Int8,8})" begin # TODO: remove this testset - @test !isinteger(Fixed{Int8,8}(-0.5)) - @test isinteger(Fixed{Int8,8}(0.0)) - @test !isinteger(Fixed{Int8,8}(127/256)) - end - end -end - @testset "rand" begin - for F in (Fixed{Int8,7}, Fixed{Int16,8}, Fixed{Int16,10}, Fixed{Int32,16}) - @test isa(rand(F), F) - a = rand(F, (3, 5)) - @test ndims(a) == 2 && eltype(a) == F - @test size(a) == (3,5) - end -end - -@testset "floatmin" begin - # issue #79 - @test floatmin(Q11f4) == Q11f4(0.06) -end - -@testset "Disambiguation constructors" begin - @test_throws ArgumentError Fixed{Int32,16}('a') - @test_throws InexactError Fixed{Int32,16}(complex(1.0, 1.0)) - @test Fixed{Int32,16}(complex(1.0, 0.0)) == 1 - @test Fixed{Int32,16}(Base.TwicePrecision(1.0, 0.0)) == 1 -end - -@testset "fractional fixed-point numbers" begin - # test all-fractional fixed-point numbers (issue #104) - for (T, f) in ((Int8, 7), - (Int16, 15), - (Int32, 31), - (Int64, 63)) - tmax = typemax(Fixed{T, f}) - @test tmax == BigInt(typemax(T)) / BigInt(2)^f - tol = (tmax + BigFloat(1.0)) / bitwidth(T) - for x in range(-1, stop=BigFloat(tmax)-tol, length=50) - @test abs(Fixed{T, f}(x) - x) <= tol - end - end -end - -@testset "low-level arithmetic" begin - @test bswap(Q0f7(0.5)) === Q0f7(0.5) - @test bswap(Q0f15(0.5)) === reinterpret(Q0f15, signed(0x0040)) -end - -@testset "clamp" begin - @test clamp(0.5Q0f7, -0.8Q0f7, 0.8Q0f7) === 0.5Q0f7 - @test clamp(0.5Q0f7, 0.75Q0f7, 0.8Q0f7) === 0.75Q0f7 - @test clamp(0.5Q0f7, -0.8Q0f7, 0.25Q0f7) === 0.25Q0f7 - @test clamp(0.5, -0.8Q0f7, 0.8Q0f7) === 0.5 - @test clamp(0.5f0, 0.75Q0f7, 0.8Q0f7) === 0.75f0 - @test clamp(0.5Q0f15, -0.8Q0f7, 0.25Q0f7) === 0.25Q0f15 - @test clamp(0.5Q0f7, -Inf, Inf) === 0.5 - @test clamp(0.5, Q0f7) === 0.5Q0f7 - @test clamp(-1.5f0, Q0f7) === -1.0Q0f7 - @test clamp(1.5Q1f6, Q0f7) === 0.992Q0f7 -end - -@testset "sign-related functions" begin - @test_throws Exception signed(Q0f7) - @test_throws Exception signed(0.5Q0f7) - @test_throws Exception unsigned(Q0f7) - @test_throws Exception unsigned(0.5Q0f7) - @test copysign(0.5Q0f7, 0x1) === 0.5Q0f7 - @test copysign(0.5Q0f7, -1) === -0.5Q0f7 - @test flipsign(0.5Q0f7, 0x1) === 0.5Q0f7 - @test flipsign(0.5Q0f7, -1) === -0.5Q0f7 - @test_throws ArgumentError sign(0Q0f7) - @test sign(0Q1f6) === 0Q1f6 - @test sign(0.5Q1f6) === 1Q1f6 - @test sign(-0.5Q1f6) === -1Q1f6 - @test signbit(0.5Q0f7) === false - @test signbit(-0.5Q0f7) === true + test_rand(Fixed) + @test !(rand(Q0f15) == rand(Q0f15) == rand(Q0f15)) # If this fails, we should suspect a bug. + @test rand(StableRNG(1234), Q0f7) === 0.531Q0f7 end @testset "Promotion within Fixed" begin @@ -453,16 +610,35 @@ end @test Fixed{Int16,3}(-1) == Fixed{Int8,5}(-1) @test Fixed{Int16,3}(0.25) == Fixed{Int8,5}(0.25) - @test promote_type(Q0f7,Float32,Int) == Float32 - @test promote_type(Q0f7,Int,Float32) == Float32 - @test promote_type(Int,Q0f7,Float32) == Float32 - @test promote_type(Int,Float32,Q0f7) == Float32 - @test promote_type(Float32,Int,Q0f7) == Float32 - @test promote_type(Float32,Q0f7,Int) == Float32 - @test promote_type(Q0f7,Q1f6,Q2f5,Q3f4,Q4f3,Q5f2) == Fixed{Int128,7} + @test @inferred(promote_type(Q0f7, Float64)) === Float64 + @test @inferred(promote_type(Float32, Q7f24)) === Float32 # Float64 on v0.9 (#207) + + @test @inferred(promote_type(Q0f7, Int8)) === Q0f7 # Float32 on v0.9 (#207) + @test @inferred(promote_type(Int128, Q7f24)) === Q7f24 # Float64 on v0.9 (#207) + + @test @inferred(promote_type(Q0f15, Rational{UInt8})) === Rational{UInt8} + + @test @inferred(promote_type(Q0f7, Float32, Int)) === Float32 + @test @inferred(promote_type(Q0f7, Int, Float32)) === Float32 + @test @inferred(promote_type(Int, Q0f7, Float32)) === Float32 + @test @inferred(promote_type(Int, Float32, Q0f7)) === Float32 + @test @inferred(promote_type(Float32, Int, Q0f7)) === Float32 + @test @inferred(promote_type(Float32, Q0f7, Int)) === Float32 + + if promote_type(Int, Float32, Complex{Int}, typeof(pi)) === ComplexF64 + # right-to-left + @test @inferred(promote_type(Q0f7, Q1f6, Q2f5, Q3f4, Q4f3, Q5f2)) == Fixed{Int128,7} + else + # left-to-right + @test @inferred(promote_type(Q5f2, Q4f3, Q3f4, Q2f5, Q1f6, Q0f7)) == Fixed{Int128,7} + end + + @test @inferred(promote_type(Q0f7, N0f32)) === FixedPoint # Float64 on v0.9 (#207) end @testset "show" begin + @test (@test_deprecated FixedPointNumbers.typechar(Q0f7)) === 'Q' + iob = IOBuffer() q0f7 = reinterpret(Q0f7, signed(0xaa)) show(iob, q0f7) @@ -470,6 +646,10 @@ end @test str == "-0.672Q0f7" @test eval(Meta.parse(str)) === q0f7 + print(iob, q0f7) + str = String(take!(iob)) + @test str == "-0.672Q0f7" # w/ type suffix (cf. PR #243) + q15f16 = reinterpret(Q15f16, signed(0xaaaaaaaa)) show(iob, q15f16) str = String(take!(iob)) @@ -492,11 +672,11 @@ end @test String(take!(iob)) == "-21845.33334Q15f16" show(iob, Fixed{Int128,64}(-1.2345e6)) - @test_broken String(take!(iob)) == "Fixed{Int128,64}(-1.2345e6)" # "Q63f64" is not defined + @test String(take!(iob)) == "Fixed{Int128,$(SP)64}(-1.2345e6)" # TODO: remove this test show(iob, reinterpret(Fixed{Int8,8}, signed(0xaa))) - @test_broken String(take!(iob)) == "Fixed{Int8,8}(-0.336)" # "Q-1f8" is invalid + @test String(take!(iob)) == "Fixed{Int8,$(SP)8}(-0.336)" end @testset "summary" begin diff --git a/test/normed.jl b/test/normed.jl index 4ed423cd..975d34d9 100644 --- a/test/normed.jl +++ b/test/normed.jl @@ -1,5 +1,39 @@ -using FixedPointNumbers, Statistics, Test -using FixedPointNumbers: bitwidth +if !isdefined(Main, :target) + include("common.jl") +end + +function symbol_to_inttype(::Type{Normed}, s::Symbol) + d = Dict(:i8 => UInt8, :i16 => UInt16, :i32 => UInt32, :i64 => UInt64, :i128 => UInt128) + d[s] +end + +# issue #288 +# The following needs to be outside of `@testset` to reproduce the issue. +_to_normed(::Val, x) = x % N0f8 +_to_normed(::Val{:N0f8}, x) = x % N0f8 +_to_normed(::Val{:N0f16}, x) = x % N0f16 +buf = IOBuffer() +# in range +for vs in ((:N0f8, :N0f16), (:N0f16, :N0f8)) + for v in vs + show(buf, _to_normed(Val(v), 1.0)) + print(buf, " ") + end +end +issue288_in = String(take!(buf)) +# out of range +for vs in ((:N0f8, :N0f16), (:N0f16, :N0f8)) + for v in vs + show(buf, _to_normed(Val(v), -1.0)) + print(buf, " ") + end +end +issue288_out = String(take!(buf)) + +@testset "issue288" begin + @test issue288_in == "1.0N0f8 1.0N0f16 1.0N0f16 1.0N0f8 " + @test issue288_out == "0.004N0f8 2.0e-5N0f16 2.0e-5N0f16 0.004N0f8 " +end @testset "domain of f" begin @test_throws DomainError zero(Normed{UInt8,-1}) @@ -40,36 +74,24 @@ end @test isa(v, Vector{N4f12}) end -UF2 = (Normed{UInt32,16}, Normed{UInt64,3}, Normed{UInt64,51}, Normed{UInt128,7}, Normed{UInt128,51}) - @testset "limits and identities" begin - for T in (FixedPointNumbers.UF..., UF2...) - @test zero(T) == 0 - @test one(T) == 1 - @test one(T) * one(T) == one(T) - @test typemin(T) == 0 - @test floatmin(T) == eps(T) - @test eps(zero(T)) == eps(typemax(T)) - @test sizeof(T) == sizeof(FixedPointNumbers.rawtype(T)) + @testset "$N" for N in target(Normed) + T, f = rawtype(N), nbitsfrac(N) + @test zero(N) == 0 + @test one(N) == 1 + @test one(N) * oneunit(N) == oneunit(N) + @test typemin(N) == 0 + @test typemax(N) == typemax(T)//(big"2"^f - 1) + @test floatmin(N) === eps(N) == 1//(big"2"^f - 1) + @test floatmax(N) === typemax(N) + @test eps(zero(N)) === eps(typemax(N)) + @test sizeof(N) == sizeof(T) end - @test typemax(N0f8) == 1 - @test typemax(N6f10) == typemax(UInt16)//(2^10-1) - @test typemax(N4f12) == typemax(UInt16)//(2^12-1) - @test typemax(N2f14) == typemax(UInt16)//(2^14-1) - @test typemax(N0f16) == 1 - @test typemax(N6f10) == typemax(UInt16) // (2^10-1) - @test typemax(N4f12) == typemax(UInt16) // (2^12-1) - @test typemax(N2f14) == typemax(UInt16) // (2^14-1) - @test typemax(Normed{UInt32,16}) == typemax(UInt32) // (2^16-1) - @test typemax(Normed{UInt64,3}) == typemax(UInt64) // (2^3-1) - @test typemax(Normed{UInt128,7}) == typemax(UInt128) // (2^7-1) - @test typemax(Normed{UInt128,100}) == typemax(UInt128) // (UInt128(2)^100-1) end @testset "inexactness" begin # TODO: change back to InexactError when it allows message strings @test_throws ArgumentError N0f8(2) - @test_throws ArgumentError N0f8(255) @test_throws ArgumentError N0f8(0xff) @test_throws ArgumentError N0f16(2) @test_throws ArgumentError N0f16(0xff) @@ -77,56 +99,81 @@ end @test_throws ArgumentError convert(N0f8, typemax(N6f10)) @test_throws ArgumentError convert(N0f16, typemax(N6f10)) @test_throws ArgumentError convert(Normed{UInt128,100}, 10^9) - @test_throws ArgumentError convert(Normed{UInt128,100}, 10.0^9) + + ret = @test_throws ArgumentError N0f8(255) + msg = ret.value.msg + @test occursin("N0f8 is an 8-bit type representing 256 values from 0.0 to 1.0;", msg) + ret = @test_throws ArgumentError convert(Normed{UInt128,100}, 10.0^9) + msg = ret.value.msg + @test occursin("Normed{UInt128,$(SP)100} is a 128-bit type representing 2^128 values", msg) +end + +@testset "disambiguation constructors" begin + @test_throws ArgumentError Normed{UInt32,16}('a') + @test_throws InexactError Normed{UInt32,16}(complex(1.0, 1.0)) + @test Normed{UInt32,16}(complex(1.0, 0.0)) == 1 + @test Normed{UInt32,16}(Base.TwicePrecision(1.0, 0.0)) == 1 end @testset "conversion" begin x = N0f8(0.5) @test convert(N0f8, x) === x - @test convert(N0f8, 1.1/typemax(UInt8)) == eps(N0f8) - @test convert(N6f10, 1.1/typemax(UInt16)*64) == eps(N6f10) - @test convert(N4f12, 1.1/typemax(UInt16)*16) == eps(N4f12) - @test convert(N2f14, 1.1/typemax(UInt16)*4) == eps(N2f14) - @test convert(N0f16, 1.1/typemax(UInt16)) == eps(N0f16) - @test convert(Normed{UInt32,16}, 1.1/typemax(UInt32)*2^16) == eps(Normed{UInt32,16}) - @test convert(Normed{UInt64,3}, 1.1/typemax(UInt64)*UInt64(2)^61) == eps(Normed{UInt64,3}) - @test convert(Normed{UInt128,7}, 1.1/typemax(UInt128)*UInt128(2)^121) == eps(Normed{UInt128,7}) + @test convert(N0f8, 1.1/typemax(UInt8)) === eps(N0f8) + @test convert(N0f8, 1.1f0/typemax(UInt8)) === eps(N0f8) + @test convert(N6f10, 1.1/typemax(UInt16)*64) === eps(N6f10) + @test convert(N4f12, 1.1/typemax(UInt16)*16) === eps(N4f12) + @test convert(N2f14, 1.1/typemax(UInt16)*4) === eps(N2f14) + @test convert(N0f16, 1.1/typemax(UInt16)) === eps(N0f16) + @test convert(N16f16, 1.1/typemax(UInt32)*2^16) === eps(N16f16) + @test convert(N61f3, 1.1/typemax(UInt64)*UInt64(2)^61) === eps(N61f3) + @test convert(Normed{UInt128,7}, 1.1/typemax(UInt128)*UInt128(2)^121) === eps(Normed{UInt128,7}) - @test convert(N0f8, 1.1f0/typemax(UInt8)) == eps(N0f8) + @test convert(N0f8, Base.TwicePrecision(1.0)) === 1N0f8 - @test convert(N0f8, 1//255) === eps(N0f8) - @test convert(N0f8, Rational{Int8}(3//5)) === N0f8(3/5) - @test convert(N0f8, Rational{UInt8}(3//5)) === N0f8(3/5) - @test_throws ArgumentError convert(N0f8, typemax(Rational{UInt8})) + @test convert(N0f16, one(N0f8)) === one(N0f16) + @test convert(N0f16, N0f8(0.5)) === reinterpret(N0f16, 0x8080) + @test convert(N9f7, N1f7(0.504)) === N9f7(0.504) - @test convert(N0f8, Base.TwicePrecision(1.0)) === 1N0f8 + # avoiding overflow with Float16 + @test N0f16(Float16(1.0)) === N0f16(1.0) + @test Float16(1.0) % N0f16 === N0f16(1.0) +end - @test convert(Float64, eps(N0f8)) == 1/typemax(UInt8) - @test convert(Float32, eps(N0f8)) == 1.0f0/typemax(UInt8) - @test convert(BigFloat, eps(N0f8)) == BigFloat(1)/typemax(UInt8) - for T in (FixedPointNumbers.UF..., UF2...) - @test convert(Bool, zero(T)) == false - @test convert(Bool, one(T)) == true - @test_throws InexactError convert(Bool, convert(T, 0.2)) - @test convert(Int, one(T)) == 1 - @test convert(Integer, one(T)) == 1 - @test convert(Rational, one(T)) == 1 +@testset "bool conversions" begin + @testset "$N to/from Bool" for N in target(Normed) + @test convert(Bool, zero(N)) === false + @test convert(Bool, oneunit(N)) === true + eps(N) < 1 && @test_throws InexactError convert(Bool, convert(N, 0.2)) + @test convert(N, true) === oneunit(N) + @test convert(N, false) === zero(N) end - @test convert(N0f16, one(N0f8)) === one(N0f16) - @test convert(N0f16, N0f8(0.5)).i === 0x8080 - @test convert(Normed{UInt16,7}, Normed{UInt8,7}(0.504)) === Normed{UInt16,7}(0.504) + @test Bool(1N0f8) === true end @testset "integer conversions" begin + @testset "$N to/from integer" for N in target(Normed) + @test convert(Int, oneunit(N)) === 1 + @test convert(Integer, oneunit(N)) === oneunit(rawtype(N)) + @test convert(N, 1) === oneunit(N) + @test convert(N, 0x0) === zero(N) + end @test convert(UInt, 1N1f7) === UInt(1) - @test convert(Integer, 1N1f7) === 0x01 - @test convert(Int, 1N1f7) === 1 @test_throws InexactError convert(Integer, 0.5N1f7) @test_throws InexactError convert(Int8, 256N8f8) end @testset "rational conversions" begin + @testset "$N to/from rational" for N in target(Normed) + @test convert(Rational, oneunit(N)) == 1//1 + @test convert(Rational{Int}, zero(N)) === 0//1 + @test convert(N, 1//1) === oneunit(N) + end + @test convert(N0f8, 1//255) === eps(N0f8) + @test convert(N0f8, Rational{Int8}(3//5)) === N0f8(3/5) + @test convert(N0f8, Rational{UInt8}(3//5)) === N0f8(3/5) + @test_throws ArgumentError convert(N0f8, typemax(Rational{UInt8})) + @test convert(Rational, 0.5N0f8) === Rational{UInt8}(0x80//0xff) @test convert(Rational, 0.5N4f12) === Rational{UInt16}(0x800//0xfff) @test convert(Rational{Int}, 0.5N0f8) === Rational{Int}(0x80//0xff) @@ -139,42 +186,52 @@ end @testset "BigFloat conversions" begin @test convert(BigFloat, 0.5N0f8)::BigFloat == 128 / big"255" + @test convert(BigFloat, eps(N0f8))::BigFloat == 1 / big"255" @test big(N7f1) === BigFloat # !== BigInt @test big(0.5N4f4)::BigFloat == 8 / big"15" end -@testset "conversion from float" begin +@testset "float/floattype" begin + @test float(0.8N4f4) === 0.8f0 + @test float(0.8N20f12) === 0.8 + @test float(0.8N8f24) === 0.8 + @test float(1N11f53)::BigFloat == big"1.0" + + test_floattype(Normed) +end + +@testset "conversions from float" begin # issue 102 - for T in (UInt8, UInt16, UInt32, UInt64, UInt128) - for Tf in (Float16, Float32, Float64) - @testset "Normed{$T,$f}(::$Tf)" for f = 1:bitwidth(T) - N = Normed{T,f} - r = FixedPointNumbers.rawone(N) - - @test reinterpret(N(zero(Tf))) == 0x0 - - input_typemax = Tf(typemax(N)) - if isinf(input_typemax) - @test reinterpret(N(floatmax(Tf))) >= round(T, floatmax(Tf)) - else - @test reinterpret(N(input_typemax)) > (typemax(T)>>1) # overflow check - @test N(input_typemax) >= N(prevfloat(input_typemax)) - end - - input_upper = Tf(BigFloat(typemax(T)) / r, RoundDown) - isinf(input_upper) && continue # for Julia v0.7 - @test reinterpret(N(input_upper)) == T(min(round(BigFloat(input_upper) * r), typemax(T))) - - input_exp2 = Tf(exp2(bitwidth(T) - f)) - isinf(input_exp2) && continue - @test reinterpret(N(input_exp2)) == T(input_exp2) * r - end + for Tf in (Float16, Float32, Float64) + @testset "$N(::$Tf)" for N in target(Normed) + T, f = rawtype(N), nbitsfrac(N) + r = FixedPointNumbers.rawone(N) + + @test reinterpret(N(zero(Tf))) == 0x0 + + input_typemax = Tf(typemax(N)) + if isinf(input_typemax) + @test reinterpret(N(floatmax(Tf))) >= round(T, floatmax(Tf)) + else + @test reinterpret(N(input_typemax)) > (typemax(T)>>1) # overflow check + @test N(input_typemax) >= N(prevfloat(input_typemax)) + end + + input_upper = Tf(BigFloat(typemax(T)) / r, RoundDown) + isinf(input_upper) && continue # for Julia v0.7 + @test reinterpret(N(input_upper)) == T(min(round(BigFloat(input_upper) * r), typemax(T))) + + input_exp2 = Tf(exp2(bitwidth(T) - f)) + isinf(input_exp2) && continue + @test reinterpret(N(input_exp2)) == T(input_exp2) * r end end @test N0f32(Float32(0x0.7FFFFFp-32)) == zero(N0f32) @test N0f32(Float32(0x0.800000p-32)) <= eps(N0f32) # should be zero in RoundNearest mode @test N0f32(Float32(0x0.800001p-32)) == eps(N0f32) + + test_convert_from_nan(Normed) end @testset "conversions to float" begin @@ -185,105 +242,193 @@ end end for Tf in (Float16, Float32, Float64) - @testset "$Tf(::Normed{$T})" for T in (UInt8, UInt16) - @testset "$Tf(::Normed{$T,$f})" for f = 1:bitwidth(T) - N = Normed{T,f} - float_err = 0.0 - for i = typemin(T):typemax(T) - f_expected = Tf(i / BigFloat(FixedPointNumbers.rawone(N))) - isinf(f_expected) && break # for Float16(::Normed{UInt16,1}) - f_actual = Tf(reinterpret(N, i)) - float_err += abs(f_actual - f_expected) - end - @test float_err == 0.0 + @testset "$Tf(::$N)" for N in target(Normed, :i8, :i16) + T = rawtype(N) + float_err = zero(Tf) + for i = typemin(T):typemax(T) + b_expected = i / BigFloat(FixedPointNumbers.rawone(N)) + f_expected = Tf(promote_type(Tf, Float32)(b_expected)) # workaround for issue #246 + isinf(f_expected) && break # for Float16(::Normed{UInt16,1}) + f_actual = Tf(reinterpret(N, i)) + float_err += abs(f_actual - f_expected) end + @test float_err == 0 end - @testset "$Tf(::Normed{$T})" for T in (UInt32, UInt64, UInt128) - @testset "$Tf(::Normed{$T,$f})" for f = 1:bitwidth(T) - N = Normed{T,f} - error_count = 0 - for i in vcat(T(0x00):T(0xFF), (typemax(T)-0xFF):typemax(T)) - f_expected = Tf(i / BigFloat(FixedPointNumbers.rawone(N))) - isinf(f_expected) && break # for Float16() and Float32() - f_actual = Tf(reinterpret(N, i)) - f_actual == f_expected && continue - f_actual == prevfloat(f_expected) && continue - f_actual == nextfloat(f_expected) && continue - error_count += 1 - end - @test error_count == 0 + @testset "$Tf(::$N)" for N in target(Normed, :i32, :i64, :i128) + T = rawtype(N) + error_count = 0 + for i in vcat(T(0x00):T(0xFF), (typemax(T)-0xFF):typemax(T)) + b_expected = i / BigFloat(FixedPointNumbers.rawone(N)) + f_expected = Tf(promote_type(Tf, Float32)(b_expected)) # workaround for issue #246 + isinf(f_expected) && break # for Float16() and Float32() + f_actual = Tf(reinterpret(N, i)) + f_actual == f_expected && continue + f_actual == prevfloat(f_expected) && continue + f_actual == nextfloat(f_expected) && continue + error_count += 1 end + @test error_count == 0 end end end -@testset "modulus" begin +@testset "type modulus" begin + test_rem_type(Normed) + test_rem_nan(Normed) + @test N0f8(0.2) % N0f8 === N0f8(0.2) @test N2f14(1.2) % N0f16 === N0f16(0.20002) @test N2f14(1.2) % N0f8 === N0f8(0.196) - for i = 0.0:0.1:1.0 - @test i % N0f8 === N0f8(i) - end @test ( 1.5 % N0f8).i == round(Int, 1.5*255) % UInt8 @test (-0.3 % N0f8).i == round(Int, -0.3*255) % UInt8 - for i = 0.0:0.1:64.0 - @test i % N6f10 === N6f10(i) - end @test (65.2 % N6f10).i == round(Int, 65.2*1023) % UInt16 @test (-0.3 % N6f10).i == round(Int, -0.3*1023) % UInt16 - @test 1 % N0f8 == 1 - @test 2 % N0f8 == N0f8(0.996) + @test 1 % N0f8 === N0f8(1) + @test 2 % N0f8 === N0f8(0.996) # issue #150 @test all(f -> 1.0f0 % Normed{UInt32,f} == oneunit(Normed{UInt32,f}), 1:32) @test all(f -> 1.0e0 % Normed{UInt64,f} == oneunit(Normed{UInt64,f}), 1:64) -end - -@testset "bitwise" begin - x = N0f8(0b01010001, 0) - @test ~x == N0f8(0b10101110, 0) - @test -x == reinterpret(N0f8, 0xaf) -end -@testset "float" begin - @test isa(float(one(Normed{UInt8,7})), Float32) - @test isa(float(one(Normed{UInt32,18})), Float64) - @test isa(float(one(Normed{UInt32,25})), Float64) + # issue #211 + @test big"1.2" % N0f8 === 0.196N0f8 + @test reinterpret(BigFloat(0x0_01234567_89abcdef) % N63f1) === 0x01234567_89abcdef end @testset "arithmetic" begin - for T in (FixedPointNumbers.UF..., UF2...) - x = T(0x10,0) - y = T(0x25,0) + @testset "$N arithmetic" for N in target(Normed; ex = :light) + x = N(0x10,0) + y = N(0x25,0) fx = float(x) fy = float(y) @test y > x @test y != x - @test typeof(x+y) == T - @test typeof((x+y)-y) == T - @test typeof(x*y) == T - @test typeof(x/y) == T - @test (x+y) ≈ T(0x35,0) - @test ((x+y)-x) ≈ fy - @test ((x-y)+y) ≈ fx - @test (x*y) ≈ convert(T, fx*fy) - @test (x/y) ≈ convert(T, fx/fy) - @test (x^2) ≈ convert(T, fx^2) + @test x+y === N(0x35,0) + @test ((x+y)-y) === x + @test ((x-y)+y) === x # wraparound + fx*fy <= typemax(N) && @test (x*y)::N ≈ convert(N, fx*fy) + @test (x/y)::N ≈ convert(N, fx/fy) + fx^2 <= typemax(N) && @test (x^2)::N ≈ convert(N, fx^2) @test (x^2.1f0) ≈ fx^2.1f0 @test (x^2.1) ≈ convert(Float64, x)^2.1 end end +@testset "neg" begin + for N in target(Normed; ex = :thin) + @test wrapping_neg(typemin(N)) === zero(N) + + @test wrapping_neg(typemax(N)) === eps(N) + + @test wrapping_neg(eps(N)) === typemax(N) + end + test_neg(Normed) +end + +@testset "abs" begin + for N in target(Normed; ex = :thin) + @test wrapping_abs(typemax(N)) === typemax(N) + + @test wrapping_abs(typemin(N)) === typemin(N) + end + test_abs(Normed) +end + +@testset "add" begin + for N in target(Normed; ex = :thin) + @test wrapping_add(typemin(N), typemin(N)) === zero(N) + + @test wrapping_add(typemax(N), eps(N)) === wrapping_add(eps(N), typemax(N)) === zero(N) + + @test wrapping_add(zero(N), eps(N)) === wrapping_add(eps(N), zero(N)) === eps(N) + end + test_add(Normed) +end + +@testset "sub" begin + for N in target(Normed; ex = :thin) + @test wrapping_sub(typemin(N), typemin(N)) === zero(N) + + @test wrapping_sub(typemin(N), eps(N)) === typemax(N) + + @test wrapping_sub(eps(N), zero(N)) === eps(N) + end + test_sub(Normed) +end + +@testset "mul" begin + for N in target(Normed; ex = :thin) + @test checked_mul(typemax(N), zero(N)) === zero(N) + + @test checked_mul(one(N), typemax(N)) === typemax(N) + + if typemax(N) != 1 + @test_throws OverflowError checked_mul(typemax(N), typemax(N)) + end + end + test_mul(Normed) +end + +@testset "fdiv" begin + for N in target(Normed; ex = :thin) + @test checked_fdiv(typemax(N), typemax(N)) === one(N) + + @test checked_fdiv(zero(N), eps(N)) === zero(N) + + @test_throws Exception checked_fdiv(typemax(N), eps(N)) # OverflowError on v0.9 (#222) + + @test_throws Exception checked_fdiv(zero(N), zero(N)) # DivideError on v0.9 (#222) + + @test_throws Exception checked_fdiv(eps(N), zero(N)) # DivideError on v0.9 (#222) + end + test_fdiv(Normed) +end + +@testset "div/cld/fld" begin + for N in target(Normed; ex = :thin) + nm, nz, ne = typemax(N), zero(N), eps(N) + T = rawtype(N) + @test checked_div(nm, nm) === checked_fld(nm, nm) === checked_cld(nm, nm) === one(T) + + @test checked_div(nz, ne) === checked_fld(nz, ne) === checked_cld(nz, ne) === zero(T) + + @test checked_div(nm, ne) === checked_fld(nm, ne) === checked_cld(nm, ne) === typemax(T) + + @test_throws DivideError checked_div(nz, nz) + @test_throws DivideError checked_fld(nz, nz) + @test_throws DivideError checked_cld(nz, nz) + + @test_throws DivideError checked_div(ne, nz) + @test_throws DivideError checked_fld(ne, nz) + @test_throws DivideError checked_cld(ne, nz) + + @test checked_div(ne, nm) === zero(T) + @test checked_fld(ne, nm) === zero(T) + @test checked_cld(ne, nm) === one(T) + end + test_div(Normed) + test_div_3arg(Normed) +end + +@testset "rem/mod" begin + @test mod(reinterpret(N0f8, 0x10), reinterpret(N0f8, 0x02)) == rem(reinterpret(N0f8, 0x10), reinterpret(N0f8, 0x02)) == 0 + @test mod(reinterpret(N0f8, 0x0f), reinterpret(N0f8, 0x02)) == rem(reinterpret(N0f8, 0x0f), reinterpret(N0f8, 0x02)) == reinterpret(N0f8, 0x01) +end + +@testset "fld1/mod1" begin + test_fld1_mod1(Normed) +end + @testset "rounding" begin - for T in (UInt8, UInt16, UInt32, UInt64) + for sym in (:i8, :i16, :i32, :i64) + T = symbol_to_inttype(Normed, sym) rs = vcat([ oneunit(T) << b - oneunit(T) << 1 for b = 1:bitwidth(T)], [ oneunit(T) << b - oneunit(T) for b = 1:bitwidth(T)], [ oneunit(T) << b for b = 2:bitwidth(T)-1]) - @testset "rounding Normed{$T,$f}" for f = 1:bitwidth(T) - N = Normed{T,f} + @testset "rounding $N" for N in target(Normed, sym) xs = (reinterpret(N, r) for r in rs) @test all(x -> trunc(x) == trunc(float(x)), xs) @test all(x -> floor(x) == floor(float(x)), xs) @@ -314,28 +459,22 @@ end end @testset "approx" begin - @testset "approx $T" for T in FixedPointNumbers.UF - xs = typemin(T):eps(T):typemax(T)-eps(T) - @test all(x -> x ≈ x + eps(T), xs) - @test all(x -> x + eps(T) ≈ x, xs) - @test !any(x -> x - eps(T) ≈ x + eps(T), xs) - end + test_isapprox(Normed) + + # PR #216 required + @test_broken isapprox(typemin(N0f8), typemax(N0f8), rtol=1.0) + @test !isapprox(zero(N0f8), typemax(N0f8), rtol=0.9) + @test isapprox(zero(N0f8), eps(N0f8), rtol=1e-6) # atol = eps(N0f8) + @test !isapprox(eps(N0f8), zero(N0f8), rtol=1e-6, atol=1e-6) + @test_broken !isapprox(0.66N6f2, 1.0N6f2, rtol=0.3, atol=0) # 1.0 * 0.3 < eps(N6f2) + + @test isapprox(eps(N8f8), eps(N0f8), rtol=1e-6) end -@testset "low-level arithmetic" begin +@testset "comparison" begin @test !(N0f8(0.5) < N0f8(0.5)) @test N0f8(0.5) <= N0f8(0.5) - @test div(reinterpret(N0f8, 0x10), reinterpret(N0f8, 0x02)) == fld(reinterpret(N0f8, 0x10), reinterpret(N0f8, 0x02)) == 8 - @test div(reinterpret(N0f8, 0x0f), reinterpret(N0f8, 0x02)) == fld(reinterpret(N0f8, 0x0f), reinterpret(N0f8, 0x02)) == 7 - @test Base.fld1(reinterpret(N0f8, 0x10), reinterpret(N0f8, 0x02)) == 8 - @test Base.fld1(reinterpret(N0f8, 0x0f), reinterpret(N0f8, 0x02)) == 8 - @test mod(reinterpret(N0f8, 0x10), reinterpret(N0f8, 0x02)) == rem(reinterpret(N0f8, 0x10), reinterpret(N0f8, 0x02)) == 0 - @test mod(reinterpret(N0f8, 0x0f), reinterpret(N0f8, 0x02)) == rem(reinterpret(N0f8, 0x0f), reinterpret(N0f8, 0x02)) == reinterpret(N0f8, 0x01) - @test mod1(reinterpret(N0f8, 0x10), reinterpret(N0f8, 0x02)) == reinterpret(N0f8, 0x02) - @test mod1(reinterpret(N0f8, 0x0f), reinterpret(N0f8, 0x02)) == reinterpret(N0f8, 0x01) - @test bswap(N0f8(0.5)) === N0f8(0.5) - @test bswap(N0f16(0.5)) === reinterpret(N0f16, 0x0080) @test minmax(N0f8(0.8), N0f8(0.2)) === (N0f8(0.2), N0f8(0.8)) end @@ -350,6 +489,8 @@ end @test clamp(0.5, N0f8) === 0.5N0f8 @test clamp(-1.0f0, N0f8) === 0.0N0f8 @test clamp(2.0N1f7, N0f8) === 1.0N0f8 + + test_clamp_nan(Normed) end @testset "sign-related functions" begin @@ -365,6 +506,32 @@ end @test signbit(1N0f8) === false end +@testset "bitwise" begin + x = N0f8(0b01010001, 0) + @test ~x == N0f8(0b10101110, 0) + @test -x == reinterpret(N0f8, 0xaf) + + @test bswap(N0f8(0.5)) === N0f8(0.5) + @test bswap(N0f16(0.5)) === reinterpret(N0f16, 0x0080) +end + +@testset "predicates" begin + @test isfinite(1N8f8) + @test !isnan(1N8f8) + @test !isinf(1N8f8) + + @testset "isinteger" begin + test_isinteger(Normed) + @testset "isinteger(::$N)" for N in target(Normed, :i32, :i64, :i128) + if nbitsfrac(N) == 1 + @test isinteger(zero(N)) & isinteger(oneunit(N)) + else + @test !isinteger(oneunit(N) - eps(N)) & isinteger(oneunit(N)) + end + end + end +end + @testset "unit range" begin @test length(N0f8(0):N0f8(1)) == 2 @test length(N0f8(1):N0f8(0)) == 0 @@ -377,7 +544,6 @@ end NInt1 = Normed{UInt,1} @test length(NInt1(0):typemax(NInt1)-oneunit(NInt1)) == typemax(UInt) @test_throws OverflowError length(NInt1(0):typemax(NInt1)) - @test Base.unsafe_length(NInt1(0):typemax(NInt1)) == 0 # overflow N64f64 = Normed{UInt128,64} @test_broken length(N64f64(0):typemax(N64f64)) == UInt128(typemax(UInt64)) + 1 @test length(N1f63(2):N1f63(0)) == 0 @@ -397,30 +563,32 @@ end @test_throws OverflowError length(NInt1(0):NInt1(1):typemax(NInt1)) end -@testset "predicates" begin - @test isfinite(1N8f8) - @test !isnan(1N8f8) - @test !isinf(1N8f8) +@testset "reductions" begin + a = N0f8[reinterpret(N0f8, 0xff), reinterpret(N0f8, 0xff)] + @test sum(a) == 2.0 + @test sum(a, dims=1) == [2.0] - @testset "isinteger" begin - for T in (UInt8, UInt16) - @testset "isinteger(::Normed{$T,$f})" for f = 1:bitwidth(T) - N = Normed{T,f} - xs = typemin(N):eps(N):typemax(N) - @test all(x -> isinteger(x) == isinteger(float(x)), xs) - end - end - for T in (UInt32, UInt64) - @testset "isinteger(::Normed{$T,$f})" for f = 1:bitwidth(T) - N = Normed{T,f} - if f == 1 - @test isinteger(zero(N)) & isinteger(oneunit(N)) - else - @test !isinteger(oneunit(N) - eps(N)) & isinteger(oneunit(N)) - end - end - end - end + a = N2f14[3.2, 2.4] + acmp = Float64(a[1])*Float64(a[2]) + @test prod(a) == acmp + @test prod(a, dims=1) == [acmp] +end + +@testset "reductions, Statistics" begin + a = N0f8[reinterpret(N0f8, 0x80), reinterpret(N0f8, 0x40)] + af = FixedPointNumbers.Treduce.(a) + @test mean(a) === mean(af) + @test std(a) === std(af) + @test var(a) === var(af) + m = mean(a) + @test stdm(a, m) === stdm(af, m) + @test varm(a, m) === varm(af, m) +end + +@testset "rand" begin + test_rand(Normed) + @test !(rand(N0f16) == rand(N0f16) == rand(N0f16)) # If this fails, we should suspect a bug. + @test rand(StableRNG(1234), N0f8) === 0.267N0f8 end @testset "Promotion within Normed" begin @@ -440,16 +608,35 @@ end @test Normed{UInt16,4}(1) == Normed{UInt8,6}(1) @test Normed{UInt16,4}(0.2) == Normed{UInt8,6}(0.2) - @test promote_type(N0f8,Float32,Int) == Float32 - @test promote_type(N0f8,Int,Float32) == Float32 - @test promote_type(Int,N0f8,Float32) == Float32 - @test promote_type(Int,Float32,N0f8) == Float32 - @test promote_type(Float32,Int,N0f8) == Float32 - @test promote_type(Float32,N0f8,Int) == Float32 - @test promote_type(N0f8,N1f7,N2f6,N3f5,N4f4,N5f3) == Normed{UInt128,8} + @test @inferred(promote_type(N0f8, Float64)) === Float64 + @test @inferred(promote_type(Float32, N8f24)) === Float64 + + @test @inferred(promote_type(N0f8, Int8)) === Float32 + @test @inferred(promote_type(Int128, N8f24)) === Float64 + + @test @inferred(promote_type(N0f16, Rational{Int8})) === Rational{Int8} + + @test @inferred(promote_type(N0f8, Float32, Int)) === Float32 + @test @inferred(promote_type(N0f8, Int, Float32)) === Float32 + @test @inferred(promote_type(Int, N0f8, Float32)) === Float32 + @test @inferred(promote_type(Int, Float32, N0f8)) === Float32 + @test @inferred(promote_type(Float32, Int, N0f8)) === Float32 + @test @inferred(promote_type(Float32, N0f8, Int)) === Float32 + + if promote_type(Int, Float32, Complex{Int}, typeof(pi)) === ComplexF64 + # right-to-left + @test @inferred(promote_type(N0f8, N1f7, N2f6, N3f5, N4f4, N5f3)) === Normed{UInt128,8} + else + # left-to-right + @test @inferred(promote_type(N5f3, N4f4, N3f5, N2f6, N1f7, N0f8)) === Normed{UInt128,8} + end + + @test @inferred(promote_type(N0f8, Q0f31)) === FixedPoint # Float64 on v0.9 (#207) end @testset "show" begin + @test (@test_deprecated FixedPointNumbers.typechar(N0f8)) === 'N' + iob = IOBuffer() n0f8 = reinterpret(N0f8, 0xaa) show(iob, n0f8) @@ -457,6 +644,10 @@ end @test str == "0.667N0f8" @test eval(Meta.parse(str)) === n0f8 + print(iob, n0f8) + str = String(take!(iob)) + @test str == "0.667N0f8" # w/ type suffix (cf. PR #243) + n16f16 = reinterpret(N16f16, 0xaaaaaaaa) show(iob, n16f16) str = String(take!(iob)) @@ -479,7 +670,7 @@ end @test String(take!(iob)) == "43691.33333N16f16" show(iob, Normed{UInt128,64}(1.2345e6)) - @test_broken String(take!(iob)) == "Normed{UInt128,64}(1.2345e6)" # "N64f64" is not defined + @test String(take!(iob)) == "Normed{UInt128,$(SP)64}(1.2345e6)" end @testset "summary" begin @@ -523,50 +714,3 @@ end bd, ad = scaledual(Float64, a) @test 1.0*a == bd*ad end - -@testset "reductions" begin - a = N0f8[reinterpret(N0f8, 0xff), reinterpret(N0f8, 0xff)] - @test sum(a) == 2.0 - @test sum(a, dims=1) == [2.0] - - a = N2f14[3.2, 2.4] - acmp = Float64(a[1])*Float64(a[2]) - @test prod(a) == acmp - @test prod(a, dims=1) == [acmp] -end - -@testset "reductions, Statistics" begin - a = N0f8[reinterpret(N0f8, 0x80), reinterpret(N0f8, 0x40)] - af = FixedPointNumbers.Treduce.(a) - @test mean(a) === mean(af) - @test std(a) === std(af) - @test var(a) === var(af) - m = mean(a) - @test stdm(a, m) === stdm(af, m) - @test varm(a, m) === varm(af, m) -end - -@testset "rand" begin - for T in (Normed{UInt8,8}, Normed{UInt8,6}, - Normed{UInt16,16}, Normed{UInt16,14}, - Normed{UInt32,32}, Normed{UInt32,30}, - Normed{UInt64,64}, Normed{UInt64,62}) - a = rand(T) - @test isa(a, T) - a = rand(T, (3, 5)) - @test ndims(a) == 2 && eltype(a) == T - @test size(a) == (3,5) - end -end - -@testset "Overflow with Float16" begin - @test N0f16(Float16(1.0)) === N0f16(1.0) - @test Float16(1.0) % N0f16 === N0f16(1.0) -end - -@testset "disambiguation constructors" begin - @test_throws ArgumentError Normed{UInt32,16}('a') - @test_throws InexactError Normed{UInt32,16}(complex(1.0, 1.0)) - @test Normed{UInt32,16}(complex(1.0, 0.0)) == 1 - @test Normed{UInt32,16}(Base.TwicePrecision(1.0, 0.0)) == 1 -end diff --git a/test/runtests.jl b/test/runtests.jl index 570cec53..d037d440 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,6 +1,11 @@ -using FixedPointNumbers, Test +using FixedPointNumbers, Test, Aqua -@test isempty(detect_ambiguities(FixedPointNumbers, Base, Core)) +Aqua.test_all(FixedPointNumbers) + +if Sys.ARCH === :x86_64 || Sys.ARCH === :i686 + using Documenter + doctest(FixedPointNumbers, manual = false) +end @testset "normed" begin include("normed.jl") diff --git a/test/traits.jl b/test/traits.jl index 9aee60b0..c248b4c0 100644 --- a/test/traits.jl +++ b/test/traits.jl @@ -1,23 +1,11 @@ using FixedPointNumbers, Test +using FixedPointNumbers: bitwidth struct MyReal <: Real end @testset "floattype" begin - function _is_fixed_type(x::Symbol) - try - @eval $(x) isa Type && $(x) <: FixedPoint && return true - catch - return false - end - end - - fixed_types = setdiff(filter(_is_fixed_type, names(FixedPointNumbers)), [:Fixed, :Normed, :FixedPoint]) - fixed_types = [@eval $(x) for x in fixed_types] - - exact_types = vcat([UInt8, UInt16, UInt32, UInt64, UInt128, Bool, - Int8, Int16, Int32, Int64, Int128], - fixed_types) - for T in exact_types + for T in (UInt8, UInt16, UInt32, UInt64, UInt128, Bool, + Int8, Int16, Int32, Int64, Int128) @test typemax(T) <= maxintfloat(floattype(T)) end @test floattype(Rational{Int}) === Float64