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 7bedac88 [Rust] structural_equal/hash/mismatch; name the parent 
`base_` and the generated handle `data_` (#951)
7bedac88 is described below

commit 7bedac88ac4f2737d089244bae4d7be9d3599140
Author: Tianqi Chen <[email protected]>
AuthorDate: Fri Nov 6 20:12:40 2026 -0500

    [Rust] structural_equal/hash/mismatch; name the parent `base_` and the 
generated handle `data_` (#951)
    
    Two commits:
    
    1. **Name the parent `base_` and the generated handle `data_`.** Object
    structs embed their parent as `base_`, and stubgen references hold their
    `ObjectArc` as `data_`, so reflected fields keep their registered names
    (`base`, `data`, `global_var_map_`); stubgen rejects only a reflected
    `base_`/`data_`. `#[rs_object]` requires `base_` and points a `base`
    field to the new name (one-time break). Stubgen emits the reference
    before its `*Obj` (a `///` above the block documents the reference; the
    `*Obj` gets a one-line summary), adds a `visibility` directive
    (`<type_key>.<field> -> pub(crate)`), and no longer imports the parent's
    type for a parameter a `field` directive narrows.
    2. **`structural_equal`, `structural_hash`,
    `get_first_structural_mismatch`** (and `_with` variants taking
    `StructuralOptions { map_free_vars, skip_tensor_content }`), thin
    wrappers over the registered
    `ffi.StructuralEqual`/`ffi.StructuralHash`/`ffi.GetFirstStructuralMismatch`,
    re-exported at the crate root.
---
 docs/guides/rust_lang_guide.rst                    |   8 +-
 examples/rust_stubgen/README.md                    |   8 +-
 .../rust/src/generated/rust_stubgen/mod.rs         |  22 +-
 python/tvm_ffi/stub/rust_generator/codegen.py      | 104 +++++++---
 python/tvm_ffi/stub/rust_generator/consts.py       |  12 +-
 python/tvm_ffi/stub/rust_generator/directives.py   |  12 +-
 python/tvm_ffi/stub/rust_generator/utils.py        |  19 +-
 rust/tvm-ffi-macros/src/object_macros.rs           |   2 +-
 rust/tvm-ffi-macros/src/rs_object.rs               |  15 +-
 rust/tvm-ffi/src/collections/tensor.rs             |  18 +-
 rust/tvm-ffi/src/derive.rs                         |   4 +-
 rust/tvm-ffi/src/extra/mod.rs                      |   1 +
 rust/tvm-ffi/src/extra/structural_eq_hash.rs       | 189 +++++++++++++++++
 rust/tvm-ffi/src/extra/structural_mutate.rs        |   6 +-
 rust/tvm-ffi/src/extra/structural_visit.rs         |   8 +-
 rust/tvm-ffi/src/extra/visit_error_context.rs      |   6 +-
 rust/tvm-ffi/src/lib.rs                            |  15 +-
 rust/tvm-ffi/src/macros.rs                         |   2 +-
 rust/tvm-ffi/src/object.rs                         |   6 +-
 rust/tvm-ffi/src/reflection/registry.rs            |   2 +-
 rust/tvm-ffi/tests/test_any.rs                     |   6 +-
 rust/tvm-ffi/tests/test_cast.rs                    |  16 +-
 rust/tvm-ffi/tests/test_match_any_ast_lookup.rs    |   8 +-
 rust/tvm-ffi/tests/test_object_ref_any.rs          |   2 +-
 rust/tvm-ffi/tests/test_object_reflection.rs       |  24 +--
 rust/tvm-ffi/tests/test_object_registration.rs     |  12 +-
 rust/tvm-ffi/tests/test_rs_object.rs               |  32 +--
 rust/tvm-ffi/tests/test_structural_eq_hash.rs      |  51 +++++
 rust/tvm-ffi/tests/test_structural_mutate.rs       |   8 +-
 tests/python/test_stubgen_rust.py                  | 227 +++++++++++----------
 30 files changed, 587 insertions(+), 258 deletions(-)

diff --git a/docs/guides/rust_lang_guide.rst b/docs/guides/rust_lang_guide.rst
index 1332452a..c32e44f0 100644
--- a/docs/guides/rust_lang_guide.rst
+++ b/docs/guides/rust_lang_guide.rst
@@ -323,7 +323,7 @@ first ``type_index()``, which allocating an instance calls:
    #[type_s_eq_hash_kind = "TreeNode"]
    #[type_reflection(PointObj::reflect_methods)]
    pub struct PointObj {
-       base: Object,
+       base_: Object,
        /// The x coordinate.
        #[reflect]
        pub x: i64,
@@ -366,7 +366,7 @@ Defining Object Types from Their Fields
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 ``#[rs_object]`` defines such a type from its object struct, like Python's
-``py_class``. The first field, ``base``, is the parent's object; every other 
field
+``py_class``. The first field, ``base_``, is the parent's object; every other 
field
 is reflected, and ``#[reflect(...)]`` sets its flags, name and doc string.
 ``reflection = path`` after the type key is ``#[type_reflection(path)]``.
 ``#[derive(ObjectRef)]`` declares the reference, and ``#[object_ref(parent = 
Point)]``
@@ -379,7 +379,7 @@ names the reference to the parent's object:
 
    #[rs_object("my.Point", s_eq_hash_kind = "TreeNode")]
    pub struct PointObj {
-       base: Object,
+       base_: Object,
        pub x: i64,
        #[reflect(writable, seq_hash_ignore)]
        pub label: tvm_ffi::String,
@@ -392,7 +392,7 @@ names the reference to the parent's object:
 
    #[rs_object("my.Point3", final)]
    pub struct Point3Obj {
-       base: PointObj,
+       base_: PointObj,
        pub z: i64,
    }
 
diff --git a/examples/rust_stubgen/README.md b/examples/rust_stubgen/README.md
index ed67c858..d7ebeda0 100644
--- a/examples/rust_stubgen/README.md
+++ b/examples/rust_stubgen/README.md
@@ -31,6 +31,9 @@ and alignment to the reflected facts, and a generated 
allocator builds the
 object in Rust. `main.rs` builds one that way, reads `pair.a` directly, and
 hands it to a C++ function that reads it back.
 
+The generated members end in `_` (`base_`, the embedded parent; `data_`, the
+reference's handle), so reflected fields keep their registered names.
+
 An object whose layout cannot be reproduced (a polymorphic one, say, with a
 vtable in front of the object header) is bound *opaquely* instead: the struct
 embeds only the parent, every field is read through an accessor that calls the
@@ -112,10 +115,11 @@ Besides `prefix` and `custom-new`, this example declares 
the integer field
 // tvm-ffi-stubgen(enum): rust_stubgen.IntPair.kind -> PairKind(i32) { 
Unordered=0, Ordered=1 }
 ```
 
-Three more are available: `field` names the Rust type of a field
+Four more are available: `field` names the Rust type of a field
 (`// tvm-ffi-stubgen(field): rust_stubgen.IntPair.a -> MyInt`), `nullable`
 wraps it in `Option` (`// tvm-ffi-stubgen(nullable): rust_stubgen.IntPair.a`),
-and `upcast` adds a conversion to a hand-written typed view
+`visibility` makes it `pub(crate)` instead of `pub`
+(`// tvm-ffi-stubgen(visibility): rust_stubgen.IntPair.a -> pub(crate)`), and 
`upcast` adds a conversion to a hand-written typed view
 (`// tvm-ffi-stubgen(upcast): rust_stubgen.IntPair -> MyView`).
 
 When the C++ side fixes the value set in its ABI, `closed` before the member
diff --git a/examples/rust_stubgen/rust/src/generated/rust_stubgen/mod.rs 
b/examples/rust_stubgen/rust/src/generated/rust_stubgen/mod.rs
index a920ad01..cbf4eece 100644
--- a/examples/rust_stubgen/rust/src/generated/rust_stubgen/mod.rs
+++ b/examples/rust_stubgen/rust/src/generated/rust_stubgen/mod.rs
@@ -38,6 +38,12 @@ use tvm_ffi::VALUE_ERROR;
 // tvm-ffi-stubgen(custom-new): rust_stubgen.IntPair
 
 // tvm-ffi-stubgen(begin): object/rust_stubgen.IntPair
+#[repr(C)]
+#[derive(tvm_ffi::derive::ObjectRef, Clone)]
+pub struct IntPair {
+    data_: ObjectArc<IntPairObj>,
+}
+
 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
 #[repr(transparent)]
 pub struct PairKind(i32);
@@ -63,13 +69,15 @@ impl TryFrom<i64> for PairKind {
     }
 }
 
+/// The object behind [`IntPair`].
+///
 /// Complete: reflected fields fill [24, 48) exactly (alignment padding [44, 
48)).
 #[repr(C)]
 #[derive(tvm_ffi::derive::Object)]
 #[type_key = "rust_stubgen.IntPair"]
 #[type_final]
 pub struct IntPairObj {
-    base: Object,
+    base_: Object,
     pub a: i64,
     pub b: i64,
     pub kind: PairKind,
@@ -80,16 +88,10 @@ const _: () = {
     assert!(::core::mem::align_of::<IntPairObj>() == 8);
 };
 
-#[repr(C)]
-#[derive(tvm_ffi::derive::ObjectRef, Clone)]
-pub struct IntPair {
-    base: ObjectArc<IntPairObj>,
-}
-
 impl IntPairObj {
     pub(crate) fn new(a: i64, b: i64, kind: PairKind) -> Self {
-        let base = Object::new();
-        Self { base, a, b, kind }
+        let base_ = Object::new();
+        Self { base_, a, b, kind }
     }
 }
 
@@ -97,7 +99,7 @@ impl IntPair {
     /// Lossless complete-field allocation.
     pub fn from_fields(a: i64, b: i64, kind: impl Into<PairKind>) -> Self {
         let obj = IntPairObj::new(a, b, kind.into());
-        Self { base: ObjectArc::new(obj) }
+        Self { data_: ObjectArc::new(obj) }
     }
 }
 
diff --git a/python/tvm_ffi/stub/rust_generator/codegen.py 
b/python/tvm_ffi/stub/rust_generator/codegen.py
index fbd58c4b..24281716 100644
--- a/python/tvm_ffi/stub/rust_generator/codegen.py
+++ b/python/tvm_ffi/stub/rust_generator/codegen.py
@@ -221,7 +221,7 @@ class _ObjectRenderer:
         return None
 
     def _base_type(self) -> tuple[str, bool]:
-        """Resolve the ``base`` struct and whether it is a generated or 
``ty-map``'d parent.
+        """Resolve the ``base_`` struct and whether it is a generated or 
``ty-map``'d parent.
 
         A builtin parent below ``ffi.Object`` is embedded as its header-only
         stand-in (see :meth:`RustImports.record_builtin_base`).
@@ -334,6 +334,10 @@ class _ObjectRenderer:
         """``#[allow(missing_docs)]`` under ``missing-docs: allow``, for a 
crate that warns on it."""
         return ["#[allow(missing_docs)]"] if 
self.imports.directives.allow_missing_docs else []
 
+    def _visibility(self, target: str) -> str:
+        """``pub``, or what a ``visibility`` directive sets."""
+        return self.imports.directives.visibility.get(target, "pub")
+
     def _accessor_lines(self, field: NamedTypeSchema) -> list[str]:
         """One ``pub fn <field>(&self) -> Result<T>`` through the C ABI getter.
 
@@ -342,6 +346,7 @@ class _ObjectRenderer:
         directives = self.imports.directives
         target = f"{self.type_key}.{field.name}"
         name = rust_ident(field.name)
+        vis = self._visibility(target)
         getter = f'FieldGetter::new(Self::type_index(), "{field.name}")?'
         allow = _allow_non_snake_case([name])
 
@@ -349,7 +354,7 @@ class _ObjectRenderer:
         if enum is not None:
             return [
                 *allow,
-                f"pub fn {name}(&self) -> Result<{enum.name}> {{",
+                f"{vis} fn {name}(&self) -> Result<{enum.name}> {{",
                 f"    let raw: i64 = {getter}.get(self)?;",
                 f"    {enum.name}::try_from(raw)",
                 "}",
@@ -365,7 +370,7 @@ class _ObjectRenderer:
             any_type = self.imports.record("tvm_ffi::Any")
             return [
                 *allow,
-                f"pub fn {name}(&self) -> Result<{any_type}> {{",
+                f"{vis} fn {name}(&self) -> Result<{any_type}> {{",
                 f"    {getter}.get_any(self)",
                 "}",
             ]
@@ -373,7 +378,7 @@ class _ObjectRenderer:
             rust_type = f"Option<{rust_type}>"
         return [
             *allow,
-            f"pub fn {name}(&self) -> Result<{rust_type}> {{",
+            f"{vis} fn {name}(&self) -> Result<{rust_type}> {{",
             f"    {getter}.get(self)",
             "}",
         ]
@@ -471,10 +476,12 @@ class _ObjectRenderer:
             f'#[type_key = "{self.type_key}"]',
             *(["#[type_final]"] if self.info.is_final else []),
             f"pub struct {self.obj_struct} {{",
-            f"    base: {base},",  # a reflected `base` field becomes `base_` 
(`rust_ident`)
+            f"    base_: {base},",  # a reflected `base_` is rejected 
(`rust_ident`)
         ]
+        summary = [f"/// The object behind [`{self.leaf}`].", "///"]
         if not verdict.is_complete:
             return [
+                *summary,
                 f"/// Opaque: {verdict.detail}. Fields are read through the C 
ABI getters.",
                 *header,
                 "}",
@@ -483,8 +490,10 @@ class _ObjectRenderer:
         for field in sorted(self.info.fields, key=lambda f: f.offset or 0):
             mirror = self._field_mirror(self.type_key, field, self.imports)
             assert mirror is not None  # the verdict already ran the 
renderability check
-            members.append(f"    pub {rust_ident(field.name)}: {mirror},")
+            vis = self._visibility(f"{self.type_key}.{field.name}")
+            members.append(f"    {vis} {rust_ident(field.name)}: {mirror},")
         return [
+            *summary,
             f"/// Complete: {verdict.detail}.",
             *_allow_non_snake_case(rust_ident(f.name) for f in 
self.info.fields),
             *header,
@@ -499,34 +508,59 @@ class _ObjectRenderer:
 
     # --- allocators --------------------------------------------------------
 
-    def _allocator_params(self, key: str, info: ObjectInfo) -> list[tuple[str, 
str]]:
-        """``(field, type)`` of every physical field root to leaf, as 
``<key>Obj::new`` takes."""
+    def _allocator_params(
+        self, key: str, info: ObjectInfo, narrowed: frozenset[str] = 
frozenset()
+    ) -> list[tuple[str, str]]:
+        """``(field, type)`` of every physical field root to leaf, as 
``<key>Obj::new`` takes.
+
+        ``narrowed`` names the fields a subtype of ``key`` narrows with a 
``field`` directive.
+        """
         parent = info.parent_type_key
         inherited: list[tuple[str, str]] = []
         if parent is not None and self._provider(parent) != "crate":
-            inherited = self._allocator_params(parent, 
object_info_from_type_key(parent))
-        return self._level_params(key, info, inherited)
+            inherited = self._allocator_params(
+                parent, object_info_from_type_key(parent), narrowed | 
self._narrowed_names(key)
+            )
+        return self._level_params(key, info, inherited, narrowed)
 
     def _level_params(
-        self, key: str, info: ObjectInfo, inherited: list[tuple[str, str]]
+        self,
+        key: str,
+        info: ObjectInfo,
+        inherited: list[tuple[str, str]],
+        narrowed: frozenset[str] = frozenset(),
     ) -> list[tuple[str, str]]:
         """Extend the parent's parameters with ``key``'s own fields by offset.
 
         A ``field`` directive on an inherited field narrows that parameter; the
         body hands it to the parent with ``.into()``.
         """
-        params = [(name, self._narrowed(key, name, rust_type)) for name, 
rust_type in inherited]
+
+        # A type a subtype narrows away is not spelled in this file: don't 
import it.
+        def imports(name: str) -> RustImports:
+            return RustImports() if name in narrowed else self.imports
+
+        params = [
+            (name, self._narrowed(key, name, rust_type, imports(name)))
+            for name, rust_type in inherited
+        ]
         for field in sorted(info.fields, key=lambda f: f.offset or 0):
-            mirror = self._field_mirror(key, field, self.imports)
+            mirror = self._field_mirror(key, field, imports(field.name))
             assert mirror is not None  # complete: every field along the chain 
has a mirror
             params.append((field.name, mirror))
         return params
 
-    def _narrowed(self, key: str, field_name: str, rust_type: str) -> str:
+    def _narrowed_names(self, key: str) -> frozenset[str]:
+        """Name the inherited fields ``key`` narrows with a ``field`` 
directive."""
+        prefix = f"{key}."
+        targets = self.imports.directives.field_types
+        return frozenset(t[len(prefix) :] for t in targets if 
t.startswith(prefix))
+
+    def _narrowed(self, key: str, field_name: str, rust_type: str, imports: 
RustImports) -> str:
         override = 
self.imports.directives.field_types.get(f"{key}.{field_name}")
         if override is None:
             return rust_type
-        return self.imports.record(override) if "::" in override else override
+        return imports.record(override) if "::" in override else override
 
     def _fn_lines(
         self,
@@ -569,7 +603,9 @@ class _ObjectRenderer:
         if has_parent:
             parent = self.info.parent_type_key
             assert parent is not None
-            inherited = self._allocator_params(parent, 
object_info_from_type_key(parent))
+            inherited = self._allocator_params(
+                parent, object_info_from_type_key(parent), 
self._narrowed_names(self.type_key)
+            )
         params = self._level_params(self.type_key, self.info, inherited)
         to_parent = [
             f"{rust_ident(name)}.into()" if rust_type != parent_type else 
rust_ident(name)
@@ -585,8 +621,8 @@ class _ObjectRenderer:
                 *self._fn_lines(
                     "pub(crate) fn new" if self.info.is_final else "pub fn 
new",
                     params,
-                    (f"base = {base}::new", to_parent),
-                    ["base", *own],
+                    (f"base_ = {base}::new", to_parent),
+                    ["base_", *own],
                 ),
                 "}",
             ]
@@ -601,7 +637,7 @@ class _ObjectRenderer:
                     "pub fn from_fields" if custom else "pub fn new",
                     params,
                     (f"obj = {self.obj_struct}::new", forward),
-                    ["base: ObjectArc::new(obj)"],
+                    ["data_: ObjectArc::new(obj)"],
                     generic=True,
                 ),
                 "}",
@@ -636,7 +672,7 @@ class _ObjectRenderer:
         shallow_copy = self.imports.record("tvm_ffi::ShallowCopy")
         own = [rust_ident(f.name) for f in sorted(self.info.fields, key=lambda 
f: f.offset or 0)]
         init = [
-            f"base: {shallow_copy}::shallow_copy(&self.base)",
+            f"base_: {shallow_copy}::shallow_copy(&self.base_)",
             *[f"{name}: Clone::clone(&self.{name})" for name in own],
         ]
         return [
@@ -660,7 +696,7 @@ class _ObjectRenderer:
         unsafe_init = self.imports.record("tvm_ffi::UnsafeInit")
         addr = self.imports.record("std::ptr::addr_of_mut")
         directives = self.imports.directives
-        drops = [f"{unsafe_init}::drop_defined_fields({addr}!((*this).base));"]
+        drops = 
[f"{unsafe_init}::drop_defined_fields({addr}!((*this).base_));"]
         for field in sorted(self.info.fields, key=lambda f: f.offset or 0):
             target = f"{self.type_key}.{field.name}"
             if target in directives.enums:
@@ -701,26 +737,26 @@ class _ObjectRenderer:
             self.imports.record("tvm_ffi::FieldGetter")
             self.imports.record("tvm_ffi::Result")
 
-        sections: list[list[str]] = []
-        enums = self.imports.directives.enums
-        sections += [
-            self._enum_lines(enums[f"{self.type_key}.{f.name}"])
-            for f in fields
-            if f"{self.type_key}.{f.name}" in enums
-        ]
-        sections.append(self._struct_lines(verdict, base))
-        sections.append(
+        # The reference comes first, so a `///` written above the block 
documents it.
+        sections: list[list[str]] = [
             [
                 *self._allow_missing_docs(),
                 "#[repr(C)]",
                 "#[derive(tvm_ffi::derive::ObjectRef, Clone)]",
                 f"pub struct {self.leaf} {{",
-                f"    base: ObjectArc<{self.obj_struct}>,",
+                f"    data_: ObjectArc<{self.obj_struct}>,",
                 "}",
             ]
-        )
+        ]
+        enums = self.imports.directives.enums
+        sections += [
+            self._enum_lines(enums[f"{self.type_key}.{f.name}"])
+            for f in fields
+            if f"{self.type_key}.{f.name}" in enums
+        ]
+        sections.append(self._struct_lines(verdict, base))
         if has_parent:
-            sections.append(self._deref_lines(self.obj_struct, base, "base"))
+            sections.append(self._deref_lines(self.obj_struct, base, "base_"))
         if has_accessors:
             accessors: list[str] = []
             for i, field in enumerate(fields):
@@ -807,7 +843,7 @@ def _builtin_mirror_lines(type_key: str, base: str) -> 
list[str]:
         "#[derive(tvm_ffi::derive::Object)]",
         f'#[type_key = "{type_key}"]',
         f"struct {builtin_mirror_name(type_key)} {{",
-        f"    base: {base},",
+        f"    base_: {base},",
         "}",
     ]
 
diff --git a/python/tvm_ffi/stub/rust_generator/consts.py 
b/python/tvm_ffi/stub/rust_generator/consts.py
index 6dab7590..2ae25f92 100644
--- a/python/tvm_ffi/stub/rust_generator/consts.py
+++ b/python/tvm_ffi/stub/rust_generator/consts.py
@@ -24,6 +24,7 @@ RUST_DIRECTIVE_KINDS = frozenset(
         "import-object",
         "field",
         "nullable",
+        "visibility",
         "enum",
         "opaque",
         "upcast",
@@ -45,7 +46,7 @@ RUST_TY_MAP_DEFAULTS = {
     "Array": "tvm_ffi::Array",  # the crate's own Array<T>, NOT Vec
     "List": "tvm_ffi::List",  # the crate's own List<T>, NOT Vec
     "Map": "tvm_ffi::Map",  # the crate's own Map<K, V>, NOT HashMap
-    # An object value is the `ObjectRef` handle; `Object` only appears as an 
embedded `base`.
+    # An object value is the `ObjectRef` handle; `Object` only appears as an 
embedded `base_`.
     "Object": "tvm_ffi::object::ObjectRef",
     "Tensor": "tvm_ffi::Tensor",
     "Shape": "tvm_ffi::Shape",
@@ -132,10 +133,11 @@ RUST_KEYWORDS = frozenset(
 )
 RUST_NOT_RAW_IDENTIFIERS = frozenset({"self", "Self", "super", "crate"})
 
-#: The one member name the generated structs use themselves: ``base`` is the 
parent slot of every
-#: object struct and the ``ObjectArc`` slot of every reference wrapper. A 
reflected field of that
-#: name would collide (or shadow through ``Deref``), so ``rust_ident`` spells 
it ``base_``.
-RUST_RESERVED_MEMBERS = frozenset({"base"})
+#: The members the generated structs use themselves: ``base_``, the parent's 
object in every
+#: object struct, and ``data_``, the ``ObjectArc`` of every reference. The 
trailing ``_`` keeps
+#: them apart from reflected names such as ``base`` and ``data``.
+RUST_GENERATED_MEMBERS = frozenset({"base_", "data_"})
+
 
 #: ``rustfmt``'s default ``max_width``; a wider allocator signature wraps one 
parameter per line.
 RUST_MAX_WIDTH = 100
diff --git a/python/tvm_ffi/stub/rust_generator/directives.py 
b/python/tvm_ffi/stub/rust_generator/directives.py
index c322bbc7..68f67b9f 100644
--- a/python/tvm_ffi/stub/rust_generator/directives.py
+++ b/python/tvm_ffi/stub/rust_generator/directives.py
@@ -16,11 +16,12 @@
 # under the License.
 """The Rust backend's one-line directives: payload grammar and per-file 
storage.
 
-Three address one reflected field as ``<type_key>.<field>``, three address a 
type,
+Four address one reflected field as ``<type_key>.<field>``, three address a 
type,
 and one applies to the whole file::
 
     // tvm-ffi-stubgen(field): tirx.Add.a -> PrimExpr
     // tvm-ffi-stubgen(nullable): ir.Expr.span
+    // tvm-ffi-stubgen(visibility): ir.IRModule.global_var_map_ -> pub(crate)
     // tvm-ffi-stubgen(enum): tirx.For.kind -> ForKind(i32) { Serial=0, 
Parallel=1 }
     // tvm-ffi-stubgen(opaque): ir.SourceName
     // tvm-ffi-stubgen(upcast): tirx.Add -> PrimExpr
