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 ee81cc80 [Rust] Generated constructors take object references as `impl 
IntoObjectRef<T>` (#956)
ee81cc80 is described below

commit ee81cc80f609c4fc83c5b69cd0120e462d987706
Author: Linzhang Li <[email protected]>
AuthorDate: Sat Nov 7 08:05:06 2026 -0500

    [Rust] Generated constructors take object references as `impl 
IntoObjectRef<T>` (#956)
    
    ## Summary
    
    This PR makes the constructors that `tvm-ffi-stubgen` generates take an
    object reference as
    `impl IntoObjectRef<T>` instead of `impl Into<T>`. This covers the
    wrapper's `new`, and
    `from_fields` under `custom-new`. Values keep `impl Into<T>`, and
    scalars stay their own type,
    so the rule depends only on the type and is the same for generated and
    hand-written APIs:
    
    - An object reference is `impl IntoObjectRef<T>`.
    - A value is `impl Into<T>`.
    - A scalar is itself.
    
    ```rust
    // Before
    pub fn new(span: impl Into<Option<Span>>, ty: impl Into<PrimType>,
               a: impl Into<PrimExpr>, b: impl Into<PrimExpr>) -> Self
    
    // After
    pub fn new(span: impl Into<Option<Span>>, ty: impl IntoObjectRef<PrimType>,
               a: impl IntoObjectRef<PrimExpr>, b: impl 
IntoObjectRef<PrimExpr>) -> Self
    ```
    
    `IntoObjectRef<T>` is the conversion that knows about borrowing. `&x` is
    a borrow that does not
    change a reference count. `into_owned_object_ref()` moves an owned value
    and clones only a
    borrowed one. A stored argument therefore costs what it cost with
    `Into`. Before this change,
    a type with a generated `from_fields` and a hand-written `new` used two
    traits for the same
    arguments.
---
 python/tvm_ffi/stub/rust_generator/codegen.py | 105 +++++++++++++++-------
 python/tvm_ffi/stub/rust_generator/consts.py  |  24 ++++-
 rust/tvm-ffi-macros/src/rs_object.rs          |   4 +-
 rust/tvm-ffi/src/lib.rs                       |   6 +-
 tests/python/test_stubgen_rust.py             | 123 +++++++++++++++++++++++---
 5 files changed, 217 insertions(+), 45 deletions(-)

diff --git a/python/tvm_ffi/stub/rust_generator/codegen.py 
b/python/tvm_ffi/stub/rust_generator/codegen.py
index 9f5917a8..3d3b8b83 100644
--- a/python/tvm_ffi/stub/rust_generator/codegen.py
+++ b/python/tvm_ffi/stub/rust_generator/codegen.py
@@ -24,9 +24,10 @@ the verdict of :mod:`tvm_ffi.stub.layout`:
 - *complete*: the struct mirrors every physical field at its real offset and
   width, pinned by a ``const`` size/alignment assertion. ``<Leaf>Obj::new``
   and the wrapper's ``new`` take every field root to leaf: the former each as
-  its own type, the latter a scalar as its own type and anything else as
-  ``impl Into``. ``custom-new`` leaves the wrapper's ``new`` to hand-written
-  code and names the generated one ``from_fields``. A type that is not final
+  its own type, the latter a scalar as its own type, an object reference as
+  ``impl IntoObjectRef`` and anything else as ``impl Into``. ``custom-new``
+  leaves the wrapper's ``new`` to hand-written code and names the generated
+  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 implements ``ShallowCopy`` and
   ``UnsafeInit`` when every ancestor's is generated too (a ``ty-map``'d one is
@@ -59,7 +60,7 @@ module, a ``crate::`` path anywhere else.
 from __future__ import annotations
 
 import dataclasses
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, NamedTuple
 
 from .. import consts as C
 from ..layout import Verdict, classify
@@ -77,6 +78,8 @@ if TYPE_CHECKING:
     from collections.abc import Iterable, Mapping
     from pathlib import Path
 
+    from tvm_ffi.core import TypeSchema
+
     from ..file_utils import CodeBlock
     from ..utils import InitConfig, NamedTypeSchema, ObjectInfo, Options
     from .directives import EnumSpec
@@ -103,14 +106,52 @@ def _call_lines(open_: str, items: list[str], close: str) 
-> list[str]:
     ]
 
 
-def _param_type(rust_type: str) -> str:
-    """Type a parameter of the wrapper's allocator: a scalar as itself, 
anything else as ``impl Into``."""
-    return rust_type if rust_type in C_RUST.RUST_EXACT_PARAMS else f"impl 
Into<{rust_type}>"
+class _Param(NamedTuple):
+    """A parameter of an allocator: one physical field, root to leaf."""
+
+    name: str
+    #: The type ``<Leaf>Obj::new`` takes: the field's mirror, or a ``field`` 
directive's narrowing.
+    rust_type: str
+    #: Whether the reflected field is an object reference 
(:func:`_is_object_ref`), not an enum.
+    of_object: bool
+
+    @property
+    def is_object(self) -> bool:
+        """Whether ``rust_type`` is an object reference; a ``nullable`` one is 
an ``Option``."""
+        return self.of_object and not self.rust_type.startswith("Option<")
 
 
-def _forward(ident: str, rust_type: str) -> str:
-    """Pass a parameter of the wrapper's allocator to ``<Leaf>Obj::new``, 
which takes ``rust_type``."""
-    return ident if rust_type in C_RUST.RUST_EXACT_PARAMS else 
f"{ident}.into()"
+def _is_object_ref(schema: TypeSchema) -> bool:
+    """Whether the Rust type of ``schema`` is an object reference.
+
+    A builtin origin is one when listed in ``RUST_OBJECT_REF_ORIGINS``; a type 
key
+    outside ``ffi.*`` always is, as a generated or ``ty-map``'d binding.
+    """
+    origin = schema.origin
+    if origin in C_RUST.RUST_OBJECT_REF_ORIGINS:
+        return True
+    return "." in origin and not origin.startswith("ctypes.") and not 
is_crate_type_key(origin)
+
+
+def _param_type(param: _Param, imports: RustImports) -> str:
+    """Type a parameter of the wrapper's allocator.
+
+    A scalar as itself, an object reference as ``impl IntoObjectRef`` 
(recording its ``use``,
+    which also brings ``into_owned_object_ref`` into scope), anything else as 
``impl Into``.
+    """
+    if param.rust_type in C_RUST.RUST_EXACT_PARAMS:
+        return param.rust_type
+    if param.is_object:
+        return f"impl 
{imports.record('tvm_ffi::IntoObjectRef')}<{param.rust_type}>"
+    return f"impl Into<{param.rust_type}>"
+
+
+def _forward(param: _Param) -> str:
+    """Pass a parameter of the wrapper's allocator to ``<Leaf>Obj::new``, 
which takes its mirror."""
+    ident = rust_ident(param.name)
+    if param.rust_type in C_RUST.RUST_EXACT_PARAMS:
+        return ident
+    return f"{ident}.into_owned_object_ref()" if param.is_object else 
f"{ident}.into()"
 
 
 def _check_width(target: str, field: NamedTypeSchema, rust_type: str, width: 
int) -> None:
@@ -522,13 +563,13 @@ class _ObjectRenderer:
 
     def _allocator_params(
         self, key: str, info: ObjectInfo, narrowed: frozenset[str] = 
frozenset()
-    ) -> list[tuple[str, str]]:
-        """``(field, type)`` of every physical field root to leaf, as 
``<key>Obj::new`` takes.
+    ) -> list[_Param]:
+        """Every physical field root to leaf, as ``<key>Obj::new`` takes it.
 
         ``narrowed`` names the fields a subtype of ``key`` narrows with a 
``field`` directive.
         """
         parent = info.parent_type_key
-        inherited: list[tuple[str, str]] = []
+        inherited: list[_Param] = []
         if parent is not None and self._provider(parent) != "crate":
             inherited = self._allocator_params(
                 parent, object_info_from_type_key(parent), narrowed | 
self._narrowed_names(key)
@@ -539,9 +580,9 @@ class _ObjectRenderer:
         self,
         key: str,
         info: ObjectInfo,
-        inherited: list[tuple[str, str]],
+        inherited: list[_Param],
         narrowed: frozenset[str] = frozenset(),
-    ) -> list[tuple[str, str]]:
+    ) -> list[_Param]:
         """Extend the parent's parameters with ``key``'s own fields by offset.
 
         A ``field`` directive on an inherited field narrows that parameter; the
@@ -553,13 +594,15 @@ class _ObjectRenderer:
             return RustImports() if name in narrowed else self.imports
 
         params = [
-            (name, self._narrowed(key, name, rust_type, imports(name)))
-            for name, rust_type in inherited
+            p._replace(rust_type=self._narrowed(key, p.name, p.rust_type, 
imports(p.name)))
+            for p in inherited
         ]
+        enums = self.imports.directives.enums
         for field in sorted(info.fields, key=lambda f: f.offset or 0):
             mirror = self._field_mirror(key, field, imports(field.name))
             assert mirror is not None  # complete: every field along the chain 
has a mirror
-            params.append((field.name, mirror))
+            of_object = f"{key}.{field.name}" not in enums and 
_is_object_ref(field)
+            params.append(_Param(field.name, mirror, of_object))
         return params
 
     def _narrowed_names(self, key: str) -> frozenset[str]:
@@ -577,7 +620,7 @@ class _ObjectRenderer:
     def _fn_lines(
         self,
         head: str,
-        params: list[tuple[str, str]],
+        params: list[_Param],
         call: tuple[str, list[str]],
         init: list[str],
         *,
@@ -585,15 +628,16 @@ class _ObjectRenderer:
     ) -> list[str]:
         """Render ``<head>(<params>) -> Self { let <call>; Self { <init> } }`` 
in an ``impl`` block.
 
-        ``generic`` takes a parameter that is not a scalar as ``impl Into`` 
(:func:`_param_type`).
+        ``generic`` takes a parameter that is not a scalar as ``impl 
IntoObjectRef`` or
+        ``impl Into`` (:func:`_param_type`).
         """
         plist = [
-            f"{rust_ident(name)}: {_param_type(rust_type) if generic else 
rust_type}"
-            for name, rust_type in params
+            f"{rust_ident(p.name)}: {_param_type(p, self.imports) if generic 
else p.rust_type}"
+            for p in params
         ]
         binding, args = call
         return [
-            *[f"    {line}" for line in _allow_non_snake_case(rust_ident(n) 
for n, _ in params)],
+            *[f"    {line}" for line in 
_allow_non_snake_case(rust_ident(p.name) for p in params)],
             *_call_lines(f"    {head}(", plist, ") -> Self {"),
             *_call_lines(f"        let {binding}(", args, ");"),
             *_call_lines("        Self { ", init, " }"),
@@ -605,13 +649,14 @@ class _ObjectRenderer:
 
         ``<Leaf>Obj::new`` takes every field as its own type, so the parent's, 
generated
         or hand-written, takes what it hands on. The wrapper's ``new`` takes a 
scalar
-        field as its own type and any other as ``impl Into`` 
(:func:`_param_type`), as
-        ``#[rs_object]``'s ``new`` does, and converts it on the way to 
``<Leaf>Obj::new``.
+        field as its own type, as ``#[rs_object]``'s ``new`` does, an object 
reference as
+        ``impl IntoObjectRef`` and any other as ``impl Into`` 
(:func:`_param_type`), and
+        converts it on the way to ``<Leaf>Obj::new``.
         ``custom-new`` names the wrapper's allocator ``from_fields`` instead.
         ``<Leaf>Obj::new`` of a type that is not final is public: a subtype 
defined in Rust
         builds its parent's object with it.
         """
-        inherited: list[tuple[str, str]] = []
+        inherited: list[_Param] = []
         if has_parent:
             parent = self.info.parent_type_key
             assert parent is not None
@@ -620,11 +665,11 @@ class _ObjectRenderer:
             )
         params = self._level_params(self.type_key, self.info, inherited)
         to_parent = [
-            f"{rust_ident(name)}.into()" if rust_type != parent_type else 
rust_ident(name)
-            for (name, rust_type), (_, parent_type) in zip(params, inherited)
+            f"{rust_ident(p.name)}.into()" if p.rust_type != q.rust_type else 
rust_ident(p.name)
+            for p, q in zip(params, inherited)
         ]
-        own = [rust_ident(name) for name, _ in params[len(inherited) :]]
-        forward = [_forward(rust_ident(name), rust_type) for name, rust_type 
in params]
+        own = [rust_ident(p.name) for p in params[len(inherited) :]]
+        forward = [_forward(p) for p in params]
         allow = self._allow_missing_docs()
         sections = [
             [
diff --git a/python/tvm_ffi/stub/rust_generator/consts.py 
b/python/tvm_ffi/stub/rust_generator/consts.py
index 99631218..fd0b9fab 100644
--- a/python/tvm_ffi/stub/rust_generator/consts.py
+++ b/python/tvm_ffi/stub/rust_generator/consts.py
@@ -96,12 +96,32 @@ RUST_SCALAR_WIDTHS = {
 }
 
 #: Parameters of the wrapper's allocator taken as their own type, as 
``#[rs_object]``'s
-#: ``new`` takes them: a literal infers only from its exact type. Any other 
parameter is
-#: ``impl Into<T>``; ``<Leaf>Obj::new`` takes every one as its own type.
+#: ``new`` takes them: a literal infers only from its exact type. An object 
reference is
+#: ``impl IntoObjectRef<T>`` and any other parameter ``impl Into<T>``; 
``<Leaf>Obj::new``
+#: takes every one as its own type.
 RUST_EXACT_PARAMS = frozenset(
     {"bool", "i8", "i16", "i32", "i64", "isize", "u8", "u16", "u32", "u64", 
"usize", "f32", "f64"}
 )
 
+#: Builtin origins whose Rust type is an object reference, taken as ``impl 
IntoObjectRef<T>``.
+#: A type key outside ``ffi.*`` is one too: a generated or ``ty-map``'d 
binding. ``Shape``,
+#: like ``Array`` / ``List`` / ``Map``, is a container taken as ``impl 
Into<T>``, which also
+#: accepts what it is built from (``From<impl AsRef<[i64]>>``).
+RUST_OBJECT_REF_ORIGINS = frozenset(
+    {
+        "Object",
+        "Tensor",
+        "Callable",
+        "ffi.Object",
+        "ffi.Tensor",
+        "ffi.Function",
+        "ffi.Module",
+        "ffi.Error",
+        "ffi.reflection.AccessPath",
+        "ffi.reflection.AccessStep",
+    }
+)
+
 #: Size of an object reference field; ``nullable`` may only wrap those.
 RUST_POINTER_SIZE = 8
 
diff --git a/rust/tvm-ffi-macros/src/rs_object.rs 
b/rust/tvm-ffi-macros/src/rs_object.rs
index 506442de..da3e0ddd 100644
--- a/rust/tvm-ffi-macros/src/rs_object.rs
+++ b/rust/tvm-ffi-macros/src/rs_object.rs
@@ -156,7 +156,9 @@ fn expand(args: &RsObjectArgs, item: &DeriveInput) -> 
syn::Result<TokenStream2>
     let base_id = &base.ident;
     let base_ty = &base.ty;
     // `new` takes a scalar as its own type, so that a literal infers as it, 
and any
-    // other field as `impl Into`.
+    // other field as `impl Into`. Tokens alone do not tell an object 
reference from a
+    // value, so this object-level allocator does not take `impl 
IntoObjectRef` as a
+    // reference's constructor does.
     let params = fields.iter().map(|field| {
         let (name, ty) = (&field.ident, &field.ty);
         if is_exact_param(ty) {
diff --git a/rust/tvm-ffi/src/lib.rs b/rust/tvm-ffi/src/lib.rs
index 2e027dd8..423bb96a 100644
--- a/rust/tvm-ffi/src/lib.rs
+++ b/rust/tvm-ffi/src/lib.rs
@@ -244,7 +244,11 @@ pub use tvm_ffi_macros::match_any;
 ///   fields of both.
 /// * `MyPairObj::new(base_, fields...)`, which takes the fields in order: a
 ///   scalar (`bool`, an integer or a float) as its own type, so that a literal
-///   infers as it, and any other field from anything that converts into it.
+///   infers as it, and any other field as `impl Into<T>`. This is the
+///   object-level allocator, which sees only tokens and so cannot tell an
+///   object reference from a value; a constructor on the reference takes an
+///   object reference as [`impl IntoObjectRef<T>`](IntoObjectRef), as the
+///   ones `tvm-ffi-stubgen` generates do.
 ///
 /// # Arguments
 ///
diff --git a/tests/python/test_stubgen_rust.py 
b/tests/python/test_stubgen_rust.py
index 14d48e5a..7c56f1d8 100644
--- a/tests/python/test_stubgen_rust.py
+++ b/tests/python/test_stubgen_rust.py
@@ -748,8 +748,8 @@ impl ExprObj {
 
 impl Expr {
     /// Lossless complete-field allocation.
-    pub fn new(span: impl Into<Option<Span>>, ty: impl Into<Type>) -> Self {
-        let obj = ExprObj::new(span.into(), ty.into());
+    pub fn new(span: impl Into<Option<Span>>, ty: impl IntoObjectRef<Type>) -> 
Self {
+        let obj = ExprObj::new(span.into(), ty.into_owned_object_ref());
         Self { data_: ObjectArc::new(obj) }
     }
 }
@@ -817,11 +817,16 @@ impl Add {
     /// Lossless complete-field allocation.
     pub fn new(
         span: impl Into<Option<Span>>,
-        ty: impl Into<PrimType>,
-        a: impl Into<PrimExpr>,
-        b: impl Into<PrimExpr>,
+        ty: impl IntoObjectRef<PrimType>,
+        a: impl IntoObjectRef<PrimExpr>,
+        b: impl IntoObjectRef<PrimExpr>,
     ) -> Self {
-        let obj = AddObj::new(span.into(), ty.into(), a.into(), b.into());
+        let obj = AddObj::new(
+            span.into(),
+            ty.into_owned_object_ref(),
+            a.into_owned_object_ref(),
+            b.into_owned_object_ref(),
+        );
         Self { data_: ObjectArc::new(obj) }
     }
 }
@@ -838,6 +843,7 @@ def test_render_complete_expr_golden() -> None:
     assert _uses(imports) == {
         "std::ptr::addr_of_mut",
         "tvm_ffi::Object",
+        "tvm_ffi::IntoObjectRef",
         "tvm_ffi::ObjectArc",
         "tvm_ffi::reflection::ShallowCopy",
         "tvm_ffi::reflection::UnsafeInit",
@@ -876,6 +882,7 @@ def test_render_complete_add_golden() -> None:
     assert text == ADD_EXPECTED
     assert _uses(imports) == {
         "std::ops::Deref",
+        "tvm_ffi::IntoObjectRef",
         "tvm_ffi::ObjectArc",
         "crate::ir::ExprObj",
         "crate::ir::Expr",
@@ -1216,18 +1223,112 @@ def test_custom_new_renames_the_wrapper_allocator() -> 
None:
     assert (
         "impl Add {\n    /// Lossless complete-field allocation.\n"
         "    pub fn from_fields(\n"
-        "        span: impl Into<Span>,\n"
-        "        ty: impl Into<Type>,\n"
-        "        a: impl Into<Expr>,\n"
-        "        b: impl Into<Expr>,\n"
+        "        span: impl IntoObjectRef<Span>,\n"
+        "        ty: impl IntoObjectRef<Type>,\n"
+        "        a: impl IntoObjectRef<Expr>,\n"
+        "        b: impl IntoObjectRef<Expr>,\n"
         "    ) -> Self {\n"
-        "        let obj = AddObj::new(span.into(), ty.into(), a.into(), 
b.into());\n"
+        "        let obj = AddObj::new(\n"
+        "            span.into_owned_object_ref(),\n"
+        "            ty.into_owned_object_ref(),\n"
+        "            a.into_owned_object_ref(),\n"
+        "            b.into_owned_object_ref(),\n"
+        "        );\n"
         "        Self { data_: ObjectArc::new(obj) }\n"
         "    }\n}"
     ) in text
     assert "    pub fn new(" not in text
 
 
+def test_wrapper_allocator_takes_object_references_as_into_object_ref() -> 
None:
+    """An object reference is `impl IntoObjectRef`, a value `impl Into`, a 
scalar itself.
+
+    A `field` directive narrowing an object reference keeps it one; `nullable` 
makes it an
+    `Option`, a value. `Shape` is a container, taken as `impl Into` like 
`Array`.
+    """
+    optional_var = TypeSchema("Optional", (TypeSchema("demo.Var"),))
+    info = _info(
+        "demo.Node",
+        (
+            _field("tensor", "Tensor", 24, 8),
+            _field("owner", "Object", 32, 8),
+            _field("callback", "Callable", 40, 8),
+            _field("module", "ffi.Module", 48, 8),
+            _field("var", "demo.Var", 56, 8),
+            _field("buffer", "demo.Var", 64, 8),
+            _field("span", "demo.Span", 72, 8),
+            _field("name", "str", 80, 16),
+            _field("items", "Array", 96, 8),
+            _field("attrs", "Map", 104, 8),
+            _field("shape", "Shape", 112, 8),
+            _field("value", "Any", 120, 16),
+            _field("kind", "int", 136, 8),
+            _field("parent", optional_var, 144, 8),
+            _field("dtype", "dtype", 152, 4, 2),
+            _field("count", "int", 156, 4),
+        ),
+        total_size=160,
+        is_final=True,
+    )
+    imports = RUST.new_imports()
+    RUST.add_directive(imports, "field", "demo.Node.buffer -> 
crate::typed::BufferVar", 1)
+    RUST.add_directive(imports, "nullable", "demo.Node.span", 2)
+    RUST.add_directive(imports, "enum", "demo.Node.kind -> Kind(i64) { A=0 }", 
3)
+    text, imports = _render(info, imports)
+    assert (
+        "    pub fn new(\n"
+        "        tensor: impl IntoObjectRef<Tensor>,\n"
+        "        owner: impl IntoObjectRef<ObjectRef>,\n"
+        "        callback: impl IntoObjectRef<Function>,\n"
+        "        module: impl IntoObjectRef<Module>,\n"
+        "        var: impl IntoObjectRef<Var>,\n"
+        "        buffer: impl IntoObjectRef<BufferVar>,\n"
+        "        span: impl Into<Option<Span>>,\n"
+        "        name: impl Into<String>,\n"
+        "        items: impl Into<Array<Any>>,\n"
+        "        attrs: impl Into<Map<Any, Any>>,\n"
+        "        shape: impl Into<Shape>,\n"
+        "        value: impl Into<Any>,\n"
+        "        kind: impl Into<Kind>,\n"
+        "        parent: impl Into<Option<Var>>,\n"
+        "        dtype: impl Into<DLDataType>,\n"
+        "        count: i32,\n"
+        "    ) -> Self {\n"
+        "        let obj = NodeObj::new(\n"
+        "            tensor.into_owned_object_ref(),\n"
+        "            owner.into_owned_object_ref(),\n"
+        "            callback.into_owned_object_ref(),\n"
+        "            module.into_owned_object_ref(),\n"
+        "            var.into_owned_object_ref(),\n"
+        "            buffer.into_owned_object_ref(),\n"
+        "            span.into(),\n"
+        "            name.into(),\n"
+        "            items.into(),\n"
+        "            attrs.into(),\n"
+        "            shape.into(),\n"
+        "            value.into(),\n"
+        "            kind.into(),\n"
+        "            parent.into(),\n"
+        "            dtype.into(),\n"
+        "            count,\n"
+        "        );\n"
+    ) in text, text
+    assert "tvm_ffi::IntoObjectRef" in _uses(imports)
+
+
+def 
test_wrapper_allocator_without_object_references_does_not_import_into_object_ref()
 -> None:
+    info = _info(
+        "demo.Pair",
+        (_field("name", "str", 24, 16), _field("count", "int", 40, 8)),
+        total_size=48,
+        is_final=True,
+    )
+    text, imports = _render(info)
+    assert "    pub fn new(name: impl Into<String>, count: i64) -> Self {" in 
text
+    assert "IntoObjectRef" not in text
+    assert "tvm_ffi::IntoObjectRef" not in _uses(imports)
+
+
 def test_allocator_body_wraps_like_rustfmt() -> None:
     """Past 100 columns a `let` first moves its call to the next line; the 
rest go one per line."""
 

Reply via email to