This is an automated email from the ASF dual-hosted git repository.

tqchen pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tvm-ffi.git


The following commit(s) were added to refs/heads/main by this push:
     new 6adc8df7 [EXTRA] Introduce StructuralKey (#453)
6adc8df7 is described below

commit 6adc8df7d2180ea14e463d3beeaa3d7eecb6f897
Author: Tianqi Chen <[email protected]>
AuthorDate: Mon Feb 16 21:08:20 2026 -0500

    [EXTRA] Introduce StructuralKey (#453)
    
    This PR introduces StructuralKey as a convenient wrapper class to
    indicate use of structural equal/hash in comparisons. It can be useful
    to serve as an intermediate wrapper class in structural equality related
    checks.
---
 include/tvm/ffi/extra/structural_key.h | 103 +++++++++++++++
 python/tvm_ffi/__init__.py             |  12 ++
 python/tvm_ffi/_ffi_api.py             |   6 +
 python/tvm_ffi/structural.py           | 230 +++++++++++++++++++++++++++++++++
 src/ffi/extra/reflection_extra.cc      |  38 +++++-
 src/ffi/extra/structural_equal.cc      |   4 +-
 tests/cpp/extra/test_structural_key.cc |  79 +++++++++++
 tests/python/test_structural.py        |  92 +++++++++++++
 8 files changed, 562 insertions(+), 2 deletions(-)

diff --git a/include/tvm/ffi/extra/structural_key.h 
b/include/tvm/ffi/extra/structural_key.h
new file mode 100644
index 00000000..08afd018
--- /dev/null
+++ b/include/tvm/ffi/extra/structural_key.h
@@ -0,0 +1,103 @@
+/*
+ * 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.
+ */
+/*!
+ * \file tvm/ffi/extra/structural_key.h
+ * \brief Structural-key wrapper that caches structural hash.
+ */
+#ifndef TVM_FFI_EXTRA_STRUCTURAL_KEY_H_
+#define TVM_FFI_EXTRA_STRUCTURAL_KEY_H_
+
+#include <tvm/ffi/any.h>
+#include <tvm/ffi/extra/structural_equal.h>
+#include <tvm/ffi/extra/structural_hash.h>
+
+#include <cstddef>
+#include <cstdint>
+#include <functional>
+#include <utility>
+
+namespace tvm {
+namespace ffi {
+
+/*!
+ * \brief Object node that contains a key and its cached structural hash.
+ */
+class StructuralKeyObj : public Object {
+ public:
+  /*! \brief The key value. */
+  Any key;
+  /*! \brief Cached structural hash of \p key, encoded as int64_t. */
+  int64_t hash_i64{0};
+
+  // Default constructor to support reflection-based initialization.
+  StructuralKeyObj() = default;
+  /*!
+   * \brief Construct a StructuralKeyObj from a key and cache its structural 
hash.
+   * \param key The key value.
+   */
+  explicit StructuralKeyObj(Any key)
+      : key(std::move(key)), 
hash_i64(static_cast<int64_t>(StructuralHash::Hash(this->key))) {}
+
+  /// \cond Doxygen_Suppress
+  TVM_FFI_DECLARE_OBJECT_INFO_FINAL("ffi.StructuralKey", StructuralKeyObj, 
Object);
+  /// \endcond
+};
+
+/*!
+ * \brief ObjectRef wrapper for StructuralKeyObj.
+ *
+ * StructuralKey can be used to ensure that we use structural equality and 
hash for the wrapped key.
+ */
+class StructuralKey : public ObjectRef {
+ public:
+  /*!
+   * \brief Construct a StructuralKey from a key.
+   * \param key The key value.
+   */
+  explicit StructuralKey(Any key) : 
ObjectRef(make_object<StructuralKeyObj>(std::move(key))) {}
+
+  bool operator==(const StructuralKey& other) const {
+    if (this->same_as(other)) {
+      return true;
+    }
+    if (this->get()->hash_i64 != other->hash_i64) {
+      return false;
+    }
+    return StructuralEqual::Equal(this->get()->key, other->key);
+  }
+  bool operator!=(const StructuralKey& other) const { return !(*this == 
other); }
+
+  /// \cond Doxygen_Suppress
+  TVM_FFI_DEFINE_OBJECT_REF_METHODS_NOTNULLABLE(StructuralKey, ObjectRef, 
StructuralKeyObj);
+  /// \endcond
+};
+
+}  // namespace ffi
+}  // namespace tvm
+
+namespace std {
+template <>
+struct hash<tvm::ffi::StructuralKey> {
+  size_t operator()(const tvm::ffi::StructuralKey& key) const {
+    return static_cast<size_t>(static_cast<uint64_t>(key->hash_i64));
+  }
+};
+}  // namespace std
+
+#endif  // TVM_FFI_EXTRA_STRUCTURAL_KEY_H_
diff --git a/python/tvm_ffi/__init__.py b/python/tvm_ffi/__init__.py
index 21eca3b6..3eedbc55 100644
--- a/python/tvm_ffi/__init__.py
+++ b/python/tvm_ffi/__init__.py
@@ -54,9 +54,16 @@ from ._tensor import from_dlpack, Tensor, Shape
 from .container import Array, List, Map
 from .module import Module, system_lib, load_module
 from .stream import StreamContext, get_raw_stream, use_raw_stream, 
use_torch_stream
+from .structural import (
+    StructuralKey,
+    get_first_structural_mismatch,
+    structural_equal,
+    structural_hash,
+)
 from . import serialization
 from . import access_path
 from . import dataclasses
+from . import structural
 from . import cpp
 
 # optional module to speedup dlpack conversion
@@ -104,6 +111,7 @@ __all__ = [
     "ObjectConvertible",
     "Shape",
     "StreamContext",
+    "StructuralKey",
     "Tensor",
     "__version__",
     "__version_tuple__",
@@ -114,6 +122,7 @@ __all__ = [
     "device",
     "dtype",
     "from_dlpack",
+    "get_first_structural_mismatch",
     "get_global_func",
     "get_global_func_metadata",
     "get_raw_stream",
@@ -124,6 +133,9 @@ __all__ = [
     "register_object",
     "remove_global_func",
     "serialization",
+    "structural",
+    "structural_equal",
+    "structural_hash",
     "system_lib",
     "use_raw_stream",
     "use_torch_stream",
diff --git a/python/tvm_ffi/_ffi_api.py b/python/tvm_ffi/_ffi_api.py
index 92606c9e..bf77b76a 100644
--- a/python/tvm_ffi/_ffi_api.py
+++ b/python/tvm_ffi/_ffi_api.py
@@ -84,6 +84,9 @@ if TYPE_CHECKING:
     def ModuleWriteToFile(_0: Module, _1: str, _2: str, /) -> None: ...
     def Shape(*args: Any) -> Any: ...
     def String(_0: str, /) -> str: ...
+    def StructuralKey(_0: Any, /) -> Any: ...
+    def StructuralKeyEqual(_0: Any, _1: Any, /) -> bool: ...
+    def StructuralEqual(_0: Any, _1: Any, _2: bool, _3: bool, /) -> bool: ...
     def StructuralHash(_0: Any, _1: bool, _2: bool, /) -> int: ...
     def SystemLib(*args: Any) -> Any: ...
     def ToJSONGraph(_0: Any, _1: Any, /) -> Any: ...
@@ -143,7 +146,10 @@ __all__ = [
     "ModuleWriteToFile",
     "Shape",
     "String",
+    "StructuralEqual",
     "StructuralHash",
+    "StructuralKey",
+    "StructuralKeyEqual",
     "SystemLib",
     "ToJSONGraph",
     "ToJSONGraphString",
diff --git a/python/tvm_ffi/structural.py b/python/tvm_ffi/structural.py
new file mode 100644
index 00000000..120268a1
--- /dev/null
+++ b/python/tvm_ffi/structural.py
@@ -0,0 +1,230 @@
+# 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.
+# pylint: disable=invalid-name
+"""Structural helper objects and functions."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Any
+
+if TYPE_CHECKING:
+    from .access_path import AccessPath
+
+from . import _ffi_api
+from .core import Object
+from .registry import register_object
+
+__all__ = [
+    "StructuralKey",
+    "get_first_structural_mismatch",
+    "structural_equal",
+    "structural_hash",
+]
+
+
+def structural_equal(
+    lhs: Any, rhs: Any, map_free_vars: bool = False, skip_tensor_content: bool 
= False
+) -> bool:
+    """Check structural equality between two values.
+
+    Structural equality compares the *shape/content structure* of two values
+    instead of Python object identity. For container-like values, this means
+    recursive comparison of elements/fields. For object types that provide
+    structural equal hooks, those hooks are used.
+
+    Parameters
+    ----------
+    lhs
+        Left-hand side value.
+    rhs
+        Right-hand side value.
+
+    map_free_vars
+        Whether free variables (variables without a definition site) can be
+        mapped to each other during comparison.
+
+    skip_tensor_content
+        Whether to skip tensor data content when comparing tensors.
+
+    Returns
+    -------
+    result
+        Whether the two values are structurally equal.
+
+    Examples
+    --------
+    .. code-block:: python
+
+        import tvm_ffi
+
+        assert tvm_ffi.structural_equal([1, 2, 3], [1, 2, 3])
+        assert not tvm_ffi.structural_equal([1, 2, 3], [1, 2, 4])
+
+    See Also
+    --------
+    :py:func:`tvm_ffi.structural_hash`
+        Hash function compatible with structural equality.
+    :py:func:`tvm_ffi.get_first_structural_mismatch`
+        Mismatch diagnostics with access paths.
+
+    """
+    return _ffi_api.StructuralEqual(lhs, rhs, map_free_vars, 
skip_tensor_content)
+
+
+def structural_hash(
+    value: Any, map_free_vars: bool = False, skip_tensor_content: bool = False
+) -> int:
+    """Compute structural hash of a value.
+
+    This hash is designed to be consistent with :py:func:`structural_equal`
+    under the same options. If two values are structurally equal, they should
+    have the same structural hash.
+
+    Parameters
+    ----------
+    value
+        Input value to hash.
+
+    map_free_vars
+        Whether free variables mapped to each other during hashing.
+
+    skip_tensor_content
+        Whether tensor data content is ignored when hashing tensors.
+
+    Returns
+    -------
+    hash_value
+        Structural hash value.
+
+    Examples
+    --------
+    .. code-block:: python
+
+        import tvm_ffi
+
+        h0 = tvm_ffi.structural_hash([1, 2, 3])
+        h1 = tvm_ffi.structural_hash([1, 2, 3])
+        assert h0 == h1
+
+    Notes
+    -----
+    Structural hash is intended for hash-table bucketing and fast pre-checks.
+    Always use structural equality to confirm semantic equivalence.
+
+    """
+    # need to mask the result so negative values are converted to u64
+    # this is because hash value were stored as int64_t in the C++ code
+    return _ffi_api.StructuralHash(value, map_free_vars, skip_tensor_content) 
& 0xFFFFFFFFFFFFFFFF
+
+
+def get_first_structural_mismatch(
+    lhs: Any, rhs: Any, map_free_vars: bool = False, skip_tensor_content: bool 
= False
+) -> tuple[AccessPath, AccessPath] | None:
+    """Like structural_equal(), but returns the AccessPath pair of the first 
detected mismatch.
+
+    Parameters
+    ----------
+    lhs
+        The left operand.
+
+    rhs
+        The right operand.
+
+    map_free_vars
+        Whether free variables (i.e. variables without a definition site) 
should be mapped
+        as equal to each other.
+
+    skip_tensor_content
+        Whether to skip the data content of tensor.
+
+    Returns
+    -------
+    mismatch: tuple[AccessPath, AccessPath] | None
+        `None` if `lhs` and `rhs` are structurally equal.
+        Otherwise, a tuple of two AccessPath objects that point to the first 
detected mismatch.
+
+    """
+    return _ffi_api.GetFirstStructuralMismatch(lhs, rhs, map_free_vars, 
skip_tensor_content)
+
+
+@register_object("ffi.StructuralKey")
+class StructuralKey(Object):
+    """Hash-cached structural key wrapper.
+
+    This wrapper can be used to hint that a dict uses structural equality and 
hash for the key.
+
+    Examples
+    --------
+    Use ``StructuralKey`` with Python dictionaries when you want key lookup by
+    structural semantics:
+
+    .. code-block:: python
+
+        import tvm_ffi
+
+        k0 = tvm_ffi.StructuralKey([1, 2, 3])
+        k1 = tvm_ffi.StructuralKey([1, 2, 3])
+        k2 = tvm_ffi.StructuralKey([1, 2, 4])
+
+        d = {k0: "value-a", k2: "value-b"}
+        assert d[k1] == "value-a"  # k1 matches k0 structurally
+        assert d[k2] == "value-b"
+
+    It can also be used directly with :py:class:`tvm_ffi.Map`:
+
+    .. code-block:: python
+
+        m = tvm_ffi.Map({k0: 1, k1: 2})
+        assert len(m) == 1
+        assert m[k0] == 2
+
+    See Also
+    --------
+    :py:func:`tvm_ffi.structural_equal`
+        Structural equality comparison.
+    :py:func:`tvm_ffi.structural_hash`
+        Structural hash computation.
+
+    """
+
+    # tvm-ffi-stubgen(begin): object/ffi.StructuralKey
+    # fmt: off
+    key: Any
+    hash_i64: int
+    # fmt: on
+    # tvm-ffi-stubgen(end)
+
+    def __init__(self, key: Any) -> None:
+        """Create a structural key from ``key``.
+
+        Parameters
+        ----------
+        key
+            The underlying value used for structural hash/equality.
+
+        """
+        self.__init_handle_by_constructor__(_ffi_api.StructuralKey, key)
+
+    def __hash__(self) -> int:
+        """Return cached structural hash."""
+        # need to mask the result so negative values are converted to u64
+        # this is because hash value were stored as int64_t in the C++ code
+        return self.hash_i64 & 0xFFFFFFFFFFFFFFFF
+
+    def __eq__(self, other: Any) -> bool:
+        """Compare by structural equality."""
+        return isinstance(other, StructuralKey) and 
_ffi_api.StructuralKeyEqual(self, other)
diff --git a/src/ffi/extra/reflection_extra.cc 
b/src/ffi/extra/reflection_extra.cc
index 66994163..20190092 100644
--- a/src/ffi/extra/reflection_extra.cc
+++ b/src/ffi/extra/reflection_extra.cc
@@ -21,6 +21,7 @@
  *
  * \brief Extra reflection registrations. *
  */
+#include <tvm/ffi/extra/structural_key.h>
 #include <tvm/ffi/reflection/access_path.h>
 #include <tvm/ffi/reflection/accessor.h>
 #include <tvm/ffi/reflection/registry.h>
@@ -133,11 +134,46 @@ inline void AccessPathRegisterReflection() {
            [](const AccessPath& self, const AccessPath& other) { return 
self->PathEqual(other); });
 }
 
+int64_t StructuralKeyHash(const Any& key) {
+  const auto* key_obj = key.as<StructuralKeyObj>();
+  TVM_FFI_ICHECK_NOTNULL(key_obj);
+  return key_obj->hash_i64;
+}
+
+bool StructuralKeyEqual(const Any& lhs, const Any& rhs) {
+  if (lhs.same_as(rhs)) {
+    return true;
+  }
+  const auto* lhs_obj = lhs.as<StructuralKeyObj>();
+  const auto* rhs_obj = rhs.as<StructuralKeyObj>();
+  TVM_FFI_ICHECK_NOTNULL(lhs_obj);
+  TVM_FFI_ICHECK_NOTNULL(rhs_obj);
+  if (lhs_obj->hash_i64 != rhs_obj->hash_i64) {
+    return false;
+  }
+  return StructuralEqual::Equal(lhs_obj->key, rhs_obj->key);
+}
+
+inline void StructuralKeyRegisterReflection() {
+  namespace refl = tvm::ffi::reflection;
+  refl::ObjectDef<StructuralKeyObj>()
+      .def_ro("key", &StructuralKeyObj::key)
+      .def_ro("hash_i64", &StructuralKeyObj::hash_i64);
+  refl::TypeAttrDef<StructuralKeyObj>()
+      .attr("__any_hash__", reinterpret_cast<void*>(&StructuralKeyHash))
+      .attr("__any_equal__", reinterpret_cast<void*>(&StructuralKeyEqual));
+}
+
 TVM_FFI_STATIC_INIT_BLOCK() {
   namespace refl = tvm::ffi::reflection;
   AccessStepRegisterReflection();
   AccessPathRegisterReflection();
-  refl::GlobalDef().def_packed("ffi.MakeObjectFromPackedArgs", 
MakeObjectFromPackedArgs);
+  StructuralKeyRegisterReflection();
+
+  refl::GlobalDef()
+      .def_packed("ffi.MakeObjectFromPackedArgs", MakeObjectFromPackedArgs)
+      .def("ffi.StructuralKey", [](Any key) { return 
StructuralKey(std::move(key)); })
+      .def("ffi.StructuralKeyEqual", StructuralKeyEqual);
 }
 
 }  // namespace reflection
diff --git a/src/ffi/extra/structural_equal.cc 
b/src/ffi/extra/structural_equal.cc
index 2fa05f24..3e8cb54a 100644
--- a/src/ffi/extra/structural_equal.cc
+++ b/src/ffi/extra/structural_equal.cc
@@ -454,7 +454,9 @@ Optional<reflection::AccessPathPair> 
StructuralEqual::GetFirstMismatch(const Any
 
 TVM_FFI_STATIC_INIT_BLOCK() {
   namespace refl = tvm::ffi::reflection;
-  refl::GlobalDef().def("ffi.GetFirstStructuralMismatch", 
StructuralEqual::GetFirstMismatch);
+  refl::GlobalDef()
+      .def("ffi.StructuralEqual", StructuralEqual::Equal)
+      .def("ffi.GetFirstStructuralMismatch", 
StructuralEqual::GetFirstMismatch);
   // ensure the type attribute column is presented in the system even if it is 
empty.
   refl::EnsureTypeAttrColumn("__s_equal__");
 }
diff --git a/tests/cpp/extra/test_structural_key.cc 
b/tests/cpp/extra/test_structural_key.cc
new file mode 100644
index 00000000..bc6b646c
--- /dev/null
+++ b/tests/cpp/extra/test_structural_key.cc
@@ -0,0 +1,79 @@
+/*
+ * 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.
+ */
+#include <gtest/gtest.h>
+#include <tvm/ffi/container/array.h>
+#include <tvm/ffi/container/map.h>
+#include <tvm/ffi/extra/structural_hash.h>
+#include <tvm/ffi/extra/structural_key.h>
+
+#include <unordered_map>
+
+namespace {
+
+using namespace tvm::ffi;
+
+TEST(StructuralKey, EqualityAndStdHash) {
+  StructuralKey k1(Array<int>{1, 2, 3});
+  StructuralKey k2(Array<int>{1, 2, 3});
+  StructuralKey k3(Array<int>{1, 2, 4});
+
+  EXPECT_FALSE(k1.same_as(k2));
+  EXPECT_EQ(k1->hash_i64, static_cast<int64_t>(StructuralHash::Hash(k1->key)));
+  EXPECT_EQ(k2->hash_i64, static_cast<int64_t>(StructuralHash::Hash(k2->key)));
+
+  EXPECT_TRUE(k1 == k2);
+  EXPECT_FALSE(k1 != k2);
+  EXPECT_FALSE(k1 == k3);
+  EXPECT_TRUE(k1 != k3);
+  EXPECT_EQ(std::hash<StructuralKey>()(k1), std::hash<StructuralKey>()(k2));
+  EXPECT_NE(std::hash<StructuralKey>()(k1), std::hash<StructuralKey>()(k3));
+
+  std::unordered_map<StructuralKey, int> map;
+  map.emplace(k1, 10);
+  map[k2] = 20;
+  map[k3] = 30;
+  EXPECT_EQ(map.size(), 2U);
+  EXPECT_EQ(map.at(k1), 20);
+  EXPECT_EQ(map.at(k2), 20);
+  EXPECT_EQ(map.at(k3), 30);
+}
+
+TEST(StructuralKey, DefaultCtorInitializesHash) {
+  StructuralKeyObj obj;
+  EXPECT_EQ(obj.hash_i64, 0);
+}
+
+TEST(StructuralKey, MapAnyKeyUsesStructuralAttrs) {
+  Map<Any, Any> map;
+  StructuralKey k1(Array<int>{1, 2, 3});
+  StructuralKey k2(Array<int>{1, 2, 3});
+  StructuralKey k3(Array<int>{1, 3, 5});
+
+  map.Set(k1, 1);
+  map.Set(k2, 2);
+  map.Set(k3, 3);
+
+  // k1 and k2 are structurally equal and should occupy the same map key.
+  EXPECT_EQ(map.size(), 2U);
+  EXPECT_EQ(map.at(k1).cast<int>(), 2);
+  EXPECT_EQ(map.at(k2).cast<int>(), 2);
+  EXPECT_EQ(map.at(k3).cast<int>(), 3);
+}
+
+}  // namespace
diff --git a/tests/python/test_structural.py b/tests/python/test_structural.py
new file mode 100644
index 00000000..df800f7c
--- /dev/null
+++ b/tests/python/test_structural.py
@@ -0,0 +1,92 @@
+# 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.
+
+import numpy as np
+import tvm_ffi
+
+
+def test_structural_key_basic() -> None:
+    k1 = tvm_ffi.StructuralKey({"a": [1, 2], "b": [3, {"c": 4}]})
+    k2 = tvm_ffi.StructuralKey({"b": [3, {"c": 4}], "a": [1, 2]})
+    k3 = tvm_ffi.StructuralKey({"a": [1, 2], "b": [3, {"c": 5}]})
+
+    assert tvm_ffi.structural_hash(k1.key) == k1.__hash__()
+    assert tvm_ffi.structural_hash(k2.key) == k2.__hash__()
+
+    assert k1 == k2
+    assert k1 != k3
+    assert hash(k1) == hash(k2)
+    assert tvm_ffi.structural_equal(k1.key, k2.key)
+    assert not tvm_ffi.structural_equal(k1.key, k3.key)
+
+
+def test_structural_helpers() -> None:
+    lhs = {"items": [1, 2, {"k": 3}], "meta": {"tag": "x"}}
+    rhs = {"meta": {"tag": "x"}, "items": [1, 2, {"k": 3}]}
+    other = {"items": [1, 2, {"k": 4}], "meta": {"tag": "x"}}
+
+    assert tvm_ffi.structural_equal(lhs, rhs)
+    assert not tvm_ffi.structural_equal(lhs, other)
+    assert tvm_ffi.structural_hash(lhs) == tvm_ffi.structural_hash(rhs)
+    assert tvm_ffi.structural_hash(lhs) != tvm_ffi.structural_hash(other)
+    assert tvm_ffi.get_first_structural_mismatch(lhs, rhs) is None
+    assert tvm_ffi.get_first_structural_mismatch(lhs, other) is not None
+
+
+def test_structural_key_in_map() -> None:
+    k1 = tvm_ffi.StructuralKey({"x": [1, 2], "y": [3]})
+    k2 = tvm_ffi.StructuralKey({"y": [3], "x": [1, 2]})
+    k3 = tvm_ffi.StructuralKey({"x": [1, 2], "y": [5]})
+
+    m = tvm_ffi.Map({k1: 1, k2: 2, k3: 3})
+    assert len(m) == 2
+    assert m[k1] == 2
+    assert m[k2] == 2
+    assert m[k3] == 3
+
+
+def test_structural_key_in_python_dict() -> None:
+    k1 = tvm_ffi.StructuralKey({"name": ["a", "b"], "ver": [1]})
+    k2 = tvm_ffi.StructuralKey({"ver": [1], "name": ["a", "b"]})
+    k3 = tvm_ffi.StructuralKey({"name": ["a", "c"], "ver": [1]})
+
+    data = {k1: "a", k3: "b"}
+    assert data[k2] == "a"
+    assert data[k3] == "b"
+
+
+def test_structural_key_tensor_content_policy() -> None:
+    t1_np = np.array([1.0, 2.0, 3.0], dtype="float32")
+    t2_np = np.array([1.0, 2.0, 4.0], dtype="float32")
+    if not hasattr(t1_np, "__dlpack__"):
+        return
+
+    t1 = tvm_ffi.from_dlpack(t1_np)
+    t2 = tvm_ffi.from_dlpack(t2_np)
+
+    # Default policy compares tensor content.
+    assert not tvm_ffi.structural_equal(t1, t2)
+    # Optional policy can ignore tensor content.
+    assert tvm_ffi.structural_equal(t1, t2, skip_tensor_content=True)
+
+    # StructuralKey should follow default structural policy.
+    k1 = tvm_ffi.StructuralKey(t1)
+    k2 = tvm_ffi.StructuralKey(t2)
+    assert k1 != k2
+
+    data = {k1: "a", k2: "b"}
+    assert len(data) == 2

Reply via email to