From bf5096062be1c4e6786847aa2b4b815f145fc700 Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Sat, 24 May 2025 19:26:30 +0200 Subject: [PATCH 1/3] depend on Collects.jl, excise `collect_as` Fixes #81 --- Project.toml | 4 + docs/Project.toml | 2 + docs/src/index.md | 2 +- docs/src/reference.md | 3 - docs/src/usage.md | 14 ++-- src/FixedSizeArray.jl | 62 +++++++++++++- src/FixedSizeArrays.jl | 2 + src/collect_as.jl | 182 ++++++++--------------------------------- test/Project.toml | 2 + test/runtests.jl | 28 ++++++- 10 files changed, 139 insertions(+), 162 deletions(-) diff --git a/Project.toml b/Project.toml index ec1d15d..99b1d6d 100644 --- a/Project.toml +++ b/Project.toml @@ -3,6 +3,9 @@ uuid = "3821ddf9-e5b5-40d5-8e25-6813ab96b5e2" version = "0.1.0" authors = ["Mosè Giordano "] +[deps] +Collects = "08986516-18db-4a8b-8eaa-f5ef1828d8f1" + [weakdeps] Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" @@ -10,5 +13,6 @@ Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" RandomExt = "Random" [compat] +Collects = "1" Random = "1" julia = "1.10" diff --git a/docs/Project.toml b/docs/Project.toml index 86bd54f..28f4bc2 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,5 +1,6 @@ [deps] Changelog = "5217a498-cd5d-4ec6-b8c2-9b85a09b6e3e" +Collects = "08986516-18db-4a8b-8eaa-f5ef1828d8f1" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" FixedSizeArrays = "3821ddf9-e5b5-40d5-8e25-6813ab96b5e2" @@ -8,4 +9,5 @@ FixedSizeArrays = {path = ".."} [compat] Changelog = "1.1" +Collects = "1" Documenter = "1" diff --git a/docs/src/index.md b/docs/src/index.md index f44f764..1548ad1 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -5,7 +5,7 @@ FixedSizeArrays supports the [standard array interfaces](https://docs.julialang.org/en/v1/manual/interfaces/#man-interface-array), so things like broadcasting, matrix multiplication, other linear algebra operations, `similar`, `copyto!` or `map` should just work. Use the constructors to convert from other array types. -Use [`collect_as`](@ref) to convert from arbitrary iterators. +Use `collect_as` from the [Collects.jl](https://github.com/JuliaCollections/Collects.jl) package to convert from arbitrary iterators. ## Comparison with other array types diff --git a/docs/src/reference.md b/docs/src/reference.md index b15293b..de9998f 100644 --- a/docs/src/reference.md +++ b/docs/src/reference.md @@ -13,7 +13,6 @@ FixedSizeMatrix FixedSizeArrayDefault FixedSizeVectorDefault FixedSizeMatrixDefault -FixedSizeArrays.collect_as Base.parent BoundsErrorLight ``` @@ -29,6 +28,4 @@ This section gathers documentation of internal functionalities of `FixedSizeArra ```@docs FixedSizeArrays.with_stripped_type_parameters FixedSizeArrays.with_stripped_type_parameters_unchecked -FixedSizeArrays.collect_as_fsv -FixedSizeArrays.push!! ``` diff --git a/docs/src/usage.md b/docs/src/usage.md index e1dd1d6..b292785 100644 --- a/docs/src/usage.md +++ b/docs/src/usage.md @@ -64,32 +64,32 @@ To make it easier to refer to the concrete type `FixedSizeArray{T,N,Mem}` with t ## The `collect_as` utility The [array literals syntax](https://docs.julialang.org/en/v1/manual/arrays/#man-array-literals) `[A, B, C, ...]` to construct arrays is limited to `Base`'s `Array` and cannot be extended to custom array types. -`FixedSizeArrays.jl` provides a convenient function [`collect_as`](@ref) to overcome this limitation and construct `FixedSizeArray`s out of any iterable: +The package [Collects.jl](https://github.com/JuliaCollections/Collects.jl) provides a convenient function, `collect_as`, to overcome this limitation and construct `FixedSizeArray`s out of any iterable: ```jldoctest julia> iter = (i for i ∈ 7:9 if i≠8); -julia> using FixedSizeArrays - -julia> const ca = FixedSizeArrays.collect_as; +julia> using FixedSizeArrays, Collects -julia> ca(FixedSizeArray, iter) # construct from an arbitrary iterator +julia> collect_as(FixedSizeArray, iter) # construct from an arbitrary iterator 2-element FixedSizeArray{Int64, 1, Memory{Int64}}: 7 9 -julia> ca(FixedSizeArray{Float64}, iter) # construct from an arbitrary iterator while converting element type +julia> collect_as(FixedSizeArray{Float64}, iter) # construct from an arbitrary iterator while converting element type 2-element FixedSizeArray{Float64, 1, Memory{Float64}}: 7.0 9.0 -julia> ca(FixedSizeVectorDefault, (3.14, -4.2, 2.68)) # construct from a tuple +julia> collect_as(FixedSizeVectorDefault, (3.14, -4.2, 2.68)) # construct from a tuple 3-element FixedSizeArray{Float64, 1, Memory{Float64}}: 3.14 -4.2 2.68 ``` +See the Collects.jl Readme for more information. + ## `BoundsErrorLight` exception To facilitate the [escape analysis](https://en.wikipedia.org/wiki/Escape_analysis) of `FixedSizeArray`s, accessing an out-of-bound index of these arrays raises a [`BoundsErrorLight`](@ref) exception when possible. diff --git a/src/FixedSizeArray.jl b/src/FixedSizeArray.jl index ed93419..b6e0a16 100644 --- a/src/FixedSizeArray.jl +++ b/src/FixedSizeArray.jl @@ -259,6 +259,66 @@ function with_stripped_type_parameters_unchecked(::TypeParametersElementType, :: Val{s}() end +let + global with_stripped_type_parameters_unchecked + Disp = Union{ + Type{FixedSizeArray}, + Type{FixedSizeArray{T}} where {T}, + } + function with_stripped_type_parameters_unchecked(::TypeParametersElementType, ::Disp) + Val{FixedSizeArray}() + end +end + +let + global with_stripped_type_parameters_unchecked + Disp = Union{ + Type{FixedSizeArray{T, N} where {T}}, + Type{FixedSizeArray{T, N}} where {T}, + } where {N} + function with_stripped_type_parameters_unchecked(::TypeParametersElementType, ::Disp{N}) where {N} + s = FixedSizeArray{T, N::Int} where {T} + Val{s}() + end +end + +let + global with_stripped_type_parameters_unchecked + Disp = Type{FixedSizeArray{T, N, Mem} where {N}} where {T, Mem <: DenseVector{T}} + # `Base.@assume_effects :consistent` is a workaround for: + # https://github.com/JuliaLang/julia/issues/56966 + Base.@assume_effects :consistent function with_stripped_type_parameters_unchecked(::TypeParametersElementType, ::Disp{T, Mem}) where {T, Mem <: DenseVector{T}} + val = with_stripped_type_parameters(TypeParametersElementType(), Mem) + s = FixedSizeArray{T, N, val_parameter(val){T}} where {T, N} + Val{s}() + end +end + +let + global with_stripped_type_parameters_unchecked + Disp = Type{FixedSizeArray{T, N, Mem}} where {T, N, Mem <: DenseVector{T}} + # `Base.@assume_effects :consistent` is a workaround for: + # https://github.com/JuliaLang/julia/issues/56966 + Base.@assume_effects :consistent function with_stripped_type_parameters_unchecked(::TypeParametersElementType, ::Disp{T, N, Mem}) where {T, N, Mem <: DenseVector{T}} + val = with_stripped_type_parameters(TypeParametersElementType(), Mem) + s = FixedSizeArray{T, N::Int, val_parameter(val){T}} where {T} + Val{s}() + end +end + +for Mem ∈ (Vector, optional_memory...) + DispNoDim = Type{FixedSizeArray{T, N, Mem{T}} where {T, N}} + DispWithDim = Type{FixedSizeArray{T, N, Mem{T}} where {T}} where {N} + @eval function with_stripped_type_parameters_unchecked(::TypeParametersElementType, ::$DispNoDim) + s = FixedSizeArray{T, N, $Mem{T}} where {T, N} + Val{s}() + end + @eval function with_stripped_type_parameters_unchecked(::TypeParametersElementType, ::$DispWithDim{N}) where {N} + s = FixedSizeArray{T, N::Int, $Mem{T}} where {T} + Val{s}() + end +end + # `Base.@assume_effects :consistent` is a workaround for: # https://github.com/JuliaLang/julia/issues/56966 Base.@assume_effects :consistent function with_stripped_type_parameters_unchecked(::TypeParametersElementTypeAndDimensionality, ::Type{<:(FixedSizeArray{T, N, Mem} where {T, N})}) where {Mem} @@ -330,7 +390,7 @@ function (::Type{FSA})(src::AbstractArray) where {FSA <: FixedSizeArray} if !axes_are_one_based(axs) throw(DimensionMismatch("source array has a non-one-based indexing axis")) end - collect_as(FSA, src) + collect_as_haseltype(FSA, src) end # `copy`: avoid the `similar` and `copyto!` in the generic fallback diff --git a/src/FixedSizeArrays.jl b/src/FixedSizeArrays.jl index fd82b12..8ca0319 100644 --- a/src/FixedSizeArrays.jl +++ b/src/FixedSizeArrays.jl @@ -1,5 +1,7 @@ module FixedSizeArrays +using Collects: Collect, collect_as + include("BoundsErrorLight.jl") const default_underlying_storage_type = (@isdefined Memory) ? Memory : Vector diff --git a/src/collect_as.jl b/src/collect_as.jl index bdd274a..bedf78d 100644 --- a/src/collect_as.jl +++ b/src/collect_as.jl @@ -1,168 +1,56 @@ -export collect_as - -function throw_bottom_type() +function (::Collect)(::Type{<:(FixedSizeArray{E, N, Union{}} where {E, N})}, ::Any) throw(ArgumentError("`Union{}` not expected")) end -function eltype_is_known(::Type{Storage}) where {S, Storage <: AbstractVector{S}} - @isdefined S -end -function eltype_is_known(::Type{Storage}) where {Storage <: AbstractVector} - false -end - -function collect_as_storage_type_helper(::Type{Storage}, ::Type) where {S, Storage <: AbstractVector{S}} - Storage -end -function collect_as_storage_type_helper(::Type{Storage}, ::Type{E}) where {Storage <: AbstractVector, E} - Storage{E} -end - -function make_abstract_vector_from_collection_with_length(::Type{V}, elems) where {E, V <: AbstractVector{E}} - ret = V(undef, length(elems)) - copyto!(ret, elems) -end - -function fsv_type_from_underlying_storage_type(::Type{V}) where {E, V <: DenseVector{E}} - FixedSizeVector{E, V} +function infer_ndims_impl(::Base.HasShape{N}) where {N} + N::Int end -function make_fsv_from_collection_with_length(::Type{V}, elems) where {V <: DenseVector} - stor = collect_as_storage_type_helper(V, eltype(elems)) - ret_type = fsv_type_from_underlying_storage_type(stor) - make_abstract_vector_from_collection_with_length(ret_type, elems) -end - -function make_vector_from_tuple(::Type{V}, elems::Tuple) where {V <: DenseVector} - stor = collect_as_storage_type_helper(V, eltype(elems)) - ret_type = Vector{eltype(stor)} - make_abstract_vector_from_collection_with_length(ret_type, elems) -end - -function push(v::Vector, e) - E = typejoin(typeof(e), eltype(v)) - ret = Vector{E}(undef, length(v) + 1) - ret = copyto!(ret, v) - ret[end] = e - ret -end - -""" - push!!(t::Type{<:AbstractVector}, v::Vector, e)::Vector - -Return a `Vector`, `r`, respecting these properties: -* `all(r[begin:(end - 1)] .=== v)` -* if `t` specifies an element type, `E`, `r[end] == E(e)`, otherwise `r[end] === e` -""" -function push!! end -function push!!(::Type{<:AbstractVector{E}}, v::Vector{E}, e::E) where {E} - push!(v, e) -end -function push!!(::Type{<:AbstractVector{E}}, v::Vector{E}, e) where {E} - push!(v, e) -end -function push!!(::Type{<:AbstractVector}, v::Vector{E}, e::E) where {E} - push!(v, e) -end -function push!!(::Type{<:AbstractVector}, v::Vector, e) - push(v, e) +function infer_ndims_impl(::Base.IteratorSize) + 1 end -function empty_fsv(::Type{V}, ::Any) where {E, V <: DenseVector{E}} - fsv_type_from_underlying_storage_type(V)(undef, 0) +function infer_ndims(iterator) + infer_ndims_impl(Base.IteratorSize(iterator)) end -function empty_fsv(::Type{V}, iterator) where {V <: DenseVector} - let E = eltype(iterator) - if isconcretetype(E) - fsv_type_from_underlying_storage_type(V{E})(undef, 0) - else - fsv_type_from_underlying_storage_type(V{Union{}})(undef, 0) - end +function inferred_shape_impl(output_ndims::Int, iterator) + if (!isone(output_ndims)) && (output_ndims != infer_ndims(iterator)) + throw(DimensionMismatch("mismatched dimensionalities")) end -end - -""" - collect_as_fsv(V::Type{<:DenseVector}, iterator)::FixedSizeVector - -Collect the elements of `iterator` into a `FixedSizeVector`, `r`. The argument `V` -specifies the underlying storage type parameter of `r`. When possible (specified as -a parameter in `V`), `eltype(r)` is also taken from `V`, otherwise it is determined -by the types of the elements of the iterator. - -When `V` does not provide an element type and `isempty(iterator)`, the element type -of the return value is: -* `eltype(iterator)`, when it's a concrete type -* `Union{}`, otherwise -""" -function collect_as_fsv(::Type{V}, iterator) where {V <: DenseVector} - es1 = iterate(iterator) # unroll a bit to avoid unnecessary allocations and help inference - if es1 isa Tuple - let (e1, s1) = es1, state = s1, ret = make_vector_from_tuple(V, (e1,)) - while true - es = iterate(iterator, state) - if es isa Tuple - let (e, s) = es - state = s - ret = push!!(V, ret, e) - end - else - break - end - end - ret_type_storage = collect_as_storage_type_helper(V, eltype(ret)) - ret_type = fsv_type_from_underlying_storage_type(ret_type_storage) - ret_type(ret) - end + if iszero(output_ndims) + () + elseif isone(output_ndims) + (:,) else - empty_fsv(V, iterator) + size(iterator) end end -function checked_dimension_count_of(::Type{<:AbstractArray{<:Any, N}}, input_size_class) where {N} - n = check_count_value(N) - m = dimension_count_of(input_size_class)::Int - if n != m - throw(DimensionMismatch()) - end - n -end -function checked_dimension_count_of(::Type, input_size_class) - check_count_value(dimension_count_of(input_size_class)) +function inferred_shape(::Type{<:(AbstractArray{T, N} where {T})}, iterator) where {N} + inferred_shape_impl(N::Int, iterator) end -function collect_as(::Type{<:(FixedSizeArray{E, N, Union{}} where {E, N})}, ::Any) - throw_bottom_type() +function inferred_shape(::Type{<:AbstractArray}, iterator) + inferred_shape_impl(infer_ndims(iterator), iterator) end -function collect_as(::Type{Union{}}, ::Any) - throw_bottom_type() -end - -""" - collect_as(t::Type{<:FixedSizeArray}, iterator) -Tries to construct a value of type `t` from the iterator `iterator`. The type `t` -must either be concrete, or a `UnionAll` without constraints. -""" -function collect_as(::Type{FSA}, iterator) where {FSA<:FixedSizeArray} - size_class = Base.IteratorSize(iterator) - if size_class == Base.IsInfinite() - throw(ArgumentError("iterator is infinite, can't fit infinitely many elements into a `FixedSizeArray`")) - end +function (collect::Collect)(::Type{FSA}, iterator) where {FSA<:FixedSizeArray} T = check_constructor_is_allowed(FSA) mem = parent_type_with_default(T) - output_dimension_count = checked_dimension_count_of(T, size_class) - fsv = if ( - (size_class isa Union{Base.HasLength, Base.HasShape}) && - (eltype_is_known(mem) || isconcretetype(eltype(iterator))) - ) - make_fsv_from_collection_with_length(mem, iterator) # fast path - else - collect_as_fsv(mem, iterator) - end - if isone(output_dimension_count) - fsv # `size(iterator)` may throw in this branch - else - reshape(fsv, size(iterator)) - end + shape = inferred_shape(T, iterator) + backing = collect(mem, iterator) + fsv = new_fixed_size_array(backing, (length(backing),)) + reshape(fsv, shape) +end + +function collect_as_haseltype(::Type{FSA}, iterator) where {T, FSA<:FixedSizeArray{T}} + collect_as(check_constructor_is_allowed(FSA), iterator) +end + +function collect_as_haseltype(::Type{FSA}, iterator) where {FSA<:FixedSizeArray} + allowed = check_constructor_is_allowed(FSA) + val = with_stripped_type_parameters(TypeParametersElementType(), allowed) + typ = val_parameter(val){eltype(iterator)} + collect_as(typ, iterator) end diff --git a/test/Project.toml b/test/Project.toml index 5447f4f..eec68c6 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,11 +1,13 @@ [compat] Aqua = "0.8" +Collects = "1" OffsetArrays = "1.14" Random = "1.10" Test = "1.10" [deps] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +Collects = "08986516-18db-4a8b-8eaa-f5ef1828d8f1" OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/test/runtests.jl b/test/runtests.jl index 9fb869f..e9f0440 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,5 +1,6 @@ using Test using FixedSizeArrays +using Collects: collect_as using OffsetArrays: OffsetArray using Random: Random import Aqua @@ -244,6 +245,17 @@ end end end + @testset "constructors taking `AbstractArray`" begin + @test [1, 2, 3] == (@inferred FSV([1, 2, 3]))::FSV{Int} + @test [1, 2, 3] == (@inferred FSA([1, 2, 3]))::FSV{Int} + @test [1, 2, 3] == (@inferred FSV(Any[1, 2, 3]))::FSV{Any} + @test [1, 2, 3] == (@inferred FSA(Any[1, 2, 3]))::FSV{Any} + @test [1, 2, 3] == (@inferred FSV(Real[1, 2, 3]))::FSV{Real} + @test [1, 2, 3] == (@inferred FSA(Real[1, 2, 3]))::FSV{Real} + @test [1, 2, 3] == (@inferred FSV{Float32}([1, 2, 3]))::FSV{Float32} + @test [1, 2, 3] == (@inferred FSA{Float32}([1, 2, 3]))::FSV{Float32} + end + @testset "setindex!" begin v = FSV{Float64}(undef, 2) v[1] = 1 @@ -516,10 +528,15 @@ end end @testset "empty iterator with inexact `eltype`" begin iterator = Iterators.map((x -> x + 0.3), []) - @test collect_as(FSV, iterator) isa FSV{Union{}} + @test collect_as(FSV, iterator; empty_iterator_handler = Returns(Union{})) isa FSV{Union{}} @test collect_as(FSV{Union{}}, iterator) isa FSV{Union{}} @test collect_as(FSV{Float32}, iterator) isa FSV{Float32} @test collect_as(FSV{Any}, iterator) isa FSV{Any} + @test_throws ArgumentError collect_as(FSV, iterator) + end + @testset "explicit handling of empty iterators" begin + @test [] == collect_as(FSV, []; empty_iterator_handler = Returns(Union{}))::FixedSizeArray{Union{}} + @test [] == collect_as(FSV, []; empty_iterator_handler = Returns(Bool))::FixedSizeArray{Bool} end @testset "`Union{}`" begin @test_throws Exception collect_as(Union{}, ()) @@ -531,11 +548,11 @@ end @test_throws ArgumentError collect_as(T, iterator) end end - @test_throws ArgumentError collect_as(FSA{Int, -1}, 7:8) + @test_throws DimensionMismatch collect_as(FSA{Int, -1}, 7:8) @test_throws TypeError collect_as(FSA{Int, 3.1}, 7:8) for T ∈ (FSA{3}, FSV{3}) iterator = (7:8, (7, 8)) - @test_throws TypeError collect_as(T, iterator) + @test_throws MethodError collect_as(T, iterator) end @testset "buggy iterator with mismatched `size` and `length" begin for iterator ∈ (Iter((), 0, 7), Iter((3, 2), 5, 7)) @@ -561,6 +578,7 @@ end (E, dim_count) = abstract_array_params(a) af = collect(Float64, iterator) @test abstract_array_params(af) == (Float64, dim_count) # meta + iszero(dim_count) || @test_throws DimensionMismatch collect_as(FSA{E,dim_count+1}, iterator) for T ∈ (FSA{E}, FSA{E,dim_count}) test_inferred(collect_as, FSA{E,dim_count}, (T, iterator)) @@ -568,6 +586,8 @@ end @test a == fsa @test first(abstract_array_params(fsa)) <: E end + # `collect_as` throws by default when given an empty iterator with imprecise `eltype` + (isconcretetype(eltype(iterator)) || (eltype(iterator) <: Union{}) || !isempty(iterator)) && for T ∈ (FSA, FSA{<:Any,dim_count}) @test collect_as(T, iterator) isa FSA{E,dim_count} fsa = collect_as(T, iterator) @@ -580,6 +600,8 @@ end @test af == fsa @test first(abstract_array_params(fsa)) <: Float64 end + # `collect_as` throws by default when given an empty iterator with imprecise `eltype` + (isconcretetype(eltype(iterator)) || (eltype(iterator) <: Union{}) || !isempty(iterator)) && for T ∈ (FixedSizeArray{<:Any,dim_count},) @test collect_as(T, iterator) isa FixedSizeArray{E,dim_count} fsa = collect_as(T, iterator) From a791cf20610a5d7b7df8b637e17c1ff84c1d5522 Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Tue, 3 Jun 2025 15:41:52 +0200 Subject: [PATCH 2/3] simplify --- src/FixedSizeArray.jl | 60 ------------------------------------------- src/collect_as.jl | 6 ++--- 2 files changed, 2 insertions(+), 64 deletions(-) diff --git a/src/FixedSizeArray.jl b/src/FixedSizeArray.jl index b6e0a16..40edba2 100644 --- a/src/FixedSizeArray.jl +++ b/src/FixedSizeArray.jl @@ -259,66 +259,6 @@ function with_stripped_type_parameters_unchecked(::TypeParametersElementType, :: Val{s}() end -let - global with_stripped_type_parameters_unchecked - Disp = Union{ - Type{FixedSizeArray}, - Type{FixedSizeArray{T}} where {T}, - } - function with_stripped_type_parameters_unchecked(::TypeParametersElementType, ::Disp) - Val{FixedSizeArray}() - end -end - -let - global with_stripped_type_parameters_unchecked - Disp = Union{ - Type{FixedSizeArray{T, N} where {T}}, - Type{FixedSizeArray{T, N}} where {T}, - } where {N} - function with_stripped_type_parameters_unchecked(::TypeParametersElementType, ::Disp{N}) where {N} - s = FixedSizeArray{T, N::Int} where {T} - Val{s}() - end -end - -let - global with_stripped_type_parameters_unchecked - Disp = Type{FixedSizeArray{T, N, Mem} where {N}} where {T, Mem <: DenseVector{T}} - # `Base.@assume_effects :consistent` is a workaround for: - # https://github.com/JuliaLang/julia/issues/56966 - Base.@assume_effects :consistent function with_stripped_type_parameters_unchecked(::TypeParametersElementType, ::Disp{T, Mem}) where {T, Mem <: DenseVector{T}} - val = with_stripped_type_parameters(TypeParametersElementType(), Mem) - s = FixedSizeArray{T, N, val_parameter(val){T}} where {T, N} - Val{s}() - end -end - -let - global with_stripped_type_parameters_unchecked - Disp = Type{FixedSizeArray{T, N, Mem}} where {T, N, Mem <: DenseVector{T}} - # `Base.@assume_effects :consistent` is a workaround for: - # https://github.com/JuliaLang/julia/issues/56966 - Base.@assume_effects :consistent function with_stripped_type_parameters_unchecked(::TypeParametersElementType, ::Disp{T, N, Mem}) where {T, N, Mem <: DenseVector{T}} - val = with_stripped_type_parameters(TypeParametersElementType(), Mem) - s = FixedSizeArray{T, N::Int, val_parameter(val){T}} where {T} - Val{s}() - end -end - -for Mem ∈ (Vector, optional_memory...) - DispNoDim = Type{FixedSizeArray{T, N, Mem{T}} where {T, N}} - DispWithDim = Type{FixedSizeArray{T, N, Mem{T}} where {T}} where {N} - @eval function with_stripped_type_parameters_unchecked(::TypeParametersElementType, ::$DispNoDim) - s = FixedSizeArray{T, N, $Mem{T}} where {T, N} - Val{s}() - end - @eval function with_stripped_type_parameters_unchecked(::TypeParametersElementType, ::$DispWithDim{N}) where {N} - s = FixedSizeArray{T, N::Int, $Mem{T}} where {T} - Val{s}() - end -end - # `Base.@assume_effects :consistent` is a workaround for: # https://github.com/JuliaLang/julia/issues/56966 Base.@assume_effects :consistent function with_stripped_type_parameters_unchecked(::TypeParametersElementTypeAndDimensionality, ::Type{<:(FixedSizeArray{T, N, Mem} where {T, N})}) where {Mem} diff --git a/src/collect_as.jl b/src/collect_as.jl index bedf78d..5ad5d2d 100644 --- a/src/collect_as.jl +++ b/src/collect_as.jl @@ -49,8 +49,6 @@ function collect_as_haseltype(::Type{FSA}, iterator) where {T, FSA<:FixedSizeArr end function collect_as_haseltype(::Type{FSA}, iterator) where {FSA<:FixedSizeArray} - allowed = check_constructor_is_allowed(FSA) - val = with_stripped_type_parameters(TypeParametersElementType(), allowed) - typ = val_parameter(val){eltype(iterator)} - collect_as(typ, iterator) + T = check_constructor_is_allowed(FSA){eltype(iterator)} + collect_as(T, iterator) end From 514d819802897a2f86081ac6b9c901fe8d0b99e8 Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Tue, 3 Jun 2025 15:56:45 +0200 Subject: [PATCH 3/3] stricter converting constructor, require matching dimensionality --- src/FixedSizeArray.jl | 13 ++++++++++++- test/runtests.jl | 2 ++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/FixedSizeArray.jl b/src/FixedSizeArray.jl index 40edba2..923f9c7 100644 --- a/src/FixedSizeArray.jl +++ b/src/FixedSizeArray.jl @@ -325,7 +325,7 @@ axes_are_one_based(axes) = all(isone ∘ first, axes) # converting constructors for copying other array types -function (::Type{FSA})(src::AbstractArray) where {FSA <: FixedSizeArray} +function converting_constructor(::Type{FSA}, src::AbstractArray) where {FSA <: FixedSizeArray} axs = axes(src) if !axes_are_one_based(axs) throw(DimensionMismatch("source array has a non-one-based indexing axis")) @@ -333,6 +333,17 @@ function (::Type{FSA})(src::AbstractArray) where {FSA <: FixedSizeArray} collect_as_haseltype(FSA, src) end +function (::Type{FSA})(src::AbstractArray) where {N, FSA <: FixedSizeArray{<:Any, N}} + if N::Int != ndims(src) + throw(DimensionMismatch("dimensionality mismatch")) + end + converting_constructor(FSA, src) +end + +function (::Type{FSA})(src::AbstractArray) where {FSA <: FixedSizeArray} + converting_constructor(FSA, src) +end + # `copy`: avoid the `similar` and `copyto!` in the generic fallback function Base.copy(a::FixedSizeArray) diff --git a/test/runtests.jl b/test/runtests.jl index e9f0440..05595c8 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -254,6 +254,8 @@ end @test [1, 2, 3] == (@inferred FSA(Real[1, 2, 3]))::FSV{Real} @test [1, 2, 3] == (@inferred FSV{Float32}([1, 2, 3]))::FSV{Float32} @test [1, 2, 3] == (@inferred FSA{Float32}([1, 2, 3]))::FSV{Float32} + @test_throws DimensionMismatch FSV(rand(Bool, 2, 2)) + @test_throws DimensionMismatch FSV{Float32}(rand(Bool, 2, 2)) end @testset "setindex!" begin