diff --git a/base/bool.jl b/base/bool.jl index c867ede02515d..98081d1693e95 100644 --- a/base/bool.jl +++ b/base/bool.jl @@ -64,6 +64,8 @@ cld(x::Bool, y::Bool) = div(x,y) rem(x::Bool, y::Bool) = y ? false : throw(DivideError()) mod(x::Bool, y::Bool) = rem(x,y) +promote_op{T}(op::Type{T}, ::Type{Bool}) = T # to fix ambiguity +promote_op(op, ::Type{Bool}) = typeof(op(true)) promote_op(op, ::Type{Bool}, ::Type{Bool}) = typeof(op(true, true)) promote_op(::typeof(^), ::Type{Bool}, ::Type{Bool}) = Bool promote_op{T<:Integer}(::typeof(^), ::Type{Bool}, ::Type{T}) = Bool diff --git a/base/nullable.jl b/base/nullable.jl index 310d0a5473731..2458129022a17 100644 --- a/base/nullable.jl +++ b/base/nullable.jl @@ -57,18 +57,6 @@ end isnull(x::Nullable) = x.isnull -function isequal(x::Nullable, y::Nullable) - if x.isnull && y.isnull - return true - elseif x.isnull || y.isnull - return false - else - return isequal(x.value, y.value) - end -end - -==(x::Nullable, y::Nullable) = throw(NullException()) - const nullablehash_seed = UInt === UInt64 ? 0x932e0143e51d0171 : 0xe51d0171 function hash(x::Nullable, h::UInt) @@ -78,3 +66,91 @@ function hash(x::Nullable, h::UInt) return hash(x.value, h + nullablehash_seed) end end + + +## Operators + +""" + null_safe_op(f::Any, ::Type)::Bool + null_safe_op(f::Any, ::Type, ::Type)::Bool + +Returns whether an operation `f` can safely be applied to any value of the passed type(s). +Returns `false` by default. + +Custom types should implement methods for some or all operations `f` when applicable: +returning `true` means that the operation may be called on any value without +throwing an error. In particular, this means that the operation can be applied on +the whole domain of the type *and on uninitialized objects*. Though returning invalid or +nonsensical results is not a problem. As a general rule, these proporties are only true for +unchecked operations on `isbits` types. + +Types declared as safe can benefit from higher performance for operations on nullable: by +always computing the result even for null values, a branch is avoided, which helps +vectorization. +""" +null_safe_op(f::Any, ::Type) = false +null_safe_op(f::Any, ::Type, ::Type) = false + +typealias UncheckedTypes Union{Checked.SignedInt, Checked.UnsignedInt} + +# Unary operators + +for op in (:+, :-, :~) + @eval begin + null_safe_op{T<:UncheckedTypes}(::typeof($op), ::Type{T}) = true + end +end + +null_safe_op(::typeof(!), ::Type{Bool}) = true + +for op in (:+, :-, :!, :~) + @eval begin + @inline function $op{S}(x::Nullable{S}) + R = promote_op($op, S) + if null_safe_op($op, S) + Nullable{R}($op(x.value), x.isnull) + else + x.isnull ? Nullable{R}() : + Nullable{R}($op(x.value)) + end + end + end +end + +# Binary operators + +# Note this list does not include ^, ÷ and % +# Operations between signed and unsigned types are not safe: promotion to unsigned +# gives an InexactError for negative numbers +for op in (:+, :-, :*, :/, :&, :|, :<<, :>>, :(>>>), + :(==), :<, :>, :<=, :>=) + @eval begin + null_safe_op{S<:Checked.SignedInt, + T<:Checked.SignedInt}(::typeof($op), ::Type{S}, ::Type{T}) = true + null_safe_op{S<:Checked.UnsignedInt, + T<:Checked.UnsignedInt}(::typeof($op), ::Type{S}, ::Type{T}) = true + end +end + +for op in (:+, :-, :*, :/, :%, :÷, :&, :|, :^, :<<, :>>, :(>>>), + :(==), :<, :>, :<=, :>=) + @eval begin + @inline function $op{S,T}(x::Nullable{S}, y::Nullable{T}) + R = promote_op($op, S, T) + if null_safe_op($op, S, T) + Nullable{R}($op(x.value, y.value), x.isnull | y.isnull) + else + (x.isnull | y.isnull) ? Nullable{R}() : + Nullable{R}($op(x.value, y.value)) + end + end + end +end + +@inline function isequal{S,T}(x::Nullable{S}, y::Nullable{T}) + if null_safe_op(isequal, S, T) + (x.isnull & y.isnull) | ((!x.isnull & !y.isnull) & isequal(x.value, y.value)) + else + (x.isnull & y.isnull) || ((!x.isnull & !y.isnull) && isequal(x.value, y.value)) + end +end diff --git a/test/nullable.jl b/test/nullable.jl index 3d4d0b3a11ac2..7371936924e9f 100644 --- a/test/nullable.jl +++ b/test/nullable.jl @@ -243,6 +243,160 @@ for T in types @test hash(x3) != hash(x4) end + +## operators + +srand(1) + +# check for fast path (null-safe combinations of types) +for S in Union{Base.Checked.SignedInt, Base.Checked.UnsignedInt}.types, + T in Union{Base.Checked.SignedInt, Base.Checked.UnsignedInt}.types + # mixing signed and unsigned types is unsafe (slow path tested below) + ((S <: Signed) $ (T <: Signed)) && continue + + u0 = zero(S) + u1 = one(S) + u2 = rand(S) + + v0 = zero(T) + v1 = one(T) + v2 = rand(T) + # unary operators + for op in (+, -, ~) + @test op(Nullable(u0)) === Nullable(op(u0)) + @test op(Nullable(u1)) === Nullable(op(u1)) + @test op(Nullable(u2)) === Nullable(op(u2)) + @test op(Nullable(u0, true)) === Nullable(op(u0), true) + end + + for u in (u0, u1, u2), v in (v0, v1, v2) + # safe binary operators: === checks that the fast-path was taken (no branch) + for op in (+, -, *, /, &, |, >>, <<, >>>, + <, >, <=, >=) + @test op(Nullable(u), Nullable(v)) === Nullable(op(u, v)) + @test op(Nullable(u, true), Nullable(v, true)) === Nullable(op(u, v), true) + @test op(Nullable(u), Nullable(v, true)) === Nullable(op(u, v), true) + @test op(Nullable(u, true), Nullable(v)) === Nullable(op(u, v), true) + end + + # unsafe binary operators: we cannot use === as the underlying value is undefined + # ^ + if S <: Integer + @test_throws DomainError Nullable(u)^Nullable(-signed(one(v))) + end + @test isequal(Nullable(u)^Nullable(2*one(T)), Nullable(u^(2*one(T)))) + R = Base.promote_op(^, S, T) + x = Nullable(u, true)^Nullable(-abs(v), true) + @test isnull(x) && eltype(x) === R + x = Nullable(u, false)^Nullable(-abs(v), true) + @test isnull(x) && eltype(x) === R + x = Nullable(u, true)^Nullable(-abs(v), false) + @test isnull(x) && eltype(x) === R + + # ÷ and % + for op in (÷, %) + if T <: Integer && v == 0 + @test_throws DivideError op(Nullable(u), Nullable(v)) + else + @test isequal(op(Nullable(u), Nullable(v)), Nullable(op(u, v))) + end + R = Base.promote_op(op, S, T) + x = op(Nullable(u, true), Nullable(v, true)) + @test isa(x, Nullable{R}) && isnull(x) + x = op(Nullable(u, false), Nullable(v, true)) + @test isa(x, Nullable{R}) && isnull(x) + x = op(Nullable(u, true), Nullable(v, false)) + @test isa(x, Nullable{R}) && isnull(x) + end + end +end + +@test !Nullable(true) === Nullable(false) +@test !Nullable(false) === Nullable(true) +@test !(Nullable(true, true)) === Nullable(false, true) +@test !(Nullable(false, true)) === Nullable(true, true) + +# test all types (including null-unsafe types and combinations of types) +for S in Union{Base.Checked.SignedInt, Base.Checked.UnsignedInt, BigInt, BigFloat}.types, + T in Union{Base.Checked.SignedInt, Base.Checked.UnsignedInt, BigInt, BigFloat}.types + u0 = zero(S) + u1 = one(S) + u2 = S <: Union{BigInt, BigFloat} ? S(rand(Int128)) : rand(S) + + v0 = zero(T) + v1 = one(T) + v2 = T <: Union{BigInt, BigFloat} ? T(rand(Int128)) : rand(T) + + v2 > 5 && (v2 = T(5)) # Work around issue #16989 + + # unary operators + for op in (+, -, ~) # ! + T <: BigFloat && op == (~) && continue + R = Base.promote_op(op, T) + x = op(Nullable(v0)) + @test isa(x, Nullable{R}) && isequal(x, Nullable(op(v0))) + x = op(Nullable(v1)) + @test isa(x, Nullable{R}) && isequal(x, Nullable(op(v1))) + x = op(Nullable(v2)) + @test isa(x, Nullable{R}) && isequal(x, Nullable(op(v2))) + x = op(Nullable(v0, true)) + @test isa(x, Nullable{R}) && isnull(x) + x = op(Nullable{R}()) + @test isa(x, Nullable{R}) && isnull(x) + end + + for u in (u0, u1, u2), v in (v0, v1, v2) + # safe binary operators + for op in (+, -, *, /, &, |, >>, <<, >>>, + <, >, <=, >=) + (T <: BigFloat || S <: BigFloat) && op in (&, |, >>, <<, >>>) && continue + if S <: Unsigned || T <: Unsigned + @test isequal(op(Nullable(abs(u)), Nullable(abs(v))), Nullable(op(abs(u), abs(v)))) + else + @test isequal(op(Nullable(u), Nullable(v)), Nullable(op(u, v))) + end + R = Base.promote_op(op, S, T) + x = op(Nullable(u, true), Nullable(v, true)) + @test isa(x, Nullable{R}) && isnull(x) + x = op(Nullable(u), Nullable(v, true)) + @test isa(x, Nullable{R}) && isnull(x) + x = op(Nullable(u, true), Nullable(v)) + @test isa(x, Nullable{R}) && isnull(x) + end + + # unsafe binary operators + # ^ + if S <: Integer && !(T <: BigFloat) + @test_throws DomainError Nullable(u)^Nullable(-signed(one(v))) + end + @test isequal(Nullable(u)^Nullable(2*one(T)), Nullable(u^(2*one(T)))) + R = Base.promote_op(^, S, T) + x = Nullable(u, true)^Nullable(-abs(v), true) + @test isnull(x) && eltype(x) === R + x = Nullable(u, false)^Nullable(-abs(v), true) + @test isnull(x) && eltype(x) === R + x = Nullable(u, true)^Nullable(-abs(v), false) + @test isnull(x) && eltype(x) === R + + # ÷ and % + for op in (÷, %) + if S <: Integer && T <: Integer && v == 0 + @test_throws DivideError op(Nullable(u), Nullable(v)) + else + @test isequal(op(Nullable(u), Nullable(v)), Nullable(op(u, v))) + end + R = Base.promote_op(op, S, T) + x = op(Nullable(u, true), Nullable(v, true)) + @test isnull(x) && eltype(x) === R + x = op(Nullable(u, false), Nullable(v, true)) + @test isnull(x) && eltype(x) === R + x = op(Nullable(u, true), Nullable(v, false)) + @test isnull(x) && eltype(x) === R + end + end +end + + type TestNType{T} v::Nullable{T} end