From 803d0d2a7d43d00d808bdde4f6af41ae81c7302d Mon Sep 17 00:00:00 2001 From: "Viral B. Shah" Date: Mon, 26 Oct 2026 13:13:23 +0000 Subject: [PATCH 1/3] Return `Int` from `length` and `axes` of a `SparseVector`, so that its views dispatch to the sparse methods `length` and `size` of a `SparseVector` and a `FixedSparseVector` return an `Int` when every value of the index type fits in one, and `Ti` otherwise, so the whole view of a narrow-indexed vector is a `SparseVectorView`. The alias is widened to any `Slice{<:OneTo}` for the wider index types, and `count` over sparse vector views uses the stored entries. Co-Authored-By: Claude Opus 5.5 --- src/SparseArrays.jl | 5 +++-- src/sparsevector.jl | 19 ++++++++++++------- test/sparsevector.jl | 17 +++++++++++------ 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/src/SparseArrays.jl b/src/SparseArrays.jl index 04c8cf3b..cd890127 100644 --- a/src/SparseArrays.jl +++ b/src/SparseArrays.jl @@ -160,9 +160,10 @@ const SparseMatrixCSCSubArray{Tv,Ti} = SubArray{Tv,2,<:AbstractSparseMatrixCSC{T const SparseMatrixCSCOrSubArray{Tv,Ti} = Union{AbstractSparseMatrixCSC{Tv,Ti}, SparseMatrixCSCSubArray{Tv,Ti}} # Whole-column views of sparse matrices and whole views of sparse vectors share the -# sparse vector interface. +# sparse vector interface. A `SparseVector` whose index type is wider than `Int` keeps its +# length in that type, so a whole view of one slices a `OneTo{Ti}`. const SparseColumnView{Tv,Ti} = SubArray{Tv,1,<:AbstractSparseMatrixCSC{Tv,Ti},Tuple{Base.Slice{Base.OneTo{Int}},Int},false} -const SparseVectorView{Tv,Ti} = SubArray{Tv,1,<:AbstractSparseVector{Tv,Ti},Tuple{Base.Slice{Base.OneTo{Int}}},false} +const SparseVectorView{Tv,Ti} = SubArray{Tv,1,<:AbstractSparseVector{Tv,Ti},<:Tuple{Base.Slice{<:Base.OneTo}},false} const SparseVectorOrView{Tv,Ti} = Union{AbstractCompressedVector{Tv,Ti}, SparseColumnView{Tv,Ti}, SparseVectorView{Tv,Ti}} const AdjOrTransSparseVectorOrView{Tv,Ti} = AdjOrTrans{Tv, <:SparseVectorOrView{Tv,Ti}} # view of a unit range of a sparse vector's indices diff --git a/src/sparsevector.jl b/src/sparsevector.jl index 5d8f997a..6938651d 100644 --- a/src/sparsevector.jl +++ b/src/sparsevector.jl @@ -58,13 +58,18 @@ SparseVector{Tv, Ti}(::UndefInitializer, (n,)::Tuple{Integer}) where {Tv, Ti} = ### Basic properties -length(x::SparseVector) = getfield(x, :n) -length(x::FixedSparseVector) = getfield(x, :n) -size(x::SparseVector) = (getfield(x, :n),) -size(x::FixedSparseVector) = (getfield(x, :n),) - -function Base._simple_count(f, x::AbstractCompressedVector, init::T) where T - init + T(count(f, nonzeros(x)) + f(zero(eltype(x)))*(length(x) - nnz(x))) +# The length is an `Int`, as for dense arrays, when every value of the index type fits in one. +# A wider index type (`UInt64`, `Int128`, `BigInt`, and `UInt32` and `Int64` on a 32-bit build) +# keeps its lengths beyond `typemax(Int)`. +_vectorlength(n::Integer) = n +_vectorlength(n::Base.BitInteger) = typemax(n) <= typemax(Int) ? Int(n) : n +length(x::SparseVector) = _vectorlength(getfield(x, :n)) +length(x::FixedSparseVector) = _vectorlength(getfield(x, :n)) +size(x::SparseVector) = (length(x),) +size(x::FixedSparseVector) = (length(x),) + +function Base._simple_count(f, x::Union{SparseVectorOrView,SparseVectorPartialView}, init::T) where T + init + T(count(f, nonzeros(x)) + f(_densezero(x))*(length(x) - nnz(x))) end # implement the nnz - nzrange - nonzeros - rowvals interface for sparse vectors diff --git a/test/sparsevector.jl b/test/sparsevector.jl index 303195a4..34b2522b 100644 --- a/test/sparsevector.jl +++ b/test/sparsevector.jl @@ -48,9 +48,11 @@ include("testhelpers.jl") @test count(SparseVector(8, [2, 5, 6], [true,false,true])) == 2 @static if COMPREHENSIVE @test count(SparseVector(8, [2, 5, 6], [true,false,true]), init=Int16(2))::Int16 == 4 - y = SparseVector(8, Int8[4], [5.0]) - @test y isa SparseVector{Float64,Int8} - @test @inferred size(y) == (@inferred(length(y))::Int8,) + # the length is an `Int` when every value of the index type fits in one + y = SparseVector(8, Int32[4], [5.0]) + @test y isa SparseVector{Float64,Int32} + @test @inferred size(y) == (@inferred(length(y))::Int,) + @test @inferred(length(SparseVector(8, Int128[4], [5.0])))::Int128 == 8 end end @@ -1791,9 +1793,9 @@ end @test ((1 + im) * v)::T == (1 + im) * d @test copy(v)::T == d end - # the full-column and full-vector views scale in place through the parent's storage - # (`view(x, :)` is a `SparseVectorView` only when the parent's axes are `Int`-sized) - for v in (view(A, :, 1), (Ti === Int ? (view(x, :),) : ())...), a in (1.0 + im,) + # the full-column and full-vector views scale in place through the parent's storage, + # also when the parent's axes are wider than `Int` + for v in (view(A, :, 1), view(x, :), view(SparseVector{ComplexF64,Int128}(x), :)), a in (1.0 + im,) @test which(*, (typeof(v), typeof(a))).module === SparseArrays @test which(*, (typeof(a), typeof(v))).module === SparseArrays @test which(/, (typeof(v), typeof(a))).module === SparseArrays @@ -1817,6 +1819,9 @@ end @test @allocated(f(v)) < 10^4 end end + # `count` calls `f` on the stored entries and on one zero + calls = Ref(0) + @test count(z -> (calls[] += 1; !iszero(z)), view(SparseVector{ComplexF64,Int32}(y), :)) == 2 && calls[] == 3 end end From c9f4e7c5d236661f7722b0e8594c7a27e489e246 Mon Sep 17 00:00:00 2001 From: "Viral B. Shah" Date: Mon, 26 Oct 2026 15:35:59 +0000 Subject: [PATCH 2/3] Keep `length` of a `SparseVector` returning `Ti` Revert the `Int` length: #176 (JuliaLang/julia#45937) decided that `length` returns the index type. The widened `SparseVectorView` alias and the O(nnz) `count` on sparse vector views stay. Co-Authored-By: Claude Opus 5.5 --- src/SparseArrays.jl | 4 ++-- src/sparsevector.jl | 13 ++++--------- test/sparsevector.jl | 10 ++++------ 3 files changed, 10 insertions(+), 17 deletions(-) diff --git a/src/SparseArrays.jl b/src/SparseArrays.jl index cd890127..f70d7a2f 100644 --- a/src/SparseArrays.jl +++ b/src/SparseArrays.jl @@ -160,8 +160,8 @@ const SparseMatrixCSCSubArray{Tv,Ti} = SubArray{Tv,2,<:AbstractSparseMatrixCSC{T const SparseMatrixCSCOrSubArray{Tv,Ti} = Union{AbstractSparseMatrixCSC{Tv,Ti}, SparseMatrixCSCSubArray{Tv,Ti}} # Whole-column views of sparse matrices and whole views of sparse vectors share the -# sparse vector interface. A `SparseVector` whose index type is wider than `Int` keeps its -# length in that type, so a whole view of one slices a `OneTo{Ti}`. +# sparse vector interface. The length of a `SparseVector` has its index type, so a whole +# view of one slices a `OneTo{Ti}`. const SparseColumnView{Tv,Ti} = SubArray{Tv,1,<:AbstractSparseMatrixCSC{Tv,Ti},Tuple{Base.Slice{Base.OneTo{Int}},Int},false} const SparseVectorView{Tv,Ti} = SubArray{Tv,1,<:AbstractSparseVector{Tv,Ti},<:Tuple{Base.Slice{<:Base.OneTo}},false} const SparseVectorOrView{Tv,Ti} = Union{AbstractCompressedVector{Tv,Ti}, SparseColumnView{Tv,Ti}, SparseVectorView{Tv,Ti}} diff --git a/src/sparsevector.jl b/src/sparsevector.jl index 6938651d..addf025c 100644 --- a/src/sparsevector.jl +++ b/src/sparsevector.jl @@ -58,15 +58,10 @@ SparseVector{Tv, Ti}(::UndefInitializer, (n,)::Tuple{Integer}) where {Tv, Ti} = ### Basic properties -# The length is an `Int`, as for dense arrays, when every value of the index type fits in one. -# A wider index type (`UInt64`, `Int128`, `BigInt`, and `UInt32` and `Int64` on a 32-bit build) -# keeps its lengths beyond `typemax(Int)`. -_vectorlength(n::Integer) = n -_vectorlength(n::Base.BitInteger) = typemax(n) <= typemax(Int) ? Int(n) : n -length(x::SparseVector) = _vectorlength(getfield(x, :n)) -length(x::FixedSparseVector) = _vectorlength(getfield(x, :n)) -size(x::SparseVector) = (length(x),) -size(x::FixedSparseVector) = (length(x),) +length(x::SparseVector) = getfield(x, :n) +length(x::FixedSparseVector) = getfield(x, :n) +size(x::SparseVector) = (getfield(x, :n),) +size(x::FixedSparseVector) = (getfield(x, :n),) function Base._simple_count(f, x::Union{SparseVectorOrView,SparseVectorPartialView}, init::T) where T init + T(count(f, nonzeros(x)) + f(_densezero(x))*(length(x) - nnz(x))) diff --git a/test/sparsevector.jl b/test/sparsevector.jl index 34b2522b..55073148 100644 --- a/test/sparsevector.jl +++ b/test/sparsevector.jl @@ -48,11 +48,9 @@ include("testhelpers.jl") @test count(SparseVector(8, [2, 5, 6], [true,false,true])) == 2 @static if COMPREHENSIVE @test count(SparseVector(8, [2, 5, 6], [true,false,true]), init=Int16(2))::Int16 == 4 - # the length is an `Int` when every value of the index type fits in one - y = SparseVector(8, Int32[4], [5.0]) - @test y isa SparseVector{Float64,Int32} - @test @inferred size(y) == (@inferred(length(y))::Int,) - @test @inferred(length(SparseVector(8, Int128[4], [5.0])))::Int128 == 8 + y = SparseVector(8, Int8[4], [5.0]) + @test y isa SparseVector{Float64,Int8} + @test @inferred size(y) == (@inferred(length(y))::Int8,) end end @@ -1794,7 +1792,7 @@ end @test copy(v)::T == d end # the full-column and full-vector views scale in place through the parent's storage, - # also when the parent's axes are wider than `Int` + # also when the parent's axes are not `Int` for v in (view(A, :, 1), view(x, :), view(SparseVector{ComplexF64,Int128}(x), :)), a in (1.0 + im,) @test which(*, (typeof(v), typeof(a))).module === SparseArrays @test which(*, (typeof(a), typeof(v))).module === SparseArrays From f3c058b06ff574e797b31d47468b44a81b12e896 Mon Sep 17 00:00:00 2001 From: "Viral B. Shah" Date: Tue, 27 Oct 2026 00:42:40 +0000 Subject: [PATCH 3/3] Copy whole sparse vector and column views from their stored entries in `SparseVector` `SparseVector(v)`, and through it `sparse`, `convert`, concatenation and `kron`, read a whole view as a dense vector: O(n), stored zeros dropped and `Int` indices. The copy constructors now take `SparseVectorOrView`. Co-Authored-By: Claude Opus 5.5 --- src/sparsevector.jl | 7 ++++--- test/sparsevector.jl | 6 ++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/sparsevector.jl b/src/sparsevector.jl index addf025c..6e9e65bf 100644 --- a/src/sparsevector.jl +++ b/src/sparsevector.jl @@ -555,6 +555,7 @@ sparsevec(a::AbstractVector{T}) where {T} = SparseVector{T, Int}(a) sparsevec(a::AbstractArray) = sparsevec(vec(a)) sparsevec(a::AbstractSparseMatrixCSC) = a[:] sparsevec(a::AbstractSparseVector) = vec(a) +sparsevec(a::Union{SparseColumnView,SparseVectorView}) = SparseVector(a) sparse(a::AbstractVector) = sparsevec(a) function _dense2indval!(nzind::Vector{Ti}, nzval::Vector{Tv}, s::AbstractArray) where {Tv,Ti} @@ -595,9 +596,9 @@ SparseVector{Tv}(s::AbstractVector) where {Tv} = SparseVector{Tv,Int}(s) SparseVector(s::AbstractVector{Tv}) where {Tv} = SparseVector{Tv,Int}(s) # copy-constructors -SparseVector(s::AbstractCompressedVector{Tv,Ti}) where {Tv,Ti} = SparseVector{Tv,Ti}(s) -SparseVector{Tv}(s::AbstractCompressedVector{<:Any,Ti}) where {Tv,Ti} = SparseVector{Tv,Ti}(s) -function SparseVector{Tv,Ti}(s::AbstractCompressedVector) where {Tv,Ti} +SparseVector(s::SparseVectorOrView{Tv,Ti}) where {Tv,Ti} = SparseVector{Tv,Ti}(s) +SparseVector{Tv}(s::SparseVectorOrView{<:Any,Ti}) where {Tv,Ti} = SparseVector{Tv,Ti}(s) +function SparseVector{Tv,Ti}(s::SparseVectorOrView) where {Tv,Ti} copyind = Vector{Ti}(nonzeroinds(s)) copynz = Vector{Tv}(nonzeros(s)) SparseVector{Tv,Ti}(length(s), copyind, copynz) diff --git a/test/sparsevector.jl b/test/sparsevector.jl index 55073148..68e22784 100644 --- a/test/sparsevector.jl +++ b/test/sparsevector.jl @@ -1820,6 +1820,12 @@ end # `count` calls `f` on the stored entries and on one zero calls = Ref(0) @test count(z -> (calls[] += 1; !iszero(z)), view(SparseVector{ComplexF64,Int32}(y), :)) == 2 && calls[] == 3 + # a whole view converts with the parent's index type and stored zeros + z = SparseVector{ComplexF64,Int32}(x) + C = SparseMatrixCSC{ComplexF64,Int32}(A) + for (v, u) in ((view(z, :), z), (view(C, :, 1), C[:, 1])), f in (SparseVector, sparse, w -> vcat(w, w), w -> kron(w, z)) + @test exact_equal(f(v), f(u)) + end end end