Skip to content

Backports for release 1.13 - #1089

Draft
dkarrasch wants to merge 40 commits into
release-1.13from
backports-release-1.13
Draft

dkarrasch wants to merge 40 commits into
release-1.13from
backports-release-1.13

Conversation

@dkarrasch

@dkarrasch dkarrasch commented Oct 13, 2026 •

Copy link
Copy Markdown
Member

Backports of the merged PRs labelled backport 1.13, cherry-picked with -x in the order they were merged on main, one per push, with CI checked before the next. Ticked PRs are included and passed CI.

Adjustments

main reorganized src/ and test/ by area in #872 and #874, after the 1.13 branch point. From #884 on, hunks for src/indexing.jl, src/concatenation.jl, src/matmul.jl, src/reductions.jl and src/constructors.jl go to the release branch file that holds the same code (mostly src/sparsematrix.jl), and tests go to the corresponding release test file. Tests that main guards with @static if COMPREHENSIVE and checks with mismatch are added unguarded and compared with ==, since release-1.13 has neither. Each commit message ends with a Backport adjustment: line when more than that was needed. Nontrivial ones:

Not included:

🤖 Generated with Claude Code

https://claude.ai/code/session_01AffnVrPcPNrYXezDCHacNT

sotashimozono and others added 3 commits October 13, 2026 08:04
This is a trivial Documentation issue.
The position of docstring was mistaken, it leads to docstring unvisible.

`copy(F::UmfpackLU, [ws::UmfpackWS])` has a docstring in the source, but
it is not attached to
anything: a comment sits between it and the definition.

```julia
julia> using SparseArrays

julia> b = Base.Docs.Binding(Base, :copy);

julia> filter(s -> occursin("Umfpack", string(s)), collect(keys(Base.Docs.meta(Base)[b].docs)))
Type[]

julia> hasmethod(Base.copy, Tuple{SparseArrays.UMFPACK.UmfpackLU})
true
```

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit f423f93)
Fixes #758.

`getindex(::Sparse, i, j)` binary-searched the row indices of a column
unconditionally, but CHOLMOD only guarantees that a column's row indices
are sorted when the struct's `sorted` flag is set. `S'S` returns an
unsorted result, so lookups silently missed stored entries — and since
`Array` and `show` go through `getindex`, those conversions were wrong
too:

```julia
julia> A = sparse([2, 1, 2], [1, 2, 2], Float64[1, 2, 3]);

julia> P = CHOLMOD.Sparse(A)' * CHOLMOD.Sparse(A); # unsorted

julia> P[1, 2]      # before: 0.0, after: 3.0
3.0

julia> Array(P)     # before: [1.0 0.0; 3.0 0.0]
2×2 Matrix{Float64}:
 1.0   3.0
 3.0  13.0
```

Changes:

* Use `searchsortedfirst` only when `sorted` is set; fall back to a
linear scan over the column otherwise.
* While computing the range of entries belonging to a column, also
handle unpacked matrices (`packed == 0`), where a column ends after
`nz[j]` entries rather than at `p[j + 1]`. Previously those reads ran
past the end of the column into uninitialized storage.

Tests cover the matrix from the issue plus an explicitly unsorted *and*
unpacked matrix built with `allocate_sparse`, over both index types.

Note that `Sparse` is not a public API, so this is not user-facing
today; it matters if/when we start relying on this indexing path
internally.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_014KpwdQ6bpWQKzjGV5prtAR

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 38288ed)
`isone(A::AbstractSparseMatrixCSC)` only validated the stored entries it
encountered and used `nnz(A) >= n` as a proxy for "every column has a
diagonal entry". A matrix with a stored zero and a missing diagonal
entry therefore passed:

```julia
julia> M = sparse([1 0; 1 1]) * sparse([1 0; -1 0])
2×2 SparseMatrixCSC{Int64, Int64} with 2 stored entries:
 1  ⋅
 0  ⋅

julia> isone(M)
true
```

This PR tracks, per column, whether a stored diagonal entry equal to one
was seen, and returns `false` otherwise. Added regression tests covering
the issue's example, stored zeros on and off the diagonal, and a missing
diagonal entry.

Fixes #763

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01XzN6CtuVBJWDVc88ShNYso

Co-authored-by: Viral B. Shah <ViralBShah@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 4ce0756)
@codecov

codecov Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.75312% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.98%. Comparing base (7ea4338) to head (1ec82a6).

Files with missing lines Patch % Lines
src/sparsematrix.jl 98.15% 3 Missing ⚠️
src/higherorderfns.jl 97.95% 2 Missing ⚠️
Additional details and impacted files
@@               Coverage Diff                @@
##           release-1.13    #1089      +/-   ##
================================================
+ Coverage         84.47%   84.98%   +0.51%     
================================================
  Files                13       13              
  Lines              9404     9484      +80     
================================================
+ Hits               7944     8060     +116     
+ Misses             1460     1424      -36     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

ViralBShah and others added 26 commits October 13, 2026 08:23
```julia
julia> spdiagm(3, 3)
ERROR: UndefVarError: `T` not defined in static parameter matching

julia> diagm(3, 3)
3×3 Matrix{Bool}
```

Since #751 the zero-pair call lands on
`spdiagm_eltype(kv::Pair{<:Integer,<:AbstractVector{T}}...) where T`,
whose `T` is unbound with no arguments. Aqua's `unbound_args` check
flagged exactly this, but it was marked `broken=true`. `_spdiagm` now
has a no-pair method returning `spzeros(Bool, m, n)` (0×0 for
`spdiagm()`), matching `diagm`. The empty case is handled there rather
than in `spdiagm_eltype` so the `Vararg` inference guarantee from #751
still holds. The variadic `spdiagm_eltype` method and `_absspvec_vcat`
(the only other hit, not reachable with zero arguments) now take a
leading argument, so the Aqua check is un-brokened and guards this from
here on.

Tested `linalg`, `sparsevector` and the Aqua/ambiguity job on nightly
(1.14.0-DEV). Also fails on 1.13.0, so this is a backport candidate.

Written with Claude Code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Viral B. Shah <ViralBShah@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit da1d02e)

Backport adjustment: test/ambiguous.jl keeps `unbound_args=(; broken=true)`, since release-1.13 has further unbound type parameters (`_absspvec_hcat`, `_getmode`) that main fixed in PRs not backported here.
…#884)

Fixes #811. Assigning through a repeated index corrupted a
`SparseMatrixCSC`: a repeated row stored the same row twice in a column,
and a repeated column dropped values. Dense arrays let the last write
win.

```julia
julia> A = spzeros(2, 2); A[[1, 1], 1] = [5.0, 6.0]; A
2×2 SparseMatrixCSC{Float64, Int64} with 2 stored entries:   # now 1 stored entry, 6.0
 5.0   ⋅
  ⋅    ⋅

julia> sum(A), sum(Matrix(A))
(11.0, 6.0)                                                  # now (6.0, 6.0)
```

`setindex!(A, V, I, J)` sorts `I` and `J` and merges `V` into each
column, assuming the sorted indices are strictly increasing. A new
helper, `_setindex_lastwrites`, returns the stable sort permutation with
all but the last position of each repeated index removed, so `V[pI, pJ]`
holds exactly the writes that survive; it returns `nothing` when the
index is already strictly increasing, which leaves the common path with
one extra `issorted` pass. The linear-index method `setindex!(A, x, I)`
already tried to skip repeats but compared `I[S[k]]` with its neighbour
in the original order rather than the sorted one, so `A[[1, 2, 1]] = x`
also stored a duplicate; it now compares with `I[S[k+1]]`. The
scalar-fill, logical-mask and `SparseVector` methods were already
correct.

Found by sweeping 3000 random `A[I, J] = V`, `A[I, j] = v`, `A[L] = x`,
`A[I, J] .= c` and `v[L] = x` assignments with repeated, sorted and
unsorted indices against `Matrix`/`Vector`: about two thirds of the
matrix cases and a quarter of the linear ones disagreed before, none
after. The new tests in `test/indexing.jl` fail on `main`. Tested with
the `indexing`, `sparsematrix`, `fixed`, `issues` and `higherorderfns`
suites on nightly (1.14).

No dispatch or public API changes. A bug fix, so a candidate for
`backport 1.x`.

Written by Claude Code (Claude Fable 5.1).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit ed2e0c0)

Backport adjustment: src/indexing.jl does not exist on release-1.13; the hunks were applied to src/sparsematrix.jl and the tests added to test/sparsematrix_constructors_indexing.jl.
…ex!` (#891)

Fixes #389. Scalar `setindex!` decided whether to insert with `v !==
zero(eltype(A))`. A heap-allocated zero such as `big(0)` is never egal
to a fresh one, so it was stored, and `S .= M` with a `Matrix{BigFloat}`
source densified `S` because the generic broadcast assigns every
element:

```julia
julia> A = spzeros(BigInt, 3, 3); A[1] = 0; nnz(A)
1

julia> S = spzeros(BigFloat, 4, 4); S .= Matrix(Diagonal(rand(BigFloat, 4))); nnz(S)
16
```

Insertion now goes through a `_isimplicitzero(v, Tv)` helper: egality
for isbits values, which keeps `-0.0`, `missing` and dual numbers with
nonzero partials exactly as #296 intended, and a value comparison for
non-isbits values, keeping the sign of a `BigFloat` zero. The value
comparison does not build `zero(Tv)`, so `BigFloat` inserts no longer
allocate a zero per call: filling a 300×300 sparse matrix from a
mostly-zero `Matrix{BigFloat}` drops from 4.6 ms / 90k allocs to 0.45 ms
/ 17 allocs, and `Float64`, `Int`, `ComplexF64` and
`Union{Missing,Float64}` are unchanged. Array elements are always
stored, as before. The matrix and vector scalar `setindex!` and the
dense source of `copyto!` into a sparse view share the helper.
Overwriting an existing stored entry with zero keeps it stored, as
before.

Tested with the full suite and the whitespace check on Julia nightly
(1.14.0-DEV). Candidate for `backport 1.x`: it restores behaviour lost
in #296 without new API.

Written with Claude Code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Viral B. Shah <ViralBShah@users.noreply.github.com>
(cherry picked from commit ba46cbf)

Backport adjustment: applied to src/sparsematrix.jl (for src/indexing.jl) and the tests to test/sparsematrix_constructors_indexing.jl and test/sparsematrix_ops.jl. The `_splice_source` hunk and its `copyto!` test are dropped: that code comes from #799, which is not on release-1.13.
`repeat(A, m, n)` fails for any sparse matrix whose index type is not
`Int`, while `repeat(A, m)` works:

```julia
julia> A = SparseMatrixCSC{Float64,Int32}(sprand(4, 3, 0.5));

julia> repeat(A, 2, 3)
ERROR: MethodError: no method matching SparseMatrixCSC{Float64, Int32}(::Int64, ::Int64, ::Vector{Int64}, ::Vector{Int32}, ::Vector{Float64})
```

The two-count method built its column pointers with `cumsum(vcat(1,
repeat(diff(colptr), n)))`, which is a `Vector{Int}` regardless of `Ti`,
while `rowval` kept `Ti`, so the constructor had no matching method. The
fix builds `colptr` with `similar` on the column pointers of the
row-repeated matrix and fills it by offsetting each block of pointers by
the block's `nnz`, so the result is `SparseMatrixCSC{Tv,Ti}` like the
single-count form and like `hcat`/`vcat` in the same file, which also
keep `Ti` without widening. This also drops the three temporary vectors
the old expression allocated.

Tested with `Pkg.test(test_args=["sparsematrix", "concatenation"])` on
Julia 1.14.0-DEV.3359 (nightly); the new test compares `repeat(A32, m,
n)` against the dense result for `m, n` in `0:2` × `0:3` and asserts the
type; whitespace check clean. No dispatch change, no docs change.
Backport candidate (`backport 1.13`): pure bug fix, no new methods.

Written by Claude Code (Fable 5.1) from a type-stability review of
`main`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit ca9a6bc)

Backport adjustment: applied to src/sparsematrix.jl (for src/concatenation.jl) and test/sparsematrix_constructors_indexing.jl.
…mented (#925)

The `sprand` docstring says that a custom `rfn` "should be a function
`rfn(k)`... alternatively, if `rng` is supplied, it should instead be a
function `rfn(rng, k)`". The matrix form honours this, the vector form
does not:

```julia
julia> sprand(10, 10, 0.5, k -> rand(Int8, k))   # works
10×10 SparseMatrixCSC{Int8, Int64} with 52 stored entries: ...

julia> sprand(10, 0.5, k -> rand(Int8, k))
ERROR: MethodError: no method matching (::var"#1#2")(::TaskLocalRNG, ::Int64)
```

`sprand(n, p, rfn)` forwarded `rfn` unchanged to `sprand(rng, n, p,
rfn)`, which calls `rfn(rng, k)`. The fix wraps it as `(r, i) ->
rfn(i)`, exactly as the matrix method `sprand(m, n, p, rfn)` does, so
both forms follow the same contract; the rng methods are unchanged. The
test seeds the default rng and checks that the no-rng vector form with a
one-argument `rfn` produces the same vector as the explicit-rng form
with the two-argument `rfn`, and that both matrix forms accept the same
functions.

Tested with `Pkg.test(test_args=["sparsevector", "constructors"])` on
Julia 1.14.0-DEV.3359 (nightly); whitespace check clean. No dispatch
change; the docstring already describes the fixed behaviour.
Deliberately left out: the `sprand(n, p, rfn, T)` vector method still
calls `rfn(rng, T, k)` (a three-argument form neither documented nor
shared with the matrix method); changing it would alter working calls,
so it is not touched here. Backport candidate: pure bug fix making a
documented call work, no new methods. Note that a two-argument `rfn`
passed to the vector form *without* an rng, which the docs never allowed
and the matrix form never accepted, now needs an explicit rng.

Written by Claude Code (Fable 5.1) from a type-stability review of
`main`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit f32603a)
`fill!` on a view of a sparse matrix returns the parent instead of the
view when either index range is empty, which also makes the return type
a `Union{SubArray, SparseMatrixCSC}`:

```julia
julia> A = sprand(4, 4, 0.5);

julia> fill!(view(A, 1:0, 1:2), 3.0) === A
true

julia> fill!(view(A, 1:2, 1:2), 3.0) === A
false
```

The early return for an empty `I` or `J` in `Base.fill!(V::SubArray{Tv,
<:Any, <:AbstractSparseMatrixCSC{Tv}, ...}, x)` in `src/indexing.jl`
wrote `return A` (the parent) where every other path returns `V`. The
fix returns `V`. The test asserts `@inferred fill!(view, x) === view`
for a non-empty view and for both empty-range views, next to the
existing "fill! for SubArrays" check that the non-empty path returns the
view.

Tested with `Pkg.test(test_args=["sparsematrix", "indexing", "fixed"])`
on Julia 1.14.0-DEV.3359 (nightly); whitespace check clean. No dispatch
change, no docs. Backport candidate: one-line pure bug fix, no new
methods or behaviour beyond the documented `fill!` return value.

Written by Claude Code (Fable 5.1) from a type-stability review of
`main`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 84b5114)

Backport adjustment: applied to src/sparsematrix.jl (for src/indexing.jl) by hand, the surrounding `fill!` code differs (no fixed-pattern branch).
…a dimension of an empty sparse array (#928)

`maximum`, `minimum` and `extrema` along a dimension of an empty sparse
array return a sparse array, where dense input gives an `Array` and
every non-empty sparse input does too. The result type is therefore
inferred as a `Union` for all sparse inputs, empty or not.

```julia
julia> maximum(spzeros(3, 0); dims = 1)
1×0 SparseMatrixCSC{Float64, Int64} with 0 stored entries

julia> maximum(zeros(3, 0); dims = 1)
1×0 Matrix{Float64}

julia> Base.return_types(A -> maximum(A; dims = 1), (SparseMatrixCSC{Float64,Int},))
1-element Vector{Any}:
 Union{SparseMatrixCSC{Float64, Int64}, Matrix{Float64}}
```

Base's `reducedim_init` for `max`, `min` and `_extrema_rf` seeds the
result from the first slice of the reduction, and when that slice is
empty it returns `map(f, slice)` as the result, which for a view of a
sparse array is sparse; a non-empty slice goes through
`reducedim_initarray`, which this package already makes dense. The fix
defines `reducedim_init` for those three ops on the sparse types
(matrices, column-subset views, their adjoints and transposes, and
vectors) to run Base's own seeding, NaN and `missing` handling on a
dense copy of the first slice (one row, column or element, O(slice) to
build), so the empty case is a dense `map` too. Base's `abs`/`abs2` seed
for `max` is restated on the sparse types to stay unambiguous with the
new method. The `sparse = true` keyword path is unchanged, since it
initializes from a dense stand-in.

Touches dispatch: adds `Base.reducedim_init` methods for the sparse
types; the `ambiguous` job passes. Tested with `reductions` and
`ambiguous` on Julia nightly 1.14.0-DEV.3359. Left out:
`Adjoint`/`Transpose` of a `SparseVector` (not covered by the existing
`reducedim_initarray` hook either, so every reduction of it along a
dimension is sparse, and `extrema(v'; dims = 1)` throws a
`MethodError`), and the inference of `maximum(A'; dims = 1)` also needs
the constant propagation fix for the `sparse` keyword on adjoints
(separate PR).

A backport candidate: a pure bug fix (result type of an empty input)
adding no new behaviour; no label applied.

Written by Claude Code (Fable 5.1) from a type-stability review of
`main`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 1b001a6)

