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 b648c5d6 feat: add DFS-based ffi.ReprPrint for unified object repr 
(#454)
b648c5d6 is described below

commit b648c5d6b7981350c165096efbb98d00787e2900
Author: Junru Shao <[email protected]>
AuthorDate: Wed Feb 18 10:31:07 2026 -0800

    feat: add DFS-based ffi.ReprPrint for unified object repr (#454)
    
    ## Summary
    
    - Single C++ `ffi.ReprPrint` function produces human-readable repr for
    any TVM FFI value
    - DFS with 3-state tracking (NotVisited/InProgress/Done):
      - **DAGs**: memoized repr returned in full on every re-encounter
      - **Cycles**: detected via InProgress state, shown as `...`
    - Addresses hidden by default; set `TVM_FFI_REPR_WITH_ADDR=1` to show
    - Per-field `Repr(false)` InfoTrait to exclude fields from repr output
    - Built-in repr for String, Bytes, Tensor, Shape, Array, List, Map
    - All Python `__repr__` methods delegate to this function
    
    ## Format Examples
    
    ```
    42                                    # int
    "hello"                               # String
    (1, 2, 3)                             # Array
    [1, 2, 3]                             # List
    {"key": "value"}                      # Map
    testing.MyObj(x=1, y="hi")            # User object
    ...                                   # Cycle marker
    float32[3, 4]@cpu:0@0x1234            # Tensor
    ```
    
    ## Test plan
    
    - [x] 55 Python tests covering primitives, containers, user objects,
    DAGs, cycles, and `TVM_FFI_REPR_WITH_ADDR`
    - [x] All pre-commit hooks pass (ruff, ty, clang-format, markdownlint,
    etc.)
    - [x] Container tests (`test_container.py`) pass with updated Array
    format
---
 CMakeLists.txt                           |   1 +
 include/tvm/ffi/c_api.h                  |   7 +
 include/tvm/ffi/reflection/registry.h    |  28 ++
 python/tvm_ffi/_ffi_api.py               |   2 +
 python/tvm_ffi/container.py              |   6 +-
 python/tvm_ffi/cython/object.pxi         |  16 +-
 python/tvm_ffi/dataclasses/_utils.py     |  40 ---
 python/tvm_ffi/dataclasses/c_class.py    |  12 +-
 python/tvm_ffi/dataclasses/field.py      |  12 +-
 src/ffi/extra/repr_print.cc              | 390 +++++++++++++++++++++
 src/ffi/testing/testing.cc               |   4 +-
 tests/python/test_container.py           |   6 +-
 tests/python/test_dataclasses_c_class.py |  35 --
 tests/python/test_repr.py                | 581 +++++++++++++++++++++++++++++++
 14 files changed, 1037 insertions(+), 103 deletions(-)

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 16c41ded..d66dbadc 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -78,6 +78,7 @@ set(_tvm_ffi_extra_objs_sources
     "${CMAKE_CURRENT_SOURCE_DIR}/src/ffi/extra/json_writer.cc"
     "${CMAKE_CURRENT_SOURCE_DIR}/src/ffi/extra/serialization.cc"
     "${CMAKE_CURRENT_SOURCE_DIR}/src/ffi/extra/deep_copy.cc"
+    "${CMAKE_CURRENT_SOURCE_DIR}/src/ffi/extra/repr_print.cc"
     "${CMAKE_CURRENT_SOURCE_DIR}/src/ffi/extra/reflection_extra.cc"
     "${CMAKE_CURRENT_SOURCE_DIR}/src/ffi/extra/module.cc"
     "${CMAKE_CURRENT_SOURCE_DIR}/src/ffi/extra/library_module.cc"
diff --git a/include/tvm/ffi/c_api.h b/include/tvm/ffi/c_api.h
index 236e342f..a48641be 100644
--- a/include/tvm/ffi/c_api.h
+++ b/include/tvm/ffi/c_api.h
@@ -870,6 +870,13 @@ typedef enum {
    * being used directly as the default value.
    */
   kTVMFFIFieldFlagBitMaskDefaultFromFactory = 1 << 5,
+  /*!
+   * \brief The field is excluded from repr output.
+   *
+   * When set, the field will not appear in the generic reflection-based repr.
+   * By default this flag is off (meaning the field is included in repr).
+   */
+  kTVMFFIFieldFlagBitMaskReprOff = 1 << 6,
 #ifdef __cplusplus
 };
 #else
diff --git a/include/tvm/ffi/reflection/registry.h 
b/include/tvm/ffi/reflection/registry.h
index 4cd46633..f75b24a8 100644
--- a/include/tvm/ffi/reflection/registry.h
+++ b/include/tvm/ffi/reflection/registry.h
@@ -223,6 +223,33 @@ class AttachFieldFlag : public InfoTrait {
   int32_t flag_;
 };
 
+/*!
+ * \brief Trait that controls whether a field appears in repr output.
+ *
+ * By default, all fields appear in repr. Use `Repr(false)` to exclude a field.
+ */
+class Repr : public InfoTrait {
+ public:
+  /*!
+   * \brief Constructor.
+   * \param show Whether the field should appear in repr output.
+   */
+  explicit Repr(bool show) : show_(show) {}
+
+  /*!
+   * \brief Apply the repr flag to the field info.
+   * \param info The field info.
+   */
+  TVM_FFI_INLINE void Apply(TVMFFIFieldInfo* info) const {
+    if (!show_) {
+      info->flags |= kTVMFFIFieldFlagBitMaskReprOff;
+    }
+  }
+
+ private:
+  bool show_;
+};
+
 /*!
  * \brief Get the byte offset of a class member field.
  *
@@ -493,6 +520,7 @@ struct init {
 namespace type_attr {
 inline constexpr const char* kInit = "__ffi_init__";
 inline constexpr const char* kShallowCopy = "__ffi_shallow_copy__";
+inline constexpr const char* kRepr = "__ffi_repr__";
 }  // namespace type_attr
 
 /*!
diff --git a/python/tvm_ffi/_ffi_api.py b/python/tvm_ffi/_ffi_api.py
index bf77b76a..d20e5fb1 100644
--- a/python/tvm_ffi/_ffi_api.py
+++ b/python/tvm_ffi/_ffi_api.py
@@ -82,6 +82,7 @@ if TYPE_CHECKING:
     def ModuleInspectSource(_0: Module, _1: str, /) -> str: ...
     def ModuleLoadFromFile(_0: str, /) -> Module: ...
     def ModuleWriteToFile(_0: Module, _1: str, _2: str, /) -> None: ...
+    def ReprPrint(_0: Any, /) -> str: ...
     def Shape(*args: Any) -> Any: ...
     def String(_0: str, /) -> str: ...
     def StructuralKey(_0: Any, /) -> Any: ...
@@ -144,6 +145,7 @@ __all__ = [
     "ModuleInspectSource",
     "ModuleLoadFromFile",
     "ModuleWriteToFile",
+    "ReprPrint",
     "Shape",
     "String",
     "StructuralEqual",
diff --git a/python/tvm_ffi/container.py b/python/tvm_ffi/container.py
index 2a358d28..009c4813 100644
--- a/python/tvm_ffi/container.py
+++ b/python/tvm_ffi/container.py
@@ -197,7 +197,7 @@ class Array(core.Object, Sequence[T]):
         # exception safety handling for chandle=None
         if self.__chandle__() == 0:
             return type(self).__name__ + "(chandle=None)"
-        return "[" + ", ".join([x.__repr__() for x in self]) + "]"
+        return str(core.__object_repr__(self))  # ty: 
ignore[unresolved-attribute]
 
     def __contains__(self, value: object) -> bool:
         """Check if the array contains a value."""
@@ -343,7 +343,7 @@ class List(core.Object, MutableSequence[T]):
         """Return a string representation of the list."""
         if self.__chandle__() == 0:
             return type(self).__name__ + "(chandle=None)"
-        return "[" + ", ".join([x.__repr__() for x in self]) + "]"
+        return str(core.__object_repr__(self))  # ty: 
ignore[unresolved-attribute]
 
     def __contains__(self, value: object) -> bool:
         """Check if the list contains a value."""
@@ -543,4 +543,4 @@ class Map(core.Object, Mapping[K, V]):
         # exception safety handling for chandle=None
         if self.__chandle__() == 0:
             return type(self).__name__ + "(chandle=None)"
-        return "{" + ", ".join([f"{k.__repr__()}: {v.__repr__()}" for k, v in 
self.items()]) + "}"
+        return str(core.__object_repr__(self))  # ty: 
ignore[unresolved-attribute]
diff --git a/python/tvm_ffi/cython/object.pxi b/python/tvm_ffi/cython/object.pxi
index c7a9a47d..c20fe1bf 100644
--- a/python/tvm_ffi/cython/object.pxi
+++ b/python/tvm_ffi/cython/object.pxi
@@ -26,8 +26,22 @@ def _set_class_object(cls):
     _CLASS_OBJECT = cls
 
 
+_REPR_PRINT = None
+_REPR_PRINT_LOADED = False
+
+
 def __object_repr__(obj: "Object") -> str:
-    """Object repr function that can be overridden by assigning to it"""
+    """Object repr function using ffi.ReprPrint when available."""
+    global _REPR_PRINT, _REPR_PRINT_LOADED
+    if not _REPR_PRINT_LOADED:
+        _REPR_PRINT_LOADED = True
+        _REPR_PRINT = _get_global_func("ffi.ReprPrint", False)
+    if _REPR_PRINT is not None:
+        try:
+            return str(_REPR_PRINT(obj))
+        except Exception:  # noqa: BLE001
+            # Silently fall back: __repr__ must never raise.
+            pass
     return type(obj).__name__ + "(" + str(obj.__ctypes_handle__().value) + ")"
 
 
diff --git a/python/tvm_ffi/dataclasses/_utils.py 
b/python/tvm_ffi/dataclasses/_utils.py
index 812a4194..80c39874 100644
--- a/python/tvm_ffi/dataclasses/_utils.py
+++ b/python/tvm_ffi/dataclasses/_utils.py
@@ -127,46 +127,6 @@ def _get_all_fields(type_info: TypeInfo) -> 
list[TypeField]:
     return fields
 
 
-def method_repr(type_cls: type, type_info: TypeInfo) -> Callable[..., str]:
-    """Generate a ``__repr__`` method for the dataclass.
-
-    The generated representation includes all fields with ``repr=True`` in
-    the format ``ClassName(field1=value1, field2=value2, ...)``.
-    """
-    # Step 0. Collect all fields from the type hierarchy
-    fields = _get_all_fields(type_info)
-
-    # Step 1. Filter fields that should appear in repr
-    repr_fields: list[str] = []
-    for field in fields:
-        assert field.name is not None
-        assert field.dataclass_field is not None
-        if field.dataclass_field.repr:
-            repr_fields.append(field.name)
-
-    # Step 2. Generate the repr method
-    if not repr_fields:
-        # No fields to show, return a simple class name representation
-        body_lines = [f"return f'{type_cls.__name__}()'"]
-    else:
-        # Build field representations
-        fields_str = ", ".join(
-            f"{field_name}={{self.{field_name}!r}}" for field_name in 
repr_fields
-        )
-        body_lines = [f"return f'{type_cls.__name__}({fields_str})'"]
-
-    source_lines = ["def __repr__(self) -> str:"]
-    source_lines.extend(f"    {line}" for line in body_lines)
-    source = "\n".join(source_lines)
-
-    # Note: Code generation in this case is guaranteed to be safe,
-    # because the generated code does not contain any untrusted input.
-    namespace: dict[str, Any] = {}
-    exec(source, {}, namespace)
-    __repr__ = namespace["__repr__"]
-    return __repr__
-
-
 def method_init(_type_cls: type, type_info: TypeInfo) -> Callable[..., None]:
     """Generate an ``__init__`` that forwards to the FFI constructor.
 
diff --git a/python/tvm_ffi/dataclasses/c_class.py 
b/python/tvm_ffi/dataclasses/c_class.py
index 60e3ad4f..295918a9 100644
--- a/python/tvm_ffi/dataclasses/c_class.py
+++ b/python/tvm_ffi/dataclasses/c_class.py
@@ -41,7 +41,7 @@ _InputClsType = TypeVar("_InputClsType")
 
 @dataclass_transform(field_specifiers=(field,), kw_only_default=False)
 def c_class(
-    type_key: str, init: bool = True, kw_only: bool = False, repr: bool = True
+    type_key: str, init: bool = True, kw_only: bool = False
 ) -> Callable[[Type[_InputClsType]], Type[_InputClsType]]:  # noqa: UP006
     """(Experimental) Create a dataclass-like proxy for a C++ class registered 
with TVM FFI.
 
@@ -77,10 +77,6 @@ def c_class(
         ``__init__``. Individual fields can override this by setting
         ``kw_only=False`` in :func:`field`. Additionally, a ``KW_ONLY`` 
sentinel
         annotation can be used to mark all subsequent fields as keyword-only.
-    repr
-        If ``True`` and the Python class does not define ``__repr__``, a
-        representation method is auto-generated that includes all fields with
-        ``repr=True``.
 
     Returns
     -------
@@ -128,9 +124,8 @@ def c_class(
     """
 
     def decorator(super_type_cls: Type[_InputClsType]) -> Type[_InputClsType]: 
 # noqa: UP006
-        nonlocal init, repr
+        nonlocal init
         init = init and "__init__" not in super_type_cls.__dict__
-        repr = repr and "__repr__" not in super_type_cls.__dict__
         # Step 1. Retrieve `type_info` from registry
         type_info: TypeInfo = 
_lookup_or_register_type_info_from_type_key(type_key)
         assert type_info.parent_type_info is not None
@@ -146,11 +141,10 @@ def c_class(
             )
         # Step 3. Create the proxy class with the fields as properties
         fn_init = _utils.method_init(super_type_cls, type_info) if init else 
None
-        fn_repr = _utils.method_repr(super_type_cls, type_info) if repr else 
None
         type_cls: Type[_InputClsType] = _utils.type_info_to_cls(  # noqa: UP006
             type_info=type_info,
             cls=super_type_cls,
-            methods={"__init__": fn_init, "__repr__": fn_repr},
+            methods={"__init__": fn_init},
         )
         _set_type_cls(type_info, type_cls)
         # Step 4. Set up __copy__, __deepcopy__, __replace__
diff --git a/python/tvm_ffi/dataclasses/field.py 
b/python/tvm_ffi/dataclasses/field.py
index a03642f6..2378da87 100644
--- a/python/tvm_ffi/dataclasses/field.py
+++ b/python/tvm_ffi/dataclasses/field.py
@@ -47,7 +47,7 @@ class Field:
     way the decorator understands.
     """
 
-    __slots__ = ("default_factory", "init", "kw_only", "name", "repr")
+    __slots__ = ("default_factory", "init", "kw_only", "name")
 
     def __init__(
         self,
@@ -55,14 +55,12 @@ class Field:
         name: str | None = None,
         default_factory: Callable[[], _FieldValue] | _MISSING_TYPE = MISSING,
         init: bool = True,
-        repr: bool = True,
         kw_only: bool | _MISSING_TYPE = MISSING,
     ) -> None:
         """Do not call directly; use :func:`field` instead."""
         self.name = name
         self.default_factory = default_factory
         self.init = init
-        self.repr = repr
         self.kw_only = kw_only
 
 
@@ -71,7 +69,6 @@ def field(
     default: _FieldValue | _MISSING_TYPE = MISSING,
     default_factory: Callable[[], _FieldValue] | _MISSING_TYPE = MISSING,
     init: bool = True,
-    repr: bool = True,
     kw_only: bool | _MISSING_TYPE = MISSING,
 ) -> _FieldValue:
     """(Experimental) Declare a dataclass-style field on a :func:`c_class` 
proxy.
@@ -94,9 +91,6 @@ def field(
     init
         If ``True`` the field is included in the generated ``__init__``.
         If ``False`` the field is omitted from input arguments of ``__init__``.
-    repr
-        If ``True`` the field is included in the generated ``__repr__``.
-        If ``False`` the field is omitted from the ``__repr__`` output.
     kw_only
         If ``True``, the field is a keyword-only argument in ``__init__``.
         If ``MISSING``, inherits from the class-level ``kw_only`` setting or
@@ -158,13 +152,11 @@ def field(
         raise ValueError("Cannot specify both `default` and `default_factory`")
     if not isinstance(init, bool):
         raise TypeError("`init` must be a bool")
-    if not isinstance(repr, bool):
-        raise TypeError("`repr` must be a bool")
     if kw_only is not MISSING and not isinstance(kw_only, bool):
         raise TypeError(f"`kw_only` must be a bool, got 
{type(kw_only).__name__!r}")
     if default is not MISSING:
         default_factory = _make_default_factory(default)
-    ret = Field(default_factory=default_factory, init=init, repr=repr, 
kw_only=kw_only)
+    ret = Field(default_factory=default_factory, init=init, kw_only=kw_only)
     return cast(_FieldValue, ret)
 
 
diff --git a/src/ffi/extra/repr_print.cc b/src/ffi/extra/repr_print.cc
new file mode 100644
index 00000000..f7aecd5f
--- /dev/null
+++ b/src/ffi/extra/repr_print.cc
@@ -0,0 +1,390 @@
+/*
+ * 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 src/ffi/extra/repr_print.cc
+ *
+ * \brief Reflection-based repr printing with DFS-based cycle/DAG handling.
+ */
+#include <tvm/ffi/any.h>
+#include <tvm/ffi/container/array.h>
+#include <tvm/ffi/container/list.h>
+#include <tvm/ffi/container/map.h>
+#include <tvm/ffi/container/shape.h>
+#include <tvm/ffi/container/tensor.h>
+#include <tvm/ffi/dtype.h>
+#include <tvm/ffi/error.h>
+#include <tvm/ffi/reflection/accessor.h>
+#include <tvm/ffi/reflection/registry.h>
+
+#include <cstdlib>
+#include <iomanip>
+#include <sstream>
+#include <string>
+#include <unordered_map>
+
+namespace tvm {
+namespace ffi {
+
+namespace {
+
+/*!
+ * \brief Convert a DLDeviceType to a short name string.
+ */
+const char* DeviceTypeName(int device_type) {
+  switch (device_type) {
+    case kDLCPU:
+      return "cpu";
+    case kDLCUDA:
+      return "cuda";
+    case kDLCUDAHost:
+      return "cuda_host";
+    case kDLOpenCL:
+      return "opencl";
+    case kDLVulkan:
+      return "vulkan";
+    case kDLMetal:
+      return "metal";
+    case kDLVPI:
+      return "vpi";
+    case kDLROCM:
+      return "rocm";
+    case kDLROCMHost:
+      return "rocm_host";
+    case kDLExtDev:
+      return "ext_dev";
+    case kDLCUDAManaged:
+      return "cuda_managed";
+    case kDLOneAPI:
+      return "oneapi";
+    case kDLWebGPU:
+      return "webgpu";
+    case kDLHexagon:
+      return "hexagon";
+    default:
+      return "unknown";
+  }
+}
+
+/*!
+ * \brief Format a DLDevice as "device_name:device_id".
+ */
+std::string DeviceToString(DLDevice device) {
+  std::ostringstream os;
+  os << DeviceTypeName(device.device_type) << ":" << device.device_id;
+  return os.str();
+}
+
+/*!
+ * \brief Format raw bytes as a Python-style bytes literal: b"...".
+ */
+std::string FormatBytes(const char* data, size_t size) {
+  std::ostringstream os;
+  os << "b\"";
+  for (size_t i = 0; i < size; ++i) {
+    unsigned char c = static_cast<unsigned char>(data[i]);
+    if (c >= 32 && c < 127 && c != '\"' && c != '\\') {
+      os << static_cast<char>(c);
+    } else {
+      os << "\\x" << std::hex << std::setw(2) << std::setfill('0') << 
static_cast<int>(c);
+    }
+  }
+  os << "\"";
+  return os.str();
+}
+
+/*!
+ * \brief Format an object address as a hex string.
+ */
+std::string AddressStr(const Object* obj) {
+  std::ostringstream os;
+  os << "0x" << std::hex << reinterpret_cast<uintptr_t>(obj);
+  return os.str();
+}
+
+/*!
+ * \brief Get the type key of an object as a std::string.
+ */
+std::string GetTypeKeyStr(const Object* obj) {
+  const TVMFFITypeInfo* type_info = TVMFFIGetTypeInfo(obj->type_index());
+  return std::string(type_info->type_key.data, type_info->type_key.size);
+}
+
+/*!
+ * \brief DFS-based repr printer.
+ *
+ * Algorithm:
+ *   1. Start from the root value and recursively process via DFS.
+ *   2. Track each object's state: NotVisited, InProgress, Done.
+ *   3. On "Done" objects, return the cached repr string (handles DAGs).
+ *   4. On "InProgress" objects, a cycle is detected — return "..." marker.
+ *   5. On first visit, mark InProgress, process children, cache result, mark 
Done.
+ *
+ * Address display is controlled by the TVM_FFI_REPR_WITH_ADDR environment 
variable.
+ */
+class ReprPrinter {
+ public:
+  String Run(const Any& value) {
+    const char* env = std::getenv("TVM_FFI_REPR_WITH_ADDR");
+    show_addr_ = env != nullptr && std::string_view(env) == "1";
+    return String(ReprOfAny(value));
+  }
+
+ private:
+  enum class State : int8_t { kNotVisited = 0, kInProgress = 1, kDone = 2 };
+
+  // ---------- Core DFS ----------
+
+  std::string ReprOfAny(const Any& value) {
+    int32_t ti = value.type_index();
+    switch (ti) {
+      case TypeIndex::kTVMFFINone:
+        return "None";
+      case TypeIndex::kTVMFFIBool:
+        return value.cast<bool>() ? "True" : "False";
+      case TypeIndex::kTVMFFIInt:
+        return std::to_string(value.cast<int64_t>());
+      case TypeIndex::kTVMFFIFloat: {
+        std::ostringstream os;
+        os << value.cast<double>();
+        return os.str();
+      }
+      case TypeIndex::kTVMFFIDataType: {
+        String s = DLDataTypeToString(value.cast<DLDataType>());
+        return std::string(s.data(), s.size());
+      }
+      case TypeIndex::kTVMFFIDevice: {
+        return DeviceToString(value.cast<DLDevice>());
+      }
+      default:
+        break;
+    }
+    if (ti == TypeIndex::kTVMFFISmallStr) {
+      String s = value.cast<String>();
+      return "\"" + std::string(s.data(), s.size()) + "\"";
+    }
+    if (ti == TypeIndex::kTVMFFISmallBytes) {
+      Bytes b = value.cast<Bytes>();
+      return FormatBytes(b.data(), b.size());
+    }
+    if (ti < TypeIndex::kTVMFFIStaticObjectBegin) {
+      // Other POD types
+      return value.GetTypeKey();
+    }
+    // Object type — use DFS with state tracking
+    const Object* obj = static_cast<const Object*>(value.as<Object>());
+    if (obj == nullptr) return "None";
+    auto it = state_.find(obj);
+    if (it != state_.end()) {
+      if (it->second == State::kDone) {
+        // DAG: already fully processed, return cached repr
+        return repr_cache_[obj];
+      }
+      if (it->second == State::kInProgress) {
+        // Cycle detected
+        return show_addr_ ? ("...@" + AddressStr(obj)) : "...";
+      }
+    }
+    // First visit: mark in-progress, process, cache, mark done
+    state_[obj] = State::kInProgress;
+    std::string repr = ProcessObject(obj);
+    repr_cache_[obj] = repr;
+    state_[obj] = State::kDone;
+    return repr;
+  }
+
+  // ---------- Processing ----------
+
+  std::string ProcessObject(const Object* obj) {
+    int32_t ti = obj->type_index();
+    static reflection::TypeAttrColumn 
repr_column(reflection::type_attr::kRepr);
+    AnyView custom_repr = repr_column[ti];
+    std::string result;
+    if (custom_repr != nullptr) {
+      // Custom __ffi_repr__: call it with fn_repr callback
+      Function repr_fn = custom_repr.cast<Function>();
+      Function fn_repr = CreateFnRepr();
+      String r = repr_fn(obj, fn_repr).cast<String>();
+      result = std::string(r.data(), r.size());
+    } else {
+      // Generic reflection-based repr
+      result = GenericRepr(obj);
+    }
+    // For Array/List: append address if env var is set
+    if (show_addr_ && (ti == TypeIndex::kTVMFFIArray || ti == 
TypeIndex::kTVMFFIList ||
+                       ti == TypeIndex::kTVMFFITensor)) {
+      result += "@" + AddressStr(obj);
+    }
+    return result;
+  }
+
+  Function CreateFnRepr() {
+    return Function::FromTyped(
+        [this](AnyView value) -> String { return 
String(ReprOfAny(Any(value))); });
+  }
+
+  // ---------- Generic Repr ----------
+
+  std::string GenericRepr(const Object* obj) {
+    const TVMFFITypeInfo* type_info = TVMFFIGetTypeInfo(obj->type_index());
+    std::string type_key = (type_info != nullptr)
+                               ? std::string(type_info->type_key.data, 
type_info->type_key.size)
+                               : GetTypeKeyStr(obj);
+    std::string header = show_addr_ ? (type_key + "@" + AddressStr(obj)) : 
type_key;
+    if (type_info == nullptr) return header;
+
+    std::ostringstream fields;
+    bool first = true;
+    bool has_fields = false;
+    reflection::ForEachFieldInfo(type_info, [&](const TVMFFIFieldInfo* finfo) {
+      if (finfo->flags & kTVMFFIFieldFlagBitMaskReprOff) return;
+      has_fields = true;
+      if (!first) fields << ", ";
+      first = false;
+      fields << std::string_view(finfo->name.data, finfo->name.size) << "=";
+      reflection::FieldGetter getter(finfo);
+      Any fv = getter(obj);
+      fields << ReprOfAny(fv);
+    });
+    if (!has_fields) return header;
+    return header + "(" + fields.str() + ")";
+  }
+
+  // ---------- Data members ----------
+  std::unordered_map<const Object*, State> state_;
+  std::unordered_map<const Object*, std::string> repr_cache_;
+  bool show_addr_ = false;
+};
+
+// ---------- Built-in __ffi_repr__ functions ----------
+
+String ReprString(const details::StringObj* obj, const Function& fn_repr) {
+  std::ostringstream os;
+  os << "\"" << std::string_view(obj->data, obj->size) << "\"";
+  return String(os.str());
+}
+
+String ReprBytes(const details::BytesObj* obj, const Function& fn_repr) {
+  return String(FormatBytes(obj->data, obj->size));
+}
+
+String ReprTensor(const TensorObj* obj, const Function& fn_repr) {
+  std::ostringstream os;
+  os << DLDataTypeToString(obj->dtype);
+  os << "[";
+  for (int i = 0; i < obj->ndim; ++i) {
+    if (i > 0) os << ", ";
+    os << obj->shape[i];
+  }
+  os << "]@" << DeviceToString(obj->device);
+  return String(os.str());
+}
+
+String ReprShape(const ShapeObj* obj, const Function& fn_repr) {
+  std::ostringstream os;
+  os << "Shape(";
+  for (size_t i = 0; i < obj->size; ++i) {
+    if (i > 0) os << ", ";
+    os << obj->data[i];
+  }
+  os << ")";
+  return String(os.str());
+}
+
+String ReprArray(const ArrayObj* obj, const Function& fn_repr) {
+  std::ostringstream os;
+  os << "(";
+  size_t count = 0;
+  for (const Any& elem : *obj) {
+    if (count > 0) os << ", ";
+    String s = fn_repr(elem).cast<String>();
+    os << std::string_view(s.data(), s.size());
+    ++count;
+  }
+  if (count == 1) os << ",";
+  os << ")";
+  return String(os.str());
+}
+
+String ReprList(const ListObj* obj, const Function& fn_repr) {
+  std::ostringstream os;
+  os << "[";
+  bool first = true;
+  for (const Any& elem : *obj) {
+    if (!first) os << ", ";
+    first = false;
+    String s = fn_repr(elem).cast<String>();
+    os << std::string_view(s.data(), s.size());
+  }
+  os << "]";
+  return String(os.str());
+}
+
+String ReprMap(const MapObj* obj, const Function& fn_repr) {
+  std::ostringstream os;
+  os << "{";
+  bool first = true;
+  for (const auto& [k, v] : *obj) {
+    if (!first) os << ", ";
+    first = false;
+    String ks = fn_repr(k).cast<String>();
+    String vs = fn_repr(v).cast<String>();
+    os << std::string_view(ks.data(), ks.size()) << ": " << 
std::string_view(vs.data(), vs.size());
+  }
+  os << "}";
+  return String(os.str());
+}
+
+/*!
+ * \brief Register a built-in __ffi_repr__ function for a given type index.
+ */
+template <typename Func>
+void RegisterBuiltinRepr(int32_t type_index, Func&& func) {
+  TVMFFIByteArray attr_name = {reflection::type_attr::kRepr,
+                               
std::char_traits<char>::length(reflection::type_attr::kRepr)};
+  Function ffi_func = Function::FromTyped(std::forward<Func>(func), 
std::string("__ffi_repr__"));
+  TVMFFIAny attr_value = AnyView(ffi_func).CopyToTVMFFIAny();
+  TVM_FFI_CHECK_SAFE_CALL(TVMFFITypeRegisterAttr(type_index, &attr_name, 
&attr_value));
+}
+
+}  // namespace
+
+String ReprPrint(const Any& value) {
+  ReprPrinter printer;
+  return printer.Run(value);
+}
+
+TVM_FFI_STATIC_INIT_BLOCK() {
+  namespace refl = tvm::ffi::reflection;
+  // Ensure type attribute columns exist
+  refl::EnsureTypeAttrColumn(refl::type_attr::kRepr);
+  // Register built-in repr functions
+  RegisterBuiltinRepr(TypeIndex::kTVMFFIStr, ReprString);
+  RegisterBuiltinRepr(TypeIndex::kTVMFFIBytes, ReprBytes);
+  RegisterBuiltinRepr(TypeIndex::kTVMFFITensor, ReprTensor);
+  RegisterBuiltinRepr(TypeIndex::kTVMFFIShape, ReprShape);
+  RegisterBuiltinRepr(TypeIndex::kTVMFFIArray, ReprArray);
+  RegisterBuiltinRepr(TypeIndex::kTVMFFIList, ReprList);
+  RegisterBuiltinRepr(TypeIndex::kTVMFFIMap, ReprMap);
+  // Register global function
+  refl::GlobalDef().def("ffi.ReprPrint",
+                        [](const Any& value) -> String { return 
ReprPrint(value); });
+}
+
+}  // namespace ffi
+}  // namespace tvm
diff --git a/src/ffi/testing/testing.cc b/src/ffi/testing/testing.cc
index 3a0f488c..b9b70bc5 100644
--- a/src/ffi/testing/testing.cc
+++ b/src/ffi/testing/testing.cc
@@ -244,8 +244,8 @@ TVM_FFI_STATIC_INIT_BLOCK() {
 
   refl::ObjectDef<TestCxxClassBase>()
       .def(refl::init<int64_t, int32_t>())
-      .def_rw("v_i64", &TestCxxClassBase::v_i64)
-      .def_rw("v_i32", &TestCxxClassBase::v_i32);
+      .def_rw("v_i64", &TestCxxClassBase::v_i64, refl::Repr(false))
+      .def_rw("v_i32", &TestCxxClassBase::v_i32, refl::Repr(false));
 
   refl::ObjectDef<TestCxxClassDerived>()
       .def(refl::init<int64_t, int32_t, double, float>())
diff --git a/tests/python/test_container.py b/tests/python/test_container.py
index bb6384eb..64468f41 100644
--- a/tests/python/test_container.py
+++ b/tests/python/test_container.py
@@ -130,18 +130,18 @@ def test_key_not_found() -> None:
 
 def test_repr() -> None:
     a = tvm_ffi.convert([1, 2, 3])
-    assert str(a) == "[1, 2, 3]"
+    assert str(a) == "(1, 2, 3)"
     amap = tvm_ffi.convert({3: 2, 4: 3})
     assert str(amap) == "{3: 2, 4: 3}"
 
     smap = tvm_ffi.convert({"a": 1, "b": 2})
-    assert str(smap) == "{'a': 1, 'b': 2}"
+    assert str(smap) == '{"a": 1, "b": 2}'
 
 
 def test_serialization() -> None:
     a = tvm_ffi.convert([1, 2, 3])
     b = pickle.loads(pickle.dumps(a))
-    assert str(b) == "[1, 2, 3]"
+    assert str(b) == "(1, 2, 3)"
 
 
 @pytest.mark.parametrize(
diff --git a/tests/python/test_dataclasses_c_class.py 
b/tests/python/test_dataclasses_c_class.py
index e3735e47..ff2df3c9 100644
--- a/tests/python/test_dataclasses_c_class.py
+++ b/tests/python/test_dataclasses_c_class.py
@@ -101,41 +101,6 @@ def test_cxx_class_init_subset_positional() -> None:
     assert obj.optional_field == 11
 
 
-def test_cxx_class_repr() -> None:
-    obj = _TestCxxClassDerived(v_i64=123, v_i32=456, v_f64=4.0, v_f32=8.0)
-    repr_str = repr(obj)
-    assert "_TestCxxClassDerived" in repr_str
-    if "__repr__" in _TestCxxClassDerived.__dict__:
-        assert "v_i64=123" in repr_str
-        assert "v_i32=456" in repr_str
-        assert "v_f64=4.0" in repr_str
-        assert "v_f32=8.0" in repr_str
-
-
-def test_cxx_class_repr_default() -> None:
-    obj = _TestCxxClassDerived(v_i64=123, v_i32=456, v_f64=4.0)
-    repr_str = repr(obj)
-    assert "_TestCxxClassDerived" in repr_str
-    if "__repr__" in _TestCxxClassDerived.__dict__:
-        assert "v_i64=123" in repr_str
-        assert "v_i32=456" in repr_str
-        assert "v_f64=4.0" in repr_str
-        assert "v_f32=8.0" in repr_str
-
-
-def test_cxx_class_repr_derived_derived() -> None:
-    obj = _TestCxxClassDerivedDerived(
-        v_i64=123, v_i32=456, v_f64=4.0, v_f32=8.0, v_str="hello", v_bool=True
-    )
-    repr_str = repr(obj)
-    assert "_TestCxxClassDerivedDerived" in repr_str
-    if "__repr__" in _TestCxxClassDerivedDerived.__dict__:
-        assert "v_i64=123" in repr_str
-        assert "v_i32=456" in repr_str
-        assert "v_str='hello'" in repr_str or 'v_str="hello"' in repr_str
-        assert "v_bool=True" in repr_str
-
-
 def test_kw_only_class_level_signature() -> None:
     sig = inspect.signature(_TestCxxKwOnly.__init__)
     params = sig.parameters
diff --git a/tests/python/test_repr.py b/tests/python/test_repr.py
new file mode 100644
index 00000000..bc962117
--- /dev/null
+++ b/tests/python/test_repr.py
@@ -0,0 +1,581 @@
+# 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_repr__ / ffi.ReprPrint."""
+
+import re
+
+import numpy as np
+import pytest
+import tvm_ffi
+import tvm_ffi.testing
+from tvm_ffi._ffi_api import ReprPrint
+
+# Regex building blocks
+A = r"0x[0-9a-f]+"  # hex address
+
+
+def _check(result: str, pattern: str) -> None:
+    """Assert result matches pattern with re.fullmatch, with a clear error 
message."""
+    assert re.fullmatch(pattern, result), (
+        f"fullmatch failed:\n  result:  {result!r}\n  pattern: {pattern!r}"
+    )
+
+
+def test_repr_primitives() -> None:
+    """Test repr of primitive types."""
+    assert ReprPrint(42) == "42"
+    assert ReprPrint(0) == "0"
+    assert ReprPrint(-1) == "-1"
+    assert ReprPrint(True) == "True"
+    assert ReprPrint(False) == "False"
+    assert ReprPrint(None) == "None"
+
+
+def test_repr_float() -> None:
+    """Test repr of floating point."""
+    assert ReprPrint(3.14) == "3.14"
+    assert ReprPrint(0.0) == "0"
+    assert ReprPrint(1e10) == "1e+10"
+
+
+def test_repr_string() -> None:
+    """Test repr of FFI String (both SmallStr and StringObj)."""
+    # SmallStr (<=7 bytes)
+    assert ReprPrint("hello") == '"hello"'
+    # StringObj (>7 bytes)
+    assert ReprPrint("hello world") == '"hello world"'
+
+
+# ---------- Array (tuple format) ----------
+
+
+def test_repr_array() -> None:
+    """Test repr of FFI Array uses tuple format."""
+    assert ReprPrint(tvm_ffi.Array([1, 2, 3])) == "(1, 2, 3)"
+
+
+def test_repr_array_single() -> None:
+    """Test repr of single-element Array has trailing comma."""
+    assert ReprPrint(tvm_ffi.Array([42])) == "(42,)"
+
+
+def test_repr_array_empty() -> None:
+    """Test repr of empty Array."""
+    assert ReprPrint(tvm_ffi.Array([])) == "()"
+
+
+def test_repr_array_nested_strings() -> None:
+    """Test repr of Array containing strings."""
+    assert ReprPrint(tvm_ffi.Array(["a", "b"])) == '("a", "b")'
+
+
+def test_repr_array_python_repr() -> None:
+    """Test that Array.__repr__ uses the centralized repr."""
+    assert repr(tvm_ffi.Array([1, 2])) == "(1, 2)"
+
+
+# ---------- List ----------
+
+
+def test_repr_list() -> None:
+    """Test repr of FFI List uses list format."""
+    assert ReprPrint(tvm_ffi.List([10, 20])) == "[10, 20]"
+
+
+def test_repr_list_single() -> None:
+    """Test repr of single-element List (no trailing comma)."""
+    assert ReprPrint(tvm_ffi.List([99])) == "[99]"
+
+
+def test_repr_list_empty() -> None:
+    """Test repr of empty List."""
+    assert ReprPrint(tvm_ffi.List([])) == "[]"
+
+
+def test_repr_list_nested_strings() -> None:
+    """Test repr of List containing strings."""
+    assert ReprPrint(tvm_ffi.List(["x", "y"])) == '["x", "y"]'
+
+
+# ---------- Map ----------
+
+
+def test_repr_map() -> None:
+    """Test repr of FFI Map."""
+    assert ReprPrint(tvm_ffi.Map({"key": "value"})) == '{"key": "value"}'
+
+
+def test_repr_map_empty() -> None:
+    """Test repr of empty Map."""
+    assert ReprPrint(tvm_ffi.Map({})) == "{}"
+
+
+# ---------- Tensor ----------
+
+
+def test_repr_tensor() -> None:
+    """Test repr of Tensor shows dtype, shape, device (no address by 
default)."""
+    x = tvm_ffi.from_dlpack(np.zeros((3, 4), dtype="float32"))
+    assert ReprPrint(x) == "float32[3, 4]@cpu:0"
+
+
+def test_repr_tensor_int8() -> None:
+    """Test repr of Tensor with int8 dtype."""
+    x = tvm_ffi.from_dlpack(np.zeros((2,), dtype="int8"))
+    assert ReprPrint(x) == "int8[2]@cpu:0"
+
+
+# ---------- Shape ----------
+
+
+def test_repr_shape() -> None:
+    """Test repr of Shape."""
+    assert ReprPrint(tvm_ffi.Shape((5, 6))) == "Shape(5, 6)"
+
+
+# ---------- User-defined objects ----------
+
+
+def test_repr_user_object_all_fields() -> None:
+    """Test repr of user-defined object with all fields shown (no address by 
default)."""
+    obj = tvm_ffi.testing.create_object("testing.TestIntPair", a=10, b=20)
+    assert ReprPrint(obj) == "testing.TestIntPair(a=10, b=20)"
+
+
+def test_repr_user_object_repr_off() -> None:
+    """Test repr of object with Repr(false) fields excluded."""
+    obj = tvm_ffi.testing._TestCxxClassDerived(v_i64=1, v_i32=2, v_f64=3.5, 
v_f32=4.5)
+    assert ReprPrint(obj) == "testing.TestCxxClassDerived(v_f64=3.5, 
v_f32=4.5)"
+
+
+def test_repr_python_repr() -> None:
+    """Test that Python __repr__ delegates to ReprPrint."""
+    obj = tvm_ffi.testing.create_object("testing.TestIntPair", a=5, b=6)
+    assert repr(obj) == "testing.TestIntPair(a=5, b=6)"
+
+
+# ---------- DAG / shared references (full form on every occurrence) ----------
+
+
+def test_repr_duplicate_reference() -> None:
+    """Test that duplicate object references use full form on every 
occurrence."""
+    inner = tvm_ffi.testing.create_object("testing.TestIntPair", a=1, b=2)
+    arr = tvm_ffi.Array([inner, inner])
+    result = ReprPrint(arr)
+    assert result == "(testing.TestIntPair(a=1, b=2), testing.TestIntPair(a=1, 
b=2))"
+
+
+def test_repr_shared_in_map_values() -> None:
+    """Test that the same Array shared in Map values uses full form on both."""
+    shared = tvm_ffi.Array([1, 2])
+    m = tvm_ffi.Map({"a": shared, "b": shared})
+    result = ReprPrint(m)
+    # Map iteration order is hash-dependent; match either ordering.
+    pat_ab = r'\{"a": \(1, 2\), "b": \(1, 2\)\}'
+    pat_ba = r'\{"b": \(1, 2\), "a": \(1, 2\)\}'
+    _check(result, rf"(?:{pat_ab}|{pat_ba})")
+
+
+def test_repr_shared_across_nesting_levels() -> None:
+    """Test shared object across different nesting levels uses full form 
everywhere."""
+    leaf = tvm_ffi.testing.create_object("testing.TestIntPair", a=7, b=8)
+    arr = tvm_ffi.Array([leaf, tvm_ffi.Array([leaf])])
+    result = ReprPrint(arr)
+    assert result == "(testing.TestIntPair(a=7, b=8), 
(testing.TestIntPair(a=7, b=8),))"
+
+
+def test_repr_triple_shared_reference() -> None:
+    """Test object appearing three times -- full form every time."""
+    inner = tvm_ffi.testing.create_object("testing.TestIntPair", a=0, b=0)
+    arr = tvm_ffi.Array([inner, inner, inner])
+    result = ReprPrint(arr)
+    assert result == (
+        "(testing.TestIntPair(a=0, b=0), "
+        "testing.TestIntPair(a=0, b=0), "
+        "testing.TestIntPair(a=0, b=0))"
+    )
+
+
+# ---------- Nested containers ----------
+
+
+def test_repr_array_of_arrays() -> None:
+    """Test repr of Array containing Arrays."""
+    inner = tvm_ffi.Array([1, 2])
+    outer = tvm_ffi.Array([inner, tvm_ffi.Array([3])])
+    assert ReprPrint(outer) == "((1, 2), (3,))"
+
+
+def test_repr_map_of_containers() -> None:
+    """Test repr of Map containing Array values."""
+    m = tvm_ffi.Map({"a": tvm_ffi.Array([1, 2])})
+    assert ReprPrint(m) == '{"a": (1, 2)}'
+
+
+def test_repr_list_of_lists() -> None:
+    """Test repr of List containing Lists."""
+    inner = tvm_ffi.List([1, 2])
+    outer = tvm_ffi.List([inner, tvm_ffi.List([3])])
+    assert ReprPrint(outer) == "[[1, 2], [3]]"
+
+
+# ---------- Nested dataclasses ----------
+
+
+def test_repr_nested_dataclass() -> None:
+    """Test repr of object with object-typed fields."""
+    inner = tvm_ffi.testing.create_object("testing.TestIntPair", a=10, b=20)
+    obj = tvm_ffi.testing.create_object(
+        "testing.TestObjectDerived",
+        v_i64=1,
+        v_f64=2.5,
+        v_str="hi",
+        v_map=tvm_ffi.Map({}),
+        v_array=tvm_ffi.Array([inner]),
+    )
+    assert ReprPrint(obj) == (
+        'testing.TestObjectDerived(v_i64=1, v_f64=2.5, v_str="hi", '
+        "v_map={}, "
+        "v_array=(testing.TestIntPair(a=10, b=20),))"
+    )
+
+
+def test_repr_object_with_none_field() -> None:
+    """Test repr of object where container fields are empty."""
+    obj = tvm_ffi.testing.create_object(
+        "testing.TestObjectDerived",
+        v_i64=0,
+        v_f64=0.0,
+        v_str="",
+        v_map=tvm_ffi.Map({}),
+        v_array=tvm_ffi.Array([]),
+    )
+    assert (
+        ReprPrint(obj)
+        == 'testing.TestObjectDerived(v_i64=0, v_f64=0, v_str="", v_map={}, 
v_array=())'
+    )
+
+
+# ---------- Deep nesting ----------
+
+
+def test_repr_deeply_nested_arrays() -> None:
+    """Test repr of deeply nested Arrays (4 levels)."""
+    a = tvm_ffi.Array([1])
+    for _ in range(3):
+        a = tvm_ffi.Array([a])
+    assert ReprPrint(a) == "((((1,),),),)"
+
+
+def test_repr_deeply_nested_lists() -> None:
+    """Test repr of deeply nested Lists (4 levels)."""
+    lst = tvm_ffi.List([1])
+    for _ in range(3):
+        lst = tvm_ffi.List([lst])
+    assert ReprPrint(lst) == "[[[[1]]]]"
+
+
+def test_repr_mixed_container_nesting() -> None:
+    """Test repr of mixed Array/List/Map nesting."""
+    inner_list = tvm_ffi.List([1, 2])
+    inner_arr = tvm_ffi.Array([inner_list])
+    m = tvm_ffi.Map({"nested": inner_arr})
+    assert ReprPrint(m) == '{"nested": ([1, 2],)}'
+
+
+# ---------- Shared reference patterns ----------
+
+
+def test_repr_dataclass_shared_subobject() -> None:
+    """Test repr of two dataclasses sharing the same sub-object (full form in 
both)."""
+    shared = tvm_ffi.testing.create_object("testing.TestIntPair", a=5, b=5)
+    obj1 = tvm_ffi.testing.create_object(
+        "testing.TestObjectDerived",
+        v_i64=1,
+        v_f64=0.0,
+        v_str="",
+        v_map=tvm_ffi.Map({}),
+        v_array=tvm_ffi.Array([shared]),
+    )
+    obj2 = tvm_ffi.testing.create_object(
+        "testing.TestObjectDerived",
+        v_i64=2,
+        v_f64=0.0,
+        v_str="",
+        v_map=tvm_ffi.Map({}),
+        v_array=tvm_ffi.Array([shared]),
+    )
+    arr = tvm_ffi.Array([obj1, obj2])
+    result = ReprPrint(arr)
+    assert result == (
+        "("
+        'testing.TestObjectDerived(v_i64=1, v_f64=0, v_str="", v_map={}, '
+        "v_array=(testing.TestIntPair(a=5, b=5),)), "
+        'testing.TestObjectDerived(v_i64=2, v_f64=0, v_str="", v_map={}, '
+        "v_array=(testing.TestIntPair(a=5, b=5),))"
+        ")"
+    )
+
+
+# ---------- Container with dataclass nesting ----------
+
+
+def test_repr_array_of_dataclasses() -> None:
+    """Test repr of Array of user-defined objects."""
+    objs = [tvm_ffi.testing.create_object("testing.TestIntPair", a=i, b=i * 
10) for i in range(3)]
+    arr = tvm_ffi.Array(objs)
+    assert ReprPrint(arr) == (
+        "(testing.TestIntPair(a=0, b=0), "
+        "testing.TestIntPair(a=1, b=10), "
+        "testing.TestIntPair(a=2, b=20))"
+    )
+
+
+def test_repr_map_with_object_values() -> None:
+    """Test repr of Map with object values."""
+    pair = tvm_ffi.testing.create_object("testing.TestIntPair", a=1, b=2)
+    m = tvm_ffi.Map({"obj": pair})
+    assert ReprPrint(m) == '{"obj": testing.TestIntPair(a=1, b=2)}'
+
+
+# ---------- Repr(false) inheritance ----------
+
+
+def test_repr_derived_derived_shows_all_own_fields() -> None:
+    """TestCxxClassDerivedDerived should show v_f64, v_f32, v_str, v_bool (not 
v_i64, v_i32)."""
+    obj = tvm_ffi.testing._TestCxxClassDerivedDerived(
+        v_i64=1, v_i32=2, v_f64=3.0, v_f32=4.0, v_str="test", v_bool=True
+    )
+    assert (
+        ReprPrint(obj)
+        == 'testing.TestCxxClassDerivedDerived(v_f64=3, v_f32=4, v_str="test", 
v_bool=True)'
+    )
+
+
+# ---------- Edge cases: special values ----------
+
+
+def test_repr_large_integer() -> None:
+    """Test repr of large integers."""
+    assert ReprPrint(2**62) == str(2**62)
+    assert ReprPrint(-(2**62)) == str(-(2**62))
+
+
+def test_repr_negative_float() -> None:
+    """Test repr of negative floats."""
+    assert ReprPrint(-1.5) == "-1.5"
+
+
+def test_repr_empty_string() -> None:
+    """Test repr of empty string (SmallStr)."""
+    assert ReprPrint("") == '""'
+
+
+def test_repr_string_with_spaces() -> None:
+    """Test repr of string with spaces."""
+    assert ReprPrint("a b c") == '"a b c"'
+
+
+def test_repr_array_of_none() -> None:
+    """Test repr of Array containing None values."""
+    assert ReprPrint(tvm_ffi.Array([None, None])) == "(None, None)"
+
+
+def test_repr_array_of_booleans() -> None:
+    """Test repr of Array containing boolean values."""
+    assert ReprPrint(tvm_ffi.Array([True, False])) == "(True, False)"
+
+
+def test_repr_array_of_mixed_types() -> None:
+    """Test repr of Array containing mixed primitive types."""
+    assert ReprPrint(tvm_ffi.Array([1, "hello", True, None])) == '(1, "hello", 
True, None)'
+
+
+def test_repr_map_int_keys() -> None:
+    """Test repr of Map with integer keys."""
+    m = tvm_ffi.Map({1: 2, 3: 4})
+    result = ReprPrint(m)
+    # Map iteration order is hash-dependent; match either ordering.
+    _check(result, r"(?:\{1: 2, 3: 4\}|\{3: 4, 1: 2\})")
+
+
+def test_repr_map_with_array_values() -> None:
+    """Test repr of Map with Array values."""
+    assert ReprPrint(tvm_ffi.Map({1: tvm_ffi.Array([10, 20])})) == "{1: (10, 
20)}"
+
+
+# ---------- Nested dataclass edge cases ----------
+
+
+def test_repr_dataclass_with_array_field() -> None:
+    """Test repr of dataclass whose field is an Array of objects."""
+    pair1 = tvm_ffi.testing.create_object("testing.TestIntPair", a=1, b=2)
+    pair2 = tvm_ffi.testing.create_object("testing.TestIntPair", a=3, b=4)
+    obj = tvm_ffi.testing.create_object(
+        "testing.TestObjectDerived",
+        v_i64=0,
+        v_f64=0.0,
+        v_str="test",
+        v_map=tvm_ffi.Map({}),
+        v_array=tvm_ffi.Array([pair1, pair2]),
+    )
+    assert ReprPrint(obj) == (
+        'testing.TestObjectDerived(v_i64=0, v_f64=0, v_str="test", '
+        "v_map={}, "
+        "v_array=(testing.TestIntPair(a=1, b=2), testing.TestIntPair(a=3, 
b=4)))"
+    )
+
+
+def test_repr_dataclass_with_map_field() -> None:
+    """Test repr of dataclass whose field is a Map."""
+    obj = tvm_ffi.testing.create_object(
+        "testing.TestObjectDerived",
+        v_i64=42,
+        v_f64=1.0,
+        v_str="s",
+        v_map=tvm_ffi.Map({"x": 10}),
+        v_array=tvm_ffi.Array([]),
+    )
+    assert ReprPrint(obj) == (
+        'testing.TestObjectDerived(v_i64=42, v_f64=1, v_str="s", v_map={"x": 
10}, v_array=())'
+    )
+
+
+# ---------- Cycle detection ----------
+
+
+def test_repr_self_reference_cycle() -> None:
+    """Test that self-referencing cycles show '...' marker."""
+    obj = tvm_ffi.testing.create_object(
+        "testing.TestObjectDerived",
+        v_i64=1,
+        v_f64=2.0,
+        v_str="hi",
+        v_map=tvm_ffi.Map({}),
+        v_array=tvm_ffi.Array([]),
+    )
+    obj.v_array = tvm_ffi.Array([obj])  # type: ignore[unresolved-attribute]
+    result = ReprPrint(obj)
+    assert result == (
+        'testing.TestObjectDerived(v_i64=1, v_f64=2, v_str="hi", v_map={}, 
v_array=(...,))'
+    )
+
+
+def test_repr_mutual_reference_cycle() -> None:
+    """Test that mutual reference cycles show '...' marker."""
+    v_map = tvm_ffi.Map({})
+    obj_a = tvm_ffi.testing.create_object(
+        "testing.TestObjectDerived",
+        v_i64=1,
+        v_f64=0.0,
+        v_str="a",
+        v_map=v_map,
+        v_array=tvm_ffi.Array([]),
+    )
+    obj_b = tvm_ffi.testing.create_object(
+        "testing.TestObjectDerived",
+        v_i64=2,
+        v_f64=0.0,
+        v_str="b",
+        v_map=v_map,
+        v_array=tvm_ffi.Array([obj_a]),
+    )
+    obj_a.v_array = tvm_ffi.Array([obj_b])  # type: 
ignore[unresolved-attribute]
+    result = ReprPrint(obj_a)
+    assert result == (
+        'testing.TestObjectDerived(v_i64=1, v_f64=0, v_str="a", v_map={}, '
+        "v_array=(testing.TestObjectDerived(v_i64=2, v_f64=0, "
+        'v_str="b", v_map={}, v_array=(...,)),))'
+    )
+
+
+# ---------- TVM_FFI_REPR_WITH_ADDR ----------
+
+
+def test_repr_with_addr_user_object(monkeypatch: pytest.MonkeyPatch) -> None:
+    """Test that user objects show address when TVM_FFI_REPR_WITH_ADDR is 
set."""
+    monkeypatch.setenv("TVM_FFI_REPR_WITH_ADDR", "1")
+    obj = tvm_ffi.testing.create_object("testing.TestIntPair", a=1, b=2)
+    _check(ReprPrint(obj), rf"testing\.TestIntPair@{A}\(a=1, b=2\)")
+
+
+def test_repr_with_addr_array(monkeypatch: pytest.MonkeyPatch) -> None:
+    """Test that Array shows address suffix when TVM_FFI_REPR_WITH_ADDR is 
set."""
+    monkeypatch.setenv("TVM_FFI_REPR_WITH_ADDR", "1")
+    arr = tvm_ffi.Array([1, 2, 3])
+    _check(ReprPrint(arr), rf"\(1, 2, 3\)@{A}")
+
+
+def test_repr_with_addr_list(monkeypatch: pytest.MonkeyPatch) -> None:
+    """Test that List shows address suffix when TVM_FFI_REPR_WITH_ADDR is 
set."""
+    monkeypatch.setenv("TVM_FFI_REPR_WITH_ADDR", "1")
+    lst = tvm_ffi.List([10, 20])
+    _check(ReprPrint(lst), rf"\[10, 20\]@{A}")
+
+
+def test_repr_with_addr_dag(monkeypatch: pytest.MonkeyPatch) -> None:
+    """Test DAG with addresses: both occurrences show full form with same 
address."""
+    monkeypatch.setenv("TVM_FFI_REPR_WITH_ADDR", "1")
+    inner = tvm_ffi.testing.create_object("testing.TestIntPair", a=1, b=2)
+    arr = tvm_ffi.Array([inner, inner])
+    result = ReprPrint(arr)
+    _check(
+        result,
+        rf"\(testing\.TestIntPair@(?P<a>{A})\(a=1, b=2\), "
+        rf"testing\.TestIntPair@(?P=a)\(a=1, b=2\)\)@{A}",
+    )
+
+
+def test_repr_with_addr_cycle(monkeypatch: pytest.MonkeyPatch) -> None:
+    """Test cycle with addresses: '...@ADDR' points back to the cyclic 
object."""
+    monkeypatch.setenv("TVM_FFI_REPR_WITH_ADDR", "1")
+    obj = tvm_ffi.testing.create_object(
+        "testing.TestObjectDerived",
+        v_i64=1,
+        v_f64=0.0,
+        v_str="",
+        v_map=tvm_ffi.Map({}),
+        v_array=tvm_ffi.Array([]),
+    )
+    obj.v_array = tvm_ffi.Array([obj])  # type: ignore[unresolved-attribute]
+    result = ReprPrint(obj)
+    _check(
+        result,
+        rf"testing\.TestObjectDerived@(?P<obj>{A})\("
+        rf'v_i64=1, v_f64=0, v_str="", v_map=\{{\}}, '
+        rf"v_array=\(\.\.\.@(?P=obj),\)@{A}"
+        rf"\)",
+    )
+
+
+def test_repr_with_addr_tensor(monkeypatch: pytest.MonkeyPatch) -> None:
+    """Test that Tensor shows address suffix when TVM_FFI_REPR_WITH_ADDR is 
set."""
+    monkeypatch.setenv("TVM_FFI_REPR_WITH_ADDR", "1")
+    x = tvm_ffi.from_dlpack(np.zeros((3, 4), dtype="float32"))
+    _check(ReprPrint(x), rf"float32\[3, 4\]@cpu:0@{A}")
+
+
+def test_repr_with_addr_no_fields(monkeypatch: pytest.MonkeyPatch) -> None:
+    """Test that object with no visible fields shows TypeKey@ADDR with env 
var."""
+    monkeypatch.setenv("TVM_FFI_REPR_WITH_ADDR", "1")
+    # TestCxxClassBase has v_i64 and v_i32, both with Repr(false)
+    obj = tvm_ffi.testing._TestCxxClassBase(v_i64=1, v_i32=2)
+    _check(ReprPrint(obj), rf"testing\.TestCxxClassBase@{A}")
+
+
+if __name__ == "__main__":
+    pytest.main([__file__, "-v"])

Reply via email to