Skip to content

Commit 1b251ee

Browse files
committed
update optional and map.
Signed-off-by: yuchuan <yuchuan.7streams@gmail.com>
1 parent 1de93dd commit 1b251ee

6 files changed

Lines changed: 517 additions & 42 deletions

File tree

docs/packaging/stubgen.rst

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -425,9 +425,40 @@ Limitations
425425
A type that mentions an origin the Rust crate cannot represent -- in any
426426
position: field, method argument, return type, or nested inside another
427427
container -- is explicitly unsupported: the whole binding is skipped with a
428-
warning. This covers ``Map`` / ``Dict`` / ``List`` / ``Union`` (no Rust
429-
counterpart) as well as ``Optional`` / ``tuple`` (std ``Option<T>`` and Rust
430-
tuples do not match the C++ ``ffi::Optional`` / ``ffi::Tuple`` memory layout).
428+
warning. This covers ``Dict`` / ``List`` / ``Union`` (no Rust counterpart) as
429+
well as ``tuple`` (Rust tuples do not match the C++ ``ffi::Tuple`` memory
430+
layout).
431+
432+
``Map<K, V>`` renders as the crate's ``tvm_ffi::Map<K, V>`` when both ``K``
433+
and ``V`` are typed; an untyped ``Map`` (``Map<Any, Any>``) is skipped because
434+
``Any`` does not satisfy the crate's ``AnyCompatible`` bounds.
435+
436+
``Optional<T>`` in argument/return position renders as plain ``Option<T>``
437+
for any payload the crate can hold (``Any`` and bare ``ObjectRef`` payloads
438+
skip the object in every position: neither has an ``AnyCompatible`` crate
439+
rendering). An ``Optional<T>`` *field* mirrors C++ ``ffi::Optional<T>``'s
440+
in-place storage, which picks one of three ABI layouts by ``T``:
441+
442+
- POD scalar -> ``tvm_ffi::option::OptionPod<T>`` (``std::optional`` layout)
443+
- ``String`` -> ``tvm_ffi::option::OptionStr`` (in-cell ``None`` sentinel)
444+
- ``ObjectRef`` subtype -> plain ``Option<T>`` (single nullable pointer)
445+
446+
Payloads outside these three categories -- ``Bytes`` (no ``OptionalBytes``
447+
mirror in the crate yet), ``Any`` / ``ObjectRef``, and POD structs like
448+
``Device`` / ``dtype`` -- have no field mirror, and objects containing such
449+
fields are skipped. The field routing also cross-checks the reflected field
450+
*size* against the expected layout, so type-name aliases with a different ABI
451+
(``std::string``, ``DLTensor*``, ``void*``, ...) degrade to a skip instead of
452+
a wrong ``#[repr(C)]`` overlay.
453+
454+
For ``Optional`` fields only the ``nullopt`` default renders (as the mirror's
455+
disengaged state); an engaged default value suppresses the generated
456+
constructor like any other unrenderable default.
457+
458+
The Rust backend targets natively-laid-out C++ objects only. Running it on
459+
Python-defined (``py_class``) types is undefined: their fields use
460+
Python-side storage conventions (``Optional``/``str`` origins are inline
461+
``Any`` cells), not the native C++ struct layout these mirrors assume.
431462

432463
A constructor alone cannot be generated when a default comes from a
433464
``refl::default_factory`` or when a default value has no Rust literal

python/tvm_ffi/stub/rust_generator/codegen.py

Lines changed: 116 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -66,22 +66,57 @@ def _rust_string_literal(s: str) -> str:
6666
return "".join(out)
6767

6868

