Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/ArrayLayouts.jl
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,7 @@ Base.print_matrix_row(io::IO,


include("cumsum.jl")
include("hash.jl")

###
# support overloading hcat/vcat for ∞-arrays
Expand Down
78 changes: 78 additions & 0 deletions src/hash.jl
Original file line number Diff line number Diff line change
@@ -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)
52 changes: 52 additions & 0 deletions test/test_hash.jl
Original file line number Diff line number Diff line change
@@ -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
Loading