blob: c3504cc37d904dc294d0636d21ec8764593e6def [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Tests for ffi.RecursiveHash."""
from __future__ import annotations
import math
import struct
import time
from collections.abc import Callable
import numpy as np
import pytest
import tvm_ffi
import tvm_ffi.testing
from tvm_ffi._ffi_api import RecursiveEq, RecursiveHash
from tvm_ffi.testing import (
TestCompare,
TestCustomCompare,
TestCustomHash,
TestEqWithoutHash,
TestHash,
TestIntPair,
_TestCxxClassDerived,
_TestCxxClassDerivedDerived,
create_object,
)
def _make_nan_from_payload(payload: int) -> float:
"""Create a quiet NaN with a deterministic payload."""
bits = 0x7FF8000000000000 | (payload & 0x0007FFFFFFFFFFFF)
return struct.unpack(">d", struct.pack(">Q", bits))[0]
# ---------------------------------------------------------------------------
# Primitives: int
# ---------------------------------------------------------------------------
def test_int_hash_equal_values() -> None:
assert RecursiveHash(42) == RecursiveHash(42)
def test_int_hash_different_values() -> None:
assert RecursiveHash(1) != RecursiveHash(2)
def test_int64_extremes_hash() -> None:
i64_min = -(2**63)
i64_max = 2**63 - 1
assert RecursiveHash(i64_min) == RecursiveHash(i64_min)
assert RecursiveHash(i64_max) == RecursiveHash(i64_max)
assert RecursiveHash(i64_min) != RecursiveHash(i64_max)
# ---------------------------------------------------------------------------
# Primitives: float
# ---------------------------------------------------------------------------
def test_float_hash_equal_values() -> None:
assert RecursiveHash(3.14) == RecursiveHash(3.14)
def test_float_hash_different_values() -> None:
assert RecursiveHash(1.0) != RecursiveHash(2.0)
def test_float_signed_zero_hash() -> None:
"""Both +0.0 and -0.0 hash the same (consistent with RecursiveEq)."""
assert RecursiveHash(-0.0) == RecursiveHash(0.0)
def test_float_infinity_hash() -> None:
assert RecursiveHash(math.inf) == RecursiveHash(math.inf)
assert RecursiveHash(-math.inf) == RecursiveHash(-math.inf)
assert RecursiveHash(math.inf) != RecursiveHash(-math.inf)
# ---------------------------------------------------------------------------
# NaN handling
# ---------------------------------------------------------------------------
def test_nan_hash() -> None:
"""All NaN values hash the same (consistent with RecursiveEq)."""
assert RecursiveHash(math.nan) == RecursiveHash(math.nan)
def test_nan_payloads_hash_equal() -> None:
nan1 = _make_nan_from_payload(0x1)
nan2 = _make_nan_from_payload(0x2)
assert math.isnan(nan1) and math.isnan(nan2)
assert RecursiveHash(nan1) == RecursiveHash(nan2)
def test_nan_payloads_in_nested_array_hash() -> None:
nan1 = _make_nan_from_payload(0xA5)
nan2 = _make_nan_from_payload(0x5A)
a = tvm_ffi.Array([1.0, nan1, 2.0])
b = tvm_ffi.Array([1.0, nan2, 2.0])
assert RecursiveHash(a) == RecursiveHash(b)
# ---------------------------------------------------------------------------
# Primitives: bool
# ---------------------------------------------------------------------------
def test_bool_hash_equal() -> None:
assert RecursiveHash(True) == RecursiveHash(True)
assert RecursiveHash(False) == RecursiveHash(False)
def test_bool_hash_different() -> None:
assert RecursiveHash(True) != RecursiveHash(False)
# ---------------------------------------------------------------------------
# Primitives: string
# ---------------------------------------------------------------------------
def test_string_hash_equal() -> None:
assert RecursiveHash("hello") == RecursiveHash("hello")
def test_string_hash_different() -> None:
assert RecursiveHash("hello") != RecursiveHash("world")
def test_string_small_boundary_hash() -> None:
small = "1234567" # SmallStr
large = "12345678" # heap-backed Str
assert RecursiveHash(small) == RecursiveHash("1234567")
assert RecursiveHash(small) != RecursiveHash(large)
def test_string_embedded_nul_hash() -> None:
assert RecursiveHash("a\x00b") == RecursiveHash("a\x00b")
assert RecursiveHash("a\x00b") != RecursiveHash("a\x00c")
# ---------------------------------------------------------------------------
# Primitives: bytes
# ---------------------------------------------------------------------------
def test_bytes_hash_equal() -> None:
assert RecursiveHash(b"hello") == RecursiveHash(b"hello")
def test_bytes_hash_different() -> None:
assert RecursiveHash(b"hello") != RecursiveHash(b"world")
def test_bytes_small_boundary_hash() -> None:
small = b"1234567" # SmallBytes
large = b"12345678" # heap-backed Bytes
assert RecursiveHash(small) == RecursiveHash(b"1234567")
assert RecursiveHash(small) != RecursiveHash(large)
# ---------------------------------------------------------------------------
# None
# ---------------------------------------------------------------------------
def test_none_hash() -> None:
assert RecursiveHash(None) == RecursiveHash(None)
def test_none_vs_other_hash() -> None:
assert RecursiveHash(None) != RecursiveHash(0)
# ---------------------------------------------------------------------------
# DataType
# ---------------------------------------------------------------------------
def test_dtype_hash_equal() -> None:
assert RecursiveHash(tvm_ffi.dtype("float32")) == RecursiveHash(tvm_ffi.dtype("float32"))
def test_dtype_hash_different() -> None:
assert RecursiveHash(tvm_ffi.dtype("float32")) != RecursiveHash(tvm_ffi.dtype("float16"))
# ---------------------------------------------------------------------------
# Device
# ---------------------------------------------------------------------------
def test_device_hash_equal() -> None:
assert RecursiveHash(tvm_ffi.Device("cpu", 0)) == RecursiveHash(tvm_ffi.Device("cpu", 0))
def test_device_hash_different() -> None:
assert RecursiveHash(tvm_ffi.Device("cpu", 0)) != RecursiveHash(tvm_ffi.Device("cpu", 1))
# ---------------------------------------------------------------------------
# Containers: Array
# ---------------------------------------------------------------------------
def test_array_hash_equal() -> None:
a = tvm_ffi.Array([1, 2, 3])
b = tvm_ffi.Array([1, 2, 3])
assert RecursiveHash(a) == RecursiveHash(b)
def test_array_hash_different() -> None:
a = tvm_ffi.Array([1, 2, 3])
c = tvm_ffi.Array([1, 2, 4])
assert RecursiveHash(a) != RecursiveHash(c)
def test_array_empty_hash() -> None:
empty1 = tvm_ffi.Array([])
empty2 = tvm_ffi.Array([])
assert RecursiveHash(empty1) == RecursiveHash(empty2)
def test_array_different_length_hash() -> None:
a = tvm_ffi.Array([1, 2])
b = tvm_ffi.Array([1, 2, 3])
assert RecursiveHash(a) != RecursiveHash(b)
# ---------------------------------------------------------------------------
# Containers: List
# ---------------------------------------------------------------------------
def test_list_hash_equal() -> None:
a = tvm_ffi.List([1, 2, 3])
b = tvm_ffi.List([1, 2, 3])
assert RecursiveHash(a) == RecursiveHash(b)
def test_list_hash_different() -> None:
a = tvm_ffi.List([1, 2, 3])
c = tvm_ffi.List([1, 2, 4])
assert RecursiveHash(a) != RecursiveHash(c)
# ---------------------------------------------------------------------------
# Shape
# ---------------------------------------------------------------------------
def test_shape_hash_equal() -> None:
a = tvm_ffi.Shape((2, 3, 4))
b = tvm_ffi.Shape((2, 3, 4))
assert RecursiveHash(a) == RecursiveHash(b)
def test_shape_hash_different() -> None:
a = tvm_ffi.Shape((2, 3, 4))
c = tvm_ffi.Shape((2, 3, 5))
assert RecursiveHash(a) != RecursiveHash(c)
# ---------------------------------------------------------------------------
# Map/Dict
# ---------------------------------------------------------------------------
def test_map_hash_equal() -> None:
a = tvm_ffi.Map({"x": 1, "y": 2})
b = tvm_ffi.Map({"x": 1, "y": 2})
assert RecursiveHash(a) == RecursiveHash(b)
def test_map_hash_different_values() -> None:
a = tvm_ffi.Map({"x": 1, "y": 2})
c = tvm_ffi.Map({"x": 1, "y": 3})
assert RecursiveHash(a) != RecursiveHash(c)
def test_map_hash_different_size() -> None:
a = tvm_ffi.Map({"x": 1})
b = tvm_ffi.Map({"x": 1, "y": 2})
assert RecursiveHash(a) != RecursiveHash(b)
def test_map_hash_order_independent() -> None:
"""Map hash should be the same regardless of insertion order."""
a = tvm_ffi.Map({"x": 1, "y": 2, "z": 3})
b = tvm_ffi.Map({"z": 3, "x": 1, "y": 2})
assert RecursiveHash(a) == RecursiveHash(b)
def test_dict_hash_equal() -> None:
a = tvm_ffi.Dict({"x": 1, "y": 2})
b = tvm_ffi.Dict({"x": 1, "y": 2})
assert RecursiveHash(a) == RecursiveHash(b)
def test_dict_hash_different() -> None:
a = tvm_ffi.Dict({"x": 1})
b = tvm_ffi.Dict({"x": 2})
assert RecursiveHash(a) != RecursiveHash(b)
# ---------------------------------------------------------------------------
# Reflected objects: TestIntPair
# ---------------------------------------------------------------------------
def test_reflected_obj_hash_equal() -> None:
a = TestIntPair(1, 2)
b = TestIntPair(1, 2)
assert RecursiveHash(a) == RecursiveHash(b)
def test_reflected_obj_hash_different() -> None:
a = TestIntPair(1, 2)
c = TestIntPair(1, 3)
assert RecursiveHash(a) != RecursiveHash(c)
# ---------------------------------------------------------------------------
# HashOff flag: TestHash
# ---------------------------------------------------------------------------
def test_hash_off_ignored_field() -> None:
"""hash_ignored is excluded from hashing via Hash(false)."""
a = TestHash(1, "x", 100)
b = TestHash(1, "x", 999)
assert RecursiveHash(a) == RecursiveHash(b)
def test_hash_off_key_differs() -> None:
a = TestHash(1, "x", 100)
b = TestHash(2, "x", 100)
assert RecursiveHash(a) != RecursiveHash(b)
def test_hash_off_name_differs() -> None:
a = TestHash(1, "a", 100)
b = TestHash(1, "b", 100)
assert RecursiveHash(a) != RecursiveHash(b)
# ---------------------------------------------------------------------------
# CompareOff implies hash-off: TestCompare
# ---------------------------------------------------------------------------
def test_compare_off_implies_hash_off() -> None:
"""Fields with Compare(false) are also excluded from hashing."""
a = TestCompare(1, "x", 100)
b = TestCompare(1, "x", 999)
assert RecursiveHash(a) == RecursiveHash(b)
# ---------------------------------------------------------------------------
# Same pointer fast path
# ---------------------------------------------------------------------------
def test_same_pointer_hash() -> None:
x = TestIntPair(42, 99)
assert RecursiveHash(x) == RecursiveHash(x)
# ---------------------------------------------------------------------------
# Different types produce different hashes
# ---------------------------------------------------------------------------
def test_different_types_hash() -> None:
"""Different type indices should generally produce different hashes."""
assert RecursiveHash(1) != RecursiveHash(1.0)
assert RecursiveHash(1) != RecursiveHash(True)
# ---------------------------------------------------------------------------
# Nested containers
# ---------------------------------------------------------------------------
def test_array_of_arrays_hash() -> None:
a = tvm_ffi.Array([tvm_ffi.Array([1, 2]), tvm_ffi.Array([3, 4])])
b = tvm_ffi.Array([tvm_ffi.Array([1, 2]), tvm_ffi.Array([3, 4])])
c = tvm_ffi.Array([tvm_ffi.Array([1, 2]), tvm_ffi.Array([3, 5])])
assert RecursiveHash(a) == RecursiveHash(b)
assert RecursiveHash(a) != RecursiveHash(c)
def test_list_of_lists_hash() -> None:
a = tvm_ffi.List([tvm_ffi.List([1, 2]), tvm_ffi.List([3])])
b = tvm_ffi.List([tvm_ffi.List([1, 2]), tvm_ffi.List([3])])
c = tvm_ffi.List([tvm_ffi.List([1, 2]), tvm_ffi.List([4])])
assert RecursiveHash(a) == RecursiveHash(b)
assert RecursiveHash(a) != RecursiveHash(c)
def test_three_level_nested_hash() -> None:
a = tvm_ffi.Array([tvm_ffi.Array([tvm_ffi.Array([1])])])
b = tvm_ffi.Array([tvm_ffi.Array([tvm_ffi.Array([1])])])
c = tvm_ffi.Array([tvm_ffi.Array([tvm_ffi.Array([2])])])
assert RecursiveHash(a) == RecursiveHash(b)
assert RecursiveHash(a) != RecursiveHash(c)
def test_map_with_array_values_hash() -> None:
a = tvm_ffi.Map({"k": tvm_ffi.Array([1, 2])})
b = tvm_ffi.Map({"k": tvm_ffi.Array([1, 2])})
c = tvm_ffi.Map({"k": tvm_ffi.Array([1, 3])})
assert RecursiveHash(a) == RecursiveHash(b)
assert RecursiveHash(a) != RecursiveHash(c)
# ---------------------------------------------------------------------------
# Inherited field hashing
# ---------------------------------------------------------------------------
def test_inherited_fields_hash_equal() -> None:
a = _TestCxxClassDerived(10, 20, 1.5, 2.5)
b = _TestCxxClassDerived(10, 20, 1.5, 2.5)
assert RecursiveHash(a) == RecursiveHash(b)
def test_inherited_fields_differ_in_base_hash() -> None:
a = _TestCxxClassDerived(10, 20, 1.5, 2.5)
b = _TestCxxClassDerived(99, 20, 1.5, 2.5)
assert RecursiveHash(a) != RecursiveHash(b)
def test_three_level_inheritance_hash() -> None:
a = _TestCxxClassDerivedDerived(1, 2, 3.0, True, 4.0, "hi")
b = _TestCxxClassDerivedDerived(1, 2, 3.0, True, 4.0, "hi")
assert RecursiveHash(a) == RecursiveHash(b)
c = _TestCxxClassDerivedDerived(1, 2, 3.0, False, 4.0, "hi")
assert RecursiveHash(a) != RecursiveHash(c)
# ---------------------------------------------------------------------------
# Objects with container fields
# ---------------------------------------------------------------------------
def test_object_with_array_field_hash() -> None:
a = create_object(
"testing.TestObjectDerived",
v_i64=1,
v_f64=2.0,
v_str="s",
v_map=tvm_ffi.Map({}),
v_array=tvm_ffi.Array([10, 20, 30]),
)
b = create_object(
"testing.TestObjectDerived",
v_i64=1,
v_f64=2.0,
v_str="s",
v_map=tvm_ffi.Map({}),
v_array=tvm_ffi.Array([10, 20, 30]),
)
assert RecursiveHash(a) == RecursiveHash(b)
def test_object_with_array_field_differ_hash() -> None:
a = create_object(
"testing.TestObjectDerived",
v_i64=1,
v_f64=2.0,
v_str="s",
v_map=tvm_ffi.Map({}),
v_array=tvm_ffi.Array([10, 20]),
)
b = create_object(
"testing.TestObjectDerived",
v_i64=1,
v_f64=2.0,
v_str="s",
v_map=tvm_ffi.Map({}),
v_array=tvm_ffi.Array([10, 21]),
)
assert RecursiveHash(a) != RecursiveHash(b)
# ---------------------------------------------------------------------------
# Array/List type mismatch: same content, different types
# ---------------------------------------------------------------------------
def test_array_vs_list_different_hash() -> None:
arr = tvm_ffi.Array([1, 2])
lst = tvm_ffi.List([1, 2])
assert RecursiveHash(arr) != RecursiveHash(lst)
def test_map_vs_dict_different_hash() -> None:
m = tvm_ffi.Map({"k": 1})
d = tvm_ffi.Dict({"k": 1})
assert RecursiveHash(m) != RecursiveHash(d)
# ---------------------------------------------------------------------------
# None in nested contexts
# ---------------------------------------------------------------------------
def test_array_with_none_elements_hash() -> None:
a = tvm_ffi.Array([None, 1, None])
b = tvm_ffi.Array([None, 1, None])
c = tvm_ffi.Array([None, 2, None])
assert RecursiveHash(a) == RecursiveHash(b)
assert RecursiveHash(a) != RecursiveHash(c)
# ---------------------------------------------------------------------------
# Cycle safety: run in subprocess
# ---------------------------------------------------------------------------
def test_cyclic_list_same_pointer_hash() -> None:
"""Same cyclic list hashed with itself succeeds (pointer identity short-circuit)."""
lst = tvm_ffi.List()
lst.append(lst)
# Should not raise; same pointer returns a deterministic hash
h = RecursiveHash(lst)
assert h == RecursiveHash(lst)
def test_cyclic_list_handled() -> None:
"""Distinct cyclic lists are handled gracefully via on-stack cycle detection."""
a = tvm_ffi.List()
a.append(a)
b = tvm_ffi.List()
b.append(b)
h_a = RecursiveHash(a)
h_b = RecursiveHash(b)
assert h_a == h_b
def test_cyclic_dict_handled() -> None:
"""Cyclic dict is handled gracefully via on-stack cycle detection."""
d = tvm_ffi.Dict()
d["self"] = d
h = RecursiveHash(d)
assert h == RecursiveHash(d)
# ---------------------------------------------------------------------------
# Consistency law: RecursiveEq(a, b) => RecursiveHash(a) == RecursiveHash(b)
# ---------------------------------------------------------------------------
def test_consistency_primitives() -> None:
"""Verify hash consistency for various primitive pairs."""
pairs = [
(42, 42),
(3.14, 3.14),
(True, True),
(False, False),
("hello", "hello"),
(b"hello", b"hello"),
(None, None),
]
for a, b in pairs:
assert RecursiveEq(a, b), f"Expected RecursiveEq({a!r}, {b!r})"
assert RecursiveHash(a) == RecursiveHash(b), (
f"Hash mismatch for equal values: {a!r} and {b!r}"
)
def test_consistency_nan() -> None:
nan1 = _make_nan_from_payload(0x1)
nan2 = _make_nan_from_payload(0x2)
assert RecursiveEq(nan1, nan2)
assert RecursiveHash(nan1) == RecursiveHash(nan2)
def test_consistency_signed_zero() -> None:
assert RecursiveEq(-0.0, 0.0)
assert RecursiveHash(-0.0) == RecursiveHash(0.0)
def test_consistency_containers() -> None:
"""Verify hash consistency for containers."""
pairs = [
(tvm_ffi.Array([1, 2, 3]), tvm_ffi.Array([1, 2, 3])),
(tvm_ffi.List([1, 2, 3]), tvm_ffi.List([1, 2, 3])),
(tvm_ffi.Shape((2, 3, 4)), tvm_ffi.Shape((2, 3, 4))),
(tvm_ffi.Map({"x": 1, "y": 2}), tvm_ffi.Map({"x": 1, "y": 2})),
(tvm_ffi.Dict({"x": 1, "y": 2}), tvm_ffi.Dict({"x": 1, "y": 2})),
]
for a, b in pairs:
assert RecursiveEq(a, b), f"Expected RecursiveEq for {a!r} and {b!r}"
assert RecursiveHash(a) == RecursiveHash(b), "Hash mismatch for equal containers"
def test_consistency_reflected_objects() -> None:
"""Verify hash consistency for reflected objects."""
a = TestIntPair(1, 2)
b = TestIntPair(1, 2)
assert RecursiveEq(a, b)
assert RecursiveHash(a) == RecursiveHash(b)
def test_consistency_compare_off() -> None:
"""Fields excluded from comparison are also excluded from hash."""
a = TestCompare(1, "x", 100)
b = TestCompare(1, "x", 999)
assert RecursiveEq(a, b)
assert RecursiveHash(a) == RecursiveHash(b)
def test_consistency_hash_off() -> None:
"""Fields excluded from hashing produce same hash when they differ."""
a = TestHash(1, "x", 100)
b = TestHash(1, "x", 999)
assert RecursiveHash(a) == RecursiveHash(b)
def test_consistency_law_on_int_pairs() -> None:
"""Verify: RecursiveEq(a, b) => RecursiveHash(a) == RecursiveHash(b)."""
values = [
TestIntPair(0, 0),
TestIntPair(0, 1),
TestIntPair(1, 0),
TestIntPair(1, 1),
]
for a in values:
for b in values:
if RecursiveEq(a, b):
assert RecursiveHash(a) == RecursiveHash(b), (
f"Hash consistency violated: RecursiveEq({a},{b}) but hashes differ"
)
def _make_nested_singleton_array(depth: int) -> object:
value: object = 0
for _ in range(depth):
value = tvm_ffi.Array([value])
return value
# ---------------------------------------------------------------------------
# Shared-reference aliasing invariants
# ---------------------------------------------------------------------------
def test_aliasing_consistency_array_of_reflected_objects() -> None:
shared = TestIntPair(11, 22)
aliased = tvm_ffi.Array([shared, shared])
duplicated = tvm_ffi.Array(
[
TestIntPair(11, 22),
TestIntPair(11, 22),
]
)
assert RecursiveEq(aliased, duplicated)
assert RecursiveHash(aliased) == RecursiveHash(duplicated)
def test_aliasing_consistency_list_of_reflected_objects() -> None:
shared = TestIntPair(13, 26)
aliased = tvm_ffi.List([shared, shared])
duplicated = tvm_ffi.List(
[
TestIntPair(13, 26),
TestIntPair(13, 26),
]
)
assert RecursiveEq(aliased, duplicated)
assert RecursiveHash(aliased) == RecursiveHash(duplicated)
def test_aliasing_consistency_array_of_arrays() -> None:
shared = tvm_ffi.Array([1, 2, 3])
aliased = tvm_ffi.Array([shared, shared])
duplicated = tvm_ffi.Array([tvm_ffi.Array([1, 2, 3]), tvm_ffi.Array([1, 2, 3])])
assert RecursiveEq(aliased, duplicated)
assert RecursiveHash(aliased) == RecursiveHash(duplicated)
def test_aliasing_consistency_list_of_lists() -> None:
shared = tvm_ffi.List([1, 2, 3])
aliased = tvm_ffi.List([shared, shared])
duplicated = tvm_ffi.List([tvm_ffi.List([1, 2, 3]), tvm_ffi.List([1, 2, 3])])
assert RecursiveEq(aliased, duplicated)
assert RecursiveHash(aliased) == RecursiveHash(duplicated)
def test_aliasing_consistency_shape_objects() -> None:
shared = tvm_ffi.Shape((3, 4))
aliased = tvm_ffi.Array([shared, shared])
duplicated = tvm_ffi.Array([tvm_ffi.Shape((3, 4)), tvm_ffi.Shape((3, 4))])
assert RecursiveEq(aliased, duplicated)
assert RecursiveHash(aliased) == RecursiveHash(duplicated)
def test_aliasing_consistency_map_shared_values() -> None:
shared = TestIntPair(31, 41)
aliased = tvm_ffi.Map({"x": shared, "y": shared})
duplicated = tvm_ffi.Map(
{
"x": TestIntPair(31, 41),
"y": TestIntPair(31, 41),
}
)
assert RecursiveEq(aliased, duplicated)
assert RecursiveHash(aliased) == RecursiveHash(duplicated)
def test_aliasing_consistency_dict_shared_values() -> None:
shared = tvm_ffi.Array([7, 8, 9])
aliased = tvm_ffi.Dict({"x": shared, "y": shared})
duplicated = tvm_ffi.Dict({"x": tvm_ffi.Array([7, 8, 9]), "y": tvm_ffi.Array([7, 8, 9])})
assert RecursiveEq(aliased, duplicated)
assert RecursiveHash(aliased) == RecursiveHash(duplicated)
def test_aliasing_consistency_reflected_object_fields() -> None:
shared = TestIntPair(5, 6)
aliased = create_object(
"testing.TestObjectDerived",
v_i64=1,
v_f64=2.0,
v_str="shared",
v_map=tvm_ffi.Map({"k": shared}),
v_array=tvm_ffi.Array([shared]),
)
duplicated = create_object(
"testing.TestObjectDerived",
v_i64=1,
v_f64=2.0,
v_str="shared",
v_map=tvm_ffi.Map({"k": TestIntPair(5, 6)}),
v_array=tvm_ffi.Array([TestIntPair(5, 6)]),
)
assert RecursiveEq(aliased, duplicated)
assert RecursiveHash(aliased) == RecursiveHash(duplicated)
# ---------------------------------------------------------------------------
# Map/Dict order-independence with shared references
# ---------------------------------------------------------------------------
def test_map_hash_order_independent_with_shared_values() -> None:
shared = TestIntPair(1, 2)
a = tvm_ffi.Map({"a": shared, "b": shared, "c": shared})
b = tvm_ffi.Map({"b": shared, "a": shared, "c": shared})
assert RecursiveEq(a, b)
assert RecursiveHash(a) == RecursiveHash(b)
def test_dict_hash_order_independent_with_shared_values() -> None:
shared = tvm_ffi.Array([1, 2])
a = tvm_ffi.Dict({"a": shared, "b": shared, "c": shared})
b = tvm_ffi.Dict({"b": shared, "a": shared, "c": shared})
assert RecursiveEq(a, b)
assert RecursiveHash(a) == RecursiveHash(b)
# ---------------------------------------------------------------------------
# Recursion-depth boundary
# ---------------------------------------------------------------------------
def test_depth_127_nested_arrays_allowed() -> None:
RecursiveHash(_make_nested_singleton_array(127))
def test_depth_1000_nested_arrays_works() -> None:
"""Deep graphs now succeed thanks to iterative (heap-based) stack."""
RecursiveHash(_make_nested_singleton_array(1000))
# ---------------------------------------------------------------------------
# Additional adversarial quality checks
# ---------------------------------------------------------------------------
def test_map_hash_collision_swap_values() -> None:
"""Swapping values across two keys should not trivially collide."""
a = tvm_ffi.Map({"a": 0, "b": 1})
b = tvm_ffi.Map({"a": 1, "b": 0})
assert not RecursiveEq(a, b)
assert RecursiveHash(a) != RecursiveHash(b)
def test_array_hash_collision_small_int_pairs() -> None:
"""Distinct short arrays should not have obvious low-domain collisions."""
a = tvm_ffi.Array([1, 2])
b = tvm_ffi.Array([2, 61])
assert not RecursiveEq(a, b)
assert RecursiveHash(a) != RecursiveHash(b)
def test_function_hash_consistent_with_eq() -> None:
"""Functions have no reflected fields, so RecursiveEq treats all as equal.
Hash must be consistent: RecursiveEq(a, b) => RecursiveHash(a) == RecursiveHash(b).
"""
f_add_one = tvm_ffi.get_global_func("testing.add_one")
f_nop = tvm_ffi.get_global_func("testing.nop")
assert RecursiveEq(f_add_one, f_nop)
assert RecursiveHash(f_add_one) == RecursiveHash(f_nop)
def test_tensor_hash_consistent_with_eq() -> None:
"""Tensors have no reflected fields, so RecursiveEq treats all as equal.
Hash must be consistent: RecursiveEq(a, b) => RecursiveHash(a) == RecursiveHash(b).
"""
t1 = tvm_ffi.from_dlpack(np.array([1, 2], dtype="int32"))
t2 = tvm_ffi.from_dlpack(np.array([[9, 8, 7]], dtype="int64"))
assert RecursiveEq(t1, t2)
assert RecursiveHash(t1) == RecursiveHash(t2)
def _make_shared_binary_dag(depth: int) -> object:
node: object = 1
for _ in range(depth):
# Two edges point to the same child object (DAG with heavy sharing).
node = tvm_ffi.Array([node, node])
return node
def test_shared_dag_hash_scaling_not_exponential() -> None:
"""Hashing shared DAGs should scale roughly linearly in depth."""
d18 = _make_shared_binary_dag(18)
d19 = _make_shared_binary_dag(19)
# Warm-up run to mitigate one-time setup costs
RecursiveHash(_make_shared_binary_dag(10))
repeats = 10
t0 = time.perf_counter()
for _ in range(repeats):
RecursiveHash(d18)
t18 = (time.perf_counter() - t0) / repeats
t0 = time.perf_counter()
for _ in range(repeats):
RecursiveHash(d19)
t19 = (time.perf_counter() - t0) / repeats
# With memoization this ratio should stay close to 1x. Threshold 4x leaves
# buffer for microsecond-scale measurement noise (CI runs frequently see
# t18~4us, t19~8us where small jitter blows past a 2x ceiling) while
# still catching true exponential blowup, which compounds far past 4x
# within a few depth steps.
assert t19 <= t18 * 4.0, f"Unexpected super-linear scaling: d18={t18:.6f}s d19={t19:.6f}s"
# ---------------------------------------------------------------------------
# Custom __ffi_hash__ hook: TestCustomHash
# ---------------------------------------------------------------------------
def test_custom_hash_ignores_label() -> None:
"""TestCustomHash hashes only `key`, ignoring `label`."""
a = TestCustomHash(42, "alpha")
b = TestCustomHash(42, "beta")
assert RecursiveHash(a) == RecursiveHash(b)
def test_custom_hash_different_key() -> None:
a = TestCustomHash(1, "same")
b = TestCustomHash(2, "same")
assert RecursiveHash(a) != RecursiveHash(b)
def test_custom_hash_in_container() -> None:
"""Custom-hooked objects inside an Array."""
a = tvm_ffi.Array(
[
TestCustomHash(1, "x"),
TestCustomHash(2, "y"),
]
)
b = tvm_ffi.Array(
[
TestCustomHash(1, "different"),
TestCustomHash(2, "labels"),
]
)
assert RecursiveHash(a) == RecursiveHash(b)
def test_custom_hash_consistency_with_eq() -> None:
"""RecursiveEq(a,b) => RecursiveHash(a)==RecursiveHash(b) for TestCustomCompare."""
a = TestCustomCompare(42, "alpha")
b = TestCustomCompare(42, "beta")
assert RecursiveEq(a, b)
assert RecursiveHash(a) == RecursiveHash(b)
# ---------------------------------------------------------------------------
# Failing regression tests for Eq=>Hash invariant
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("key", [-7, -1, 0, 1, 7, 1024])
@pytest.mark.parametrize("lhs_label,rhs_label", [("alpha", "beta"), ("x", "y"), ("foo", "bar")])
def test_custom_compare_eq_implies_hash_same_direct(
key: int, lhs_label: str, rhs_label: str
) -> None:
lhs = TestCustomCompare(key, lhs_label)
rhs = TestCustomCompare(key, rhs_label)
assert RecursiveEq(lhs, rhs)
assert RecursiveHash(lhs) == RecursiveHash(rhs)
@pytest.mark.parametrize("key", [-3, -1, 0, 2, 11])
@pytest.mark.parametrize(
"wrap",
[
lambda obj: tvm_ffi.Array([obj]),
lambda obj: tvm_ffi.List([obj]),
lambda obj: tvm_ffi.Array([0, obj, 1]),
lambda obj: tvm_ffi.List([0, obj, 1]),
lambda obj: tvm_ffi.Map({"k": obj}),
lambda obj: tvm_ffi.Dict({"k": obj}),
lambda obj: tvm_ffi.Array([tvm_ffi.Array([obj])]),
lambda obj: tvm_ffi.List([tvm_ffi.List([obj])]),
],
)
def test_custom_compare_eq_implies_hash_same_in_wrappers(
key: int, wrap: Callable[[object], object]
) -> None:
lhs_obj = TestCustomCompare(key, "left")
rhs_obj = TestCustomCompare(key, "right")
lhs = wrap(lhs_obj)
rhs = wrap(rhs_obj)
assert RecursiveEq(lhs, rhs)
assert RecursiveHash(lhs) == RecursiveHash(rhs)
# ---------------------------------------------------------------------------
# Guard: __ffi_eq__ without __ffi_hash__ must raise in RecursiveHash
# ---------------------------------------------------------------------------
def test_eq_without_hash_raises() -> None:
"""RecursiveHash rejects types that define __ffi_eq__ but not __ffi_hash__."""
obj = TestEqWithoutHash(1, "hello")
with pytest.raises(ValueError, match="__ffi_eq__ or __ffi_compare__ but not __ffi_hash__"):
RecursiveHash(obj)
def test_eq_without_hash_inside_container_raises() -> None:
"""The guard also triggers when the object is nested inside a container."""
obj = TestEqWithoutHash(1, "hello")
arr = tvm_ffi.Array([obj])
with pytest.raises(ValueError, match="__ffi_eq__ or __ffi_compare__ but not __ffi_hash__"):
RecursiveHash(arr)
# ---------------------------------------------------------------------------
# Custom __ffi_hash__ hook via @py_class
# ---------------------------------------------------------------------------
import itertools as _itertools_hash
from typing import Any as _Any_hash
from typing import Callable as _Callable_hash
from tvm_ffi.core import Object as _Object_hash
from tvm_ffi.dataclasses import py_class as _py_class_hash
_counter_hash = _itertools_hash.count()
def _unique_key_hash(base: str) -> str:
return f"testing.hash_pc.{base}_{next(_counter_hash)}"
@_py_class_hash(_unique_key_hash("PyCustomHash"))
class _PyCustomHash(_Object_hash):
key: int
label: str
def __ffi_hash__(self, fn_hash: _Callable_hash[..., _Any_hash]) -> int:
return fn_hash(self.key)
def test_py_class_custom_hash_ignores_label() -> None:
a = _PyCustomHash(42, "alpha")
b = _PyCustomHash(42, "beta")
assert RecursiveHash(a) == RecursiveHash(b)
def test_py_class_custom_hash_different_key() -> None:
a = _PyCustomHash(1, "same")
b = _PyCustomHash(2, "same")
assert RecursiveHash(a) != RecursiveHash(b)