Backport adjustment: applied to src/sparsematrix.jl and test/sparsematrix_ops.jl (for src/reductions.jl and test/reductions.jl). The `reducedim_init` methods are defined for `AbstractSparseMatrixCSC` and `AbstractCompressedVector` only: on release-1.13, reductions over adjoints and views of sparse matrices do not go through the sparse kernels (#798 is not backported), so the adjoint and view cases and the `sparse = true` keyword test are dropped.
… from `sparse(F.L)` (#933)

`F \ B`, `F' \ B`, `F.L \ B` and `F \ B'` for a `CHOLMOD.Factor` with a
`SparseMatrixCSC` right-hand side, and `sparse(F.L)` / `sparse(F.LD)`,
infer `Union{Symmetric{T,SparseMatrixCSC{T,Ti}}, SparseMatrixCSC{T,Ti}}`
(`Hermitian` for complex), although they always return a
`SparseMatrixCSC`.

Before:

```julia
julia> using LinearAlgebra, SparseArrays

julia> using SparseArrays: CHOLMOD

julia> Base.return_types(\, (CHOLMOD.Factor{Float64,Int}, SparseMatrixCSC{Float64,Int}))
1-element Vector{Any}:
 Union{Symmetric{Float64, SparseMatrixCSC{Float64, Int64}}, SparseMatrixCSC{Float64, Int64}}

julia> Base.return_types(sparse, (CHOLMOD.FactorComponent{Float64,:L,Int},))
1-element Vector{Any}:
 Union{Symmetric{Float64, SparseMatrixCSC{Float64, Int64}}, SparseMatrixCSC{Float64, Int64}}
```

After:

```julia
julia> Base.return_types(\, (CHOLMOD.Factor{Float64,Int}, SparseMatrixCSC{Float64,Int}))
1-element Vector{Any}:
 SparseMatrixCSC{Float64, Int64}

julia> Base.return_types(sparse, (CHOLMOD.FactorComponent{Float64,:L,Int},))
1-element Vector{Any}:
 SparseMatrixCSC{Float64, Int64}
```

All of them wrapped their `CHOLMOD.Sparse` result with
`sparse(::Sparse)`, which branches on the runtime `stype` and returns a
`Symmetric`/`Hermitian` view when it is nonzero. The output of
`cholmod_spsolve` and of `cholmod_factor_to_sparse` is always
unsymmetric (`stype == 0`; checked at runtime for `cholesky` and `ldlt`,
real and complex), so the four call sites (`\(::Factor,
::SparseMatrixCSC)`, `\(::FactorComponent, ::SparseMatrixCSC)`,
`sparse(::FactorComponent{_,:L})`, `sparse(::FactorComponent{_,:LD})`)
now use `SparseMatrixCSC(::Sparse)`, which is inferred and throws an
`ArgumentError` if a symmetric one ever appeared. The adjoint-factor and
adjoint-right-hand-side methods route through these and are fixed with
them. `sparse(F::Factor)` is unchanged; it already infers
`SparseMatrixCSC`. No method signatures change, so dispatch is
untouched.

Tested on Julia nightly 1.14.0-DEV: `solvers/cholmod` passes,
`.ci/check-whitespace.jl` clean; the added `@inferred` tests fail on
`main` without the source change. They cover `F \ B`, `F' \ B`, `F \ B'`
and `F.PtL \ B` for real `cholesky` and `ldlt`, one complex `cholesky`
solve, and `sparse(F.L)` / `sparse(F.LD)` in the existing
factor-extraction tests. UMFPACK and SPQR are unaffected: their solves
and factor components already infer concrete types.

Independent of #912 and #915, which do not touch these lines. Backport
candidate for 1.13: an inference-only fix with no new methods or
behaviour (the values returned are the same objects as before); no label
applied.

Written by Claude Code (Fable 5.1) from a type-stability review of
`main`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Viral B. Shah <ViralBShah@users.noreply.github.com>
(cherry picked from commit 7f0badd)

Backport adjustment: tests in test/cholmod.jl (for test/solvers/cholmod.jl) assert the index type `Int`, since `Ti` is not bound there on release-1.13. The `F' \\ B` case is dropped: on release-1.13 the adjoint of a CHOLMOD factor solved with a sparse right-hand side returns a `CHOLMOD.Sparse`, which main changed in a PR not labelled for backport.
…`prod` and `mapreduce` as for dense (#932)

`sum`, `prod` and `mapreduce` with `+` or `*` over a sparse array of
small integers give a different result type than dense input when
unstored entries are involved: `prod` and `sum` keep the narrow type (or
give `Int` for unsigned), and a plain `+` widens to `Int` where dense
wraps in the element type. The scalar results therefore infer as a
`Union`.

Before:

```julia
julia> prod(sparse(Int8[2 0; 0 3])), sum(spzeros(UInt8, 2, 2)), mapreduce(abs2, +, sparse(Int8[2 0; 0 3]))
(0, 0, 13)   # Int8, Int64, Int64

julia> Base.return_types(prod, (SparseMatrixCSC{Int8,Int},))
1-element Vector{Any}:
 Union{Int64, Int8}
```

After, as for dense:

```julia
julia> prod(sparse(Int8[2 0; 0 3])), sum(spzeros(UInt8, 2, 2)), mapreduce(abs2, +, sparse(Int8[2 0; 0 3]))
(0, 0x0000000000000000, 13)   # Int64, UInt64, Int8

julia> Base.return_types(prod, (SparseMatrixCSC{Int8,Int},))
1-element Vector{Any}:
 Int64
```

The unstored entries are folded in at once as `f(0) * nzeros` or
`f(0)^nzeros`, so they missed the `reduce_first` widening Base applies
to the stored ones, and the count's `Int` leaked into a plain `+`. The
fix seeds their contribution with `Base.reduce_first(op, f(0))`,
converted to the accumulator's type so an `init` or another mapped value
that is wider still wins, and multiplies a bit integer by the count in
its own type so a plain `+` on `Int8` wraps as dense does.

Tested with `reductions` and `sparsevector` on Julia nightly
1.14.0-DEV.3359, comparing type and value against dense for `Int8` and
`Bool`, with stored and unstored zeros, scalar, with `dims`, with `init`
and with `sparse = true`. No dispatch change, no docs needed. A backport
candidate: a result-type bug fix with no new methods.

Written by Claude Code (Fable 5.1).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Viral B. Shah <ViralBShah@users.noreply.github.com>
(cherry picked from commit 6657313)

Backport adjustment: applied to src/sparsematrix.jl and test/sparsematrix_ops.jl (for src/reductions.jl and test/reductions.jl); the `sparse = true` comparison is dropped from one test, since that keyword comes from #798, which is not backported.
…operty access (#930)

`F.L`, `F.PtL`, `F.UP`, `F.D`, ... on a `CHOLMOD.Factor` infer to
`FactorComponent{Tv,_A,Ti} where _A`, so every solve through a
component, including the documented `F.UP \ (F.PtL \ b)` idiom, infers
`Any` and dispatches dynamically. `F.prow` / `F.pcol` on a `QRSparse`
infer `Vector` and a four-way `Union{ReentrantLock, QRSparseQ,
SparseMatrixCSC, Vector}`, and the same through `lq`
(`AdjointQRSparse`).

Before:

```julia
using LinearAlgebra, SparseArrays
using SparseArrays: CHOLMOD, SPQR
Base.return_types(F -> F.L, (CHOLMOD.Factor{Float64,Int},))
# FactorComponent{Float64, _A, Int64} where _A
Base.return_types((F, b) -> F.UP \ (F.PtL \ b), (CHOLMOD.Factor{Float64,Int}, Vector{Float64}))
# Any
Base.return_types(F -> F.pcol, (SPQR.QRSparse{Float64,Int},))
# Union{ReentrantLock, QRSparseQ{Float64,Int64}, SparseMatrixCSC{Float64,Int64}, Vector}
```

After:

```julia
Base.return_types(F -> F.L, (CHOLMOD.Factor{Float64,Int},))
# SparseArrays.CHOLMOD.FactorComponent{Float64, :L, Int64}
Base.return_types((F, b) -> F.UP \ (F.PtL \ b), (CHOLMOD.Factor{Float64,Int}, Vector{Float64}))
# Vector{Float64}
Base.return_types(F -> F.pcol, (SPQR.QRSparse{Float64,Int},))
# Vector{Int64}
```

`getproperty(::Factor)` is `@inline` and sees a constant `sym`, but the
symbol is not constant-propagated into `FactorComponent(F, sym)`, which
is what puts it in the type parameter; `Base.@constprop :aggressive` on
that constructor makes `F.L` infer `FactorComponent{Float64,:L,Int}`,
`F.L'` infer the `:U` component, and the component solves infer
`Vector{T}`. The `QRSparse` `getproperty` branches for `:prow` / `:pcol`
read `F.rpivinv` / `F.cpiv` (and `P.pcol` / `P.prow` in the adjoint
method) through `getproperty` again, and inference refuses the recursive
edge; reading the fields with `getfield` makes all four infer
`Vector{Ti}`. No method signature changes, so dispatch is untouched.

Tested on Julia nightly 1.14.0-DEV (linux x64): `solvers/cholmod` and
`solvers/spqr` pass (1812 tests), `.ci/check-whitespace.jl` clean; the
added `@inferred` tests fail on `main` without the source change. Real
and complex factors from `cholesky` and `ldlt`, and both index types for
`qr`/`lq`. The After output above was re-checked on nightly (macOS).

Independent of #912 and #915: #912 edits the neighbouring
`getproperty(::Factor)` and #915 the neighbouring `copy(::QRSparse)`,
but neither touches these lines, so no conflict is expected. Backport
candidate for 1.13: an inference-only fix that adds no methods or
behaviour; no label applied.

Written by Claude Code (Fable 5.1) from a type-stability review of
`main`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 53b8970)

Backport adjustment: the `AdjointQRSparse` (`lq`) hunk and its test are dropped, as `lq` of a sparse matrix is not on release-1.13; the CHOLMOD tests assert the index type `Int`, since `Ti` is not bound there in test/cholmod.jl.
…ting-point eltypes (#921)

`\(::AbstractSparseMatrixCSC, B)`, its `Adjoint`/`Transpose` variants
and `factorize(::AbstractSparseMatrixCSC)` wrap any square matrix that
passes `ishermitian` in `Hermitian`. For an integer eltype that reaches
LinearAlgebra's generic `factorize(::HermOrSym)`, which is a *dense*
Bunch–Kaufman in `Rational{BigInt}` (O(n²) memory, exact arithmetic),
while a non-symmetric integer matrix converts to `Float64` and goes to
UMFPACK. Dense `Matrix{Int} \ Vector{Int}` returns `Vector{Float64}` in
both cases.

Before:

```julia
julia> sparse([2 1; 1 2]) \ [1, 2]
2-element Vector{Rational{BigInt}}: ...
julia> sparse([2 1; 3 2]) \ [1, 2]
2-element Vector{Float64}: ...
julia> Base.return_types(\, (SparseMatrixCSC{Int,Int}, Vector{Int}))
1-element Vector{Any}: Union{Vector{Rational{BigInt}}, Vector{Float64}}
julia> factorize(sparse([2 1; 1 2]))
BunchKaufman{Rational{BigInt}, Matrix{Rational{BigInt}}, Vector{Int64}} ...
```

After:

```julia
julia> sparse([2 1; 1 2]) \ [1, 2]
2-element Vector{Float64}:
 0.0
 1.0

julia> sparse([2 1; 3 2]) \ [1, 2]
2-element Vector{Float64}:
 0.0
 1.0

julia> Base.return_types(\, (SparseMatrixCSC{Int,Int}, Vector{Int}))
1-element Vector{Any}:
 Vector{Float64} (alias for Array{Float64, 1})

julia> factorize(sparse([2 1; 1 2]))
SparseArrays.UMFPACK.UmfpackLU{Float64, Int64}
L factor:
2×2 SparseMatrixCSC{Float64, Int64} with 3 stored entries:
 1.0   ⋅
 0.5  1.0
U factor:
2×2 SparseMatrixCSC{Float64, Int64} with 3 stored entries:
 0.666667  0.333333
  ⋅        0.5
```

The same happened for `A'`, `transpose(A)`, `Complex{Int}` and a
`SparseVector` right-hand side. The fix gates the three Hermitian
branches on the eltype not being an `Integer` or `Complex{<:Integer}`,
so integer matrices fall through to the `lu` branch, which already
converts to floating point and to the promoted result type. Every other
eltype keeps its current path: floating point still goes to the sparse
Cholesky/LDLt, and `Rational` still solves exactly through
LinearAlgebra's generic factorization (an earlier revision rerouted
`Rational` too, which returned inexact binary fractions; a test now pins
the exact result). No floating-point path changes. The triangular and
diagonal branches already matched dense for integers and are untouched.

Tested on Julia nightly: `solvers/solvers` and `linalg` suites (1242
pass), `ambiguous` (13 pass, 1 known broken), and the whitespace check;
the new testset fails without the source change. No new methods or
dispatch changes beyond a private helper, no docs impact.

Left out: an explicit `Hermitian(sparse_int) \ b` by the user still goes
through LinearAlgebra's generic dense path; that is LinearAlgebra's
`factorize(::HermOrSym)` and out of scope here. A non-Hermitian
`Rational` sparse matrix already went through floating-point UMFPACK on
`main` and is unchanged.

Backport candidate: yes, a pure bug fix with no new methods or behaviour
for float eltypes, so labelled `backport 1.13`.

Written by Claude Code (Fable 5.1) from a type-stability review of
`main`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Viral B. Shah <ViralBShah@users.noreply.github.com>
(cherry picked from commit 683d9b0)

Backport adjustment: the reroute to `lu` applies to a dense right-hand side only. On release-1.13 UMFPACK cannot solve with a sparse right-hand side, so a sparse one keeps the previous Hermitian path, which works there; the test in test/linalg_solvers.jl (for test/solvers/solvers.jl) checks only values for a sparse right-hand side, skips the complex transpose case that fails on release-1.13 before and after this change, and does not use `@inferred`, as `\\` is not inferred for these eltypes on release-1.13.
…apply `f` to the empty case (#922)

The index returned by `findmin`/`findmax` on a `SparseVector` (and so
`argmin`/`argmax`) depends on which branch runs, and with no stored
entries `findmin(f, x)` ignores `f`.

Before:

```julia
julia> using SparseArrays

julia> Base.return_types(findmax, (SparseVector{Float64,Int},))
1-element Vector{Any}:
 Tuple{Float64, Union{Nothing, Int64}}

julia> Base.return_types(argmin, (SparseVector{Float64,Int32},))
1-element Vector{Any}:
 Union{Nothing, Int32, Int64}

julia> x = SparseVector(5, Int32[2, 4], [2.0, -1.0]); typeof(findmax(x)[2]), typeof(findmin(x -> 1.0, x)[2])
(Int32, Int64)

julia> findmin(x -> x + 1, spzeros(3)), findmin(x -> x + 1, zeros(3))
((0.0, 1), (1.0, 1))
```

After:

```julia
julia> Base.return_types(findmax, (SparseVector{Float64,Int},))
1-element Vector{Any}:
 Tuple{Float64, Int64}

julia> Base.return_types(argmin, (SparseVector{Float64,Int32},))
1-element Vector{Any}:
 Int64

julia> x = SparseVector(5, Int32[2, 4], [2.0, -1.0]); typeof(findmax(x)[2]), typeof(findmin(x -> 1.0, x)[2])
(Int64, Int64)

julia> findmin(x -> x + 1, spzeros(3)), findmin(x -> x + 1, zeros(3))
((1.0, 1), (1.0, 1))
```

Base's `Int` position was returned when every entry is stored,
`nzinds[index]::Ti` when a stored entry wins, and the
`Union{Nothing,Int}` from `findfirst` in the search for the first zero.
Every path now returns an `Int`, as dense `findmin`/`findmax` do
(`Vector{Int}` keys). The stored winner from `findmin(f, nonzeros(x))`
is compared with `f(0)` at the first implicit zero using the same test
Base's dense scan uses to move on to a later element (`Base.isgreater`
for `findmin`, `isless` for `findmax`), with a tie going to the lower
index. That replaces the `<`/`isnan` special-casing and the two
`findfirst` searches with one O(nnz) scan, and it also makes the result
agree with dense when `f` of a stored value ties with `f(0)`, and on NaN
and signed zeros (before, `findmin(x -> x^2 - x, SparseVector(3, [1],
[1.0]))` gave index 2 where dense gives 1, and `findmin(SparseVector(3,
[3], [-0.0]))` gave `(0.0, 1)` where dense gives `(-0.0, 3)`). The empty
case returns `f(zero(T))`.

Tested on nightly 1.14.0-DEV.3359: `sparsevector` suite (5663 passed),
whitespace check, and a local sweep of 27,000 random sparse vectors
(`Ti` in `Int`, `Int32`, `Int8`; stored zeros, `-0.0`, NaN; six `f`s;
complex through `abs`/`abs2`) checked `===` against `Vector(x)` with
`@inferred`. The committed test is a small subset with `Ti = Int32`: the
empty, implicit-zero-first, stored-zero, all-stored, `-0.0` and NaN
vectors, a tie with `f(0)` and NaN at the implicit zero, one complex
case and the empty `f` case. No dispatch change, no docs change.

Left out: `length`/`axes` of a `SparseVector{Tv,Int32}` are themselves
`Int32` (the `n::Ti` field), which is why `firstindex(x)` needed an
`Int` conversion here; that is a separate issue. `findmin(x; dims=1)` on
a sparse vector goes through Base's generic `reducedim` path and is
untouched.

Backport candidate for 1.13: a bug fix with no new methods or public
surface (the helper `_firstimplicitzero` is internal).

Written by Claude Code (Fable 5.1) from a type-stability review of
`main`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Viral B. Shah <ViralBShah@users.noreply.github.com>
(cherry picked from commit f265466)
…dimensions without touching its pattern (#964)

Indexing a `FixedSparseCSC` with an unsorted row vector throws, because
`getindex_general` in `src/indexing.jl` permutes the rows of the already
fixed result of `getindex_I_sorted`, and `F[I, j]` rebuilds a
`SparseVector` from the read-only buffers of a fixed `F[I, [j]]`.
`cumsum`, `cumprod` and `accumulate` along a dimension of a fixed array
throw too, because Base allocates their result with the pattern-keeping
`similar(A, T)`.

**Before**
```julia
julia> F = SparseArrays.fixed(sparse([1,2,3],[1,2,3],[1.0,2.0,3.0]));

julia> F[[2,1],[1,2]]
ERROR: ArgumentError: cannot modify a ReadOnly array, the sparsity pattern of a fixed sparse array is read-only

julia> F[[1,2], 1]
ERROR: MethodError: no method matching SparseVector(::Int64, ::SparseArrays.ReadOnly{Int64, 1, Vector{Int64}}, ::Vector{Float64})

julia> cumsum(SparseArrays.fixed(sparse([1,2],[1,2],[1.0,2.0])), dims=1)
ERROR: ArgumentError: cannot store a new entry at (2, 1) in a FixedSparseCSC, its sparsity pattern is read-only
```

**After**
```julia
julia> F[[2,1],[1,2]]
2×2 SparseArrays.FixedSparseCSC{Float64, Int64} with 2 stored entries:
  ⋅   2.0
 1.0   ⋅

julia> F[[1,2], 1]
2-element SparseArrays.FixedSparseVector{Float64, Int64} with 1 stored entry:
  [1]  =  1.0

julia> cumsum(SparseArrays.fixed(sparse([1,2],[1,2],[1.0,2.0])), dims=1)
2×2 SparseMatrixCSC{Float64, Int64} with 3 stored entries:
 1.0   ⋅
 1.0  2.0
```

`getindex_general` and the `F[I, j]` path now index `_unsafe_unfix(A)`,
permute or rewrap the plain result, and fix its pattern afterwards, so
indexing keeps returning an array with the fixedness of its input as the
existing test requires; the input's pattern is never written. For the
accumulations, Base offers no shape-taking `similar` hook, so `cumsum`,
`cumprod` and `accumulate` on `FixedSparseCSC` and `FixedSparseVector`
delegate to the plain array and return a writable
`SparseMatrixCSC`/`SparseVector`, the way `circshift` already does: the
result has fill-in, so there is no pattern to share.

Left out: `v[[1,3]]` on a `FixedSparseVector` still throws a
`MethodError`, since that path builds a `SparseMatrixCSC` from the
read-only index vector; it is a separate kernel. Touches dispatch.
Backport candidate.

Written by Claude Code (Claude Fable 5.1) in an autonomous session.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 2b6db54)

Backport adjustment: src/fixed.jl does not exist on release-1.13; the `cumsum`/`cumprod`/`accumulate` methods go to src/sparsevector.jl after the other `FixedSparseVector` methods, and the `getindex` hunks to src/sparsematrix.jl and src/sparsevector.jl (for src/indexing.jl).
…D.Factor` (#942)

`ldiv!(x, F, b)` with an `UmfpackLU` or a `CHOLMOD.Factor` rejects only
`x === b`, so overlapping views pass, and with UMFPACK iterative
refinement on the solve reads `b` after writing `x`
(`src/solvers/umfpack.jl` `solve!`, `src/solvers/cholmod.jl` `ldiv!`).

**Before**
```julia
using LinearAlgebra, SparseArrays
A = sparse([2.0 1 0; 1 3 1; 0 1 4])
F = lu(A)
F.control[SparseArrays.UMFPACK.JL_UMFPACK_IRSTEP] = 2
B = A * ones(3, 2)
ldiv!(view(B, :, 1), F, view(vec(B), 1:3)); B[:, 1]
# [0.2592592592592592, -0.07407407407407393, 0.07407407407407399]
ldiv!(B, F, B)
# ERROR: ArgumentError: output array must not be aliased with input array
deepcopy(SparseArrays.CHOLMOD.CholmodWS(cholesky(A))).Y[]   # shares the source's Y/E handles
```

**After**
```julia
ldiv!(view(B, :, 1), F, view(vec(B), 1:3)); B[:, 1]
# [1.0000000000000002, 0.9999999999999998, 1.0]
ldiv!(B, F, B)
# 3×2 Matrix{Float64}: ones
deepcopy(SparseArrays.CHOLMOD.CholmodWS(cholesky(A))).Y[]
# Ptr{cholmod_dense_struct}(0x0000000000000000)
```

The `===` check becomes `Base.mightalias(x, b) && (b = copy(b))` in
UMFPACK's `solve!` and CHOLMOD's `ldiv!`, and once up front in
`_Aq_ldiv_B!` so a matrix right-hand side is copied once rather than per
column. `copy` rather than `Base.unalias`, because unaliasing a
`SubArray` copies its whole parent. `deepcopy(::CholmodWS)` returns a
fresh workspace instead of a field-wise copy whose shared `Y`/`E`
handles both copies would free.

Left out: SPQR, which already gathers `B` into its workspace before
writing `X`. Touches dispatch (adds `deepcopy_internal` for `CholmodWS`,
memoized in the `IdDict` so repeated references resolve to one copy).
Backport candidate.

Written by Claude Code (Claude Fable 5.1) in an autonomous session.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 0bf1d1b)

Backport adjustment: release-1.13 has no `CholmodWS` and no workspace keyword on CHOLMOD `ldiv!`, so the `deepcopy` hunk and the workspace parts of the test are dropped. Where an aliased right-hand side is copied, the solver is called again with the copy rather than rebinding `b`: in release-1.13's UMFPACK matrix kernel, `b` is a column view, and rebinding it to a `Vector` made it type-unstable and broke the zero-allocation test in test/umfpack.jl.
Indexing a `FixedSparseVector` with an index vector, a Bool mask or a
step range throws, because the two `getindex(x::AbstractSparseVector,
I::AbstractVector)` / `I::AbstractArray` methods in
`src/sparsevector.jl` punt to `SparseMatrixCSC` built from
`nonzeroinds(x)`, which is a `ReadOnly` vector for a fixed vector and
not a valid `SparseMatrixCSC` field.

**Before**
```julia
julia> v = SparseArrays.fixed(sparsevec([1, 3], [1.0, 2.0], 4));

julia> v[[3, 1]]
ERROR: MethodError: no method matching SparseMatrixCSC(::Int64, ::Int64, ::Vector{Int64}, ::SparseArrays.ReadOnly{Int64, 1, Vector{Int64}}, ::Vector{Float64})

julia> v[[true, false, true, false]]
ERROR: MethodError: no method matching SparseMatrixCSC(::Int64, ::Int64, ::Vector{Int64}, ::SparseArrays.ReadOnly{Int64, 1, Vector{Int64}}, ::Vector{Float64})
```

**After**
```julia
julia> v[[3, 1]]
2-element SparseArrays.FixedSparseVector{Float64, Int64} with 2 stored entries:
  [1]  =  2.0
  [2]  =  1.0

julia> v[[true, false, true, false]]
2-element SparseArrays.FixedSparseVector{Float64, Int64} with 2 stored entries:
  [1]  =  1.0
  [2]  =  2.0
```

Both methods now build the scratch matrix from `_unsafe_unfix(x)`, which
shares the underlying buffers without copying, and wrap the result in
`@if_move_fixed x`, the same rule the unit-range and colon paths already
follow: a fixed input gives a fixed result with fresh buffers, and an
unfixed input is unchanged. A matrix-shaped index on a fixed vector
returns a `FixedSparseCSC`, matching what a fixed matrix returns for the
same index.

Left out: the matrix-side `getindex_general` fixedness, which is handled
elsewhere. Backport candidate.

Written by Claude Code (Claude Fable 5.1) in an autonomous session.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 7fbaf6a)

Backport adjustment: applied by hand to src/sparsevector.jl, whose surrounding code differs slightly (the `Bool` mask methods have no `checkbounds` on release-1.13).
#965)

`istriu(A, k)` and `istril(A, k)` for `AbstractSparseMatrixCSC` in
`src/sparsematrix.jl` scan the columns `1:min(n, m-1)` and `2:n`, which
is only right for `k == 0`: `istriu` skips the columns that gain
sub-band rows when `k > 0`, and `istril` skips column 1 when `k < 0`.
Through LinearAlgebra's forwarding the same answers come back from
`istril(adjoint(S), -k)` and `istriu(transpose(S), -k)`.

**Before**
```julia
julia> A = sparse([1], [2], [1.0], 1, 2); istriu(A, 2), istriu(Matrix(A), 2)
(true, false)

julia> B = sparse([1], [1], [1.0], 2, 1); istril(B, -1), istril(Matrix(B), -1)
(true, false)

julia> istril(adjoint(A), -2), istril(Matrix(adjoint(A)), -2)
(true, false)
```

**After**
```julia
julia> A = sparse([1], [2], [1.0], 1, 2); istriu(A, 2), istriu(Matrix(A), 2)
(false, false)

julia> B = sparse([1], [1], [1.0], 2, 1); istril(B, -1), istril(Matrix(B), -1)
(false, false)

julia> istril(adjoint(A), -2), istril(Matrix(adjoint(A)), -2)
(false, false)
```

The per-column walk already compared each stored row against `col - k`;
the column ranges now account for `k` as well. Both predicates handle
offsets outside the matrix's diagonal range before doing arithmetic with
`k`. Within that range, `istriu` computes the equivalent of `min(n,
m+k-1)` by comparing before adding, avoiding overflow even for a matrix
with `typemax(Int)` rows; `istril` starts at `max(1, k+2)`. Both still
touch stored entries only, and a stored zero still does not count as a
nonzero. Regression tests cover rectangular and empty matrices, real and
complex values, adjoint/transpose forwarding, stored zeros, and extreme
integer offsets. Extreme-offset tests assert the expected result
directly because nightly's dense predicates also overflow for some of
these inputs.

Backport candidate.

Written by Claude Code (Claude Fable 5.1) in an autonomous session.

Overflow handling and regression tests updated by OpenAI Codex.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Viral B. Shah <ViralBShah@users.noreply.github.com>
(cherry picked from commit 76665a5)

Backport adjustment: applied by hand: release-1.13's `istriu`/`istril` still index `colptr` directly (#852 rewrote them with `nzrange` on main), so the band checks and loop bounds were carried over into that form.
…loat` and `complex` of fixed-pattern arrays (#960)

`is_hermsym` in src/sparsematrix.jl advances a `copy` of the column
pointers as its tracker; for a `FixedSparseCSC` that copy is still
`ReadOnly`, so `issymmetric`, `ishermitian`, `factorize` and `\` throw
on a fixed matrix. Unary `-`, `float` and `complex` of a fixed matrix
also lose the fixed type, and `float`/`complex` of an integer
`FixedSparseVector` throw.

**Before**
```julia
julia> using SparseArrays, LinearAlgebra; using SparseArrays: fixed

julia> F = fixed(sparse([4.0 1 0; 1 4 1; 0 1 4]));

julia> issymmetric(F)   # same for ishermitian(F), factorize(F), F \ [1.0, 2, 3]
ERROR: ArgumentError: cannot modify a ReadOnly array, the sparsity pattern of a fixed sparse array is read-only

julia> typeof(-F), typeof(-fixed(sparsevec([1, 3], [1.0, 2.0], 4)))
(SparseMatrixCSC{Float64, Int64}, SparseArrays.FixedSparseVector{Float64, Int64})

julia> typeof(float(fixed(sparse([1 0; 0 2]))))   # Base's generic array conversion
SparseMatrixCSC{Float64, Int64}

julia> float(fixed(sparsevec([1, 3], [1, 2], 4)))
ERROR: MethodError: no method matching SparseVector(::Int64, ::SparseArrays.ReadOnly{Int64, 1, Vector{Int64}}, ::Vector{Float64})
```

**After**
```julia
julia> issymmetric(F), ishermitian(F), typeof(factorize(F)), F \ [1.0, 2, 3]
(true, true, SparseArrays.CHOLMOD.Factor{Float64, Int64}, [0.17857142857142855, 0.28571428571428575, 0.6785714285714286])

julia> typeof(-F), typeof(-fixed(sparsevec([1, 3], [1.0, 2.0], 4)))
(SparseArrays.FixedSparseCSC{Float64, Int64}, SparseArrays.FixedSparseVector{Float64, Int64})

julia> typeof(float(fixed(sparse([1 0; 0 2])))), typeof(float(fixed(sparsevec([1, 3], [1, 2], 4))))
(SparseArrays.FixedSparseCSC{Float64, Int64}, SparseArrays.FixedSparseVector{Float64, Int64})
```

The tracker is now `Vector(getcolptr(A))`. Unary `-` of a fixed array
returns a fixed array sharing the source pattern with fresh nonzeros, as
`copy` of a fixed array does; the vector method sits next to the
`SparseVector` one instead of going through broadcast.
`float`/`complex(::FixedSparseCSC)` are defined next to the
`SparseMatrixCSC` methods and share the pattern the same way; the vector
methods build their result through a `_withnonzeros` helper that keeps
the input's type. Stored zeros stay stored.

Left out: `conj(::FixedSparseCSC{<:Complex})` still returns a
`SparseMatrixCSC`; an indefinite Hermitian fixed matrix still fails in
`factorize`/`\` because the Cholesky fallback calls `ldlt!`, which takes
a `SparseMatrixCSC` only (a `@test_broken` in test/solvers/solvers.jl).
Touches dispatch. Backport candidate. Other batch PRs edit
src/sparsematrix.jl and src/sparsevector.jl in disjoint regions.

Written by Claude Code (Claude Fable 5.1) in an autonomous session.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 069abf7)

Backport adjustment: the hunks for src/constructors.jl go to src/sparsematrix.jl, and the tests from test/solvers/solvers.jl and test/fixed.jl go to test/fixed.jl with a local `same_pattern` helper. The `factorize`/`\\` checks on a fixed Hermitian matrix are dropped: on release-1.13 they still fail later in `copytrito!`, which main fixed in PRs not labelled for backport.
…970)

The pair and N-ary `map`/`broadcast` kernels in `src/higherorderfns.jl`
convert their end-of-column sentinel `numrows(C) + 1` to the input index
type, so any sparse array whose row count equals `typemax(Ti)` throws
before the kernel starts. The vector column bound `colboundind` converts
`nnz + 1` the same way, so a fully stored vector of that length fails
too.

**Before**
```julia
julia> A = SparseMatrixCSC(127, 1, Int8[1, 2], Int8[1], [1.0]); x = SparseVector(127, Int8[1], [1.0]);

julia> map(+, A, A)      # likewise A .+ A, map(+, A, A, A), x .+ x
ERROR: InexactError: trunc(Int8, 128)

julia> y = SparseVector(127, Int8.(1:127), ones(127)); y .+ y
ERROR: InexactError: trunc(Int8, 128)
```

**After**
```julia
julia> map(+, A, A) == 2Array(A) && SparseArrays.indtype(map(+, A, A)) == Int8
true

julia> (A .+ A, map(+, A, A, A), x .+ x, y .+ y) .== (2Array(A), 3Array(A), 2Array(x), 2Array(y))
(true, true, true, true)

julia> n = Int128(typemax(Int)) + 1; w = SparseVector(n, [n], [1.0]);   # indices wider than Int are kept

julia> nonzeroinds(map!(+, SparseVector(n, [n], [0.0]), w, w)) == [n]
true
```

Each kernel compares rows in a working type `W = promote_type(Int,
indtype(A), indtype(B), ...)`: the sentinels are `W(numrows(C)) +
one(W)` and the stored row indices are loaded as `W(storedinds(A)[Ak])`
(the N-ary helpers `_rowforind`, `_fusedupdate`, `_initrowforcol` and
`_fusedupdatebc` use `oftype(rowsentinel, ...)`), so small index types
widen to `Int`, `Int128` stays `Int128`, and every comparison variable
has one concrete type. The vector `colstartind`/`colboundind` return
`Int` storage positions, since a vector has no `Ti`-typed column
pointer; the matrix versions keep reading `colptr`. The `Ci::indtype(C)`
declarations and the typed `for Ci::indtype(C)` loops go, because with a
wider source they lowered to `Union{Ti,Int}` temporaries; the values
written are at most `numrows(C)` and convert in `storedinds(C)[Ck] = Ci`
as before. Loop order and results are unchanged; a seeded sweep over the
pair, N-ary and vertically expanding kernels with `Int8`/`Int32`/`Int64`
indices hashes identically to main. The new tests assert the kernels
have no `Union{Ti,Int}` local for `Int8` and `Int32`.

Left out: a dense-structured 127-row `Int8` matrix cannot exist because
its column pointer would need 128, so the non-zero-preserving case is
tested on a vector only. Backport candidate.

Written by Claude Code (Claude Fable 5.1) in an autonomous session.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 5f2a954)

Backport adjustment: the two-argument and n-ary kernel hunks were applied by hand, as release-1.13 has no `_checkfixedpattern` (that hunk is dropped). The test defines the `hasunionlocal` helper locally (it lives in test/testhelpers.jl on main) and qualifies `SparseArrays.nonzeroinds`.
… `setindex!` kernel and check the value's shape (#946)

`setindex!(A::AbstractSparseMatrixCSC, x, I::AbstractMatrix{Bool})` in
`src/indexing.jl` has its own column walk that never compares
`length(x)` with `count(I)`, so it reads `x` past its end under
`@inbounds`, and it duplicates the linear-index kernel below it, which
in turn accepts a Bool vector of the wrong length, writes a fixed
pattern partially before throwing, and reads `I` and `x` while rewriting
the storage they may alias.

**Before**
```julia
julia> S = sparse([1 0; 0 0.0]); S[[true false; false true]] = [1.0]; S
2×2 SparseMatrixCSC{Float64, Int64} with 2 stored entries:
 1.0            ⋅
     ⋅   6.6578e-310            # uninitialized memory; BoundsError under --check-bounds=yes

julia> S[[true false; false true]] = [1.0, 2.0, 3.0]   # tail silently dropped
julia> S[falses(2, 2)] = [1.0]                         # accepted
julia> S[[true, false, true]] = [1.0, 2.0]             # accepted
julia> F = SparseArrays.fixed(sparse([1 0; 0 2.0])); F[[true true; false true]] = [5.0, 6.0, 7.0]
ERROR: ArgumentError: cannot store a new entry at (1, 2) in a FixedSparseCSC, its sparsity pattern is read-only
julia> F[1, 1]
5.0                                                    # written before the throw
julia> A = sparse(reshape([2, 1], 2, 1)); A[nonzeros(A)] = [20, 10]   # I aliases the storage
ERROR: BoundsError                                     # under --check-bounds=yes; a segfault otherwise
julia> A = sparse(reshape([1, 2], 2, 1)); A[trues(2, 1)] = view(nonzeros(A), 2:-1:1); Matrix(A)
2×1 Matrix{Int64}:
 2
 2                                                     # should be [2; 1]
```

**After**
```julia
julia> S[[true false; false true]] = [1.0]
ERROR: DimensionMismatch: tried to assign 1 elements to 2 destinations
julia> S[[true false; false true]] = [1.0, 2.0, 3.0]
ERROR: DimensionMismatch: tried to assign 3 elements to 2 destinations
julia> S[falses(2, 2)] = [1.0]
ERROR: DimensionMismatch: tried to assign 1 elements to 0 destinations
julia> S[[true, false, true]] = [1.0, 2.0]
ERROR: BoundsError: attempt to access 2×2 SparseMatrixCSC{Float64, Int64} with 1 stored entry at index [Bool[1, 0, 1]]
julia> F[[true true; false true]] = [5.0, 6.0, 7.0]
ERROR: ArgumentError: cannot store a new entry at (1, 2) in a FixedSparseCSC, its sparsity pattern is read-only
julia> F[1, 1]
1.0                                                    # untouched
julia> A = sparse(reshape([2, 1], 2, 1)); A[nonzeros(A)] = [20, 10]; Matrix(A)
2×1 Matrix{Int64}:
 10
 20
julia> A = sparse(reshape([1, 2], 2, 1)); A[trues(2, 1)] = view(nonzeros(A), 2:-1:1); Matrix(A)
2×1 Matrix{Int64}:
 2
 1
```

2000×2000, 1% stored, min of 5 runs, same Julia, Before = main, After =
this PR's final commit. `Matrix{Bool}` is flat because scanning the
dense mask dominates; the new path allocates the index vector (8 bytes
per true); the unaliasing check is not measurable. The last four rows
are a mask with a single true on a stored entry in the last column (an
update, the worst case for a scan of the mask).

| mask | trues | values | Before | After |
|---|---|---|---|---|
| BitMatrix | 0.1% | random | 3.52 ms, 2.07 MiB | 0.27 ms, 2.21 MiB |
| BitMatrix | 0.1% | zeros | 3.41 ms, 0 B | 0.16 ms, 31 KiB |
| Matrix{Bool} | 0.1% | random | 2.44 ms, 2.07 MiB | 1.42 ms, 2.21 MiB |
| sparse Bool | 0.1% | random | 8.12 ms, 2.07 MiB | 0.24 ms, 2.21 MiB |
| sparse Bool | 0.1% | zeros | 8.01 ms, 0 B | 0.12 ms, 31 KiB |
| BitMatrix | 0.1% in pattern | updates | 3.42 ms, 0 B | 0.15 ms, 31 KiB
|
| sparse Bool | 0.1% in pattern | updates | 7.98 ms, 0 B | 0.11 ms, 31
KiB |
| BitMatrix | 1% | random | 3.84 ms, 2.07 MiB | 1.00 ms, 2.50 MiB |
| BitMatrix | 1% | zeros | 4.14 ms, 0 B | 0.78 ms, 315 KiB |
| Matrix{Bool} | 1% | random | 2.72 ms, 2.07 MiB | 2.13 ms, 2.50 MiB |
| sparse Bool | 1% | random | 16.71 ms, 2.07 MiB | 0.79 ms, 2.50 MiB |
| sparse Bool | 1% | zeros | 17.15 ms, 0 B | 0.58 ms, 315 KiB |
| BitMatrix | 1% in pattern | updates | 3.96 ms, 0 B | 0.53 ms, 314 KiB
|
| sparse Bool | 1% in pattern | updates | 16.73 ms, 0 B | 0.33 ms, 314
KiB |
| column-range view of sparse Bool | 1, in pattern | update | 2257 µs, 0
B | 2.8 µs, 80 B |
| Transpose of sparse Bool | 1, in pattern | update | 2283 µs, 0 B | 2.8
µs, 80 B |
| Adjoint of sparse Bool | 1, in pattern | update | 2282 µs, 0 B | 2.8
µs, 80 B |
| transpose of a column-range view of sparse Bool | 1, in pattern |
update | 2847 µs, 0 B | 3.0 µs, 80 B |

The logical method becomes `checkbounds(A, I)` followed by the
linear-index method on `_masklinearindices(I)`: a walk of the stored
entries for a sparse mask, its column views
(`SparseMatrixCSCOrColumnSubset`) and the `Transpose`/`Adjoint` of
either (parent walked, indices sorted), and `findall(vec(I))` otherwise.
The linear method first copies the resolved indices and `x` when
`Base.mightalias` says they share storage with `nonzeros`, `rowvals` or
`getcolptr` of `A`, runs `setindex_shape_check(x, n)` and a
`checkbounds` for Bool vectors before the fixed-pattern branch and the
empty return, always reserves `n` slots when it grows the storage (the
`x == 0` test was dead for an array `x`), and sends a fixed `A` to
`_setindex_fixed!`, which locates every index and rejects a nonzero
outside the pattern before writing anything.

Left out: scalar `S[mask] = v`, which still takes Base's per-element
path. Backport candidate.

Written by Claude Code (Claude Fable 5.1) in an autonomous session.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 2c4594f)

Backport adjustment: ported by hand to src/sparsematrix.jl (for src/indexing.jl). The fixed-pattern part (`_setindex_fixed!`) is dropped: release-1.13's linear-index kernel has no fixed-pattern branch and no `_throwfixedinsert`, both from later fixed-pattern work on main. The sparse mask walk is defined for `AbstractSparseMatrixCSC{Bool}` and its transposes, as release-1.13 has no `SparseMatrixCSCOrColumnSubset`; views of a sparse mask take the `findall` path. The fixed-pattern test cases are dropped accordingly.
…he diagonal scaling loops (#944)

`D \ A` and `A / D` with a sparse `A` and a singular `Diagonal` return
`Inf` instead of throwing, because the out-of-place kernels `ldiv!(C, D,
A)` and `LinearAlgebra._rdiv!(C, A, D)` in `src/linalg.jl` never check
the diagonal; they and `lmul!`/`rmul!`/`ldiv!`/`rdiv!` also carry five
copies of the same stored-entry loop.

**Before**
```julia
julia> D = Diagonal([1.0, 0.0]); A = sparse([1.0 2.0; 3.0 4.0]);

julia> D \ A
2×2 SparseMatrixCSC{Float64, Int64} with 4 stored entries:
 1.0  2.0
 Inf  Inf

julia> A / D
2×2 SparseMatrixCSC{Float64, Int64} with 4 stored entries:
 1.0  Inf
 3.0  Inf
```

**After**
```julia
julia> D \ A
ERROR: SingularException(2)

julia> A / D
ERROR: SingularException(2)
```

All six diagonal scaling methods now share one `_scalecols!` /
`_scalerows!` pair under a "diagonal scaling" section of
`src/linalg.jl`, with `_checkscaledims` and `_checknonsingular` (first
zero -> `SingularException(i)`) run before anything is written; the
2-arg `ldiv!`/`rdiv!` are one-liners over the 3-arg kernels, and
`rdiv!(A, D)` no longer leaves `A` partially scaled when the zero is not
the first entry. Tests compare a complex matrix and vector against
dense, and assert the destination is unchanged after the throw.

Left out: `D \ x` for a sparse vector `x` keeps LinearAlgebra's generic
3-arg `ldiv!`, which already throws. Touches dispatch (the 2-arg `ldiv!`
union method is now two methods). Backport candidate.

Written by Claude Code (Claude Fable 5.1) in an autonomous session.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 82b5f16)

Backport adjustment: the `rmul!`/`lmul!` hunks (src/matmul.jl on main) and the removal of the old `ldiv!`/`_rdiv!` methods were applied by hand to src/linalg.jl, which holds all of them on release-1.13.
…ed!` (#1052)

`SparseArrays.dropstored!(A, I::AbstractVector{<:Integer},
J::AbstractVector{<:Integer})` mishandles its index vectors in three
ways: it returns before checking bounds when nothing is stored, it reads
a Boolean mask as a vector of the indices 0 and 1 (and so throws a
`BoundsError` for any mask with a `false`), and it reads index vectors
that are the matrix's own buffers while it compacts them.

```julia
using SparseArrays
using SparseArrays: dropstored!
show_or_err(f) = try display(Matrix(f())) catch e; println(sprint(showerror, e)) end
show_or_err(() -> dropstored!(spzeros(2, 2), [5], [1]))
show_or_err(() -> dropstored!(sparse([1.0 2 3; 4 5 6; 7 8 9]), [true, false, true], [1, 3]))
B = sparse([11.0 21; 12 0])
show_or_err(() -> dropstored!(B, view(rowvals(B), 1:1), 1:2))
```

**Before**

```
2×2 Matrix{Float64}:
 0.0  0.0
 0.0  0.0
BoundsError: attempt to access 3×3 SparseMatrixCSC{Float64, Int64} with 9 stored entries at index [Bool[0, 1, 1], [1, 3]]
2×2 Matrix{Float64}:
  0.0  21.0
 12.0   0.0
```

**After**

```
BoundsError: attempt to access 2×2 SparseMatrixCSC{Float64, Int64} with 0 stored entries at index [[5], [1]]
3×3 Matrix{Float64}:
 0.0  2.0  0.0
 4.0  5.0  6.0
 0.0  8.0  0.0
2×2 Matrix{Float64}:
  0.0  0.0
 12.0  0.0
```

The method checked bounds by hand, on the sorted vectors, after the
`nnz(A) == 0` early return, and never lowered its indices; the kernel
then tests `rowval[ridx] in I` while it overwrites `rowval`. It now
calls `_lower_indices(A, I, J)` first, which checks the bounds as
`getindex` does and turns a mask into the indices it selects, and passes
both vectors through `_unalias_setindex`, the helper `setindex!` already
uses, which copies a vector only when it may share storage with one of
the matrix's buffers. The hand-written check is removed.

Split from #1021. This is a bug fix and a backport candidate, provided
the release branch has `_lower_indices` and `_unalias_setindex`.

Written by Claude Code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 6aa11ed)

Backport adjustment: ported by hand to src/sparsematrix.jl: release-1.13 has no `_lower_indices` helper (from #857) and no `_checkpatternwritable`, so the lowering (`to_indices`, `checkbounds`, `ensure_indexable`) is written out in `dropstored!`. The tests go to test/sparsematrix_constructors_indexing.jl unguarded and compare with `==`, as release-1.13 has neither comprehensive mode nor `mismatch`.
#1053)

Two-index `setindex!` accepts a Boolean mask whose length is not that of
the dimension it indexes, where a dense array throws `BoundsError`. This
holds both for a sparse destination and for assigning a sparse matrix
into a `Matrix` through masks.

```julia
using SparseArrays
A = sparse([1.0 2 0; 0 3 4])
try; A[[true], 1] = [9.0]; println("sparse dest: no error, A = ", Matrix(A)); catch e; println("sparse dest: ", typeof(e)); end
M = zeros(2, 3)
try; M[[true], [1]] = sparse(fill(9.0, 1, 1)); println("dense dest:  no error, M = ", M); catch e; println("dense dest:  ", typeof(e)); end
D = zeros(2, 3)
try; D[[true], 1] = [9.0]; println("dense:       no error"); catch e; println("dense:       ", typeof(e)); end
```

**Before**
```
sparse dest: no error, A = [9.0 2.0 0.0; 0.0 3.0 4.0]
dense dest:  no error, M = [9.0 0.0 0.0; 0.0 0.0 0.0]
dense:       BoundsError
```

**After**
```
sparse dest: BoundsError
dense dest:  BoundsError
dense:       BoundsError
```

`setindex!(A::AbstractSparseMatrixCSC, V::AbstractVecOrMat, Ix, Jx)`
called `Base.ensure_indexable(to_indices(A, (Ix, Jx)))` and only then
`checkbounds`. `to_indices` turns a mask into a `Base.LogicalIndex`,
which still knows the mask, but `ensure_indexable` collects it into the
vector of true positions, so the bounds check saw in-range integers and
the mask's length was gone. The method now lowers its indices with
`_lower_indices`, which `getindex` already uses and which checks bounds
between the two steps. That check covers every index, so the
hand-written test of the first and last index further down, already
unreachable, is removed. The five `setindex!(A::Matrix,
x::AbstractSparseMatrixCSC, I, J)` mask methods lose the length in the
same way through `findall`, and now call `checkbounds(A, I, J)` first.
In both cases the throw happens before the destination is written.

Split from #1021. This is a bug fix with no new methods, so it is a
backport candidate.

Written by Claude Code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 5f4149f)

Backport adjustment: ported by hand to src/sparsematrix.jl: release-1.13 has no `_lower_indices` helper (from #857), so the lowering with the bounds check before `ensure_indexable` is written out; the test goes to test/sparsematrix_constructors_indexing.jl unguarded and uses `==` for `exact_equal`.
#1068)

`mapreduce(f, +, A; dims=2)` returns `NaN` for a row that stores every
entry when `f(0)` is not finite, where dense input gives the sum of the
row. The same kernel serves `dims=1` of an adjoint or transpose and a
column view.

```julia
using SparseArrays
f = x -> x == 0 ? Inf : x
D = [1.0 2.0; 0.0 3.0]
mapreduce(f, +, sparse(D); dims=2), mapreduce(f, +, D; dims=2)
```

**Before** (`main`, and Julia 1.13)
```
([NaN; Inf;;], [3.0; Inf;;])
```

**After**
```
([3.0; Inf;;], [3.0; Inf;;])
```

The `+` method of `_mapreducecols!` counts the entries each row does not
store and then adds `f(0) * count` to every row. For a row with a count
of zero that is `Inf * 0`, which is `NaN`. Such a row has no `f(0)` to
add, so it is now skipped. The path is only reached when the matrix has
some unstored entry and `f(0)` is not zero; a matrix stored in full
already avoided evaluating `f(0)`.

The `dims=1` kernel and the whole-array reduction go through
`_mapreducezeros`, which returns early on a count of zero, and a sweep
of `mapreduce(f, +, ...)` and `sum(f, ...)` over `dims` in `1`, `2`,
`(1, 2)` and `:`, with and without `sparse=true`, on a matrix, its
adjoint and a column view agrees with dense on this branch.

Split from #1022. A candidate for backport: a wrong result, with no
change to any type or signature.

Written by Claude Code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 763cc76)

Backport adjustment: applied to src/sparsematrix.jl and test/sparsematrix_ops.jl (for src/reductions.jl and test/reductions.jl).
…1058)

`A[I, J]` throws a `MethodError` when the row range `I` is a
`StepRangeLen`, whatever its step, while the same index works on a dense
matrix.

**Before**
```julia
julia> A = sparse([1.0 2 0; 0 3 4]);

julia> A[StepRangeLen(2, -1, 2), :]
ERROR: MethodError: no method matching getindex_traverse_col(::StepRangeLen{Int64, Int64, Int64, Int64}, ::Int64, ::Int64)

julia> A[StepRangeLen(1, 0, 3), :]
ERROR: MethodError: no method matching getindex_traverse_col(::StepRangeLen{Int64, Int64, Int64, Int64}, ::Int64, ::Int64)
```

**After**
```julia
julia> A[StepRangeLen(2, -1, 2), :] == Matrix(A)[StepRangeLen(2, -1, 2), :]
true

julia> A[StepRangeLen(1, 0, 3), :]
3×3 SparseMatrixCSC{Float64, Int64} with 6 stored entries:
 1.0  2.0   ⋅
 1.0  2.0   ⋅
 1.0  2.0   ⋅
```

`getindex(A, I::AbstractRange, J::AbstractVector)` accepts any range,
but `getindex_traverse_col`, which picks the direction to walk a column
in, had methods only for `AbstractUnitRange` and `StepRange`. Its
`StepRange` method only reads `step(I)`, so it now takes
`AbstractRange`. A `StepRangeLen` can also have a zero step, which
`rangesearch` divides by, so a range whose step is zero or not an
integer is collected and indexed as a vector.

Touches dispatch: `getindex_traverse_col(::StepRange, ...)` (internal)
is widened to `AbstractRange`.

Split from #1021.

A bug fix with no change to a result type, so a backport candidate.

Written by Claude Code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 85bbcd0)

Backport adjustment: applied by hand to src/sparsematrix.jl (for src/indexing.jl), whose `getindex` does not lower its indices with `_lower_indices` on release-1.13; the test goes to test/sparsematrix_constructors_indexing.jl unguarded and uses `==` for `mismatch`.
A unit range of `Bool` as the row index with a scalar column was read as
a range of row numbers, where a dense matrix reads it as a mask.
`false:true` so selected rows 0 and 1 of the column instead of row 2,
and `false:false` returned one element instead of none.

**Before**
```julia
julia> A = sparse([1.0 2 0; 0 3 4]);

julia> A[false:true, 2]
2-element SparseVector{Float64, Int64} with 1 stored entry:
  [2]  =  2.0

julia> Array(A)[false:true, 2]
1-element Vector{Float64}:
 3.0

julia> sparse(fill(5.0, 1, 1))[false:false, 1]
1-element SparseVector{Float64, Int64} with 0 stored entries
```

**After**
```julia
julia> A[false:true, 2]
1-element SparseVector{Float64, Int64} with 1 stored entry:
  [1]  =  3.0

julia> sparse(fill(5.0, 1, 1))[false:false, 1]
0-element SparseVector{Float64, Int64} with 0 stored entries
```

`getindex(::AbstractSparseMatrixCSC, ::AbstractUnitRange, ::Integer)`
slices the column by `first(I)` and `last(I)`, and a `Bool` range
reached it because it is more specific than the `AbstractVector{Bool}`
mask method. The new method collects the range and goes through the mask
path, as the existing `AbstractRange{Bool}` method does for a vector of
columns.

Touches dispatch: adds `getindex(::AbstractSparseMatrixCSC,
::AbstractUnitRange{Bool}, ::Integer)`.

Split from #1021. A bug fix that adds no behaviour beyond matching
dense, so a backport candidate.

Written by Claude Code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 6a4f7a4)

Backport adjustment: the method goes next to the logical `getindex` methods in src/sparsematrix.jl (for src/indexing.jl); the test goes to test/sparsematrix_constructors_indexing.jl unguarded and uses `==` for `mismatch`. On release-1.13 `M[false:true, 2]` returned a wrong result, a vector of length 2, without throwing.
…#1071)

In-place `map!` and `broadcast!` with a function that is not zero at the
zeros store every entry of the destination. They densified it first and
only then hit failures that could have been found beforehand, so a throw
left the destination corrupt: column pointers half rewritten, or a full
pattern with uninitialized values.

```julia
using SparseArrays
C = spzeros(Float64, Int8, 12, 12)
try; C .= 1.0; catch e; println(typeof(e)); end
println(SparseArrays.getcolptr(C))
Ci = sparse([1 0; 0 2])
try; Ci .= Ci .+ 0.5; catch e; println(typeof(e)); end
println(nnz(Ci), " ", Ci == [1 0; 0 2])
```

**Before**
```
InexactError
Int8[1, 13, 25, 37, 49, 61, 73, 85, 97, 109, 121, 1, 1]
InexactError
4 false
```

**After**
```
InexactError
Int8[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
InexactError
2 true
```

`_densestructure!` resized the buffers and wrote the column pointers
before the last one, `m * n + 1`, turned out not to fit the index type,
and the fill kernels called it before `fill!` converted the fill value
to the element type. `_checkdensestructure` now holds what
`_densestructure!` checks before it writes anything (the fixed-pattern
check, and for a matrix that the last column pointer converts), and
`_guardfill` runs it and the conversion of the fill value ahead of
`_map_notzeropres!`, `_broadcast_notzeropres!` and the zero-argument `C
.= f.()` path. The conversion is skipped for an empty destination, which
stores nothing. Fixed destinations throw the same errors as before.
`map!(cos, C, A)` and `C .= A .+ 1.0` at 10^6 entries take the same time
and allocations as on `main`.

Left out: an `f` that throws part-way through the kernel, after the
destination is densified, still leaves it with values that are not its
own. That is a follow-up stacked on this branch.

Split from #1022. A backport candidate: the destination is corrupt after
the throw.

Written by Claude Code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 4cbae1e)