69-
def _default_expr(field: NamedTypeSchema) -> str | None:
70-
"""Render ``field``'s registered default as a Rust expression (``None``: can't).
69+
def _scalar_literal(value: object) -> str | None:
70+
"""Render a ``bool``/``int``/finite ``float`` value as a Rust literal (``None``: can't).
7171
72-
Only values whose Rust spelling is self-evident are supported: ``bool`` /
73-
``int`` / finite ``float`` literals (which coerce to the field's possibly
74-
narrowed scalar type in the struct-literal position) and ``str`` (which
75-
becomes a ``tvm_ffi::String``). Anything else -- objects, containers,
76-
non-finite floats, factories -- has no native materialization.
72+
The literal coerces to the field's possibly narrowed scalar type in the
73+
struct-literal position.
7774
"""
78-
value = field.default
7975
if isinstance(value, bool):
8076
return "true" if value else "false"
8177
if isinstance(value, int):
8278
return repr(value)
8379
if isinstance(value, float):
8480
return repr(value) if math.isfinite(value) else None
81+
return None
82+
83+
84+
def _optional_default_expr(field: NamedTypeSchema) -> str | None:
85+
"""Render an ``Optional`` field's ``nullopt`` default as the mirror's disengaged state.
86+
87+
Only the ``None`` default is supported: an engaged default's type-erased
88+
value can disagree with the payload's kind or width, so it is treated as
89+
unrenderable (``ffi_new`` is then skipped loudly).
90+
"""
91+
if field.default is not None:
92+
return None
93+
(payload,) = field.args or (None,) # Optional always has exactly one argument
94+
assert payload is not None
95+
origin = payload.origin
96+
if origin in C_RUST.RUST_OPTION_POD_ORIGINS:
97+
return f"{C_RUST.RUST_OPTION_POD_PATH}::none()"
98+
if origin in C_RUST.RUST_OPTION_STR_ORIGINS:
99+
return f"{C_RUST.RUST_OPTION_STR_PATH}::none()"
100+
is_ptr_based = (
101+
"." in origin or origin in C_RUST.RUST_OPTION_PTR_ORIGINS
102+
) and origin not in C_RUST.RUST_OPTION_BYTES_ORIGINS
103+
# Ptr-based fields are plain `Option<T>`, so disengaged is the literal `None`.
104+
return "None" if is_ptr_based else None
105+
106+
107+
def _default_expr(field: NamedTypeSchema) -> str | None:
108+
"""Render ``field``'s registered default as a Rust expression (``None``: can't).
109+
110+
Supported: ``bool``/``int``/finite ``float`` literals, ``str`` (as
111+
``tvm_ffi::String``), and the ``nullopt`` default of ``Optional`` fields.
112+
Anything else has no native materialization.
113+
"""
114+
if field.origin == "Optional":
115+
return _optional_default_expr(field)
116+
value = field.default
117+
literal = _scalar_literal(value)
118+
if literal is not None:
119+
return literal
85120
if isinstance(value, str):
86121
return f"tvm_ffi::String::from({_rust_string_literal(value)})"
87122
return None
@@ -209,11 +244,83 @@ def render_struct_field(self, schema: NamedTypeSchema) -> str:
209244
210245
An ``int32_t`` field must render as ``i32``, not the schema-erased
211246
default ``i64``; the width comes from reflection's per-field ``size``.
212-
Non-scalar origins (or schemas without a size) render plainly.
247+
``Optional`` fields are layout-sensitive and route to their in-place
248+
mirror. Non-scalar origins (or schemas without a size) render plainly.
213249
"""
250+
if schema.origin == "Optional":
251+
return self._render_optional_field(schema)
214252
narrowed = C_RUST.RUST_SCALAR_BY_SIZE.get((schema.origin, schema.size))
215253
return narrowed if narrowed is not None else render_rust_type(schema, self._ty_render)
216254

255+
def _render_optional_field(self, schema: NamedTypeSchema) -> str:
256+
"""Route an ``Optional<T>`` FIELD to the mirror of its C++ in-place layout.
257+
258+
POD scalar -> ``OptionPod<T>``, ``String`` -> ``OptionStr``,
259+
``ObjectRef`` subtype -> ``Option<T>`` (see ``RUST_OPTION_*_ORIGINS``);
260+
anything else raises and skips the object.
261+
"""
262+
(payload,) = schema.args or (None,) # Optional always has exactly one argument
263+
assert payload is not None
264+
origin = payload.origin
265+
if origin in C_RUST.RUST_OPTION_POD_ORIGINS:
266+
payload_ty = None
267+
if origin == "bool":
268+
payload_ty = "bool" # always one byte; no width recovery needed
269+
elif schema.size is None:
270+
# No layout metadata (synthetic schemas): schema-erased default width.
271+
payload_ty = render_rust_type(payload, self._ty_render)
272+
else:
273+
# sizeof(ffi::Optional<T>) == 2 * sizeof(T) for every supported scalar.
274+
half, rem = divmod(schema.size, 2)
275+
if rem == 0:
276+
payload_ty = C_RUST.RUST_SCALAR_BY_SIZE.get((origin, half))
277+
if payload_ty is None:
278+
raise UnsupportedTypeError(
279+
"Optional",
280+
f"cannot recover the `Optional<{origin}>` payload width from "
281+
f"field size {schema.size}",
282+
)
283+
pod = self.imports.record(C_RUST.RUST_OPTION_POD_PATH)
284+
return f"{pod}<{payload_ty}>"
285+
if origin in C_RUST.RUST_OPTION_STR_ORIGINS:
286+
if schema.size not in (None, 16):
287+
# Origin folding also maps `std::string` to "str"; only the
288+
# 16-byte in-cell `ffi::Optional<String>` has the OptionStr mirror.
289+
raise UnsupportedTypeError(
290+
"Optional",
291+
f"`Optional<{origin}>` field has size {schema.size}, not the "
292+
"16-byte in-cell `ffi::Optional<String>` layout",
293+
)
294+
return self.imports.record(C_RUST.RUST_OPTION_STR_PATH)
295+
if origin in C_RUST.RUST_OPTION_BYTES_ORIGINS:
296+
raise UnsupportedTypeError(
297+
"Optional",
298+
"`Optional<bytes>` fields have no crate mirror yet (OptionalBytes "
299+
"is not implemented)",
300+
)
301+
if origin in C_RUST.RUST_NON_ELEMENT_ORIGINS:
302+
# Before the dotted-key rule: `Object`/`ffi.Object` render as the
303+
# header struct, which is no valid mirror (see RUST_NON_ELEMENT_ORIGINS).
304+
raise UnsupportedTypeError(
305+
"Optional", f"`Optional<{origin}>` fields have no in-place Rust mirror"
306+
)
307+
if "." in origin or origin in C_RUST.RUST_OPTION_PTR_ORIGINS:
308+
if schema.size not in (None, 8):
309+
# Folded aliases (`DLTensor*` -> "Tensor", `void*` ->
310+
# "ctypes.c_void_p") are std::optional-backed, not a nullable
311+
# pointer; only the 8-byte layout may take this branch.
312+
raise UnsupportedTypeError(
313+
"Optional",
314+
f"`Optional<{origin}>` field has size {schema.size}, not the "
315+
"8-byte nullable pointer of a ptr-based `ffi::Optional`",
316+
)
317+
# `Option<T>` niche-optimizes over the ref's NonNull data, matching
318+
# C++'s single nullable pointer (`None` == nullptr).
319+
return f"Option<{render_rust_type(payload, self._ty_render)}>"
320+
raise UnsupportedTypeError(
321+
"Optional", f"`Optional<{origin}>` fields have no in-place Rust mirror"
322+
)
323+
217324
def render_param(self, schema: TypeSchema) -> str:
218325
"""Render an argument type (a top-level ``Any`` is the non-owning ``AnyView``)."""
219326
if schema.origin == "Any":