@@ -29,7 +30,8 @@ and one applies to the whole file::
 
 ``field`` sets the field's Rust type (a name in scope, or a ``::`` path to
 ``use``); on a field inherited from an ancestor it narrows the allocator
-parameter instead. ``nullable`` wraps it in ``Option``; ``enum`` declares an 
open
+parameter instead. ``nullable`` wraps it in ``Option``; ``visibility`` makes 
the
+field and its accessor ``pub(crate)`` instead of ``pub``; ``enum`` declares an 
open
 integer newtype for it, or with ``closed`` before the member list a
 ``#[repr(<int>)]`` enum of exactly those members; ``opaque`` keeps a type 
opaque
 even when its layout is reproducible. ``upcast`` adds an upcast to a reference
@@ -72,6 +74,7 @@ class Directives:
 
     field_types: dict[str, str] = dataclasses.field(default_factory=dict)
     nullable: set[str] = dataclasses.field(default_factory=set)
+    visibility: dict[str, str] = dataclasses.field(default_factory=dict)
     enums: dict[str, EnumSpec] = dataclasses.field(default_factory=dict)
     opaque: set[str] = dataclasses.field(default_factory=set)
     upcasts: dict[str, list[str]] = dataclasses.field(default_factory=dict)
@@ -85,6 +88,11 @@ class Directives:
             self.field_types[_field_target(name, lhs, lineno)] = rust_type
         elif name == "nullable":
             self.nullable.add(_field_target(name, payload, lineno))