Backport adjustment: release-1.13 densifies with its own `_densestructure!` in src/higherorderfns.jl (without the fixed-pattern checks of main), so `_checkdensestructure` and `_guardfill` are defined there and check only the index type and the fill value. `S .= x` with a scalar goes through `fill!` and `_fillnonzero!` on release-1.13, not through `_densestructure!` as on main, so `_fillnonzero!` gets the same checks before it resizes anything. The test is a new testset in test/higherorderfns.jl that compares with `==`.
#1061)

`fill!` on a view of a sparse matrix, which is what `S[I, J] .= x`
calls, throws when `I` or `J` is a decreasing range.

```julia
S = sparse([1.0 2 0; 0 3 4; 5 0 6])
S[3:-1:1, 1] .= 0.0;                 println(Matrix(S), " nnz=", nnz(S))
S[:, 3:-2:1] .= 2.0;                 println(Matrix(S), " nnz=", nnz(S))
fill!(view(S, 3:-1:2, 3:-1:1), 7.0); println(Matrix(S), " nnz=", nnz(S))
```

**Before**, each of the three statements:
```
CanonicalIndexError: setindex! not defined for StepRange{Int64, Int64}
```

**After**
```
[0.0 2.0 0.0; 0.0 3.0 4.0; 0.0 0.0 6.0] nnz=6
[2.0 2.0 2.0; 2.0 3.0 2.0; 2.0 0.0 2.0] nnz=8
[2.0 2.0 2.0; 7.0 7.0 7.0; 7.0 7.0 7.0] nnz=9
```

