diff --git a/NEWS.md b/NEWS.md index 211c561167122..f74d00bb1fb7f 100644 --- a/NEWS.md +++ b/NEWS.md @@ -11,6 +11,8 @@ Language changes * `mod(x::AbstractFloat, -Inf)` now returns `x` (as long as `x` is finite), this aligns with C standard and is considered a bug fix ([#47102]) + - The `hash` algorithm and its values have changed. Most `hash` specializations will remain correct and require no action. Types that reimplement the core hashing logic independently, such as some third-party string packages do, may require a migration to the new algorithm. ([#57509]) + Compiler/Runtime improvements ----------------------------- diff --git a/base/abstractarray.jl b/base/abstractarray.jl index 1c1e7ebb5c3ee..bf2a6ebabecba 100644 --- a/base/abstractarray.jl +++ b/base/abstractarray.jl @@ -3563,7 +3563,7 @@ sizehint!(a::AbstractVector, _) = a const hash_abstractarray_seed = UInt === UInt64 ? 0x7e2d6fb6448beb77 : 0xd4514ce5 function hash(A::AbstractArray, h::UInt) - h += hash_abstractarray_seed + h ⊻= hash_abstractarray_seed # Axes are themselves AbstractArrays, so hashing them directly would stack overflow # Instead hash the tuple of firsts and lasts along each dimension h = hash(map(first, axes(A)), h) diff --git a/base/binaryplatforms.jl b/base/binaryplatforms.jl index 51c65a6b310a6..86fd9118b6738 100644 --- a/base/binaryplatforms.jl +++ b/base/binaryplatforms.jl @@ -157,7 +157,7 @@ end # Hash definition to ensure that it's stable function Base.hash(p::Platform, h::UInt) - h += 0x506c6174666f726d % UInt + h ⊻= 0x506c6174666f726d % UInt h = hash(p.tags, h) h = hash(p.compare_strategies, h) return h diff --git a/base/char.jl b/base/char.jl index fc173bb3c3a44..22ffa977ca6ed 100644 --- a/base/char.jl +++ b/base/char.jl @@ -222,7 +222,7 @@ in(x::AbstractChar, y::AbstractChar) = x == y ==(x::Char, y::Char) = bitcast(UInt32, x) == bitcast(UInt32, y) isless(x::Char, y::Char) = bitcast(UInt32, x) < bitcast(UInt32, y) hash(x::Char, h::UInt) = - hash_uint64(((bitcast(UInt32, x) + UInt64(0xd4d64234)) << 32) ⊻ UInt64(h)) + hash_finalizer(((bitcast(UInt32, x) + UInt64(0xd4d64234)) << 32) ⊻ UInt64(h)) % UInt # fallbacks: isless(x::AbstractChar, y::AbstractChar) = isless(Char(x), Char(y)) diff --git a/base/gmp.jl b/base/gmp.jl index 1d7c4266c331c..a91226cebd737 100644 --- a/base/gmp.jl +++ b/base/gmp.jl @@ -892,7 +892,7 @@ if Limb === UInt64 === UInt return hash(ldexp(flipsign(Float64(limb), sz), pow), h) end h = hash_integer(pow, h) - h ⊻= hash_uint(flipsign(limb, sz) ⊻ h) + h ⊻= hash_finalizer(flipsign(limb, sz) ⊻ h) for idx = idx+1:asz if shift == 0 limb = unsafe_load(ptr, idx) @@ -906,7 +906,7 @@ if Limb === UInt64 === UInt limb = limb2 << upshift | limb1 >> shift end end - h ⊻= hash_uint(limb ⊻ h) + h ⊻= hash_finalizer(limb ⊻ h) end return h end diff --git a/base/hashing.jl b/base/hashing.jl index a2b00844ecaf5..c409d3ae7940f 100644 --- a/base/hashing.jl +++ b/base/hashing.jl @@ -1,6 +1,11 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license -## hashing a single value ## +const HASH_SEED = UInt == UInt64 ? 0xbdd89aa982704029 : 0xeabe9406 +const HASH_SECRET = tuple( + 0x2d358dccaa6c78a5, + 0x8bb84b93962eacc9, + 0x4b33a62ed433d4a3, +) """ hash(x[, h::UInt])::UInt @@ -17,75 +22,52 @@ The hash value may change when a new Julia process is started. ```jldoctest; filter = r"0x[0-9a-f]{16}" julia> a = hash(10) -0x95ea2955abd45275 +0x759d18cc5346a65f julia> hash(10, a) # only use the output of another hash function as the second argument -0xd42bad54a8575b16 +0x03158cd61b1b0bd1 ``` See also: [`objectid`](@ref), [`Dict`](@ref), [`Set`](@ref). """ -hash(x::Any) = hash(x, zero(UInt)) +hash(data::Any) = hash(data, HASH_SEED) hash(w::WeakRef, h::UInt) = hash(w.value, h) # Types can't be deleted, so marking as total allows the compiler to look up the hash -hash(T::Type, h::UInt) = hash_uint(3h - @assume_effects :total ccall(:jl_type_hash, UInt, (Any,), T)) +hash(T::Type, h::UInt) = + hash((@assume_effects :total ccall(:jl_type_hash, UInt, (Any,), T)), h) +hash(@nospecialize(data), h::UInt) = hash(objectid(data), h) -## hashing general objects ## - -hash(@nospecialize(x), h::UInt) = hash_uint(3h - objectid(x)) - -hash(x::Symbol) = objectid(x) - -## core data hashing functions ## - -function hash_64_64(n::UInt64) - a::UInt64 = n - a = ~a + a << 21 - a = a ⊻ a >> 24 - a = a + a << 3 + a << 8 - a = a ⊻ a >> 14 - a = a + a << 2 + a << 4 - a = a ⊻ a >> 28 - a = a + a << 31 - return a +function mul_parts(a::UInt64, b::UInt64) + p = widemul(a, b) + return (p >> 64) % UInt64, p % UInt64 end - -function hash_64_32(n::UInt64) - a::UInt64 = n - a = ~a + a << 18 - a = a ⊻ a >> 31 - a = a * 21 - a = a ⊻ a >> 11 - a = a + a << 6 - a = a ⊻ a >> 22 - return a % UInt32 +hash_mix(a::UInt64, b::UInt64) = ⊻(mul_parts(a, b)...) + +# faster-but-weaker than hash_mix intended for small keys +hash_mix_linear(x::UInt64, h::UInt) = 3h - x +function hash_finalizer(x::UInt64) + x ⊻= (x >> 32) + x *= 0x63652a4cd374b267 + x ⊻= (x >> 33) + return x end -function hash_32_32(n::UInt32) - a::UInt32 = n - a = a + 0x7ed55d16 + a << 12 - a = a ⊻ 0xc761c23c ⊻ a >> 19 - a = a + 0x165667b1 + a << 5 - a = a + 0xd3a2646c ⊻ a << 9 - a = a + 0xfd7046c5 + a << 3 - a = a ⊻ 0xb55a4f09 ⊻ a >> 16 - return a -end +hash_64_64(data::UInt64) = hash_finalizer(data) +hash_64_32(data::UInt64) = hash_64_64(data) % UInt32 +hash_32_32(data::UInt32) = hash_64_32(UInt64(data)) if UInt === UInt64 - hash_uint64(x::UInt64) = hash_64_64(x) - hash_uint(x::UInt) = hash_64_64(x) + const hash_uint64 = hash_64_64 + const hash_uint = hash_64_64 else - hash_uint64(x::UInt64) = hash_64_32(x) - hash_uint(x::UInt) = hash_32_32(x) + const hash_uint64 = hash_64_32 + const hash_uint = hash_32_32 end -## efficient value-based hashing of integers ## - -hash(x::Int64, h::UInt) = hash_uint64(bitcast(UInt64, x)) - 3h -hash(x::UInt64, h::UInt) = hash_uint64(x) - 3h -hash(x::Union{Bool,Int8,UInt8,Int16,UInt16,Int32,UInt32}, h::UInt) = hash(Int64(x), h) +hash(x::UInt64, h::UInt) = hash_uint64(hash_mix_linear(x, h)) +hash(x::Int64, h::UInt) = hash(bitcast(UInt64, x), h) +hash(x::Union{Bool, Int8, UInt8, Int16, UInt16, Int32, UInt32}, h::UInt) = hash(Int64(x), h) function hash_integer(n::Integer, h::UInt) h ⊻= hash_uint((n % UInt) ⊻ h) @@ -100,7 +82,7 @@ end ## efficient value-based hashing of floats ## -const hx_NaN = hash_uint64(reinterpret(UInt64, NaN)) +const hx_NaN = hash(reinterpret(UInt64, NaN)) function hash(x::Float64, h::UInt) # see comments on trunc and hash(Real, UInt) if typemin(Int64) <= x < typemax(Int64) @@ -116,7 +98,7 @@ function hash(x::Float64, h::UInt) elseif isnan(x) return hx_NaN ⊻ h # NaN does not have a stable bit pattern end - return hash_uint64(bitcast(UInt64, x)) - 3h + return hash(bitcast(UInt64, x), h) end hash(x::Float32, h::UInt) = hash(Float64(x), h) @@ -131,7 +113,7 @@ function hash(x::Float16, h::UInt) elseif isnan(x) return hx_NaN ⊻ h # NaN does not have a stable bit pattern end - return hash_uint64(bitcast(UInt64, Float64(x))) - 3h + return hash(bitcast(UInt64, Float64(x)), h) end ## generic hashing for rational values ## @@ -180,21 +162,100 @@ end ## symbol & expression hashing ## - if UInt === UInt64 - hash(x::Expr, h::UInt) = hash(x.args, hash(x.head, h + 0x83c7900696d26dc6)) - hash(x::QuoteNode, h::UInt) = hash(x.value, h + 0x2c97bf8b3de87020) + hash(x::Expr, h::UInt) = hash(x.args, hash(x.head, h ⊻ 0x83c7900696d26dc6)) + hash(x::QuoteNode, h::UInt) = hash(x.value, h ⊻ 0x2c97bf8b3de87020) else - hash(x::Expr, h::UInt) = hash(x.args, hash(x.head, h + 0x96d26dc6)) - hash(x::QuoteNode, h::UInt) = hash(x.value, h + 0x469d72af) + hash(x::Expr, h::UInt) = hash(x.args, hash(x.head, h ⊻ 0x469d72af)) + hash(x::QuoteNode, h::UInt) = hash(x.value, h ⊻ 0x469d72af) end -## hashing strings ## +hash(x::Symbol) = objectid(x) -const memhash = UInt === UInt64 ? :memhash_seed : :memhash32_seed -const memhash_seed = UInt === UInt64 ? 0x71e729fd56419c81 : 0x56419c81 -@assume_effects :total function hash(s::String, h::UInt) - h += memhash_seed - ccall(memhash, UInt, (Ptr{UInt8}, Csize_t, UInt32), s, sizeof(s), h % UInt32) + h +load_le(::Type{T}, ptr::Ptr{UInt8}, i) where {T <: Union{UInt32, UInt64}} = + unsafe_load(convert(Ptr{T}, ptr + i - 1)) + +function read_small(ptr::Ptr{UInt8}, n::Int) + return (UInt64(unsafe_load(ptr)) << 56) | + (UInt64(unsafe_load(ptr, div(n, 2) + 1)) << 32) | + UInt64(unsafe_load(ptr, n)) end + +@assume_effects :terminates_globally function hash_bytes( + ptr::Ptr{UInt8}, + n::Int, + seed::UInt64, + secret::NTuple{3, UInt64} + ) + # Adapted with gratitude from [rapidhash](https://github.com/Nicoshev/rapidhash) + buflen = UInt64(n) + seed = seed ⊻ (hash_mix(seed ⊻ secret[1], secret[2]) ⊻ buflen) + + a = zero(UInt64) + b = zero(UInt64) + + if buflen ≤ 16 + if buflen ≥ 4 + a = (UInt64(load_le(UInt32, ptr, 1)) << 32) | + UInt64(load_le(UInt32, ptr, n - 3)) + + delta = (buflen & 24) >>> (buflen >>> 3) + b = (UInt64(load_le(UInt32, ptr, delta + 1)) << 32) | + UInt64(load_le(UInt32, ptr, n - 3 - delta)) + elseif buflen > 0 + a = read_small(ptr, n) + end + else + pos = 1 + i = buflen + while i ≥ 48 + see1 = seed + see2 = seed + while i ≥ 48 + seed = hash_mix( + load_le(UInt64, ptr, pos) ⊻ secret[1], + load_le(UInt64, ptr, pos + 8) ⊻ seed + ) + see1 = hash_mix( + load_le(UInt64, ptr, pos + 16) ⊻ secret[2], + load_le(UInt64, ptr, pos + 24) ⊻ see1 + ) + see2 = hash_mix( + load_le(UInt64, ptr, pos + 32) ⊻ secret[3], + load_le(UInt64, ptr, pos + 40) ⊻ see2 + ) + pos += 48 + i -= 48 + end + seed = seed ⊻ see1 ⊻ see2 + end + if i > 16 + seed = hash_mix( + load_le(UInt64, ptr, pos) ⊻ secret[3], + load_le(UInt64, ptr, pos + 8) ⊻ seed ⊻ secret[2] + ) + if i > 32 + seed = hash_mix( + load_le(UInt64, ptr, pos + 16) ⊻ secret[3], + load_le(UInt64, ptr, pos + 24) ⊻ seed + ) + end + end + + a = load_le(UInt64, ptr, n - 15) + b = load_le(UInt64, ptr, n - 7) + end + + a = a ⊻ secret[2] + b = b ⊻ seed + b, a = mul_parts(a, b) + return hash_mix(a ⊻ secret[1] ⊻ buflen, b ⊻ secret[2]) +end + +@assume_effects :total hash(data::String, h::UInt) = + GC.@preserve data hash_bytes(pointer(data), sizeof(data), UInt64(h), HASH_SECRET) % UInt + +# no longer used in Base, but a lot of packages access these internals +const memhash = UInt === UInt64 ? :memhash_seed : :memhash32_seed +const memhash_seed = UInt === UInt64 ? 0x71e729fd56419c81 : 0x56419c81 diff --git a/base/multidimensional.jl b/base/multidimensional.jl index 1b2dd748bda97..7add7b9e74205 100644 --- a/base/multidimensional.jl +++ b/base/multidimensional.jl @@ -148,7 +148,7 @@ module IteratorsMD # hashing const cartindexhash_seed = UInt == UInt64 ? 0xd60ca92f8284b8b0 : 0xf2ea7c2e function Base.hash(ci::CartesianIndex, h::UInt) - h += cartindexhash_seed + h ⊻= cartindexhash_seed for i in ci.I h = hash(i, h) end diff --git a/base/pkgid.jl b/base/pkgid.jl index 8c776d79a69cb..577529bbe7f63 100644 --- a/base/pkgid.jl +++ b/base/pkgid.jl @@ -17,7 +17,7 @@ end ==(a::PkgId, b::PkgId) = a.uuid == b.uuid && a.name == b.name function hash(pkg::PkgId, h::UInt) - h += 0xc9f248583a0ca36c % UInt + h ⊻= 0xc9f248583a0ca36c % UInt h = hash(pkg.uuid, h) h = hash(pkg.name, h) return h diff --git a/base/regex.jl b/base/regex.jl index baef3f9fdd197..691dbc94c5563 100644 --- a/base/regex.jl +++ b/base/regex.jl @@ -802,7 +802,7 @@ end ## hash ## const hashre_seed = UInt === UInt64 ? 0x67e195eb8555e72d : 0xe32373e4 function hash(r::Regex, h::UInt) - h += hashre_seed + h ⊻= hashre_seed h = hash(r.pattern, h) h = hash(r.compile_options, h) h = hash(r.match_options, h) diff --git a/base/stacktraces.jl b/base/stacktraces.jl index 7ba23f6e715dc..9e454e430e2b6 100644 --- a/base/stacktraces.jl +++ b/base/stacktraces.jl @@ -90,7 +90,7 @@ function ==(a::StackFrame, b::StackFrame) end function hash(frame::StackFrame, h::UInt) - h += 0xf4fbda67fe20ce88 % UInt + h ⊻= 0xf4fbda67fe20ce88 % UInt h = hash(frame.line, h) h = hash(frame.file, h) h = hash(frame.func, h) diff --git a/base/strings/substring.jl b/base/strings/substring.jl index 148c860705dd3..860895207f444 100644 --- a/base/strings/substring.jl +++ b/base/strings/substring.jl @@ -135,10 +135,8 @@ end pointer(x::SubString{String}) = pointer(x.string) + x.offset pointer(x::SubString{String}, i::Integer) = pointer(x.string) + x.offset + (i-1) -function hash(s::SubString{String}, h::UInt) - h += memhash_seed - ccall(memhash, UInt, (Ptr{UInt8}, Csize_t, UInt32), s, sizeof(s), h % UInt32) + h -end +hash(data::SubString{String}, h::UInt) = + GC.@preserve data hash_bytes(pointer(data), sizeof(data), UInt64(h), HASH_SECRET) % UInt _isannotated(::SubString{T}) where {T} = _isannotated(T) diff --git a/base/tuple.jl b/base/tuple.jl index 4982ef1b23eb0..5255f8ba07539 100644 --- a/base/tuple.jl +++ b/base/tuple.jl @@ -576,10 +576,10 @@ function _eq(t1::Any32, t2::Any32) end const tuplehash_seed = UInt === UInt64 ? 0x77cfa1eef01bca90 : 0xf01bca90 -hash(::Tuple{}, h::UInt) = h + tuplehash_seed +hash(::Tuple{}, h::UInt) = h ⊻ tuplehash_seed hash(t::Tuple, h::UInt) = hash(t[1], hash(tail(t), h)) function hash(t::Any32, h::UInt) - out = h + tuplehash_seed + out = h ⊻ tuplehash_seed for i = length(t):-1:1 out = hash(t[i], out) end diff --git a/base/version.jl b/base/version.jl index b362daa78f04f..71192916a5b22 100644 --- a/base/version.jl +++ b/base/version.jl @@ -218,7 +218,7 @@ function isless(a::VersionNumber, b::VersionNumber) end function hash(v::VersionNumber, h::UInt) - h += 0x8ff4ffdb75f9fede % UInt + h ⊻= 0x8ff4ffdb75f9fede % UInt h = hash(v.major, h) h = hash(v.minor, h) h = hash(v.patch, h) diff --git a/stdlib/TOML/test/print.jl b/stdlib/TOML/test/print.jl index e8a6431cb34a7..9734d96b3c8c1 100644 --- a/stdlib/TOML/test/print.jl +++ b/stdlib/TOML/test/print.jl @@ -58,7 +58,7 @@ end [option] """ d = TOML.parse(s) - @test toml_str(d) == "user = \"me\"\n\n[julia]\n\n[option]\n" + @test toml_str(d; sorted=true) == "user = \"me\"\n\n[julia]\n\n[option]\n" end @testset "special characters" begin @@ -83,17 +83,28 @@ loaders = ["gzip", { driver = "csv", args = {delim = "\t"}}] @testset "vec with dicts and non-dicts" begin # https://github.com/JuliaLang/julia/issues/45340 d = Dict("b" => Any[111, Dict("a" => 222, "d" => 333)]) - @test toml_str(d) == "b = [111, {a = 222, d = 333}]\n" + @test toml_str(d) == (sizeof(Int) == 8 ? + "b = [111, {a = 222, d = 333}]\n" : + "b = [111, {d = 333, a = 222}]\n") + d = Dict("b" => Any[Dict("a" => 222, "d" => 333), 111]) - @test toml_str(d) == "b = [{a = 222, d = 333}, 111]\n" + @test toml_str(d) == (sizeof(Int) == 8 ? + "b = [{a = 222, d = 333}, 111]\n" : + "b = [{d = 333, a = 222}, 111]\n") d = Dict("b" => Any[Dict("a" => 222, "d" => 333)]) - @test toml_str(d) == """ - [[b]] - a = 222 - d = 333 - """ + @test toml_str(d) == (sizeof(Int) == 8 ? + """ + [[b]] + a = 222 + d = 333 + """ : + """ + [[b]] + d = 333 + a = 222 + """) # https://github.com/JuliaLang/julia/pull/57584 d = Dict("b" => [MyStruct(1), MyStruct(2)]) diff --git a/test/arrayops.jl b/test/arrayops.jl index d5fba79c47017..7e2454eabd93c 100644 --- a/test/arrayops.jl +++ b/test/arrayops.jl @@ -2203,7 +2203,7 @@ end # All we really care about is that we have an optimized # implementation, but the seed is a useful way to check that. -@test hash(CartesianIndex()) == Base.IteratorsMD.cartindexhash_seed +@test hash(CartesianIndex()) == Base.IteratorsMD.cartindexhash_seed ⊻ Base.HASH_SEED @test hash(CartesianIndex(1, 2)) != hash((1, 2)) @testset "itr, iterate" begin diff --git a/test/hashing.jl b/test/hashing.jl index 173a31d10a6a9..41a1d525961cc 100644 --- a/test/hashing.jl +++ b/test/hashing.jl @@ -88,6 +88,7 @@ vals = Any[ Dict(42 => 101, 77 => 93), Dict{Any,Any}(42 => 101, 77 => 93), (1,2,3,4), (1.0,2.0,3.0,4.0), (1,3,2,4), ("a","b"), (SubString("a",1,1), SubString("b",1,1)), + join('c':'s'), SubString(join('a':'z'), 3, 19), # issue #6900 Dict(x => x for x in 1:10), Dict(7=>7,9=>9,4=>4,10=>10,2=>2,3=>3,8=>8,5=>5,6=>6,1=>1), @@ -108,7 +109,7 @@ vals = Any[ ["a", "b", 1, 2], ["a", 1, 2], ["a", "b", 2, 2], ["a", "a", 1, 2], ["a", "b", 2, 3] ] -for a in vals, b in vals +for (i, a) in enumerate(vals), b in vals[i:end] @test isequal(a,b) == (hash(a)==hash(b)) end @@ -249,7 +250,9 @@ end ) for a in vals, b in vals - @test isequal(a, b) == (Base.hash_64_32(a) == Base.hash_64_32(b)) + ha = Base.hash_64_32(a) + hb = Base.hash_64_32(b) + @test isequal(a, b) == (ha == hb) end end @@ -260,7 +263,9 @@ end ) for a in vals, b in vals - @test isequal(a, b) == (Base.hash_32_32(a) == Base.hash_32_32(b)) + ha = Base.hash_32_32(a) + hb = Base.hash_32_32(b) + @test isequal(a, b) == (ha == hb) end end end diff --git a/test/show.jl b/test/show.jl index 17f0e2bfbf5e3..fa5989d6cd91d 100644 --- a/test/show.jl +++ b/test/show.jl @@ -1957,9 +1957,9 @@ end @test replstr(view(A, [1], :)) == "1×1 view(::Matrix{Float64}, [1], :) with eltype Float64:\n 0.0" # issue #27680 - @test showstr(Set([(1.0,1.0), (2.0,2.0), (3.0, 3.0)])) == (sizeof(Int) == 8 ? - "Set([(1.0, 1.0), (3.0, 3.0), (2.0, 2.0)])" : - "Set([(1.0, 1.0), (2.0, 2.0), (3.0, 3.0)])") + @test showstr(Set([(1.0, 1.0), (2.0, 2.0), (3.0, 3.0)])) == (sizeof(Int) == 8 ? + "Set([(2.0, 2.0), (1.0, 1.0), (3.0, 3.0)])" : + "Set([(2.0, 2.0), (1.0, 1.0), (3.0, 3.0)])") # issue #27747 let t = (x = Integer[1, 2],) diff --git a/test/tuple.jl b/test/tuple.jl index 13af5ac992434..560a1425c6bb6 100644 --- a/test/tuple.jl +++ b/test/tuple.jl @@ -369,9 +369,9 @@ end @test !isless((1,2), (1,2)) @test !isless((2,1), (1,2)) - @test hash(()) === Base.tuplehash_seed - @test hash((1,)) === hash(1, Base.tuplehash_seed) - @test hash((1,2)) === hash(1, hash(2, Base.tuplehash_seed)) + @test hash(()) === Base.tuplehash_seed ⊻ Base.HASH_SEED + @test hash((1,)) === hash(1, Base.tuplehash_seed ⊻ Base.HASH_SEED) + @test hash((1,2)) === hash(1, hash(2, Base.tuplehash_seed ⊻ Base.HASH_SEED)) # Test Any32 methods t = ntuple(identity, 32) @@ -393,7 +393,7 @@ end @test !isless((t...,1,2), (t...,1,2)) @test !isless((t...,2,1), (t...,1,2)) - @test hash(t) === foldr(hash, [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,(),UInt(0)]) + @test hash(t) === foldr(hash, vcat(1:32, (), Base.HASH_SEED)) end @testset "functions" begin