diff --git a/lib/JLArrays/src/JLArrays.jl b/lib/JLArrays/src/JLArrays.jl index c6caf8524..ff9d7c3ec 100644 --- a/lib/JLArrays/src/JLArrays.jl +++ b/lib/JLArrays/src/JLArrays.jl @@ -22,6 +22,13 @@ import KernelAbstractions import KernelAbstractions: POCL end +# the size of an element in an array, including padding up to the element's alignment +@static if VERSION < v"1.11" + @generated aligned_sizeof(::Type{T}) where {T} = :($(Base.aligned_sizeof(T))) +else + import Base: aligned_sizeof +end + module AS const Generic = 0 diff --git a/lib/JLArrays/src/array.jl b/lib/JLArrays/src/array.jl index d47184a8b..d3f708ae9 100644 --- a/lib/JLArrays/src/array.jl +++ b/lib/JLArrays/src/array.jl @@ -14,30 +14,35 @@ mutable struct JLArray{T, N} <: AbstractGPUArray{T, N} offset::Int # offset of the data in the buffer, in bytes + # arrays of isbits unions keep one selector byte per element past the data, like + # `Array` does: this is the offset of the array's first selector byte in the buffer + tagoffset::Int + dims::Dims{N} # allocating constructor function JLArray{T,N}(::UndefInitializer, dims::Dims{N}) where {T,N} check_eltype(T) - maxsize = prod(dims) * sizeof(T) + datasize = prod(dims) * aligned_sizeof(T) + bufsize = Base.isbitsunion(T) ? datasize + prod(dims) : datasize - ref = GPUArrays.cached_alloc((JLArray, maxsize)) do - data = Vector{UInt8}(undef, maxsize) + ref = GPUArrays.cached_alloc((JLArray, bufsize)) do + data = Vector{UInt8}(undef, bufsize) DataRef(data) do data resize!(data, 0) end end - obj = new{T, N}(ref, 0, dims) + obj = new{T, N}(ref, 0, datasize, dims) finalizer(unsafe_free!, obj) return obj end # low-level constructor for wrapping existing data - function JLArray{T,N}(ref::DataRef{Vector{UInt8}}, dims::Dims{N}; - offset::Int=0) where {T,N} + function JLArray{T,N}(ref::DataRef{Vector{UInt8}}, dims::Dims{N}; offset::Int=0, + tagoffset::Int=offset + prod(dims) * aligned_sizeof(T)) where {T,N} check_eltype(T) - obj = new{T,N}(ref, offset, dims) + obj = new{T,N}(ref, offset, tagoffset, dims) finalizer(unsafe_free!, obj) return obj end @@ -57,10 +62,25 @@ function typed_data(x::JLArray{T}) where {T} unsafe_wrap(Array, pointer(x), x.dims) end +# the selector byte of element `i` of an array of isbits unions +typetagdata(a::JLArray, i=1) = pointer(a.data[]) + a.tagoffset + i - 1 +@static if VERSION >= v"1.11.0-DEV.753" + function typetagdata(a::Array, i=1) + # for memory of isbits unions, the reference holds the offset of the data in elements + ptr_or_offset = Int(a.ref.ptr_or_offset) + @ccall(jl_genericmemory_typetagdata(a.ref.mem::Any)::Ptr{UInt8}) + ptr_or_offset + i - 1 + end +else + typetagdata(a::Array, i=1) = ccall(:jl_array_typetagdata, Ptr{UInt8}, (Any,), a) + i - 1 +end + function GPUArrays.derive(::Type{T}, a::JLArray, dims::Dims{N}, offset::Int) where {T,N} ref = copy(a.data) - offset = a.offset + offset * sizeof(T) - JLArray{T,N}(ref, dims; offset) + # arrays of isbits unions are only derived as arrays of the same union + # (`reinterpret` rejects them), so their selector bytes advance by element + tagoffset = a.tagoffset + offset + offset = a.offset + offset * aligned_sizeof(T) + JLArray{T,N}(ref, dims; offset, tagoffset) end @@ -120,7 +140,7 @@ StridedJLVecOrMat{T} = Union{StridedJLVector{T}, StridedJLMatrix{T}} Base.pointer(x::JLArray{T}) where {T} = convert(Ptr{T}, pointer(x.data[])) + x.offset @inline function Base.pointer(x::JLArray{T}, i::Integer) where T - pointer(x) + (i - 1) * sizeof(T) + pointer(x) + (i - 1) * aligned_sizeof(T) end # anything that's (secretly) backed by a JLArray @@ -132,7 +152,7 @@ AnyJLVecOrMat{T} = Union{AnyJLVector{T}, AnyJLMatrix{T}} ## array interface -Base.elsize(::Type{<:JLArray{T}}) where {T} = sizeof(T) +Base.elsize(::Type{<:JLArray{T}}) where {T} = aligned_sizeof(T) Base.size(x::JLArray) = x.dims Base.sizeof(x::JLArray) = Base.elsize(x) * length(x) @@ -174,7 +194,7 @@ Adapt.adapt_storage(::Type{Array}, xs::JLArray) = convert(Array, xs) # adapt for the device @static if !isdefined(JLArrays.KernelAbstractions, :POCL) # KA v0.9 Adapt.adapt_storage(::Adaptor, x::JLArray{T,N}) where {T,N} = - JLDeviceArray{T,N}(x.data[], x.offset, x.dims) + JLDeviceArray{T,N}(x.data[], x.offset, x.tagoffset, x.dims) else function Adapt.adapt_storage(::Adaptor, x::JLArray{T,N}) where {T,N} arr = typed_data(x) @@ -189,54 +209,39 @@ Base.convert(::Type{T}, x::T) where T <: JLArray = x ## memory operations -function Base.copyto!(dest::Array{T}, d_offset::Integer, - source::DenseJLArray{T}, s_offset::Integer, - amount::Integer) where T - amount==0 && return dest - @boundscheck checkbounds(dest, d_offset) - @boundscheck checkbounds(dest, d_offset+amount-1) - @boundscheck checkbounds(source, s_offset) - @boundscheck checkbounds(source, s_offset+amount-1) - GC.@preserve dest source Base.unsafe_copyto!(pointer(dest, d_offset), - pointer(source, s_offset), amount) +# copy the elements, and the selector bytes of isbits unions +function unsafe_copy_elements!(dest, d_offset, source, s_offset, amount) + T = eltype(dest) + GC.@preserve dest source begin + Base.unsafe_copyto!(convert(Ptr{UInt8}, pointer(dest, d_offset)), + convert(Ptr{UInt8}, pointer(source, s_offset)), + amount * aligned_sizeof(T)) + if Base.isbitsunion(T) + Base.unsafe_copyto!(typetagdata(dest, d_offset), typetagdata(source, s_offset), + amount) + end + end return dest end -Base.copyto!(dest::Array{T}, source::DenseJLArray{T}) where {T} = - copyto!(dest, 1, source, 1, length(source)) - -function Base.copyto!(dest::DenseJLArray{T}, d_offset::Integer, - source::Array{T}, s_offset::Integer, - amount::Integer) where T - amount==0 && return dest - @boundscheck checkbounds(dest, d_offset) - @boundscheck checkbounds(dest, d_offset+amount-1) - @boundscheck checkbounds(source, s_offset) - @boundscheck checkbounds(source, s_offset+amount-1) - GC.@preserve dest source Base.unsafe_copyto!(pointer(dest, d_offset), - pointer(source, s_offset), amount) - return dest -end +for (D, S) in ((:Array, :DenseJLArray), (:DenseJLArray, :Array), (:DenseJLArray, :DenseJLArray)) + @eval begin + function Base.copyto!(dest::$D{T}, d_offset::Integer, + source::$S{T}, s_offset::Integer, + amount::Integer) where T + amount==0 && return dest + @boundscheck checkbounds(dest, d_offset) + @boundscheck checkbounds(dest, d_offset+amount-1) + @boundscheck checkbounds(source, s_offset) + @boundscheck checkbounds(source, s_offset+amount-1) + unsafe_copy_elements!(dest, d_offset, source, s_offset, amount) + end -Base.copyto!(dest::DenseJLArray{T}, source::Array{T}) where {T} = - copyto!(dest, 1, source, 1, length(source)) - -function Base.copyto!(dest::DenseJLArray{T}, d_offset::Integer, - source::DenseJLArray{T}, s_offset::Integer, - amount::Integer) where T - amount==0 && return dest - @boundscheck checkbounds(dest, d_offset) - @boundscheck checkbounds(dest, d_offset+amount-1) - @boundscheck checkbounds(source, s_offset) - @boundscheck checkbounds(source, s_offset+amount-1) - GC.@preserve dest source Base.unsafe_copyto!(pointer(dest, d_offset), - pointer(source, s_offset), amount) - return dest + Base.copyto!(dest::$D{T}, source::$S{T}) where {T} = + copyto!(dest, 1, source, 1, length(source)) + end end -Base.copyto!(dest::DenseJLArray{T}, source::DenseJLArray{T}) where {T} = - copyto!(dest, 1, source, 1, length(source)) - function Base.resize!(a::DenseJLVector{T}, nl::Integer) where {T} # JLArrays aren't performance critical, so simply allocate a new one # instead of duplicating the underlying data allocation from the ctor. @@ -249,6 +254,7 @@ function Base.resize!(a::DenseJLVector{T}, nl::Integer) where {T} a.data = copy(b.data) a.offset = b.offset + a.tagoffset = b.tagoffset a.dims = b.dims return a end diff --git a/lib/JLArrays/src/device/array.jl b/lib/JLArrays/src/device/array.jl index 457697782..f6fa65970 100644 --- a/lib/JLArrays/src/device/array.jl +++ b/lib/JLArrays/src/device/array.jl @@ -17,10 +17,11 @@ Adapt.adapt_structure(to::Adaptor, r::Base.RefValue) = JlRefValue(adapt(to, r[]) struct JLDeviceArray{T, N} <: AbstractDeviceArray{T, N} data::Vector{UInt8} offset::Int + tagoffset::Int # see `JLArray` dims::Dims{N} end - Base.elsize(::Type{<:JLDeviceArray{T}}) where {T} = sizeof(T) + Base.elsize(::Type{<:JLDeviceArray{T}}) where {T} = aligned_sizeof(T) Base.size(x::JLDeviceArray) = x.dims Base.sizeof(x::JLDeviceArray) = Base.elsize(x) * length(x) @@ -33,6 +34,53 @@ Adapt.adapt_structure(to::Adaptor, r::Base.RefValue) = JlRefValue(adapt(to, r[]) unsafe_wrap(Array, pointer(x), x.dims) end - @inline Base.getindex(A::JLDeviceArray, index::Integer) = getindex(typed_data(A), index) - @inline Base.setindex!(A::JLDeviceArray, x, index::Integer) = setindex!(typed_data(A), x, index) + @inline function Base.getindex(A::JLDeviceArray{T}, index::Integer) where {T} + if Base.isbitsunion(T) + @boundscheck checkbounds(A, index) + union_load(A, index) + else + getindex(typed_data(A), index) + end + end + @inline function Base.setindex!(A::JLDeviceArray{T}, x, index::Integer) where {T} + if Base.isbitsunion(T) + @boundscheck checkbounds(A, index) + union_store!(A, convert(T, x)::T, index) + return A + else + setindex!(typed_data(A), x, index) + end + end + + # elements of isbits unions are stored as the member type that their selector byte + # indexes in `Base.uniontypes(T)` + + @generated function union_load(A::JLDeviceArray{T}, index::Integer) where {T} + ex = :(error("invalid selector byte")) + for (sel, U) in Iterators.reverse(enumerate(Base.uniontypes(T))) + val = Base.issingletontype(U) ? U.instance : :(unsafe_load(convert(Ptr{$U}, ptr))) + ex = :(selector == $(UInt8(sel - 1)) ? $val : $ex) + end + quote + data = A.data + selector = @inbounds data[A.tagoffset + index] + GC.@preserve data begin + ptr = pointer(data) + A.offset + (index - 1) * $(Base.aligned_sizeof(T)) + $ex + end + end + end + + @generated function union_store!(A::JLDeviceArray{T}, x, index::Integer) where {T} + sel = findfirst(==(x), Base.uniontypes(T)) + quote + data = A.data + @inbounds data[A.tagoffset + index] = $(UInt8(sel - 1)) + GC.@preserve data begin + ptr = pointer(data) + A.offset + (index - 1) * $(Base.aligned_sizeof(T)) + $(Base.issingletontype(x) ? nothing : :(unsafe_store!(convert(Ptr{$x}, ptr), x))) + end + return + end + end end diff --git a/lib/JLArrays/src/mapreduce.jl b/lib/JLArrays/src/mapreduce.jl index 5d5ccff58..c1725e1bb 100644 --- a/lib/JLArrays/src/mapreduce.jl +++ b/lib/JLArrays/src/mapreduce.jl @@ -1,7 +1,13 @@ # reductions struct ArrayNoCopy end -Adapt.adapt_storage(::ArrayNoCopy, x::JLArray) = typed_data(x) +# (an `Array` can't wrap the storage of isbits unions, whose selector bytes are elsewhere) +Adapt.adapt_storage(::ArrayNoCopy, x::JLArray{T}) where {T} = + Base.isbitsunion(T) ? jlconvert(x) : typed_data(x) +@static if !isdefined(KernelAbstractions, :POCL) # KA v0.9 + # (AcceleratedKernels picks its host algorithms by the back-end) + KernelAbstractions.get_backend(::JLDeviceArray) = KernelAbstractions.CPU() +end # GPUArrays reduces with AcceleratedKernels, whose kernels do not run on JLArrays' back-end. The # reference implementation runs AcceleratedKernels' host algorithms on the arrays' storage diff --git a/test/testsuite.jl b/test/testsuite.jl index 59dcbe05b..93beae865 100644 --- a/test/testsuite.jl +++ b/test/testsuite.jl @@ -140,6 +140,7 @@ include("testsuite/statistics.jl") include("testsuite/sparse.jl") include("testsuite/alloc_cache.jl") include("testsuite/jld2ext.jl") +include("testsuite/unions.jl") """ Runs the entire GPUArrays test suite on array type `AT` diff --git a/test/testsuite/base.jl b/test/testsuite/base.jl index 77e92135d..1704eb463 100644 --- a/test/testsuite/base.jl +++ b/test/testsuite/base.jl @@ -607,17 +607,7 @@ end end if AT <: AbstractGPUArray - # XXX: use a predicate function? - supports_bitsunion = try - AT([1,nothing]) - true - catch - false - end - - if supports_bitsunion - @test_throws "cannot reinterpret an `Union{Nothing, Int64}` array to `Float64`, because not all types are bitstypes" reinterpret(Float64, AT([1,nothing])) - end + @test_throws "cannot reinterpret an `Union{Nothing, Int64}` array to `Float64`, because not all types are bitstypes" reinterpret(Float64, AT([1,nothing])) @test_throws "cannot reinterpret a zero-dimensional `Float32` array to `Int128` which is of a different size" reinterpret(Int128, AT(fill(1f0))) diff --git a/test/testsuite/findall.jl b/test/testsuite/findall.jl index cad14c294..43c6d55e6 100644 --- a/test/testsuite/findall.jl +++ b/test/testsuite/findall.jl @@ -110,17 +110,9 @@ end @test isequal(any(f, AT(xs)), any(f, xs)) @test isequal(all(f, AT(xs)), all(f, xs)) end - # ... also stored in the array, where the back-end supports isbits unions - supports_unions = try - Array(AT(Union{Missing,Bool}[missing, true]) .| false) - true - catch - false - end - if supports_unions - for (a, b) in ((missing, true), (missing, false), (true, true)) - @test isequal(any(AT(Union{Missing,Bool}[a, b])), any([a, b])) - @test isequal(all(AT(Union{Missing,Bool}[a, b])), all([a, b])) - end + # ... also stored in the array + for (a, b) in ((missing, true), (missing, false), (true, true)) + @test isequal(any(AT(Union{Missing,Bool}[a, b])), any([a, b])) + @test isequal(all(AT(Union{Missing,Bool}[a, b])), all([a, b])) end end diff --git a/test/testsuite/unions.jl b/test/testsuite/unions.jl new file mode 100644 index 000000000..d7162f0b4 --- /dev/null +++ b/test/testsuite/unions.jl @@ -0,0 +1,232 @@ +# Arrays whose element type is an isbits union (`Union{Int32, Nothing}`, ...). Like `Array`, +# they store each element inline as one of the union's members, with a selector byte per +# element that says which one. + +# a struct with a union field is stored inline too, but with the selector inside the element +struct UnionField + x::Union{Int32, Float32} +end + +# compare elements by identity, as `==` propagates `missing` and `isequal` ignores types +# (`isequal(Int8(1), 1f0)`) +identical(a, b) = size(a) == size(b) && all(map(===, a, b)) + +@kernel function union_increment!(x) + i = @index(Global, Linear) + val = x[i] + if val isa Integer + x[i] = val + one(val) + end +end +increment_integers!(x) = union_increment!(get_backend(x))(x; ndrange=length(x)) + +@kernel function union_isequal!(ok, x, y) + i = @index(Global, Linear) + ok[i] = x[i] === y[i] +end + +# swap the members of `Union{Int64, NTuple{3, Int32}}`, whose elements are padded +@kernel function union_swap!(x) + i = @index(Global, Linear) + val = x[i] + x[i] = val isa Int64 ? (Int32(val), Int32(0), Int32(0)) : Int64(sum(val)) +end + +@kernel function union_field!(x) + i = @index(Global, Linear) + x[i] = UnionField(isodd(i) ? Int32(i) : Float32(i)) +end + +@testsuite "unions" (AT, eltypes)->begin + @testset "copies" begin + for a in (Union{Int32, Nothing}[1, nothing, 3, nothing, 5], + # members of different sizes and alignments + Union{Int8, Float32}[Int8(1), 2f0, Int8(3), 4f0, Int8(5)], + Union{NTuple{3, Int8}, Int16}[Int8.((1, 2, 3)), Int16(4), Int8.((5, 6, 7)), + Int16(8), Int8.((9, 10, 11))], + # elements of 12 bytes, padded to 16 + Union{Int64, NTuple{3, Int32}}[1, Int32.((2, 3, 4)), 5, Int32.((6, 7, 8)), 9], + # only singletons, so selector bytes but no data + Union{Nothing, Missing}[nothing, missing, missing, nothing, missing], + # nested unions are flattened + Union{Union{Missing, Nothing}, Int32}[0, missing, nothing, 3, missing]) + T = eltype(a) + b = AT(a) + @test b isa AT{T, 1} + @test identical(Array(b), a) + @test identical(Array(copy(b)), a) + @test identical(Array(reshape(b, 1, 5)), reshape(a, 1, 5)) + if VERSION >= v"1.11" # (`sizeof` of such an `Array` used to ignore padding) + @test sizeof(b) == sizeof(a) + end + + c = similar(b) + copyto!(c, b) + @test identical(Array(c), a) + copyto!(c, 2, b, 1, 3) + @test identical(Array(c), a[[1, 1, 2, 3, 5]]) + + # partial copies to and from the host + copyto!(c, 3, a, 4, 2) + @test identical(Array(c), a[[1, 1, 4, 5, 5]]) + d = similar(a) + copyto!(d, 2, b, 3, 2) + @test identical(d[2:3], a[3:4]) + + # broadcasting reads and writes the elements too + @test identical(Array(similar(b) .= b), a) + @test identical(Array(fill!(similar(b), a[2])), fill(a[2], 5)) + end + end + + @testset "views" begin + # the parent's first selector bytes differ from the view's + a = Union{Int32, Nothing}[nothing, nothing, 1, nothing, 3, 5] + b = AT(a) + v = view(b, 3:5) + @test identical(Array(v), a[3:5]) + @test identical(Array(copy(v)), a[3:5]) + @test identical(Array(something.(v, Int32(0))), Int32[1, 0, 3]) + @test @allowscalar v[1] === Int32(1) + + # writes through a view don't touch the parent outside of it + increment_integers!(v) + @test identical(Array(b), Union{Int32, Nothing}[nothing, nothing, 2, nothing, 4, 5]) + v .= something.(v, Int32(0)) + @test identical(Array(b), Union{Int32, Nothing}[nothing, nothing, 2, 0, 4, 5]) + fill!(view(b, 4:5), nothing) + @test identical(Array(b), Union{Int32, Nothing}[nothing, nothing, 2, nothing, nothing, 5]) + copyto!(view(b, 1:2), Union{Int32, Nothing}[7, nothing]) + @test identical(Array(b), Union{Int32, Nothing}[7, nothing, 2, nothing, nothing, 5]) + copyto!(view(b, 4:5), view(b, 1:2)) + @test identical(Array(b), Union{Int32, Nothing}[7, nothing, 2, 7, nothing, 5]) + view(b, 2:6) .= view(b, 1:5) # overlapping, so the source is copied first + @test identical(Array(b), Union{Int32, Nothing}[7, 7, nothing, 2, 7, nothing]) + copyto!(b, Union{Int32, Nothing}[7, nothing, 2, 7, nothing, 5]) + + # non-contiguous views, views of views, and reshaped views + w = view(b, 1:2:5) + w .= ifelse.(w .=== nothing, Int32(0), nothing) + @test identical(Array(b), Union{Int32, Nothing}[nothing, nothing, nothing, 7, 0, 5]) + @test identical(Array(view(view(b, 2:6), 3:5)), Union{Int32, Nothing}[7, 0, 5]) + increment_integers!(view(view(b, 2:6), 3:5)) + @test identical(Array(b), Union{Int32, Nothing}[nothing, nothing, nothing, 8, 1, 6]) + @test identical(Array(reshape(view(b, 3:6), 2, 2)), + Union{Int32, Nothing}[nothing 1; 8 6]) + + # views of unions of singletons have no byte offset, only one in the selector bytes + c = AT([nothing, nothing, missing, missing]) + @test identical(Array(view(c, 3:4)), [missing, missing]) + @test identical(Array(copy(view(c, 2:3))), [nothing, missing]) + ok = AT(fill(false, 2)) + union_isequal!(get_backend(ok))(ok, view(c, 3:4), AT([missing, missing]); ndrange=2) + @test all(Array(ok)) + fill!(view(c, 2:3), missing) + @test identical(Array(c), [nothing, missing, missing, missing]) + view(c, 2:4) .= view(c, 1:3) + @test identical(Array(c), [nothing, nothing, missing, missing]) + end + + @testset "kernels" begin + a = AT(Union{Int32, Nothing}[1, nothing, 3]) + increment_integers!(a) + @test identical(Array(a), Union{Int32, Nothing}[2, nothing, 4]) + + b = AT(Union{Union{Missing, Nothing}, Int32}[0, missing, nothing]) + increment_integers!(b) + @test identical(Array(b), Union{Union{Missing, Nothing}, Int32}[1, missing, nothing]) + + c = AT(Union{Int64, NTuple{3, Int32}}[1, Int32.((2, 3, 4)), 5]) + union_swap!(get_backend(c))(c; ndrange=length(c)) + @test identical(Array(c), Union{Int64, NTuple{3, Int32}}[Int32.((1, 0, 0)), 9, Int32.((5, 0, 0))]) + union_swap!(get_backend(c))(view(c, 2:3); ndrange=2) + @test identical(Array(c), Union{Int64, NTuple{3, Int32}}[Int32.((1, 0, 0)), Int32.((9, 0, 0)), 5]) + + d = AT{UnionField}(undef, 4) + union_field!(get_backend(d))(d; ndrange=length(d)) + @test Array(d) == [UnionField(Int32(1)), UnionField(2f0), + UnionField(Int32(3)), UnionField(4f0)] + @test Array(map(u -> u.x isa Int32, d)) == [true, false, true, false] + end + + @testset "fill!" begin + # also for members that could be filled with a memset, which would skip the selectors + for (T, xs) in ((Union{Int8, UInt8}, (Int8(-1), 0xff)), + (Union{Int16, UInt16}, (Int16(-1), 0xffff)), + (Union{Int32, Float32}, (Int32(1), 2f0)), + (Union{Int8, Int32}, (Int8(1), Int32(2))), + (Union{Nothing, Missing}, (nothing, missing))) + b = AT{T}(undef, 3) + for x in xs + fill!(b, x) + @test identical(Array(b), T[x, x, x]) + end + end + end + + @testset "array operations" begin + x = AT(Union{Missing, Bool}[missing, true, false]) + @test identical(Array(x .| false), Union{Missing, Bool}[missing, true, false]) + @test identical(Array(map(!, x)), Union{Missing, Bool}[missing, false, true]) + @test any(x) === true + @test all(x) === false + @test count(ismissing, x) == 1 + @test identical(Array(reverse(x)), Union{Missing, Bool}[false, true, missing]) + @test identical(Array(vcat(x, x)), Union{Missing, Bool}[missing, true, false, missing, true, false]) + @test identical(Array(x[2:3]), Union{Missing, Bool}[true, false]) + + y = AT(Union{Int32, Nothing}[1, nothing, 3, nothing]) + @test sum(v -> something(v, Int32(0)), y) == 4 + @test mapreduce(v -> something(v, Int32(10)), max, y) === Int32(10) + @test Array(sum(something.(reshape(y, 2, 2), Int32(0)); dims=1)) == Int32[1 3] + @test identical(Array(y[AT([4, 1])]), Union{Int32, Nothing}[nothing, 1]) + @test Array(findall(isnothing, y)) == [2, 4] + end + + @testset "scalar indexing" begin + a = AT(Union{Int32, Nothing}[1, nothing, 3]) + @allowscalar begin + @test a[1] === Int32(1) + @test a[2] === nothing + a[1] = nothing + a[2] = 2 + @test view(a, 2:3)[2] === Int32(3) + end + @test identical(Array(a), Union{Int32, Nothing}[nothing, 2, 3]) + + b = AT(Union{Nothing, Missing}[nothing, nothing]) + @allowscalar b[2] = missing + @test identical(Array(b), [nothing, missing]) + end + + @testset "resize!" begin + a = AT(Union{Int32, Nothing}[1, nothing, 3]) + resize!(a, 5) + @test length(a) == 5 + @test identical(Array(view(a, 1:3)), Union{Int32, Nothing}[1, nothing, 3]) + resize!(a, 2) + @test identical(Array(a), Union{Int32, Nothing}[1, nothing]) + + c = AT(Union{Nothing, Missing}[nothing, nothing, missing, missing]) + if AT <: AbstractGPUArray # (contiguous views are arrays of their own) + # a view gets its own memory, leaving the parent alone + b = AT(Union{Int32, Nothing}[nothing, nothing, 1, nothing, 3]) + v = view(b, 3:5) + resize!(v, 4) + @test identical(Array(view(v, 1:3)), Union{Int32, Nothing}[1, nothing, 3]) + resize!(v, 2) + @test identical(Array(v), Union{Int32, Nothing}[1, nothing]) + @test identical(Array(b), Union{Int32, Nothing}[nothing, nothing, 1, nothing, 3]) + + w = view(c, 3:4) + resize!(w, 3) + @test identical(Array(view(w, 1:2)), [missing, missing]) + resize!(w, 1) + @test identical(Array(w), [missing]) + end + resize!(c, 6) + @test identical(Array(view(c, 1:4)), [nothing, nothing, missing, missing]) + resize!(c, 1) + @test identical(Array(c), [nothing]) + end +end