The kernels behind this method need each index in increasing order
without repeats, and the method got there with `sort!(unique(I))`.
`unique` of a range returns the range, which cannot be sorted in place.
The new internal helper `_strictlysorted` returns an integer or a unit
range as it is, reverses a range with a negative step, and sorts a copy
of any other vector that is not already strictly increasing, so each
index keeps its type where it can and the caller stays type-stable.

Split from #1021.

A bug fix with no change to results that worked before, so a candidate
for backporting.

Written by Claude Code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 49515df)

Backport adjustment: applied by hand to the `fill!` method in src/sparsematrix.jl (for src/indexing.jl), which on release-1.13 has no fixed-pattern branch; the test goes to test/sparsematrix_constructors_indexing.jl unguarded and uses `==` for `mismatch`.
Two-index `setindex!(A, V, I, J)` reads its index vectors while it
rewrites the storage, so an index vector that is one of the matrix's own
buffers changes under it. A row index that is `rowvals(S)` throws a
`BoundsError` and leaves the matrix corrupt; as a column index it
silently assigns the wrong entries.

```julia
using SparseArrays, LinearAlgebra
S = sparse(Diagonal([1.0, 2, 3, 4]))
try
    S[rowvals(S), 2] = [10.0, 20, 30, 40]
    println(Matrix(S))
catch e
    println(typeof(e), ": length(rowvals(S)) = ", length(rowvals(S)), ", colptr = ", SparseArrays.getcolptr(S))
end
S = sparse(Diagonal([1.0, 2, 3, 4]))
S[2, rowvals(S)] = [10.0, 20, 30, 40]
println(Matrix(S))
```

