From cc467b4419f6208ceb201306419479691ac1be77 Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Fri, 23 Oct 2026 17:21:21 +0200 Subject: [PATCH 1/3] Support arrays of isbits unions in JLArrays JLArray accepted element types like `Union{Int, Nothing}`, but stored no selector bytes and wrapped its memory as a plain `Array`, so reading such an array back returned wrong values and kernels failed. Store the elements and selector bytes like `Array` does: elements are padded to `aligned_sizeof(T)`, and one selector byte per element follows the data. Arrays track the offset of their first selector byte, which derived arrays advance by element, so that views of unions of singletons, which have no data, still find their own selectors. Copies to, from and between JLArrays copy the selector bytes too, and the device array loads and stores elements according to them. The host algorithms that ran on a wrapped `Array` use the device array for these element types instead. --- lib/JLArrays/src/JLArrays.jl | 7 ++ lib/JLArrays/src/array.jl | 114 ++++++++++++++++--------------- lib/JLArrays/src/device/array.jl | 54 ++++++++++++++- lib/JLArrays/src/mapreduce.jl | 8 ++- 4 files changed, 125 insertions(+), 58 deletions(-) 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 From af63dae4a22f9f4c03b59aac257a7297eb4aac15 Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Fri, 23 Oct 2026 17:21:21 +0200 Subject: [PATCH 2/3] Test arrays of isbits unions in the testsuite Every back-end now supports arrays of isbits unions, but the testsuite only covered them in passing, and only where the back-end supported them. Add tests for them, based on the bugs that came up implementing them in the back-ends: views whose selector bytes were taken from their parent, views of unions of singletons (which have no data), elements whose size is not a multiple of their alignment, `fill!` fast paths that skip the selector bytes, and `resize!` of views. --- test/testsuite.jl | 1 + test/testsuite/unions.jl | 232 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 233 insertions(+) create mode 100644 test/testsuite/unions.jl 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/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 From 04ff55e1741668746f2ae110c23518b379e42707 Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Fri, 23 Oct 2026 17:21:21 +0200 Subject: [PATCH 3/3] Require isbits-union support in the existing tests The `reinterpret` and `any`/`all` tests of isbits-union arrays only ran when the back-end could construct one. Run them unconditionally now that the testsuite requires support for such arrays. --- test/testsuite/base.jl | 12 +----------- test/testsuite/findall.jl | 16 ++++------------ 2 files changed, 5 insertions(+), 23 deletions(-) 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