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

Reply via email to