**Before**
```
BoundsError: length(rowvals(S)) = 8, colptr = [1, 2, 3, 4, 5]
[1.0 0.0 0.0 0.0; 10.0 20.0 0.0 0.0; 0.0 0.0 3.0 0.0; 0.0 0.0 0.0 4.0]
```

**After**
```
[1.0 10.0 0.0 0.0; 0.0 20.0 0.0 0.0; 0.0 30.0 3.0 0.0; 0.0 40.0 0.0 4.0]
[1.0 0.0 0.0 0.0; 10.0 20.0 30.0 40.0; 0.0 0.0 3.0 0.0; 0.0 0.0 0.0 4.0]
```

The kernel resizes `rowval` and `nzval` and shifts their entries as it
inserts, and keeps indexing `I` and `J` throughout. The linear-index
method already passes its indices through `_unalias_setindex`, which
copies a vector only when `Base.mightalias` reports that it shares
memory with `nzval`, `rowval` or `colptr`; the two-index method now does
the same for `I` and `J` once they are lowered. An index that is not one
of the buffers is returned as it is, without an allocation.

Split from #1021. This is a backport candidate, since the failure
corrupts the matrix.

Written by Claude Code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 3d6d696)

Backport adjustment: applied by hand to src/sparsematrix.jl (for src/indexing.jl), after the index lowering that release-1.13 writes out instead of `_lower_indices`; the test goes to test/sparsematrix_constructors_indexing.jl unguarded and uses `==` for `mismatch`.
`setindex!(A, x::AbstractArray, I::AbstractVector{<:Integer})` with
linear indices converts each value as it stores it, after it has started
to rewrite the pattern. A value that does not convert to `eltype(A)`
throws part of the way through and leaves a matrix whose `colptr` no
longer matches its buffers.

