From e8dc7280284545120ba14aefd3056855fd9630e4 Mon Sep 17 00:00:00 2001 From: Cody Tapscott Date: Thu, 27 Aug 2026 21:52:22 +0000 Subject: [PATCH] Make `sparse` and `spdiagm` statically dispatchable Two constructors defeated static call resolution (e.g. under juliac's `--trim`) for reasons invisible at the call site: The `sparse(I, J, V, m, n, combine)` methods took `combine` as an unconstrained, unspecialized argument, so the pass-through call into the `Tv`/`Ti`-typed method was made on an abstractly-typed function value. Parameterizing the methods on `combine::F` specializes the pass-through and the call chain resolves statically. `spdiagm`'s eltype computation mapped `eltype` over the `kv::Pair...` tuple; under Vararg widening the compiled signature is `(::Pair, ::Vararg{Pair})`, for which the tuple `map` itself cannot be resolved. A dispatch-based `spdiagm_eltype` (pairwise recursion, with a homogeneous fast path that binds the common eltype) mirrors `Base.promote_eltypeof` and resolves statically for every arity. Both changes only sharpen dispatch; behavior is unchanged. Compiling A = sparse([1, 2, 3], [1, 2, 3], [1.0, 2.0, 3.0]) B = spdiagm(0 => ones(3), 1 => ones(2)) with `juliac --output-exe --trim=safe` fails before this change with ten verifier errors (unresolved `sparse!`, the tuple `map`, its `_apply_iterate` splat, and a `Vector{_A}` allocation whose type never became concrete) and verifies cleanly with it. This commit was written with the assistance of generative AI (Claude). Co-Authored-By: Claude Opus 5 --- src/sparsematrix.jl | 14 +++++++++++--- test/linalg.jl | 4 ++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/sparsematrix.jl b/src/sparsematrix.jl index 51ff5caf..f51a8fec 100644 --- a/src/sparsematrix.jl +++ b/src/sparsematrix.jl @@ -1069,7 +1069,7 @@ julia> sparse(Is, Js, Vs) ⋅ ⋅ 3 ``` """ -function sparse(I::AbstractVector{Ti}, J::AbstractVector{Ti}, V::AbstractVector{Tv}, m::Integer, n::Integer, combine) where {Tv,Ti<:Integer} +function sparse(I::AbstractVector{Ti}, J::AbstractVector{Ti}, V::AbstractVector{Tv}, m::Integer, n::Integer, combine::F) where {Tv,Ti<:Integer,F} require_one_based_indexing(I, J, V) coolen = length(I) if length(J) != coolen || length(V) != coolen @@ -1110,7 +1110,7 @@ function sparse(I::AbstractVector{Ti}, J::AbstractVector{Ti}, V::AbstractVector{ end end -sparse(I::AbstractVector, J::AbstractVector, V::AbstractVector, m::Integer, n::Integer, combine) = +sparse(I::AbstractVector, J::AbstractVector, V::AbstractVector, m::Integer, n::Integer, combine::F) where {F} = sparse(AbstractVector{Int}(I), AbstractVector{Int}(J), V, m, n, combine) """ @@ -4238,6 +4238,14 @@ end _nzvals(v::AbstractSparseVector) = nonzeros(v) _nzvals(v::AbstractVector) = v +# Promoted element type of the diagonals, mirroring `Base.promote_eltypeof` +spdiagm_eltype(p::Pair) = eltype(p.second) +spdiagm_eltype(p::Pair, q::Pair, rest::Pair...) = + (@inline; promote_type(promote_type(eltype(p.second), eltype(q.second)), + spdiagm_eltype(rest...))) +spdiagm_eltype(p::Pair, q::Pair) = promote_type(eltype(p.second), eltype(q.second)) +spdiagm_eltype(kv::Pair{<:Integer,<:AbstractVector{T}}...) where {T} = T + function spdiagm_internal(kv::Pair{<:Integer,<:AbstractVector}...) ncoeffs = 0 for p in kv @@ -4245,7 +4253,7 @@ function spdiagm_internal(kv::Pair{<:Integer,<:AbstractVector}...) end I = Vector{Int}(undef, ncoeffs) J = Vector{Int}(undef, ncoeffs) - V = Vector{promote_type(map(x -> eltype(x.second), kv)...)}(undef, ncoeffs) + V = Vector{spdiagm_eltype(kv...)}(undef, ncoeffs) i = 0 m = 0 n = 0 diff --git a/test/linalg.jl b/test/linalg.jl index 758ed3cb..16291ce3 100644 --- a/test/linalg.jl +++ b/test/linalg.jl @@ -598,6 +598,10 @@ end # promotion @test spdiagm(0 => [1,2], 1 => [3.5], -1 => [4+5im]) == [1 3.5; 4+5im 2] + # sparse eltypes should infer well, even for a `Vararg` tail of unknown length + @test Base.infer_return_type(SparseArrays.spdiagm_eltype, + Tuple{Vararg{Pair{Int,Vector{Float64}}}}) === Core.Typeof(Float64) + # convenience constructor @test spdiagm(x)::SparseMatrixCSC == diagm(x) @test nnz(spdiagm(x)) == count(!iszero, x)