python/tvm_ffi/stub/rust_generator/consts.py

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"Any": "tvm_ffi::Any",
3232
"Callable": "tvm_ffi::Function",
3333
"Array": "tvm_ffi::Array", # the crate's own Array<T>, NOT Vec
34+
"Map": "tvm_ffi::Map", # the crate's own Map<K, V>, NOT HashMap
3435
"Object": "tvm_ffi::Object",
3536
"Tensor": "tvm_ffi::Tensor",
3637
"Shape": "tvm_ffi::Shape",
@@ -62,13 +63,34 @@
6263
("float", 8): "f64",
6364
}
6465

65-
#: Origins the crate has no FFI type for: ``Map``/``Dict``/``List``/``Union``
66-
#: have no Rust counterpart at all (do NOT map to ``HashMap``/``Vec``), and
67-
#: ``Optional``/``tuple`` only have std renderings (``Option<T>``, Rust tuples)
68-
#: that are not layout-compatible with C++ ``ffi::Optional``/``ffi::Tuple``.
69-
#: ``render_rust_type`` raises ``UnsupportedTypeError`` wherever one appears
70-
#: (field, argument, return, or nested) and the enclosing object is skipped.
71-
RUST_UNSUPPORTED_ORIGINS = frozenset({"Map", "Dict", "List", "Union", "Optional", "tuple"})
66+
#: Origins the crate has no FFI type for (do NOT map to ``HashMap``/``Vec``;
67+
#: Rust tuples don't match ``ffi::Tuple``'s layout). ``render_rust_type``
68+
#: raises wherever one appears and the enclosing object is skipped.
69+
RUST_UNSUPPORTED_ORIGINS = frozenset({"Dict", "List", "Union", "tuple"})
70+
71+
#: ``Optional<T>`` FIELD payload classification: C++ ``ffi::Optional<T>`` has
72+
#: three in-place layouts (include/tvm/ffi/optional.h), mirrored as
73+
#: POD scalar -> ``OptionPod<T>``, ``String`` -> ``OptionStr``, ``ObjectRef``
74+
#: subtype (dotted key or a bare origin below) -> ``Option<T>``. Any other
75+
#: payload has no mirror and skips the object.
76+
RUST_OPTION_POD_ORIGINS = frozenset({"int", "float", "bool"})
77+
RUST_OPTION_STR_ORIGINS = frozenset({"str", "ffi.String"})
78+
#: Checked before the dotted-key rule: ``ffi.Bytes`` is dotted but in-cell,
79+
#: not ptr-based, and has no mirror yet.
80+
RUST_OPTION_BYTES_ORIGINS = frozenset({"bytes", "ffi.Bytes"})
81+
#: Bare ``Object`` is deliberately absent: the ty_map renders it as the inline
82+
#: header struct ``tvm_ffi::Object`` (no niche, not AnyCompatible), so
83+
#: ``Option<Object>`` would silently break the layout.
84+
RUST_OPTION_PTR_ORIGINS = frozenset({"Array", "Map", "Callable", "Tensor", "Shape"})
85+
86+
#: Origins whose Rust rendering is not ``AnyCompatible``: the crate's bounds
87+
#: reject them as container elements / ``Optional`` payloads, so rendering
88+
#: would emit uncompilable code -- raise and skip the object instead.
89+
RUST_NON_ELEMENT_ORIGINS = frozenset({"Any", "Object", "ffi.Object"})
90+
91+
#: Fully qualified paths of the ``Optional`` field mirrors.
92+
RUST_OPTION_POD_PATH = "tvm_ffi::option::OptionPod"
93+
RUST_OPTION_STR_PATH = "tvm_ffi::option::OptionStr"
7294

