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 c244bc5f [Rust] Final-audit fixes: allocation overflow, immutable 
subtypes, dtype naming, API follow-ups (#960)
c244bc5f is described below

commit c244bc5fcbaac54bbc6f0563fccf302cb570388a
Author: Tianqi Chen <[email protected]>
AuthorDate: Sat Nov 7 15:28:42 2026 -0500

    [Rust] Final-audit fixes: allocation overflow, immutable subtypes, dtype 
naming, API follow-ups (#960)
    
    Follow-up fixes to the Rust bindings after #959.
    
    Soundness:
    - `List::reserve` and list growth reject a capacity whose byte size
    exceeds `isize::MAX` with a `ValueError` (the check sits in the cold
    grow path).
    - Trailing-item layouts (`Array`, `Shape`, `Tensor`) are built with
    `Layout::array`/`Layout::extend` and panic with "capacity overflow", as
    `Vec` does; `ObjectArc::new_with_extra_items` leaks rather than drops
    the object on that panic.
    - The object derive rejects at compile time a subtype of an
    `ImmutableObject` type that is not itself immutable, and an immutable
    type that inherits a `#[reflect(writable)]` field. The Rust stub
    generator marks a subtype with no registered mutability immutable when
    its parent binding is.
    - `Tensor::from_nd_alloc` and `Tensor::from_env_alloc` panic when the
    shape length does not fit the `i32` ndim, instead of truncating it and
    overflowing the trailing shape buffer.
    - `Tensor::from_nd_alloc` computes row-major strides with wrapping
    products, as C++ does, so a zero-element tensor with large extents no
    longer panics in debug builds while the object is half built.
    
    Correctness:
    - The `Module` binding is no longer `final` (`ffi.Module` has subtypes).
    - `DLDataTypeExt::to_string` falls back to `dtype(code=.., bits=..,
    lanes=..)` for a type the runtime cannot name, so
    `Tensor::data_as_slice` returns its `TypeError` instead of panicking.
    - Generated `type_index_available` calls `::core::mem::drop`.
    
    API:
    - `DefRegionKind`, `InplaceMode`, `StructuralVisitorView` and
    `StructuralMutatorView` are re-exported at the crate root (module paths
    stay).
    - `Debug` for `Any` and `AnyView`: the type key and the repr, such as
    `Any(int: 1)`.
    
    Docs: `structural_mutate`/`structural_map` state that a panicking
    callback aborts; the crate-level FFI contracts list the structural hook
    contract; `rs_object` states that structural equality and hashing see
    the fields only under an `s_eq_hash_kind`.
    
    Source-breaking:
    - A binding or `rs_object` type whose parent is immutable must be marked
    `#[type_immutable]` / `immutable`, for example an enum type under
    `ffi.Enum`, or a Rust-defined type under an immutable IR node.
---
 docs/guides/rust_lang_guide.rst               | 16 ++++++------
 python/tvm_ffi/stub/rust_generator/codegen.py | 20 ++++++++++-----
 rust/tvm-ffi-macros/src/object_macros.rs      | 37 ++++++++++++++++++---------
 rust/tvm-ffi/src/any.rs                       | 17 ++++++++++++
 rust/tvm-ffi/src/collections/list.rs          | 18 ++++++++++---
 rust/tvm-ffi/src/collections/shape.rs         |  5 ++--
 rust/tvm-ffi/src/collections/tensor.rs        |  6 +++--
 rust/tvm-ffi/src/dtype.rs                     | 20 ++++++++++-----
 rust/tvm-ffi/src/enum_ref.rs                  |  1 +
 rust/tvm-ffi/src/extra/mod.rs                 |  8 +++---
 rust/tvm-ffi/src/extra/module.rs              |  1 -
 rust/tvm-ffi/src/extra/structural_map.rs      |  3 +++
 rust/tvm-ffi/src/extra/structural_mutate.rs   |  7 +++--
 rust/tvm-ffi/src/extra/structural_visit.rs    |  2 +-
 rust/tvm-ffi/src/lib.rs                       | 19 ++++++++++----
 rust/tvm-ffi/src/object.rs                    | 25 +++++++++++++++---
 rust/tvm-ffi/src/reflection/enum_def.rs       |  1 +
 rust/tvm-ffi/src/reflection/registry.rs       |  5 ++--
 rust/tvm-ffi/tests/test_cast.rs               |  1 +
 rust/tvm-ffi/tests/test_enum.rs               |  1 +
 rust/tvm-ffi/tests/test_match_any.rs          |  6 +++--
 21 files changed, 158 insertions(+), 61 deletions(-)

diff --git a/docs/guides/rust_lang_guide.rst b/docs/guides/rust_lang_guide.rst
index 818749de..1f4439a2 100644
--- a/docs/guides/rust_lang_guide.rst
+++ b/docs/guides/rust_lang_guide.rst
@@ -685,7 +685,7 @@ Walk handlers may take an optional trailing 
``DefRegionKind`` argument:
 .. code-block:: rust
 
    use tvm_ffi::{dispatch, structural_walk, Array, Result, WalkOrder, 
WalkResult};
-   use tvm_ffi::extra::structural_visit::DefRegionKind;
+   use tvm_ffi::DefRegionKind;
 
    #[derive(Default)]
    struct Probe {
@@ -726,7 +726,7 @@ take a trailing ``DefRegionKind`` argument:
 .. code-block:: rust
 
    use tvm_ffi::{structural_walk, Array, Object, WalkOrder, WalkResult};
-   use tvm_ffi::extra::structural_visit::DefRegionKind;
+   use tvm_ffi::DefRegionKind;
 
    let values = Array::from_vec(vec![1_i64, 2, 3]);
 
@@ -792,7 +792,7 @@ dispatchers and closures.
 .. code-block:: rust
 
    use tvm_ffi::{dispatch, structural_visit, Array, Result, VisitInterrupt};
-   use tvm_ffi::extra::structural_visit::StructuralVisitorView;
+   use tvm_ffi::StructuralVisitorView;
 
    #[derive(Default)]
    struct Stats {
@@ -832,7 +832,7 @@ argument tuple and return type supply the engine signature 
directly:
 .. code-block:: rust
 
    use tvm_ffi::{dispatch, structural_visit, AnyView, Array, Result, 
VisitInterrupt};
-   use tvm_ffi::extra::structural_visit::StructuralVisitorView;
+   use tvm_ffi::StructuralVisitorView;
 
    #[derive(Default)]
    struct Depth {
@@ -894,7 +894,7 @@ catch-all and should therefore come last:
    use tvm_ffi::{
        dispatch, structural_map, Any, Array, AnyView, Result, WalkOrder, 
UnchangedOr, Unchanged,
    };
-   use tvm_ffi::extra::structural_visit::DefRegionKind;
+   use tvm_ffi::DefRegionKind;
 
    #[derive(Default)]
    struct Increment {
@@ -988,7 +988,7 @@ engines, ``DefRegionKind`` and ``InplaceMode`` live in
 .. code-block:: rust
 
    use tvm_ffi::{dispatch, structural_mutate, Any, Array, MutationView, 
Result, UnchangedOr};
-   use tvm_ffi::extra::structural_mutate::StructuralMutatorView;
+   use tvm_ffi::StructuralMutatorView;
 
    #[derive(Default)]
    struct Stats {
@@ -1045,7 +1045,7 @@ other handler consumes it:
 .. code-block:: rust
 
    use tvm_ffi::{dispatch, structural_mutate, Any, Array, Result, UnchangedOr};
-   use tvm_ffi::extra::structural_mutate::StructuralMutatorView;
+   use tvm_ffi::StructuralMutatorView;
 
    #[derive(Default)]
    struct Increment {
@@ -1074,7 +1074,7 @@ while retaining typed scalar replacements:
 .. code-block:: rust
 
    use tvm_ffi::{dispatch, structural_mutate, Any, Array, MutationView, 
Result, UnchangedOr};
-   use tvm_ffi::extra::structural_mutate::{InplaceMode, StructuralMutatorView};
+   use tvm_ffi::{InplaceMode, StructuralMutatorView};
 
    struct Increment;
 
diff --git a/python/tvm_ffi/stub/rust_generator/codegen.py 
b/python/tvm_ffi/stub/rust_generator/codegen.py
index b31e760b..95b9a900 100644
--- a/python/tvm_ffi/stub/rust_generator/codegen.py
+++ b/python/tvm_ffi/stub/rust_generator/codegen.py
@@ -733,22 +733,30 @@ class _ObjectRenderer:
         )
         return sections
 
-    def _is_immutable(self) -> bool:
+    def _is_immutable(self, info: ObjectInfo | None = None) -> bool:
         """Whether C++ declares the class not ``_type_mutable`` and no field 
along its chain writable.
 
         Such a class only hands out const access and changes through 
copy-on-write,
         so its handles may cross threads when its fields can. A mutable class, 
one
-        whose mutability is not registered, or one with a ``def_rw`` field 
stays
-        single-threaded.
+        with a ``def_rw`` field, or one whose mutability is not registered 
stays
+        single-threaded, except that an unregistered subtype of an immutable 
class
+        is immutable too, as the derive requires.
         """
-        if self.info.is_mutable is not False:
+        info = self.info if info is None else info
+        parent = info.parent_type_key
+        if info.is_mutable is None and parent is not None:
+            if parent == C_RUST.RUST_ENUM_TYPE_KEY:
+                return True
+            if self._provider(parent) == "generated":
+                return self._is_immutable(object_info_from_type_key(parent))
+        if info.is_mutable is not False:
             return False
         chain = [
             object_info_from_type_key(key)
-            for key in self.info.ancestors
+            for key in info.ancestors
             if self._provider(key) in ("generated", "mapped")
         ]
-        return all(field.frozen for info in [*chain, self.info] for field in 
info.fields)
+        return all(field.frozen for each in [*chain, info] for field in 
each.fields)
 
     def _is_rs_parent(self, verdict: Verdict) -> bool:
         """Whether the object derives ``ShallowCopy`` and ``UnsafeInit`` 
(``#[type_rs_parent]``).
diff --git a/rust/tvm-ffi-macros/src/object_macros.rs 
b/rust/tvm-ffi-macros/src/object_macros.rs
index b2e8dd92..7d8a08bb 100644
--- a/rust/tvm-ffi-macros/src/object_macros.rs
+++ b/rust/tvm-ffi-macros/src/object_macros.rs
@@ -124,18 +124,17 @@ pub(crate) fn expand_object(
     .expect("First field must be `<base_name>: <ObjectCoreType>`");
 
     let reflection = Reflection::parse(derive_input, rs_object);
-    if type_immutable {
-        let writable = reflection
-            .iter()
-            .flat_map(|reflection| &reflection.reflected)
-            .find(|field| field.flags.iter().any(|flag| flag == "writable"));
-        if let Some(field) = writable {
-            abort!(
-                field.id,
-                "an immutable type cannot have a #[reflect(writable)] field"
-            );
-        }
+    let writable = reflection
+        .iter()
+        .flat_map(|reflection| &reflection.reflected)
+        .find(|field| field.flags.iter().any(|flag| flag == "writable"));
+    if let (true, Some(field)) = (type_immutable, writable) {
+        abort!(
+            field.id,
+            "an immutable type cannot have a #[reflect(writable)] field"
+        );
     }
+    let has_writable_field = writable.is_some();
     let immutable_impl = if type_immutable {
         quote! {
             unsafe impl #tvm_ffi_crate::object::ImmutableObject for 
#struct_name {}
@@ -282,7 +281,7 @@ pub(crate) fn expand_object(
                     };
                     if !found {
                         // Discard the error the failed lookup raised.
-                        drop(#tvm_ffi_crate::Error::from_raised());
+                        
::core::mem::drop(#tvm_ffi_crate::Error::from_raised());
                     }
                     found
                 }
@@ -323,6 +322,16 @@ pub(crate) fn expand_object(
                         !<#base_ty as 
#tvm_ffi_crate::object::ObjectCore>::TYPE_FINAL,
                         "an object type cannot derive from a final parent"
                     );
+                    ::core::assert!(
+                        #type_immutable
+                            || !<#base_ty as 
#tvm_ffi_crate::object::ObjectCore>::TYPE_IMMUTABLE,
+                        "a subtype of an immutable object type must be 
immutable"
+                    );
+                    ::core::assert!(
+                        !#type_immutable
+                            || !<#base_ty as 
#tvm_ffi_crate::object::ObjectCore>::TYPE_HAS_WRITABLE_FIELD,
+                        "an immutable type cannot inherit a 
#[reflect(writable)] field"
+                    );
                 };
             }
         }),
@@ -366,6 +375,10 @@ pub(crate) fn expand_object(
         unsafe impl #tvm_ffi_crate::object::ObjectCore for #struct_name {
             const TYPE_KEY: &'static str = #type_key;
             const TYPE_FINAL: bool = #type_final;
+            const TYPE_IMMUTABLE: bool = #type_immutable;
+            const TYPE_HAS_WRITABLE_FIELD: bool =
+                <#base_ty as 
#tvm_ffi_crate::object::ObjectCore>::TYPE_HAS_WRITABLE_FIELD
+                    || #has_writable_field;
 
             #type_index_tokens
 
diff --git a/rust/tvm-ffi/src/any.rs b/rust/tvm-ffi/src/any.rs
index 7fd54c80..a5f05243 100644
--- a/rust/tvm-ffi/src/any.rs
+++ b/rust/tvm-ffi/src/any.rs
@@ -444,6 +444,23 @@ impl std::fmt::Display for Any {
     }
 }
 
+/// Formats the type key and the `repr`, such as `AnyView(int: 1)`.
+impl std::fmt::Debug for AnyView<'_> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let key = object::type_key_of_index(self.type_index());
+        write!(f, "AnyView({key}: {self})")
+    }
+}
+
+/// Formats the type key and the `repr`, such as `Any(int: 1)`.
+impl std::fmt::Debug for Any {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let view = AnyView::from(self);
+        let key = object::type_key_of_index(view.type_index());
+        write!(f, "Any({key}: {view})")
+    }
+}
+
 /// Whether a `TVMFFIAny` cell owns everything it holds, so its owning form is 
a
 /// bitwise copy.
 #[inline]
diff --git a/rust/tvm-ffi/src/collections/list.rs 
b/rust/tvm-ffi/src/collections/list.rs
index 10bc5ff3..91241a68 100644
--- a/rust/tvm-ffi/src/collections/list.rs
+++ b/rust/tvm-ffi/src/collections/list.rs
@@ -130,6 +130,9 @@ unsafe fn write_cell<T: ContainerElement>(cell: *mut 
TVMFFIAny, value: T) {
     ptr::write(cell, raw);
 }
 
+/// The most elements a list buffer can hold: its byte size must fit in 
`isize`.
+const MAX_CAPACITY: usize = isize::MAX as usize / 
std::mem::size_of::<TVMFFIAny>();
+
 // The error paths stay out of line so the hot paths are small enough to 
inline.
 #[cold]
 #[inline(never)]
@@ -474,7 +477,7 @@ impl<T: ContainerElement> List<T> {
     /// Makes room for at least `additional` more elements without
     /// reallocating again, like `Vec::reserve`.
     pub fn reserve(&self, additional: usize) -> Result<()> {
-        let total = self.len() + additional;
+        let total = self.len().saturating_add(additional);
         if total > self.capacity() {
             self.grow_to(total)?;
         }
@@ -486,8 +489,9 @@ impl<T: ContainerElement> List<T> {
     #[inline]
     fn reserve_for(&self, extra: usize) -> Result<()> {
         let (size, capacity) = (self.len(), self.capacity());
-        if size + extra > capacity {
-            self.grow_to((capacity * 2).max(size + extra))?;
+        let needed = size.saturating_add(extra);
+        if needed > capacity {
+            self.grow_to((capacity * 2).min(MAX_CAPACITY).max(needed))?;
         }
         Ok(())
     }
@@ -497,6 +501,14 @@ impl<T: ContainerElement> List<T> {
     #[cold]
     #[inline(never)]
     fn grow_to(&self, capacity: usize) -> Result<()> {
+        if capacity > MAX_CAPACITY {
+            crate::bail!(
+                crate::error::VALUE_ERROR,
+                "List capacity {} exceeds the maximum {}",
+                capacity,
+                MAX_CAPACITY
+            );
+        }
         crate::cached_global_func!("ffi.ListReserve")
             .call_packed(&[AnyView::from(self), AnyView::from(&(capacity as 
i64))])?;
         Ok(())
diff --git a/rust/tvm-ffi/src/collections/shape.rs 
b/rust/tvm-ffi/src/collections/shape.rs
index 8cd2c776..6bbd5c43 100644
--- a/rust/tvm-ffi/src/collections/shape.rs
+++ b/rust/tvm-ffi/src/collections/shape.rs
@@ -106,7 +106,8 @@ impl Shape {
         }
     }
 
-    /// Fill `strides` with the row-major strides of `shape`.
+    /// Fill `strides` with the row-major strides of `shape`, wrapping on
+    /// overflow as C++ does: only a zero-element tensor gets that far.
     pub(crate) fn fill_strides_from_shape<T>(shape: T, strides: &mut [i64])
     where
         T: AsRef<[i64]>,
@@ -115,7 +116,7 @@ impl Shape {
         let mut stride = 1;
         for i in (0..shape.len()).rev() {
             strides[i] = stride;
-            stride *= shape[i];
+            stride = stride.wrapping_mul(shape[i]);
         }
     }
 }
diff --git a/rust/tvm-ffi/src/collections/tensor.rs 
b/rust/tvm-ffi/src/collections/tensor.rs
index d1bd2968..df39460a 100644
--- a/rust/tvm-ffi/src/collections/tensor.rs
+++ b/rust/tvm-ffi/src/collections/tensor.rs
@@ -336,10 +336,11 @@ impl Tensor {
     where
         TNDAlloc: NDAllocator,
     {
+        let ndim = i32::try_from(shape.len()).expect("tensor ndim exceeds 
i32");
         let dltensor = DLTensor {
             data: std::ptr::null_mut(),
             device,
-            ndim: shape.len() as i32,
+            ndim,
             dtype,
             shape: shape.as_ptr() as *mut i64,
             strides: std::ptr::null_mut(),
@@ -397,10 +398,11 @@ impl Tensor {
     /// The error the allocator raises, or a `RuntimeError` when no allocator
     /// is set.
     pub fn from_env_alloc(shape: &[i64], dtype: DLDataType, device: DLDevice) 
-> Result<Self> {
+        let ndim = i32::try_from(shape.len()).expect("tensor ndim exceeds 
i32");
         let mut prototype = DLTensor {
             data: std::ptr::null_mut(),
             device,
-            ndim: shape.len() as i32,
+            ndim,
             dtype,
             shape: shape.as_ptr() as *mut i64,
             strides: std::ptr::null_mut(),
diff --git a/rust/tvm-ffi/src/dtype.rs b/rust/tvm-ffi/src/dtype.rs
index d926fd6e..cf00c082 100644
--- a/rust/tvm-ffi/src/dtype.rs
+++ b/rust/tvm-ffi/src/dtype.rs
@@ -27,7 +27,8 @@ use tvm_ffi_sys::{TVMFFIAny, TVMFFIByteArray, 
TVMFFIDataTypeFromString, TVMFFIDa
 /// Name conversions of a `DLDataType`.
 pub trait DLDataTypeExt: Sized {
     /// Return the name of the data type, such as `"int32"`, `"float64"` or
-    /// `"int8x4"` (bits and lanes), the name every language uses for it.
+    /// `"int8x4"` (bits and lanes), the name every language uses for it, or
+    /// `"dtype(code=.., bits=.., lanes=..)"` for one the runtime cannot name.
     ///
     /// ```
     /// use tvm_ffi::{DLDataType, DLDataTypeExt};
@@ -53,13 +54,18 @@ pub trait DLDataTypeExt: Sized {
 
 impl DLDataTypeExt for DLDataType {
     fn to_string(&self) -> std::string::String {
-        unsafe {
+        let name = unsafe {
             let mut ffi_any = TVMFFIAny::new();
-            crate::check_safe_call!(TVMFFIDataTypeToString(self, &mut 
ffi_any)).unwrap();
-            let name: crate::string::String = 
crate::any::Any::from_raw_ffi_any(ffi_any)
-                .try_into()
-                .unwrap();
-            name.as_str().to_owned()
+            crate::check_safe_call!(TVMFFIDataTypeToString(self, &mut 
ffi_any)).and_then(|()| {
+                
crate::string::String::try_from(crate::any::Any::from_raw_ffi_any(ffi_any))
+            })
+        };
+        match name {
+            Ok(name) => name.as_str().to_owned(),
+            Err(_) => format!(
+                "dtype(code={}, bits={}, lanes={})",
+                self.code, self.bits, self.lanes
+            ),
         }
     }
 
diff --git a/rust/tvm-ffi/src/enum_ref.rs b/rust/tvm-ffi/src/enum_ref.rs
index eb548de1..1d41a0ee 100644
--- a/rust/tvm-ffi/src/enum_ref.rs
+++ b/rust/tvm-ffi/src/enum_ref.rs
@@ -80,6 +80,7 @@ impl EnumObj {
 /// #[type_key = "my_ext.Color"]
 /// #[type_register]
 /// #[type_final]
+/// #[type_immutable]
 /// pub struct ColorObj {
 ///     base_: EnumObj,
 /// }
diff --git a/rust/tvm-ffi/src/extra/mod.rs b/rust/tvm-ffi/src/extra/mod.rs
index bc405301..babc87b6 100644
--- a/rust/tvm-ffi/src/extra/mod.rs
+++ b/rust/tvm-ffi/src/extra/mod.rs
@@ -51,7 +51,7 @@ mod visit_error_context;
 ///
 /// ```compile_fail,E0277
 /// use tvm_ffi::Any;
-/// use tvm_ffi::extra::structural_visit::StructuralVisitorView;
+/// use tvm_ffi::StructuralVisitorView;
 /// fn retain<D>(view: &StructuralVisitorView<'_, D>) -> Any {
 ///     Any::from(view.base())
 /// }
@@ -60,7 +60,7 @@ mod visit_error_context;
 /// A node borrowed through a copied `MutationView` cannot survive consumption 
of the view:
 /// ```compile_fail,E0505
 /// use tvm_ffi::MutationView;
-/// use tvm_ffi::extra::structural_mutate::StructuralMutatorView;
+/// use tvm_ffi::StructuralMutatorView;
 /// fn invalid<D>(value: MutationView<'_>, dispatch: &mut D, mutator: &mut 
StructuralMutatorView<'_, D>) {
 ///     let view = value.as_any_view();
 ///     let copy = view;
@@ -73,7 +73,7 @@ mod visit_error_context;
 /// Moving the handle into a nested callback cannot bypass that borrow:
 /// ```compile_fail,E0505
 /// use tvm_ffi::{dispatch, structural_mutate, Any, MutationView, Result, 
UnchangedOr};
-/// use tvm_ffi::extra::structural_mutate::StructuralMutatorView;
+/// use tvm_ffi::StructuralMutatorView;
 /// struct Inner<'a>(Option<MutationView<'a>>);
 /// #[dispatch(mutate, (MutationView<'_>, &mut StructuralMutatorView<'_, 
Self>), Result<UnchangedOr<Any>>)]
 /// impl<'a> Inner<'a> {
@@ -93,7 +93,7 @@ mod visit_error_context;
 /// A copied inspection view also cannot survive low-level default mutation:
 /// ```compile_fail,E0505
 /// use tvm_ffi::{dispatch, Any, MutationView, Result, Unchanged, UnchangedOr};
-/// use tvm_ffi::extra::structural_mutate::StructuralMutatorView;
+/// use tvm_ffi::StructuralMutatorView;
 /// struct Pass;
 /// #[dispatch(mutate, (MutationView<'_>, &mut StructuralMutatorView<'_, 
Self>), Result<UnchangedOr<Any>>)]
 /// impl Pass {
diff --git a/rust/tvm-ffi/src/extra/module.rs b/rust/tvm-ffi/src/extra/module.rs
index 46a898e1..aff890dd 100644
--- a/rust/tvm-ffi/src/extra/module.rs
+++ b/rust/tvm-ffi/src/extra/module.rs
@@ -34,7 +34,6 @@ use tvm_ffi_sys::TVMFFITypeIndex as TypeIndex;
 #[derive(Object)]
 #[type_key = "ffi.Module"]
 #[type_index(unsafe TypeIndex::kTVMFFIModule)]
-#[type_final]
 pub struct ModuleObj {
     object: Object,
 }
diff --git a/rust/tvm-ffi/src/extra/structural_map.rs 
b/rust/tvm-ffi/src/extra/structural_map.rs
index 2f4872e9..db55e99f 100644
--- a/rust/tvm-ffi/src/extra/structural_map.rs
+++ b/rust/tvm-ffi/src/extra/structural_map.rs
@@ -178,6 +178,9 @@ where
 /// this function consumes `root`, an error does not return the partly mapped
 /// root to the caller.
 ///
+/// Callbacks must report failures through [`Result`] and must not panic: a 
panic
+/// reaching the non-unwinding native callback boundary aborts the process.
+///
 /// A callback must not modify an `ffi.List` or `ffi.Dict` that holds the
 /// value it receives or one of that value's ancestors: the traversal borrows
 /// their cells while the callback runs. A callback that returns an `Error`
diff --git a/rust/tvm-ffi/src/extra/structural_mutate.rs 
b/rust/tvm-ffi/src/extra/structural_mutate.rs
index 749f3b74..54913571 100644
--- a/rust/tvm-ffi/src/extra/structural_mutate.rs
+++ b/rust/tvm-ffi/src/extra/structural_mutate.rs
@@ -519,7 +519,7 @@ impl<T: object::ObjectRefCore> MutationView<'_, T> {
     ///
     /// ```
     /// # use tvm_ffi::*;
-    /// # use tvm_ffi::extra::structural_mutate::StructuralMutatorView;
+    /// # use tvm_ffi::StructuralMutatorView;
     /// # #[rs_object("my_ext.MyPair", final)]
     /// # pub struct MyPairObj { base_: Object, pub first: Array<i64>, pub 
second: Array<i64> }
     /// # #[derive(Clone, tvm_ffi::derive::ObjectRef)]
@@ -794,6 +794,9 @@ impl<D, Base: StructuralMutatorBase> 
StructuralMutatorView<'_, D, Base> {
 /// in-place mutation. A matching callback supplies the final value; unmatched
 /// values use default mutation.
 ///
+/// Callbacks must report failures through [`Result`] and must not panic: a 
panic
+/// reaching the non-unwinding native callback boundary aborts the process.
+///
 /// A callback must not modify an `ffi.List` or `ffi.Dict` that holds the
 /// value it receives or one of that value's ancestors: the traversal borrows
 /// their cells while the callback runs. A callback that returns an `Error`
@@ -815,7 +818,7 @@ impl<D, Base: StructuralMutatorBase> 
StructuralMutatorView<'_, D, Base> {
 ///
 /// ```
 /// use tvm_ffi::{dispatch, structural_mutate, Any, Array, MutationView, 
Result, UnchangedOr};
-/// use tvm_ffi::extra::structural_mutate::StructuralMutatorView;
+/// use tvm_ffi::StructuralMutatorView;
 ///
 /// struct Double;
 ///
diff --git a/rust/tvm-ffi/src/extra/structural_visit.rs 
b/rust/tvm-ffi/src/extra/structural_visit.rs
index 910b45f0..74b70293 100644
--- a/rust/tvm-ffi/src/extra/structural_visit.rs
+++ b/rust/tvm-ffi/src/extra/structural_visit.rs
@@ -447,7 +447,7 @@ impl<D, Base: StructuralVisitorBase> 
StructuralVisitorView<'_, D, Base> {
 /// ```
 /// # fn main() -> tvm_ffi::Result<()> {
 /// use tvm_ffi::{dispatch, structural_visit, AnyView, Array, Result, 
VisitInterrupt};
-/// use tvm_ffi::extra::structural_visit::StructuralVisitorView;
+/// use tvm_ffi::StructuralVisitorView;
 ///
 /// struct Sum(i64);
 /// #[dispatch(visit, (AnyView<'_>, &mut StructuralVisitorView<'_, Self>), 
Result<Option<VisitInterrupt>>)]
diff --git a/rust/tvm-ffi/src/lib.rs b/rust/tvm-ffi/src/lib.rs
index 577d5e94..ed3fd393 100644
--- a/rust/tvm-ffi/src/lib.rs
+++ b/rust/tvm-ffi/src/lib.rs
@@ -53,6 +53,9 @@
 //! * A [`TensorView`] received as a function argument is valid only during
 //!   the call, and so is an [`Any`] holding a `DLTensor*` converted from it
 //!   or from an [`AnyView`].
+//! * A registered structural visit or mutate hook uses the visitor or
+//!   mutator only through its arguments, during the call, and does not keep
+//!   it.
 #![warn(missing_docs)]
 
 // Lets macro-generated `::tvm_ffi` paths resolve inside this crate too.
@@ -105,8 +108,12 @@ pub use crate::extra::structural_eq_hash::{
     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, MutationView};
-pub use crate::extra::structural_visit::{structural_visit, VisitInterrupt};
+pub use crate::extra::structural_mutate::{
+    structural_mutate, InplaceMode, MutationView, StructuralMutatorView,
+};
+pub use crate::extra::structural_visit::{
+    structural_visit, DefRegionKind, StructuralVisitorView, VisitInterrupt,
+};
 pub use crate::extra::structural_walk::{structural_walk, IntoWalkResult, 
WalkOrder, WalkResult};
 pub use crate::extra::unchanged::{Unchanged, UnchangedOr};
 pub use crate::function::Function;
@@ -262,10 +269,12 @@ pub use tvm_ffi_macros::match_any;
 /// * `final` - The type has no subtypes.
 /// * `immutable` - Shared objects of the type are never mutated, so its
 ///   references cross threads when its fields can
-///   ([`ImmutableObject`](crate::object::ImmutableObject)); a field cannot be
-///   `writable`.
+///   ([`ImmutableObject`](crate::object::ImmutableObject)); a field, own or
+///   inherited, cannot be `writable`. Every subtype must be `immutable` too.
 /// * `s_eq_hash_kind = "TreeNode"` - The 
[`SEqHashKind`](crate::reflection::SEqHashKind) of structural
-///   equality; by default the parent's.
+///   equality; by default the parent's. Structural equality and hashing see
+///   the fields only when the type or an ancestor has one, and otherwise fail
+///   with a `TypeError`.
 /// * `child_slots = N`, `child_slots_can_overflow = bool` - Type indices kept
 ///   for subtypes; 0 and `true` by default.
 /// * `reflection = path` - A `fn(&mut ObjectDef<Self>)` that registers the
diff --git a/rust/tvm-ffi/src/object.rs b/rust/tvm-ffi/src/object.rs
index bc97fa17..70234aaf 100644
--- a/rust/tvm-ffi/src/object.rs
+++ b/rust/tvm-ffi/src/object.rs
@@ -164,6 +164,9 @@ impl<T: ObjectCore> ObjectArc<T> {
     where
         T: ObjectCoreWithExtraItems<ExtraItem = U>,
     {
+        // `data` may describe items it does not hold yet, so a panic before
+        // the write leaks it rather than dropping it.
+        let data = std::mem::ManuallyDrop::new(data);
         unsafe {
             // ensure strict alignment requirements
             // so we can have { T, U*extra_items } layout
@@ -176,7 +179,7 @@ impl<T: ObjectCore> ObjectArc<T> {
                 std::alloc::handle_alloc_error(layout);
             }
             let ptr = raw_data_ptr as *mut T;
-            std::ptr::write(ptr, data);
+            std::ptr::write(ptr, std::mem::ManuallyDrop::into_inner(data));
             // now override the header directly
             std::ptr::write(
                 ptr as *mut TVMFFIObject,
@@ -340,7 +343,9 @@ impl<T: ObjectCore> Clone for ObjectArc<T> {
 /// copy-on-write, in place only on a uniquely held instance (one strong
 /// reference, no weak ones). This holds for every type not registered as
 /// mutable (`_type_mutable`), in any language. Every subclass must also be
-/// immutable while shared, with thread-safe fields.
+/// immutable while shared, with thread-safe fields; the derives reject a
+/// subtype that is not `immutable` at compile time. A hand-written impl must
+/// also set `ObjectCore::TYPE_IMMUTABLE` and keep every subtype immutable.
 pub unsafe trait ImmutableObject: ObjectCore {}
 
 // SAFETY: `T` is never mutated while shared, its fields are thread-safe, and 
the
@@ -390,6 +395,12 @@ pub unsafe trait ObjectCore: Sized + 'static {
     /// A final type has no separately registered object-system subtype.
     #[doc(hidden)]
     const TYPE_FINAL: bool = false;
+    /// Whether `Self` implements [`ImmutableObject`].
+    #[doc(hidden)]
+    const TYPE_IMMUTABLE: bool = false;
+    /// Whether `Self` or an ancestor declares a `writable` reflected field.
+    #[doc(hidden)]
+    const TYPE_HAS_WRITABLE_FIELD: bool = false;
     /// The runtime type index of this type.
     fn type_index() -> i32;
     /// Whether [`type_index`](Self::type_index) can return now: false while
@@ -1162,10 +1173,16 @@ pub mod unsafe_ {
 
     /// Layout of a `T` followed by `count` extra items, kept at least one 
word past the
     /// header: once a weak reference outlives the data, the deleter stores 
`count` there.
+    /// Panics with "capacity overflow" (like `Vec`) when the size does not 
fit in `isize`.
     pub(crate) fn extra_items_layout<T, U>(count: usize) -> std::alloc::Layout 
{
-        let size = std::mem::size_of::<T>() + count * std::mem::size_of::<U>();
+        use std::alloc::Layout;
         let min_size = std::mem::size_of::<TVMFFIObject>() + 
std::mem::size_of::<u64>();
-        std::alloc::Layout::from_size_align(size.max(min_size), 
std::mem::align_of::<T>()).unwrap()
+        Layout::array::<U>(count)
+            .and_then(|items| Layout::new::<T>().extend(items))
+            .and_then(|(layout, _)| {
+                Layout::from_size_align(layout.size().max(min_size), 
layout.align())
+            })
+            .expect("capacity overflow")
     }
 
     pub(crate) unsafe extern "C" fn object_deleter_for_new_with_extra_items<T, 
U>(
diff --git a/rust/tvm-ffi/src/reflection/enum_def.rs 
b/rust/tvm-ffi/src/reflection/enum_def.rs
index 46b00185..15996a1d 100644
--- a/rust/tvm-ffi/src/reflection/enum_def.rs
+++ b/rust/tvm-ffi/src/reflection/enum_def.rs
@@ -60,6 +60,7 @@ use crate::{Any, AnyView, EnumRef, Error, List, Result, 
String, TYPE_ERROR, VALU
 /// #[type_key = "my_ext.Color"]
 /// #[type_register]
 /// #[type_final]
+/// #[type_immutable]
 /// pub struct ColorObj {
 ///     base_: EnumObj,
 /// }
diff --git a/rust/tvm-ffi/src/reflection/registry.rs 
b/rust/tvm-ffi/src/reflection/registry.rs
index 6d0fd3cf..36d61ed2 100644
--- a/rust/tvm-ffi/src/reflection/registry.rs
+++ b/rust/tvm-ffi/src/reflection/registry.rs
@@ -435,8 +435,9 @@ impl<N: ObjectCore> ObjectDef<N> {
             return Ok(());
         }
         if let Some(creator) = self.creator {
-            // Objects from this creator start zeroed: another language must 
set
-            // every field before Rust code reads it.
+            // Objects from this creator start zero-filled, in the `UnsafeInit`
+            // state: whoever creates one, in any language, must set every
+            // field before it is used.
             let new = Function::from_typed(move || -> Result<ObjectRef> {
                 let mut handle: TVMFFIObjectHandle = std::ptr::null_mut();
                 crate::check_safe_call!(unsafe { creator(&mut handle) })?;
diff --git a/rust/tvm-ffi/tests/test_cast.rs b/rust/tvm-ffi/tests/test_cast.rs
index f509e8e3..a1399c59 100644
--- a/rust/tvm-ffi/tests/test_cast.rs
+++ b/rust/tvm-ffi/tests/test_cast.rs
@@ -55,6 +55,7 @@ struct TestBase {
 #[type_key = "testing.rust.CastDerived"]
 #[type_register]
 #[type_final]
+#[type_immutable]
 struct TestDerivedObj {
     base_: TestBaseObj,
     extra: i64,
diff --git a/rust/tvm-ffi/tests/test_enum.rs b/rust/tvm-ffi/tests/test_enum.rs
index 524e10e5..7aef0428 100644
--- a/rust/tvm-ffi/tests/test_enum.rs
+++ b/rust/tvm-ffi/tests/test_enum.rs
@@ -29,6 +29,7 @@ use tvm_ffi::{EnumRef, ObjectArc, ObjectRefCore, Result, 
VALUE_ERROR};
 #[derive(Object)]
 #[type_key = "testing.TestEnumVariant"]
 #[type_final]
+#[type_immutable]
 struct TestEnumVariantObj {
     base_: EnumObj,
 }
diff --git a/rust/tvm-ffi/tests/test_match_any.rs 
b/rust/tvm-ffi/tests/test_match_any.rs
index 463889dd..8999cd80 100644
--- a/rust/tvm-ffi/tests/test_match_any.rs
+++ b/rust/tvm-ffi/tests/test_match_any.rs
@@ -20,7 +20,9 @@
 use std::any::TypeId;
 
 use tvm_ffi::match_any_internal::{ArmId, LeafLookupTable, LeafPatternMetadata, 
LeafPatternProbe};
-use tvm_ffi::{match_any, Any, AnyView, Array, Function, Map, Module, Shape, 
Tensor, TypeIndex};
+use tvm_ffi::{
+    match_any, Any, AnyView, Array, Function, Map, Module, Shape, Tensor, 
TypeIndex, VisitInterrupt,
+};
 
 type CustomModuleMatcher = Module;
 
@@ -416,7 +418,7 @@ fn 
direct_lookup_table_maps_runtime_indices_to_local_arm_ids() {
 // `(&probe)` mirrors the receiver `match_any!` generates for autoref 
specialization.
 #[allow(clippy::needless_borrow)]
 fn metadata_only_accepts_exact_leaf_patterns() {
-    type Leaf = (Module, ());
+    type Leaf = (VisitInterrupt, ());
     let leaf = LeafPatternProbe::<Leaf>::new();
     let mut type_indices = [0; 1];
     assert!((&leaf).leaf_pattern_list_id().is_some());

Reply via email to