This is an automated email from the ASF dual-hosted git repository.
tlopex 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 24cfa44c [STUBGEN][RUST] Generate complete object mirrors and
allocators (#740)
24cfa44c is described below
commit 24cfa44ca425ece2f2547ae87b81fea2835ddebd
Author: Linzhang Li <[email protected]>
AuthorDate: Fri Sep 4 17:58:14 2026 -0400
[STUBGEN][RUST] Generate complete object mirrors and allocators (#740)
## Summary
Attach the layout classifier (#730) to the Rust backend. A type whose
layout the registry proves is now bound *complete*: a `#[repr(C)]`
struct with every physical field at its reflected offset and width, a
`const` size/alignment assertion, and a lossless allocator
(`<Leaf>Obj::new` crate-private, `<Leaf>::new` public). Everything else
keeps the opaque form of #738. Three new directives: `opaque` vetoes a
reproducible layout, `upcast` adds a hand-written typed view,
`custom-new` renames the generated allocator to `from_complete_fields`
so a hand-written `new` can own the name.
## Motivation
This is what tvm-rust-ext's `STUBGEN_FEEDBACK.md` asks for: ordinary
data nodes allocated in Rust from their complete fields, polymorphic and
unreflected-byte types kept opaque. Generating `ir.` / `tirx.` /
`arith.` / `target.` from tvm-rust-ext's `libtvm_compiler.so` gives 131
types (98 complete, 33 opaque) that compile against the crate without
edits; the differences from the hand-written bindings are exactly what
the directives cover.
## Changes
- `stub/layout.py`: `classify(..., unmirrored=...)` and the `no-mirror`
reason, so types under a builtin parent (`ffi.Enum`, ...) stay opaque:
their base is only a header-only stand-in.
- `rust_generator/codegen.py`: classify each object with its ancestors;
mirror fields (scalars by reflected width, `Optional<T>` as `Option<T>`
or `tvm_ffi::Optional<T>` by payload, directive widths checked against
the field size); render the complete struct and its allocators; `upcast`
and `custom-new` handling.
- `rust_generator/directives.py`, `consts.py`: the three directives and
the width tables.
- `rust_generator/utils.py`: a reflected field named `base` or `data` is
spelled `base_` / `data_`, since those are the generated struct's own
members (TVM has `tirx.Ramp.base`, `tirx.DeclBuffer.data`).
- `examples/rust_stubgen/`: `IntPair` is now a plain data object,
allocated from Rust with the generated `IntPair::new` and read back by
C++.
## Testing
`test_stubgen_rust.py` (36 cases), `test_stubgen.py`,
`test_stub_layout.py`: 121 passed; ruff clean. The example regenerates
an identical `mod.rs` and `cargo run` prints `a=1 b=2 kind=PairKind(1)`
and `sum=3`.
---------
Signed-off-by: yuchuan <[email protected]>
---
examples/rust_stubgen/README.md | 28 +-
.../rust/src/generated/rust_stubgen/mod.rs | 33 +-
examples/rust_stubgen/rust/src/main.rs | 15 +-
examples/rust_stubgen/src/int_pair.cc | 16 +-
python/tvm_ffi/stub/layout.py | 16 +-
python/tvm_ffi/stub/rust_generator/codegen.py | 328 ++++++++--
python/tvm_ffi/stub/rust_generator/consts.py | 50 +-
python/tvm_ffi/stub/rust_generator/directives.py | 49 +-
python/tvm_ffi/stub/rust_generator/utils.py | 8 +-
tests/python/test_stub_layout.py | 10 +
tests/python/test_stubgen_rust.py | 664 ++++++++++++++++++---
11 files changed, 1019 insertions(+), 198 deletions(-)
diff --git a/examples/rust_stubgen/README.md b/examples/rust_stubgen/README.md
index 080b5d6a..42b2758c 100644
--- a/examples/rust_stubgen/README.md
+++ b/examples/rust_stubgen/README.md
@@ -22,11 +22,19 @@ into Rust bindings. This example registers one object,
`rust_stubgen.IntPair`
(`src/int_pair.cc`), and lets CMake regenerate `rust/src/generated/` after
every build.
-Every object is bound *opaquely*: Rust gets a `#[repr(C)]` wrapper that embeds
-only the parent, a reference type, `Deref`, the upcasts along the ancestor
-chain, and one accessor per reflected field that reads through the C ABI
-getter. The object's bytes are never reproduced, so the binding is correct for
-any registered type; construction goes through the registered global functions.
+Every object gets a `#[repr(C)]` wrapper, a reference type, `Deref`, and the
+upcasts along its ancestor chain. `IntPair` is plain data, so its reflected
+fields account for every byte and the binding is *complete*: the struct mirrors
+the fields at their real offsets and widths, a `const` assertion pins its size
+and alignment to the reflected facts, and a generated `new` allocates the
object
+in Rust. `main.rs` builds one that way, reads `pair.a` directly, and hands it
to
+a C++ function that reads it back.
+
+An object whose layout cannot be reproduced (a polymorphic one, say, with a
+vtable in front of the object header) is bound *opaquely* instead: the struct
+embeds only the parent, every field is read through an accessor that calls the
+C ABI getter, and construction stays on the C++ side.
+
A builtin parent such as `ffi.IntEnum` has no `<Leaf>Obj` in the crate; the
import section defines a header-only stand-in per builtin ancestor so the
derived type depth matches the registry.
@@ -56,6 +64,10 @@ open newtype:
// tvm-ffi-stubgen(enum): rust_stubgen.IntPair.kind -> PairKind(i32) {
Unordered=0, Ordered=1 }
```
-Two more are available: `field` names the Rust type of a field's accessor
-(`// tvm-ffi-stubgen(field): rust_stubgen.IntPair.a -> MyInt`) and `nullable`
-wraps it in `Option` (`// tvm-ffi-stubgen(nullable): rust_stubgen.IntPair.a`).
+Four more directives are available: `field` names the Rust type of a field
+(`// tvm-ffi-stubgen(field): rust_stubgen.IntPair.a -> MyInt`), `nullable`
+wraps it in `Option` (`// tvm-ffi-stubgen(nullable): rust_stubgen.IntPair.a`),
+`upcast` adds a conversion to a hand-written typed view
+(`// tvm-ffi-stubgen(upcast): rust_stubgen.IntPair -> MyView`), and
+`custom-new` names the generated allocator `from_complete_fields` when `new`
+is hand-written (`// tvm-ffi-stubgen(custom-new): rust_stubgen.IntPair`).
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 4e7f47e6..d1085be2 100644
--- a/examples/rust_stubgen/rust/src/generated/rust_stubgen/mod.rs
+++ b/examples/rust_stubgen/rust/src/generated/rust_stubgen/mod.rs
@@ -23,16 +23,14 @@
// tvm-ffi-stubgen(begin): import-section
use std::ops::Deref;
use tvm_ffi::Error;
-use tvm_ffi::FieldGetter;
use tvm_ffi::Object;
use tvm_ffi::ObjectArc;
-use tvm_ffi::ObjectCore;
use tvm_ffi::Result;
use tvm_ffi::VALUE_ERROR;
// tvm-ffi-stubgen(end)
-// The `kind` field is an integer on the C++ side; this directive gives it an
-// open integer newtype in Rust and makes the accessor return it.
+// The `kind` field is an integer on the C++ side; this directive types it as
an
+// open integer newtype in Rust.
// tvm-ffi-stubgen(enum): rust_stubgen.IntPair.kind -> PairKind(i32) {
Unordered=0, Ordered=1 }
// tvm-ffi-stubgen(begin): object/rust_stubgen.IntPair
@@ -61,14 +59,23 @@ 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]
pub struct IntPairObj {
base: Object,
+ pub a: i64,
+ pub b: i64,
+ pub kind: PairKind,
}
+const _: () = {
+ assert!(::core::mem::size_of::<IntPairObj>() == 48);
+ assert!(::core::mem::align_of::<IntPairObj>() == 8);
+};
+
#[repr(C)]
#[derive(tvm_ffi::derive::ObjectRef, Clone)]
pub struct IntPair {
@@ -83,17 +90,17 @@ impl Deref for IntPair {
}
impl IntPairObj {
- pub fn a(&self) -> Result<i64> {
- FieldGetter::new(Self::type_index(), "a")?.get(self)
- }
-
- pub fn b(&self) -> Result<i64> {
- FieldGetter::new(Self::type_index(), "b")?.get(self)
+ pub(crate) fn new(a: i64, b: i64, kind: PairKind) -> Self {
+ let base = Object::new();
+ Self { base, a, b, kind }
}
+}
- pub fn kind(&self) -> Result<PairKind> {
- let raw: i64 = FieldGetter::new(Self::type_index(),
"kind")?.get(self)?;
- PairKind::try_from(raw)
+impl IntPair {
+ /// Lossless complete-field allocation.
+ pub fn new(a: i64, b: i64, kind: PairKind) -> Self {
+ let obj = IntPairObj::new(a, b, kind);
+ Self { data: ObjectArc::new(obj) }
}
}
// tvm-ffi-stubgen(end)
diff --git a/examples/rust_stubgen/rust/src/main.rs
b/examples/rust_stubgen/rust/src/main.rs
index 27afeb3a..f49fadbe 100644
--- a/examples/rust_stubgen/rust/src/main.rs
+++ b/examples/rust_stubgen/rust/src/main.rs
@@ -37,20 +37,19 @@ fn lib_path() -> String {
fn main() -> Result<()> {
// Load the C++ library so `IntPair` is registered with the FFI type
registry.
- // Keep it alive for as long as the bindings are used.
+ // Keep it alive for as long as the binding is used.
let _lib = Module::load_from_file(lib_path())?;
- // The object is opaque to Rust: it is constructed by the registered C++
- // function and its fields are read through the reflection getters.
- let pair: IntPair = tvm_ffi::cached_global_func!("rust_stubgen.IntPair")
- .call_tuple((1i64, 2i64, i64::from(PairKind::Ordered.as_raw())))?
- .try_into()?;
- println!("a={} b={} kind={:?}", pair.a()?, pair.b()?, pair.kind()?);
- assert_eq!(pair.kind()?, PairKind::Ordered);
+ // The object has a reproducible layout: it is allocated in Rust and its
+ // fields are plain struct members, on both sides of the ABI.
+ let pair = IntPair::new(1, 2, PairKind::Ordered);
+ println!("a={} b={} kind={:?}", pair.a, pair.b, pair.kind);
+ assert_eq!(pair.kind, PairKind::Ordered);
let sum: i64 = tvm_ffi::cached_global_func!("rust_stubgen.IntPairSum")
.call_tuple((pair.clone(),))?
.try_into()?;
println!("sum={sum}");
+ assert_eq!(sum, 3);
Ok(())
}
diff --git a/examples/rust_stubgen/src/int_pair.cc
b/examples/rust_stubgen/src/int_pair.cc
index 3d7631b4..b758333d 100644
--- a/examples/rust_stubgen/src/int_pair.cc
+++ b/examples/rust_stubgen/src/int_pair.cc
@@ -29,9 +29,9 @@ namespace rust_stubgen {
namespace ffi = tvm::ffi;
// [object.begin]
-// A polymorphic object: the vtable in front of the object header means Rust
-// cannot mirror its bytes, so the generated binding reads every field through
-// the reflection getters and construction stays on the C++ side.
+// A plain data object: every byte is accounted for by a reflected field, so
the
+// generated binding mirrors the layout; Rust allocates it and reads the fields
+// directly, and the registered function below reads it back.
class IntPairObj : public ffi::Object {
public:
int64_t a;
@@ -39,16 +39,13 @@ class IntPairObj : public ffi::Object {
int32_t kind;
IntPairObj(int64_t a, int64_t b, int32_t kind) : a(a), b(b), kind(kind) {}
- virtual ~IntPairObj() = default;
- virtual int64_t Sum() const { return a + b; }
+ int64_t Sum() const { return a + b; }
TVM_FFI_DECLARE_OBJECT_INFO_FINAL("rust_stubgen.IntPair", IntPairObj,
ffi::Object);
};
class IntPair : public ffi::ObjectRef {
public:
- IntPair(int64_t a, int64_t b, int32_t kind) { data_ =
ffi::make_object<IntPairObj>(a, b, kind); }
-
TVM_FFI_DEFINE_OBJECT_REF_METHODS_NOTNULLABLE(IntPair, ffi::ObjectRef,
IntPairObj);
};
@@ -58,10 +55,7 @@ TVM_FFI_STATIC_INIT_BLOCK() {
.def_ro("a", &IntPairObj::a, "the first operand")
.def_ro("b", &IntPairObj::b, "the second operand")
.def_ro("kind", &IntPairObj::kind, "0 = unordered, 1 = ordered");
- refl::GlobalDef()
- .def("rust_stubgen.IntPair",
- [](int64_t a, int64_t b, int32_t kind) { return IntPair(a, b,
kind); })
- .def("rust_stubgen.IntPairSum", [](const IntPair& pair) { return
pair->Sum(); });
+ refl::GlobalDef().def("rust_stubgen.IntPairSum", [](const IntPair& pair) {
return pair->Sum(); });
}
// [object.end]
diff --git a/python/tvm_ffi/stub/layout.py b/python/tvm_ffi/stub/layout.py
index 798676a4..73c68bed 100644
--- a/python/tvm_ffi/stub/layout.py
+++ b/python/tvm_ffi/stub/layout.py
@@ -70,6 +70,7 @@ OpaqueReason: TypeAlias = Literal[
"uncovered-bytes",
"unrenderable-field",
"by-directive",
+ "no-mirror",
]
"""Why a type is opaque.
@@ -82,6 +83,8 @@ OpaqueReason: TypeAlias = Literal[
a vptr, ...).
- ``unrenderable-field``: the caller's predicate rejected a field.
- ``by-directive``: the caller vetoed a type whose layout is reproducible.
+- ``no-mirror``: the caller's target never reproduces this type's bytes
+ (a builtin its runtime owns); everything below it is ``parent-opaque``.
"""
@@ -175,6 +178,7 @@ def classify(
infos: Mapping[str, ObjectInfo],
*,
forced_opaque: AbstractSet[str] = frozenset(),
+ unmirrored: AbstractSet[str] = frozenset(),
field_renderable: Callable[[NamedTypeSchema], bool] | None = None,
) -> dict[str, Verdict]:
"""Classify every type in ``infos``, parents before children.
@@ -188,6 +192,9 @@ def classify(
Type keys vetoed by the caller (semantic blockers such as interned
identities). The veto only demotes a type that would otherwise be
complete; a type that is opaque for a layout reason keeps that reason.
+ unmirrored
+ Type keys whose bytes the target never reproduces (builtins its runtime
+ owns): opaque with reason ``no-mirror`` before any layout evidence is
weighed.
field_renderable
Predicate deciding whether a reflected field has a native mirror in the
target language. A rejected field demotes its type to opaque with
@@ -203,7 +210,9 @@ def classify(
raise KeyError(f"Ancestor {type_key!r} is not among the types to
classify")
info = infos[type_key]
parent = None if info.parent_type_key is None else
_classify(info.parent_type_key)
- verdicts[type_key] = _classify_one(info, parent, forced_opaque,
field_renderable)
+ verdicts[type_key] = _classify_one(
+ info, parent, forced_opaque, unmirrored, field_renderable
+ )
return verdicts[type_key]
for type_key in infos:
@@ -215,6 +224,7 @@ def _classify_one(
info: ObjectInfo,
parent: Verdict | None,
forced_opaque: AbstractSet[str],
+ unmirrored: AbstractSet[str],
field_renderable: Callable[[NamedTypeSchema], bool] | None,
) -> Verdict:
assert info.type_key is not None, "cannot classify an ObjectInfo without a
type key"
@@ -228,6 +238,10 @@ def _classify_one(
total_size=info.total_size,
is_final=info.is_final,
)
+ if info.type_key in unmirrored:
+ verdict.reason = "no-mirror"
+ verdict.detail = "its bytes are owned by the target's runtime and
never mirrored"
+ return verdict
outcome = _prove_layout(info, parent, verdict)
if outcome is None:
outcome = _apply_target_rules(info, forced_opaque, field_renderable)
diff --git a/python/tvm_ffi/stub/rust_generator/codegen.py
b/python/tvm_ffi/stub/rust_generator/codegen.py
index 26f2603a..78d778fb 100644
--- a/python/tvm_ffi/stub/rust_generator/codegen.py
+++ b/python/tvm_ffi/stub/rust_generator/codegen.py
@@ -14,45 +14,27 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
-"""Rust code generation for ``tvm-ffi-stubgen``: the opaque binding form.
-
-Every reflected object renders as a ``#[repr(C)]`` struct embedding only its
-parent, a reference wrapper, ``Deref``, the upcasts along the ancestor chain,
-and one accessor per reflected field that reads through the C ABI getter. The
-object's bytes are never reproduced. For ``tirx.IterVar`` deriving from
-``ir.PrimExprConvertible``::
-
- #[repr(C)]
- #[derive(tvm_ffi::derive::Object)]
- #[type_key = "tirx.IterVar"]
- #[type_final]
- pub struct IterVarObj {
- base: PrimExprConvertibleObj,
- }
-
- #[repr(C)]
- #[derive(tvm_ffi::derive::ObjectRef, Clone)]
- pub struct IterVar {
- data: ObjectArc<IterVarObj>,
- }
-
- impl Deref for IterVar { ... } // IterVar -> IterVarObj
- impl Deref for IterVarObj { ... } // IterVarObj ->
PrimExprConvertibleObj
-
- impl IterVarObj {
- pub fn dom(&self) -> Result<Option<Range>> {
- FieldGetter::new(Self::type_index(), "dom")?.get(self)
- }
- ...
- }
-
- tvm_ffi::impl_object_upcast!(IterVar => PrimExprConvertible);
-
-Construction and behaviour go through the registered global functions,
-hand-written outside the markers. A builtin parent (``ffi.IntEnum``, say) has
-no ``<Leaf>Obj`` in the crate: the import section defines a header-only
-stand-in per builtin ancestor, so ``derive(Object)`` computes the registry's
-``TYPE_DEPTH``.
+"""Rust code generation for ``tvm-ffi-stubgen``.
+
+Every reflected object gets a ``#[repr(C)]`` object struct, a reference
wrapper,
+``Deref``, and the upcasts along its ancestor chain. The struct's contents
follow
+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``
+ (crate-private) and the wrapper's ``new`` take every field root to leaf;
+ ``custom-new`` leaves the wrapper's ``new`` to hand-written code and names
+ the generated one ``from_complete_fields``.
+- *opaque*: the struct embeds only its parent, and each field is read through
+ the C ABI getter. Nothing allocates it.
+
+A field without a Rust mirror (``Optional<Any>``, ``Union``, ``void*``, ...)
+makes the type opaque and is read as ``Any``; an ``opaque`` directive vetoes a
+reproducible layout; a scalar width named by a directive is checked against the
+reflected field size. A builtin parent (``ffi.IntEnum``, say) has no
+``<Leaf>Obj`` in the crate: the import section defines a header-only stand-in
+per builtin ancestor so ``derive(Object)`` computes the registry's
+``TYPE_DEPTH``, and everything under such a parent stays opaque
(``no-mirror``).
"""
from __future__ import annotations
@@ -61,6 +43,8 @@ import dataclasses
from typing import TYPE_CHECKING
from .. import consts as C
+from ..layout import Verdict, classify
+from ..lib_state import object_info_from_type_key
from . import consts as C_RUST
from .utils import RustImports, builtin_mirror_name, render_rust_type,
rust_ident
@@ -72,6 +56,24 @@ if TYPE_CHECKING:
from .directives import EnumSpec
+def _call_lines(open_: str, items: list[str], close: str) -> list[str]:
+ """``open_ + items + close`` on one line, or one item per line when it
would overflow."""
+ line = f"{open_}{', '.join(items)}{close}"
+ if len(line) <= C_RUST.RUST_MAX_WIDTH:
+ return [line]
+ indent = open_[: len(open_) - len(open_.lstrip())]
+ return [open_, *[f"{indent} {item}," for item in items],
f"{indent}{close}"]
+
+
+def _check_width(target: str, field: NamedTypeSchema, rust_type: str, width:
int) -> None:
+ """Reject a directive whose scalar type does not match the reflected field
size."""
+ if field.size is not None and field.size != width:
+ raise ValueError(
+ f"Directive on `{target}` maps a {field.size}-byte field to
`{rust_type}` "
+ f"({width} bytes)"
+ )
+
+
@dataclasses.dataclass
class _ObjectRenderer:
"""Renders one ``object/<key>`` block into Rust source lines."""
@@ -100,14 +102,17 @@ class _ObjectRenderer:
# --- name resolution ---------------------------------------------------
- def _ty_render(self, origin: str) -> str | None:
+ def _resolve(self, origin: str, imports: RustImports) -> str | None:
"""Resolve a leaf origin to its in-scope Rust name (recording its
``use``), or ``None``."""
mapped = self.ty_map.get(origin)
if mapped is None:
if "." not in origin or origin.startswith("ctypes."):
return None
mapped = self._generated_type_path(origin)
- return self.imports.record(mapped)
+ return imports.record(mapped)
+
+ def _ty_render(self, origin: str) -> str | None:
+ return self._resolve(origin, self.imports)
def _generated_type_path(self, type_key: str) -> str:
"""Spell a generated type key from this file.
@@ -143,6 +148,95 @@ class _ObjectRenderer:
assert not any(self._generated(key) for key in chain), (self.type_key,
chain)
return self.imports.record_builtin_base(chain), False
+ # --- classification ----------------------------------------------------
+
+ def classify(self) -> Verdict:
+ """Classify this object with its ancestors, under the file's
directives."""
+ infos = {key: object_info_from_type_key(key) for key in
self.info.ancestors}
+ infos[self.type_key] = self.info
+ owner_of = {id(f): key for key, owner in infos.items() for f in
owner.fields}
+ scratch = RustImports()
+
+ def renderable(field: NamedTypeSchema) -> bool:
+ return self._field_mirror(owner_of[id(field)], field, scratch) is
not None
+
+ # Builtin ancestors are header-only stand-ins (`_base_type`): none
below is complete.
+ unmirrored = {
+ key
+ for key in self.info.ancestors
+ if key != C_RUST.RUST_ROOT_TYPE_KEY and not self._generated(key)
+ }
+ verdicts = classify(
+ infos,
+ forced_opaque=self.imports.directives.opaque,
+ unmirrored=unmirrored,
+ field_renderable=renderable,
+ )
+ return verdicts[self.type_key]
+
+ # --- field types -------------------------------------------------------
+
+ def _field_mirror(self, owner: str, field: NamedTypeSchema, imports:
RustImports) -> str | None:
+ """Render the ``#[repr(C)]`` mirror type of ``field``, or ``None``.
+
+ Directives win; then scalars by reflected width, ``Optional`` by C++
layout, else schema.
+ """
+ directives = self.imports.directives
+ target = f"{owner}.{field.name}"
+ enum = directives.enums.get(target)
+ if enum is not None:
+ _check_width(target, field, enum.repr,
C_RUST.RUST_SCALAR_WIDTHS[enum.repr])
+ return enum.name
+ override = directives.field_types.get(target)
+ if override is not None:
+ width = C_RUST.RUST_SCALAR_WIDTHS.get(override)
+ if width is not None:
+ _check_width(target, field, override, width)
+ mirror: str | None = imports.record(override) if "::" in override
else override
+ elif field.origin == "Optional":
+ mirror = self._optional_mirror(field, imports)
+ else:
+ narrowed = C_RUST.RUST_SCALAR_BY_SIZE.get((field.origin,
field.size))
+ mirror = narrowed or render_rust_type(field, lambda o:
self._resolve(o, imports))
+ if mirror is None:
+ return None
+ if target in directives.nullable and not mirror.startswith("Option<"):
+ if field.size not in (None, C_RUST.RUST_POINTER_SIZE):
+ raise ValueError(
+ f"`nullable` directive on `{target}`: the field is
{field.size} bytes, "
+ "not a pointer-sized object reference"
+ )
+ mirror = f"Option<{mirror}>"
+ return mirror
+
+ def _optional_mirror(self, field: NamedTypeSchema, imports: RustImports)
-> str | None:
+ """Mirror an ``Optional<T>`` field in place.
+
+ An object payload is a nullable pointer (``Option<T>``); any other
payload
+ is a 16-byte ``TVMFFIAny`` cell (``tvm_ffi::Optional<T>``).
``Optional<Any>``
+ and a size mismatch have no mirror.
+ """
+ (payload,) = field.args # TypeSchema's post_init enforces exactly one
argument.
+ if payload.origin == "Any":
+ return None
+ inner = render_rust_type(payload, lambda o: self._resolve(o, imports))
+ if inner is None:
+ return None
+ any_backed = (
+ payload.origin in C_RUST.RUST_ANY_BACKED_OPTIONAL_PAYLOADS
+ or payload.origin == "Optional"
+ )
+ expected = (
+ C_RUST.RUST_OPTIONAL_FIELD_SIZE
+ if any_backed
+ else C_RUST.RUST_OBJECT_OPTIONAL_FIELD_SIZE
+ )
+ if field.size not in (None, expected):
+ return None
+ if any_backed:
+ return f"{imports.record(C_RUST.RUST_OPTIONAL_PATH)}<{inner}>"
+ return f"Option<{inner}>"
+
# --- pieces ------------------------------------------------------------
def _accessor_lines(self, field: NamedTypeSchema) -> list[str]:
@@ -183,6 +277,7 @@ class _ObjectRenderer:
"""Render the open integer newtype an ``enum`` directive declares."""
error = self.imports.record("tvm_ffi::Error")
value_error = self.imports.record("tvm_ffi::VALUE_ERROR")
+ result = self.imports.record("tvm_ffi::Result")
return [
"#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]",
"#[repr(transparent)]",
@@ -201,7 +296,7 @@ class _ObjectRenderer:
"",
f"impl TryFrom<i64> for {spec.name} {{",
f" type Error = {error};",
- " fn try_from(value: i64) -> Result<Self> {",
+ f" fn try_from(value: i64) -> {result}<Self> {{",
f" {spec.repr}::try_from(value).map(Self).map_err(|_| {{",
f' {error}::new({value_error}, &format!("{spec.name}
value {{value}} does not fit '
f'{spec.repr}"), "")',
@@ -221,25 +316,151 @@ class _ObjectRenderer:
]
def _upcast_lines(self) -> list[str]:
- """``impl_object_upcast!`` from the wrapper to every ancestor's
wrapper."""
+ """``impl_object_upcast!`` to every ancestor's wrapper, then the
``upcast`` directives."""
targets = [
self.imports.record(self._generated_type_path(key))
for key in self.info.ancestors
if self._generated(key)
]
+ for view in self.imports.directives.upcasts.get(self.type_key, []):
+ targets.append(self.imports.record(view) if "::" in view else view)
if not targets:
return []
pairs = ", ".join(f"{self.leaf} => {target}" for target in targets)
return [f"tvm_ffi::impl_object_upcast!({pairs});"]
+ def _struct_lines(self, verdict: Verdict, base: str) -> list[str]:
+ """Render the object struct: every field when complete, the parent
alone when opaque."""
+ header = [
+ "#[repr(C)]",
+ "#[derive(tvm_ffi::derive::Object)]",
+ f'#[type_key = "{self.type_key}"]',
+ *(["#[type_final]"] if self.info.is_final else []),
+ f"pub struct {self.obj_struct} {{",
+ f" base: {base},", # a reflected `base` field becomes `base_`
(`rust_ident`)
+ ]
+ if not verdict.is_complete:
+ return [
+ f"/// Opaque: {verdict.detail}. Fields are read through the C
ABI getters.",
+ *header,
+ "}",
+ ]
+ members = []
+ for field in sorted(self.info.fields, key=lambda f: f.offset or 0):
+ mirror = self._field_mirror(self.type_key, field, self.imports)
+ assert mirror is not None # the verdict already ran the
renderability check
+ members.append(f" pub {rust_ident(field.name)}: {mirror},")
+ return [
+ f"/// Complete: {verdict.detail}.",
+ *header,
+ *members,
+ "}",
+ "",
+ "const _: () = {",
+ f" assert!(::core::mem::size_of::<{self.obj_struct}>() ==
{verdict.total_size});",
+ f" assert!(::core::mem::align_of::<{self.obj_struct}>() ==
{verdict.alignment});",
+ "};",
+ ]
+
+ # --- allocators --------------------------------------------------------
+
+ def _allocator_params(self, key: str, info: ObjectInfo) -> list[tuple[str,
str]]:
+ """``(field, type)`` of every physical field root to leaf, as
``<key>Obj::new`` takes."""
+ parent = info.parent_type_key
+ inherited: list[tuple[str, str]] = []
+ if parent is not None and self._generated(parent):
+ inherited = self._allocator_params(parent,
object_info_from_type_key(parent))
+ return self._level_params(key, info, inherited)
+
+ def _level_params(
+ self, key: str, info: ObjectInfo, inherited: list[tuple[str, str]]
+ ) -> list[tuple[str, str]]:
+ """Extend the parent's parameters with ``key``'s own fields by offset.
+
+ A ``field`` directive on an inherited field narrows that parameter; the
+ body hands it to the parent with ``.into()``.
+ """
+ params = [(name, self._narrowed(key, name, rust_type)) for name,
rust_type in inherited]
+ for field in sorted(info.fields, key=lambda f: f.offset or 0):
+ mirror = self._field_mirror(key, field, self.imports)
+ assert mirror is not None # complete: every field along the chain
has a mirror
+ params.append((field.name, mirror))
+ return params
+
+ def _narrowed(self, key: str, field_name: str, rust_type: str) -> str:
+ override =
self.imports.directives.field_types.get(f"{key}.{field_name}")
+ if override is None:
+ return rust_type
+ return self.imports.record(override) if "::" in override else override
+
+ def _fn_lines(
+ self, head: str, params: list[tuple[str, str]], call: tuple[str,
list[str]], result: str
+ ) -> list[str]:
+ """Render ``<head>(<params>) -> Self { let <call>; <result> }`` inside
an ``impl`` block."""
+ plist = [f"{rust_ident(name)}: {rust_type}" for name, rust_type in
params]
+ binding, args = call
+ return [
+ *_call_lines(f" {head}(", plist, ") -> Self {"),
+ *_call_lines(f" let {binding}(", args, ");"),
+ f" {result}",
+ " }",
+ ]
+
+ def _allocator_sections(self, base: str, has_parent: bool) ->
list[list[str]]:
+ """``<Leaf>Obj::new`` and the wrapper's ``new``.
+
+ ``custom-new`` names the wrapper's allocator ``from_complete_fields``
instead.
+ """
+ inherited: list[tuple[str, str]] = []
+ if has_parent:
+ parent = self.info.parent_type_key
+ assert parent is not None
+ inherited = self._allocator_params(parent,
object_info_from_type_key(parent))
+ 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)
+ ]
+ own = [rust_ident(f.name) for f in sorted(self.info.fields, key=lambda
f: f.offset or 0)]
+ forward = [rust_ident(name) for name, _ in params]
+ sections = [
+ [
+ f"impl {self.obj_struct} {{",
+ *self._fn_lines(
+ "pub(crate) fn new",
+ params,
+ (f"base = {base}::new", to_parent),
+ f"Self {{ {', '.join(['base', *own])} }}",
+ ),
+ "}",
+ ]
+ ]
+ custom = self.type_key in self.imports.directives.custom_new
+ sections.append(
+ [
+ f"impl {self.leaf} {{",
+ " /// Lossless complete-field allocation.",
+ *self._fn_lines(
+ "pub fn from_complete_fields" if custom else "pub fn new",
+ params,
+ (f"obj = {self.obj_struct}::new", forward),
+ "Self { data: ObjectArc::new(obj) }",
+ ),
+ "}",
+ ]
+ )
+ return sections
+
def body(self) -> list[str]:
"""Build the Rust source lines for the object."""
+ verdict = self.classify()
# Derive macros are spelled by full path: their leaves collide with
`Object` / `ObjectRef`.
self.imports.record("std::ops::Deref")
self.imports.record("tvm_ffi::ObjectArc")
base, has_parent = self._base_type()
fields = self.info.fields
- if fields:
+ has_accessors = bool(fields) and not verdict.is_complete
+ if has_accessors:
self.imports.record("tvm_ffi::ObjectCore") # `Self::type_index()`
self.imports.record("tvm_ffi::FieldGetter")
self.imports.record("tvm_ffi::Result")
@@ -251,22 +472,13 @@ class _ObjectRenderer:
for f in fields
if f"{self.type_key}.{f.name}" in enums
]
- sections.append(
- [
- "#[repr(C)]",
- "#[derive(tvm_ffi::derive::Object)]",
- f'#[type_key = "{self.type_key}"]',
- *(["#[type_final]"] if self.info.is_final else []),
- f"pub struct {self.obj_struct} {{",
- f" base: {base},",
- "}",
- ]
- )
+ sections.append(self._struct_lines(verdict, base))
sections.append(
[
"#[repr(C)]",
"#[derive(tvm_ffi::derive::ObjectRef, Clone)]",
f"pub struct {self.leaf} {{",
+ # a reflected `data` field becomes `data_` (`rust_ident`)
f" data: ObjectArc<{self.obj_struct}>,",
"}",
]
@@ -274,7 +486,7 @@ class _ObjectRenderer:
sections.append(self._deref_lines(self.leaf, self.obj_struct, "data"))
if has_parent:
sections.append(self._deref_lines(self.obj_struct, base, "base"))
- if fields:
+ if has_accessors:
accessors: list[str] = []
for i, field in enumerate(fields):
if i:
@@ -287,6 +499,8 @@ class _ObjectRenderer:
"}",
]
)
+ elif verdict.is_complete:
+ sections += self._allocator_sections(base, has_parent)
upcasts = self._upcast_lines()
if upcasts:
sections.append(upcasts)
@@ -306,7 +520,7 @@ def generate_rust_object(
opt: Options,
obj_info: ObjectInfo,
) -> None:
- """Emit the opaque Rust binding of ``obj_info`` into an ``object/<key>``
block."""
+ """Emit the Rust binding of ``obj_info`` into an ``object/<key>`` block."""
assert len(code.lines) >= 2
assert isinstance(obj_info.type_key, str)
renderer = _ObjectRenderer(
diff --git a/python/tvm_ffi/stub/rust_generator/consts.py
b/python/tvm_ffi/stub/rust_generator/consts.py
index 218def67..0636cbfb 100644
--- a/python/tvm_ffi/stub/rust_generator/consts.py
+++ b/python/tvm_ffi/stub/rust_generator/consts.py
@@ -19,7 +19,9 @@
from __future__ import annotations
#: One-line directives the Rust backend consumes.
-RUST_DIRECTIVE_KINDS = frozenset({"import-object", "field", "nullable",
"enum"})
+RUST_DIRECTIVE_KINDS = frozenset(
+ {"import-object", "field", "nullable", "enum", "opaque", "upcast",
"custom-new"}
+)
#: Default FFI-origin -> Rust-type map; ``::`` paths get a ``use``, bare names
do not.
RUST_TY_MAP_DEFAULTS = {
@@ -54,6 +56,44 @@ RUST_TY_MAP_DEFAULTS = {
#: Origins without a crate mirror; such a field is read as ``tvm_ffi::Any``.
RUST_UNSUPPORTED_ORIGINS = frozenset({"Dict", "List", "Union", "tuple"})
+#: Mirror scalar by ``(origin, reflected size)``: the schema erases widths and
signedness.
+RUST_SCALAR_BY_SIZE = {
+ ("int", 1): "i8",
+ ("int", 2): "i16",
+ ("int", 4): "i32",
+ ("int", 8): "i64",
+ ("float", 4): "f32",
+ ("float", 8): "f64",
+}
+
+#: Byte width of the scalars a ``field`` / ``enum`` directive may name;
checked against the field.
+RUST_SCALAR_WIDTHS = {
+ "i8": 1,
+ "u8": 1,
+ "bool": 1,
+ "i16": 2,
+ "u16": 2,
+ "i32": 4,
+ "u32": 4,
+ "f32": 4,
+ "i64": 8,
+ "u64": 8,
+ "f64": 8,
+}
+
+#: Size of an object reference field; ``nullable`` may only wrap those.
+RUST_POINTER_SIZE = 8
+
+#: C++ ``Optional<T>`` is a 16-byte ``TVMFFIAny`` cell, or a nullable pointer
for object payloads.
+RUST_OPTIONAL_PATH = "tvm_ffi::Optional"
+RUST_OPTIONAL_FIELD_SIZE = 16
+RUST_OBJECT_OPTIONAL_FIELD_SIZE = 8
+
+#: ``Optional`` payloads kept as the 16-byte cell (a nested ``Optional`` too);
the size is checked.
+RUST_ANY_BACKED_OPTIONAL_PAYLOADS = frozenset(
+ {"int", "float", "bool", "Device", "dtype", "DataType", "str", "bytes"}
+)
+
#: ``use``-path rewrites: builtin ``ffi.*`` type keys live at the crate root.
RUST_MOD_MAP = {
"ffi": "tvm_ffi",
@@ -71,3 +111,11 @@ RUST_KEYWORDS = frozenset(
"unsized virtual yield".split()
)
RUST_NOT_RAW_IDENTIFIERS = frozenset({"self", "Self", "super", "crate"})
+
+#: Member names the generated structs use themselves: ``base`` is the parent
slot of every object
+#: struct, ``data`` the wrapper's ``ObjectArc``. A reflected field with one of
these names would
+#: collide (or shadow through ``Deref``), so ``rust_ident`` spells it
``base_`` / ``data_``.
+RUST_RESERVED_MEMBERS = frozenset({"base", "data"})
+
+#: ``rustfmt``'s default ``max_width``; a wider allocator signature wraps one
parameter per line.
+RUST_MAX_WIDTH = 100
diff --git a/python/tvm_ffi/stub/rust_generator/directives.py
b/python/tvm_ffi/stub/rust_generator/directives.py
index 388ffb5f..185b9065 100644
--- a/python/tvm_ffi/stub/rust_generator/directives.py
+++ b/python/tvm_ffi/stub/rust_generator/directives.py
@@ -16,15 +16,22 @@
# under the License.
"""The Rust backend's one-line directives: payload grammar and per-file
storage.
-All three address one reflected field as ``<type_key>.<field>``::
+Three address one reflected field as ``<type_key>.<field>``, three address a
type::
// tvm-ffi-stubgen(field): tirx.Add.a -> PrimExpr
// tvm-ffi-stubgen(nullable): ir.Expr.span
// tvm-ffi-stubgen(enum): tirx.For.kind -> ForKind(i32) { Serial=0,
Parallel=1 }
-
-``field`` sets the accessor's Rust type (a name in scope, or a ``::`` path to
-``use``); ``nullable`` wraps it in ``Option``; ``enum`` declares an open
integer
-newtype the accessor returns.
+ // tvm-ffi-stubgen(opaque): ir.SourceName
+ // tvm-ffi-stubgen(upcast): tirx.Add -> PrimExpr
+ // tvm-ffi-stubgen(custom-new): tirx.Add
+
+``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_complete_fields`` instead.
"""
from __future__ import annotations
@@ -50,22 +57,32 @@ class EnumSpec:
@dataclasses.dataclass
class Directives:
- """The Rust directives of one file, keyed by ``<type_key>.<field>``."""
+ """The Rust directives of one file, keyed by ``<type_key>.<field>`` or
``<type_key>``."""
field_types: dict[str, str] = dataclasses.field(default_factory=dict)
nullable: set[str] = dataclasses.field(default_factory=set)
enums: dict[str, EnumSpec] = dataclasses.field(default_factory=dict)
+ opaque: set[str] = dataclasses.field(default_factory=set)
+ upcasts: dict[str, list[str]] = dataclasses.field(default_factory=dict)
+ custom_new: set[str] = dataclasses.field(default_factory=set)
def add(self, name: str, payload: str, lineno: int) -> None:
"""Parse and store one directive; raise ``ValueError`` on a malformed
payload."""
if name == "field":
- target, rust_type = _split_arrow(name, payload, lineno)
- self.field_types[target] = rust_type
+ lhs, rust_type = _split_arrow(name, payload, lineno,
"<type_key>.<field> -> <RustType>")
+ self.field_types[_field_target(name, lhs, lineno)] = rust_type
elif name == "nullable":
self.nullable.add(_field_target(name, payload, lineno))
elif name == "enum":
target, spec = _parse_enum(payload, lineno)
self.enums[target] = spec
+ elif name == "opaque":
+ self.opaque.add(_type_target(name, payload, lineno))
+ elif name == "upcast":
+ lhs, rust_type = _split_arrow(name, payload, lineno, "<type_key>
-> <RustType>")
+ self.upcasts.setdefault(_type_target(name, lhs, lineno),
[]).append(rust_type)
+ elif name == "custom-new":
+ self.custom_new.add(_type_target(name, payload, lineno))
else:
raise ValueError(f"Unknown directive `{name}` at line {lineno}")
@@ -74,6 +91,14 @@ def _invalid(name: str, lineno: int, expected: str) ->
ValueError:
return ValueError(f"Invalid `{name}` directive at line {lineno}. Expected
`{expected}`")
+def _type_target(name: str, text: str, lineno: int) -> str:
+ """Validate a ``<type_key>`` reference."""
+ target = text.strip()
+ if not target or " " in target:
+ raise _invalid(name, lineno, "<type_key>")
+ return target
+
+
def _field_target(name: str, text: str, lineno: int) -> str:
"""Validate a ``<type_key>.<field>`` reference."""
target = text.strip()
@@ -82,12 +107,12 @@ def _field_target(name: str, text: str, lineno: int) ->
str:
return target
-def _split_arrow(name: str, payload: str, lineno: int) -> tuple[str, str]:
- """Split ``<type_key>.<field> -> <rust type>``."""
+def _split_arrow(name: str, payload: str, lineno: int, expected: str) ->
tuple[str, str]:
+ """Split ``<target> -> <rust type>``; the caller validates the target."""
lhs, arrow, rhs = payload.partition("->")
if not arrow or not rhs.strip():
- raise _invalid(name, lineno, "<type_key>.<field> -> <RustType>")
- return _field_target(name, lhs, lineno), rhs.strip()
+ raise _invalid(name, lineno, expected)
+ return lhs, rhs.strip()
def _parse_enum(payload: str, lineno: int) -> tuple[str, EnumSpec]:
diff --git a/python/tvm_ffi/stub/rust_generator/utils.py
b/python/tvm_ffi/stub/rust_generator/utils.py
index c60f52d7..27510efd 100644
--- a/python/tvm_ffi/stub/rust_generator/utils.py
+++ b/python/tvm_ffi/stub/rust_generator/utils.py
@@ -133,9 +133,13 @@ def _generic(base: str | None, *params: str | None) -> str
| None:
def rust_ident(name: str) -> str:
- """Spell a reflected field name in Rust: drop the C++ trailing underscore,
escape keywords."""
+ """Spell a reflected field name in Rust: drop the C++ trailing underscore,
escape collisions.
+
+ Keywords become raw identifiers; the four that cannot, and the names the
+ generated structs use themselves (``base``, ``data``), get a trailing
underscore.
+ """
name = name.rstrip("_") or name
- if name in C.RUST_NOT_RAW_IDENTIFIERS:
+ if name in C.RUST_NOT_RAW_IDENTIFIERS or name in C.RUST_RESERVED_MEMBERS:
return f"{name}_"
if name in C.RUST_KEYWORDS:
return f"r#{name}"
diff --git a/tests/python/test_stub_layout.py b/tests/python/test_stub_layout.py
index e0a6edcb..7e4379f5 100644
--- a/tests/python/test_stub_layout.py
+++ b/tests/python/test_stub_layout.py
@@ -239,6 +239,16 @@ def test_children_of_any_opaque_parent_are_parent_opaque()
-> None:
assert "by-directive" in verdicts["t.Child"].detail
+def test_unmirrored_type_and_its_descendants_are_opaque() -> None:
+ child = _info("t.Child", parent="t.Base", total_size=48,
fields=(_field("x", 40, 8),))
+ infos = {"ffi.Object": ROOT, "t.Base": BASE, "t.Child": child}
+ verdicts = classify(infos, unmirrored={"t.Base"})
+ assert verdicts["t.Base"].reason == "no-mirror"
+ assert verdicts["t.Base"].own_bytes is None # unconditional: no layout
evidence is weighed
+ assert verdicts["t.Child"].reason == "parent-opaque"
+ assert "no-mirror" in verdicts["t.Child"].detail
+
+
def test_order_does_not_matter_but_ancestors_must_be_present() -> None:
child = _info("t.Child", parent="t.Base", total_size=48,
fields=(_field("x", 40, 8),))
verdicts = classify({"t.Child": child, "t.Base": BASE, "ffi.Object": ROOT})
diff --git a/tests/python/test_stubgen_rust.py
b/tests/python/test_stubgen_rust.py
index 9f2e0fdf..446e1ade 100644
--- a/tests/python/test_stubgen_rust.py
+++ b/tests/python/test_stubgen_rust.py
@@ -14,11 +14,12 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
-"""Tests for the Rust backend of ``tvm-ffi-stubgen``: opaque bindings."""
+"""Tests for the Rust backend of ``tvm-ffi-stubgen``: complete and opaque
bindings."""
from __future__ import annotations
import re
+from collections.abc import Iterator
from pathlib import Path
import pytest
@@ -29,6 +30,8 @@ from tvm_ffi.stub import consts as C
from tvm_ffi.stub.cli import _stage_3
from tvm_ffi.stub.file_utils import CodeBlock, FileInfo
from tvm_ffi.stub.generator import get_generator
+from tvm_ffi.stub.lib_state import object_info_from_type_key
+from tvm_ffi.stub.rust_generator import codegen
from tvm_ffi.stub.rust_generator import consts as RC
from tvm_ffi.stub.rust_generator.codegen import (
finalize_rust_module_tree,
@@ -41,28 +44,70 @@ from tvm_ffi.stub.rust_generator.utils import RustImports,
RustUse, render_rust_
from tvm_ffi.stub.utils import InitConfig, NamedTypeSchema, ObjectInfo, Options
RUST = get_generator("rust")
+HEADER = 24 # sizeof(TVMFFIObject)
+
+
+def _field(
+ name: str,
+ schema: str | TypeSchema,
+ offset: int | None = None,
+ size: int | None = None,
+ alignment: int | None = None,
+) -> NamedTypeSchema:
+ if isinstance(schema, str):
+ schema = TypeSchema(schema)
+ if alignment is None and size is not None:
+ alignment = min(size, 8) # a 16-byte `TVMFFIAny` cell is 8-aligned
+ return NamedTypeSchema(name, schema, offset=offset, size=size,
alignment=alignment)
def _info(
type_key: str,
- fields: tuple[tuple[str, TypeSchema], ...] = (),
+ fields: tuple[NamedTypeSchema, ...] = (),
*,
parent: str | None = "ffi.Object",
ancestors: list[str] | None = None,
is_final: bool | None = None,
+ total_size: int | None = None,
) -> ObjectInfo:
if ancestors is None:
ancestors = ["ffi.Object"] if parent in (None, "ffi.Object") else
["ffi.Object", parent]
return ObjectInfo(
- fields=[NamedTypeSchema(name, schema) for name, schema in fields],
+ fields=list(fields),
methods=[],
type_key=type_key,
parent_type_key=parent,
ancestors=ancestors,
is_final=is_final,
+ total_size=total_size,
)
+#: Ancestors the tests define with byte facts; anything else a test names but
+#: does not define resolves to a type without metadata of its own.
+_SYNTHETIC: dict[str, ObjectInfo] = {}
+
+
+def _register(*infos: ObjectInfo) -> None:
+ for info in infos:
+ assert info.type_key is not None
+ _SYNTHETIC[info.type_key] = info
+
+
[email protected](autouse=True)
+def _synthetic_registry(monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
+ def lookup(type_key: str) -> ObjectInfo:
+ if type_key in _SYNTHETIC:
+ return _SYNTHETIC[type_key]
+ if type_key.startswith(("ffi.", "testing.")):
+ return object_info_from_type_key(type_key)
+ return _info(type_key, total_size=None)
+
+ monkeypatch.setattr(codegen, "object_info_from_type_key", lookup)
+ yield
+ _SYNTHETIC.clear()
+
+
def _object_block(type_key: str) -> CodeBlock:
return CodeBlock(
kind="object",
@@ -86,6 +131,9 @@ def _uses(imports: RustImports) -> set[str]:
return {item.path for item in imports.items}
+NO_METADATA = "/// Opaque: no metadata of its own: total_size is unknown.
Fields are read through the C ABI getters."
+
+
# ---------------------------------------------------------------------------
# `use` modelling and type rendering
# ---------------------------------------------------------------------------
@@ -154,6 +202,8 @@ def test_rust_ident() -> None:
assert rust_ident("type") == "r#type"
assert rust_ident("self") == "self_"
assert rust_ident("crate") == "crate_"
+ assert rust_ident("base") == "base_" # the parent slot of every generated
object struct
+ assert rust_ident("data") == "data_" # the wrapper's `ObjectArc` member
# ---------------------------------------------------------------------------
@@ -167,12 +217,19 @@ def test_directives_parse() -> None:
directives.add("nullable", "ir.Expr.span", 2)
directives.add("enum", "tirx.For.kind -> ForKind(i32) { Serial=0, Parallel
= 1 }", 3)
directives.add("enum", "tirx.For.mode -> Mode(u8)", 4)
+ directives.add("opaque", " ir.SourceName ", 5)
+ directives.add("upcast", "tirx.Add -> PrimExpr", 6)
+ directives.add("upcast", "tirx.Add -> crate::typed::TypedExpr", 7)
+ directives.add("custom-new", " tirx.Add ", 8)
assert directives.field_types == {"tirx.Add.a": "PrimExpr"}
assert directives.nullable == {"ir.Expr.span"}
assert directives.enums == {
"tirx.For.kind": EnumSpec("ForKind", "i32", (("Serial", 0),
("Parallel", 1))),
"tirx.For.mode": EnumSpec("Mode", "u8", ()),
}
+ assert directives.opaque == {"ir.SourceName"}
+ assert directives.upcasts == {"tirx.Add": ["PrimExpr",
"crate::typed::TypedExpr"]}
+ assert directives.custom_new == {"tirx.Add"}
@pytest.mark.parametrize(
@@ -185,7 +242,11 @@ def test_directives_parse() -> None:
("enum", "tirx.For.kind -> ForKind", "Name(i32)"),
("enum", "tirx.For.kind -> ForKind(i128)", "Name(i32)"),
("enum", "tirx.For.kind -> ForKind(i32) { Serial }", "Name(i32)"),
- ("upcast", "tirx.Add -> PrimExpr", "Unknown directive"),
+ ("opaque", "ir.SourceName ir.Source", "<type_key>"),
+ ("upcast", "tirx.Add", "-> <RustType>"),
+ ("upcast", "tirx.Add PrimExpr -> PrimExpr", "<type_key>"),
+ ("custom-new", "", "<type_key>"),
+ ("typed-view", "tirx.Add -> PrimExpr", "Unknown directive"),
],
)
def test_directives_reject_malformed(name: str, payload: str, expected: str)
-> None:
@@ -195,7 +256,15 @@ def test_directives_reject_malformed(name: str, payload:
str, expected: str) ->
def test_generator_declares_its_directives_and_records_imports() -> None:
- assert RUST.directive_kinds == {"import-object", "field", "nullable",
"enum"}
+ assert RUST.directive_kinds == {
+ "import-object",
+ "field",
+ "nullable",
+ "enum",
+ "opaque",
+ "upcast",
+ "custom-new",
+ }
imports = RUST.new_imports()
RUST.add_directive(imports, "import-object",
"tvm_ffi.libinfo.Foo;False;_Foo", 1)
RUST.add_directive(imports, "nullable", "demo.Node.span", 2)
@@ -204,51 +273,52 @@ def
test_generator_declares_its_directives_and_records_imports() -> None:
# ---------------------------------------------------------------------------
-# Object rendering
+# Opaque rendering (no byte facts: the layout cannot be proven)
# ---------------------------------------------------------------------------
-ROOT_EXPECTED = """\
+ROOT_EXPECTED = f"""\
+{NO_METADATA}
#[repr(C)]
#[derive(tvm_ffi::derive::Object)]
#[type_key = "demo.Pair"]
-pub struct PairObj {
+pub struct PairObj {{
base: Object,
-}
+}}
#[repr(C)]
#[derive(tvm_ffi::derive::ObjectRef, Clone)]
-pub struct Pair {
+pub struct Pair {{
data: ObjectArc<PairObj>,
-}
+}}
-impl Deref for Pair {
+impl Deref for Pair {{
type Target = PairObj;
- fn deref(&self) -> &PairObj {
+ fn deref(&self) -> &PairObj {{
&self.data
- }
-}
+ }}
+}}
-impl PairObj {
- pub fn a(&self) -> Result<i64> {
+impl PairObj {{
+ pub fn a(&self) -> Result<i64> {{
FieldGetter::new(Self::type_index(), "a")?.get(self)
- }
+ }}
- pub fn tag(&self) -> Result<Option<String>> {
+ pub fn tag(&self) -> Result<Option<String>> {{
FieldGetter::new(Self::type_index(), "tag")?.get(self)
- }
+ }}
- pub fn items(&self) -> Result<Array<Any>> {
+ pub fn items(&self) -> Result<Array<Any>> {{
FieldGetter::new(Self::type_index(), "items")?.get(self)
- }
+ }}
- pub fn owner(&self) -> Result<ObjectRef> {
+ pub fn owner(&self) -> Result<ObjectRef> {{
FieldGetter::new(Self::type_index(), "owner")?.get(self)
- }
+ }}
- pub fn r#type(&self) -> Result<Any> {
+ pub fn r#type(&self) -> Result<Any> {{
FieldGetter::new(Self::type_index(), "type")?.get_any(self)
- }
-}"""
+ }}
+}}"""
def test_render_root_object() -> None:
@@ -256,11 +326,11 @@ def test_render_root_object() -> None:
info = _info(
"demo.Pair",
(
- ("a", TypeSchema("int")),
- ("tag", TypeSchema("Optional", (TypeSchema("str"),))),
- ("items", TypeSchema("Array")),
- ("owner", TypeSchema("Object")),
- ("type", TypeSchema("Union", (TypeSchema("int"),
TypeSchema("str")))),
+ _field("a", "int"),
+ _field("tag", TypeSchema("Optional", (TypeSchema("str"),))),
+ _field("items", "Array"),
+ _field("owner", "Object"),
+ _field("type", TypeSchema("Union", (TypeSchema("int"),
TypeSchema("str")))),
),
)
text, imports = _render(info)
@@ -290,7 +360,7 @@ def test_render_derived_object_same_module() -> None:
"""A generated parent is embedded, dereferenced to, and upcast to along
the chain."""
info = _info(
"demo.Add",
- (("a", TypeSchema("demo.Expr")),),
+ (_field("a", "demo.Expr"),),
parent="demo.Expr",
ancestors=["ffi.Object", "demo.BaseExpr", "demo.Expr"],
is_final=True,
@@ -317,7 +387,7 @@ def test_render_object_under_builtin_parent() -> None:
"""A builtin parent is embedded via header-only stand-ins so `TYPE_DEPTH`
matches the registry."""
info = _info(
"demo.Color",
- (("value", TypeSchema("int")),),
+ (_field("value", "int"),),
parent="ffi.IntEnum",
ancestors=["ffi.Object", "ffi.Enum", "ffi.IntEnum"],
)
@@ -401,82 +471,83 @@ def test_import_section_defines_builtin_mirrors_once() ->
None:
assert "\n".join(block.lines[1:-1]) == BUILTIN_MIRRORS_EXPECTED
-ITER_VAR_EXPECTED = """\
+ITER_VAR_EXPECTED = f"""\
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[repr(transparent)]
pub struct IterVarType(i32);
#[allow(non_upper_case_globals)]
-impl IterVarType {
+impl IterVarType {{
pub const kDataPar: Self = Self(0);
pub const kThreadIndex: Self = Self(1);
- pub const fn from_raw(value: i32) -> Self {
+ pub const fn from_raw(value: i32) -> Self {{
Self(value)
- }
- pub const fn as_raw(self) -> i32 {
+ }}
+ pub const fn as_raw(self) -> i32 {{
self.0
- }
-}
+ }}
+}}
-impl TryFrom<i64> for IterVarType {
+impl TryFrom<i64> for IterVarType {{
type Error = Error;
- fn try_from(value: i64) -> Result<Self> {
- i32::try_from(value).map(Self).map_err(|_| {
- Error::new(VALUE_ERROR, &format!("IterVarType value {value} does
not fit i32"), "")
- })
- }
-}
-
+ fn try_from(value: i64) -> Result<Self> {{
+ i32::try_from(value).map(Self).map_err(|_| {{
+ Error::new(VALUE_ERROR, &format!("IterVarType value {{value}} does
not fit i32"), "")
+ }})
+ }}
+}}
+
+{NO_METADATA}
#[repr(C)]
#[derive(tvm_ffi::derive::Object)]
#[type_key = "tirx.IterVar"]
#[type_final]
-pub struct IterVarObj {
+pub struct IterVarObj {{
base: PrimExprConvertibleObj,
-}
+}}
#[repr(C)]
#[derive(tvm_ffi::derive::ObjectRef, Clone)]
-pub struct IterVar {
+pub struct IterVar {{
data: ObjectArc<IterVarObj>,
-}
+}}
-impl Deref for IterVar {
+impl Deref for IterVar {{
type Target = IterVarObj;
- fn deref(&self) -> &IterVarObj {
+ fn deref(&self) -> &IterVarObj {{
&self.data
- }
-}
+ }}
+}}
-impl Deref for IterVarObj {
+impl Deref for IterVarObj {{
type Target = PrimExprConvertibleObj;
- fn deref(&self) -> &PrimExprConvertibleObj {
+ fn deref(&self) -> &PrimExprConvertibleObj {{
&self.base
- }
-}
+ }}
+}}
-impl IterVarObj {
- pub fn dom(&self) -> Result<Option<Range>> {
+impl IterVarObj {{
+ pub fn dom(&self) -> Result<Option<Range>> {{
FieldGetter::new(Self::type_index(), "dom")?.get(self)
- }
+ }}
- pub fn var(&self) -> Result<PrimVar> {
+ pub fn var(&self) -> Result<PrimVar> {{
FieldGetter::new(Self::type_index(), "var")?.get(self)
- }
+ }}
- pub fn iter_type(&self) -> Result<IterVarType> {
+ pub fn iter_type(&self) -> Result<IterVarType> {{
let raw: i64 = FieldGetter::new(Self::type_index(),
"iter_type")?.get(self)?;
IterVarType::try_from(raw)
- }
+ }}
- pub fn thread_tag(&self) -> Result<String> {
+ pub fn thread_tag(&self) -> Result<String> {{
FieldGetter::new(Self::type_index(), "thread_tag")?.get(self)
- }
+ }}
- pub fn span(&self) -> Result<Option<Span>> {
+ pub fn span(&self) -> Result<Option<Span>> {{
FieldGetter::new(Self::type_index(), "span")?.get(self)
- }
-}
+ }}
+}}
tvm_ffi::impl_object_upcast!(IterVar => PrimExprConvertible);"""
@@ -486,11 +557,11 @@ def test_render_iter_var_golden() -> None:
info = _info(
"tirx.IterVar",
(
- ("dom", TypeSchema("ir.Range")),
- ("var", TypeSchema("ir.Var")),
- ("iter_type", TypeSchema("int")),
- ("thread_tag", TypeSchema("str")),
- ("span", TypeSchema("ir.Span")),
+ _field("dom", "ir.Range"),
+ _field("var", "ir.Var"),
+ _field("iter_type", "int"),
+ _field("thread_tag", "str"),
+ _field("span", "ir.Span"),
),
parent="ir.PrimExprConvertible",
is_final=True,
@@ -518,7 +589,10 @@ def test_render_iter_var_golden() -> None:
def
test_field_directive_with_path_records_use_and_nullable_does_not_double_wrap()
-> None:
info = _info(
"demo.Node",
- (("buffer", TypeSchema("demo.Var")), ("dom", TypeSchema("Optional",
(TypeSchema("int"),)))),
+ (
+ _field("buffer", "demo.Var"),
+ _field("dom", TypeSchema("Optional", (TypeSchema("int"),))),
+ ),
)
imports = RUST.new_imports()
RUST.add_directive(imports, "field", "demo.Node.buffer ->
crate::typed::BufferVar", 1)
@@ -529,6 +603,376 @@ def
test_field_directive_with_path_records_use_and_nullable_does_not_double_wrap
assert "crate::typed::BufferVar" in _uses(imports)
+# ---------------------------------------------------------------------------
+# Complete rendering (byte facts prove the layout)
+# ---------------------------------------------------------------------------
+
+
+def _span() -> ObjectInfo:
+ return _info(
+ "ir.Span",
+ (
+ _field("source_name", "ir.SourceName", 24, 8),
+ _field("line", "int", 32, 4),
+ _field("column", "int", 36, 4),
+ _field("end_line", "int", 40, 4),
+ _field("end_column", "int", 44, 4),
+ ),
+ total_size=48,
+ )
+
+
+def _expr() -> ObjectInfo:
+ return _info(
+ "ir.Expr",
+ (_field("span", "ir.Span", 24, 8), _field("ty", "ir.Type", 32, 8)),
+ total_size=40,
+ )
+
+
+def _add() -> ObjectInfo:
+ return _info(
+ "tirx.Add",
+ (_field("a", "ir.Expr", 40, 8), _field("b", "ir.Expr", 48, 8)),
+ parent="ir.Expr",
+ total_size=56,
+ is_final=True,
+ )
+
+
+EXPR_EXPECTED = """\
+/// Complete: reflected fields fill [24, 40) exactly.
+#[repr(C)]
+#[derive(tvm_ffi::derive::Object)]
+#[type_key = "ir.Expr"]
+pub struct ExprObj {
+ base: Object,
+ pub span: Option<Span>,
+ pub ty: Type,
+}
+
+const _: () = {
+ assert!(::core::mem::size_of::<ExprObj>() == 40);
+ assert!(::core::mem::align_of::<ExprObj>() == 8);
+};
+
+#[repr(C)]
+#[derive(tvm_ffi::derive::ObjectRef, Clone)]
+pub struct Expr {
+ data: ObjectArc<ExprObj>,
+}
+
+impl Deref for Expr {
+ type Target = ExprObj;
+ fn deref(&self) -> &ExprObj {
+ &self.data
+ }
+}
+
+impl ExprObj {
+ pub(crate) fn new(span: Option<Span>, ty: Type) -> Self {
+ let base = Object::new();
+ Self { base, span, ty }
+ }
+}
+
+impl Expr {
+ /// Lossless complete-field allocation.
+ pub fn new(span: Option<Span>, ty: Type) -> Self {
+ let obj = ExprObj::new(span, ty);
+ Self { data: ObjectArc::new(obj) }
+ }
+}"""
+
+ADD_EXPECTED = """\
+/// Complete: reflected fields fill [40, 56) exactly.
+#[repr(C)]
+#[derive(tvm_ffi::derive::Object)]
+#[type_key = "tirx.Add"]
+#[type_final]
+pub struct AddObj {
+ base: ExprObj,
+ pub a: PrimExpr,
+ pub b: PrimExpr,
+}
+
+const _: () = {
+ assert!(::core::mem::size_of::<AddObj>() == 56);
+ assert!(::core::mem::align_of::<AddObj>() == 8);
+};
+
+#[repr(C)]
+#[derive(tvm_ffi::derive::ObjectRef, Clone)]
+pub struct Add {
+ data: ObjectArc<AddObj>,
+}
+
+impl Deref for Add {
+ type Target = AddObj;
+ fn deref(&self) -> &AddObj {
+ &self.data
+ }
+}
+
+impl Deref for AddObj {
+ type Target = ExprObj;
+ fn deref(&self) -> &ExprObj {
+ &self.base
+ }
+}
+
+impl AddObj {
+ pub(crate) fn new(span: Option<Span>, ty: PrimType, a: PrimExpr, b:
PrimExpr) -> Self {
+ let base = ExprObj::new(span, ty.into());
+ Self { base, a, b }
+ }
+}
+
+impl Add {
+ /// Lossless complete-field allocation.
+ pub fn new(span: Option<Span>, ty: PrimType, a: PrimExpr, b: PrimExpr) ->
Self {
+ let obj = AddObj::new(span, ty, a, b);
+ Self { data: ObjectArc::new(obj) }
+ }
+}
+
+tvm_ffi::impl_object_upcast!(Add => Expr, Add => PrimExpr);"""
+
+
+def test_render_complete_expr_golden() -> None:
+ """`ir.Expr` as tvm-rust-ext hand-writes it: `span: Option<Span>`, `ty:
Type`, no getters."""
+ imports = RUST.new_imports()
+ RUST.add_directive(imports, "nullable", "ir.Expr.span", 1)
+ text, imports = _render(_expr(), imports)
+ assert text == EXPR_EXPECTED
+ assert _uses(imports) == {"std::ops::Deref", "tvm_ffi::Object",
"tvm_ffi::ObjectArc"}
+
+
+def test_render_complete_add_golden() -> None:
+ """`tirx.Add` as tvm-rust-ext hand-writes it, on top of a complete
`ir.Expr`.
+
+ `field` directives narrow `a` / `b` and the inherited allocator parameter
+ `ty` (upcast with `.into()` on the way to `ExprObj::new`); `upcast` adds
+ the `PrimExpr` view.
+ """
+ _register(_expr())
+ imports = RUST.new_imports()
+ RUST.add_directive(imports, "nullable", "ir.Expr.span", 1)
+ RUST.add_directive(imports, "field", "tirx.Add.a -> PrimExpr", 2)
+ RUST.add_directive(imports, "field", "tirx.Add.b -> PrimExpr", 3)
+ RUST.add_directive(imports, "field", "tirx.Add.ty -> PrimType", 4)
+ RUST.add_directive(imports, "upcast", "tirx.Add -> PrimExpr", 5)
+ text, imports = _render(_add(), imports)
+ assert text == ADD_EXPECTED
+ assert _uses(imports) == {
+ "std::ops::Deref",
+ "tvm_ffi::ObjectArc",
+ "super::ir::ExprObj",
+ "super::ir::Expr",
+ "super::ir::Span",
+ "super::ir::Type",
+ }
+
+
+def test_render_complete_span_and_prim_type() -> None:
+ """Scalars take their reflected width; alignment padding is reported, not
mirrored."""
+ text, _ = _render(_span())
+ assert (
+ "pub struct SpanObj {\n"
+ " base: Object,\n"
+ " pub source_name: SourceName,\n"
+ " pub line: i32,\n"
+ " pub column: i32,\n"
+ " pub end_line: i32,\n"
+ " pub end_column: i32,\n"
+ "}"
+ ) in text
+ assert "/// Complete: reflected fields fill [24, 48) exactly." in text
+
+ _register(_info("ir.Type", (_field("span", "ir.Span", 24, 8),),
total_size=32))
+ prim_type = _info(
+ "ir.PrimType",
+ (_field("dtype", "dtype", 32, 4, 2),),
+ parent="ir.Type",
+ total_size=40,
+ is_final=True,
+ )
+ text, imports = _render(prim_type)
+ assert (
+ "/// Complete: reflected fields fill [32, 40) exactly (alignment
padding [36, 40))." in text
+ )
+ assert "pub struct PrimTypeObj {\n base: TypeObj,\n pub dtype:
DLDataType,\n}" in text
+ assert "assert!(::core::mem::size_of::<PrimTypeObj>() == 40);" in text
+ assert "tvm_ffi::DLDataType" in _uses(imports)
+
+
+def test_reserved_member_names_get_a_trailing_underscore() -> None:
+ """A reflected `base` or `data` field must not collide with the generated
members."""
+ info = _info(
+ "demo.Ramp",
+ (_field("base", "int", 24, 8), _field("data", "int", 32, 8)),
+ total_size=40,
+ is_final=True,
+ )
+ text, _ = _render(info)
+ assert " base: Object,\n pub base_: i64,\n pub data_: i64,\n" in
text
+ assert " pub fn new(base_: i64, data_: i64) -> Self {" in text
+ assert " Self { base, base_, data_ }" in text
+ # The opaque form keeps the reflected name on the C ABI side.
+ text, _ = _render(_info("demo.Node", (_field("base", "int"),)))
+ assert (
+ 'pub fn base_(&self) -> Result<i64> {\n
FieldGetter::new(Self::type_index(), "base")'
+ in text
+ )
+
+
+def test_render_complete_enum_field() -> None:
+ """An `enum` directive types the mirrored field; the newtype brings its
own `Result` import."""
+ 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, "enum", "demo.Pair.kind -> Kind(i32) { A=0,
B=1 }", 1)
+ text, imports = _render(info, imports)
+ assert " pub kind: Kind,\n" in text
+ assert " pub fn new(a: i64, kind: Kind) -> Self {" in text
+ assert {"tvm_ffi::Result", "tvm_ffi::Error", "tvm_ffi::VALUE_ERROR"} <=
_uses(imports)
+ assert "tvm_ffi::FieldGetter" not in _uses(imports)
+
+
+def test_complete_optional_field_mirrors() -> None:
+ """`Optional<T>` fields mirror their C++ layout: a 16-byte cell or a
nullable pointer."""
+ info = _info(
+ "demo.Opt",
+ (
+ _field("count", TypeSchema("Optional", (TypeSchema("int"),)), 24,
16),
+ _field("name", TypeSchema("Optional", (TypeSchema("str"),)), 40,
16),
+ _field("items", TypeSchema("Optional", (TypeSchema("Array"),)),
56, 8),
+ ),
+ total_size=64,
+ )
+ text, imports = _render(info)
+ assert "pub count: Optional<i64>," in text
+ assert "pub name: Optional<String>," in text
+ assert "pub items: Option<Array<Any>>," in text
+ assert "tvm_ffi::Optional" in _uses(imports)
+
+
[email protected](
+ "field",
+ [
+ _field("x", TypeSchema("Optional", (TypeSchema("Any"),)), 24, 16),
+ _field("x", TypeSchema("Optional", (TypeSchema("int"),)), 24, 8), #
not the 16-byte cell
+ _field("x", TypeSchema("Union", (TypeSchema("int"),
TypeSchema("str"))), 24, 16),
+ _field("x", "ctypes.c_void_p", 24, 8),
+ ],
+)
+def test_unrenderable_field_keeps_the_type_opaque(field: NamedTypeSchema) ->
None:
+ """A field without a mirror demotes an otherwise complete type; it is read
as `Any`."""
+ assert field.size is not None
+ info = _info("demo.Holder", (field,), total_size=HEADER + field.size)
+ text, _ = _render(info)
+ assert "/// Opaque: field 'x'" in text
+ assert "has no native mirror" in text
+ assert "impl HolderObj {\n pub fn x(&self) -> Result<" in text
+ assert "const _: () =" not in text
+
+
+def test_custom_new_renames_the_wrapper_allocator() -> None:
+ """`custom-new`: `Add::new` stays hand-written, the allocator is
`from_complete_fields`."""
+ _register(_expr())
+ imports = RUST.new_imports()
+ RUST.add_directive(imports, "custom-new", "tirx.Add", 1)
+ text, _ = _render(_add(), imports)
+ assert (
+ "impl AddObj {\n pub(crate) fn new(span: Span, ty: Type, a: Expr,
b: Expr) -> Self {"
+ in text
+ )
+ assert (
+ "impl Add {\n /// Lossless complete-field allocation.\n"
+ " pub fn from_complete_fields(span: Span, ty: Type, a: Expr, b:
Expr) -> Self {\n"
+ " let obj = AddObj::new(span, ty, a, b);\n"
+ " Self { data: ObjectArc::new(obj) }\n"
+ " }\n}"
+ ) in text
+ assert " pub fn new(" not in text
+
+
+def test_upcast_directive_adds_typed_views() -> None:
+ """`upcast` targets follow the ancestor chain; a `::` path is imported.
Opaque: no allocator."""
+ info = _info("demo.Leaf", parent="demo.Base")
+ imports = RUST.new_imports()
+ RUST.add_directive(imports, "upcast", "demo.Leaf ->
crate::typed::LeafView", 1)
+ RUST.add_directive(imports, "upcast", "demo.Leaf -> Other", 2)
+ text, imports = _render(info, imports)
+ assert text.endswith(
+ "tvm_ffi::impl_object_upcast!(Leaf => Base, Leaf => LeafView, Leaf =>
Other);"
+ )
+ assert "crate::typed::LeafView" in _uses(imports)
+ assert "fn new(" not in text
+
+
+def test_opaque_directive_vetoes_a_complete_type() -> None:
+ imports = RUST.new_imports()
+ RUST.add_directive(imports, "opaque", "ir.Span", 1)
+ text, _ = _render(_span(), imports)
+ assert "/// Opaque: vetoed by directive although the layout is
reproducible." in text
+ assert "pub fn line(&self) -> Result<i64> {" in text
+
+
+def test_builtin_parent_keeps_the_type_opaque() -> None:
+ """The crate never mirrors a builtin's bytes: the header-only stand-in
cannot be complete."""
+ # Fieldless under `ffi.Enum` (48 bytes): the fill criterion alone would
call this complete.
+ info = _info(
+ "demo.Flag", parent="ffi.Enum", ancestors=["ffi.Object", "ffi.Enum"],
total_size=48
+ )
+ text, _ = _render(info)
+ assert "/// Opaque: parent 'ffi.Enum' is opaque (no-mirror)." in text
+ assert " base: FfiEnumObj," in text
+ assert "const _: () =" not in text
+ assert "fn new(" not in text
+ # The registry fixtures under `ffi.Enum` / `ffi.IntEnum` / `ffi.StrEnum`
follow the same rule.
+ for type_key, parent in (
+ ("testing.TestEnumVariant", "ffi.Enum"),
+ ("testing.TestCxxIntEnum", "ffi.IntEnum"),
+ ("testing.TestCxxStrEnum", "ffi.StrEnum"),
+ ):
+ text, _ = _render(object_info_from_type_key(type_key))
+ assert f"/// Opaque: parent '{parent}' is opaque (no-mirror)." in text
+ assert "const _: () =" not in text
+ assert "fn new(" not in text
+
+
+def test_opaque_parent_keeps_the_child_opaque() -> None:
+ """A child of an opaque type cannot embed a mirror of its parent: it stays
opaque."""
+ _register(_info("ir.Expr", (_field("span", "ir.Span", 24, 8),),
total_size=40)) # hole
+ text, _ = _render(_add())
+ assert "/// Opaque: parent 'ir.Expr' is opaque (uncovered-bytes)." in text
+ assert " base: ExprObj," in text
+ assert "pub fn a(&self) -> Result<Expr> {" in text
+
+
[email protected](
+ ("name", "payload", "message"),
+ [
+ ("field", "demo.Pair.count -> i64", "maps a 4-byte field to `i64` (8
bytes)"),
+ ("enum", "demo.Pair.count -> Kind(i64)", "maps a 4-byte field to `i64`
(8 bytes)"),
+ ("nullable", "demo.Pair.count", "the field is 4 bytes, not a
pointer-sized"),
+ ],
+)
+def test_directive_disagreeing_with_bytes_is_an_error(
+ name: str, payload: str, message: str
+) -> None:
+ info = _info("demo.Pair", (_field("count", "int", 24, 4),), total_size=32)
+ imports = RUST.new_imports()
+ RUST.add_directive(imports, name, payload, 1)
+ with pytest.raises(ValueError, match=re.escape(message)):
+ _render(info, imports)
+
+
# ---------------------------------------------------------------------------
# File scaffolding
# ---------------------------------------------------------------------------
@@ -578,10 +1022,61 @@ def test_finalize_module_tree(tmp_path: Path) -> None:
# ---------------------------------------------------------------------------
-# The pipeline end to end
+# The real registry, and the pipeline end to end
# ---------------------------------------------------------------------------
+def test_registry_complete_chain_is_mirrored() -> None:
+ base, _ = _render(object_info_from_type_key("testing.TestCxxClassBase"))
+ assert (
+ "/// Complete: reflected fields fill [24, 40) exactly (alignment
padding [36, 40))." in base
+ )
+ assert (
+ "pub struct TestCxxClassBaseObj {\n base: Object,\n pub v_i64:
i64,\n pub v_i32: i32,\n}"
+ in base
+ )
+ assert "assert!(::core::mem::size_of::<TestCxxClassBaseObj>() == 40);" in
base
+ assert "FieldGetter" not in base
+ assert " pub fn new(v_i64: i64, v_i32: i32) -> Self {" in base
+
+ dd, imports =
_render(object_info_from_type_key("testing.TestCxxClassDerivedDerived"))
+ assert (
+ " base: TestCxxClassDerivedObj,\n pub v_str: String,\n pub
v_bool: bool,\n}" in dd
+ )
+ # The allocator flattens the chain; a signature over 100 columns wraps.
+ assert (
+ "impl TestCxxClassDerivedDerivedObj {\n"
+ " pub(crate) fn new(\n"
+ " v_i64: i64,\n"
+ " v_i32: i32,\n"
+ " v_f64: f64,\n"
+ " v_f32: f32,\n"
+ " v_str: String,\n"
+ " v_bool: bool,\n"
+ " ) -> Self {\n"
+ " let base = TestCxxClassDerivedObj::new(v_i64, v_i32, v_f64,
v_f32);\n"
+ " Self { base, v_str, v_bool }\n"
+ " }\n"
+ "}"
+ ) in dd
+ assert dd.endswith(
+ "tvm_ffi::impl_object_upcast!(TestCxxClassDerivedDerived =>
TestCxxClassBase, "
+ "TestCxxClassDerivedDerived => TestCxxClassDerived);"
+ )
+ assert "tvm_ffi::String" in _uses(imports)
+
+
+def test_registry_hidden_field_and_vptr_stay_opaque() -> None:
+ hidden, _ =
_render(object_info_from_type_key("testing.TestCxxClassHiddenField"))
+ assert (
+ "/// Opaque: bytes [32, 40) of [24, 48) are not accounted for by
reflected fields. "
+ "Fields are read through the C ABI getters."
+ ) in hidden
+ assert "pub fn v_i32(&self) -> Result<i64> {" in hidden
+ poly, _ =
_render(object_info_from_type_key("testing.TestCxxClassPolymorphic"))
+ assert "/// Opaque: bytes [32, 40) of [24, 40)" in poly
+
+
def test_stage_3_applies_directives_to_a_registered_type(tmp_path: Path) ->
None:
src = tmp_path / "mod.rs"
src.write_text(
@@ -602,9 +1097,8 @@ def
test_stage_3_applies_directives_to_a_registered_type(tmp_path: Path) -> None
_stage_3(info, Options(dry_run=True), RUST.default_ty_map(), {}, RUST)
text = "\n".join(line for block in info.code_blocks for line in
block.lines)
assert "pub struct Kind(i32);" in text
- assert "pub fn v_i32(&self) -> Result<Kind> {" in text
- assert "pub fn v_i64(&self) -> Result<i64> {" in text
- assert "use tvm_ffi::FieldGetter;" in text
+ assert " pub v_i32: Kind,\n" in text
+ assert "use tvm_ffi::Error;" in text
def test_cli_init_generates_a_module_tree(tmp_path: Path, monkeypatch:
pytest.MonkeyPatch) -> None:
@@ -627,9 +1121,9 @@ def test_cli_init_generates_a_module_tree(tmp_path: Path,
monkeypatch: pytest.Mo
assert (tmp_path / "mod.rs").read_text(encoding="utf-8") == "pub mod
testing;\n"
text = (tmp_path / "testing" / "mod.rs").read_text(encoding="utf-8")
assert text.startswith("#![allow(dead_code, unused_imports)]\n")
- assert "use tvm_ffi::FieldGetter;" in text
+ assert "use tvm_ffi::FieldGetter;" in text # the opaque fixtures read
through getters
assert '#[type_key = "testing.TestCxxClassDerivedDerived"]' in text
- assert " base: TestCxxClassDerivedObj," in text
+ assert " base: TestCxxClassDerivedObj,\n pub v_str: String," in text
assert (
"tvm_ffi::impl_object_upcast!(TestCxxClassDerivedDerived =>
TestCxxClassBase, "
"TestCxxClassDerivedDerived => TestCxxClassDerived);"