+        elif name == "visibility":
+            lhs, vis = _split_arrow(name, payload, lineno, "<type_key>.<field> 
-> pub(crate)")
+            if vis != "pub(crate)":  # the one non-default level so far
+                raise _invalid(name, lineno, "<type_key>.<field> -> 
pub(crate)")
+            self.visibility[_field_target(name, lhs, lineno)] = vis
         elif name == "enum":
             target, spec = _parse_enum(payload, lineno)
             self.enums[target] = spec
diff --git a/python/tvm_ffi/stub/rust_generator/utils.py 
b/python/tvm_ffi/stub/rust_generator/utils.py
index 375b217e..8f99d480 100644
--- a/python/tvm_ffi/stub/rust_generator/utils.py
+++ b/python/tvm_ffi/stub/rust_generator/utils.py
@@ -196,17 +196,16 @@ def check_tokens(lines: Iterable[str]) -> tuple[str, ...]:
 def rust_ident(name: str) -> str:
     """Spell a reflected field name in Rust.
 
-    Two naming conventions are unwrapped, and nothing else is renamed: a C++
-    member with one trailing underscore (``global_var_map_``) loses it, and a
-    dunder name (``__dict__``) loses both pairs. Keywords become raw
-    identifiers; the four that cannot, and ``base`` (the member name the
-    generated structs use themselves), get a trailing underscore.
+    The name is kept as registered: keywords become raw identifiers, and the
+    four that cannot get a trailing underscore. The generated structs' own
+    members, ``base_`` and ``data_``, are rejected as reflected names.
     """
-    if len(name) > 4 and name.startswith("__") and name.endswith("__"):
-        name = name[2:-2]
-    elif len(name) > 1 and name.endswith("_") and not name.endswith("__"):
-        name = name[:-1]
-    if name in C.RUST_NOT_RAW_IDENTIFIERS or name in C.RUST_RESERVED_MEMBERS:
+    if name in C.RUST_GENERATED_MEMBERS:
+        raise ValueError(
+            f"reflected field `{name}` collides with the member the generated 
Rust bindings "
+            f"name `{name}`; register the field under another name"
+        )
+    if name in C.RUST_NOT_RAW_IDENTIFIERS:
         return f"{name}_"
     if name in C.RUST_KEYWORDS:
         return f"r#{name}"
diff --git a/rust/tvm-ffi-macros/src/object_macros.rs 
b/rust/tvm-ffi-macros/src/object_macros.rs
index b6d3aba1..ed500c2e 100644
--- a/rust/tvm-ffi-macros/src/object_macros.rs
+++ b/rust/tvm-ffi-macros/src/object_macros.rs
@@ -530,7 +530,7 @@ pub(crate) fn expand_object_ref(
     };
 
     // The `ObjectArc<T>` slot is the first field; its name is the struct's 
choice
-    // (`data` in this crate, `base` in stubgen output, so that reflected 
fields
+    // (`data` in this crate, `data_` in stubgen output, so that reflected 
fields
     // named `data` keep their name).
     let (data_id, data_ty) = match &derive_input.data {
         syn::Data::Struct(s) => s
diff --git a/rust/tvm-ffi-macros/src/rs_object.rs 
b/rust/tvm-ffi-macros/src/rs_object.rs
index d268ea1e..90bc2cf3 100644
--- a/rust/tvm-ffi-macros/src/rs_object.rs
+++ b/rust/tvm-ffi-macros/src/rs_object.rs
@@ -107,8 +107,8 @@ impl Parse for RsObjectArgs {
     }
 }
 
-const EXPECTED_BASE: &str =
-    "the first field holds the parent's object: `base: Object` at the root, 
else `base: ParentObj`";
+const EXPECTED_BASE: &str = "the first field holds the parent's object: \
+     `base_: Object` at the root, else `base_: ParentObj`";
 
 fn expand(args: &RsObjectArgs, item: &DeriveInput) -> 
syn::Result<TokenStream2> {
     if !item.generics.params.is_empty() || 
item.generics.where_clause.is_some() {
@@ -134,16 +134,23 @@ fn expand(args: &RsObjectArgs, item: &DeriveInput) -> 
syn::Result<TokenStream2>
         }
     };
     let (base, fields) = match fields.split_first() {
-        Some((base, fields)) if base.ident.as_ref().is_some_and(|id| 
id.unraw() == "base") => {
+        Some((base, fields)) if base.ident.as_ref().is_some_and(|id| 
id.unraw() == "base_") => {
             (*base, fields)
         }
+        // The trailing `_` marks plumbing, so a reflected field can be named 
`base`.
+        Some((field, _)) if field.ident.as_ref().is_some_and(|id| id.unraw() 
== "base") => {
+            return Err(syn::Error::new_spanned(
+                field,
+                "the parent's object is named `base_`: rename `base` to 
`base_`",
+            ));
+        }
         Some((field, _)) => return Err(syn::Error::new_spanned(field, 
EXPECTED_BASE)),
         None => return Err(syn::Error::new(item.ident.span(), EXPECTED_BASE)),
     };
     if let Some(attr) = base.attrs.iter().find(|a| a.path.is_ident("reflect")) 
{
         return Err(syn::Error::new_spanned(
             attr,
-            "`base` is the parent's object, whose fields the parent reflects",
+            "`base_` is the parent's object, whose fields the parent reflects",
         ));
     }
     let base_id = &base.ident;
diff --git a/rust/tvm-ffi/src/collections/tensor.rs 
b/rust/tvm-ffi/src/collections/tensor.rs
index c22511af..1dd1b2da 100644
--- a/rust/tvm-ffi/src/collections/tensor.rs
+++ b/rust/tvm-ffi/src/collections/tensor.rs
@@ -279,7 +279,7 @@ struct TensorObjFromNDAlloc<TNDAlloc>
 where
     TNDAlloc: NDAllocator,
 {
-    base: TensorObj,
+    base_: TensorObj,
     alloc: TNDAlloc,
 }
 
@@ -290,21 +290,21 @@ unsafe impl<TNDAlloc: NDAllocator> ObjectCore for 
TensorObjFromNDAlloc<TNDAlloc>
         TensorObj::type_index()
     }
     unsafe fn object_header_mut(this: &mut Self) -> &mut 
tvm_ffi_sys::TVMFFIObject {
-        TensorObj::object_header_mut(&mut this.base)
+        TensorObj::object_header_mut(&mut this.base_)
     }
 }
 
 unsafe impl<TNDAlloc: NDAllocator> ObjectCoreWithExtraItems for 
TensorObjFromNDAlloc<TNDAlloc> {
     type ExtraItem = i64;
     fn extra_items_count(this: &Self) -> usize {
-        (this.base.dltensor.ndim * 2) as usize
+        (this.base_.dltensor.ndim * 2) as usize
     }
 }
 
 impl<TNDAlloc: NDAllocator> Drop for TensorObjFromNDAlloc<TNDAlloc> {
     fn drop(&mut self) {
         unsafe {
-            self.alloc.free_data(&self.base.dltensor);
+            self.alloc.free_data(&self.base_.dltensor);
         }
     }
 }
@@ -330,7 +330,7 @@ impl Tensor {
         TNDAlloc: NDAllocator,
     {
         let tensor_obj = TensorObjFromNDAlloc {
-            base: TensorObj {
+            base_: TensorObj {
                 object: Object::new(),
                 dltensor: DLTensor {
                     data: std::ptr::null_mut(),
@@ -348,14 +348,14 @@ impl Tensor {
             let mut obj_arc = ObjectArc::new_with_extra_items(tensor_obj);
             // The fresh allocation is unique and has not been exposed.
             let object = &mut *ObjectArc::as_mut_ptr(&mut obj_arc);
-            object.base.dltensor.shape =
+            object.base_.dltensor.shape =
                 TensorObjFromNDAlloc::extra_items(object).as_ptr() as *mut i64;
-            object.base.dltensor.strides = 
object.base.dltensor.shape.add(shape.len());
+            object.base_.dltensor.strides = 
object.base_.dltensor.shape.add(shape.len());
             let extra_items = TensorObjFromNDAlloc::extra_items_mut(object);
             extra_items[..shape.len()].copy_from_slice(shape);
             Shape::fill_strides_from_shape(shape, &mut 
extra_items[shape.len()..]);
-            let dltensor_ptr = &object.base.dltensor as *const DLTensor;
-            object.base.dltensor.data = 
object.alloc.alloc_data(&*dltensor_ptr);
+            let dltensor_ptr = &object.base_.dltensor as *const DLTensor;
+            object.base_.dltensor.data = 
object.alloc.alloc_data(&*dltensor_ptr);
             Self {
                 data: ObjectArc::from_raw(ObjectArc::into_raw(obj_arc) as *mut 
TensorObj),
             }
diff --git a/rust/tvm-ffi/src/derive.rs b/rust/tvm-ffi/src/derive.rs
index 89d54be9..7f89db37 100644
--- a/rust/tvm-ffi/src/derive.rs
+++ b/rust/tvm-ffi/src/derive.rs
@@ -54,7 +54,7 @@ pub use tvm_ffi_macros::Object;
 ///
 /// #[rs_object("my_ext.MyPair")]
 /// pub struct MyPairObj {
-///     base: Object,
+///     base_: Object,
 ///     pub first: i64,
 ///     pub second: i64,
 /// }
@@ -66,7 +66,7 @@ pub use tvm_ffi_macros::Object;
 ///
 /// #[rs_object("my_ext.MyTriple", final)]
 /// pub struct MyTripleObj {
-///     base: MyPairObj,
+///     base_: MyPairObj,
 ///     pub third: i64,
 /// }
 ///
diff --git a/rust/tvm-ffi/src/extra/mod.rs b/rust/tvm-ffi/src/extra/mod.rs
index af025a4b..192a0208 100644
--- a/rust/tvm-ffi/src/extra/mod.rs
+++ b/rust/tvm-ffi/src/extra/mod.rs
@@ -29,6 +29,7 @@ pub mod cold_init_lazy_lock;
 pub mod dispatch;
 pub mod module;
 mod structural_common;
+pub mod structural_eq_hash;
 pub mod structural_map;
 pub mod structural_mutate;
 pub mod structural_visit;
diff --git a/rust/tvm-ffi/src/extra/structural_eq_hash.rs 
b/rust/tvm-ffi/src/extra/structural_eq_hash.rs
new file mode 100644
index 00000000..d388236d
--- /dev/null
+++ b/rust/tvm-ffi/src/extra/structural_eq_hash.rs
@@ -0,0 +1,189 @@
+/*
+ * 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.
+ */
+//! Structural equality and hashing of values.
+
+use crate::reflection::AccessPath;
+use crate::{AnyView, Array, Result};
+
+/// Options of structural equality and hashing.
+///
+/// See also [`structural_equal_with`], [`structural_hash_with`] and
+/// [`get_first_structural_mismatch_with`].
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
+pub struct StructuralOptions {
+    /// Whether free variables (variables without a definition site) can be
+    /// mapped to each other.
+    pub map_free_vars: bool,
+    /// Whether to skip the data content of tensors.
+    pub skip_tensor_content: bool,
+}
+
+/// Whether two values are structurally equal: they have the same structure
+/// and content, compared recursively through containers and reflected
+/// fields, not by identity.
+///
+/// Objects compare as their [`SEqHashKind`](crate::SEqHashKind) declares.
+///
+/// # Arguments
+///
+/// * `lhs` - The left value.
+/// * `rhs` - The right value.
+///
+/// # Returns
+///
+/// Whether the values are structurally equal.
+///
+/// # Errors
+///
+/// Returns an error if an object in either value does not support structural
+/// equality.
+///
+/// # Examples
+///
+/// ```
+/// use tvm_ffi::derive::ObjectRef;
+/// use tvm_ffi::{rs_object, structural_equal, Array, Object, ObjectArc};
+///
+/// #[rs_object("my_ext.MyPair", final, s_eq_hash_kind = "TreeNode")]
+/// pub struct MyPairObj {
+///     base_: Object,
+///     pub first: i64,
+///     pub second: Array<i64>,
+/// }
+///
+/// #[derive(Clone, ObjectRef)]
+/// pub struct MyPair {
+///     data: ObjectArc<MyPairObj>,
+/// }
+///
+/// let pair = |second: Vec<i64>| {
+///     MyPair::from_object(MyPairObj::new(Object::new(), 1, 
Array::from_vec(second)))
+/// };
+/// assert!(structural_equal(&pair(vec![2, 3]), &pair(vec![2, 3]))?);
+/// assert!(!structural_equal(&pair(vec![2, 3]), &pair(vec![2, 4]))?);
+/// # Ok::<(), tvm_ffi::Error>(())
+/// ```
+pub fn structural_equal<'l, 'r>(
+    lhs: impl Into<AnyView<'l>>,
+    rhs: impl Into<AnyView<'r>>,
+) -> Result<bool> {
+    structural_equal_with(lhs, rhs, StructuralOptions::default())
+}
+
+/// As [`structural_equal`], with [`StructuralOptions`].
+pub fn structural_equal_with<'l, 'r>(
+    lhs: impl Into<AnyView<'l>>,
+    rhs: impl Into<AnyView<'r>>,
+    options: StructuralOptions,
+) -> Result<bool> {
+    let args = [
+        lhs.into(),
+        rhs.into(),
+        AnyView::from(&options.map_free_vars),
+        AnyView::from(&options.skip_tensor_content),
+    ];
+    crate::cached_global_func!("ffi.StructuralEqual")
+        .call_packed(&args)?
+        .try_into()
+}
+
+/// The structural hash of a value, consistent with [`structural_equal`]:
+/// structurally equal values have the same hash.
+///
+/// # Arguments
+///
+/// * `value` - The value to hash.
+///
+/// # Returns
+///
+/// The hash.
+///
+/// # Errors
+///
+/// Returns an error if an object in the value does not support structural
+/// hashing.
+///
+/// See also [`structural_equal`].
+pub fn structural_hash<'a>(value: impl Into<AnyView<'a>>) -> Result<u64> {
+    structural_hash_with(value, StructuralOptions::default())
+}
+
+/// As [`structural_hash`], with [`StructuralOptions`].
+pub fn structural_hash_with<'a>(
+    value: impl Into<AnyView<'a>>,
+    options: StructuralOptions,
+) -> Result<u64> {
+    let args = [
+        value.into(),
+        AnyView::from(&options.map_free_vars),
+        AnyView::from(&options.skip_tensor_content),
+    ];
+    let hash: i64 = crate::cached_global_func!("ffi.StructuralHash")
+        .call_packed(&args)?
+        .try_into()?;
+    // The hash crosses the ABI as an `i64` bit pattern.
+    Ok(hash as u64)
+}
+
+/// The first place where two values are not structurally equal, as the
+/// [`AccessPath`] of that place in each value.
+///
+/// # Arguments
+///
+/// * `lhs` - The left value.
+/// * `rhs` - The right value.
+///
+/// # Returns
+///
+/// `None` if the values are structurally equal, else the paths of the first
+/// mismatch in `lhs` and in `rhs`.
+///
+/// # Errors
+///
+/// Returns an error if an object in either value does not support structural
+/// equality.
+///
+/// See also [`structural_equal`].
+pub fn get_first_structural_mismatch<'l, 'r>(
+    lhs: impl Into<AnyView<'l>>,
+    rhs: impl Into<AnyView<'r>>,
+) -> Result<Option<(AccessPath, AccessPath)>> {
+    get_first_structural_mismatch_with(lhs, rhs, StructuralOptions::default())
+}
+
+/// As [`get_first_structural_mismatch`], with [`StructuralOptions`].
+pub fn get_first_structural_mismatch_with<'l, 'r>(
+    lhs: impl Into<AnyView<'l>>,
+    rhs: impl Into<AnyView<'r>>,
+    options: StructuralOptions,
+) -> Result<Option<(AccessPath, AccessPath)>> {
+    let args = [
+        lhs.into(),
+        rhs.into(),
+        AnyView::from(&options.map_free_vars),
+        AnyView::from(&options.skip_tensor_content),
+    ];
+    // The pair crosses the ABI as a two-item array.
+    let pair: Option<Array<AccessPath>> =
+        crate::cached_global_func!("ffi.GetFirstStructuralMismatch")
+            .call_packed(&args)?
+            .try_into()?;
+    pair.map(|pair| Ok((pair.get(0)?, pair.get(1)?)))
+        .transpose()
+}
diff --git a/rust/tvm-ffi/src/extra/structural_mutate.rs 
b/rust/tvm-ffi/src/extra/structural_mutate.rs
index e6e564d6..2d205453 100644
--- a/rust/tvm-ffi/src/extra/structural_mutate.rs
+++ b/rust/tvm-ffi/src/extra/structural_mutate.rs
@@ -94,7 +94,7 @@ struct StructuralMutatorVTable {
 /// `StructuralMutatorObj`, with no Rust traversal state.
 #[repr(C)]
 pub struct StructuralMutatorObj {
-    base: Object,
+    base_: Object,
     vtable: *const StructuralMutatorVTable,
     def_region_mode: UnsafeCell<i32>,
 }
@@ -108,7 +108,7 @@ impl Default for StructuralMutatorObj {
     #[inline(always)]
     fn default() -> Self {
         Self {
-            base: Object::new(),
+            base_: Object::new(),
             vtable: std::ptr::null(),
             def_region_mode: UnsafeCell::new(DefRegionKind::None as i32),
         }
@@ -516,7 +516,7 @@ impl<T: object::ObjectRefCore> MutationView<'_, T> {
     /// # use tvm_ffi::object::ObjectRef;
     /// # use tvm_ffi::*;
     /// # #[rs_object("my_ext.MyPair", final)]
-    /// # pub struct MyPairObj { base: Object, pub first: Array<i64>, pub 
second: Array<i64> }
+    /// # pub struct MyPairObj { base_: Object, pub first: Array<i64>, pub 
second: Array<i64> }
     /// # #[derive(Clone, tvm_ffi::derive::ObjectRef)]
     /// # pub struct MyPair { data: ObjectArc<MyPairObj> }
     /// # struct Pass;
diff --git a/rust/tvm-ffi/src/extra/structural_visit.rs 
b/rust/tvm-ffi/src/extra/structural_visit.rs
index 1b928028..e1a7eb60 100644
--- a/rust/tvm-ffi/src/extra/structural_visit.rs
+++ b/rust/tvm-ffi/src/extra/structural_visit.rs
@@ -100,7 +100,7 @@ struct StructuralVisitorVTable {
 /// `StructuralVisitorObj`.
 #[repr(C)]
 pub struct StructuralVisitorObj {
-    base: Object,
+    base_: Object,
     vtable: *const StructuralVisitorVTable,
     def_region_mode: UnsafeCell<i32>,
 }
@@ -114,7 +114,7 @@ impl Default for StructuralVisitorObj {
     #[inline(always)]
     fn default() -> Self {
         Self {
-            base: Object::new(),
+            base_: Object::new(),
             vtable: std::ptr::null(),
             def_region_mode: UnsafeCell::new(DefRegionKind::None as i32),
         }
@@ -128,7 +128,7 @@ impl Default for StructuralVisitorObj {
 #[type_index(TVMFFITypeIndex::kTVMFFIVisitInterrupt)]
 #[type_final]
 pub struct VisitInterruptObj {
-    base: Object,
+    base_: Object,
     /// Payload returned with the interrupt, or FFI `None` for no payload.
     pub value: Any,
 }
@@ -200,7 +200,7 @@ impl VisitInterrupt {
     #[inline(always)]
     pub fn new<T: Into<Any>>(payload: T) -> Self {
         Self::from_object(VisitInterruptObj {
-            base: Object::new(),
+            base_: Object::new(),
             value: payload.into(),
         })
     }
diff --git a/rust/tvm-ffi/src/extra/visit_error_context.rs 
b/rust/tvm-ffi/src/extra/visit_error_context.rs
index 3d9855e0..f33a0c7b 100644
--- a/rust/tvm-ffi/src/extra/visit_error_context.rs
+++ b/rust/tvm-ffi/src/extra/visit_error_context.rs
@@ -36,7 +36,7 @@ use crate::tvm_ffi_sys::{TVMFFIAny, TVMFFIObject, 
TVMFFITypeIndex};
 #[type_key = "ffi.VisitErrorContext"]
 #[type_final]
 pub(crate) struct VisitErrorContextObj {
-    base: Object,
+    base_: Object,
     /// Visited nodes, innermost first; an entry may be `None`.
     reverse_visit_pattern: Array<Option<ObjectRef>>,
     /// The `extra_context` the error carried before this context replaced it.
@@ -93,7 +93,7 @@ pub(crate) fn update_visit_error_context(mut error: Error, 
raw: TVMFFIAny) -> Er
             };
             if !ObjectArc::is_unique(&context) {
                 context = ObjectArc::new(VisitErrorContextObj {
-                    base: Object::new(),
+                    base_: Object::new(),
                     reverse_visit_pattern: 
context.reverse_visit_pattern.clone(),
                     prev_error_context: context.prev_error_context.clone(),
                 });
@@ -107,7 +107,7 @@ pub(crate) fn update_visit_error_context(mut error: Error, 
raw: TVMFFIAny) -> Er
             context
         }
         previous => ObjectArc::new(VisitErrorContextObj {
-            base: Object::new(),
+            base_: Object::new(),
             reverse_visit_pattern: Array::from_vec(vec![Some(node)]),
             prev_error_context: previous,
         }),
diff --git a/rust/tvm-ffi/src/lib.rs b/rust/tvm-ffi/src/lib.rs
index 72f0da38..36651f58 100644
--- a/rust/tvm-ffi/src/lib.rs
+++ b/rust/tvm-ffi/src/lib.rs
@@ -83,6 +83,10 @@ pub use crate::error::{
 };
 pub use crate::extra::dispatch::{Dispatcher, IntoDispatcher};
 pub use crate::extra::module::Module;
+pub use crate::extra::structural_eq_hash::{
+    get_first_structural_mismatch, get_first_structural_mismatch_with, 
structural_equal,
+    structural_equal_with, structural_hash, structural_hash_with, 
StructuralOptions,
+};
 pub use crate::extra::structural_map::structural_map;
 pub use crate::extra::structural_mutate::{
     structural_mutate, InplaceMode, MutationView, StructuralMutateEngine, 
StructuralMutatorBase,
@@ -229,15 +233,16 @@ pub use tvm_ffi_macros::match_any;
 /// `#[rs_object("type.Key", options...)]` registers the struct as an object
 /// type with the reflection of every field, so that structural equality and
 /// hashing, `repr`, deep copy and Python see the fields. The first field,
-/// `base`, is the parent type's object, embedded at the start of the struct:
-/// `base: Object` for a type under `ffi.Object`. Declare the managed reference
-/// with [`#[derive(ObjectRef)]`](derive::ObjectRef).
+/// `base_`, is the parent type's object, embedded at the start of the struct:
+/// `base_: Object` for a type under `ffi.Object`; the trailing `_` leaves
+/// `base` free for a reflected field. Declare the managed reference with
+/// [`#[derive(ObjectRef)]`](derive::ObjectRef).
 ///
 /// The macro adds to the struct:
 ///
 /// * `#[repr(C)]` and `Deref` to the parent's object, so `obj.field` reads the
 ///   fields of both.
-/// * `MyPairObj::new(base, fields...)`, which takes the fields in order: a
+/// * `MyPairObj::new(base_, fields...)`, which takes the fields in order: a
 ///   scalar (`bool`, an integer or a float) as its own type, so that a literal
 ///   infers as it, and any other field from anything that converts into it.
 ///
@@ -276,7 +281,7 @@ pub use tvm_ffi_macros::match_any;
 ///
 /// #[rs_object("my_ext.MyPair", final)]
 /// pub struct MyPairObj {
-///     base: Object,
+///     base_: Object,
 ///     pub first: i64,
 ///     #[reflect(seq_hash_ignore)]
 ///     pub second: tvm_ffi::String,
diff --git a/rust/tvm-ffi/src/macros.rs b/rust/tvm-ffi/src/macros.rs
index aecaa0af..13e0383d 100644
--- a/rust/tvm-ffi/src/macros.rs
+++ b/rust/tvm-ffi/src/macros.rs
@@ -355,7 +355,7 @@ macro_rules! unsafe_impl_object_ref_upcast {
 ///
 /// #[rs_object("my_ext.MyPair", final)]
 /// pub struct MyPairObj {
-///     base: Object,
+///     base_: Object,
 ///     pub first: i64,
 ///     pub second: i64,
 /// }
diff --git a/rust/tvm-ffi/src/object.rs b/rust/tvm-ffi/src/object.rs
index d16487a8..91d8f3e6 100644
--- a/rust/tvm-ffi/src/object.rs
+++ b/rust/tvm-ffi/src/object.rs
@@ -39,7 +39,7 @@ pub static TYPE_REGISTRATION: std::sync::Mutex<()> = 
std::sync::Mutex::new(());
 /// The root object type, `ffi.Object`: just the object header.
 ///
 /// An object type defined in Rust starts with its parent's object, which for
-/// a direct subtype is `base: Object`.
+/// a direct subtype is `base_: Object`.
 #[repr(C)]
 pub struct Object {
     header: TVMFFIObject,
@@ -511,7 +511,7 @@ pub unsafe trait ObjectRefCore: Sized + Clone + 
AnyCompatible {
 ///
 /// #[rs_object("my_ext.MyPair", final)]
 /// pub struct MyPairObj {
-///     base: Object,
+///     base_: Object,
 ///     pub first: i64,
 ///     pub second: i64,
 /// }
@@ -675,7 +675,7 @@ unsafe impl<T: ObjectRefAnyCompatible> AnyCompatible for T {
 ///
 /// #[rs_object("my_ext.MyPair", final)]
 /// pub struct MyPairObj {
-///     base: Object,
+///     base_: Object,
 ///     pub first: i64,
 ///     pub second: i64,
 /// }
diff --git a/rust/tvm-ffi/src/reflection/registry.rs 
b/rust/tvm-ffi/src/reflection/registry.rs
index b6a5da06..d21279d9 100644
--- a/rust/tvm-ffi/src/reflection/registry.rs
+++ b/rust/tvm-ffi/src/reflection/registry.rs
@@ -169,7 +169,7 @@ impl BitOr for FieldFlags {
 ///
 /// #[rs_object("my_ext.MyPair", final, reflection = 
MyPairObj::reflect_methods)]
 /// pub struct MyPairObj {
-///     base: Object,
+///     base_: Object,
 ///     pub first: i64,
 ///     pub second: i64,
 /// }
diff --git a/rust/tvm-ffi/tests/test_any.rs b/rust/tvm-ffi/tests/test_any.rs
index c7ae479c..fa91998b 100644
--- a/rust/tvm-ffi/tests/test_any.rs
+++ b/rust/tvm-ffi/tests/test_any.rs
@@ -360,7 +360,7 @@ fn test_any_dl_device() {
 // Fields only reachable through `Deref`, where `Any::from` is not applicable.
 #[repr(C)]
 struct FieldHolderObj {
-    base: Object,
+    base_: Object,
     lhs: String,
     rhs: Option<String>,
     count: i64,
@@ -375,7 +375,7 @@ unsafe impl ObjectCore for FieldHolderObj {
     }
     #[inline]
     unsafe fn object_header_mut(this: &mut Self) -> &mut TVMFFIObject {
-        Object::object_header_mut(&mut this.base)
+        Object::object_header_mut(&mut this.base_)
     }
 }
 
@@ -419,7 +419,7 @@ fn test_to_any_matches_from_value() {
 fn test_to_any_from_borrowed_object_field() {
     let lhs = String::from("hello world this is a long string");
     let holder = ObjectArc::new(FieldHolderObj {
-        base: Object::new(),
+        base_: Object::new(),
         lhs: lhs.clone(),
         rhs: Some(lhs.clone()),
         count: 11,
diff --git a/rust/tvm-ffi/tests/test_cast.rs b/rust/tvm-ffi/tests/test_cast.rs
index f4159806..228f3198 100644
--- a/rust/tvm-ffi/tests/test_cast.rs
+++ b/rust/tvm-ffi/tests/test_cast.rs
@@ -33,7 +33,7 @@ use tvm_ffi::*;
 #[derive(Object)]
 #[type_key = "testing.TestObjectBase"]
 struct TestBaseObj {
-    base: Object,
+    base_: Object,
     value: i64,
     // counter for recording the number of times the object is deleted
     delete_counter: Arc<AtomicU32>,
@@ -56,7 +56,7 @@ struct TestBase {
 #[type_key = "testing.TestObjectDerived"]
 #[type_final]
 struct TestDerivedObj {
-    base: TestBaseObj,
+    base_: TestBaseObj,
     extra: i64,
 }
 
@@ -79,7 +79,7 @@ fn expect_err<T>(res: Result<T>) -> Error {
 fn new_base(value: i64, delete_counter: Arc<AtomicU32>) -> TestBase {
     TestBase {
         data: ObjectArc::new(TestBaseObj {
-            base: Object::new(),
+            base_: Object::new(),
             value,
             delete_counter,
         }),
@@ -89,8 +89,8 @@ fn new_base(value: i64, delete_counter: Arc<AtomicU32>) -> 
TestBase {
 fn new_derived(value: i64, extra: i64, delete_counter: Arc<AtomicU32>) -> 
TestDerived {
     TestDerived {
         data: ObjectArc::new(TestDerivedObj {
-            base: TestBaseObj {
-                base: Object::new(),
+            base_: TestBaseObj {
+                base_: Object::new(),
                 value,
                 delete_counter,
             },
@@ -134,7 +134,7 @@ fn test_upcast_downcast_roundtrip() {
     let obj: ObjectRef = base.into();
     // downcast all the way back
     let derived2: TestDerived = obj.downcast().unwrap();
-    assert_eq!(derived2.data.base.value, 7);
+    assert_eq!(derived2.data.base_.value, 7);
     assert_eq!(derived2.data.extra, 8);
     // every step moved ownership; no extra references were created
     assert_eq!(ObjectArc::strong_count(&derived2.data), 1);
@@ -150,7 +150,7 @@ fn test_borrowed_node_cast_preserves_reference_count() {
     let strong_count = ObjectArc::strong_count(TestBase::data(&base));
 
     let derived = base.downcast_ref::<TestDerivedObj>().unwrap();
-    assert_eq!(derived.base.value, 7);
+    assert_eq!(derived.base_.value, 7);
     assert_eq!(derived.extra, 8);
     let base_node = base.downcast_ref::<TestBaseObj>().unwrap();
     assert_eq!(base_node.value, 7);
@@ -228,7 +228,7 @@ fn test_try_cast_from_any_view_preserves_runtime_subtype() {
     // Array::get calls this method directly, without check_any_strict first.
     let base = unsafe { TestBase::try_cast_from_any_view(&raw) }.unwrap();
     let derived: TestDerived = base.downcast().unwrap();
-    assert_eq!(derived.data.base.value, 3);
+    assert_eq!(derived.data.base_.value, 3);
     assert_eq!(derived.data.extra, 4);
     assert_eq!(ObjectArc::strong_count(&derived.data), 2);
     drop(derived);
diff --git a/rust/tvm-ffi/tests/test_match_any_ast_lookup.rs 
b/rust/tvm-ffi/tests/test_match_any_ast_lookup.rs
index f68d82b0..a06bee7d 100644
--- a/rust/tvm-ffi/tests/test_match_any_ast_lookup.rs
+++ b/rust/tvm-ffi/tests/test_match_any_ast_lookup.rs
@@ -30,7 +30,7 @@ use tvm_ffi::{
 #[type_child_slots = 20]
 #[type_child_slots_can_overflow = false]
 struct ExprObj {
-    base: ObjectBase,
+    base_: ObjectBase,
 }
 
 fn register_type<T: ObjectCore>() -> i32 {
@@ -48,7 +48,7 @@ macro_rules! define_expr_leaves {
             #[type_register]
             #[type_final]
             struct $object {
-                base: ExprObj,
+                base_: ExprObj,
             }
 
             #[repr(C)]
@@ -61,8 +61,8 @@ macro_rules! define_expr_leaves {
                 fn default() -> Self {
                     Self {
                         data: ObjectArc::new($object {
-                            base: ExprObj {
-                                base: ObjectBase::new(),
+                            base_: ExprObj {
+                                base_: ObjectBase::new(),
                             },
                         }),
                     }
diff --git a/rust/tvm-ffi/tests/test_object_ref_any.rs 
b/rust/tvm-ffi/tests/test_object_ref_any.rs
index c3c18582..02536d98 100644
--- a/rust/tvm-ffi/tests/test_object_ref_any.rs
+++ b/rust/tvm-ffi/tests/test_object_ref_any.rs
@@ -28,7 +28,7 @@ use tvm_ffi::{Any, AnyCompatible, AnyView, Object, 
ObjectRefCore};
 // A derived reference.
 #[rs_object("testing.object_ref_any.Pair", final)]
 pub struct PairObj {
-    base: Object,
+    base_: Object,
     pub first: i64,
     pub second: i64,
 }
diff --git a/rust/tvm-ffi/tests/test_object_reflection.rs 
b/rust/tvm-ffi/tests/test_object_reflection.rs
index cd4cc99c..e58436a0 100644
--- a/rust/tvm-ffi/tests/test_object_reflection.rs
+++ b/rust/tvm-ffi/tests/test_object_reflection.rs
@@ -36,7 +36,7 @@ use tvm_ffi_sys::{
 #[type_s_eq_hash_kind = "TreeNode"]
 #[type_reflection(PointObj::reflect_methods)]
 struct PointObj {
-    base: Object,
+    base_: Object,
     /// The x coordinate.
     ///
     /// Signed.
@@ -85,7 +85,7 @@ struct Point {
 #[type_register]
 #[type_final]
 struct Point3Obj {
-    base: PointObj,
+    base_: PointObj,
     #[reflect]
     z: i64,
     #[reflect(name = "labels")]
@@ -104,7 +104,7 @@ tvm_ffi::unsafe_impl_object_ref_upcast!(Point3 => Point);
 
 fn point_obj(x: i64, y: f64, label: &str) -> PointObj {
     PointObj {
-        base: Object::new(),
+        base_: Object::new(),
         x,
         y,
         label: label.into(),
@@ -121,7 +121,7 @@ fn point(x: i64, y: f64, label: &str) -> Point {
 fn point3(x: i64, z: i64) -> Point3 {
     Point3 {
         data: ObjectArc::new(Point3Obj {
-            base: point_obj(x, 0.5, "p3"),
+            base_: point_obj(x, 0.5, "p3"),
             z,
             tags: Array::from_vec(vec!["a".into(), "b".into()]),
             r#type: Some(point(1, 2.0, "inner")),
@@ -311,7 +311,7 @@ fn test_methods_are_called_through_the_type_table() {
 #[type_final]
 #[type_reflection(Self::reflect_methods)]
 struct TallyObj {
-    base: Object,
+    base_: Object,
     count: i64,
 }
 
@@ -343,7 +343,7 @@ fn test_types_with_only_methods_register_their_reflection() 
{
     let count = 
tvm_ffi::reflection::get_method_by_index(TallyObj::type_index(), 
"count").unwrap();
     let tally = Tally {
         data: ObjectArc::new(TallyObj {
-            base: Object::new(),
+            base_: Object::new(),
             count: 11,
         }),
     };
@@ -433,7 +433,7 @@ fn test_repr_and_deep_copy_see_reflected_fields() {
     let any = Any::from(original.clone());
     let copy = Point3::try_from(call("ffi.DeepCopy", 
&[AnyView::from(&any)])).unwrap();
     assert!(!copy.same_as(&original));
-    assert_eq!((copy.data.base.x, copy.data.z), (1, 2));
+    assert_eq!((copy.data.base_.x, copy.data.z), (1, 2));
     assert_eq!(copy.data.tags.len(), 2);
     // The nested object is copied too.
     let (inner, original_inner) = (
@@ -468,7 +468,7 @@ fn test_shallow_copy_and_finality_are_type_attributes() {
     // Applied to another type, it is a type error.
     let other = Counter {
         data: ObjectArc::new(CounterObj {
-            base: Object::new(),
+            base_: Object::new(),
             count: 0,
         }),
     };
@@ -495,7 +495,7 @@ fn test_shallow_copy_and_finality_are_type_attributes() {
 #[type_register]
 #[type_final]
 struct CounterObj {
-    base: Object,
+    base_: Object,
     count: i64,
 }
 
@@ -523,7 +523,7 @@ mod no_imports {
     #[type_key = "testing.rust.ReflectedNoImports"]
     #[type_register]
     pub struct LeafObj {
-        base: super::PointObj,
+        base_: super::PointObj,
         #[reflect]
         pub w: i64,
     }
@@ -537,7 +537,7 @@ mod no_imports {
     pub fn leaf(w: i64) -> Leaf {
         Leaf {
             data: tvm_ffi::ObjectArc::new(LeafObj {
-                base: super::point_obj(0, 0.0, ""),
+                base_: super::point_obj(0, 0.0, ""),
                 w,
             }),
         }
@@ -558,7 +558,7 @@ fn test_derives_need_nothing_in_scope() {
 #[type_register]
 #[type_reflection(Self::reflect_methods)]
 struct DuplicateMethodObj {
-    base: Object,
+    base_: Object,
 }
 
 impl DuplicateMethodObj {
diff --git a/rust/tvm-ffi/tests/test_object_registration.rs 
b/rust/tvm-ffi/tests/test_object_registration.rs
index 03403289..f7df78e4 100644
--- a/rust/tvm-ffi/tests/test_object_registration.rs
+++ b/rust/tvm-ffi/tests/test_object_registration.rs
@@ -32,7 +32,7 @@ use tvm_ffi_sys::{TVMFFIByteArray, TVMFFIGetTypeInfo, 
TVMFFITypeKeyToIndex};
 #[type_child_slots = 2]
 #[type_child_slots_can_overflow = false]
 struct RegisteredBaseObj {
-    base: Object,
+    base_: Object,
     value: i64,
 }
 
@@ -48,7 +48,7 @@ struct RegisteredBase {
 #[type_register]
 #[type_final]
 struct RegisteredLeafObj {
-    base: RegisteredBaseObj,
+    base_: RegisteredBaseObj,
     extra: i64,
 }
 
@@ -94,8 +94,8 @@ fn test_derived_object_types_are_registered_on_first_use() {
 fn test_registered_object_types_round_trip_through_any() {
     let leaf = RegisteredLeaf {
         data: ObjectArc::new(RegisteredLeafObj {
-            base: RegisteredBaseObj {
-                base: Object::new(),
+            base_: RegisteredBaseObj {
+                base_: Object::new(),
                 value: 7,
             },
             extra: 8,
@@ -119,7 +119,7 @@ macro_rules! concurrent_types {
             #[type_key = $key]
             #[type_register]
             struct $name {
-                base: Object,
+                base_: Object,
             }
         )*
     };
@@ -180,7 +180,7 @@ fn test_registration_from_threads() {
 #[derive(Object)]
 #[type_key = "testing.rust.NeverRegistered"]
 struct UnregisteredBindingObj {
-    base: Object,
+    base_: Object,
 }
 
 #[test]
diff --git a/rust/tvm-ffi/tests/test_rs_object.rs 
b/rust/tvm-ffi/tests/test_rs_object.rs
index e3dc68b8..ab334b95 100644
--- a/rust/tvm-ffi/tests/test_rs_object.rs
+++ b/rust/tvm-ffi/tests/test_rs_object.rs
@@ -33,7 +33,7 @@ use tvm_ffi_sys::{TVMFFIFieldInfo, TVMFFIGetTypeInfo, 
TVMFFISEqHashKind, TVMFFIT
     reflection = RsPointObj::reflect_methods
 )]
 pub struct RsPointObj {
-    base: Object,
+    base_: Object,
     /// The x coordinate.
     pub x: i64,
     #[reflect(writable, seq_hash_ignore, doc = "A label.")]
@@ -60,7 +60,7 @@ impl RsPointObj {
 /// A subtype of a Rust-defined type, with a private field.
 #[rs_object("testing.rust.RsPoint3", final)]
 pub struct RsPoint3Obj {
-    base: RsPointObj,
+    base_: RsPointObj,
     pub z: i64,
     pub tags: Array<tvm_ffi::String>,
     tag: i64,
@@ -76,7 +76,7 @@ pub struct RsPoint3 {
 /// A subtype without fields, which still registers its reflection.
 #[rs_object("testing.rust.RsMarker", final)]
 pub struct RsMarkerObj {
-    base: RsPointObj,
+    base_: RsPointObj,
 }
 
 #[derive(Clone, ObjectRef)]
@@ -88,7 +88,7 @@ pub struct RsMarker {
 /// Scalar fields, whose literal arguments to `new` infer as the field types.
 #[rs_object("testing.rust.RsScalars", final)]
 pub struct RsScalarsObj {
-    base: Object,
+    base_: Object,
     pub big: i64,
     pub count: u64,
     pub port: u16,
@@ -108,22 +108,26 @@ pub struct RsScalars {
 #[derive(Object)]
 #[type_key = "testing.TestCxxClassBase"]
 pub struct TestCxxClassBaseObj {
-    base: Object,
+    base_: Object,
     pub v_i64: i64,
     pub v_i32: i32,
 }
 
 impl TestCxxClassBaseObj {
     pub fn new(v_i64: i64, v_i32: i32) -> Self {
-        let base = Object::new();
-        Self { base, v_i64, v_i32 }
+        let base_ = Object::new();
+        Self {
+            base_,
+            v_i64,
+            v_i32,
+        }
     }
 }
 
 impl ShallowCopy for TestCxxClassBaseObj {
     fn shallow_copy(&self) -> Self {
         Self {
-            base: ShallowCopy::shallow_copy(&self.base),
+            base_: ShallowCopy::shallow_copy(&self.base_),
             v_i64: self.v_i64,
             v_i32: self.v_i32,
         }
@@ -133,7 +137,7 @@ impl ShallowCopy for TestCxxClassBaseObj {
 unsafe impl UnsafeInit for TestCxxClassBaseObj {
     unsafe fn drop_defined_fields(this: *mut Self) {
         unsafe {
-            
UnsafeInit::drop_defined_fields(std::ptr::addr_of_mut!((*this).base));
+            
UnsafeInit::drop_defined_fields(std::ptr::addr_of_mut!((*this).base_));
             drop_if_defined(std::ptr::addr_of_mut!((*this).v_i64));
             drop_if_defined(std::ptr::addr_of_mut!((*this).v_i32));
         }
@@ -149,7 +153,7 @@ pub struct TestCxxClassBase {
 /// A subtype of a C++ type.
 #[rs_object("testing.rust.RsCxxChild", final, s_eq_hash_kind = "TreeNode")]
 pub struct RsCxxChildObj {
-    base: TestCxxClassBaseObj,
+    base_: TestCxxClassBaseObj,
     pub name: tvm_ffi::String,
 }
 
@@ -162,7 +166,7 @@ pub struct RsCxxChild {
 /// A type that defines its own `__ffi_init__`, over which C++ generates none.
 #[rs_object("testing.rust.RsOwnInit", final, reflection = 
RsOwnInitObj::reflect_methods)]
 pub struct RsOwnInitObj {
-    base: Object,
+    base_: Object,
     pub v: i64,
 }
 
@@ -189,7 +193,7 @@ impl RsOwnInitObj {
 /// `init_off` fields, which `__ffi_init__` leaves all-zero: a value of these 
types.
 #[rs_object("testing.rust.RsInitOff", final)]
 pub struct RsInitOffObj {
-    base: Object,
+    base_: Object,
     pub v: i64,
     #[reflect(init_off)]
     pub count: i64,
@@ -205,7 +209,7 @@ pub struct RsInitOff {
 /// An `init_off` reference, which all-zero bytes leave undefined: its 
registration fails.
 #[rs_object("testing.rust.RsInitOffReference", final)]
 pub struct RsInitOffReferenceObj {
-    base: Object,
+    base_: Object,
     #[reflect(init_off)]
     pub items: Array<i64>,
 }
@@ -218,7 +222,7 @@ mod reexported {
 
     #[::tvm_ffi::rs_object("testing.rust.RsReexported", crate = ::tvm_ffi)]
     pub struct ReexportedObj {
-        base: ::tvm_ffi::Object,
+        base_: ::tvm_ffi::Object,
         pub v: i64,
     }
 
diff --git a/rust/tvm-ffi/tests/test_structural_eq_hash.rs 
b/rust/tvm-ffi/tests/test_structural_eq_hash.rs
new file mode 100644
index 00000000..1b6bb724
--- /dev/null
+++ b/rust/tvm-ffi/tests/test_structural_eq_hash.rs
@@ -0,0 +1,51 @@
+/*
+ * 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.
+ */
+use tvm_ffi::reflection::AccessKind;
+use tvm_ffi::{
+    get_first_structural_mismatch, structural_equal, structural_equal_with, 
structural_hash, Array,
+    StructuralOptions,
+};
+
+#[test]
+fn structural_functions_agree_and_locate_the_mismatch() -> tvm_ffi::Result<()> 
{
+    let a = Array::from_vec(vec![1_i64, 2, 3]);
+    let same = Array::from_vec(vec![1_i64, 2, 3]);
+    let other = Array::from_vec(vec![1_i64, 2, 4]);
+
+    assert!(structural_equal(&a, &same)?);
+    assert!(!structural_equal(&a, &other)?);
+    let options = StructuralOptions {
+        map_free_vars: true,
+        ..Default::default()
+    };
+    assert!(structural_equal_with(&a, &same, options)?);
+    assert_eq!(structural_hash(&a)?, structural_hash(&same)?);
+    assert_ne!(structural_hash(&a)?, structural_hash(&other)?);
+
+    assert!(get_first_structural_mismatch(&a, &same)?.is_none());
+    let (lhs, rhs) = get_first_structural_mismatch(&a, &other)?.unwrap();
+    for path in [lhs, rhs] {
+        let step = path.step.clone().unwrap();
+        assert_eq!(
+            (step.kind, i64::try_from(step.key.clone())?),
+            (AccessKind::ArrayItem, 2)
+        );
+    }
+    Ok(())
+}
diff --git a/rust/tvm-ffi/tests/test_structural_mutate.rs 
b/rust/tvm-ffi/tests/test_structural_mutate.rs
index 5f0a2faf..6d9a388a 100644
--- a/rust/tvm-ffi/tests/test_structural_mutate.rs
+++ b/rust/tvm-ffi/tests/test_structural_mutate.rs
@@ -191,7 +191,7 @@ impl RecursiveEntryMutator {
 #[type_register]
 #[type_s_eq_hash_kind = "DAGNode"]
 struct RemapNodeObj {
-    base: Object,
+    base_: Object,
     #[reflect]
     value: i64,
 }
@@ -207,7 +207,7 @@ struct RemapNode {
 #[type_key = "testing.rust.MutationFieldNode"]
 #[type_register]
 struct FieldNodeObj {
-    base: Object,
+    base_: Object,
     a: Array<i64>,
     b: Any,
 }
@@ -365,7 +365,7 @@ fn 
user_driven_mutator_controls_default_recursion_and_in_place_opt_in() {
 
     let key = Any::from(RemapNode {
         data: ObjectArc::new(RemapNodeObj {
-            base: Object::new(),
+            base_: Object::new(),
             value: 7,
         }),
     });
@@ -852,7 +852,7 @@ fn 
consuming_callbacks_forward_permissions_without_temporary_owners() {
     }
     let mut owner: Any = FieldNode {
         data: ObjectArc::new(FieldNodeObj {
-            base: Object::new(),
+            base_: Object::new(),
             a: Array::from_vec(vec![1_i64]),
             b: Array::from_vec(vec![2_i64]).into(),
         }),
diff --git a/tests/python/test_stubgen_rust.py 
b/tests/python/test_stubgen_rust.py
index 0cbcfa83..1ad7c48b 100644
--- a/tests/python/test_stubgen_rust.py
+++ b/tests/python/test_stubgen_rust.py
@@ -255,18 +255,16 @@ def test_render_rust_type_without_mirror(schema: 
TypeSchema) -> None:
 
 def test_rust_ident() -> None:
     assert rust_ident("value") == "value"
-    assert rust_ident("imports_") == "imports"  # C++ member convention: one 
trailing underscore
-    assert rust_ident("__dict__") == "dict"  # dunder convention: both pairs
-    assert rust_ident("__dict") == "__dict"  # neither convention: kept 
verbatim
+    assert rust_ident("imports_") == "imports_"  # names are kept as registered
+    assert rust_ident("__dict__") == "__dict__"
     assert rust_ident("_private") == "_private"
-    assert rust_ident("odd__") == "odd__"
-    assert rust_ident("_") == "_"
-    assert rust_ident("____") == "____"
     assert rust_ident("type") == "r#type"
     assert rust_ident("self") == "self_"
     assert rust_ident("crate") == "crate_"
-    assert rust_ident("base") == "base_"  # the parent slot of every generated 
object struct
-    assert rust_ident("data") == "data"  # the wrapper slot is `base`, so 
`data` is free
+    assert rust_ident("base") == "base"  # the generated members are `base_` 
and `data_`
+    for name in ("base_", "data_"):
+        with pytest.raises(ValueError, match="collides"):
+            rust_ident(name)
 
 
 def test_check_tokens_ignores_spelling() -> None:
@@ -274,7 +272,7 @@ def test_check_tokens_ignores_spelling() -> None:
         "use super::super::ir::ExprObj;",
         "/// Complete: every field is mirrored.",
         "#[derive(tvm_ffi::derive::Object)]",
-        "pub struct AddObj { base: ExprObj, pub a: i64, pub b: i64 }",
+        "pub struct AddObj { base_: ExprObj, pub a: i64, pub b: i64 }",
         "const _: () = assert!(::core::mem::size_of::<AddObj>() == 40);",
         "tvm_ffi::unsafe_impl_object_ref_upcast!(Add => 
super::super::ir::Expr);",
     ]
@@ -282,7 +280,7 @@ def test_check_tokens_ignores_spelling() -> None:
         "use crate::ir::Expr;",
         "#[derive(Object)]",
         "pub struct AddObj {",
-        "    base: crate::ir::ExprObj,",
+        "    base_: crate::ir::ExprObj,",
         "    pub a: i64,",
         "    pub b: i64,",
         "}",
@@ -349,6 +347,7 @@ def 
test_generator_declares_its_directives_and_records_imports() -> None:
         "import-object",
         "field",
         "nullable",
+        "visibility",
         "enum",
         "opaque",
         "upcast",
@@ -367,18 +366,20 @@ def 
test_generator_declares_its_directives_and_records_imports() -> None:
 # ---------------------------------------------------------------------------
 
 ROOT_EXPECTED = f"""\
+#[repr(C)]
+#[derive(tvm_ffi::derive::ObjectRef, Clone)]
+pub struct Pair {{
+    data_: ObjectArc<PairObj>,
+}}
+
+/// The object behind [`Pair`].
+///
 {NO_METADATA}
 #[repr(C)]
 #[derive(tvm_ffi::derive::Object)]
 #[type_key = "demo.Pair"]
 pub struct PairObj {{
-    base: Object,
-}}
-
-#[repr(C)]
-#[derive(tvm_ffi::derive::ObjectRef, Clone)]
-pub struct Pair {{
-    base: ObjectArc<PairObj>,
+    base_: Object,
 }}
 
 impl PairObj {{
@@ -456,7 +457,7 @@ def test_render_derived_object_same_module() -> None:
         is_final=True,
     )
     text, imports = _render(info)
-    assert "#[type_final]\npub struct AddObj {\n    base: ExprObj,\n}" in text
+    assert "#[type_final]\npub struct AddObj {\n    base_: ExprObj,\n}" in text
     assert "impl Deref for AddObj {\n    type Target = ExprObj;" in text
     assert "pub fn a(&self) -> Result<Expr> {" in text
     assert text.endswith("tvm_ffi::unsafe_impl_object_ref_upcast!(Add => 
BaseExpr, Add => Expr);")
@@ -468,7 +469,7 @@ def test_render_derived_object_cross_module() -> None:
     """A parent in another module is reached through the generated root."""
     info = _info("tirx.Add", parent="ir.Expr", ancestors=["ffi.Object", 
"ir.Expr"])
     text, imports = _render(info)
-    assert "    base: ExprObj," in text
+    assert "    base_: ExprObj," in text
     assert text.endswith("tvm_ffi::unsafe_impl_object_ref_upcast!(Add => 
Expr);")
     assert {"crate::ir::ExprObj", "crate::ir::Expr"} <= _uses(imports)
 
@@ -482,7 +483,7 @@ def test_render_object_under_builtin_parent() -> None:
         ancestors=["ffi.Object", "ffi.Enum", "ffi.IntEnum"],
     )
     text, imports = _render(info)
-    assert "    base: FfiIntEnumObj," in text
+    assert "    base_: FfiIntEnumObj," in text
     # One stand-in per builtin ancestor below `ffi.Object`, chained through 
`base`.
     assert imports.builtin_mirrors == {"ffi.Enum": "Object", "ffi.IntEnum": 
"FfiEnumObj"}
     assert "tvm_ffi::Object" in _uses(imports)
@@ -492,7 +493,7 @@ def test_render_object_under_builtin_parent() -> None:
     assert not any("Enum" in path for path in _uses(imports))
     # A child of `ffi.Object` embeds the crate's header.
     text, imports = _render(_info("demo.Root"))
-    assert "    base: Object," in text
+    assert "    base_: Object," in text
     assert imports.builtin_mirrors == {}
     # A generated parent is embedded by name.
     red = _info(
@@ -501,7 +502,7 @@ def test_render_object_under_builtin_parent() -> None:
         ancestors=["ffi.Object", "ffi.Enum", "ffi.IntEnum", "demo.Color"],
     )
     text, imports = _render(red)
-    assert "    base: ColorObj," in text
+    assert "    base_: ColorObj," in text
     assert imports.builtin_mirrors == {}
 
 
@@ -515,7 +516,7 @@ use tvm_ffi::ObjectArc;
 #[derive(tvm_ffi::derive::Object)]
 #[type_key = "ffi.Enum"]
 struct FfiEnumObj {
-    base: Object,
+    base_: Object,
 }
 
 /// Header-only stand-in for the builtin `ffi.IntEnum`; it only carries the 
ancestor depth.
@@ -524,7 +525,7 @@ struct FfiEnumObj {
 #[derive(tvm_ffi::derive::Object)]
 #[type_key = "ffi.IntEnum"]
 struct FfiIntEnumObj {
-    base: FfiEnumObj,
+    base_: FfiEnumObj,
 }
 
 /// Header-only stand-in for the builtin `ffi.StrEnum`; it only carries the 
ancestor depth.
@@ -533,7 +534,7 @@ struct FfiIntEnumObj {
 #[derive(tvm_ffi::derive::Object)]
 #[type_key = "ffi.StrEnum"]
 struct FfiStrEnumObj {
-    base: FfiEnumObj,
+    base_: FfiEnumObj,
 }"""
 
 
@@ -561,6 +562,12 @@ def test_import_section_defines_builtin_mirrors_once() -> 
None:
 
 
 ITER_VAR_EXPECTED = f"""\
+#[repr(C)]
+#[derive(tvm_ffi::derive::ObjectRef, Clone)]
+pub struct IterVar {{
+    data_: ObjectArc<IterVarObj>,
+}}
+
 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
 #[repr(transparent)]
 pub struct IterVarType(i32);
@@ -586,25 +593,21 @@ impl TryFrom<i64> for IterVarType {{
     }}
 }}
 
+/// The object behind [`IterVar`].
+///
 {NO_METADATA}
 #[repr(C)]
 #[derive(tvm_ffi::derive::Object)]
 #[type_key = "tirx.IterVar"]
 #[type_final]
 pub struct IterVarObj {{
-    base: PrimExprConvertibleObj,
-}}
-
-#[repr(C)]
-#[derive(tvm_ffi::derive::ObjectRef, Clone)]
-pub struct IterVar {{
-    base: ObjectArc<IterVarObj>,
+    base_: PrimExprConvertibleObj,
 }}
 
 impl Deref for IterVarObj {{
     type Target = PrimExprConvertibleObj;
     fn deref(&self) -> &PrimExprConvertibleObj {{
-        &self.base
+        &self.base_
     }}
 }}
 
@@ -723,12 +726,20 @@ def _add() -> ObjectInfo:
 
 
 EXPR_EXPECTED = """\
+#[repr(C)]
+#[derive(tvm_ffi::derive::ObjectRef, Clone)]
+pub struct Expr {
+    data_: ObjectArc<ExprObj>,
+}
+
+/// The object behind [`Expr`].
+///
 /// Complete: reflected fields fill [24, 40) exactly.
 #[repr(C)]
 #[derive(tvm_ffi::derive::Object)]
 #[type_key = "ir.Expr"]
 pub struct ExprObj {
-    base: Object,
+    base_: Object,
     pub span: Option<Span>,
     pub ty: Type,
 }
@@ -738,16 +749,10 @@ const _: () = {
     assert!(::core::mem::align_of::<ExprObj>() == 8);
 };
 
-#[repr(C)]
-#[derive(tvm_ffi::derive::ObjectRef, Clone)]
-pub struct Expr {
-    base: ObjectArc<ExprObj>,
-}
-
 impl ExprObj {
     pub fn new(span: Option<Span>, ty: Type) -> Self {
-        let base = Object::new();
-        Self { base, span, ty }
+        let base_ = Object::new();
+        Self { base_, span, ty }
     }
 }
 
@@ -755,14 +760,14 @@ impl Expr {
     /// Lossless complete-field allocation.
     pub fn new(span: impl Into<Option<Span>>, ty: impl Into<Type>) -> Self {
         let obj = ExprObj::new(span.into(), ty.into());
-        Self { base: ObjectArc::new(obj) }
+        Self { data_: ObjectArc::new(obj) }
     }
 }
 
 impl ShallowCopy for ExprObj {
     fn shallow_copy(&self) -> Self {
         Self {
-            base: ShallowCopy::shallow_copy(&self.base),
+            base_: ShallowCopy::shallow_copy(&self.base_),
             span: Clone::clone(&self.span),
             ty: Clone::clone(&self.ty),
         }
@@ -772,7 +777,7 @@ impl ShallowCopy for ExprObj {
 unsafe impl UnsafeInit for ExprObj {
     unsafe fn drop_defined_fields(this: *mut Self) {
         unsafe {
-            UnsafeInit::drop_defined_fields(addr_of_mut!((*this).base));
+            UnsafeInit::drop_defined_fields(addr_of_mut!((*this).base_));
             drop_if_defined(addr_of_mut!((*this).span));
             drop_if_defined(addr_of_mut!((*this).ty));
         }
@@ -780,13 +785,21 @@ unsafe impl UnsafeInit for ExprObj {
 }"""
 
 ADD_EXPECTED = """\
+#[repr(C)]
+#[derive(tvm_ffi::derive::ObjectRef, Clone)]
+pub struct Add {
+    data_: ObjectArc<AddObj>,
+}
+
+/// The object behind [`Add`].
+///
 /// Complete: reflected fields fill [40, 56) exactly.
 #[repr(C)]
 #[derive(tvm_ffi::derive::Object)]
 #[type_key = "tirx.Add"]
 #[type_final]
 pub struct AddObj {
-    base: ExprObj,
+    base_: ExprObj,
     pub a: PrimExpr,
     pub b: PrimExpr,
 }
@@ -796,23 +809,17 @@ const _: () = {
     assert!(::core::mem::align_of::<AddObj>() == 8);
 };
 
-#[repr(C)]
-#[derive(tvm_ffi::derive::ObjectRef, Clone)]
-pub struct Add {
-    base: ObjectArc<AddObj>,
-}
-
 impl Deref for AddObj {
     type Target = ExprObj;
     fn deref(&self) -> &ExprObj {
-        &self.base
+        &self.base_
     }
 }
 
 impl AddObj {
     pub(crate) fn new(span: Option<Span>, ty: PrimType, a: PrimExpr, b: 
PrimExpr) -> Self {
-        let base = ExprObj::new(span, ty.into());
-        Self { base, a, b }
+        let base_ = ExprObj::new(span, ty.into());
+        Self { base_, a, b }
     }
 }
 
@@ -825,7 +832,7 @@ impl Add {
         b: impl Into<PrimExpr>,
     ) -> Self {
         let obj = AddObj::new(span.into(), ty.into(), a.into(), b.into());
-        Self { base: ObjectArc::new(obj) }
+        Self { data_: ObjectArc::new(obj) }
     }
 }
 
@@ -854,7 +861,7 @@ def test_render_shallow_copy_that_fits_is_one_line() -> 
None:
     assert (
         "impl ShallowCopy for StmtObj {\n"
         "    fn shallow_copy(&self) -> Self {\n"
-        "        Self { base: ShallowCopy::shallow_copy(&self.base), "
+        "        Self { base_: ShallowCopy::shallow_copy(&self.base_), "
         "span: Clone::clone(&self.span) }\n"
         "    }\n"
         "}"
@@ -883,7 +890,6 @@ def test_render_complete_add_golden() -> None:
         "crate::ir::ExprObj",
         "crate::ir::Expr",
         "crate::ir::Span",
-        "crate::ir::Type",
     }
 
 
@@ -892,7 +898,7 @@ def test_render_complete_span_and_prim_type() -> None:
     text, _ = _render(_span())
     assert (
         "pub struct SpanObj {\n"
-        "    base: Object,\n"
+        "    base_: Object,\n"
         "    pub source_name: SourceName,\n"
         "    pub line: i32,\n"
         "    pub column: i32,\n"
@@ -914,29 +920,41 @@ def test_render_complete_span_and_prim_type() -> None:
     assert (
         "/// Complete: reflected fields fill [32, 40) exactly (alignment 
padding [36, 40))." in text
     )
-    assert "pub struct PrimTypeObj {\n    base: TypeObj,\n    pub dtype: 
DLDataType,\n}" in text
+    assert "pub struct PrimTypeObj {\n    base_: TypeObj,\n    pub dtype: 
DLDataType,\n}" in text
     assert "assert!(::core::mem::size_of::<PrimTypeObj>() == 40);" in text
     assert "tvm_ffi::DLDataType" in _uses(imports)
 
 
-def test_reserved_member_names_get_a_trailing_underscore() -> None:
-    """A reflected `base` or `data` field must not collide with the generated 
members."""
+def test_reflected_base_and_data_keep_their_names() -> None:
+    """Reflected names are kept (`base`, `data`, `cache_`); `visibility` makes 
one `pub(crate)`."""
     info = _info(
         "demo.Ramp",
-        (_field("base", "int", 24, 8), _field("data", "int", 32, 8)),
-        total_size=40,
+        (
+            _field("base", "int", 24, 8),
+            _field("data", "int", 32, 8),
+            _field("cache_", "int", 40, 8),
+        ),
+        total_size=48,
         is_final=True,
     )
-    text, _ = _render(info)
-    assert "    base: Object,\n    pub base_: i64,\n    pub data: i64,\n" in 
text
-    assert "    pub fn new(base_: i64, data: i64) -> Self {" in text
-    assert "        Self { base, base_, data }" in text
-    # The opaque form keeps the reflected name on the C ABI side.
-    text, _ = _render(_info("demo.Node", (_field("base", "int"),)))
+    imports = RUST.new_imports()
+    RUST.add_directive(imports, "visibility", "demo.Ramp.cache_ -> 
pub(crate)", 1)
+    text, _ = _render(info, imports)
+    assert (
+        "    base_: Object,\n    pub base: i64,\n    pub data: i64,\n    
pub(crate) cache_: i64,\n"
+    ) in text
+    assert "    pub fn new(base: i64, data: i64, cache_: i64) -> Self {" in 
text
+    assert "        Self { base_, base, data, cache_ }" in text
+    imports = RUST.new_imports()
+    RUST.add_directive(imports, "visibility", "demo.Node.cache_ -> 
pub(crate)", 1)
+    text, _ = _render(_info("demo.Node", (_field("base", "int"), 
_field("cache_", "int"))), imports)
     assert (
-        'pub fn base_(&self) -> Result<i64> {\n        
FieldGetter::new(Self::type_index(), "base")'
+        'pub fn base(&self) -> Result<i64> {\n        
FieldGetter::new(Self::type_index(), "base")'
         in text
     )
+    assert "    pub(crate) fn cache_(&self) -> Result<i64> {" in text
+    with pytest.raises(ValueError, match=r"`visibility` directive at line 2"):
+        Directives().add("visibility", "demo.Node.cache_ -> pub(super)", 2)
 
 
 def test_render_complete_enum_field() -> None:
@@ -969,6 +987,10 @@ def test_allow_missing_docs_and_closed_enum() -> None:
     RUST.add_directive(imports, "enum", "demo.Pair.kind -> Kind(i32) closed { 
A=0, B=-1 }", 2)
     text, _ = _render(info, imports)
     assert text.startswith(
+        "#[allow(missing_docs)]\n"
+        "#[repr(C)]\n"
+        "#[derive(tvm_ffi::derive::ObjectRef, Clone)]\n"
+        "pub struct Pair {\n    data_: ObjectArc<PairObj>,\n}\n\n"
         "#[allow(missing_docs)]\n"
         "#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]\n"
         "#[repr(i32)]\n"
@@ -1030,7 +1052,7 @@ def 
test_unsafe_init_drops_each_field_by_its_all_zero_bytes() -> None:
         "unsafe impl UnsafeInit for MixedObj {\n"
         "    unsafe fn drop_defined_fields(this: *mut Self) {\n"
         "        unsafe {\n"
-        "            
UnsafeInit::drop_defined_fields(addr_of_mut!((*this).base));\n"
+        "            
UnsafeInit::drop_defined_fields(addr_of_mut!((*this).base_));\n"
         "            drop_in_place(addr_of_mut!((*this).count));\n"
         "            drop_in_place(addr_of_mut!((*this).value));\n"
         "            drop_if_defined(addr_of_mut!((*this).items));\n"
@@ -1083,7 +1105,7 @@ def test_complete_big_int_field_mirrors() -> None:
     )
     text, imports = _render(info)
     assert (
-        "    base: Object,\n"
+        "    base_: Object,\n"
         "    pub value: BigInt,\n"
         "    pub maybe: Optional<BigInt>,\n"
         "    pub count: i64,\n"
@@ -1160,7 +1182,7 @@ def 
test_registry_big_int_fields_are_mirrored(big_int_holder: ObjectInfo) -> Non
     text, imports = _render(big_int_holder)
     assert (
         "pub struct BigIntHolderObj {\n"
-        "    base: Object,\n"
+        "    base_: Object,\n"
         "    pub value: BigInt,\n"
         "    pub maybe: Optional<BigInt>,\n"
         "    pub count: i64,\n"
@@ -1210,7 +1232,7 @@ def test_custom_new_renames_the_wrapper_allocator() -> 
None:
         "        b: impl Into<Expr>,\n"
         "    ) -> Self {\n"
         "        let obj = AddObj::new(span.into(), ty.into(), a.into(), 
b.into());\n"
-        "        Self { base: ObjectArc::new(obj) }\n"
+        "        Self { data_: ObjectArc::new(obj) }\n"
         "    }\n}"
     ) in text
     assert "    pub fn new(" not in text
@@ -1226,13 +1248,12 @@ def test_allocator_body_wraps_like_rustfmt() -> None:
 
     name, text = render(70)  # the `let` is exactly 100 columns
     assert f"        let obj = WObj::new({name});\n" in text
-    name, text = render(76)  # 106 columns; the call alone is exactly 100
+    name, text = render(76)  # 106 columns; the call alone and `Self { .. }` 
are exactly 100
     assert f"        let obj =\n            WObj::new({name});\n" in text
-    name, text = render(77)  # the call alone is 101 columns; `Self { .. }` is 
exactly 100
+    assert f"        Self {{ base_, {name} }}\n" in text
+    name, text = render(77)  # the call alone is 101 columns
     assert f"        let obj = WObj::new(\n            {name},\n        );\n" 
in text
-    assert f"        Self {{ base, {name} }}\n" in text
-    name, text = render(78)
-    assert f"        Self {{\n            base,\n            {name},\n        
}}\n" in text
+    assert f"        Self {{\n            base_,\n            {name},\n        
}}\n" in text
 
 
 def test_upcast_directive_adds_typed_views() -> None:
@@ -1265,7 +1286,7 @@ def test_builtin_parent_keeps_the_type_opaque() -> None:
     )
     text, _ = _render(info)
     assert "/// Opaque: parent 'ffi.Enum' is opaque (no-mirror)." in text
-    assert "    base: FfiEnumObj," in text
+    assert "    base_: FfiEnumObj," in text
     assert "const _: () =" not in text
     assert "fn new(" not in text
     # The registry fixtures under `ffi.Enum` / `ffi.IntEnum` / `ffi.StrEnum` 
follow the same rule.
@@ -1285,7 +1306,7 @@ def test_opaque_parent_keeps_the_child_opaque() -> None:
     _register(_info("ir.Expr", (_field("span", "ir.Span", 24, 8),), 
total_size=40))  # hole
     text, _ = _render(_add())
     assert "/// Opaque: parent 'ir.Expr' is opaque (uncovered-bytes)." in text
-    assert "    base: ExprObj," in text
+    assert "    base_: ExprObj," in text
     assert "pub fn a(&self) -> Result<Expr> {" in text
 
 
@@ -1366,7 +1387,7 @@ def test_registry_complete_chain_is_mirrored() -> None:
         "/// Complete: reflected fields fill [24, 40) exactly (alignment 
padding [36, 40))." in base
     )
     assert (
-        "pub struct TestCxxClassBaseObj {\n    base: Object,\n    pub v_i64: 
i64,\n    pub v_i32: i32,\n}"
+        "pub struct TestCxxClassBaseObj {\n    base_: Object,\n    pub v_i64: 
i64,\n    pub v_i32: i32,\n}"
         in base
     )
     assert "assert!(::core::mem::size_of::<TestCxxClassBaseObj>() == 40);" in 
base
@@ -1375,7 +1396,7 @@ def test_registry_complete_chain_is_mirrored() -> None:
 
     dd, imports = 
_render(object_info_from_type_key("testing.TestCxxClassDerivedDerived"))
     assert (
-        "    base: TestCxxClassDerivedObj,\n    pub v_str: String,\n    pub 
v_bool: bool,\n}" in dd
+        "    base_: TestCxxClassDerivedObj,\n    pub v_str: String,\n    pub 
v_bool: bool,\n}" in dd
     )
     # The allocator flattens the chain; a signature over 100 columns wraps.
     assert (
@@ -1388,8 +1409,8 @@ def test_registry_complete_chain_is_mirrored() -> None:
         "        v_str: String,\n"
         "        v_bool: bool,\n"
         "    ) -> Self {\n"
-        "        let base = TestCxxClassDerivedObj::new(v_i64, v_i32, v_f64, 
v_f32);\n"
-        "        Self { base, v_str, v_bool }\n"
+        "        let base_ = TestCxxClassDerivedObj::new(v_i64, v_i32, v_f64, 
v_f32);\n"
+        "        Self { base_, v_str, v_bool }\n"
         "    }\n"
         "}"
     ) in dd
@@ -1406,7 +1427,7 @@ def test_registry_complete_chain_is_mirrored() -> None:
         "impl ShallowCopy for TestCxxClassDerivedDerivedObj {\n"
         "    fn shallow_copy(&self) -> Self {\n"
         "        Self {\n"
-        "            base: ShallowCopy::shallow_copy(&self.base),\n"
+        "            base_: ShallowCopy::shallow_copy(&self.base_),\n"
         "            v_str: Clone::clone(&self.v_str),\n"
         "            v_bool: Clone::clone(&self.v_bool),\n"
         "        }\n"
@@ -1479,19 +1500,19 @@ def test_cli_init_generates_a_module_tree(tmp_path: 
Path, monkeypatch: pytest.Mo
     assert text.startswith("#![allow(dead_code, unused_imports)]\n")
     assert "use tvm_ffi::FieldGetter;" in text  # the opaque fixtures read 
through getters
     assert '#[type_key = "testing.TestCxxClassDerivedDerived"]' in text
-    assert "    base: TestCxxClassDerivedObj,\n    pub v_str: String," in text
+    assert "    base_: TestCxxClassDerivedObj,\n    pub v_str: String," in text
     assert (
         "tvm_ffi::unsafe_impl_object_ref_upcast!(TestCxxClassDerivedDerived => 
TestCxxClassBase, "
         "TestCxxClassDerivedDerived => TestCxxClassDerived);"
     ) in text
     # Builtin ancestors are mirrored once, in the import section; enum 
fixtures embed the last one.
-    assert text.count("struct FfiEnumObj {\n    base: Object,\n}") == 1
-    assert text.count("struct FfiIntEnumObj {\n    base: FfiEnumObj,\n}") == 1
-    assert text.count("struct FfiStrEnumObj {\n    base: FfiEnumObj,\n}") == 1
+    assert text.count("struct FfiEnumObj {\n    base_: Object,\n}") == 1
+    assert text.count("struct FfiIntEnumObj {\n    base_: FfiEnumObj,\n}") == 1
+    assert text.count("struct FfiStrEnumObj {\n    base_: FfiEnumObj,\n}") == 1
     assert text.index("struct FfiIntEnumObj") < 
text.index(f"{C.RUST_SYNTAX.begin} object/")
-    assert "pub struct TestEnumVariantObj {\n    base: FfiEnumObj,\n}" in text
-    assert "pub struct TestCxxIntEnumObj {\n    base: FfiIntEnumObj,\n}" in 
text
-    assert "pub struct TestCxxStrEnumObj {\n    base: FfiStrEnumObj,\n}" in 
text
+    assert "pub struct TestEnumVariantObj {\n    base_: FfiEnumObj,\n}" in text
+    assert "pub struct TestCxxIntEnumObj {\n    base_: FfiIntEnumObj,\n}" in 
text
+    assert "pub struct TestCxxStrEnumObj {\n    base_: FfiStrEnumObj,\n}" in 
text
     # Running again over the generated tree is a no-op.
     assert stub_cli.__main__() == 0
     assert (root / "testing" / "mod.rs").read_text(encoding="utf-8") == text
@@ -1566,7 +1587,7 @@ def test_cli_check_ignores_how_a_binding_is_spelled(
     for old, new in [
         ("use tvm_ffi::String;\n", ""),
         ("use std::ops::Deref;\n", "use std::ops::Deref;\nuse 
crate::testing::TestCompare;\n"),
-        ("    base: Object,\n", "    base: tvm_ffi::Object,\n"),
+        ("    base_: Object,\n", "    base_: tvm_ffi::Object,\n"),
         ("/// Complete: ", "/// Mirrored: "),
         (
             "    pub fn new(\n"
@@ -1652,7 +1673,7 @@ def test_unasked_dependencies_are_an_error() -> None:
         _render(info, declared=_declared("tirx.Add"))
     # Declared in another file of the run: reached by the `crate::` path of 
its module.
     text, imports = _render(info, declared=_declared("tirx.Add", "ir.Expr", 
"tirx.Var"))
-    assert "    base: ExprObj," in text
+    assert "    base_: ExprObj," in text
     assert "pub fn a(&self) -> Result<Var> {" in text  # same module: a bare 
name
     assert {"crate::ir::ExprObj", "crate::ir::Expr"} <= _uses(imports)
 
@@ -1702,7 +1723,7 @@ def test_blocks_are_named_by_the_module_of_their_file(
     assert stub_cli.__main__() == 0
     text = nested.read_text(encoding="utf-8")
     assert "use crate::TestCxxClassBase;\nuse crate::TestCxxClassBaseObj;\n" 
in text
-    assert "    base: TestCxxClassBaseObj," in text
+    assert "    base_: TestCxxClassBaseObj," in text
     assert (
         "tvm_ffi::unsafe_impl_object_ref_upcast!(TestCxxClassDerived => 
TestCxxClassBase);" in text
     )
@@ -1726,7 +1747,7 @@ def test_ty_map_parent_redirects_base_deref_and_upcast() 
-> None:
     imports = RustImports()
     generate_rust_object(block, ty_map, imports, Options(), info, 
_declared("tirx.Add"))
     text = "\n".join(block.lines[1:-1])
-    assert "    base: ExprObj," in text
+    assert "    base_: ExprObj," in text
     assert "impl Deref for AddObj {\n    type Target = ExprObj;" in text
     assert text.endswith("tvm_ffi::unsafe_impl_object_ref_upcast!(Add => 
Expr);")
     assert {"crate::ir::ExprObj", "crate::ir::Expr"} <= _uses(imports)
@@ -1762,8 +1783,8 @@ def test_allocator_hands_a_ty_map_parent_its_own_types() 
-> None:
     assert (
         "impl ChildObj {\n"
         "    pub(crate) fn new(name: String, kind: Kind, n: i64) -> Self {\n"
-        "        let base = BaseObj::new(name, kind.into());\n"
-        "        Self { base, n }\n"
+        "        let base_ = BaseObj::new(name, kind.into());\n"
+        "        Self { base_, n }\n"
     ) in text
     assert (
         "    pub fn new(name: impl Into<String>, kind: impl Into<Kind>, n: 
i64) -> Self {\n"
@@ -1796,7 +1817,7 @@ def 
test_stage_3_checks_dependencies_across_the_run(tmp_path: Path) -> None:
     _stage_1(info, ty_map)
     _stage_3(info, Options(dry_run=True), ty_map, {}, RUST, declared)
     text = "\n".join(line for block in info.code_blocks for line in 
block.lines)
-    assert "    base: TestCxxClassBaseObj," in text
+    assert "    base_: TestCxxClassBaseObj," in text
     assert "use crate::hand::TestCxxClassBaseObj;" in text
     # Not final: public `new`; a hand-written parent may lack `ShallowCopy`, 
so the type has none.
     assert "impl TestCxxClassDerivedObj {\n    pub fn new(" in text
@@ -1826,7 +1847,7 @@ def 
test_parent_with_the_objects_own_name_is_spelled_in_full() -> None:
     info = _info("demo.s.Frame", parent="demo.t.Frame", 
ancestors=["ffi.Object", "demo.t.Frame"])
     text, imports = _render(info)
     parent = "crate::demo::t::Frame"
-    assert f"    base: {parent}Obj," in text
+    assert f"    base_: {parent}Obj," in text
     assert f"    type Target = {parent}Obj;" in text
     assert text.endswith(f"tvm_ffi::unsafe_impl_object_ref_upcast!(Frame => 
{parent});")
     assert not any(path.startswith(f"{parent}") for path in _uses(imports))
@@ -1866,7 +1887,7 @@ def 
test_ty_map_target_in_the_files_own_module_is_not_imported() -> None:
     assert "    pub fn op(&self) -> Result<Op> {" in text
     assert "crate::ir::Op" not in _uses(imports)
     text, imports = render(_info("ir.Special", parent="ir.Op", 
ancestors=["ffi.Object", "ir.Op"]))
-    assert "    base: OpObj," in text
+    assert "    base_: OpObj," in text
     assert text.endswith("tvm_ffi::unsafe_impl_object_ref_upcast!(Special => 
Op);")
     assert not any(path.startswith("crate::") for path in _uses(imports))
     # Elsewhere the target is imported as written.

Reply via email to