```julia
using SparseArrays
S = sparse([1 0 0; 0 2 0; 0 0 3])
try
    S[[2, 4, 6]] = [7.0, 8.0, 8.5]
catch e
    showerror(stdout, e); println()
end
@show SparseArrays.getcolptr(S) length(rowvals(S)) length(nonzeros(S))
```

**Before**
```
InexactError: Int64(8.5)
SparseArrays.getcolptr(S) = [1, 3, 3, 4]
length(rowvals(S)) = 6
length(nonzeros(S)) = 6
```

**After**
```
InexactError: Int64(8.5)
SparseArrays.getcolptr(S) = [1, 2, 3, 4]
length(rowvals(S)) = 3
length(nonzeros(S)) = 3
```

The method grows `rowval` and `nzval` and shifts entries while it walks
the sorted indices, and fixes up `colptr` only at the end. It now calls
`_checkconvertible(eltype(A), x)` after the bounds check and before the
first write, the check `copyto!` already makes; the loop compiles away
when `eltype(x) <: eltype(A)`. `_checkconvertible` is widened from
`AbstractVector` to `AbstractArray`, since `x` may be any array with the
right number of elements.

Touches dispatch: the internal helper `_checkconvertible` now accepts
`AbstractArray`; no public method changes.

Split from #1021.

