From 63c0d927410f0460511a985e1edcde6c13e5dbe4 Mon Sep 17 00:00:00 2001 From: Jishnu Bhattacharya Date: Wed, 2 Sep 2026 23:37:06 +0400 Subject: [PATCH] Hash infinite arrays --- src/ArrayLayouts.jl | 1 + src/hash.jl | 78 +++++++++++++++++++++++++++++++++++++++++++++ test/test_hash.jl | 52 ++++++++++++++++++++++++++++++ 3 files changed, 131 insertions(+) create mode 100644 src/hash.jl create mode 100644 test/test_hash.jl diff --git a/src/ArrayLayouts.jl b/src/ArrayLayouts.jl index e928c3a..6aade6a 100644 --- a/src/ArrayLayouts.jl +++ b/src/ArrayLayouts.jl @@ -411,6 +411,7 @@ Base.print_matrix_row(io::IO, include("cumsum.jl") +include("hash.jl") ### # support overloading hcat/vcat for ∞-arrays diff --git a/src/hash.jl b/src/hash.jl new file mode 100644 index 0000000..e14e962 --- /dev/null +++ b/src/hash.jl @@ -0,0 +1,78 @@ +## +# hash +# +# `Base.hash(::AbstractArray, ::UInt)` hashes the endpoints of the axes and then walks the +# entries backwards from the last index, which neither terminates nor is well-defined when an +# axis is infinite, as it is for the arrays built on top of this package by `InfiniteArrays`. +# +# Such an array is hashed with the same recipe as `Base`'s, except that the entries are taken from +# a finite corner at the start of the array. Equal arrays have equal axes and equal entries, so the +# contract `isequal(a,b) ⟹ hash(a) == hash(b)` is preserved; the price is that arrays which first +# differ outside the corner collide. Arrays with finite axes are left to `Base`. +# +# `FillArrays`, `BlockArrays` and `InfiniteArrays` hash the infinite arrays they own with this same +# scheme; keep the seeds and the window size in sync so that arrays which compare equal across +# packages keep hashing alike. +## + +const hash_infinity_seed = UInt === UInt64 ? 0x3a4c1b6d9e2f5087 : 0x9e2f5087 +const hash_infarray_seed = UInt === UInt64 ? 0x5d7e3f11a8c6b024 : 0xa8c6b024 + +# number of entries hashed along each dimension of an infinite array +const hash_nentries = 8 + +# An infinite axis has an infinite endpoint, which `Base` cannot hash, so hash the direction it +# points in instead: infinities pointing the same way compare equal and hence have to hash alike. +_hash_endpoint(x, h::UInt) = isinf(x) ? hash(signbit(x), h + hash_infinity_seed) : hash(x, h) + +""" + _hash_indices(ax) + +The indices along the axis `ax` at which entries get hashed: the first `hash_nentries` of them, or +all of `ax` when it is shorter than that. A bi-infinite axis has no first index, so there the +window starts at zero instead. +""" +function _hash_indices(ax::AbstractUnitRange) + a = first(ax) + isinf(a) && return 0:hash_nentries-1 + b = a + hash_nentries - 1 + a:(isinf(last(ax)) ? b : min(last(ax), b)) +end +_hash_indices(ax) = Iterators.take(ax, hash_nentries) + +function _hash_infarray(A::AbstractArray, h::UInt) + h += hash_infarray_seed + # the axes are arrays in their own right, so hash their endpoints instead of the axes + for ax in axes(A) + h = _hash_endpoint(first(ax), h) + end + for ax in axes(A) + h = _hash_endpoint(last(ax), h) + end + for I in Iterators.product(map(_hash_indices, axes(A))...) + h = hash(A[I...], h) + end + h +end + +# `length` is the product of the axis lengths, and `0 * ℵ₀` throws, so ask the axes one at a time +_hasinfaxes(A::AbstractArray) = any(ax -> isinf(length(ax)), axes(A)) + +const HashableLayout = Union{LayoutArray, + Diagonal{<:Any,<:LayoutVector}, + Bidiagonal{<:Any,<:LayoutVector}, + Tridiagonal{<:Any,<:LayoutVector}, + SymTridiagonal{<:Any,<:LayoutVector}, + HermOrSym{<:Any,<:LayoutMatrix}, + UpperOrLowerTriangular{<:Any,<:LayoutMatrix}} + +const HashableLayouts = Union{HashableLayout, + AdjOrTrans{<:Any,<:HashableLayout}, + SubArray{<:Any,<:Any,<:HashableLayout}} + +Base.hash(A::HashableLayouts, h::UInt) = + _hasinfaxes(A) ? _hash_infarray(A, h) : invoke(hash, Tuple{AbstractArray,UInt}, A, h) + +# `Base.isequal(::AbstractArray, ::AbstractArray)` walks the entries, so it does not terminate for +# two equal infinite cumsums. Mirror `==`, which compares the ranges. +Base.isequal(a::RangeCumsum, b::RangeCumsum) = isequal(a.range, b.range) diff --git a/test/test_hash.jl b/test/test_hash.jl new file mode 100644 index 0000000..158c211 --- /dev/null +++ b/test/test_hash.jl @@ -0,0 +1,52 @@ +module TestHash + +using ArrayLayouts, LinearAlgebra, Test, Infinities + +include("infinitearrays.jl") +using .InfiniteArrays + +@testset "hash" begin + @testset "finite arrays hash as in Base" begin + for (a, b) in ((RangeCumsum(Base.OneTo(4)), [1,3,6,10]), + (RangeCumsum(2:5), [2,5,9,14]), + (Diagonal(RangeCumsum(Base.OneTo(3))), Diagonal([1,3,6])), + (RangeCumsum(Base.OneTo(3))', [1 3 6])) + @test hash(a) == hash(b) + @test hash(a, UInt(7)) == hash(b, UInt(7)) + end + @test hash(RangeCumsum(Base.OneTo(3))) == hash(RangeCumsum(1:3)) + end + + @testset "infinite arrays" begin + r = RangeCumsum(OneToInf()) + @test hash(r) isa UInt + @test hash(r) == hash(RangeCumsum(OneToInf{Int16}())) + @test hash(r, UInt(7)) == hash(RangeCumsum(OneToInf{Int16}()), UInt(7)) + @test hash(r) ≠ hash(RangeCumsum(InfiniteArrays.InfUnitRange(2))) + @test isequal(r, RangeCumsum(OneToInf())) + + # a `LayoutArray` whose entries are only realised on demand + v = InfiniteArrays.InfVec() + @test hash(v) isa UInt + @test hash(v) == hash(v) + @test hash(v) ≠ hash(InfiniteArrays.InfVec()) # distinct data + A = InfiniteArrays.InfMat() + @test hash(A) isa UInt + @test hash(A) == hash(A) + + @testset "wrappers" begin + @test hash(v') == hash(transpose(v)) + @test hash(Diagonal(v)) isa UInt + @test hash(Diagonal(v)) == hash(Diagonal(v)) + @test hash(InfBidiagonal(:U)) isa UInt + @test hash(InfUpperTriangular()) isa UInt + @test hash(Symmetric(A)) isa UInt + @test hash(view(A, OneToInf(), OneToInf())) isa UInt + + D = Diagonal(v) + @test hash(D') == hash(D) + end + end +end + +end # module