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 e9a4ab04 [Rust] Group Object derive attributes under #[object(...)]; 
std-shaped Array/List accessors (#962)
e9a4ab04 is described below

commit e9a4ab04944de2a8dc55d554dd7ceda76a76d8e3
Author: Tianqi Chen <[email protected]>
AuthorDate: Sun Nov 8 09:07:15 2026 -0500

    [Rust] Group Object derive attributes under #[object(...)]; std-shaped 
Array/List accessors (#962)
    
    This PR makes two Rust API consistency changes.
    
    **`#[derive(Object)]` options grouped under `#[object(...)]`.** The ten
    flat `#[type_*]` helper attributes become one attribute, parsed like
    `#[object_ref(...)]`:
    
    ```rust
    #[object(type_key = "my_ext.Point", register, final, immutable, child_slots 
= 4)]
    #[object(type_key = "ffi.String", unsafe type_index = 
TypeIndex::kTVMFFIStr)]
    ```
    
    - `type_key` and `type_index` keep their names. The other options drop
    the `type_` prefix: `final`, `immutable`, `register`, `rs_parent`,
    `child_slots`, `child_slots_can_overflow`, `s_eq_hash_kind`,
    `reflection`.
    - `unsafe` is allowed only before `type_index`. An unknown option is an
    error that lists the valid ones.
    - The flat attributes are removed. `#[rs_object]`, the Rust stub
    generator, the in-tree users and the `derive::Object` docs now use the
    new form.
    
    **`Array` and `List` accessors follow std shapes.** A method that can
    genuinely fail returns `Result`: a type mismatch, an FFI call or growth.
    Out-of-range is then an `IndexError` inside it. A method that can fail
    only on its bounds follows std instead.
    
    - `Array::get` returns `Option<T>`, since elements are checked when the
    array is built.
    - `Array::set`, `Array::insert` and `List::set` return `()` and panic on
    an out-of-range index, as `Vec` does.
    - `List::remove_range` takes `impl RangeBounds<usize>`, returns `()` and
    panics on an out-of-range or inverted range, as `Vec::drain` does.
---
 docs/guides/rust_lang_guide.rst                    |   2 +-
 .../rust/src/generated/rust_stubgen/mod.rs         |   4 +-
 python/tvm_ffi/stub/rust_generator/codegen.py      |  21 ++-
 rust/tvm-ffi-macros/src/lib.rs                     |  17 +-
 rust/tvm-ffi-macros/src/object_macros.rs           | 209 ++++++++++++---------
 rust/tvm-ffi-macros/src/rs_object.rs               |  18 +-
 rust/tvm-ffi-macros/src/utils.rs                   |  33 ----
 rust/tvm-ffi/src/big_int/mod.rs                    |   4 +-
 rust/tvm-ffi/src/collections/array.rs              |  76 +++-----
 rust/tvm-ffi/src/collections/list.rs               |  51 +++--
 rust/tvm-ffi/src/collections/map.rs                |   3 +-
 rust/tvm-ffi/src/collections/shape.rs              |   3 +-
 rust/tvm-ffi/src/collections/tensor.rs             |   3 +-
 rust/tvm-ffi/src/derive.rs                         |  36 +++-
 rust/tvm-ffi/src/enum_ref.rs                       |   8 +-
 rust/tvm-ffi/src/error.rs                          |   3 +-
 rust/tvm-ffi/src/extra/module.rs                   |   3 +-
 rust/tvm-ffi/src/extra/structural_eq_hash.rs       |   3 +-
 rust/tvm-ffi/src/extra/structural_mutate.rs        |   2 +-
 rust/tvm-ffi/src/extra/structural_visit.rs         |   8 +-
 rust/tvm-ffi/src/extra/visit_error_context.rs      |   3 +-
 rust/tvm-ffi/src/function.rs                       |   3 +-
 rust/tvm-ffi/src/lib.rs                            |   2 +-
 rust/tvm-ffi/src/object.rs                         |   2 +-
 rust/tvm-ffi/src/reflection/access_path.rs         |   6 +-
 rust/tvm-ffi/src/reflection/enum_def.rs            |   8 +-
 rust/tvm-ffi/src/reflection/registry.rs            |   2 +-
 rust/tvm-ffi/src/reflection/unsafe_init.rs         |   2 +-
 rust/tvm-ffi/src/string.rs                         |   6 +-
 rust/tvm-ffi/tests/test_array.rs                   |   8 +-
 rust/tvm-ffi/tests/test_cast.rs                    |  14 +-
 rust/tvm-ffi/tests/test_enum.rs                    |   4 +-
 rust/tvm-ffi/tests/test_list.rs                    |  15 +-
 rust/tvm-ffi/tests/test_match_any_ast_lookup.rs    |  14 +-
 rust/tvm-ffi/tests/test_object_reflection.rs       |  37 ++--
 rust/tvm-ffi/tests/test_object_registration.rs     |  20 +-
 rust/tvm-ffi/tests/test_rs_object.rs               |   3 +-
 rust/tvm-ffi/tests/test_structural_mutate.rs       |  17 +-
 tests/python/test_stubgen_rust.py                  |  29 ++-
 39 files changed, 341 insertions(+), 361 deletions(-)

diff --git a/docs/guides/rust_lang_guide.rst b/docs/guides/rust_lang_guide.rst
index 1f4439a2..4f476985 100644
--- a/docs/guides/rust_lang_guide.rst
+++ b/docs/guides/rust_lang_guide.rst
@@ -443,7 +443,7 @@ take all-zero bytes as a value, as a scalar, an ``Option`` 
of a reference or an
 ``Optional<T>`` does; a reference or
 a string does not, and fails the type's registration. The parent's object must
 implement ``ShallowCopy`` and ``UnsafeInit``, as ``rs_object`` types do, and so
-does a binding with ``#[type_rs_parent]`` on its ``derive(Object)``, such as a
+does a binding with ``#[object(rs_parent)]`` on its ``derive(Object)``, such 
as a
 non-final type bound by ``tvm-ffi-stubgen``.
 
 Converting Borrowed Values into ``Any``
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 b2cdae8a..ee27babc 100644
--- a/examples/rust_stubgen/rust/src/generated/rust_stubgen/mod.rs
+++ b/examples/rust_stubgen/rust/src/generated/rust_stubgen/mod.rs
@@ -74,9 +74,7 @@ impl TryFrom<i64> for PairKind {
 /// 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]
-#[type_immutable]
+#[object(type_key = "rust_stubgen.IntPair", final, immutable)]
 pub struct IntPairObj {
     base_: Object,
     pub a: i64,
diff --git a/python/tvm_ffi/stub/rust_generator/codegen.py 
b/python/tvm_ffi/stub/rust_generator/codegen.py
index d2de8b75..b8eb3f46 100644
--- a/python/tvm_ffi/stub/rust_generator/codegen.py
+++ b/python/tvm_ffi/stub/rust_generator/codegen.py
@@ -30,7 +30,7 @@ the verdict of :mod:`tvm_ffi.stub.layout`:
   one ``from_fields``. A type that is not final
   can be the parent of a type defined in Rust (``tvm_ffi::rs_object``): its
   ``<Leaf>Obj::new`` is public, and its object derives ``ShallowCopy`` and
-  ``UnsafeInit`` (``#[type_rs_parent]``) when every ancestor's is generated
+  ``UnsafeInit`` (``rs_parent``) when every ancestor's is generated
   too (a ``ty-map``'d one is hand-written and may not). ``<Leaf>Obj::new`` of 
a final type is crate-private.
 - *opaque*: the struct embeds only its parent, and each field is read through
   the C ABI getter. Nothing allocates it.
@@ -577,16 +577,23 @@ class _ObjectRenderer:
                 
lines.append(f"tvm_ffi::impl_object_ref_upcast!({prefix}{pairs});")
         return lines
 
+    def _object_attr(self, verdict: Verdict) -> str:
+        """Render the ``#[object(...)]`` attribute of the object struct."""
+        options = [
+            f'type_key = "{self.type_key}"',
+            *(["final"] if self.info.is_final else []),
+            *(["immutable"] if self._is_immutable() else []),
+            *(["rs_parent"] if self._is_rs_parent(verdict) else []),
+        ]
+        return f"#[object({', '.join(options)})]"
+
     def _struct_lines(self, verdict: Verdict, base: str) -> list[str]:
         """Render the object struct: every field when complete, the parent 
alone when opaque."""
         header = [
             *self._allow_missing_docs(),
             "#[repr(C)]",
             "#[derive(tvm_ffi::derive::Object)]",
-            f'#[type_key = "{self.type_key}"]',
-            *(["#[type_final]"] if self.info.is_final else []),
-            *(["#[type_immutable]"] if self._is_immutable() else []),
-            *(["#[type_rs_parent]"] if self._is_rs_parent(verdict) else []),
+            self._object_attr(verdict),
             f"pub struct {self.obj_struct} {{",
             f"    base_: {base},",  # a reflected `base_` is rejected 
(`rust_ident`)
         ]
@@ -787,7 +794,7 @@ class _ObjectRenderer:
         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]``).
+        """Whether the object derives ``ShallowCopy`` and ``UnsafeInit`` 
(``rs_parent``).
 
         A subtype defined in Rust needs both of its parent's object. They are 
derived
         for a complete type that is not final and whose ancestors below 
``ffi.Object``
@@ -925,7 +932,7 @@ def _builtin_mirror_lines(type_key: str, base: str) -> 
list[str]:
         "#[allow(dead_code)]",
         "#[repr(C)]",
         "#[derive(tvm_ffi::derive::Object)]",
-        f'#[type_key = "{type_key}"]',
+        f'#[object(type_key = "{type_key}")]',
         f"pub struct {builtin_mirror_name(type_key)} {{",
         f"    base_: {base},",
         "}",
diff --git a/rust/tvm-ffi-macros/src/lib.rs b/rust/tvm-ffi-macros/src/lib.rs
index 4a8ab6f8..408fd2fd 100644
--- a/rust/tvm-ffi-macros/src/lib.rs
+++ b/rust/tvm-ffi-macros/src/lib.rs
@@ -45,22 +45,7 @@ pub fn match_any(input: TokenStream) -> TokenStream {
 // Documented on the `tvm_ffi::derive::Object` re-export, whose examples run
 // as tvm-ffi doctests.
 #[proc_macro_error]
-#[proc_macro_derive(
-    Object,
-    attributes(
-        type_key,
-        type_index,
-        type_final,
-        type_immutable,
-        type_rs_parent,
-        type_register,
-        type_child_slots,
-        type_child_slots_can_overflow,
-        type_s_eq_hash_kind,
-        type_reflection,
-        reflect
-    )
-)]
+#[proc_macro_derive(Object, attributes(object, reflect))]
 pub fn derive_object(input: TokenStream) -> TokenStream {
     object_macros::derive_object(input)
 }
diff --git a/rust/tvm-ffi-macros/src/object_macros.rs 
b/rust/tvm-ffi-macros/src/object_macros.rs
index 7d8a08bb..58eef552 100644
--- a/rust/tvm-ffi-macros/src/object_macros.rs
+++ b/rust/tvm-ffi-macros/src/object_macros.rs
@@ -35,7 +35,7 @@ pub fn derive_object(input: proc_macro::TokenStream) -> 
TokenStream {
 /// registers reflection, naming the tvm-ffi crate `tvm_ffi_crate`.
 ///
 /// `rs_object` registers the reflection of a type without `#[reflect]` fields
-/// or `#[type_s_eq_hash_kind]` too, with the creator of its `UnsafeInit`, as
+/// or `s_eq_hash_kind` too, with the creator of its `UnsafeInit`, as
 /// `#[rs_object]` does.
 pub(crate) fn expand_object(
     derive_input: &DeriveInput,
@@ -43,63 +43,37 @@ pub(crate) fn expand_object(
     rs_object: bool,
 ) -> TokenStream2 {
     let struct_name = derive_input.ident.clone();
+    let options = object_options(derive_input);
 
-    let type_key = get_attr(derive_input, "type_key")
-        .map(attr_to_str)
-        .expect("Expect #[type_key = \"<my_type_key>\"] attribute");
-    let type_final = match get_attr(derive_input, "type_final") {
-        Some(attr) if matches!(attr.parse_meta(), Ok(syn::Meta::Path(_))) => 
true,
-        Some(_) => panic!("Expect #[type_final] attribute"),
-        None => false,
-    };
-
-    let type_immutable = match get_attr(derive_input, "type_immutable") {
-        Some(attr) if matches!(attr.parse_meta(), Ok(syn::Meta::Path(_))) => 
true,
-        Some(_) => panic!("Expect #[type_immutable] attribute"),
-        None => false,
-    };
-
+    let type_key = options
+        .type_key
+        .clone()
+        .expect("Expect #[object(type_key = \"<my_type_key>\")]");
+    let type_final = options.type_final;
+    let type_immutable = options.immutable;
     // Whether an `rs_object` can derive from this type, which needs its
     // `ShallowCopy` and `UnsafeInit`.
-    let type_rs_parent = match get_attr(derive_input, "type_rs_parent") {
-        Some(attr) if matches!(attr.parse_meta(), Ok(syn::Meta::Path(_))) => 
true,
-        Some(_) => panic!("Expect #[type_rs_parent] attribute"),
-        None => false,
-    };
-
+    let type_rs_parent = options.rs_parent;
     // Whether this type is defined here, so that it registers its key, as C++
     // `TVM_FFI_DECLARE_OBJECT_INFO` does. Without it, the type binds a key
     // that its defining library, usually C++, has registered.
-    let type_register = match get_attr(derive_input, "type_register") {
-        Some(attr) if matches!(attr.parse_meta(), Ok(syn::Meta::Path(_))) => 
true,
-        Some(_) => panic!("Expect #[type_register] attribute"),
-        None => false,
-    };
+    let type_register = options.register;
 
     // Reserved child slots and whether children may exceed them, as C++
     // `_type_child_slots` and `_type_child_slots_can_overflow` declare them.
-    let child_slots = match get_attr(derive_input, "type_child_slots").map(|a| 
a.parse_meta()) {
-        Some(Ok(syn::Meta::NameValue(syn::MetaNameValue {
-            lit: syn::Lit::Int(n),
-            ..
-        }))) => n
+    let child_slots = match &options.child_slots {
+        Some(n) => n
             .base10_parse::<i32>()
-            .expect("Expect #[type_child_slots = <non-negative integer>]"),
-        Some(_) => panic!("Expect #[type_child_slots = <non-negative 
integer>]"),
+            .expect("Expect `child_slots = <non-negative integer>`"),
         None => 0,
     };
-    let child_slots_can_overflow =
-        match get_attr(derive_input, "type_child_slots_can_overflow").map(|a| 
a.parse_meta()) {
-            Some(Ok(syn::Meta::NameValue(syn::MetaNameValue {
-                lit: syn::Lit::Bool(b),
-                ..
-            }))) => b.value,
-            Some(_) => panic!("Expect #[type_child_slots_can_overflow = 
<bool>]"),
-            None => true,
-        };
+    let child_slots_can_overflow = options
+        .child_slots_can_overflow
+        .as_ref()
+        .is_none_or(|b| b.value);
     assert!(
         child_slots >= 0,
-        "Expect #[type_child_slots = <non-negative integer>]"
+        "Expect `child_slots = <non-negative integer>`"
     );
     assert!(
         !(type_final && child_slots > 0),
@@ -107,9 +81,8 @@ pub(crate) fn expand_object(
     );
     assert!(
         type_register
-            || (get_attr(derive_input, "type_child_slots").is_none()
-                && get_attr(derive_input, 
"type_child_slots_can_overflow").is_none()),
-        "#[type_child_slots] and #[type_child_slots_can_overflow] need 
#[type_register]"
+            || (options.child_slots.is_none() && 
options.child_slots_can_overflow.is_none()),
+        "`child_slots` and `child_slots_can_overflow` need `register`"
     );
     // Upcasts reinterpret a pointer to the object as one to its parent, which
     // only `repr(C)` keeps at offset zero.
@@ -123,7 +96,7 @@ pub(crate) fn expand_object(
     }
     .expect("First field must be `<base_name>: <ObjectCoreType>`");
 
-    let reflection = Reflection::parse(derive_input, rs_object);
+    let reflection = Reflection::parse(derive_input, &options, rs_object);
     let writable = reflection
         .iter()
         .flat_map(|reflection| &reflection.reflected)
@@ -143,9 +116,8 @@ pub(crate) fn expand_object(
         quote! {}
     };
     assert!(
-        reflection.is_none() || (type_register && get_attr(derive_input, 
"type_index").is_none()),
-        "#[reflect], #[type_s_eq_hash_kind] and #[type_reflection] need 
#[type_register] and no \
-         #[type_index]"
+        reflection.is_none() || (type_register && 
options.type_index.is_none()),
+        "#[reflect], `s_eq_hash_kind` and `reflection` need `register` and no 
`type_index`"
     );
     let register_reflection = match &reflection {
         Some(reflection) => reflection.register_tokens(&struct_name, 
&type_key, tvm_ffi_crate),
@@ -161,7 +133,7 @@ pub(crate) fn expand_object(
         if !derive_input.generics.params.is_empty() {
             abort!(
                 struct_name,
-                "#[type_rs_parent] needs a struct without generic parameters"
+                "`rs_parent` needs a struct without generic parameters"
             );
         }
         unsafe_init_tokens(&struct_name, &base_ty, &base_id, &field_ids, 
tvm_ffi_crate)
@@ -174,22 +146,10 @@ pub(crate) fn expand_object(
     // calls at load, or takes the index the key already has,
     // as C++ `TVM_FFI_DECLARE_OBJECT_INFO` does; any other type binds the
     // index its defining library registered.
-    let type_index_tokens = match get_attr(derive_input, "type_index") {
-        Some(attr) => {
-            // A static index is trusted: the type must be the one the runtime
-            // gives that index.
-            let type_index_expr = attr
-                .parse_args_with(|input: syn::parse::ParseStream| {
-                    input.parse::<syn::Token![unsafe]>()?;
-                    input.parse::<syn::Expr>()
-                })
-                .unwrap_or_else(|error| {
-                    abort!(
-                        attr,
-                        "expected #[type_index(unsafe TypeIndex::...)]: {}",
-                        error
-                    )
-                });
+    let type_index_tokens = match &options.type_index {
+        // A static index is trusted: the type must be the one the runtime
+        // gives that index.
+        Some(type_index_expr) => {
             quote! {
                 #[inline]
                 fn type_index() -> i32 {
@@ -255,7 +215,8 @@ pub(crate) fn expand_object(
                             if ret != 0 {
                                 ::core::panic!(
                                     "Type key {} is not registered: load the 
library that defines \
-                                     it first, or add #[type_register] if this 
crate defines it",
+                                     it first, or add #[object(register)] if 
this crate defines \
+                                     it",
                                     #type_key
                                 );
                             }
@@ -291,11 +252,11 @@ pub(crate) fn expand_object(
     // A type defined here registers when its library loads, as a C++ type does
     // through `refl::ObjectDef` in a static init block; a binding stays lazy,
     // and so does a type under a binding whose library is not loaded yet.
-    let register_at_load = if type_register && get_attr(derive_input, 
"type_index").is_none() {
+    let register_at_load = if type_register && options.type_index.is_none() {
         if !derive_input.generics.params.is_empty() {
             abort!(
                 struct_name,
-                "#[type_register] needs a struct without generic parameters"
+                "`register` needs a struct without generic parameters"
             );
         }
         // The key is in the text of the block, which identifies the block, so
@@ -402,12 +363,12 @@ pub(crate) fn expand_object(
     }
 }
 
-/// The reflection a `#[type_register]` type declares: its `#[reflect]` fields,
-/// its `#[type_s_eq_hash_kind]` and its `#[type_reflection]` function.
+/// The reflection a `register` type declares: its `#[reflect]` fields, its
+/// `s_eq_hash_kind` and its `reflection` function.
 struct Reflection {
     /// The structural kind; `None` inherits the parent's.
     kind: Option<syn::Ident>,
-    /// The function `#[type_reflection(path)]` names, which registers methods.
+    /// The function `reflection = path` names, which registers methods.
     methods: Option<syn::Path>,
     /// The reflected fields.
     reflected: Vec<ReflectedField>,
@@ -501,15 +462,12 @@ fn doc_comment(attrs: &[syn::Attribute]) -> String {
 impl Reflection {
     /// The type's reflection, or `None` when it declares none and is not an
     /// `rs_object`.
-    fn parse(derive_input: &DeriveInput, rs_object: bool) -> Option<Self> {
-        let kind = get_attr(derive_input, "type_s_eq_hash_kind").map(|attr| {
-            let kind = attr_to_str(attr);
-            syn::Ident::new(&kind.value(), kind.span())
-        });
-        let methods = get_attr(derive_input, "type_reflection").map(|attr| {
-            attr.parse_args::<syn::Path>()
-                .expect("Expect #[type_reflection(<path to fn(&mut 
ObjectDef<Self>)>)]")
-        });
+    fn parse(derive_input: &DeriveInput, options: &ObjectOptions, rs_object: 
bool) -> Option<Self> {
+        let kind = options
+            .s_eq_hash_kind
+            .as_ref()
+            .map(|kind| syn::Ident::new(&kind.value(), kind.span()));
+        let methods = options.reflection.clone();
         let syn::Data::Struct(s) = &derive_input.data else {
             return None;
         };
@@ -577,7 +535,7 @@ impl Reflection {
                 )?;
             }
         });
-        // The `#[type_reflection]` function registers the methods after the
+        // The `reflection` function registers the methods after the
         // fields, as a C++ `ObjectDef` chains `def` after `def_ro`.
         let methods = self.methods.as_ref().map(|path| {
             // The registration is a `static` initializer, where `Self` does
@@ -988,3 +946,86 @@ fn object_ref_options(derive_input: &DeriveInput) -> 
ObjectRefOptions {
     }
     options
 }
+
+/// The options of `#[object(...)]` on an `Object` derive.
+#[derive(Default)]
+struct ObjectOptions {
+    type_key: Option<syn::LitStr>,
+    type_index: Option<syn::Expr>,
+    type_final: bool,
+    immutable: bool,
+    register: bool,
+    rs_parent: bool,
+    child_slots: Option<syn::LitInt>,
+    child_slots_can_overflow: Option<syn::LitBool>,
+    s_eq_hash_kind: Option<syn::LitStr>,
+    reflection: Option<syn::Path>,
+}
+
+impl ObjectOptions {
+    /// Parses one `#[object(...)]` item: a flag, `name = value` or
+    /// `unsafe type_index = expr`.
+    fn parse_item(&mut self, input: syn::parse::ParseStream) -> 
syn::Result<()> {
+        let is_unsafe = 
input.parse::<Option<syn::Token![unsafe]>>()?.is_some();
+        // `final` is a keyword.
+        let name: syn::Ident = input.call(syn::Ident::parse_any)?;
+        if is_unsafe != (name == "type_index") {
+            let message = if is_unsafe {
+                "only `type_index` takes `unsafe`"
+            } else {
+                "expected `unsafe type_index = ...`: the type must be the one 
the runtime gives \
+                 that index"
+            };
+            return Err(syn::Error::new(name.span(), message));
+        }
+        match name.to_string().as_str() {
+            "final" => self.type_final = true,
+            "immutable" => self.immutable = true,
+            "register" => self.register = true,
+            "rs_parent" => self.rs_parent = true,
+            "type_key" => self.type_key = Some(parse_value(input)?),
+            "type_index" => self.type_index = Some(parse_value(input)?),
+            "child_slots" => self.child_slots = Some(parse_value(input)?),
+            "child_slots_can_overflow" => self.child_slots_can_overflow = 
Some(parse_value(input)?),
+            "s_eq_hash_kind" => self.s_eq_hash_kind = 
Some(parse_value(input)?),
+            "reflection" => self.reflection = Some(parse_value(input)?),
+            _ => {
+                return Err(syn::Error::new(
+                    name.span(),
+                    "unknown `object` option; expected `type_key = \"...\"`, \
+                     `unsafe type_index = ...`, `final`, `immutable`, 
`register`, `rs_parent`, \
+                     `child_slots = <n>`, `child_slots_can_overflow = <bool>`, 
\
+                     `s_eq_hash_kind = \"<kind>\"` or `reflection = <path>`",
+                ))
+            }
+        }
+        Ok(())
+    }
+}
+
+/// The `= value` of a `name = value` option.
+fn parse_value<T: syn::parse::Parse>(input: syn::parse::ParseStream) -> 
syn::Result<T> {
+    input.parse::<syn::Token![=]>()?;
+    input.parse()
+}
+
+fn object_options(derive_input: &DeriveInput) -> ObjectOptions {
+    let mut options = ObjectOptions::default();
+    for attr in derive_input
+        .attrs
+        .iter()
+        .filter(|a| a.path.is_ident("object"))
+    {
+        attr.parse_args_with(|input: syn::parse::ParseStream| {
+            while !input.is_empty() {
+                options.parse_item(input)?;
+                if !input.is_empty() {
+                    input.parse::<syn::Token![,]>()?;
+                }
+            }
+            Ok(())
+        })
+        .unwrap_or_else(|error| abort!(error.span(), "{}", error));
+    }
+    options
+}
diff --git a/rust/tvm-ffi-macros/src/rs_object.rs 
b/rust/tvm-ffi-macros/src/rs_object.rs
index 06cef3d9..da53a2e3 100644
--- a/rust/tvm-ffi-macros/src/rs_object.rs
+++ b/rust/tvm-ffi-macros/src/rs_object.rs
@@ -188,26 +188,26 @@ fn expand(args: &RsObjectArgs, item: &DeriveInput) -> 
syn::Result<TokenStream2>
     let obj = &item.ident;
     let key = &args.type_key;
 
-    // `derive(Object)` reads the type from these attributes; the struct 
written out
+    // `derive(Object)` reads the type from these options; the struct written 
out
     // carries neither them nor `#[reflect]`, which no derive on it would take.
-    let mut derive_attrs = vec![quote!(#[type_key = #key]), 
quote!(#[type_register])];
+    let mut options = vec![quote!(type_key = #key), quote!(register)];
     if args.type_final {
-        derive_attrs.push(quote!(#[type_final]));
+        options.push(quote!(final));
     }
     if args.immutable {
-        derive_attrs.push(quote!(#[type_immutable]));
+        options.push(quote!(immutable));
     }
     if let Some(kind) = &args.s_eq_hash_kind {
-        derive_attrs.push(quote!(#[type_s_eq_hash_kind = #kind]));
+        options.push(quote!(s_eq_hash_kind = #kind));
     }
     if let Some(slots) = &args.child_slots {
-        derive_attrs.push(quote!(#[type_child_slots = #slots]));
+        options.push(quote!(child_slots = #slots));
     }
     if let Some(overflow) = &args.child_slots_can_overflow {
-        derive_attrs.push(quote!(#[type_child_slots_can_overflow = 
#overflow]));
+        options.push(quote!(child_slots_can_overflow = #overflow));
     }
     if let Some(reflection) = &args.reflection {
-        derive_attrs.push(quote!(#[type_reflection(#reflection)]));
+        options.push(quote!(reflection = #reflection));
     }
     let reflected_fields = fields.iter().map(|field| {
         if field.attrs.iter().any(|a| a.path.is_ident("reflect")) {
@@ -218,7 +218,7 @@ fn expand(args: &RsObjectArgs, item: &DeriveInput) -> 
syn::Result<TokenStream2>
     });
     let object_input: DeriveInput = syn::parse2(quote! {
         #[repr(C)]
-        #(#derive_attrs)*
+        #[object(#(#options),*)]
         #vis struct #obj {
             #base,
             #(#reflected_fields,)*
diff --git a/rust/tvm-ffi-macros/src/utils.rs b/rust/tvm-ffi-macros/src/utils.rs
index ec1a5a6b..e6e3560a 100644
--- a/rust/tvm-ffi-macros/src/utils.rs
+++ b/rust/tvm-ffi-macros/src/utils.rs
@@ -26,36 +26,3 @@ use quote::quote;
 pub(crate) fn get_tvm_ffi_crate() -> TokenStream {
     quote!(::tvm_ffi)
 }
-
-/// Get an attribute by name from a derive input
-///
-/// # Arguments
-/// * `derive_input` - The derive input to get the attribute from
-/// * `name` - The name of the attribute to get
-///
-/// # Returns
-/// * `Option<&syn::Attribute>` - The attribute if it exists
-pub(crate) fn get_attr<'a>(
-    derive_input: &'a syn::DeriveInput,
-    name: &str,
-) -> Option<&'a syn::Attribute> {
-    derive_input.attrs.iter().find(|a| a.path.is_ident(name))
-}
-
-/// Convert an attribute to a string
-///
-/// # Arguments
-/// * `attr` - The attribute to convert
-///
-/// # Returns
-/// * `syn::LitStr` - The string value of the attribute
-pub(crate) fn attr_to_str(attr: &syn::Attribute) -> syn::LitStr {
-    match attr.parse_meta() {
-        Ok(syn::Meta::NameValue(syn::MetaNameValue {
-            lit: syn::Lit::Str(s),
-            ..
-        })) => s,
-        Ok(_m) => panic!("Expected a string literal, got"),
-        Err(e) => panic!("{}", e),
-    }
-}
diff --git a/rust/tvm-ffi/src/big_int/mod.rs b/rust/tvm-ffi/src/big_int/mod.rs
index 3edad11f..bd17e6f8 100644
--- a/rust/tvm-ffi/src/big_int/mod.rs
+++ b/rust/tvm-ffi/src/big_int/mod.rs
@@ -65,9 +65,7 @@ unsafe impl Sync for BigInt {}
 // two's-complement words as trailing items. Mirrors C++ `detail::BigIntObj`.
 #[repr(C)]
 #[derive(Object)]
-#[type_key = "ffi.BigInt"]
-#[type_index(unsafe TypeIndex::kTVMFFIBigInt)]
-#[type_final]
+#[object(type_key = "ffi.BigInt", unsafe type_index = 
TypeIndex::kTVMFFIBigInt, final)]
 struct BigIntObj {
     object: Object,
     size: usize,
diff --git a/rust/tvm-ffi/src/collections/array.rs 
b/rust/tvm-ffi/src/collections/array.rs
index c52808ea..8d28473b 100644
--- a/rust/tvm-ffi/src/collections/array.rs
+++ b/rust/tvm-ffi/src/collections/array.rs
@@ -20,7 +20,6 @@
 
 use std::fmt::Debug;
 use std::marker::PhantomData;
-use std::ops::Deref;
 
 use crate::any::TryFromTemp;
 use crate::collections::list::seq_cells;
@@ -35,8 +34,7 @@ use tvm_ffi_sys::{TVMFFIAny, TVMFFIObject};
 /// The object behind an [`Array`], laid out as the C++ `ArrayObj`.
 #[repr(C)]
 #[derive(Object)]
-#[type_key = "ffi.Array"]
-#[type_index(unsafe TypeIndex::kTVMFFIArray)]
+#[object(type_key = "ffi.Array", unsafe type_index = TypeIndex::kTVMFFIArray)]
 pub struct ArrayObj {
     pub(crate) object: Object,
     /// The element buffer, `capacity` `TVMFFIAny` cells.
@@ -93,8 +91,8 @@ impl Drop for ArrayObj {
 ///
 /// let mut array = Array::from_vec(vec![1_i64, 2, 3]);
 /// assert_eq!(array.len(), 3);
-/// assert_eq!(array.get(1)?, 2);
-/// assert!(array.get(3).is_err());
+/// assert_eq!(array.get(1), Some(2));
+/// assert_eq!(array.get(3), None);
 ///
 /// let snapshot = array.clone();
 /// array.push(4);
@@ -103,7 +101,6 @@ impl Drop for ArrayObj {
 ///
 /// let doubled: Array<i64> = array.iter().map(|x| x * 2).collect();
 /// assert_eq!(doubled.iter().copied().collect::<Vec<_>>(), vec![2, 4, 6, 8]);
-/// # Ok::<(), tvm_ffi::Error>(())
 /// ```
 #[repr(C)]
 #[derive(Clone)]
@@ -305,17 +302,18 @@ impl<T: ContainerElement + Clone> Array<T> {
         container.size += 1;
     }
 
-    /// Insert `value` at `index`, shifting the elements after it.
+    /// Insert `value` at `index`, shifting the elements after it, as
+    /// [`Vec::insert`].
     ///
-    /// # Errors
+    /// # Panics
     ///
-    /// Returns an `IndexError` if `index > len`.
+    /// Panics if `index > len`.
     ///
     /// See also [`List::insert`](crate::List::insert).
-    pub fn insert(&mut self, index: usize, value: T) -> Result<(), 
crate::Error> {
+    pub fn insert(&mut self, index: usize, value: T) {
         let size = self.len();
         if index > size {
-            crate::bail!(crate::error::INDEX_ERROR, "Array insert index out of 
bound");
+            panic!("insertion index (is {index}) should be <= len (is 
{size})");
         }
         let container = self.make_mut(size + 1);
         // SAFETY: `make_mut` left room for one more cell; the tail moves up 
by one.
@@ -325,7 +323,6 @@ impl<T: ContainerElement + Clone> Array<T> {
             write_cell(cell, value);
         }
         container.size += 1;
-        Ok(())
     }
 
     /// Remove and return the last element, or `None` if the array is empty,
@@ -368,17 +365,17 @@ impl<T: ContainerElement + Clone> Array<T> {
         }
     }
 
-    /// Replace the element at `index`.
+    /// Replace the element at `index`, as `vec[index] = value`.
     ///
-    /// # Errors
+    /// # Panics
     ///
-    /// Returns an `IndexError` if `index >= len`.
+    /// Panics if `index >= len`.
     ///
     /// See also [`List::set`](crate::List::set).
-    pub fn set(&mut self, index: usize, value: T) -> Result<(), crate::Error> {
+    pub fn set(&mut self, index: usize, value: T) {
         let size = self.len();
         if index >= size {
-            crate::bail!(crate::error::INDEX_ERROR, "Array set index out of 
bound");
+            panic!("Index out of bounds: the len is {size} but the index is 
{index}");
         }
         let container = self.make_mut(size);
         // SAFETY: the cell at `index` is initialized; its old value is dropped
@@ -389,7 +386,6 @@ impl<T: ContainerElement + Clone> Array<T> {
             write_cell(cell, value);
             drop(old);
         }
-        Ok(())
     }
 
     /// Return the number of elements.
@@ -410,39 +406,17 @@ impl<T: ContainerElement + Clone> Array<T> {
         self.len() == 0
     }
 
-    /// Return the element at `index`.
-    ///
-    /// # Arguments
-    ///
-    /// * `index` - The position of the element.
-    ///
-    /// # Returns
-    ///
-    /// The element at `index`.
-    ///
-    /// # Errors
-    ///
-    /// Returns an `IndexError` if `index` is out of bounds, or a `TypeError`
-    /// if the element is not a `T`.
-    pub fn get(&self, index: usize) -> Result<T, crate::Error> {
-        if index >= self.len() {
-            crate::bail!(crate::error::INDEX_ERROR, "Array get index out of 
bound");
-        }
-        unsafe {
-            let container = self.data.deref();
-            let base_ptr = container.data as *const TVMFFIAny;
-            let raw_any_ref = &*base_ptr.add(index);
-
-            match T::container_try_cast_from_any_view(raw_any_ref) {
-                Ok(val) => Ok(val),
-                Err(_) => crate::bail!(
-                    crate::error::TYPE_ERROR,
-                    "Failed to cast element at {} to {}",
-                    index,
-                    T::container_type_str()
-                ),
-            }
-        }
+    /// Return the element at `index`, or `None` if it is out of bounds, as
+    /// [`slice::get`]. `array[index]` borrows it instead, and panics when out
+    /// of bounds.
+    pub fn get(&self, index: usize) -> Option<T> {
+        let cell = self.cells().get(index)?;
+        // SAFETY: every cell of an `Array<T>` holds a `T`, checked when the
+        // array is built.
+        Some(unsafe {
+            debug_assert!(T::container_check_any_strict(cell));
+            T::container_copy_from_any_view_after_check(cell)
+        })
     }
 
     /// An array of `items` with room for `capacity` elements.
diff --git a/rust/tvm-ffi/src/collections/list.rs 
b/rust/tvm-ffi/src/collections/list.rs
index 91241a68..1951145f 100644
--- a/rust/tvm-ffi/src/collections/list.rs
+++ b/rust/tvm-ffi/src/collections/list.rs
@@ -48,6 +48,7 @@
 use std::cell::UnsafeCell;
 use std::fmt::Debug;
 use std::marker::PhantomData;
+use std::ops::{Bound, RangeBounds};
 use std::ptr;
 
 use crate::any::TryFromTemp;
@@ -64,8 +65,7 @@ use tvm_ffi_sys::{TVMFFIAny, TVMFFIObject};
 /// runtime replaces when the list grows.
 #[repr(C)]
 #[derive(Object)]
-#[type_key = "ffi.List"]
-#[type_index(unsafe TypeIndex::kTVMFFIList)]
+#[object(type_key = "ffi.List", unsafe type_index = TypeIndex::kTVMFFIList)]
 pub struct ListObj {
     pub(crate) object: Object,
     // `List` mutates these through `&self` (`push`, `set`, growth), so they 
are
@@ -310,20 +310,24 @@ impl<T: ContainerElement> List<T> {
         }
     }
 
-    /// Replaces the element at `index`.
+    /// Replaces the element at `index`, as `vec[index] = value`.
+    ///
+    /// # Panics
+    ///
+    /// Panics if `index >= len`.
     #[inline]
-    pub fn set(&self, index: usize, value: T) -> Result<()> {
+    pub fn set(&self, index: usize, value: T) {
         let obj = self.obj();
         unsafe {
-            if index >= (*(*obj).size.get()) as usize {
-                return index_error("set");
+            let size = (*(*obj).size.get()) as usize;
+            if index >= size {
+                panic!("Index out of bounds: the len is {size} but the index 
is {index}");
             }
             let cell = ListObj::cell(obj, index);
             let old = take_cell(cell);
             write_cell(cell, value);
             drop(old);
         }
-        Ok(())
     }
 
     /// Appends `value` to the end of the list.
@@ -427,16 +431,38 @@ impl<T: ContainerElement> List<T> {
         unsafe { ListObj::truncate(self.obj(), len) }
     }
 
-    /// Removes the elements in `start..end`, shifting the ones after them.
-    pub fn remove_range(&self, start: usize, end: usize) -> Result<()> {
+    /// Removes the elements in `range`, shifting the ones after them, as
+    /// [`Vec::drain`] does without returning them.
+    ///
+    /// # Panics
+    ///
+    /// Panics if `range` starts after it ends or ends past the list.
+    pub fn remove_range(&self, range: impl RangeBounds<usize>) {
         let obj = self.obj();
         unsafe {
             let size = (*(*obj).size.get()) as usize;
-            if start > end || end > size {
-                return index_error("remove_range");
+            let start = match range.start_bound() {
+                Bound::Included(&start) => start,
+                Bound::Excluded(&start) => start
+                    .checked_add(1)
+                    .expect("attempted to index slice from after maximum 
usize"),
+                Bound::Unbounded => 0,
+            };
+            let end = match range.end_bound() {
+                Bound::Included(&end) => end
+                    .checked_add(1)
+                    .expect("attempted to index slice up to maximum usize"),
+                Bound::Excluded(&end) => end,
+                Bound::Unbounded => size,
+            };
+            if start > end {
+                panic!("slice index starts at {start} but ends at {end}");
+            }
+            if end > size {
+                panic!("range end index {end} out of range for slice of length 
{size}");
             }
             if start == end {
-                return Ok(());
+                return;
             }
             // Rotate the removed cells to the back and drop them from there,
             // so the list is consistent while each element is released.
@@ -444,7 +470,6 @@ impl<T: ContainerElement> List<T> {
                 .rotate_left(end - start);
             ListObj::truncate(obj, size - (end - start));
         }
-        Ok(())
     }
 
     /// Inserts every item of `iter`, in order, at `index`, shifting the
diff --git a/rust/tvm-ffi/src/collections/map.rs 
b/rust/tvm-ffi/src/collections/map.rs
index 5cec416b..511cf96d 100644
--- a/rust/tvm-ffi/src/collections/map.rs
+++ b/rust/tvm-ffi/src/collections/map.rs
@@ -96,8 +96,7 @@ fn element_from_any<T: ContainerElement>(value: Any) -> 
Result<T> {
 // that would shift every offset below.
 #[repr(C)]
 #[derive(Object)]
-#[type_key = "ffi.Map"]
-#[type_index(unsafe TypeIndex::kTVMFFIMap)]
+#[object(type_key = "ffi.Map", unsafe type_index = TypeIndex::kTVMFFIMap)]
 pub struct MapObj {
     pub(crate) object: Object,
     /// Pointer to the (opaque) key/value storage region (`MapBaseObj::data_`).
diff --git a/rust/tvm-ffi/src/collections/shape.rs 
b/rust/tvm-ffi/src/collections/shape.rs
index 6bbd5c43..f5a965c0 100644
--- a/rust/tvm-ffi/src/collections/shape.rs
+++ b/rust/tvm-ffi/src/collections/shape.rs
@@ -33,8 +33,7 @@ use tvm_ffi_sys::TVMFFITypeIndex as TypeIndex;
 /// The object behind a [`Shape`].
 #[repr(C)]
 #[derive(Object)]
-#[type_key = "ffi.Shape"]
-#[type_index(unsafe TypeIndex::kTVMFFIShape)]
+#[object(type_key = "ffi.Shape", unsafe type_index = TypeIndex::kTVMFFIShape)]
 pub struct ShapeObj {
     object: Object,
     data: TVMFFIShapeCell,
diff --git a/rust/tvm-ffi/src/collections/tensor.rs 
b/rust/tvm-ffi/src/collections/tensor.rs
index df39460a..0cee1734 100644
--- a/rust/tvm-ffi/src/collections/tensor.rs
+++ b/rust/tvm-ffi/src/collections/tensor.rs
@@ -115,8 +115,7 @@ unsafe fn data_size(tensor: &DLTensor) -> usize {
 /// The object behind a [`Tensor`]: an object header followed by a `DLTensor`.
 #[repr(C)]
 #[derive(Object)]
-#[type_key = "ffi.Tensor"]
-#[type_index(unsafe TypeIndex::kTVMFFITensor)]
+#[object(type_key = "ffi.Tensor", unsafe type_index = 
TypeIndex::kTVMFFITensor)]
 pub struct TensorObj {
     object: Object,
     dltensor: DLTensor,
diff --git a/rust/tvm-ffi/src/derive.rs b/rust/tvm-ffi/src/derive.rs
index 90f0b5ec..868514be 100644
--- a/rust/tvm-ffi/src/derive.rs
+++ b/rust/tvm-ffi/src/derive.rs
@@ -24,29 +24,51 @@
 /// parent. Define a new type in Rust with 
[`rs_object`](macro@crate::rs_object)
 /// instead.
 ///
-/// `#[type_key = "..."]` names the type. `type_index()` takes the index the
+/// Without `register` or `type_index`, `type_index()` takes the index the
 /// defining library registered for the key, and panics if that library is not
 /// loaded yet, so a binding never registers a key with the wrong child slots.
 /// Such a panic is final: `type_index()` of the type, and of every type under
 /// it, keeps panicking after the library loads, so call it only once the
 /// library is loaded. It also panics unless the registered key has the type's
 /// parent and depth and, when the type table records a size, a size no
-/// smaller than the type's. `#[type_index(unsafe ...)]` gives the type a
-/// static index instead; the `unsafe` marks the caller's promise that the
-/// runtime gives that index to this type.
+/// smaller than the type's.
 ///
 /// A binding that declares only part of its object must not be constructed:
 /// keep its fields private.
 ///
-/// * `#[type_final]` - The type has no subtypes, as C++ `_type_final`.
-/// * `#[type_immutable]` - Implement
+/// # Options
+///
+/// `#[object(...)]` takes a comma-separated list, as in
+/// `#[object(type_key = "ffi.String", unsafe type_index = 
TypeIndex::kTVMFFIStr)]`:
+///
+/// * `type_key = "..."` - The type key; required.
+/// * `unsafe type_index = expr` - A static index instead of the registered
+///   one; the `unsafe` marks the caller's promise that the runtime gives that
+///   index to this type.
+/// * `final` - The type has no subtypes, as C++ `_type_final`.
+/// * `immutable` - Implement
 ///   [`ImmutableObject`](crate::object::ImmutableObject), as for a C++ type
 ///   not registered as `_type_mutable`; every subtype must be immutable too.
-/// * `#[type_rs_parent]` - Implement `ShallowCopy` and
+/// * `rs_parent` - Implement `ShallowCopy` and
 ///   [`UnsafeInit`](crate::reflection::UnsafeInit), so an `rs_object` can
 ///   derive from the type. Each field other than the parent must be `Clone`
 ///   and implement [`AnyCompatible`](crate::AnyCompatible), be an
 ///   [`Any`](crate::Any) or have no drop glue.
+/// * `register` - The type is defined here: register its key under its
+///   parent when the library loads, as `rs_object` does.
+/// * `child_slots = N`, `child_slots_can_overflow = bool` - Type indices kept
+///   for subtypes, as C++ `_type_child_slots` and
+///   `_type_child_slots_can_overflow`; 0 and `true` by default. Needs
+///   `register`.
+/// * `s_eq_hash_kind = "TreeNode"` - The
+///   [`SEqHashKind`](crate::reflection::SEqHashKind) of structural equality;
+///   by default the parent's.
+/// * `reflection = path` - A `fn(&mut ObjectDef<Self>)` that registers the
+///   type's methods.
+///
+/// `s_eq_hash_kind`, `reflection` and field `#[reflect(...)]`, as
+/// [`rs_object`](macro@crate::rs_object) takes it, need `register` and no
+/// `type_index`.
 pub use tvm_ffi_macros::Object;
 
 /// Declare an object reference: the managed, reference-counted handle to an
diff --git a/rust/tvm-ffi/src/enum_ref.rs b/rust/tvm-ffi/src/enum_ref.rs
index 1d41a0ee..8f751493 100644
--- a/rust/tvm-ffi/src/enum_ref.rs
+++ b/rust/tvm-ffi/src/enum_ref.rs
@@ -33,8 +33,7 @@ use crate::{AnyView, Error, Result, String, VALUE_ERROR};
 /// `base_: EnumObj`.
 #[repr(C)]
 #[derive(Object)]
-#[type_key = "ffi.Enum"]
-#[type_immutable]
+#[object(type_key = "ffi.Enum", immutable)]
 pub struct EnumObj {
     base_: Object,
     // The registration order, which can differ between processes, so values
@@ -77,10 +76,7 @@ impl EnumObj {
 ///
 /// #[repr(C)]
 /// #[derive(Object)]
-/// #[type_key = "my_ext.Color"]
-/// #[type_register]
-/// #[type_final]
-/// #[type_immutable]
+/// #[object(type_key = "my_ext.Color", register, final, immutable)]
 /// pub struct ColorObj {
 ///     base_: EnumObj,
 /// }
diff --git a/rust/tvm-ffi/src/error.rs b/rust/tvm-ffi/src/error.rs
index c6b33f5e..57cae23e 100644
--- a/rust/tvm-ffi/src/error.rs
+++ b/rust/tvm-ffi/src/error.rs
@@ -31,8 +31,7 @@ use tvm_ffi_sys::{
 /// The object behind an [`Error`]: its kind, message, backtrace, cause and 
context.
 #[repr(C)]
 #[derive(Object)]
-#[type_key = "ffi.Error"]
-#[type_index(unsafe TVMFFITypeIndex::kTVMFFIError)]
+#[object(type_key = "ffi.Error", unsafe type_index = 
TVMFFITypeIndex::kTVMFFIError)]
 pub struct ErrorObj {
     object: Object,
     cell: TVMFFIErrorCell,
diff --git a/rust/tvm-ffi/src/extra/module.rs b/rust/tvm-ffi/src/extra/module.rs
index aff890dd..fbebcc88 100644
--- a/rust/tvm-ffi/src/extra/module.rs
+++ b/rust/tvm-ffi/src/extra/module.rs
@@ -32,8 +32,7 @@ use tvm_ffi_sys::TVMFFITypeIndex as TypeIndex;
 /// The object behind a [`Module`].
 #[repr(C)]
 #[derive(Object)]
-#[type_key = "ffi.Module"]
-#[type_index(unsafe TypeIndex::kTVMFFIModule)]
+#[object(type_key = "ffi.Module", unsafe type_index = 
TypeIndex::kTVMFFIModule)]
 pub struct ModuleObj {
     object: Object,
 }
diff --git a/rust/tvm-ffi/src/extra/structural_eq_hash.rs 
b/rust/tvm-ffi/src/extra/structural_eq_hash.rs
index cbf90b88..a60d1f8c 100644
--- a/rust/tvm-ffi/src/extra/structural_eq_hash.rs
+++ b/rust/tvm-ffi/src/extra/structural_eq_hash.rs
@@ -184,6 +184,5 @@ pub fn get_first_structural_mismatch_with<'l, 'r>(
         crate::cached_global_func!("ffi.GetFirstStructuralMismatch")
             .call_packed(&args)?
             .try_into()?;
-    pair.map(|pair| Ok((pair.get(0)?, pair.get(1)?)))
-        .transpose()
+    Ok(pair.map(|pair| (pair[0].clone(), pair[1].clone())))
 }
diff --git a/rust/tvm-ffi/src/extra/structural_mutate.rs 
b/rust/tvm-ffi/src/extra/structural_mutate.rs
index 54913571..156b50bd 100644
--- a/rust/tvm-ffi/src/extra/structural_mutate.rs
+++ b/rust/tvm-ffi/src/extra/structural_mutate.rs
@@ -835,7 +835,7 @@ impl<D, Base: StructuralMutatorBase> 
StructuralMutatorView<'_, D, Base> {
 ///
 /// let root = Array::from_vec(vec![Array::from_vec(vec![1_i64, 2]), 
Array::from_vec(vec![3_i64])]);
 /// let result = Array::<Array<i64>>::try_from(structural_mutate(root, 
Double)?)?;
-/// assert_eq!(result.get(0)?.iter().copied().collect::<Vec<_>>(), vec![2, 4]);
+/// assert_eq!(result[0].iter().copied().collect::<Vec<_>>(), vec![2, 4]);
 /// # Ok::<(), tvm_ffi::Error>(())
 /// ```
 pub fn structural_mutate<R, H, M, D, O>(root: R, dispatcher: H) -> Result<Any>
diff --git a/rust/tvm-ffi/src/extra/structural_visit.rs 
b/rust/tvm-ffi/src/extra/structural_visit.rs
index 74b70293..349ee77c 100644
--- a/rust/tvm-ffi/src/extra/structural_visit.rs
+++ b/rust/tvm-ffi/src/extra/structural_visit.rs
@@ -123,9 +123,11 @@ impl Default for StructuralVisitorObj {
 /// Native `ffi.VisitInterrupt` object containing the interruption payload.
 #[repr(C)]
 #[derive(Object)]
-#[type_key = "ffi.VisitInterrupt"]
-#[type_index(unsafe TVMFFITypeIndex::kTVMFFIVisitInterrupt)]
-#[type_final]
+#[object(
+    type_key = "ffi.VisitInterrupt",
+    unsafe type_index = TVMFFITypeIndex::kTVMFFIVisitInterrupt,
+    final
+)]
 pub struct VisitInterruptObj {
     base_: Object,
     /// Payload returned with the interrupt, or FFI `None` for no payload.
diff --git a/rust/tvm-ffi/src/extra/visit_error_context.rs 
b/rust/tvm-ffi/src/extra/visit_error_context.rs
index f33a0c7b..965ca02b 100644
--- a/rust/tvm-ffi/src/extra/visit_error_context.rs
+++ b/rust/tvm-ffi/src/extra/visit_error_context.rs
@@ -33,8 +33,7 @@ use crate::tvm_ffi_sys::{TVMFFIAny, TVMFFIObject, 
TVMFFITypeIndex};
 /// visit unwound through, laid out as the C++ `VisitErrorContextObj`.
 #[repr(C)]
 #[derive(Object)]
-#[type_key = "ffi.VisitErrorContext"]
-#[type_final]
+#[object(type_key = "ffi.VisitErrorContext", final)]
 pub(crate) struct VisitErrorContextObj {
     base_: Object,
     /// Visited nodes, innermost first; an entry may be `None`.
diff --git a/rust/tvm-ffi/src/function.rs b/rust/tvm-ffi/src/function.rs
index 4af45203..5f709ced 100644
--- a/rust/tvm-ffi/src/function.rs
+++ b/rust/tvm-ffi/src/function.rs
@@ -34,8 +34,7 @@ use tvm_ffi_sys::{
 /// The object behind a [`Function`].
 #[repr(C)]
 #[derive(Object)]
-#[type_key = "ffi.Function"]
-#[type_index(unsafe TVMFFITypeIndex::kTVMFFIFunction)]
+#[object(type_key = "ffi.Function", unsafe type_index = 
TVMFFITypeIndex::kTVMFFIFunction)]
 pub struct FunctionObj {
     object: Object,
     cell: TVMFFIFunctionCell,
diff --git a/rust/tvm-ffi/src/lib.rs b/rust/tvm-ffi/src/lib.rs
index ed3fd393..8dd0421a 100644
--- a/rust/tvm-ffi/src/lib.rs
+++ b/rust/tvm-ffi/src/lib.rs
@@ -295,7 +295,7 @@ pub use tvm_ffi_macros::match_any;
 /// field's type must take all-zero bytes as a value. An optional field is an
 /// `Option` of an object reference or an [`Optional`] of any other value. The 
parent's object must
 /// implement `ShallowCopy` and `UnsafeInit`, as `rs_object` types and
-/// `#[type_rs_parent]` bindings, such as the non-final ones `tvm-ffi-stubgen`
+/// `#[object(rs_parent)]` bindings, such as the non-final ones 
`tvm-ffi-stubgen`
 /// generates, do.
 ///
 /// # Registration
diff --git a/rust/tvm-ffi/src/object.rs b/rust/tvm-ffi/src/object.rs
index 70234aaf..a937e6f3 100644
--- a/rust/tvm-ffi/src/object.rs
+++ b/rust/tvm-ffi/src/object.rs
@@ -334,7 +334,7 @@ impl<T: ObjectCore> Clone for ObjectArc<T> {
 /// `Send + Sync` when `T`'s fields are.
 ///
 /// Implement it with the `immutable` option of [`rs_object`](crate::rs_object)
-/// or `#[type_immutable]` on [`derive(Object)`](crate::derive::Object), which
+/// or `#[object(immutable)]` on [`derive(Object)`](crate::derive::Object), 
which
 /// reject `writable` fields.
 ///
 /// # Safety
diff --git a/rust/tvm-ffi/src/reflection/access_path.rs 
b/rust/tvm-ffi/src/reflection/access_path.rs
index 3c65e44b..9eb2bcd5 100644
--- a/rust/tvm-ffi/src/reflection/access_path.rs
+++ b/rust/tvm-ffi/src/reflection/access_path.rs
@@ -48,8 +48,7 @@ pub enum AccessKind {
 /// The object behind an [`AccessStep`], laid out as the C++ `AccessStepObj`.
 #[repr(C)]
 #[derive(Object)]
-#[type_key = "ffi.reflection.AccessStep"]
-#[type_final]
+#[object(type_key = "ffi.reflection.AccessStep", final)]
 pub struct AccessStepObj {
     object: Object,
     /// What the step accesses.
@@ -68,8 +67,7 @@ pub struct AccessStep {
 /// The object behind an [`AccessPath`], laid out as the C++ `AccessPathObj`.
 #[repr(C)]
 #[derive(Object)]
-#[type_key = "ffi.reflection.AccessPath"]
-#[type_final]
+#[object(type_key = "ffi.reflection.AccessPath", final)]
 pub struct AccessPathObj {
     object: Object,
     /// The path without its last step; `None` at the root.
diff --git a/rust/tvm-ffi/src/reflection/enum_def.rs 
b/rust/tvm-ffi/src/reflection/enum_def.rs
index 15996a1d..63278daf 100644
--- a/rust/tvm-ffi/src/reflection/enum_def.rs
+++ b/rust/tvm-ffi/src/reflection/enum_def.rs
@@ -57,10 +57,7 @@ use crate::{Any, AnyView, EnumRef, Error, List, Result, 
String, TYPE_ERROR, VALU
 ///
 /// #[repr(C)]
 /// #[derive(Object)]
-/// #[type_key = "my_ext.Color"]
-/// #[type_register]
-/// #[type_final]
-/// #[type_immutable]
+/// #[object(type_key = "my_ext.Color", register, final, immutable)]
 /// pub struct ColorObj {
 ///     base_: EnumObj,
 /// }
@@ -294,8 +291,7 @@ const ENUM_STATE_ATTR: &str = "__ffi_enum__";
 /// columns (`Dict<String, Dict<ObjectRef, Any>>`).
 #[repr(C)]
 #[derive(Object)]
-#[type_key = "ffi.EnumState"]
-#[type_final]
+#[object(type_key = "ffi.EnumState", final)]
 pub(crate) struct EnumStateObj {
     base_: Object,
     entries: List<Any>,
diff --git a/rust/tvm-ffi/src/reflection/registry.rs 
b/rust/tvm-ffi/src/reflection/registry.rs
index 36d61ed2..05595700 100644
--- a/rust/tvm-ffi/src/reflection/registry.rs
+++ b/rust/tvm-ffi/src/reflection/registry.rs
@@ -57,7 +57,7 @@ use crate::{Any, Error, Function, Result, INTERNAL_ERROR, 
TYPE_ERROR, VALUE_ERRO
 /// `__ffi_shallow_copy__` returns; fields that are references share their
 /// objects, as `Clone` of an `Arc` does.
 ///
-/// [`rs_object`](crate::rs_object) and `#[type_rs_parent]` on
+/// [`rs_object`](crate::rs_object) and `#[object(rs_parent)]` on
 /// [`derive(Object)`](crate::derive::Object) implement it by copying the
 /// parent with its `ShallowCopy` and cloning every other field. An
 /// `rs_object` needs its parent's object to implement it.
diff --git a/rust/tvm-ffi/src/reflection/unsafe_init.rs 
b/rust/tvm-ffi/src/reflection/unsafe_init.rs
index a9b1c9e9..d11fea85 100644
--- a/rust/tvm-ffi/src/reflection/unsafe_init.rs
+++ b/rust/tvm-ffi/src/reflection/unsafe_init.rs
@@ -48,7 +48,7 @@ use crate::{Error, INTERNAL_ERROR};
 /// [`unsafe_init`], which a JSON graph read back, `__ffi_init__` and
 /// `__ffi_new__` then fill. `#[rs_object]` implements it for its object, so 
the
 /// parent's object must implement it too: [`Object`] does, and so does a
-/// binding with `#[type_rs_parent]` on its 
[`derive(Object)`](crate::derive::Object).
+/// binding with `#[object(rs_parent)]` on its 
[`derive(Object)`](crate::derive::Object).
 ///
 /// `__ffi_init__` leaves an `init_off` field as the creator made it, so such a
 /// field's type must take all-zero bytes as a value, as a scalar, `Option<T>`
diff --git a/rust/tvm-ffi/src/string.rs b/rust/tvm-ffi/src/string.rs
index 461d6e5c..2eba2150 100644
--- a/rust/tvm-ffi/src/string.rs
+++ b/rust/tvm-ffi/src/string.rs
@@ -54,8 +54,7 @@ unsafe impl Sync for Bytes {}
 // BytesObj for heap-allocated bytes
 #[repr(C)]
 #[derive(Object)]
-#[type_key = "ffi.Bytes"]
-#[type_index(unsafe TypeIndex::kTVMFFIBytes)]
+#[object(type_key = "ffi.Bytes", unsafe type_index = TypeIndex::kTVMFFIBytes)]
 pub(crate) struct BytesObj {
     object: Object,
     data: TVMFFIByteArray,
@@ -274,8 +273,7 @@ unsafe impl Sync for String {}
 // StringObj for heap-allocated string
 #[repr(C)]
 #[derive(Object)]
-#[type_key = "ffi.String"]
-#[type_index(unsafe TypeIndex::kTVMFFIStr)]
+#[object(type_key = "ffi.String", unsafe type_index = TypeIndex::kTVMFFIStr)]
 pub(crate) struct StringObj {
     object: Object,
     data: TVMFFIByteArray,
diff --git a/rust/tvm-ffi/tests/test_array.rs b/rust/tvm-ffi/tests/test_array.rs
index 7780ec83..9c003e33 100644
--- a/rust/tvm-ffi/tests/test_array.rs
+++ b/rust/tvm-ffi/tests/test_array.rs
@@ -181,21 +181,19 @@ fn test_array_copy_on_write() {
 
     let mut array = Array::from_vec(vec![s("a"), s("b"), s("c")]);
     let shared = array.clone();
-    array.set(0, s("x")).unwrap();
+    array.set(0, s("x"));
     assert_ne!(ptr(&array), ptr(&shared));
     assert_eq!(values(&shared), ["a", "b", "c"]);
 
     let unique = ptr(&array);
-    array.set(1, s("y")).unwrap();
+    array.set(1, s("y"));
     array.truncate(2);
     assert_eq!(array.pop().unwrap().as_str(), "y");
     assert_eq!(ptr(&array), unique);
 
     array.push(s("z"));
-    array.insert(0, s("w")).unwrap();
+    array.insert(0, s("w"));
     assert_eq!(values(&array), ["w", "x", "z"]);
 
-    assert!(array.set(3, s("q")).is_err());
-    assert!(array.insert(4, s("q")).is_err());
     assert!(Array::<String>::new().pop().is_none());
 }
diff --git a/rust/tvm-ffi/tests/test_cast.rs b/rust/tvm-ffi/tests/test_cast.rs
index a1399c59..ce6ab003 100644
--- a/rust/tvm-ffi/tests/test_cast.rs
+++ b/rust/tvm-ffi/tests/test_cast.rs
@@ -28,9 +28,7 @@ use tvm_ffi::*;
 // must have repr(C) for the object header to stay in the same position
 #[repr(C)]
 #[derive(Object)]
-#[type_key = "testing.rust.CastBase"]
-#[type_register]
-#[type_immutable]
+#[object(type_key = "testing.rust.CastBase", register, immutable)]
 struct TestBaseObj {
     base_: Object,
     value: i64,
@@ -52,10 +50,7 @@ struct TestBase {
 
 #[repr(C)]
 #[derive(Object)]
-#[type_key = "testing.rust.CastDerived"]
-#[type_register]
-#[type_final]
-#[type_immutable]
+#[object(type_key = "testing.rust.CastDerived", register, final, immutable)]
 struct TestDerivedObj {
     base_: TestBaseObj,
     extra: i64,
@@ -127,7 +122,7 @@ fn test_is_instance_of() {
 // Bindings of the C++ testing.TestObjectBase and testing.TestObjectDerived.
 #[repr(C)]
 #[derive(Object)]
-#[type_key = "testing.TestObjectBase"]
+#[object(type_key = "testing.TestObjectBase")]
 struct CxxTestBaseObj {
     base_: Object,
     v_i64: i64,
@@ -137,8 +132,7 @@ struct CxxTestBaseObj {
 
 #[repr(C)]
 #[derive(Object)]
-#[type_key = "testing.TestObjectDerived"]
-#[type_final]
+#[object(type_key = "testing.TestObjectDerived", final)]
 struct CxxTestDerivedObj {
     base_: CxxTestBaseObj,
     v_map: Map<Any, Any>,
diff --git a/rust/tvm-ffi/tests/test_enum.rs b/rust/tvm-ffi/tests/test_enum.rs
index 7aef0428..8c9baba9 100644
--- a/rust/tvm-ffi/tests/test_enum.rs
+++ b/rust/tvm-ffi/tests/test_enum.rs
@@ -27,9 +27,7 @@ use tvm_ffi::{EnumRef, ObjectArc, ObjectRefCore, Result, 
VALUE_ERROR};
 
 #[repr(C)]
 #[derive(Object)]
-#[type_key = "testing.TestEnumVariant"]
-#[type_final]
-#[type_immutable]
+#[object(type_key = "testing.TestEnumVariant", final, immutable)]
 struct TestEnumVariantObj {
     base_: EnumObj,
 }
diff --git a/rust/tvm-ffi/tests/test_list.rs b/rust/tvm-ffi/tests/test_list.rs
index 30f4e334..6a7a87a7 100644
--- a/rust/tvm-ffi/tests/test_list.rs
+++ b/rust/tvm-ffi/tests/test_list.rs
@@ -78,9 +78,8 @@ fn test_list_mutation() {
     assert!(list.capacity() >= 100);
     assert_eq!(list.get(99).unwrap(), 99);
 
-    list.set(0, -1).unwrap();
+    list.set(0, -1);
     assert_eq!(list.get(0).unwrap(), -1);
-    assert!(list.set(100, 0).is_err());
 
     list.insert(1, 7).unwrap();
     assert_eq!(list.get(1).unwrap(), 7);
@@ -147,7 +146,7 @@ fn test_list_releases_elements() {
     list.insert(0, array.clone()).unwrap();
     assert_eq!(probe.debug_strong_count().unwrap(), base + 3);
 
-    list.set(0, Array::from_vec(vec![2i64])).unwrap();
+    list.set(0, Array::from_vec(vec![2i64]));
     assert_eq!(probe.debug_strong_count().unwrap(), base + 2);
     drop(list.remove(1).unwrap());
     assert_eq!(probe.debug_strong_count().unwrap(), base + 1);
@@ -158,7 +157,7 @@ fn test_list_releases_elements() {
     list.extend([array.clone(), array.clone(), array.clone()])
         .unwrap();
     assert_eq!(probe.debug_strong_count().unwrap(), base + 3);
-    list.remove_range(0, 2).unwrap();
+    list.remove_range(0..2);
     assert_eq!(probe.debug_strong_count().unwrap(), base + 1);
     list.reverse();
     list.truncate(0);
@@ -268,12 +267,10 @@ fn test_list_bulk_edits() {
     list.extend(std::iter::empty()).unwrap();
     assert!(list.insert_many(list.len() + 1, [0i64]).is_err());
 
-    list.remove_range(2, 4).unwrap();
+    list.remove_range(2..4);
     assert_eq!(list.len(), len);
     assert_eq!(list.get(2).unwrap(), 2);
-    list.remove_range(1, 1).unwrap();
-    assert!(list.remove_range(3, 2).is_err());
-    assert!(list.remove_range(0, list.len() + 1).is_err());
+    list.remove_range(1..1);
 
     list.truncate(3);
     assert_eq!(
@@ -390,7 +387,7 @@ fn test_list_mismatched_element_is_reported() {
     // Another handle to the same object puts a `String` where an `i64` is 
expected.
     let ints: List<i64> = [1i64, 2, 3].into_iter().collect();
     let as_any: List<Any> = Any::from(ints.clone()).try_into().unwrap();
-    as_any.set(1, Any::from(String::from("x"))).unwrap();
+    as_any.set(1, Any::from(String::from("x")));
 
     // `get` and the iterator report that element instead of truncating.
     assert!(ints.get(1).is_err());
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 a06bee7d..e94e2869 100644
--- a/rust/tvm-ffi/tests/test_match_any_ast_lookup.rs
+++ b/rust/tvm-ffi/tests/test_match_any_ast_lookup.rs
@@ -25,10 +25,12 @@ use tvm_ffi::{
 
 #[repr(C)]
 #[derive(Object)]
-#[type_key = "testing.match_any.Expr"]
-#[type_register]
-#[type_child_slots = 20]
-#[type_child_slots_can_overflow = false]
+#[object(
+    type_key = "testing.match_any.Expr",
+    register,
+    child_slots = 20,
+    child_slots_can_overflow = false
+)]
 struct ExprObj {
     base_: ObjectBase,
 }
@@ -44,9 +46,7 @@ macro_rules! define_expr_leaves {
         $(
             #[repr(C)]
             #[derive(Object)]
-            #[type_key = $type_key]
-            #[type_register]
-            #[type_final]
+            #[object(type_key = $type_key, register, final)]
             struct $object {
                 base_: ExprObj,
             }
diff --git a/rust/tvm-ffi/tests/test_object_reflection.rs 
b/rust/tvm-ffi/tests/test_object_reflection.rs
index 669a35a6..7d0f54cf 100644
--- a/rust/tvm-ffi/tests/test_object_reflection.rs
+++ b/rust/tvm-ffi/tests/test_object_reflection.rs
@@ -25,16 +25,18 @@ use tvm_ffi_sys::{
     TVMFFIMethodInfo, TVMFFISEqHashKind, TVMFFITypeInfo,
 };
 
-// Rust-defined types whose `#[reflect]` fields and `#[type_reflection]`
+// Rust-defined types whose `#[reflect]` fields and `reflection`
 // methods reach the type table, so that the C++ structural engines, `repr`,
 // deep copy and other languages see them.
 
 #[repr(C)]
 #[derive(Object)]
-#[type_key = "testing.rust.ReflectedPoint"]
-#[type_register]
-#[type_s_eq_hash_kind = "TreeNode"]
-#[type_reflection(PointObj::reflect_methods)]
+#[object(
+    type_key = "testing.rust.ReflectedPoint",
+    register,
+    s_eq_hash_kind = "TreeNode",
+    reflection = PointObj::reflect_methods
+)]
 struct PointObj {
     base_: Object,
     /// The x coordinate.
@@ -81,9 +83,7 @@ struct Point {
 
 #[repr(C)]
 #[derive(Object)]
-#[type_key = "testing.rust.ReflectedPoint3"]
-#[type_register]
-#[type_final]
+#[object(type_key = "testing.rust.ReflectedPoint3", register, final)]
 struct Point3Obj {
     base_: PointObj,
     #[reflect]
@@ -297,10 +297,12 @@ fn test_methods_are_called_through_the_type_table() {
 
 #[repr(C)]
 #[derive(Object)]
-#[type_key = "testing.rust.ReflectedTally"]
-#[type_register]
-#[type_final]
-#[type_reflection(Self::reflect_methods)]
+#[object(
+    type_key = "testing.rust.ReflectedTally",
+    register,
+    final,
+    reflection = Self::reflect_methods
+)]
 struct TallyObj {
     base_: Object,
     count: i64,
@@ -477,9 +479,7 @@ fn test_shallow_copy_and_finality_are_type_attributes() {
 
 #[repr(C)]
 #[derive(Object)]
-#[type_key = "testing.rust.Unreflected"]
-#[type_register]
-#[type_final]
+#[object(type_key = "testing.rust.Unreflected", register, final)]
 struct CounterObj {
     base_: Object,
     count: i64,
@@ -505,8 +505,7 @@ fn test_types_without_reflect_register_no_reflection() {
 mod no_imports {
     #[repr(C)]
     #[derive(tvm_ffi::derive::Object)]
-    #[type_key = "testing.rust.ReflectedNoImports"]
-    #[type_register]
+    #[object(type_key = "testing.rust.ReflectedNoImports", register)]
     pub(super) struct LeafObj {
         base_: super::PointObj,
         #[reflect]
@@ -538,9 +537,7 @@ fn test_derives_need_nothing_in_scope() {
 
 #[repr(C)]
 #[derive(Object)]
-#[type_key = "testing.rust.DuplicateMethod"]
-#[type_register]
-#[type_reflection(Self::reflect_methods)]
+#[object(type_key = "testing.rust.DuplicateMethod", register, reflection = 
Self::reflect_methods)]
 struct DuplicateMethodObj {
     base_: Object,
 }
diff --git a/rust/tvm-ffi/tests/test_object_registration.rs 
b/rust/tvm-ffi/tests/test_object_registration.rs
index 3966b457..a18dd0b4 100644
--- a/rust/tvm-ffi/tests/test_object_registration.rs
+++ b/rust/tvm-ffi/tests/test_object_registration.rs
@@ -22,15 +22,17 @@ use tvm_ffi::*;
 use tvm_ffi_sys::{TVMFFIByteArray, TVMFFIGetTypeInfo, TVMFFITypeKeyToIndex};
 
 // Type keys that nothing else registers: deriving `Object` with
-// `#[type_register]` registers them when the test binary loads, under their
+// `#[object(register)]` registers them when the test binary loads, under their
 // parents, as C++ `TVM_FFI_DECLARE_OBJECT_INFO` does.
 
 #[repr(C)]
 #[derive(Object)]
-#[type_key = "testing.rust.RegisteredBase"]
-#[type_register]
-#[type_child_slots = 2]
-#[type_child_slots_can_overflow = false]
+#[object(
+    type_key = "testing.rust.RegisteredBase",
+    register,
+    child_slots = 2,
+    child_slots_can_overflow = false
+)]
 struct RegisteredBaseObj {
     base_: Object,
     value: i64,
@@ -44,9 +46,7 @@ struct RegisteredBase {
 
 #[repr(C)]
 #[derive(Object)]
-#[type_key = "testing.rust.RegisteredLeaf"]
-#[type_register]
-#[type_final]
+#[object(type_key = "testing.rust.RegisteredLeaf", register, final)]
 struct RegisteredLeafObj {
     base_: RegisteredBaseObj,
     extra: i64,
@@ -111,10 +111,10 @@ fn test_registered_object_types_round_trip_through_any() {
 }
 
 // A type that binds a key its defining library registers, without
-// `#[type_register]`, does not register it.
+// `#[object(register)]`, does not register it.
 #[repr(C)]
 #[derive(Object)]
-#[type_key = "testing.rust.NeverRegistered"]
+#[object(type_key = "testing.rust.NeverRegistered")]
 struct UnregisteredBindingObj {
     base_: Object,
 }
diff --git a/rust/tvm-ffi/tests/test_rs_object.rs 
b/rust/tvm-ffi/tests/test_rs_object.rs
index e648198e..9f68e23e 100644
--- a/rust/tvm-ffi/tests/test_rs_object.rs
+++ b/rust/tvm-ffi/tests/test_rs_object.rs
@@ -108,8 +108,7 @@ pub struct RsScalars {
 
 #[repr(C)]
 #[derive(Object)]
-#[type_key = "testing.TestCxxClassBase"]
-#[type_rs_parent]
+#[object(type_key = "testing.TestCxxClassBase", rs_parent)]
 pub struct TestCxxClassBaseObj {
     base_: Object,
     pub v_i64: i64,
diff --git a/rust/tvm-ffi/tests/test_structural_mutate.rs 
b/rust/tvm-ffi/tests/test_structural_mutate.rs
index 36b0e6c2..61b7ea95 100644
--- a/rust/tvm-ffi/tests/test_structural_mutate.rs
+++ b/rust/tvm-ffi/tests/test_structural_mutate.rs
@@ -189,9 +189,11 @@ impl RecursiveEntryMutator {
 
 #[repr(C)]
 #[derive(tvm_ffi::derive::Object)]
-#[type_key = "testing.rust.MutationRemapNode"]
-#[type_register]
-#[type_s_eq_hash_kind = "DAGNode"]
+#[object(
+    type_key = "testing.rust.MutationRemapNode",
+    register,
+    s_eq_hash_kind = "DAGNode"
+)]
 struct RemapNodeObj {
     base_: Object,
     #[reflect]
@@ -206,8 +208,7 @@ struct RemapNode {
 
 #[repr(C)]
 #[derive(tvm_ffi::derive::Object)]
-#[type_key = "testing.rust.MutationFieldNode"]
-#[type_register]
+#[object(type_key = "testing.rust.MutationFieldNode", register)]
 struct FieldNodeObj {
     base_: Object,
     a: Array<i64>,
@@ -704,7 +705,7 @@ fn owned_entry_mode_is_forwarded_by_generated_callbacks() {
                 assert_eq!(owner.debug_strong_count(), Some(count));
                 let result = change.value_or_unchanged(owner);
                 if let Some(alias) = alias {
-                    assert_eq!(Array::<i64>::try_from(alias)?.get(0)?, 1);
+                    assert_eq!(Array::<i64>::try_from(alias)?[0], 1);
                 }
                 Ok(UnchangedOr::changed(result))
             })
@@ -917,7 +918,7 @@ fn 
consuming_default_descent_transfers_between_contexts_without_node_borrows() {
         structural_mutate(true, &mut inner)?;
         let result = inner.result;
         if let Some(alias) = alias {
-            assert_eq!(alias.get(0)?, 1);
+            assert_eq!(alias[0], 1);
         }
         result.try_into()
     }
@@ -1118,7 +1119,7 @@ fn 
callback_errors_preserve_message_and_add_object_context() {
     );
     #[repr(C)]
     #[derive(tvm_ffi::derive::Object)]
-    #[type_key = "ffi.VisitErrorContext"]
+    #[object(type_key = "ffi.VisitErrorContext")]
     struct VisitErrorContextObj {
         base_: Object,
     }
diff --git a/tests/python/test_stubgen_rust.py 
b/tests/python/test_stubgen_rust.py
index 1406698e..c42c1da6 100644
--- a/tests/python/test_stubgen_rust.py
+++ b/tests/python/test_stubgen_rust.py
@@ -377,7 +377,7 @@ pub struct Pair {{
 {NO_METADATA}
 #[repr(C)]
 #[derive(tvm_ffi::derive::Object)]
-#[type_key = "demo.Pair"]
+#[object(type_key = "demo.Pair")]
 pub struct PairObj {{
     base_: Object,
 }}
@@ -445,14 +445,14 @@ def test_render_root_object() -> None:
 
 def test_render_object_without_fields_has_no_impl() -> None:
     text, imports = _render(_info("demo.Marker", is_final=True))
-    assert "#[type_final]" in text
+    assert '#[object(type_key = "demo.Marker", final)]' in text
     assert "impl MarkerObj" not in text
     assert "FieldGetter" not in _uses(imports)
 
 
 def test_render_thread_safe_only_when_not_mutable() -> None:
     """A class C++ declares not ``_type_mutable`` is ``ImmutableObject``; 
others are not."""
-    marker = "#[type_immutable]"
+    marker = ", immutable)]"
     assert marker in _render(_info("demo.Marker", is_final=True, 
is_mutable=False))[0]
     assert marker not in _render(_info("demo.Marker", is_final=True, 
is_mutable=True))[0]
     assert marker not in _render(_info("demo.Marker", is_final=True))[0]
@@ -468,7 +468,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 ", 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::impl_object_ref_upcast!(Add => BaseExpr, 
Add => Expr);")
@@ -525,7 +525,7 @@ use tvm_ffi::enum_ref::EnumObj;
 #[allow(dead_code)]
 #[repr(C)]
 #[derive(tvm_ffi::derive::Object)]
-#[type_key = "ffi.IntEnum"]
+#[object(type_key = "ffi.IntEnum")]
 pub struct FfiIntEnumObj {
     base_: EnumObj,
 }
@@ -535,7 +535,7 @@ pub struct FfiIntEnumObj {
 #[allow(dead_code)]
 #[repr(C)]
 #[derive(tvm_ffi::derive::Object)]
-#[type_key = "ffi.StrEnum"]
+#[object(type_key = "ffi.StrEnum")]
 pub struct FfiStrEnumObj {
     base_: EnumObj,
 }"""
@@ -601,8 +601,7 @@ impl TryFrom<i64> for IterVarType {{
 {NO_METADATA}
 #[repr(C)]
 #[derive(tvm_ffi::derive::Object)]
-#[type_key = "tirx.IterVar"]
-#[type_final]
+#[object(type_key = "tirx.IterVar", final)]
 pub struct IterVarObj {{
     base_: PrimExprConvertibleObj,
 }}
@@ -758,8 +757,7 @@ pub struct Expr {
 /// Complete: reflected fields fill [24, 40) exactly.
 #[repr(C)]
 #[derive(tvm_ffi::derive::Object)]
-#[type_key = "ir.Expr"]
-#[type_rs_parent]
+#[object(type_key = "ir.Expr", rs_parent)]
 pub struct ExprObj {
     base_: Object,
     pub span: Option<Span>,
@@ -798,8 +796,7 @@ pub struct Add {
 /// Complete: reflected fields fill [40, 56) exactly.
 #[repr(C)]
 #[derive(tvm_ffi::derive::Object)]
-#[type_key = "tirx.Add"]
-#[type_final]
+#[object(type_key = "tirx.Add", final)]
 pub struct AddObj {
     base_: ExprObj,
     pub a: PrimExpr,
@@ -1029,7 +1026,7 @@ def test_final_type_has_no_unsafe_init() -> None:
     """No Rust type derives from a final type, which needs no `UnsafeInit`."""
     info = _info("demo.Leaf", (_field("a", "int", 24, 8),), total_size=32, 
is_final=True)
     text, _ = _render(info)
-    assert "#[type_rs_parent]" not in text
+    assert ", rs_parent)]" not in text
 
 
 def test_complete_list_field_mirrors() -> None:
@@ -1495,7 +1492,7 @@ def test_registry_complete_chain_is_mirrored() -> None:
         "            TestCxxClassDerivedDerivedObj::new(v_i64, v_i32, v_f64, 
v_f32, v_str.into(), v_bool);\n"
     ) in dd
     # Not final, under generated ancestors: a Rust subtype can copy its 
parent's object.
-    assert "#[type_rs_parent]\npub struct TestCxxClassDerivedDerivedObj {" in 
dd
+    assert ", rs_parent)]\npub struct TestCxxClassDerivedDerivedObj {" in dd
     assert dd.endswith(
         "tvm_ffi::impl_object_ref_upcast!(TestCxxClassDerivedDerived => 
TestCxxClassBase, "
         "TestCxxClassDerivedDerived => TestCxxClassDerived);"
@@ -1563,7 +1560,7 @@ def test_cli_init_generates_a_module_tree(tmp_path: Path, 
monkeypatch: pytest.Mo
     assert (
         "use tvm_ffi::reflection::FieldGetter;" in text
     )  # the opaque fixtures read through getters
-    assert '#[type_key = "testing.TestCxxClassDerivedDerived"]' in text
+    assert '#[object(type_key = "testing.TestCxxClassDerivedDerived"' in text
     # Without a directive for the narrow `v_i32`, the chain is opaque.
     assert (
         "pub struct TestCxxClassDerivedDerivedObj {\n    base_: 
TestCxxClassDerivedObj,\n}" in text
@@ -1888,7 +1885,7 @@ def 
test_stage_3_checks_dependencies_across_the_run(tmp_path: Path) -> None:
     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
-    assert "#[type_rs_parent]" not in text
+    assert ", rs_parent)]" not in text
 
 
 # ---------------------------------------------------------------------------

Reply via email to