Backport candidate: a throwing assignment corrupts the matrix.

Written by Claude Code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit b93de0b)

Backport adjustment: release-1.13 has no `_checkconvertible` (main added it for `copyto!` in a PR that is not labelled for backport), so the helper is defined next to the linear-index `setindex!` in src/sparsematrix.jl, with the `AbstractArray` signature this PR gives it. The test goes to test/sparsematrix_constructors_indexing.jl unguarded and uses `==` for `exact_equal`.
`opnorm(A::AbstractSparseMatrixCSC, p)` accepts any `p` when a dimension
of `A` is zero, where the dense method throws: only the 1, 2 and Inf
operator norms exist.

```julia
using SparseArrays, LinearAlgebra
for A in (spzeros(0, 0), spzeros(3, 0), spzeros(1, 4), spzeros(4, 1), sparse([1.0 2; 3 4]))
    r = try opnorm(A, 3) catch e; typeof(e) end
    d = try opnorm(Matrix(A), 3) catch e; typeof(e) end
    println(rpad(string("opnorm(", size(A), " sparse, 3) = ", r), 44), "dense: ", d)
end
```

**Before**
```
opnorm((0, 0) sparse, 3) = 0.0              dense: ArgumentError
opnorm((3, 0) sparse, 3) = 0.0              dense: ArgumentError
opnorm((1, 4) sparse, 3) = ArgumentError    dense: ArgumentError
opnorm((4, 1) sparse, 3) = ArgumentError    dense: ArgumentError
opnorm((2, 2) sparse, 3) = ArgumentError    dense: ArgumentError
```

**After**
```
opnorm((0, 0) sparse, 3) = ArgumentError    dense: ArgumentError
opnorm((3, 0) sparse, 3) = ArgumentError    dense: ArgumentError
opnorm((1, 4) sparse, 3) = ArgumentError    dense: ArgumentError
opnorm((4, 1) sparse, 3) = ArgumentError    dense: ArgumentError
opnorm((2, 2) sparse, 3) = ArgumentError    dense: ArgumentError
```

The method returned zero for an empty matrix before looking at `p`, and
rejected an invalid `p` only by falling through every branch to a
trailing `throw`. It now checks `p in (1, 2, Inf)` first, with the same
`ArgumentError`, so the single-row and single-column branches and the
last `elseif` no longer test `p` again and the trailing `throw` goes
away. Results for a valid `p` are unchanged.

Split from #1023. A bug fix, so a backport candidate.

Written by Claude Code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 7bee547)

Backport adjustment: release-1.13's `opnorm` has no 2-norm estimate for a general sparse matrix (main added `opnorm2est` in a PR not labelled for backport), so only the up-front check of `p` is ported, with release-1.13's error message; the rest of the function is unchanged. The test goes to test/linalg.jl unguarded.
… does (#1072)

`findmin` and `findmax` of a sparse matrix that holds a `missing` throw,
or return a `NaN` that dense ranks below the `missing`.

**Before**
```julia
julia> findmin(sparse([1 missing; 0 2]))
ERROR: TypeError: non-boolean (Missing) used in boolean context

julia> findmax(sparse([NaN missing; 0.0 2.0]))
(NaN, CartesianIndex(1, 1))
```

**After**
```julia
julia> findmin(sparse([1 missing; 0 2]))    # as findmin([1 missing; 0 2])
(missing, CartesianIndex(1, 2))

julia> findmax(sparse([NaN missing; 0.0 2.0]))
(missing, CartesianIndex(1, 2))
```

The two comparison helpers of `_findr` spelled out the `NaN` rule with
`==` and `!=`, which return `missing` next to a `missing`. They now use
the replacement test of Base's dense `findmin` and `findmax`,
`Base.isgreater(b, a)` and `isless(b, a)`, which are total: `missing`
ranks above `NaN`, `NaN` above every number, and `-0.0` below `0.0`.
Results for `NaN` and a stored `-0.0` are unchanged. A sweep of 240,000
comparisons against dense over random matrices up to 4×4 drawn from
`0.0`, `-0.0`, `±1`, `±Inf`, `NaN` and `missing`, with a random subset
of the zeros stored, finds no mismatch.

| 10^4 × 10^4, 10^6 stored entries | `main` | branch |
|---|---|---|
| `findmin(A)` | 1.127 ms | 1.126 ms |
| `findmax(A; dims=1)` | 1.795 ms | 1.795 ms |

Split from #1022. A bug fix, so a backport candidate.

Written by Claude Code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 0671810)

Backport adjustment: applied to src/sparsematrix.jl and test/sparsematrix_ops.jl (for src/reductions.jl and test/reductions.jl).
`getindex(A, I)` with an index array `I` that is neither a vector nor a
matrix takes the method for a matrix of linear indices, which sizes its
result from `size(I, 1)` and `size(I, 2)` only. With three or more
dimensions it writes past the end of the result's buffers; with a
zero-dimensional `I` it returns a 1×1 sparse matrix. Sparse vectors go
through the same method.

```julia
using SparseArrays
A = sparse([1.0 2 0; 0 3 4]); x = sparse([1, 2, 3])
show(x[fill(3)]); println()
show(A[fill(2)]); println()
show(A[reshape([1, 3, 4, 6, 1, 3, 4, 6], 2, 2, 2)]); println()
```

**Before**
```
sparse([1], [1], [3], 1, 1)
sparse(Int64[], Int64[], Float64[], 1, 1)
sparse([1, 2, 1, 2], [1, 1, 2, 2], [Fault at memory address: 0x30000722250b5

[4140366] signal 11 (1): Segmentation fault
```

**After**
```
fill(3)
fill(0.0)
[1.0 3.0; 2.0 4.0;;; 1.0 3.0; 2.0 4.0]
```

There is no sparse array of such a shape, so
`getindex(A::AbstractSparseMatrixCSC, I::AbstractArray)` now hands any
`I` with `ndims(I) != 2` to `_getindex_dense`, which checks the bounds
and fills an `Array` of `size(I)` with one binary search per element, as
indexing a dense array does. The elements of `I` may be integers or
`CartesianIndex`es, and anything else (a float, a `Bool`) throws in
`to_index` as it does for a dense array. A stored entry is copied, and
the implicit zero comes from `_densezero`, so the eltype is that of
`Array(A)` and no `zero` is needed when only stored entries are
selected. The result being dense, its length need not fit the index
type. For a sparse vector, `getindex(x, I::AbstractArray)` checks the
bounds against `x` first, so that the `BoundsError` names the vector and
not the one-column matrix it is indexed through.

Split from #1021. Touches dispatch: no new `getindex` signature, but
index arrays with `ndims != 2` now return an `Array` instead of a
`SparseMatrixCSC`. This is a bug fix for an out-of-bounds write, so a
backport candidate.

Written by Claude Code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit c463cce)

Backport adjustment: ported to src/sparsematrix.jl (for src/indexing.jl). release-1.13 has no `_densezero` (the zero of an element type whose zero is of another type, from main), so an unstored entry is `zero(Tv)` and the result is always an `Array{Tv}`; the test for such an element type is dropped. On release-1.13 a 3-d index array segfaulted in the matrix kernel. The tests go to test/sparsematrix_constructors_indexing.jl unguarded.
…NaN` next to a zero (#1065)

`prod` of a sparse matrix with an unstored entry returns zero without
looking at the stored ones, but a stored `Inf` or `NaN` times zero is
`NaN`, which is what the dense `prod` gives.

**Before** (`main`, and Julia 1.13)
```julia
julia> prod(sparse([0.0 Inf; 1.0 2.0])), prod([0.0 Inf; 1.0 2.0])
(0.0, NaN)

julia> prod(sparse([0.0 NaN; 1.0 2.0]); dims=(1, 2))
1×1 Matrix{Float64}:
 0.0
