This is an automated email from the ASF dual-hosted git repository.
tlopex 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 c62b07c2 [CORE] Publish native layout traits from ObjectDef (#721)
c62b07c2 is described below
commit c62b07c2b27ec09a2616eb314a0ef2acc006d55d
Author: Linzhang Li <[email protected]>
AuthorDate: Wed Sep 2 13:33:02 2026 -0400
[CORE] Publish native layout traits from ObjectDef (#721)
Class::_type_final never reaches the runtime. TVMFFITypeInfo exposes the
type index, depth, ancestors, fields and methods; TVMFFITypeMetadata
adds the creator, total_size and the structural-eq kind. Neither carries
num_child_slots / child_slots_can_overflow — those are arguments to the
registration call and stay inside the registry. So nothing downstream
can ask whether a class admits subtypes, and a binding generator that
wants to store an object by value has no way to find out.
Every ObjectDef<Class> now registers __ffi_type_final__ as a bool type
attribute, reusing the existing RegisterTypeAttrValue mechanism (one
bool Any per type, no C ABI change). ObjectInfo.is_final surfaces it to
the stub generator.
is_final stays None for a type registered without an ObjectDef (e.g.
ffi.Object), so a registered False remains distinguishable from "never
published" — the distinction matters because a consumer must not treat
"not final" and "unknown" alike.
Testing — tests/cpp/test_reflection.cc: a non-final type, a final one,
and ffi.Module. tests/python/test_stubgen.py: the same three cases
through ObjectInfo, including the None case.
Signed-off-by: yuchuan <[email protected]>
---
include/tvm/ffi/reflection/accessor.h | 7 +++++++
include/tvm/ffi/reflection/registry.h | 2 ++
python/tvm_ffi/stub/utils.py | 3 +++
tests/cpp/test_reflection.cc | 8 ++++++++
tests/python/test_stubgen.py | 16 ++++++++++++++++
5 files changed, 36 insertions(+)
diff --git a/include/tvm/ffi/reflection/accessor.h
b/include/tvm/ffi/reflection/accessor.h
index 0f8337d9..c0e3d93f 100644
--- a/include/tvm/ffi/reflection/accessor.h
+++ b/include/tvm/ffi/reflection/accessor.h
@@ -564,6 +564,13 @@ inline constexpr const char* kDataToJson =
"__data_to_json__";
inline constexpr const char* kDataFromJson = "__data_from_json__";
/*! \brief Per-class enum state: ordered entries, canonical indices, and
extensible attrs. */
inline constexpr const char* kEnumState = "__ffi_enum__";
+/*!
+ * \brief Whether the class is declared final (``Class::_type_final``).
+ *
+ * Registered as a ``bool`` by every ``ObjectDef<Class>``. A final class can
+ * never gain a subtype, so a binding generator may store it by value.
+ */
+inline constexpr const char* kTypeFinal = "__ffi_type_final__";
} // namespace type_attr
/*!
diff --git a/include/tvm/ffi/reflection/registry.h
b/include/tvm/ffi/reflection/registry.h
index 04e47d84..2d1b3a43 100644
--- a/include/tvm/ffi/reflection/registry.h
+++ b/include/tvm/ffi/reflection/registry.h
@@ -781,6 +781,8 @@ class ObjectDef : public ReflectionDefBase {
}
}
}
+ // Step 4. Publish finality, which no other registered metadata exposes.
+ RegisterTypeAttrValue(type_index_, type_attr::kTypeFinal,
Class::_type_final);
}
/*!
diff --git a/python/tvm_ffi/stub/utils.py b/python/tvm_ffi/stub/utils.py
index c16eb657..a053758e 100644
--- a/python/tvm_ffi/stub/utils.py
+++ b/python/tvm_ffi/stub/utils.py
@@ -202,6 +202,8 @@ class ObjectInfo:
"""Type keys of every ancestor, root first (``["ffi.Object", "ir.Expr"]``
for ``tirx.Add``)."""
total_size: int | None = None
"""Native ``sizeof`` in bytes, or ``None`` when the type has no metadata
of its own."""
+ is_final: bool | None = None
+ """``__ffi_type_final__``: the class admits no subtype. ``None`` when
unregistered."""
def has_overloaded_methods(self) -> bool:
"""Return whether reflection exposed multiple signatures for a
method."""
@@ -264,4 +266,5 @@ class ObjectInfo:
has_init=has_init,
ancestors=[info.type_key for info in ancestor_infos],
total_size=type_info.total_size if type_info._has_type_metadata
else None,
+ is_final=_lookup_type_attr(type_info.type_index,
"__ffi_type_final__"),
)
diff --git a/tests/cpp/test_reflection.cc b/tests/cpp/test_reflection.cc
index 361ef9d5..af4629f2 100644
--- a/tests/cpp/test_reflection.cc
+++ b/tests/cpp/test_reflection.cc
@@ -375,6 +375,14 @@ TEST(Reflection, TypeAttrDefDirectValues) {
EXPECT_EQ(func_attr[type_index].cast<Function>()(5).cast<int64_t>(), 10);
}
+TEST(Reflection, ObjectDefPublishesTypeFinal) {
+ reflection::TypeAttrColumn final_attr(reflection::type_attr::kTypeFinal);
+
+ EXPECT_FALSE(final_attr[TestObjA::RuntimeTypeIndex()].cast<bool>());
+ EXPECT_TRUE(final_attr[TestObjADerived::RuntimeTypeIndex()].cast<bool>());
+ EXPECT_TRUE(final_attr[TypeKeyToIndex("ffi.Module")].cast<bool>());
+}
+
TVM_FFI_STATIC_INIT_BLOCK() {
namespace refl = tvm::ffi::reflection;
refl::GlobalDef().def_method("testing.Int_GetValue", &TIntObj::GetValue);
diff --git a/tests/python/test_stubgen.py b/tests/python/test_stubgen.py
index 4fefdc24..199ad796 100644
--- a/tests/python/test_stubgen.py
+++ b/tests/python/test_stubgen.py
@@ -1030,6 +1030,22 @@ def test_objectinfo_total_size_requires_own_metadata()
-> None:
assert func.ancestors == ["ffi.Object"]
+def test_objectinfo_is_final() -> None:
+ """``ObjectDef`` publishes finality; a type registered without one
publishes none."""
+ # A registered `False` must stay distinguishable from an unregistered
attribute.
+ base = ObjectInfo.from_type_info(_TestCxxClassBase.__tvm_ffi_type_info__)
# ty: ignore[unresolved-attribute]
+ assert base.is_final is False
+
+ derived = ObjectInfo.from_type_info(
+
_lookup_or_register_type_info_from_type_key("testing.TestObjectDerived")
+ )
+ assert derived.is_final is True
+
+ # `ffi.Object` is registered without an `ObjectDef`, so it claims nothing.
+ root =
ObjectInfo.from_type_info(_lookup_or_register_type_info_from_type_key("ffi.Object"))
+ assert root.is_final is None
+
+
def test_named_type_schema_keeps_positional_signature() -> None:
"""Synthetic schemas (function params, tests) carry no layout facts."""
schema = NamedTypeSchema("x", TypeSchema("int"))