This is an automated email from the ASF dual-hosted git repository.
junrushao 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 aed51c0 fix(py_class): propagate parent total_size through empty
intermediate classes (#539)
aed51c0 is described below
commit aed51c05236be1835065c0ce4bd86dc54c3b934c
Author: Junru Shao <[email protected]>
AuthorDate: Sun Apr 12 13:04:48 2026 -0700
fix(py_class): propagate parent total_size through empty intermediate
classes (#539)
## Summary
- **Bug**: In Python-defined type hierarchies, when an intermediate
parent class
had no own fields (e.g., `GrandParent(x, y) -> Parent(pass) -> Child(a,
b)`),
`TypeInfo.total_size` fell back to `sizeof(TVMFFIObject)` (~24 bytes)
for the
empty parent, ignoring inherited fields. This caused child field offsets
to
overlap with grandparent field offsets, leading to silent data
corruption on
read/write.
- **Fix**: Replace the `sizeof(TVMFFIObject)` floor with
`self.parent_type_info.total_size` in `TypeInfo.total_size`, ensuring
inherited
field space propagates correctly through arbitrarily deep empty parents.
Added a
guard that `parent_type_info` must be present and an assertion that the
base size
is at least `sizeof(TVMFFIObject)`.
- **Tests**: Added 6 regression tests in `TestFieldInheritance` covering
offset
correctness, total_size propagation, non-overlapping child offsets,
two-level
empty intermediate parents, mutation aliasing, and the `@py_class`
decorator path.
## Architecture
- **Changed file**: `python/tvm_ffi/cython/type_info.pxi` —
`TypeInfo.total_size`
property body (6 lines replaced with 6 lines; decorator unchanged).
- **Root cause**: The old code initialized `end = sizeof(TVMFFIObject)`
and only
iterated over `self.fields` (the type's own fields). An empty
intermediate class
has no own fields, so it returned a total_size equal to just the object
header,
erasing all inherited field space.
- **Fix mechanism**: `end = self.parent_type_info.total_size` inherits
the parent's
full layout (including transitively inherited fields), then `max(end,
f.offset + f.size)`
extends it for any own fields.
## Behavioral Changes
- Before: `Child.a` at offset 24 overlapping `GrandParent.x` at offset
24 when
`Parent` had no own fields. Reading `obj.x` returned `obj.a`'s value.
- After: `Child.a` starts at or after `Parent.total_size` (which equals
`GrandParent.total_size`), so no field overlap occurs.
## Test Plan
- [x] `uv run pytest -vvs
tests/python/test_dataclass_py_class.py::TestFieldInheritance` -- all
tests pass (8 existing + 6 new)
- [x] All pre-commit hooks pass
---
python/tvm_ffi/cython/type_info.pxi | 12 +--
python/tvm_ffi/dataclasses/py_class.py | 2 +-
python/tvm_ffi/registry.py | 9 ++-
tests/python/test_dataclass_py_class.py | 128 ++++++++++++++++++++++++++++++++
4 files changed, 143 insertions(+), 8 deletions(-)
diff --git a/python/tvm_ffi/cython/type_info.pxi
b/python/tvm_ffi/cython/type_info.pxi
index 7a19590..67be0d8 100644
--- a/python/tvm_ffi/cython/type_info.pxi
+++ b/python/tvm_ffi/cython/type_info.pxi
@@ -696,12 +696,12 @@ class TypeInfo:
cdef const TVMFFITypeInfo* c_info = TVMFFIGetTypeInfo(self.type_index)
if c_info != NULL and c_info.metadata != NULL:
return c_info.metadata.total_size
- cdef int64_t end = sizeof(TVMFFIObject)
- if self.fields:
- for f in self.fields:
- f_end = f.offset + f.size
- if f_end > end:
- end = f_end
+ if self.parent_type_info is None:
+ raise ValueError(f"Cannot find parent type of {type(self)}")
+ cdef int64_t end = self.parent_type_info.total_size
+ assert end >= sizeof(TVMFFIObject)
+ for f in self.fields:
+ end = max(end, f.offset + f.size)
return (end + 7) & ~7 # align to 8 bytes
def _register_fields(self, fields, structure_kind=None):
diff --git a/python/tvm_ffi/dataclasses/py_class.py
b/python/tvm_ffi/dataclasses/py_class.py
index 2d3d287..b5b9c92 100644
--- a/python/tvm_ffi/dataclasses/py_class.py
+++ b/python/tvm_ffi/dataclasses/py_class.py
@@ -149,7 +149,7 @@ def _collect_own_fields(
"""
fields: list[Field] = []
kw_only_active = decorator_kw_only
- own_annotations: dict[str, str] = getattr(cls, "__annotations__", {})
+ own_annotations: dict[str, str] = cls.__dict__.get("__annotations__", {})
for name in own_annotations:
resolved_type = hints.get(name)
diff --git a/python/tvm_ffi/registry.py b/python/tvm_ffi/registry.py
index 7d6359a..0dc2bb7 100644
--- a/python/tvm_ffi/registry.py
+++ b/python/tvm_ffi/registry.py
@@ -431,13 +431,20 @@ def _make_init_signature(type_info: TypeInfo) ->
inspect.Signature:
# Walk the parent chain to collect all fields (parent-first order).
all_fields: list[Any] = []
+ seen_names: set[str] = set()
ti: TypeInfo | None = type_info
chain: list[TypeInfo] = []
while ti is not None:
chain.append(ti)
ti = ti.parent_type_info
for ancestor_info in reversed(chain):
- all_fields.extend(ancestor_info.fields)
+ for f in ancestor_info.fields:
+ if f.name in seen_names:
+ raise ValueError(
+ f"duplicate field name {f.name!r} in type hierarchy of
{type_info.type_key!r}"
+ )
+ seen_names.add(f.name)
+ all_fields.append(f)
for field in all_fields:
if not field.c_init:
diff --git a/tests/python/test_dataclass_py_class.py
b/tests/python/test_dataclass_py_class.py
index e1d3dac..57a7c1c 100644
--- a/tests/python/test_dataclass_py_class.py
+++ b/tests/python/test_dataclass_py_class.py
@@ -2763,6 +2763,134 @@ class TestFieldInheritance:
obj_copy.c = 99
assert obj.c == 3
+ # -- Regression: empty intermediate parent must propagate total_size -----
+
+ def test_empty_intermediate_parent_offsets(self) -> None:
+ """GrandParent(x,y) -> Parent(no fields) -> Child(a,b): no overlap."""
+ GP = _make_type(
+ "OffGP",
+ [
+ Field(name="x", ty=TypeSchema("int"), default=MISSING),
+ Field(name="y", ty=TypeSchema("int"), default=MISSING),
+ ],
+ )
+ P = _make_type("OffP", [], parent=GP)
+ C = _make_type(
+ "OffC",
+ [
+ Field(name="a", ty=TypeSchema("int"), default=MISSING),
+ Field(name="b", ty=TypeSchema("int"), default=MISSING),
+ ],
+ parent=P,
+ )
+ obj = C(x=10, y=20, a=30, b=40)
+ assert obj.x == 10
+ assert obj.y == 20
+ assert obj.a == 30
+ assert obj.b == 40
+
+ def test_empty_intermediate_parent_total_size(self) -> None:
+ """Parent with no own fields inherits total_size from its parent."""
+ GP = _make_type(
+ "OffGP2",
+ [
+ Field(name="x", ty=TypeSchema("int"), default=MISSING),
+ Field(name="y", ty=TypeSchema("int"), default=MISSING),
+ ],
+ )
+ P = _make_type("OffP2", [], parent=GP)
+ gp_info = getattr(GP, "__tvm_ffi_type_info__")
+ p_info = getattr(P, "__tvm_ffi_type_info__")
+ assert p_info.total_size >= gp_info.total_size
+
+ def test_empty_intermediate_child_offsets_non_overlapping(self) -> None:
+ """Child field offsets must start at or after Parent.total_size."""
+ GP = _make_type(
+ "OffGP3",
+ [
+ Field(name="x", ty=TypeSchema("int"), default=MISSING),
+ Field(name="y", ty=TypeSchema("int"), default=MISSING),
+ ],
+ )
+ P = _make_type("OffP3", [], parent=GP)
+ C = _make_type(
+ "OffC3",
+ [Field(name="a", ty=TypeSchema("int"), default=MISSING)],
+ parent=P,
+ )
+ p_info = getattr(P, "__tvm_ffi_type_info__")
+ c_info = getattr(C, "__tvm_ffi_type_info__")
+ assert c_info.fields[0].offset >= p_info.total_size
+
+ def test_two_empty_intermediate_parents(self) -> None:
+ """GP(x) -> Mid1() -> Mid2() -> Leaf(a): offsets propagated through
two empty layers."""
+ GP = _make_type(
+ "OffGP4",
+ [Field(name="x", ty=TypeSchema("int"), default=MISSING)],
+ )
+ Mid1 = _make_type("OffMid1", [], parent=GP)
+ Mid2 = _make_type("OffMid2", [], parent=Mid1)
+ Leaf = _make_type(
+ "OffLeaf",
+ [Field(name="a", ty=TypeSchema("int"), default=MISSING)],
+ parent=Mid2,
+ )
+ obj = Leaf(x=1, a=2)
+ assert obj.x == 1
+ assert obj.a == 2
+ gp_info = getattr(GP, "__tvm_ffi_type_info__")
+ leaf_info = getattr(Leaf, "__tvm_ffi_type_info__")
+ assert leaf_info.fields[0].offset >= gp_info.total_size
+
+ def test_empty_intermediate_mutation_no_aliasing(self) -> None:
+ """Mutating Child.a must not corrupt GrandParent.x through an empty
parent."""
+ GP = _make_type(
+ "OffGP5",
+ [
+ Field(name="x", ty=TypeSchema("int"), default=MISSING),
+ Field(name="y", ty=TypeSchema("int"), default=MISSING),
+ ],
+ )
+ P = _make_type("OffP5", [], parent=GP)
+ C = _make_type(
+ "OffC5",
+ [
+ Field(name="a", ty=TypeSchema("int"), default=MISSING),
+ Field(name="b", ty=TypeSchema("int"), default=MISSING),
+ ],
+ parent=P,
+ )
+ obj = C(x=10, y=20, a=30, b=40)
+ obj.a = 99
+ assert obj.x == 10
+ assert obj.y == 20
+ obj.x = 77
+ assert obj.a == 99
+ assert obj.b == 40
+
+ def test_empty_intermediate_py_class_decorator(self) -> None:
+ """Same bug via @py_class (the high-level API)."""
+
+ @py_class(_unique_key("OffDecGP"))
+ class GP(Object):
+ x: int = 0
+ y: int = 0
+
+ @py_class(_unique_key("OffDecP"))
+ class P(GP):
+ pass
+
+ @py_class(_unique_key("OffDecC"))
+ class C(P):
+ a: int = 0
+ b: int = 0
+
+ obj = C(x=10, y=20, a=30, b=40)
+ assert obj.x == 10
+ assert obj.y == 20
+ assert obj.a == 30
+ assert obj.b == 40
+
# ###########################################################################
# 15. Mutual / Self References