7395
#: Module-prefix rewrites for ``use`` paths: builtin ``ffi.*`` type keys live at
7496
#: the crate root.

python/tvm_ffi/stub/rust_generator/generator.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,12 @@
4444
class RustGenerator:
4545
"""Generator that emits Rust binding stubs.
4646
47-
Objects using an unrepresentable origin (``Union`` / ``Map`` / ``Dict`` /
48-
``List``) are skipped with a warning; global functions and
49-
``__all__``/``export`` re-exports are not generated.
47+
Objects using an unrepresentable origin (``Union`` / ``Dict`` / ``List`` /
48+
``tuple``, or containers/``Optional`` over payloads the crate cannot hold)
49+
are skipped with a warning; global functions and ``__all__``/``export``
50+
re-exports are not generated. The backend targets natively-laid-out C++
51+
objects only -- running it on Python-defined (``py_class``) types is
52+
undefined (their fields use Python-side storage conventions).
5053
"""
5154

5255
name = "rust"

python/tvm_ffi/stub/rust_generator/utils.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828

2929
from ..utils import UnsupportedTypeError
3030
from . import consts as C
31-
from .consts import RUST_UNSUPPORTED_ORIGINS
31+
from .consts import RUST_NON_ELEMENT_ORIGINS, RUST_UNSUPPORTED_ORIGINS
3232

3333
if TYPE_CHECKING:
3434
from tvm_ffi.core import TypeSchema
@@ -98,6 +98,19 @@ def record(self, name: str) -> str:
9898
return probe.leaf
9999

100100

101+
def _check_element(container_origin: str, elem: TypeSchema) -> None:
102+
"""Reject a container element / Optional payload the crate cannot hold.
103+
104+
Deeper nesting is covered by the recursive :func:`render_rust_type` call.
105+
"""
106+
if elem.origin in RUST_NON_ELEMENT_ORIGINS:
107+
raise UnsupportedTypeError(
108+
container_origin,
109+
f"`{elem.origin}` cannot be a `{container_origin}` element/payload "
110+
f"(its crate rendering is not `AnyCompatible`)",
111+
)
112+
113+
101114
def render_rust_type(schema: TypeSchema, ty_render: Callable[[str], str]) -> str:
102115
"""Render a :class:`TypeSchema` into a Rust type expression.
103116
@@ -106,16 +119,32 @@ def render_rust_type(schema: TypeSchema, ty_render: Callable[[str], str]) -> str
106119
:class:`UnsupportedTypeError` for origins the crate cannot represent.
107120
"""
108121
origin = schema.origin
109-
args = schema.args or ()
122+
args = schema.args
110123

111124
if origin in RUST_UNSUPPORTED_ORIGINS:
112125
raise UnsupportedTypeError(origin)
113126

114127
if origin == "Array":
115128
assert args # TypeSchema's post_init fills a missing element type.
129+
_check_element(origin, args[0])
116130
elem = render_rust_type(args[0], ty_render)
117131
return f"{ty_render('Array')}<{elem}>"
118132

133+
if origin == "Map":
134+
assert len(args) == 2 # TypeSchema's post_init fills a bare Map to (Any, Any).
135+
for arg in args:
136+
_check_element(origin, arg)
137+
key = render_rust_type(args[0], ty_render)
138+
value = render_rust_type(args[1], ty_render)
139+
return f"{ty_render('Map')}<{key}, {value}>"
140+
141+
if origin == "Optional":
142+
# Value position only (`None` <-> kTVMFFINone via Any); FIELD position
143+
# is layout-sensitive and routes through `render_struct_field`.
144+
(payload,) = args # TypeSchema's post_init enforces exactly one argument.
145+
_check_element(origin, payload)
146+
return f"Option<{render_rust_type(payload, ty_render)}>"
147+
119148
if origin == "Callable":
120149
# The crate's Function is type-erased: no generic params.
121150
return ty_render("Callable")

0 commit comments

Comments
 (0)