```

**After**
```julia
julia> prod(sparse([0.0 Inf; 1.0 2.0])), prod([0.0 Inf; 1.0 2.0])
(NaN, NaN)

julia> prod(sparse([0.0 NaN; 1.0 2.0]); dims=(1, 2))
1×1 Matrix{Float64}:
 NaN
```

The `*` kernel of `Base._mapreduce` for a `SparseMatrixCSC` or a column
view of one short-circuits when the unstored entries multiply to zero.
That is kept only where `f` returns an integer (`Base.promote_op(f, T)
<: Integer`), which has no value that survives a zero. Otherwise the
zero is multiplied by the product of the stored entries. That product
can overflow to `Inf` on its own and turn the zero into a `NaN` that the
dense fold never sees, so when the result is nonzero the stored entries
are folded into the zero one at a time instead: `prod(sparse([0.0 1e200;
1e200 1.0]))` is still `0.0`.

The cost is that `prod` of a float matrix with unstored entries now
reads the stored entries, O(nnz) where it was O(1). For a 2000×2000
`Float64` matrix with 10^6 stored entries:

| | time | allocations |
|---|---|---|
| `main` | 0.00006 ms | 0 bytes |
| this PR | 0.096 ms | 0 bytes |

Left out: `prod` of a sparse vector and `prod(A; dims=1)` or `dims=2` go
through other kernels and still give `NaN` where the stored entries of
the vector or slice overflow next to a zero, as on `main`
(`prod(sparsevec([0.0, 1e200, 1e200]))` is `NaN`, dense gives `0.0`).

Split from #1022. This is a wrong result on released Julia, so it is a
backport candidate.

Written by Claude Code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 775677b)

Backport adjustment: applied to src/sparsematrix.jl and test/sparsematrix_ops.jl (for src/reductions.jl and test/reductions.jl).
…ense destination (#1074)

`mul!(C, A, B, α, β)` with `A` a `Symmetric` or `Hermitian` sparse
matrix and a dense `C` mishandles `α`: a `false` `α` is not a strong
zero, so a `NaN` in `B` reaches `C`; `α` is multiplied on the wrong side
for a non-commutative eltype; and `α` and `β` are converted to
`eltype(C)`, which throws for a result that the destination can hold.

```julia
using SparseArrays, LinearAlgebra
include(joinpath(Sys.BINDIR, "..", "share", "julia", "test", "testhelpers", "Quaternions.jl"))
using .Quaternions: Quaternion
mul!(ones(2, 2), Symmetric(sparse([1.0 2; 3 4])), [NaN 1; 1 1], false, 1.0)
H = sparse([Quaternion(1.0, 0, 0, 0) Quaternion(0.0, 1, 2, 3); Quaternion(0.0, 0, 0, 0) Quaternion(2.0, 0, 0, 0)])
B = [Quaternion(0.0, 0, 0, 1) Quaternion(1.0, 1, 0, 0); Quaternion(1.0, 0, 0, 0) Quaternion(0.0, 0, 1, 0)]
j = Quaternion(0.0, 0, 1, 0)
mul!(zeros(eltype(B), 2, 2), Hermitian(H), B, j, false) == Matrix(Hermitian(H)) * B * j
mul!(zeros(Int, 2, 2), Symmetric(sparse([1 2; 3 4])), [2 2; 4 4], 0.5, 1)
```

**Before**
```
[NaN 1.0; NaN 1.0]
false
ERROR: InexactError: Int64(0.5)
```

**After**
```
[1.0 1.0; 1.0 1.0]
true
[5 5; 10 10]
```

`spdensemul!` passed `T(alpha), T(beta)` with `T = eltype(C)`, and
`_symherm_mul!` computed `C[col,k] += α * sumcol`, with `α` on the left,
and always ran the loop. Now the scalars are passed as they are,
`_symherm_mul!` multiplies `α` on the right in both places (`B[col,k] *
α` and `sumcol * α`), as `A*B*α` does, skips the multiplication for a
`Bool` `α`, and returns after scaling `C` by `β` when `α === false`. It
also specializes on its three function arguments.

`mul!(y, Symmetric(A), b)` real and `mul!(yc, Hermitian(Ac), bc, 2.0,
3.0)` complex, n = 100 000 with 9·10^5 stored entries, minimum of 25
runs:

| | `main` | this PR |
|---|---|---|
| real, 3-arg | 0.68 ms / 0 bytes | 0.67 ms / 0 bytes |
| complex, 5-arg | 0.82 ms / 0 bytes | 0.76 ms / 0 bytes |

Split from #1023. The first two cases are wrong results, so this is a
backport candidate. It touches no dispatch: the signature of the
internal `_symherm_mul!` changes, the `mul!` methods do not.

Written by Claude Code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 6324de6)

Backport adjustment: ported by hand to src/linalg.jl, where release-1.13 has the kernel as `_mul!` with an `nzrang` argument (main renamed it `_symherm_mul!` in src/matmul.jl). The test goes to test/linalg.jl unguarded, loads Base's Quaternions test helper directly and uses literal values in place of the `fixturedense` fixture.
…1079)

`mul!` of a sparse and a dense matrix into a dense destination that
shares storage with the dense operand silently returns a wrong product:
the kernels read the operand while they write the destination. Dense
`mul!` throws for the same call.

**Before**
```julia
julia> S = sparse([1.0 2 0; 0 3 4; 5 0 6]); D = [1.0 2 3; 4 5 6; 7 8 10];

julia> C = copy(D); mul!(C, S, C) == S * D
false

julia> C = copy(D); mul!(C, S, UpperTriangular(C)) == S * UpperTriangular(D)
false

julia> C = copy(D); mul!(C, D, D)
ERROR: ArgumentError: output matrix must not be aliased with input matrix
```

**After**
```julia
julia> C = copy(D); mul!(C, S, C)
ERROR: ArgumentError: output matrix must not be aliased with input matrix

julia> C = copy(D); mul!(C, S, UpperTriangular(C))
ERROR: ArgumentError: output matrix must not be aliased with input matrix

julia> C == D
true
```

`_checknoalias(C, B, α)` runs after the size checks of the kernels that
write a dense destination from a dense operand (`_spmatmul!`,
`_At_or_Ac_mul_B!`, `_A_mul_Bt_or_Bc!`, `_trimatmul!`, `_mattrimul!`,
`_symherm_mul!`, `_A_mul_symherm!`), so a shape error is still reported
first and the destination is untouched. It strips the operand's adjoint,
transpose, symmetric, triangular and Hessenberg wrappers and compares
with `Base.mightalias`, so a triangle or an overlapping view of the
destination is caught and non-overlapping views of one array are not. A
`false` `α` never reads the operand and skips the check.

Behaviour to review: the check also applies to a vector destination,
`mul!(x, S, x)`, and to the triangular kernels unless the destination is
the operand itself, which is the in-place `lmul!`/`rmul!`. Dense `mul!`
returns the right product for some aliased calls that now throw, such as
`mul!(C, A, view(C, :, :))`. Through a symmetric or Hermitian sparse
factor on the left a `false` `α` is converted to the eltype before it
reaches the kernel, so that aliased call throws too.

Left out: `mul!(C, UpperTriangular(C), S)`, with a dense triangle of the
destination on the left, reaches LinearAlgebra's dense triangular kernel
and is wrong for a dense `S` too.

The check is one comparison of pointers per call. Minimum of 25 runs, ms
/ bytes:

| | `main` | this PR |
|---|---|---|
| `mul!(y, A, b)`, n = 100 000, 9·10^5 stored entries | 1.29 / 0 | 1.28
/ 0 |
| `mul!(C, D, S)`, 2000×2000 dense, 1% sparse | 14.84 / 0 | 14.82 / 0 |

Split from #1023. This is a fix for a silently wrong result and a
backport candidate.

Written by Claude Code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit ead43a0)

Backport adjustment: ported by hand to the product kernels in src/linalg.jl (src/matmul.jl on main), with release-1.13's `DenseViewWrappers` for main's `MatrixWrappers`. release-1.13 has no kernels of its own for a dense matrix times a symmetric/Hermitian sparse matrix (`_A_mul_symherm!`) or times a triangular sparse matrix (`_mattrimul!`); those products go through LinearAlgebra's generic code, so the check is not added there and their two test cases are dropped. The test goes to test/linalg.jl unguarded.
…oduct `x .* y'` (#1077)

`x .* y'` for sparse vectors drops a non-finite product with an unstored
zero: `Inf * 0` and `NaN * 0` are `NaN` in the dense result and an
unstored zero in the sparse one. `kron(x, y')` goes through the same
broadcast and has the same defect.

**Before**
```julia
julia> x, y = sparsevec([Inf, 0.0]), sparsevec([0.0, 1.0]);

julia> Array(x .* y')
2×2 Matrix{Float64}:
 0.0  Inf
 0.0  0.0

julia> Array(x) .* Array(y)'
2×2 Matrix{Float64}:
 NaN    Inf
   0.0   0.0
```

**After**
```julia
julia> Array(x .* y')
2×2 Matrix{Float64}:
 NaN    Inf
   0.0   0.0
```
`Array(kron(x, y'))` changes in the same way and now equals
`kron(Array(x), Array(y)')`.

The outer-product shortcut `_outer` multiplies stored entries with
stored entries alone, which is the product only when every stored entry
times a zero is a zero. `_copy(*, x, y')` now checks that with
`_outerkeepszeros`, an O(nnz) pass over both vectors, and otherwise
takes the general broadcast kernel. Both branches return the same
`SparseMatrixCSC` type, so the result stays inferable.

`x .* y'` with 10^3 stored `Float64` entries in each vector of length
10^4 (10^6 stored results), minimum of 20 runs on a shared machine:

| | time | allocations |
|---|---|---|
| `main` | 2.87 ms | 9 (16087432 bytes) |
| this PR | 2.77 ms | 9 (16087432 bytes) |

Left out: `x .* y'` for an eltype without `zero`, such as a vector of
matrices, throws a `MethodError` as it does on `main`.

Split from #1022. Backport candidate: a wrong result, also on Julia
1.13.

Written by Claude Code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 3d2e6df)

Backport adjustment: applied by hand to src/higherorderfns.jl, where the outer product dispatches on `SparseVectorUnion` on release-1.13. release-1.13 has no `_densezero`, so the zeros are `zero(eltype(x))` and `zero(eltype(y))`, and `wrapperop` is called qualified. The test is a new testset in test/higherorderfns.jl, unguarded.
…ap` and `broadcast` kernels (#1087)

`map` and `broadcast!` of a function that is not zero at zero compare
each result with the fill value `f(0)` using `!=`. That throws when the
comparison is `missing`, and it leaves the fill value in place of a
result that equals it but has another type.

**Before**
```julia
julia> A = sparse([1.0 0; 0 2.0]);

julia> map(x -> x == 0 ? 1.0 : missing, A)
ERROR: TypeError: non-boolean (Missing) used in boolean context

julia> typeof.(map(x -> x == 0 ? 1 : 1.0, A))
2×2 Matrix{DataType}:
 Int64  Int64
 Int64  Int64
```

**After**
```julia
julia> map(x -> x == 0 ? 1.0 : missing, A)
2×2 SparseMatrixCSC{Union{Missing, Float64}, Int64} with 4 stored entries:
  missing  1.0
 1.0        missing

julia> typeof.(map(x -> x == 0 ? 1 : 1.0, A))   # as for Array(A)
2×2 Matrix{DataType}:
 Float64  Int64
 Int64    Float64
```

The `_map_notzeropres!` and `_broadcast_notzeropres!` kernels (one, two
and N arguments) fill the destination with `f(0)` and then skip the
store of a result that equals it. They now skip only a result that is
identical to it (`!==`/`===`), which is always a `Bool` and is the only
case in which the store is redundant.

Time at 10^6 stored entries (4000×4000, `x -> x + 1.0` and `(x, y) -> x
+ y + 1.0`, `Float64`, minimum of 15 runs): the one-argument kernel is
unchanged (`map!` 13.0 ms on both), the two-argument kernel goes from
18.6 ms to 21.1 ms in `map!` and from 36–39 ms to 38–40 ms in `map`.
Allocations are unchanged.

Split from #1022.

Backport candidate: a bug fix, with no new method and no change of a
result type that was computed correctly before.

Written by Claude Code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 079bcc1)

Backport adjustment: the test is a new testset in test/higherorderfns.jl, with a literal matrix in place of the `fixturepair` fixture.
@ViralBShah

Copy link
Copy Markdown
Member

Wow, thanks!

@dkarrasch

Copy link
Copy Markdown
Member Author

Nothing to thank for, I just gave instructions.

@ViralBShah

Copy link
Copy Markdown
Member

Given the extensive nature of these fixes, I wonder if we should do a pkgeval run.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants