diff --git a/.github/workflows/UnitTest.yml b/.github/workflows/UnitTest.yml new file mode 100644 index 0000000..228b8fd --- /dev/null +++ b/.github/workflows/UnitTest.yml @@ -0,0 +1,49 @@ +name: Unit test + +on: + push: + pull_request: +defaults: + run: + shell: bash +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + julia-version: ['1.0', '1', 'nightly'] + os: [ubuntu-latest, windows-latest, macOS-latest] + julia-arch: [x64] + include: + - os: ubuntu-latest + julia-version: '1' + julia-arch: x86 + steps: + - uses: actions/checkout@v2 + + - name: "Set up Julia" + uses: julia-actions/setup-julia@v1 + with: + version: ${{ matrix.julia-version }} + arch: ${{ matrix.julia-arch }} + - run: julia --project -e 'using Pkg; Pkg.develop([PackageSpec(path="CheckedArithmeticCore")])' + + - name: Cache artifacts + uses: actions/cache@v1 + env: + cache-name: cache-artifacts + with: + 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-processcoverage@v1 + - uses: codecov/codecov-action@v1 + with: + file: lcov.info diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 6c8e3fa..0000000 --- a/.travis.yml +++ /dev/null @@ -1,15 +0,0 @@ -# Documentation: http://docs.travis-ci.com/user/languages/julia/ -language: julia -os: - - linux - - osx - - windows -julia: - - 1.0 - - nightly -matrix: - # allow_failures: - # - julia: nightly - fast_finish: true -notifications: - email: false diff --git a/CheckedArithmeticCore/LICENSE b/CheckedArithmeticCore/LICENSE new file mode 100644 index 0000000..81f7101 --- /dev/null +++ b/CheckedArithmeticCore/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2019 Tim Holy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/CheckedArithmeticCore/Project.toml b/CheckedArithmeticCore/Project.toml new file mode 100644 index 0000000..b99afdb --- /dev/null +++ b/CheckedArithmeticCore/Project.toml @@ -0,0 +1,13 @@ +name = "CheckedArithmeticCore" +uuid = "740b204e-26e5-40b1-866a-9c367e60c4b6" +authors = ["Tim Holy "] +version = "0.1.0" + +[compat] +julia = "1" + +[extras] +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[targets] +test = ["Test"] diff --git a/CheckedArithmeticCore/README.md b/CheckedArithmeticCore/README.md new file mode 100644 index 0000000..06628ae --- /dev/null +++ b/CheckedArithmeticCore/README.md @@ -0,0 +1,7 @@ +# CheckedArithmeticCore + +CheckedArithmeticCore is a component of +[CheckedArithmetic](https://github.com/JuliaMath/CheckedArithmetic.jl). + +This package provides a set of low-level APIs for the packages which provide +numeric types and their arithmetic. \ No newline at end of file diff --git a/CheckedArithmeticCore/src/CheckedArithmeticCore.jl b/CheckedArithmeticCore/src/CheckedArithmeticCore.jl new file mode 100644 index 0000000..a374255 --- /dev/null +++ b/CheckedArithmeticCore/src/CheckedArithmeticCore.jl @@ -0,0 +1,65 @@ +module CheckedArithmeticCore + +export safearg_type, safearg, safeconvert, accumulatortype, acc + +""" + newT = CheckedArithmeticCore.safearg_type(::Type{T}) + +Return a "reasonably safe" type `newT` for computation with numbers of type `T`. +For example, for `UInt8` one might return `UInt128`, because one is much less likely +to overflow with `UInt128`. +""" +function safearg_type end + + +""" + xsafe = CheckedArithmeticCore.safearg(x) + +Return a variant `xsafe` of `x` that is "reasonably safe" for non-overflowing computation. +For numbers, this uses [`CheckedArithmetic.safearg_type`](@ref). +For containers and other non-number types, specialize `safearg` directly. +""" +safearg(x::T) where T = convert(safearg_type(T), x) + + +""" + xc = safeconvert(T, x) + +Convert `x` to type `T`, "safely." This is designed for comparison to results computed by +[`CheckedArithmetic.@check`](@ref), i.e., for arguments converted by +[`CheckedArithmeticCore.safearg`](@ref). +""" +safeconvert(::Type{T}, x) where T = convert(T, x) + + +""" + Tnew = accumulatortype(op, T1, T2, ...) + Tnew = accumulatortype(T1, T2, ...) + +Return a type `Tnew` suitable for accumulation (reduction) of elements of type `T` under +operation `op`. + +# Examples + +```jldoctest; setup = :(using CheckedArithmetic) +julia> accumulatortype(+, UInt8) +$UInt +``` +""" +Base.@pure accumulatortype(op::Function, T1::Type, T2::Type, T3::Type...) = + accumulatortype(op, promote_type(T1, T2, T3...)) +Base.@pure accumulatortype(T1::Type, T2::Type, T3::Type...) = + accumulatortype(*, T1, T2, T3...) +accumulatortype(::Type{T}) where T = accumulatortype(*, T) + + +""" + xacc = acc(x) + +Convert `x` to type [`accumulatortype`](@ref)`(typeof(x))`. +""" +acc(x) = convert(accumulatortype(typeof(x)), x) +acc(f::F, x) where F<:Function = convert(accumulatortype(f, typeof(x)), x) + + +end # module diff --git a/CheckedArithmeticCore/test/runtests.jl b/CheckedArithmeticCore/test/runtests.jl new file mode 100644 index 0000000..d4f51ca --- /dev/null +++ b/CheckedArithmeticCore/test/runtests.jl @@ -0,0 +1,30 @@ +using CheckedArithmeticCore +using Test + +struct MyType end +struct MySafeType end +Base.convert(::Type{MySafeType}, ::MyType) = MySafeType() +CheckedArithmeticCore.safearg_type(::Type{MyType}) = MySafeType +CheckedArithmeticCore.accumulatortype(::typeof(+), ::Type{MyType}) = MyType +CheckedArithmeticCore.accumulatortype(::typeof(+), ::Type{MySafeType}) = MySafeType +CheckedArithmeticCore.accumulatortype(::typeof(*), ::Type{MyType}) = MySafeType +CheckedArithmeticCore.accumulatortype(::typeof(*), ::Type{MySafeType}) = MySafeType +Base.promote_rule(::Type{MyType}, ::Type{MySafeType}) = MySafeType + +# fallback +@test safearg(MyType()) === MySafeType() + +# fallback +@test safeconvert(UInt16, 0x12) === 0x0012 + +# fallback +@test accumulatortype(MyType) === MySafeType +@test accumulatortype(MyType, MyType) === MySafeType +@test accumulatortype(+, MyType) === MyType +@test accumulatortype(*, MyType) === MySafeType +@test accumulatortype(+, MyType, MySafeType) === MySafeType +@test accumulatortype(*, MySafeType, MyType) === MySafeType + +# acc +@test acc(MyType()) === MySafeType() +@test acc(+, MyType()) === MyType() diff --git a/Project.toml b/Project.toml index 3bf41a6..3be9f49 100644 --- a/Project.toml +++ b/Project.toml @@ -1,13 +1,21 @@ name = "CheckedArithmetic" uuid = "2c4a1fb8-30c1-4c71-8b84-dff8d59868ee" authors = ["Tim Holy "] -version = "0.1.0" +version = "0.2.0" [deps] +CheckedArithmeticCore = "740b204e-26e5-40b1-866a-9c367e60c4b6" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [compat] +CheckedArithmeticCore = "0.1" julia = "1" + +[extras] +Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[targets] +test = ["Pkg", "Test"] diff --git a/src/CheckedArithmetic.jl b/src/CheckedArithmetic.jl index 11d71d5..56cb814 100644 --- a/src/CheckedArithmetic.jl +++ b/src/CheckedArithmetic.jl @@ -1,33 +1,39 @@ module CheckedArithmetic +using CheckedArithmeticCore +import CheckedArithmeticCore: safearg_type, safearg, safeconvert, accumulatortype, acc + using Base.Meta: isexpr -using Test using LinearAlgebra: Factorization, UniformScaling using Random: AbstractRNG using Dates -export @checked, @check, accumulatortype, acc - -# + and - are not listed because of the unary/binary issues -const opcheck = Dict(:abs => :(Base.Checked.checked_abs), - :+ => :(Base.Checked.checked_add), - :- => :(Base.Checked.checked_sub), - :* => :(Base.Checked.checked_mul), - :÷ => :(Base.Checked.checked_div), - :rem => :(Base.Checked.checked_rem), - :fld => :(Base.Checked.checked_fld), - :mod => :(Base.Checked.checked_mod), - :cld => :(Base.Checked.checked_cld), - ) - -function replace_checked!(expr::Expr) +export @checked, @check +export accumulatortype, acc # re-export + +const op_checked = Dict( + Symbol("unary-") => :(Base.Checked.checked_neg), + :abs => :(Base.Checked.checked_abs), + :+ => :(Base.Checked.checked_add), + :- => :(Base.Checked.checked_sub), + :* => :(Base.Checked.checked_mul), + :÷ => :(Base.Checked.checked_div), + :div => :(Base.Checked.checked_div), + :% => :(Base.Checked.checked_rem), + :rem => :(Base.Checked.checked_rem), + :fld => :(Base.Checked.checked_fld), + :mod => :(Base.Checked.checked_mod), + :cld => :(Base.Checked.checked_cld), + ) + +function replace_op!(expr::Expr, op_map::Dict) if expr.head == :call f, len = expr.args[1], length(expr.args) op = isexpr(f, :.) ? f.args[2].value : f # handle module-scoped functions if op === :+ && len == 2 # unary + # no action required elseif op === :- && len == 2 # unary - - op = :(Base.Checked.checked_neg) + op = get(op_map, Symbol("unary-"), op) if isexpr(f, :.) f.args[2].value = op expr.args[1] = f @@ -35,7 +41,7 @@ function replace_checked!(expr::Expr) expr.args[1] = op end else # arbitrary call - op = get(opcheck, op, op) + op = get(op_map, op, op) if isexpr(f, :.) f.args[2].value = op expr.args[1] = f @@ -45,13 +51,13 @@ function replace_checked!(expr::Expr) end for a in Iterators.drop(expr.args, 1) if isa(a, Expr) - replace_checked!(a) + replace_op!(a, op_map) end end else for a in expr.args if isa(a, Expr) - replace_checked!(a) + replace_op!(a, op_map) end end end @@ -96,7 +102,7 @@ ERROR: OverflowError: 16 - 32 overflowed for type UInt8 macro checked(expr) isa(expr, Expr) || return expr expr = copy(expr) - return esc(replace_checked!(expr)) + return esc(replace_op!(expr, op_checked)) end macro check(expr, kws...) @@ -120,15 +126,9 @@ macro check(expr, kws...) end end -""" - newT = CheckedArithmetic.safearg_type(::Type{T}) - -Return a "reasonably safe" type `newT` for computation with numbers of type `T`. -For example, for `UInt8` one might return `UInt128`, because one is much less likely -to overflow with `UInt128`. -""" -function safearg_type end +# safearg_type +# ------------ safearg_type(::Type{BigInt}) = BigInt safearg_type(::Type{Int128}) = Int128 safearg_type(::Type{Int64}) = Int128 @@ -151,15 +151,9 @@ safearg_type(::Type{T}) where T<:Base.TwicePrecision = T safearg_type(::Type{<:Rational}) = Float64 -""" - xsafe = CheckedArithmetic.safearg(x) - -Return a variant `xsafe` of `x` that is "reasonably safe" for non-overflowing computation. -For numbers, this uses [`CheckedArithmetic.safearg_type`](@ref). -For containers and other non-number types, specialize `safearg` directly. -""" -safearg(x::Number) = convert(safearg_type(typeof(x)), x) +# safearg +#-------- # Containers safearg(t::Tuple) = map(safearg, t) safearg(t::NamedTuple) = map(safearg, t) @@ -219,42 +213,17 @@ safearg(t::Dates.AbstractTime) = t safearg(t::Dates.AbstractDateToken) = t -""" - xc = safeconvert(T, x) - -Convert `x` to type `T`, "safely." This is designed for comparison to results computed by -[`@check`](@ref), i.e., for arguments converted by [`CheckedArithmetic.safearg`](@ref). -""" -safeconvert(::Type{T}, x) where T = convert(T, x) - +# safeconvert +# ----------- safeconvert(::Type{T}, x) where T<:Integer = round(T, x) safeconvert(::Type{T}, x) where T<:AbstractFloat = T(x) safeconvert(::Type{AA}, A::AbstractArray) where AA<:AbstractArray{T} where T<:Integer = round.(T, A) -""" - Tnew = accumulatortype(op, T1, T2, ...) - Tnew = accumulatortype(T1, T2, ...) - -Return a type `Tnew` suitable for accumulation (reduction) of elements of type `T` under -operation `op`. - -# Examples - -```jldoctest -julia> accumulatortype(+, UInt8) -$UInt - -julia> accumulatortype -""" -Base.@pure accumulatortype(op::Function, T1::Type, T2::Type, T3::Type...) = - accumulatortype(op, promote_type(T1, T2, T3...)) -Base.@pure accumulatortype(T1::Type, T2::Type, T3::Type...) = - accumulatortype(*, T1, T2, T3...) -accumulatortype(::Type{T}) where T = accumulatortype(*, T) - +# accumulatortype +# --------------- const SignPreserving = Union{typeof(+), typeof(*)} -const ArithmeticOp = Union{SignPreserving,typeof(-)} +const ArithmeticOp = Union{SignPreserving, typeof(-)} accumulatortype(::ArithmeticOp, ::Type{BigInt}) = BigInt accumulatortype(::ArithmeticOp, ::Type{Int128}) = Int128 @@ -279,12 +248,4 @@ accumulatortype(::ArithmeticOp, ::Type{Float64}) = Float64 accumulatortype(::ArithmeticOp, ::Type{Float32}) = Float64 accumulatortype(::ArithmeticOp, ::Type{Float16}) = Float64 -""" - xacc = acc(x) - -Convert `x` to type [`accumulatortype`](@ref)`(typeof(x))`. -""" -acc(x) = convert(accumulatortype(typeof(x)), x) -acc(f::F, x) where F<:Function = convert(accumulatortype(f, typeof(x)), x) - end # module diff --git a/test/runtests.jl b/test/runtests.jl index 80c92a1..0efdbfb 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,5 +1,10 @@ -using CheckedArithmetic -using Test, Dates + +# Explicitly use `import` instead of `using` to make sure there is no problem with scoping. +import CheckedArithmetic +import CheckedArithmetic: @checked, @check, acc, accumulatortype +using Pkg, Test, Dates + +Pkg.test("CheckedArithmeticCore") @test isempty(detect_ambiguities(CheckedArithmetic, Base, Core)) @@ -34,8 +39,12 @@ end @test_throws OverflowError @checked(0x10*0x10) @test @checked(7 ÷ 2) === 3 @test_throws DivideError @checked(typemin(Int8)÷Int8(-1)) + @test @checked(div(0x7, 0x2)) === 0x3 + @test_throws DivideError @checked(div(typemin(Int16), Int16(-1))) @test @checked(rem(typemin(Int8), Int8(-1))) === Int8(0) @test_throws DivideError @checked(rem(typemax(Int8), Int8(0))) + @test @checked(typemin(Int16) % Int16(-1)) === Int16(0) + @test_throws DivideError @checked(typemax(Int16) % Int16(0)) @test @checked(fld(typemax(Int8), Int8(-1))) === -typemax(Int8) @test_throws DivideError @checked(fld(typemin(Int8), Int8(-1))) @test @checked(mod(typemax(Int8), Int8(1))) === Int8(0) @@ -76,8 +85,8 @@ end s2 = copy(s + s) return 1 - s2 end - @test_throws ErrorException @check diff2from1(1//3) - @test (@check diff2from1(1//3) atol=1e-12) == 1//3 + @test_throws ErrorException @check diff2from1(Rational{Int64}(1, 3)) + @test (@check diff2from1(Rational{Int64}(1, 3)) atol=1e-12) == 1//3 end @testset "acc" begin