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"])