From 6acafe0fb364bbe5958f65db51fbacc594a53edf Mon Sep 17 00:00:00 2001 From: Sheehan Olver Date: Mon, 5 Oct 2026 21:46:50 +0100 Subject: [PATCH 1/2] Add equals_layout hook for == on LayoutArrays `==` with a LayoutArray (or a view of one) on either side now reduces to `equals_layout(MemoryLayout(A), MemoryLayout(B), A, B)`, which can be overloaded per layout. The default falls back to the generic AbstractArray implementation in Base, so behaviour is unchanged unless a layout opts in. Also resolves the resulting ambiguities with SparseArrays.ReadOnly in the SparseArrays extension. Co-Authored-By: Claude Opus 5.5 --- ext/ArrayLayoutsSparseArraysExt.jl | 13 +++++++++++-- src/ArrayLayouts.jl | 18 ++++++++++++++++++ test/test_layoutarray.jl | 30 ++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/ext/ArrayLayoutsSparseArraysExt.jl b/ext/ArrayLayoutsSparseArraysExt.jl index 38a8fbc..52e9c03 100644 --- a/ext/ArrayLayoutsSparseArraysExt.jl +++ b/ext/ArrayLayoutsSparseArraysExt.jl @@ -1,13 +1,13 @@ module ArrayLayoutsSparseArraysExt using ArrayLayouts -using ArrayLayouts: copyto!_layout +using ArrayLayouts: copyto!_layout, equals_layout using SparseArrays # Specifying the full namespace is necessary because of https://github.com/JuliaLang/julia/issues/48533 # See https://github.com/JuliaStats/LogExpFunctions.jl/pull/63 import ArrayLayouts.LinearAlgebra -import Base: copyto! +import Base: copyto!, == # ambiguity from sparsematrix.jl copyto!(dest::LayoutMatrix, src::SparseArrays.AbstractSparseMatrixCSC) = @@ -22,4 +22,13 @@ copyto!(dest::SubArray{<:Any,2,<:LayoutMatrix}, src::SparseArrays.AbstractSparse @inline LinearAlgebra.dot(a::SparseArrays.SparseVectorUnion{<:Number}, b::LayoutArray{<:Number}) = ArrayLayouts.dot(a,b) + +# ambiguity from SparseArrays.ReadOnly +if isdefined(SparseArrays, :ReadOnly) + ==(A::SparseArrays.ReadOnly, B::LayoutVector) = equals_layout(A, B) + ==(A::LayoutVector, B::SparseArrays.ReadOnly) = equals_layout(A, B) + ==(A::SparseArrays.ReadOnly, B::SubArray{<:Any,1,<:LayoutArray}) = equals_layout(A, B) + ==(A::SubArray{<:Any,1,<:LayoutArray}, B::SparseArrays.ReadOnly) = equals_layout(A, B) +end + end diff --git a/src/ArrayLayouts.jl b/src/ArrayLayouts.jl index d6effe3..931037e 100644 --- a/src/ArrayLayouts.jl +++ b/src/ArrayLayouts.jl @@ -309,6 +309,24 @@ mapreduce_layout(::Any, f, op, A, dims; kw...) = invoke(mapreduce, Tuple{Any,Any Base.mapreduce(f, op, A::LayoutArray; dims=:, kw...) = mapreduce_layout(MemoryLayout(A), f, op, A, dims; kw...) Base.mapreduce(f, op, A::SubArray{<:Any,N,<:LayoutArray}; dims=:, kw...) where N = mapreduce_layout(MemoryLayout(A), f, op, A, dims; kw...) +""" + equals_layout(layA, layB, A, B) + +Layout-aware hook for `A == B`, called with `layA = MemoryLayout(A)` and `layB = MemoryLayout(B)`. +Overload this to specialise equality on memory layouts. The default falls back to the +generic `AbstractArray` implementation in Base. +""" +equals_layout(_, _, A, B) = invoke(==, Tuple{AbstractArray,AbstractArray}, A, B) +equals_layout(A, B) = equals_layout(MemoryLayout(A), MemoryLayout(B), A, B) +==(A::LayoutArray, B::LayoutArray) = equals_layout(A, B) +==(A::LayoutArray, B::AbstractArray) = equals_layout(A, B) +==(A::AbstractArray, B::LayoutArray) = equals_layout(A, B) +==(A::SubArray{<:Any,<:Any,<:LayoutArray}, B::SubArray{<:Any,<:Any,<:LayoutArray}) = equals_layout(A, B) +==(A::SubArray{<:Any,<:Any,<:LayoutArray}, B::LayoutArray) = equals_layout(A, B) +==(A::LayoutArray, B::SubArray{<:Any,<:Any,<:LayoutArray}) = equals_layout(A, B) +==(A::SubArray{<:Any,<:Any,<:LayoutArray}, B::AbstractArray) = equals_layout(A, B) +==(A::AbstractArray, B::SubArray{<:Any,<:Any,<:LayoutArray}) = equals_layout(A, B) + _fill_lmul!(β, A::AbstractArray) = iszero(β) ? zero!(A) : lmul!(β, A) diff --git a/test/test_layoutarray.jl b/test/test_layoutarray.jl index a24af5c..d2c6510 100644 --- a/test/test_layoutarray.jl +++ b/test/test_layoutarray.jl @@ -730,6 +730,36 @@ ArrayLayouts.mapreduce_layout(::MyReduceLayout, f, op, A, ::Colon; kw...) = -1.0 @test sum(v; dims=1) == [6.0] end +struct MyEqualsLayout <: MemoryLayout end +struct MyEqualsVector <: LayoutVector{Float64} end +Base.size(::MyEqualsVector) = (3,) +Base.getindex(::MyEqualsVector, k::Int) = Float64(k) +MemoryLayout(::Type{MyEqualsVector}) = MyEqualsLayout() +ArrayLayouts.equals_layout(::MyEqualsLayout, ::MyEqualsLayout, A, B) = true +ArrayLayouts.equals_layout(::MyEqualsLayout, _, A, B) = false + +@testset "==" begin + A = randn(5,4) + M = MyMatrix(A) + @test M == A + @test A == M + @test M == MyMatrix(copy(A)) + @test M != MyMatrix(A .+ 1) + @test M != A[:,1:3] + @test view(M, 1:3, 2:4) == A[1:3, 2:4] + @test A[1:3, 2:4] == view(M, 1:3, 2:4) + @test view(M, 1:3, 2:4) == view(MyMatrix(copy(A)), 1:3, 2:4) + @test view(M, 1:3, 2:4) == MyMatrix(A[1:3, 2:4]) + @test MyMatrix(A[1:3, 2:4]) == view(M, 1:3, 2:4) + @test view(M, :, 1) != view(M, 1:5, 1:1) + @test MyVector([1.,2,3]) == [1,2,3] + + v = MyEqualsVector() + @test v == MyEqualsVector() + @test v != [1.,2,3] + @test [1.,2,3] == v # falls back to generic implementation +end + @testset "* for infinite layouts" begin tup = InfSymTridiagonal(), InfTridiagonal(), InfBidiagonal('U'), InfBidiagonal('L'), From 3f26a0e97abcc51c525504d93ba09d3405ffb350 Mon Sep 17 00:00:00 2001 From: Sheehan Olver Date: Wed, 7 Oct 2026 10:11:39 +0100 Subject: [PATCH 2/2] Test == with SparseArrays.ReadOnly for coverage Co-Authored-By: Claude Opus 5.5 --- test/test_layoutarray.jl | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/test_layoutarray.jl b/test/test_layoutarray.jl index d2c6510..047e365 100644 --- a/test/test_layoutarray.jl +++ b/test/test_layoutarray.jl @@ -758,6 +758,15 @@ ArrayLayouts.equals_layout(::MyEqualsLayout, _, A, B) = false @test v == MyEqualsVector() @test v != [1.,2,3] @test [1.,2,3] == v # falls back to generic implementation + + if isdefined(SparseArrays, :ReadOnly) + r = SparseArrays.ReadOnly([1.,2,3]) + w = MyVector([1.,2,3]) + @test r == w + @test w == r + @test r == view(w, :) + @test view(w, :) == r + end end @testset "* for infinite layouts" begin