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 ec4dc714 [Stubgen] Rust directives for allow-missing-docs and closed 
enums (#950)
ec4dc714 is described below

commit ec4dc7143feff7a12c06db39068899fd9349d466
Author: Tianqi Chen <[email protected]>
AuthorDate: Fri Nov 6 19:26:28 2026 -0500

    [Stubgen] Rust directives for allow-missing-docs and closed enums (#950)
    
    Two opt-in Rust stubgen directives:
    
    - `tvm-ffi-stubgen(missing-docs): allow` puts `#[allow(missing_docs)]`
    on every generated type and `impl` of the file, so a crate can enable
    `#![warn(missing_docs)]` and document the generated types by hand.
    - `closed` before the member list of an `enum` directive emits a
    `#[repr(<int>)]` Rust enum of exactly those members, for value sets the
    ABI fixes, instead of the open integer newtype.
    
    Without either, the generated code is unchanged.
---
 examples/rust_stubgen/README.md                  | 14 ++++++++
 python/tvm_ffi/stub/rust_generator/codegen.py    | 38 ++++++++++++++++++++-
 python/tvm_ffi/stub/rust_generator/consts.py     | 11 +++++-
 python/tvm_ffi/stub/rust_generator/directives.py | 43 ++++++++++++++++++------
 tests/python/test_stubgen_rust.py                | 37 ++++++++++++++++++++
 5 files changed, 131 insertions(+), 12 deletions(-)

diff --git a/examples/rust_stubgen/README.md b/examples/rust_stubgen/README.md
index b089b79a..ed67c858 100644
--- a/examples/rust_stubgen/README.md
+++ b/examples/rust_stubgen/README.md
@@ -118,6 +118,20 @@ wraps it in `Option` (`// tvm-ffi-stubgen(nullable): 
rust_stubgen.IntPair.a`),
 and `upcast` adds a conversion to a hand-written typed view
 (`// tvm-ffi-stubgen(upcast): rust_stubgen.IntPair -> MyView`).
 
+When the C++ side fixes the value set in its ABI, `closed` before the member
+list makes `enum` emit a `#[repr(i32)]` Rust enum instead. Reading a value
+outside the set is undefined behavior, so the set should include 0, the value 
of
+a field never set:
+
+```rust
+// tvm-ffi-stubgen(enum): rust_stubgen.IntPair.kind -> PairKind(i32) closed { 
Unordered=0, Ordered=1 }
+```
+
+A crate built with `#![warn(missing_docs)]` adds
+`// tvm-ffi-stubgen(missing-docs): allow` to a file: every generated type and
+`impl` in it gets `#[allow(missing_docs)]`, and docs written above a block
+still document its first item.
+
 ## Partial generation
 
 Every type an object refers to (its parent, its ancestors, the types of its
diff --git a/python/tvm_ffi/stub/rust_generator/codegen.py 
b/python/tvm_ffi/stub/rust_generator/codegen.py
index e4378814..fbd58c4b 100644
--- a/python/tvm_ffi/stub/rust_generator/codegen.py
+++ b/python/tvm_ffi/stub/rust_generator/codegen.py
@@ -330,6 +330,10 @@ class _ObjectRenderer:
 
     # --- pieces ------------------------------------------------------------
 
+    def _allow_missing_docs(self) -> list[str]:
+        """``#[allow(missing_docs)]`` under ``missing-docs: allow``, for a 
crate that warns on it."""
+        return ["#[allow(missing_docs)]"] if 
self.imports.directives.allow_missing_docs else []
+
     def _accessor_lines(self, field: NamedTypeSchema) -> list[str]:
         """One ``pub fn <field>(&self) -> Result<T>`` through the C ABI getter.
 
@@ -375,15 +379,41 @@ class _ObjectRenderer:
         ]
 
     def _enum_lines(self, spec: EnumSpec) -> list[str]:
-        """Render the open integer newtype an ``enum`` directive declares."""
+        """Render the type an ``enum`` directive declares: an open integer 
newtype, or a closed enum."""
         error = self.imports.record("tvm_ffi::Error")
         value_error = self.imports.record("tvm_ffi::VALUE_ERROR")
         result = self.imports.record("tvm_ffi::Result")
+        allow = self._allow_missing_docs()
+        if spec.closed:
+            message = f'&format!("{spec.name} has no value {{value}}")'
+            rejected = _call_lines(
+                f"            _ => Err({error}::new(", [value_error, message, 
'""'], ")),"
+            )
+            return [
+                *allow,
+                "#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]",
+                f"#[repr({spec.repr})]",
+                f"pub enum {spec.name} {{",
+                *[f"    {member} = {value}," for member, value in 
spec.members],
+                "}",
+                "",
+                f"impl TryFrom<i64> for {spec.name} {{",
+                f"    type Error = {error};",
+                f"    fn try_from(value: i64) -> {result}<Self> {{",
+                "        match value {",
+                *[f"            {value} => Ok(Self::{member})," for member, 
value in spec.members],
+                *rejected,
+                "        }",
+                "    }",
+                "}",
+            ]
         return [
+            *allow,
             "#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]",
             "#[repr(transparent)]",
             f"pub struct {spec.name}({spec.repr});",
             "",
+            *allow,
             "#[allow(non_upper_case_globals)]",
             f"impl {spec.name} {{",
             *[f"    pub const {member}: Self = Self({value});" for member, 
value in spec.members],
@@ -435,6 +465,7 @@ class _ObjectRenderer:
     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}"]',
@@ -546,8 +577,10 @@ class _ObjectRenderer:
         ]
         own = [rust_ident(name) for name, _ in params[len(inherited) :]]
         forward = [_forward(rust_ident(name), rust_type) for name, rust_type 
in params]
+        allow = self._allow_missing_docs()
         sections = [
             [
+                *allow,
                 f"impl {self.obj_struct} {{",
                 *self._fn_lines(
                     "pub(crate) fn new" if self.info.is_final else "pub fn 
new",
@@ -561,6 +594,7 @@ class _ObjectRenderer:
         custom = self.type_key in self.imports.directives.custom_new
         sections.append(
             [
+                *allow,
                 f"impl {self.leaf} {{",
                 "    /// Lossless complete-field allocation.",
                 *self._fn_lines(
@@ -677,6 +711,7 @@ class _ObjectRenderer:
         sections.append(self._struct_lines(verdict, base))
         sections.append(
             [
+                *self._allow_missing_docs(),
                 "#[repr(C)]",
                 "#[derive(tvm_ffi::derive::ObjectRef, Clone)]",
                 f"pub struct {self.leaf} {{",
@@ -694,6 +729,7 @@ class _ObjectRenderer:
                 accessors += self._accessor_lines(field)
             sections.append(
                 [
+                    *self._allow_missing_docs(),
                     f"impl {self.obj_struct} {{",
                     *[f"    {line}" if line else "" for line in accessors],
                     "}",
diff --git a/python/tvm_ffi/stub/rust_generator/consts.py 
b/python/tvm_ffi/stub/rust_generator/consts.py
index 5db26ab8..6dab7590 100644
--- a/python/tvm_ffi/stub/rust_generator/consts.py
+++ b/python/tvm_ffi/stub/rust_generator/consts.py
@@ -20,7 +20,16 @@ from __future__ import annotations
 
 #: One-line directives the Rust backend consumes.
 RUST_DIRECTIVE_KINDS = frozenset(
-    {"import-object", "field", "nullable", "enum", "opaque", "upcast", 
"custom-new"}
+    {
+        "import-object",
+        "field",
+        "nullable",
+        "enum",
+        "opaque",
+        "upcast",
+        "custom-new",
+        "missing-docs",
+    }
 )
 
 #: Default FFI-origin -> Rust-type map; ``::`` paths get a ``use``, bare names 
do not.
diff --git a/python/tvm_ffi/stub/rust_generator/directives.py 
b/python/tvm_ffi/stub/rust_generator/directives.py
index ef2037ac..c322bbc7 100644
--- a/python/tvm_ffi/stub/rust_generator/directives.py
+++ b/python/tvm_ffi/stub/rust_generator/directives.py
@@ -16,7 +16,8 @@
 # under the License.
 """The Rust backend's one-line directives: payload grammar and per-file 
storage.
 
-Three address one reflected field as ``<type_key>.<field>``, three address a 
type::
+Three address one reflected field as ``<type_key>.<field>``, three address a 
type,
+and one applies to the whole file::
 
     // tvm-ffi-stubgen(field): tirx.Add.a -> PrimExpr
     // tvm-ffi-stubgen(nullable): ir.Expr.span
@@ -24,14 +25,18 @@ Three address one reflected field as 
``<type_key>.<field>``, three address a typ
     // tvm-ffi-stubgen(opaque): ir.SourceName
     // tvm-ffi-stubgen(upcast): tirx.Add -> PrimExpr
     // tvm-ffi-stubgen(custom-new): tirx.Add
+    // tvm-ffi-stubgen(missing-docs): allow
 
 ``field`` sets the field's Rust type (a name in scope, or a ``::`` path to
 ``use``); on a field inherited from an ancestor it narrows the allocator
 parameter instead. ``nullable`` wraps it in ``Option``; ``enum`` declares an 
open
-integer newtype for it; ``opaque`` keeps a type opaque even when its layout is
-reproducible. ``upcast`` adds a typed view outside the ancestor chain;
-``custom-new`` says the wrapper's ``new`` is hand-written; the generated one is
-named ``from_fields`` instead.
+integer newtype for it, or with ``closed`` before the member list a
+``#[repr(<int>)]`` enum of exactly those members; ``opaque`` keeps a type 
opaque
+even when its layout is reproducible. ``upcast`` adds an upcast to a reference
+outside the ancestor chain; ``custom-new`` says the wrapper's ``new`` is
+hand-written; the generated one is named ``from_fields`` instead.
+``missing-docs: allow`` puts ``#[allow(missing_docs)]`` on the file's generated
+types and ``impl`` blocks.
 """
 
 from __future__ import annotations
@@ -41,18 +46,24 @@ import re
 
 _ENUM_RE = re.compile(
     
r"^(?P<target>\S+)\s*->\s*(?P<name>[A-Za-z_]\w*)\((?P<repr>[iu](?:8|16|32|64))\)"
-    r"\s*(?:\{(?P<body>[^{}]*)\})?$"
+    r"\s*(?:(?P<closed>closed\s*)?\{(?P<body>[^{}]*)\})?$"
 )
 _MEMBER_RE = re.compile(r"^(?P<name>[A-Za-z_]\w*)\s*=\s*(?P<value>-?\d+)$")
 
 
 @dataclasses.dataclass(frozen=True)
 class EnumSpec:
-    """An ``enum`` directive: the newtype's name, its integer repr, and its 
members."""
+    """An ``enum`` directive: the type's name, its integer repr, and its 
members.
+
+    ``closed`` emits a Rust ``enum``: reading any other value from a mirrored 
field is
+    undefined behavior, so it is for value sets fixed by the ABI that include 
0, the
+    value of a field an object zero-filled at creation never set.
+    """
 
     name: str
     repr: str
     members: tuple[tuple[str, int], ...]
+    closed: bool = False
 
 
 @dataclasses.dataclass
@@ -65,6 +76,7 @@ class Directives:
     opaque: set[str] = dataclasses.field(default_factory=set)
     upcasts: dict[str, list[str]] = dataclasses.field(default_factory=dict)
     custom_new: set[str] = dataclasses.field(default_factory=set)
+    allow_missing_docs: bool = False
 
     def add(self, name: str, payload: str, lineno: int) -> None:
         """Parse and store one directive; raise ``ValueError`` on a malformed 
payload."""
@@ -83,6 +95,10 @@ class Directives:
             self.upcasts.setdefault(_type_target(name, lhs, lineno), 
[]).append(rust_type)
         elif name == "custom-new":
             self.custom_new.add(_type_target(name, payload, lineno))
+        elif name == "missing-docs":
+            if payload.strip() != "allow":  # the one level so far
+                raise _invalid(name, lineno, "allow")
+            self.allow_missing_docs = True
         else:
             raise ValueError(f"Unknown directive `{name}` at line {lineno}")
 
@@ -116,8 +132,11 @@ def _split_arrow(name: str, payload: str, lineno: int, 
expected: str) -> tuple[s
 
 
 def _parse_enum(payload: str, lineno: int) -> tuple[str, EnumSpec]:
-    """Parse ``<type_key>.<field> -> Name(i32) { A=0, B=1 }`` (the member list 
is optional)."""
-    expected = "<type_key>.<field> -> Name(i32) { A=0, B=1 }"
+    """Parse ``<type_key>.<field> -> Name(i32) [closed] { A=0, B=1 }``.
+
+    The member list is optional unless ``closed``.
+    """
+    expected = "<type_key>.<field> -> Name(i32) [closed] { A=0, B=1 }"
     match = _ENUM_RE.match(payload.strip())
     if match is None:
         raise _invalid("enum", lineno, expected)
@@ -129,5 +148,9 @@ def _parse_enum(payload: str, lineno: int) -> tuple[str, 
EnumSpec]:
         if member is None:
             raise _invalid("enum", lineno, expected)
         members.append((member.group("name"), int(member.group("value"))))
+    closed = match.group("closed") is not None
+    if closed and not members:
+        raise _invalid("enum", lineno, expected)
     target = _field_target("enum", match.group("target"), lineno)
-    return target, EnumSpec(match.group("name"), match.group("repr"), 
tuple(members))
+    spec = EnumSpec(match.group("name"), match.group("repr"), tuple(members), 
closed)
+    return target, spec
diff --git a/tests/python/test_stubgen_rust.py 
b/tests/python/test_stubgen_rust.py
index 445ba40a..0cbcfa83 100644
--- a/tests/python/test_stubgen_rust.py
+++ b/tests/python/test_stubgen_rust.py
@@ -353,6 +353,7 @@ def 
test_generator_declares_its_directives_and_records_imports() -> None:
         "opaque",
         "upcast",
         "custom-new",
+        "missing-docs",
     }
     imports = RUST.new_imports()
     RUST.add_directive(imports, "import-object", 
"tvm_ffi.libinfo.Foo;False;_Foo", 1)
@@ -955,6 +956,42 @@ def test_render_complete_enum_field() -> None:
     assert "tvm_ffi::FieldGetter" not in _uses(imports)
 
 
+def test_allow_missing_docs_and_closed_enum() -> None:
+    """`missing-docs: allow` marks every generated type and `impl`; `closed` 
emits a Rust enum."""
+    info = _info(
+        "demo.Pair",
+        (_field("a", "int", 24, 8), _field("kind", "int", 32, 4)),
+        total_size=40,
+        is_final=True,
+    )
+    imports = RUST.new_imports()
+    RUST.add_directive(imports, "missing-docs", " allow ", 1)
+    RUST.add_directive(imports, "enum", "demo.Pair.kind -> Kind(i32) closed { 
A=0, B=-1 }", 2)
+    text, _ = _render(info, imports)
+    assert text.startswith(
+        "#[allow(missing_docs)]\n"
+        "#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]\n"
+        "#[repr(i32)]\n"
+        "pub enum Kind {\n    A = 0,\n    B = -1,\n}\n"
+    )
+    assert (
+        "        match value {\n"
+        "            0 => Ok(Self::A),\n"
+        "            -1 => Ok(Self::B),\n"
+        '            _ => Err(Error::new(VALUE_ERROR, &format!("Kind has no 
value {value}"), "")),\n'
+        "        }\n"
+    ) in text
+    # The enum, the object struct, the wrapper and both allocator `impl`s.
+    assert text.count("#[allow(missing_docs)]\n") == 5
+    with pytest.raises(ValueError, match="closed"):
+        Directives().add("enum", "demo.Pair.kind -> Kind(i32) closed {}", 3)
+    for payload in ("", "warn"):
+        with pytest.raises(
+            ValueError, match=r"`missing-docs` directive at line 4\. Expected 
`allow`"
+        ):
+            Directives().add("missing-docs", payload, 4)
+
+
 def test_complete_optional_field_mirrors() -> None:
     """`Optional<T>` fields mirror their C++ layout: a 16-byte cell or a 
nullable pointer."""
     info = _info(

Reply via email to