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 e2179b2e [Stubgen] One grammar for one-line directives: `subject:
target(keys)` (#968)
e2179b2e is described below
commit e2179b2e82d39be91076da07d840720b6e706359
Author: Tianqi Chen <[email protected]>
AuthorDate: Sun Nov 8 16:50:53 2026 -0500
[Stubgen] One grammar for one-line directives: `subject: target(keys)`
(#968)
Stubgen's one-line directives each had their own payload syntax (`a ->
b`, `T.f -> N(i32) { A=0 }`, `x;False;y`, bare `allow`). This replaces
all of them with one grammar, shared by the Python and Rust generators:
```
<comment> tvm-ffi-stubgen(<subject>): <target>(<key>, <key> = <value>, ...)
```
The parentheses are omitted when there are no keys. Lines for the same
subject and target merge; a repeatable key (`upcast`) collects every
value, and any other key may repeat only with the same value. Commas and
`=` inside brackets belong to the value, so `pub(crate)` and `Option<A,
B>` stay whole.
| Before | After |
|---|---|
| `ty-map: A -> B` | `object: A(ty_map = B)` |
| `skip: T` / `opaque: T` / `custom-new: T` / `no-new: T` | `object:
T(skip)` / `(opaque)` / `(custom_new)` / `(no_new)` |
| `upcast: T -> V` | `object: T(upcast = V)` |
| `field: T.f -> Ty` / `nullable: T.f` / `visibility: T.f -> pub(crate)`
| `field: T.f(type = Ty, nullable, visibility = pub(crate))` |
| `enum: T.f -> K(i32) closed { A=0, B=1 }` | `enum: K(repr = i32,
closed, A = 0, B = 1)` + `field: T.f(type = K)` |
| `prefix: p` / `missing-docs: allow` | `file: mod(prefix = p,
missing_docs = allow)` |
| `import-object: a.b.C;True;_C` | `import: a.b.C(type_checking_only,
alias = _C)` |
`begin`/`end` blocks and `skip-file` are unchanged.
A small parser (`stub/directives.py`) checks each key against a
per-subject table. The pipeline declares `object(skip, ty_map)` and
`file(prefix)`. Each generator declares its own keys in
`Generator.directive_keys` and gets the parsed file through
`new_imports`. In-tree directives, the `rust_stubgen` example, and the
stubgen docs are updated to the new form.
---
docs/packaging/stubgen.rst | 52 ++--
.../python/my_ffi_extension/_ffi_api.py | 6 +-
examples/rust_stubgen/README.md | 48 ++--
.../rust/src/generated/rust_stubgen/mod.rs | 9 +-
python/tvm_ffi/access_path.py | 4 +-
python/tvm_ffi/stub/cli.py | 94 ++++----
python/tvm_ffi/stub/consts.py | 30 +--
python/tvm_ffi/stub/directives.py | 113 +++++++++
python/tvm_ffi/stub/file_utils.py | 6 +-
python/tvm_ffi/stub/generator.py | 26 +-
python/tvm_ffi/stub/python_generator/codegen.py | 4 +-
python/tvm_ffi/stub/python_generator/consts.py | 7 +
python/tvm_ffi/stub/python_generator/generator.py | 26 +-
python/tvm_ffi/stub/python_generator/utils.py | 2 +-
python/tvm_ffi/stub/rust_generator/codegen.py | 34 +--
python/tvm_ffi/stub/rust_generator/consts.py | 18 +-
python/tvm_ffi/stub/rust_generator/directives.py | 217 ++++++++---------
python/tvm_ffi/stub/rust_generator/generator.py | 20 +-
python/tvm_ffi/stub/rust_generator/utils.py | 4 +-
python/tvm_ffi/testing/testing.py | 26 +-
tests/python/test_stubgen.py | 85 ++++---
tests/python/test_stubgen_rust.py | 268 +++++++++++----------
22 files changed, 592 insertions(+), 507 deletions(-)
diff --git a/docs/packaging/stubgen.rst b/docs/packaging/stubgen.rst
index f9271f28..ecf01dfe 100644
--- a/docs/packaging/stubgen.rst
+++ b/docs/packaging/stubgen.rst
@@ -384,45 +384,51 @@ When you run the tool, it:
# tvm-ffi-stubgen(end)
]
-``ty-map`` - Type Mapping
+``skip-file`` - Skip File
+ Prevents the tool from modifying the file. Place anywhere in the file.
+
+ .. code-block:: python
+
+ # tvm-ffi-stubgen(skip-file)
+
+**One-line directives**
+
+The other directives are settings on a single line, written
+``<subject>: <target>(<key>, <key> = <value>, ...)``; the parentheses are
omitted when
+there are no keys. Several lines for the same subject and target merge.
+
+``object: <type_key>(ty_map = <path>)`` - Type Mapping
Maps a C++ type key to a different Python type path. This is useful when
the Python
class is defined in a different module than the type key suggests.
.. code-block:: python
- # tvm-ffi-stubgen(ty-map): ffi.reflection.AccessStep ->
ffi.access_path.AccessStep
+ # tvm-ffi-stubgen(object): ffi.reflection.AccessStep(ty_map =
ffi.access_path.AccessStep)
-``prefix`` - Demand a Namespace
+``file: mod(prefix = <prefix>)`` - Demand a Namespace
Rust target only. Adds an ``object/<type_key>`` block to the file for every
object
- registered directly under the prefix that no processed file defines yet;
``skip``
- leaves one out, and a key the ``tvm_ffi`` crate binds itself (every
``ffi.*`` key)
- is never added. A block under the prefix whose type is no longer registered
is
- removed. Code outside the blocks is preserved.
+ registered directly under the prefix that no processed file defines yet; an
object's
+ ``skip`` key leaves one out, and a key the ``tvm_ffi`` crate binds itself
(every
+ ``ffi.*`` key) is never added. A block under the prefix whose type is no
longer
+ registered is removed. Code outside the blocks is preserved.
.. code-block:: rust
- // tvm-ffi-stubgen(prefix): my_ffi_extension
- // tvm-ffi-stubgen(skip): my_ffi_extension.Internal
+ // tvm-ffi-stubgen(file): mod(prefix = my_ffi_extension)
+ // tvm-ffi-stubgen(object): my_ffi_extension.Internal(skip)
A generated type is named from the module of the file its block is in, not
from its
type key: the module follows the file's place under the ``src/`` of the
nearest
``Cargo.toml`` (``src/ir/prim.rs`` is ``crate::ir::prim``). Other files
refer to it by
- that ``crate::`` path, so a namespace can live anywhere in the module tree.
+ that ``crate::`` path, so a namespace can live anywhere in the module tree.
The Rust
+ target's other keys are listed in ``examples/rust_stubgen``.
-``import-object`` - Import Object
- Injects a custom import into generated code. The format is
- ``<full_name>;<type_checking_only>;<alias>``.
+``import: <full_name>(type_checking_only, alias = <alias>)`` - Import Object
+ Keeps an import in the generated import section; both keys are optional.
.. code-block:: python
- # tvm-ffi-stubgen(import-object): ffi.Object;False;_ffi_Object
-
- This imports ``ffi.Object`` as ``_ffi_Object`` for use in generated code.
The second
- field (``False``) indicates the import is not TYPE_CHECKING-only.
-
-``skip-file`` - Skip File
- Prevents the tool from modifying the file. Place anywhere in the file.
-
- .. code-block:: python
+ # tvm-ffi-stubgen(import): ffi.Object(alias = _ffi_Object)
- # tvm-ffi-stubgen(skip-file)
+ This imports ``ffi.Object`` as ``_ffi_Object`` for use in generated code;
+ ``type_checking_only`` would import it under ``TYPE_CHECKING`` only.
diff --git a/examples/python_packaging/python/my_ffi_extension/_ffi_api.py
b/examples/python_packaging/python/my_ffi_extension/_ffi_api.py
index 0b67bd8b..de71f937 100644
--- a/examples/python_packaging/python/my_ffi_extension/_ffi_api.py
+++ b/examples/python_packaging/python/my_ffi_extension/_ffi_api.py
@@ -27,7 +27,7 @@ if TYPE_CHECKING:
# isort: on
# fmt: on
# tvm-ffi-stubgen(end)
-# tvm-ffi-stubgen(import-object):
tvm_ffi.libinfo.load_lib_module;False;_FFI_LOAD_LIB
+# tvm-ffi-stubgen(import): tvm_ffi.libinfo.load_lib_module(alias =
_FFI_LOAD_LIB)
LIB = _FFI_LOAD_LIB("my_ffi_extension", "my_ffi_extension")
# tvm-ffi-stubgen(begin): global/my_ffi_extension
# fmt: off
@@ -37,8 +37,8 @@ if TYPE_CHECKING:
def raise_error(_0: str, /) -> None: ...
# fmt: on
# tvm-ffi-stubgen(end)
-# tvm-ffi-stubgen(import-object): tvm_ffi.register_object;False;_FFI_REG_OBJ
-# tvm-ffi-stubgen(import-object): ffi.Object;False;_ffi_Object
+# tvm-ffi-stubgen(import): tvm_ffi.register_object(alias = _FFI_REG_OBJ)
+# tvm-ffi-stubgen(import): ffi.Object(alias = _ffi_Object)
@_FFI_REG_OBJ("my_ffi_extension.IntPair")
class IntPair(_ffi_Object):
"""FFI binding for `my_ffi_extension.IntPair`."""
diff --git a/examples/rust_stubgen/README.md b/examples/rust_stubgen/README.md
index d7ebeda0..60ac7c37 100644
--- a/examples/rust_stubgen/README.md
+++ b/examples/rust_stubgen/README.md
@@ -70,12 +70,12 @@ The Rust crate depends on the `tvm-ffi` crate of this
repository and needs
The binding file opens with a `prefix` line:
```rust
-// tvm-ffi-stubgen(prefix): rust_stubgen
+// tvm-ffi-stubgen(file): mod(prefix = rust_stubgen)
```
It makes the file own the `rust_stubgen` namespace: every object registered
directly under it gets an `object/<type_key>` block in this file on the next
-run, and a `skip` line (`// tvm-ffi-stubgen(skip): rust_stubgen.Internal`)
+run, and a `skip` key (`// tvm-ffi-stubgen(object):
rust_stubgen.Internal(skip)`)
leaves one out. Add a second object to `int_pair.cc` and rebuild, and its block
appears after `IntPair`; delete a block and it comes back, so dropping a
binding
means writing `skip`. Remove the object from the C++ side and its block is
@@ -87,12 +87,12 @@ file the command line points at. This example keeps them
under
## Hand-written constructors
-`IntPair::new` is hand-written. The `custom-new` directive makes the generator
+`IntPair::new` is hand-written. The object's `custom_new` key makes the
generator
name its lossless allocator `from_fields` instead of `new`, and the
hand-written constructor calls it after deriving `kind` from the operands:
```rust
-// tvm-ffi-stubgen(custom-new): rust_stubgen.IntPair
+// tvm-ffi-stubgen(object): rust_stubgen.IntPair(custom_new)
impl IntPair {
pub fn new(a: i64, b: i64) -> Self {
@@ -102,37 +102,39 @@ impl IntPair {
}
```
-Without the directive the block would define `IntPair::new` too and the crate
+Without the key the block would define `IntPair::new` too and the crate
would not compile.
## Directives
-The generated file keeps one-line directives the generator reads on every run.
-Besides `prefix` and `custom-new`, this example declares the integer field
-`kind` as an open newtype:
+The generated file keeps one-line directives the generator reads on every run,
+each `<subject>: <target>(<key>, <key> = <value>, ...)` (no parentheses without
+keys). Lines for the same subject and target merge. Besides `prefix` and
+`custom_new`, this example declares the integer field `kind` as an open
+newtype:
```rust
-// tvm-ffi-stubgen(enum): rust_stubgen.IntPair.kind -> PairKind(i32) {
Unordered=0, Ordered=1 }
+// tvm-ffi-stubgen(enum): PairKind(repr = i32, Unordered = 0, Ordered = 1)
+// tvm-ffi-stubgen(field): rust_stubgen.IntPair.kind(type = PairKind)
```
-Four more 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`),
-`visibility` makes it `pub(crate)` instead of `pub`
-(`// tvm-ffi-stubgen(visibility): rust_stubgen.IntPair.a -> pub(crate)`), and
`upcast` adds a conversion to a hand-written typed view
-(`// tvm-ffi-stubgen(upcast): rust_stubgen.IntPair -> MyView`).
+A field also takes `type` naming any Rust type
+(`// tvm-ffi-stubgen(field): rust_stubgen.IntPair.a(type = MyInt)`), `nullable`
+to wrap it in `Option`, and `visibility = pub(crate)` instead of `pub`. An
+object takes `upcast = MyView` (repeatable) to add a conversion to a
+hand-written typed view, `opaque` to keep its layout opaque, and `no_new` to
+skip the generated allocator.
-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:
+When the C++ side fixes the value set in its ABI, `closed` 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 }
+// tvm-ffi-stubgen(enum): PairKind(repr = 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
+`// tvm-ffi-stubgen(file): mod(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.
@@ -142,9 +144,9 @@ Every type an object refers to (its parent, its ancestors,
the types of its
fields) has to be provided in the same `tvm-ffi-stubgen` run: `ffi.*` types
come from the `tvm-ffi` crate (a field of an `ffi.*` type the crate does not
bind is read as `Any`), an `object/<key>` block in any processed file
-generates it, and a `ty-map` points at a hand-written binding whose object
+generates it, and a `ty_map` key points at a hand-written binding whose object
struct is named `<Name>Obj`
-(`// tvm-ffi-stubgen(ty-map): rust_stubgen.IntPair -> crate::hand::IntPair`).
+(`// tvm-ffi-stubgen(object): rust_stubgen.IntPair(ty_map =
crate::hand::IntPair)`).
Anything else is an error listing the missing keys, so a partial binding never
references a module that does not exist. A hand-written binding may sit next to
the generated ones: the file of its own module refers to it by name instead of
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 1c7e1ad2..36d6d5c2 100644
--- a/examples/rust_stubgen/rust/src/generated/rust_stubgen/mod.rs
+++ b/examples/rust_stubgen/rust/src/generated/rust_stubgen/mod.rs
@@ -21,7 +21,7 @@
//! FFI bindings for `rust_stubgen` (generated by tvm-ffi-stubgen).
// Every object registered under `rust_stubgen` gets its block in this file;
`skip` leaves one out.
-// tvm-ffi-stubgen(prefix): rust_stubgen
+// tvm-ffi-stubgen(file): mod(prefix = rust_stubgen)
// tvm-ffi-stubgen(begin): import-section
use tvm_ffi::Error;
@@ -31,11 +31,12 @@ 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 types it as
an
+// The `kind` field is an integer on the C++ side; these directives type 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(enum): PairKind(repr = i32, Unordered = 0, Ordered = 1)
+// tvm-ffi-stubgen(field): rust_stubgen.IntPair.kind(type = PairKind)
// `IntPair::new` is hand-written below, so the generated allocator is
`from_fields`.
-// tvm-ffi-stubgen(custom-new): rust_stubgen.IntPair
+// tvm-ffi-stubgen(object): rust_stubgen.IntPair(custom_new)
// tvm-ffi-stubgen(begin): object/rust_stubgen.IntPair
#[repr(C)]
diff --git a/python/tvm_ffi/access_path.py b/python/tvm_ffi/access_path.py
index 8e887639..d690ba22 100644
--- a/python/tvm_ffi/access_path.py
+++ b/python/tvm_ffi/access_path.py
@@ -50,7 +50,7 @@ class AccessKind(IntEnum):
class AccessStep(Object):
"""Access step container."""
- # tvm-ffi-stubgen(ty-map): ffi.reflection.AccessStep ->
ffi.access_path.AccessStep
+ # tvm-ffi-stubgen(object): ffi.reflection.AccessStep(ty_map =
ffi.access_path.AccessStep)
# tvm-ffi-stubgen(begin): object/ffi.reflection.AccessStep
# fmt: off
kind: int
@@ -81,7 +81,7 @@ class AccessPath(Object):
"""
- # tvm-ffi-stubgen(ty-map): ffi.reflection.AccessPath ->
ffi.access_path.AccessPath
+ # tvm-ffi-stubgen(object): ffi.reflection.AccessPath(ty_map =
ffi.access_path.AccessPath)
# tvm-ffi-stubgen(begin): object/ffi.reflection.AccessPath
# fmt: off
parent: Object | None
diff --git a/python/tvm_ffi/stub/cli.py b/python/tvm_ffi/stub/cli.py
index eb3381c6..5a824273 100644
--- a/python/tvm_ffi/stub/cli.py
+++ b/python/tvm_ffi/stub/cli.py
@@ -29,6 +29,7 @@ from typing import TYPE_CHECKING
from tvm_ffi.core import _object_type_key_to_index
from . import consts as C
+from .directives import Directives, Value
from .file_utils import CodeBlock, FileInfo, collect_files, syntax_for
from .generator import generator_names, get_generator
from .layout import classify, write_coverage_report
@@ -70,20 +71,14 @@ def __main__() -> int:
init_path = init_path.parent
# Stage 1: Collect information
- # - type maps: `tvm-ffi-stubgen(ty-map)`
+ # - type maps: `tvm-ffi-stubgen(object): <type_key>(ty_map = <name>)`
# - defined global functions: `tvm-ffi-stubgen(begin): global/...`
# - defined object types: `tvm-ffi-stubgen(begin): object/...`
ty_map: dict[str, str] = generator.default_ty_map()
failed = 0
stale = 0
for file in files:
- try:
- _stage_1(file, ty_map)
- except Exception:
- failed += 1
- print(
- f'{C.TERM_RED}[Failed] File "{file.path}":
{traceback.format_exc()}{C.TERM_RESET}'
- )
+ _stage_1(file, ty_map, generator)
# Stage 2. Generate stubs if they are not defined on the file.
generated_prefixes: set[str] = set()
@@ -101,8 +96,8 @@ def __main__() -> int:
# `--check` compares the generator's view of each file before the run
edits any block.
before = {file.path: generator.check_view(file.code_blocks) for file in
files if opt.check}
- # Stage 2b. Add the object blocks a `tvm-ffi-stubgen(prefix)` file asks
for. This runs
- # after `--init`, which rewrites files on disk and reloads them.
+ # Stage 2b. Add the object blocks a `tvm-ffi-stubgen(file): mod(prefix =
...)` file asks
+ # for. This runs after `--init`, which rewrites files on disk and reloads
them.
failed += _roll_out_prefixes(files, generator)
# Stage 3: Process
@@ -150,20 +145,31 @@ def __main__() -> int:
return 2 if failed else 1 if stale else 0
-def _stage_1(
- file: FileInfo,
- ty_map: dict[str, str],
-) -> None:
+def _directives(file: FileInfo, generator: Generator) -> Directives:
+ """Parse the one-line directives of ``file`` against the pipeline's and
the generator's keys."""
+ pipeline, own = C.PIPELINE_DIRECTIVE_KEYS, generator.directive_keys
+ directives = Directives(
+ {s: {**pipeline.get(s, {}), **own.get(s, {})} for s in {*pipeline,
*own}}
+ )
for code in file.code_blocks:
- if code.kind != "directive" or code.param[0] != "ty-map":
- continue
- try:
- lhs, rhs = code.param[1].split("->")
- except ValueError as e:
- raise ValueError(
- f"Invalid ty_map format at line {code.lineno_start}. Example:
`A.B -> C.D`"
- ) from e
- ty_map[lhs.strip()] = rhs.strip()
+ if code.kind == "directive":
+ subject, payload = code.param
+ directives.add(subject, payload, code.lineno_start)
+ return directives
+
+
+def _settings(file: FileInfo, generator: Generator) -> dict[str, dict[str,
dict[str, Value]]]:
+ """Return the settings the pipeline reads ahead of ``_stage_3``, which
reports a bad file."""
+ try:
+ return _directives(file, generator).settings
+ except ValueError:
+ return {}
+
+
+def _stage_1(file: FileInfo, ty_map: dict[str, str], generator: Generator) ->
None:
+ for type_key, keys in _settings(file, generator).get("object", {}).items():
+ if "ty_map" in keys:
+ ty_map[type_key] = str(keys["ty_map"])
def _roll_out_prefixes(files: list[FileInfo], generator: Generator) -> int:
@@ -179,20 +185,17 @@ def _roll_out_prefixes(files: list[FileInfo], generator:
Generator) -> int:
owners: dict[str, Path] = {}
failed = 0
for file in files:
- directives = [c for c in file.code_blocks if c.kind == "directive"]
- heads = [c for c in directives if c.param[0] == "prefix"]
- if not heads:
+ settings = _settings(file, generator)
+ if "prefix" not in settings.get("file", {}).get("mod", {}):
continue
- head = heads[0]
- prefix = head.param[1].rstrip(".")
- error = ""
- if len(heads) > 1:
- error = f"more than one `prefix` directive (line
{heads[1].lineno_start})"
- elif owners.setdefault(prefix, file.path) != file.path:
- error = f"prefix `{prefix}` is already declared by
{owners[prefix]}"
- if error:
+ prefix = str(settings["file"]["mod"]["prefix"]).rstrip(".")
+ head = next(c for c in file.code_blocks if c.kind == "directive" and
c.param[0] == "file")
+ if owners.setdefault(prefix, file.path) != file.path:
failed += 1
- print(f'{C.TERM_RED}[Failed] File "{file.path}":
{error}{C.TERM_RESET}')
+ print(
+ f'{C.TERM_RED}[Failed] File "{file.path}": prefix `{prefix}`
is already '
+ f"declared by {owners[prefix]}{C.TERM_RESET}"
+ )
continue
_remove_unregistered_blocks(file, prefix, registry.get(prefix, []))
if prefix not in registry:
@@ -200,7 +203,7 @@ def _roll_out_prefixes(files: list[FileInfo], generator:
Generator) -> int:
f"{C.TERM_YELLOW}[Skipped] No registered object under prefix
`{prefix}`{C.TERM_RESET}"
)
continue
- skipped = {c.param[1].strip() for c in directives if c.param[0] ==
"skip"}
+ skipped = {key for key, keys in settings.get("object", {}).items() if
"skip" in keys}
keys = [
key
for key in registry[prefix]
@@ -279,10 +282,10 @@ def _stage_2(
code.param for file in files for code in file.code_blocks if code.kind
== "object"
}
skipped: set[str] = {
- code.param[1].strip()
+ type_key
for file in files
- for code in file.code_blocks
- if code.kind == "directive" and code.param[0] == "skip"
+ for type_key, keys in _settings(file, generator).get("object",
{}).items()
+ if "skip" in keys
}
# Step 0. Generate missing `_ffi_api.py` and `__init__.py` under each
prefix.
@@ -367,17 +370,8 @@ def _stage_3( # noqa: PLR0912
defined_funcs: set[str] = set()
unregistered: list[str] = []
defined_types: set[str] = set()
- imports = generator.new_imports()
- # Stage 1. Hand the one-line directives the pipeline does not consume
itself to the generator.
- for code in file.code_blocks:
- if code.kind != "directive":
- continue
- name, payload = code.param
- if name in C.PIPELINE_DIRECTIVE_KINDS:
- continue # consumed by `_stage_1`
- if name not in generator.directive_kinds:
- raise ValueError(f"Unknown directive `{name}` at line
{code.lineno_start}")
- generator.add_directive(imports, name, payload, code.lineno_start)
+ # Stage 1. Parse the one-line directives and seed the generator's
collector with them.
+ imports = generator.new_imports(_directives(file, generator))
# Stage 2. Process `tvm-ffi-stubgen(begin): global/...`
for code in file.code_blocks:
if code.kind == "global":
diff --git a/python/tvm_ffi/stub/consts.py b/python/tvm_ffi/stub/consts.py
index 5cb36304..6b4dcfb0 100644
--- a/python/tvm_ffi/stub/consts.py
+++ b/python/tvm_ffi/stub/consts.py
@@ -23,6 +23,8 @@ from typing import Literal
from typing_extensions import TypeAlias
+from .directives import Key, KeyTable
+
@dataclasses.dataclass(frozen=True)
class MarkerSyntax:
@@ -52,24 +54,14 @@ class MarkerSyntax:
"""Marker that closes a generated block: ``<comment>
tvm-ffi-stubgen(end)``."""
return f"{self.prefix}end)"
- @property
- def ty_map(self) -> str:
- """The ``ty-map`` directive marker: ``<comment>
tvm-ffi-stubgen(ty-map):``."""
- return self.directive("ty-map")
-
- @property
- def import_object(self) -> str:
- """The ``import-object`` directive marker: ``<comment>
tvm-ffi-stubgen(import-object):``."""
- return self.directive("import-object")
-
@property
def skip_file(self) -> str:
"""Whole-file opt-out directive: ``<comment>
tvm-ffi-stubgen(skip-file)``."""
return f"{self.prefix}skip-file)"
- def directive(self, name: str) -> str:
- """One-line directive marker: ``<comment>
tvm-ffi-stubgen(<name>):``."""
- return f"{self.prefix}{name}):"
+ def directive(self, subject: str) -> str:
+ """One-line directive marker: ``<comment>
tvm-ffi-stubgen(<subject>):``."""
+ return f"{self.prefix}{subject}):"
PYTHON_SYNTAX = MarkerSyntax(comment="#")
@@ -84,10 +76,14 @@ SYNTAX_BY_EXT: dict[str, MarkerSyntax] = {
".rs": RUST_SYNTAX,
}
-#: One-line directive names consumed by the language-neutral pipeline.
Generators
-#: must not declare these names; every other name must be declared by the
active
-#: generator (``Generator.directive_kinds``).
-PIPELINE_DIRECTIVE_KINDS: frozenset[str] = frozenset({"ty-map", "prefix",
"skip"})
+#: Directive keys the language-neutral pipeline reads (see
:mod:`.directives`): an
+#: ``object`` maps its type key (``ty_map``) or opts out of ``--init`` and
``prefix``
+#: (``skip``); ``file: mod(prefix = <p>)`` asks for a block per object under
``p``.
+#: The active generator declares every other key
(``Generator.directive_keys``).
+PIPELINE_DIRECTIVE_KEYS: KeyTable = {
+ "object": {"skip": Key(), "ty_map": Key(takes_value=True)},
+ "file": {"prefix": Key(takes_value=True)},
+}
STUB_BLOCK_KINDS: TypeAlias = Literal[
"global",
diff --git a/python/tvm_ffi/stub/directives.py
b/python/tvm_ffi/stub/directives.py
new file mode 100644
index 00000000..2d362e0a
--- /dev/null
+++ b/python/tvm_ffi/stub/directives.py
@@ -0,0 +1,113 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""The one-line directive grammar shared by every generator.
+
+A directive names a subject, a target, and optional keys::
+
+ <comment> tvm-ffi-stubgen(<subject>): <target>
+ <comment> tvm-ffi-stubgen(<subject>): <target>(<key>, <key> = <value>, ...)
+
+A key is a flag or takes a value; commas and ``=`` inside ``()``, ``[]``,
``{}`` or
+``<>`` belong to the value, so ``pub(crate)`` and ``Option<A, B>`` are one
value.
+Lines for the same subject and target merge: a repeatable key collects every
value,
+any other key may repeat only with the same value. The pipeline reads the
+``object`` / ``file`` keys of :data:`consts.PIPELINE_DIRECTIVE_KEYS`; the
active
+generator declares the rest (``Generator.directive_keys``).
+"""
+
+from __future__ import annotations
+
+import dataclasses
+from collections.abc import Mapping
+from typing import Union
+
+#: A parsed value: ``True`` for a flag, the text, or every text of a
repeatable key.
+Value = Union[bool, str, list[str]]
+
+
[email protected](frozen=True)
+class Key:
+ """How a key is written: a flag, or ``key = value`` (``values`` lists the
legal ones)."""
+
+ takes_value: bool = False
+ repeatable: bool = False
+ values: tuple[str, ...] = ()
+
+
+#: Key tables: subject -> key -> spelling. ``"*"`` accepts any other key with
a value.
+KeyTable = Mapping[str, Mapping[str, Key]]
+
+
+class Directives:
+ """The directives of one file: ``settings[subject][target][key]``."""
+
+ def __init__(self, keys: KeyTable) -> None:
+ self.keys = keys
+ self.settings: dict[str, dict[str, dict[str, Value]]] = {s: {} for s
in keys}
+ self._lines: dict[tuple[str, str, str], int] = {}
+
+ def add(self, subject: str, payload: str, lineno: int) -> None:
+ """Parse and merge one directive; raise ``ValueError`` on a malformed
one."""
+ if subject not in self.keys:
+ raise ValueError(f"Unknown directive `{subject}` at line {lineno}")
+
+ def invalid(reason: str) -> ValueError:
+ return ValueError(f"Invalid `{subject}` directive at line
{lineno}: {reason}")
+
+ target, paren, rest = payload.strip().partition("(")
+ target = target.strip()
+ if paren and not rest.endswith(")"):
+ raise invalid("expected `<target>(<key>, <key> = <value>, ...)`")
+ if not target or any(c.isspace() for c in target):
+ raise invalid(f"bad target `{target}`")
+ if (subject == "field" and "." not in target.strip(".")) or (
+ subject == "file" and target != "mod"
+ ):
+ raise invalid("the target is `<type_key>.<field>`" if subject ==
"field" else "`mod`")
+ entry = self.settings[subject].setdefault(target, {})
+ for arg in filter(str.strip, _split(rest[:-1], ",")):
+ key, *value_parts = _split(arg, "=")
+ key, eq, value = key.strip(), bool(value_parts),
"=".join(value_parts).strip()
+ spec = self.keys[subject].get(key) or self.keys[subject].get("*")
+ if not key.isidentifier() or spec is None:
+ raise invalid(f"unknown key `{key}`")
+ if eq != spec.takes_value or (eq and not value):
+ raise invalid(
+ f"`{key}` takes a value" if spec.takes_value else
f"`{key}` is a flag"
+ )
+ if spec.values and value not in spec.values:
+ raise invalid(f"`{key}` is one of {', '.join(spec.values)}")
+ if spec.repeatable:
+ collected = entry.setdefault(key, [])
+ assert isinstance(collected, list)
+ collected.append(value)
+ continue
+ new: Value = value if eq else True
+ first = self._lines.setdefault((subject, target, key), lineno)
+ if entry.setdefault(key, new) != new:
+ raise invalid(f"`{key}` conflicts with line {first}")
+
+
+def _split(text: str, sep: str) -> list[str]:
+ """Split ``text`` on each ``sep`` outside brackets."""
+ parts, depth, start = [], 0, 0
+ for i, char in enumerate(text):
+ depth += (char in "([{<") - (char in ")]}>")
+ if char == sep and depth == 0:
+ parts.append(text[start:i])
+ start = i + 1
+ return [*parts, text[start:]]
diff --git a/python/tvm_ffi/stub/file_utils.py
b/python/tvm_ffi/stub/file_utils.py
index 381254cf..8e93ed42 100644
--- a/python/tvm_ffi/stub/file_utils.py
+++ b/python/tvm_ffi/stub/file_utils.py
@@ -68,8 +68,8 @@ class CodeBlock:
def from_begin_line(lineo: int, line: str, syntax: C.MarkerSyntax) ->
CodeBlock:
"""Parse a line to create a CodeBlock if it contains a stub begin
marker."""
if not line.startswith(syntax.begin):
- # One-line directive `<comment> tvm-ffi-stubgen(<name>):
<payload>`. The payload is
- # kept verbatim for whoever consumes the name: the pipeline or the
generator.
+ # One-line directive `<comment> tvm-ffi-stubgen(<subject>):
<payload>`. The payload is
+ # kept verbatim for `directives.Directives` to parse.
name, sep, payload = line[len(syntax.prefix) :].partition("):")
if not sep or not name or not name.replace("-",
"_").isidentifier():
raise ValueError(f"Unknown stub type at line {lineo}: {line}")
@@ -188,7 +188,7 @@ class FileInfo:
codes.append(code)
code = None
elif clean_line.startswith(syntax.prefix):
- # Process a one-line directive "<comment>
tvm-ffi-stubgen(<name>): ..."
+ # Process a one-line directive "<comment>
tvm-ffi-stubgen(<subject>): ..."
dir_code = CodeBlock.from_begin_line(lineno, clean_line,
syntax)
dir_code.lineno_end = lineno
dir_code.lines.append(line)
diff --git a/python/tvm_ffi/stub/generator.py b/python/tvm_ffi/stub/generator.py
index 3a8c6885..1c6b3407 100644
--- a/python/tvm_ffi/stub/generator.py
+++ b/python/tvm_ffi/stub/generator.py
@@ -30,8 +30,8 @@ The stub generator separates two concerns:
A :class:`Generator` encapsulates concern (2); ``cli.py`` drives concern (1)
and
delegates every act of emitting text — and every act of collecting imports — to
the active generator. The import collector is opaque to the pipeline:
``cli.py``
-asks the generator to create one, seed it from the one-line directives the
-generator declares, and later render it, but never reaches inside. Adding a
+asks the generator to create one from the file's one-line directives
+(:mod:`.directives`), and later render it, but never reaches inside. Adding a
language is therefore "implement one more :class:`Generator`" rather than
forking the pipeline.
"""
@@ -48,6 +48,7 @@ if TYPE_CHECKING:
from collections.abc import Mapping
from pathlib import Path
+ from .directives import Directives, KeyTable
from .file_utils import CodeBlock
from .utils import FuncInfo, InitConfig, ObjectInfo, Options
@@ -77,10 +78,10 @@ class Generator(Protocol):
#: Comment-marker syntax for the files this generator emits.
syntax: C.MarkerSyntax
- #: Names of the one-line directives (``<comment> tvm-ffi-stubgen(<name>):
<payload>``)
- #: this generator consumes. Names in
:data:`consts.PIPELINE_DIRECTIVE_KINDS` belong
- #: to the pipeline; any other undeclared name is an error.
- directive_kinds: frozenset[str]
+ #: Keys of the one-line directives (``<comment>
tvm-ffi-stubgen(<subject>): <target>(...)``)
+ #: this generator reads, per subject; merged with
:data:`consts.PIPELINE_DIRECTIVE_KEYS`.
+ #: A subject or key in neither table is an error.
+ directive_keys: KeyTable
def default_ty_map(self) -> dict[str, str]:
"""Return the default FFI-origin -> target-type name map for this
language."""
@@ -88,16 +89,11 @@ class Generator(Protocol):
# --- import collection (representation is generator-private) ------------
- def new_imports(self) -> Any:
- """Create a fresh, empty import collector for one file."""
- ...
-
- def add_directive(self, imports: Any, name: str, payload: str, lineno:
int) -> None:
- """Record a one-line directive (raw payload) into ``imports``.
+ def new_imports(self, directives: Directives) -> Any:
+ """Create the import collector for one file, seeded from the file's
``directives``.
- The collector is per file, so a directive applies to the blocks of the
- file it appears in. ``name`` is always one of :attr:`directive_kinds`;
- the payload's grammar is the generator's to define.
+ The collector is per file, so a directive applies to the blocks of the
file it
+ appears in.
"""
...
diff --git a/python/tvm_ffi/stub/python_generator/codegen.py
b/python/tvm_ffi/stub/python_generator/codegen.py
index 2669231d..40b9c53f 100644
--- a/python/tvm_ffi/stub/python_generator/codegen.py
+++ b/python/tvm_ffi/stub/python_generator/codegen.py
@@ -64,7 +64,7 @@ class {type_name}({parent_type_name}):
def _prompt_import_object(type_key: str, type_name: str, syntax:
C.MarkerSyntax) -> str:
- return f"""{syntax.import_object} {type_key};False;{type_name}\n"""
+ return f"""{syntax.directive("import")} {type_key}(alias =
{type_name})\n"""
def _prompt_import_section(syntax: C.MarkerSyntax) -> str:
@@ -105,7 +105,7 @@ def _type_suffix_and_record(
def _make_input_ty_map(ty_map: dict[str, str]) -> dict[str, str]:
- """Derive input-side defaults without overriding explicit ty-map
entries."""
+ """Derive input-side defaults without overriding explicit ty_map
entries."""
input_ty_map = ty_map.copy()
for key, input_default in PC.TY_MAP_INPUT_DEFAULTS.items():
output_default = PC.TY_MAP_DEFAULTS.get(key)
diff --git a/python/tvm_ffi/stub/python_generator/consts.py
b/python/tvm_ffi/stub/python_generator/consts.py
index a2bee13d..8dfbf68f 100644
--- a/python/tvm_ffi/stub/python_generator/consts.py
+++ b/python/tvm_ffi/stub/python_generator/consts.py
@@ -24,6 +24,13 @@ typing assumptions.
from __future__ import annotations
+from ..directives import Key, KeyTable
+
+#: ``import: a.b.C(type_checking_only, alias = _C)`` keeps ``a.b.C`` in the
import section.
+PYTHON_DIRECTIVE_KEYS: KeyTable = {
+ "import": {"type_checking_only": Key(), "alias": Key(takes_value=True)},
+}
+
#: Default FFI-origin -> Python-type name map used to seed a render.
TY_MAP_DEFAULTS = {
"Any": "typing.Any",
diff --git a/python/tvm_ffi/stub/python_generator/generator.py
b/python/tvm_ffi/stub/python_generator/generator.py
index 40a47bfa..231eb5ac 100644
--- a/python/tvm_ffi/stub/python_generator/generator.py
+++ b/python/tvm_ffi/stub/python_generator/generator.py
@@ -35,6 +35,7 @@ if TYPE_CHECKING:
from collections.abc import Mapping
from pathlib import Path
+ from ..directives import Directives
from ..file_utils import CodeBlock
from ..utils import FuncInfo, InitConfig, ObjectInfo, Options
@@ -45,7 +46,7 @@ class PythonGenerator:
name = "python"
syntax = C.PYTHON_SYNTAX
source_exts = frozenset({".py", ".pyi"})
- directive_kinds: frozenset[str] = frozenset({"import-object"})
+ directive_keys = PC.PYTHON_DIRECTIVE_KEYS
def default_ty_map(self) -> dict[str, str]:
"""Return the default FFI-origin -> Python-type name map."""
@@ -53,19 +54,16 @@ class PythonGenerator:
# --- import collection (Python representation is private) ---------------
- def new_imports(self) -> PythonImports:
- """Create an empty import collector."""
- return PythonImports()
-
- def add_directive(self, imports: PythonImports, name: str, payload: str,
lineno: int) -> None:
- """Record an ``import-object`` directive
(``<full_name>;<type_checking_only>;<alias>``)."""
- assert name == "import-object", name
- parts = [part.strip() for part in payload.split(";")]
- full_name, type_checking_only, alias = parts + [""] * (3 - len(parts))
- tco = type_checking_only.lower() == "true"
- imports.items.append(ImportItem(full_name, type_checking_only=tco,
alias=alias or None))
- if alias == "_FFI_LOAD_LIB" or
full_name.endswith("libinfo.load_lib_module"):
- imports.has_lib_load = True
+ def new_imports(self, directives: Directives) -> PythonImports:
+ """Create an import collector holding the file's ``import``
directives."""
+ imports = PythonImports()
+ for full_name, keys in directives.settings["import"].items():
+ alias = str(keys["alias"]) if "alias" in keys else None
+ tco = "type_checking_only" in keys
+ imports.items.append(ImportItem(full_name, type_checking_only=tco,
alias=alias))
+ if alias == "_FFI_LOAD_LIB" or
full_name.endswith("libinfo.load_lib_module"):
+ imports.has_lib_load = True
+ return imports
def canonical_type_name(self, type_key: str) -> str:
"""Return the canonical (import-comparable) full name for a defined
type key."""
diff --git a/python/tvm_ffi/stub/python_generator/utils.py
b/python/tvm_ffi/stub/python_generator/utils.py
index e4d6dfc9..bfecc02d 100644
--- a/python/tvm_ffi/stub/python_generator/utils.py
+++ b/python/tvm_ffi/stub/python_generator/utils.py
@@ -89,7 +89,7 @@ class PythonImports:
"""Opaque import collector threaded through the Python generation pipeline.
The language-agnostic ``cli`` treats this as an opaque token: it asks the
- generator to create one, seed it from ``import-object`` directives, and
later
+ generator to create one, seed it from ``import`` directives, and later
render it. Only the Python generator reaches inside.
"""
diff --git a/python/tvm_ffi/stub/rust_generator/codegen.py
b/python/tvm_ffi/stub/rust_generator/codegen.py
index 708dae3e..858650e8 100644
--- a/python/tvm_ffi/stub/rust_generator/codegen.py
+++ b/python/tvm_ffi/stub/rust_generator/codegen.py
@@ -25,22 +25,22 @@ the verdict of :mod:`tvm_ffi.stub.layout`:
width, pinned by a ``const`` size/alignment assertion. ``<Leaf>Obj::new``
and the wrapper's ``new`` take every field root to leaf: the former each as
its own type, the latter a scalar as its own type, an object reference as
- ``impl IntoArg`` and anything else as ``impl Into``. ``custom-new``
+ ``impl IntoArg`` and anything else as ``impl Into``. ``custom_new``
leaves the wrapper's ``new`` to hand-written code and names the generated
one ``from_fields``. A type that is not final
can be the parent of a type defined in Rust (``tvm_ffi::rs_object``): its
``<Leaf>Obj::new`` is public, and its object derives ``ShallowCopy`` and
``UnsafeInit`` (``rs_parent``) when every ancestor's is generated
- too (a ``ty-map``'d one is hand-written and may not). ``<Leaf>Obj::new`` of
a final type is crate-private.
+ too (a ``ty_map``'d one is hand-written and may not). ``<Leaf>Obj::new`` of
a final type is crate-private.
- *opaque*: the struct embeds only its parent, and each field is read through
the C ABI getter. Nothing allocates it.
A field without a Rust mirror (``Optional<Any>``, ``Union``, ``void*``, an
-``ffi.*`` type the default ``ty-map`` does not list, a C++ standard-library
type
+``ffi.*`` type the default ``ty_map`` does not list, a C++ standard-library
type
such as ``std::optional``, an integer narrower than 8 bytes, whose signedness
the
schema does not record, or any field whose reflected size or alignment is not
its
-mirror's) makes the type opaque and is read through its getter; a ``field`` or
-``enum`` directive types a narrow integer; an ``opaque`` directive vetoes a
+mirror's) makes the type opaque and is read through its getter; a field's
``type``
+(a scalar or an ``enum``) types a narrow integer; an object's ``opaque``
vetoes a
reproducible layout; a scalar width named by a directive is checked against the
reflected field size.
A builtin parent the crate defines (``ffi.Enum``:
``tvm_ffi::enum_ref::EnumObj``) is
@@ -52,7 +52,7 @@ An enum type (under ``ffi.Enum``) has no allocator, since its
registry
allocates the values, and its reference implements ``tvm_ffi::EnumRef``.
Every type key an object refers to (parent, ancestors, field types) must be
-provided in the same run: ``ffi.*`` by the crate, a ``ty-map`` by a
hand-written
+provided in the same run: ``ffi.*`` by the crate, a ``ty_map`` by a
hand-written
binding whose object struct is ``<Name>Obj``, anything else by an ``object/``
block in one of the processed files. Otherwise the block is an error naming the
missing keys, so a partial binding never references a module that does not
exist.
@@ -165,7 +165,7 @@ def _is_object_ref(schema: TypeSchema) -> bool:
"""Whether the Rust type of ``schema`` is an object reference.
A builtin origin is one when listed in ``RUST_OBJECT_REF_ORIGINS``; a type
key
- outside ``ffi.*`` always is, as a generated or ``ty-map``'d binding.
+ outside ``ffi.*`` always is, as a generated or ``ty_map``'d binding.
"""
origin = schema.origin
if origin in C_RUST.RUST_OBJECT_REF_ORIGINS:
@@ -258,7 +258,7 @@ class _ObjectRenderer:
) -> str | None:
"""Resolve a leaf origin to its in-scope Rust name (recording its
``use``), or ``None``.
- A ``ty-map`` entry for the type key the schema was parsed from wins
over the origin.
+ A ``ty_map`` entry for the type key the schema was parsed from wins
over the origin.
"""
key = type_key if type_key in self.ty_map else origin
mapped = self.ty_map.get(key)
@@ -266,12 +266,12 @@ class _ObjectRenderer:
if "." not in origin or origin.startswith("ctypes."):
return None
if self._provider(origin) == "crate": # a key nobody provides is
recorded
- return None # an `ffi.*` key off the default `ty-map`: no
crate type, read as `Any`
+ return None # an `ffi.*` key off the default `ty_map`: no
crate type, read as `Any`
return imports.record(self._generated_type_path(origin))
return self._record_mapped(key, mapped, imports)
def _record_mapped(self, type_key: str, path: str, imports: RustImports)
-> str:
- """Record the ``ty-map`` target of ``type_key``; one in this file's
own module is in scope.
+ """Record the ``ty_map`` target of ``type_key``; one in this file's
own module is in scope.
A hand-written binding next to the generated ones (``ir.Op ->
crate::ir::Op``
in ``crate::ir``) must not import itself.
@@ -303,7 +303,7 @@ class _ObjectRenderer:
def _provider(self, type_key: str) -> str | None:
"""Who provides the binding of ``type_key``; a key nobody does is
recorded in :attr:`missing`.
- ``"crate"`` for builtin ``ffi.*`` types, ``"mapped"`` for a ``ty-map``
to a
+ ``"crate"`` for builtin ``ffi.*`` types, ``"mapped"`` for a ``ty_map``
to a
hand-written binding, ``"generated"`` for an ``object/`` block in this
run.
"""
if is_crate_type_key(type_key):
@@ -316,7 +316,7 @@ class _ObjectRenderer:
return None
def _base_type(self) -> tuple[str, bool]:
- """Resolve the ``base_`` struct and whether it is a generated or
``ty-map``'d parent.
+ """Resolve the ``base_`` struct and whether it is a generated or
``ty_map``'d parent.
A builtin parent below ``ffi.Object`` is embedded as the crate's object
struct, or as its header-only stand-in over the deepest builtin
ancestor the
@@ -441,7 +441,7 @@ class _ObjectRenderer:
# --- pieces ------------------------------------------------------------
def _allow_missing_docs(self) -> list[str]:
- """``#[allow(missing_docs)]`` under ``missing-docs: allow``, for a
crate that warns on it."""
+ """``#[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 _visibility(self, target: str) -> str:
@@ -566,7 +566,7 @@ class _ObjectRenderer:
"""``impl_object_ref_upcast!`` to every ancestor's wrapper, then the
``upcast`` directives.
A generated ancestor accepts every object of its type, so its upcast
takes the
- compile-time checked form; a ``ty-map``'d one or an ``upcast`` view
may promise
+ compile-time checked form; a ``ty_map``'d one or an ``upcast`` view
may promise
more, so it takes the ``unsafe`` form.
"""
checked: list[str] = []
@@ -727,7 +727,7 @@ class _ObjectRenderer:
field as its own type, as ``#[rs_object]``'s ``new`` does, an object
reference as
``impl IntoArg`` and any other as ``impl Into`` (:func:`_param_type`),
and
converts it on the way to ``<Leaf>Obj::new``.
- ``custom-new`` names the wrapper's allocator ``from_fields`` instead;
``no-new``
+ ``custom_new`` names the wrapper's allocator ``from_fields`` instead;
``no_new``
skips it, and ``<Leaf>Obj::new`` of a final type with it.
``<Leaf>Obj::new`` of a type that is not final is public: a subtype
defined in Rust
builds its parent's object with it.
@@ -814,7 +814,7 @@ class _ObjectRenderer:
A subtype defined in Rust needs both of its parent's object. They are
derived
for a complete type that is not final and whose ancestors below
``ffi.Object``
are all generated: each of those derives them by this rule, while a
- ``ty-map``'d one is hand-written and may not.
+ ``ty_map``'d one is hand-written and may not.
"""
if self.info.is_final or not verdict.is_complete:
return False
@@ -920,7 +920,7 @@ def generate_rust_object(
keys = ", ".join(f"`{key}`" for key in sorted(renderer.missing))
raise ValueError(
f"`{obj_info.type_key}` (line {code.lineno_start}) depends on
{keys}, which this run "
- "does not ask for: add an `object/<key>` block or a `ty-map`
directive for each"
+ "does not ask for: add an `object/<key>` block or a `ty_map`
directive key for each"
)
indent = " " * code.indent
code.lines = [
diff --git a/python/tvm_ffi/stub/rust_generator/consts.py
b/python/tvm_ffi/stub/rust_generator/consts.py
index 01e1d521..091f4377 100644
--- a/python/tvm_ffi/stub/rust_generator/consts.py
+++ b/python/tvm_ffi/stub/rust_generator/consts.py
@@ -18,22 +18,6 @@
from __future__ import annotations
-#: One-line directives the Rust backend consumes.
-RUST_DIRECTIVE_KINDS = frozenset(
- {
- "import-object",
- "field",
- "nullable",
- "visibility",
- "enum",
- "opaque",
- "upcast",
- "custom-new",
- "no-new",
- "missing-docs",
- }
-)
-
#: Default FFI-origin -> Rust-type map; ``::`` paths get a ``use``, bare names
do not.
RUST_TY_MAP_DEFAULTS = {
"int": "i64",
@@ -120,7 +104,7 @@ RUST_EXACT_PARAMS = frozenset(
)
#: Builtin origins whose Rust type is an object reference, taken as ``impl
IntoArg<T>``.
-#: A type key outside ``ffi.*`` is one too: a generated or ``ty-map``'d
binding. ``Shape``,
+#: A type key outside ``ffi.*`` is one too: a generated or ``ty_map``'d
binding. ``Shape``,
#: like ``Array`` / ``List`` / ``Map``, is a container taken as ``impl
Into<T>``, which also
#: accepts what it is built from (``From<impl AsRef<[i64]>>``).
RUST_OBJECT_REF_ORIGINS = frozenset(
diff --git a/python/tvm_ffi/stub/rust_generator/directives.py
b/python/tvm_ffi/stub/rust_generator/directives.py
index 4ca394a4..0065c511 100644
--- a/python/tvm_ffi/stub/rust_generator/directives.py
+++ b/python/tvm_ffi/stub/rust_generator/directives.py
@@ -14,46 +14,63 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
-"""The Rust backend's one-line directives: payload grammar and per-file
storage.
-
-Four 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
- // tvm-ffi-stubgen(visibility): ir.IRModule.global_var_map_ -> pub(crate)
- // tvm-ffi-stubgen(enum): tirx.For.kind -> ForKind(i32) { Serial=0,
Parallel=1 }
- // tvm-ffi-stubgen(opaque): ir.SourceName
- // tvm-ffi-stubgen(upcast): tirx.Add -> PrimExpr
- // tvm-ffi-stubgen(custom-new): tirx.Add
- // tvm-ffi-stubgen(no-new): ir.Expr
- // 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``; ``visibility`` makes
the
-field and its accessor ``pub(crate)`` instead of ``pub``; ``enum`` declares an
open
-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. ``no-new``
-skips the wrapper's generated allocator, and for a final type
``<Leaf>Obj::new``
-too, which only that allocator uses.
-``missing-docs: allow`` puts ``#[allow(missing_docs)]`` on the file's generated
-types and ``impl`` blocks.
+"""The Rust backend's directive keys, and the settings they give one file.
+
+Each subject's keys (see :mod:`tvm_ffi.stub.directives` for the grammar)::
+
+ // tvm-ffi-stubgen(object): tirx.Add(custom_new, upcast = PrimExpr)
+ // tvm-ffi-stubgen(object): ir.SourceName(opaque)
+ // tvm-ffi-stubgen(object): ir.Expr(no_new)
+ // tvm-ffi-stubgen(field): tirx.Add.a(type = PrimExpr)
+ // tvm-ffi-stubgen(field): ir.Expr.span(nullable)
+ // tvm-ffi-stubgen(field): ir.IRModule.global_var_map_(visibility =
pub(crate))
+ // tvm-ffi-stubgen(enum): ForKind(repr = i32, Serial = 0, Parallel = 1)
+ // tvm-ffi-stubgen(field): tirx.For.kind(type = ForKind)
+ // tvm-ffi-stubgen(file): mod(missing_docs = allow)
+ // tvm-ffi-stubgen(import): tvm_ffi::AnyView
+
+``object`` keys: ``opaque`` keeps a type opaque even when its layout is
+reproducible; ``upcast`` (repeatable) adds an upcast to a reference outside the
+ancestor chain; ``custom_new`` says the wrapper's ``new`` is hand-written, so
the
+generated one is named ``from_fields``; ``no_new`` skips the wrapper's
generated
+allocator, and for a final type ``<Leaf>Obj::new`` too, which only that
allocator
+uses. ``field`` keys: ``type`` sets the field's Rust type (a name in scope, a
+``::`` path to ``use``, or an ``enum`` of the file); on a field inherited from
an
+ancestor it narrows the allocator parameter instead. ``nullable`` wraps it in
+``Option``; ``visibility = pub(crate)`` makes the field and its accessor
+``pub(crate)`` instead of ``pub``. ``enum`` declares an open integer newtype of
+``repr`` with the members ``Name = <int>``, or with ``closed`` a
``#[repr(<int>)]``
+enum of exactly those members. ``file: mod(missing_docs = allow)`` puts
+``#[allow(missing_docs)]`` on the file's generated types and ``impl`` blocks.
+``import`` keeps a path ``use``d in the import section.
"""
from __future__ import annotations
import dataclasses
-import re
+from typing import TYPE_CHECKING
-_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<closed>closed\s*)?\{(?P<body>[^{}]*)\})?$"
-)
-_MEMBER_RE = re.compile(r"^(?P<name>[A-Za-z_]\w*)\s*=\s*(?P<value>-?\d+)$")
+from ..directives import Key, KeyTable
+
+if TYPE_CHECKING:
+ from ..directives import Directives
+
+_FLAG, _VALUE = Key(), Key(takes_value=True)
+_INT_TYPES = ("i8", "i16", "i32", "i64", "u8", "u16", "u32", "u64")
+
+#: The keys the Rust backend reads; an ``enum``'s other keys are its members.
+RUST_DIRECTIVE_KEYS: KeyTable = {
+ "object": {
+ "custom_new": _FLAG,
+ "no_new": _FLAG,
+ "opaque": _FLAG,
+ "upcast": Key(takes_value=True, repeatable=True),
+ },
+ "field": {"type": _VALUE, "nullable": _FLAG, "visibility": Key(True,
values=("pub(crate)",))},
+ "enum": {"repr": Key(True, values=_INT_TYPES), "closed": _FLAG, "*":
_VALUE},
+ "file": {"missing_docs": Key(True, values=("allow",))},
+ "import": {},
+}
@dataclasses.dataclass(frozen=True)
@@ -72,8 +89,8 @@ class EnumSpec:
@dataclasses.dataclass
-class Directives:
- """The Rust directives of one file, keyed by ``<type_key>.<field>`` or
``<type_key>``."""
+class RustDirectives:
+ """The Rust settings 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)
@@ -85,91 +102,49 @@ class Directives:
no_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."""
- if name == "field":
- 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 == "visibility":
- lhs, vis = _split_arrow(name, payload, lineno, "<type_key>.<field>
-> pub(crate)")
- if vis != "pub(crate)": # the one non-default level so far
- raise _invalid(name, lineno, "<type_key>.<field> ->
pub(crate)")
- self.visibility[_field_target(name, lhs, lineno)] = vis
- 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))
- elif name == "no-new":
- self.no_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}")
-
-
-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()
- if not target or " " in target or "." not in target.strip("."):
- raise _invalid(name, lineno, "<type_key>.<field>")
- return target
-
-
-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, expected)
- return lhs, rhs.strip()
-
-
-def _parse_enum(payload: str, lineno: int) -> tuple[str, EnumSpec]:
- """Parse ``<type_key>.<field> -> Name(i32) [closed] { A=0, B=1 }``.
-
- The member list is optional unless ``closed``, which needs a member equal
to 0.
- """
- 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)
- members: list[tuple[str, int]] = []
- for item in (match.group("body") or "").split(","):
- if not item.strip():
- continue
- member = _MEMBER_RE.match(item.strip())
- 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)
- if closed and all(value != 0 for _, value in members):
+ @staticmethod
+ def from_directives(directives: Directives) -> RustDirectives:
+ """Collect the settings of ``directives``; raise ``ValueError`` on a
malformed ``enum``."""
+ settings, out = directives.settings, RustDirectives()
+ specs = {name: _enum_spec(name, keys) for name, keys in
settings["enum"].items()}
+ for target, keys in settings["field"].items():
+ rust_type = str(keys.get("type", ""))
+ if rust_type in specs:
+ out.enums[target] = specs[rust_type]
+ elif rust_type:
+ out.field_types[target] = rust_type
+ if "nullable" in keys:
+ out.nullable.add(target)
+ if "visibility" in keys:
+ out.visibility[target] = str(keys["visibility"])
+ for target, keys in settings["object"].items():
+ for name, chosen in (
+ ("opaque", out.opaque),
+ ("custom_new", out.custom_new),
+ ("no_new", out.no_new),
+ ):
+ if name in keys:
+ chosen.add(target)
+ upcasts = keys.get("upcast", [])
+ if isinstance(upcasts, list) and upcasts:
+ out.upcasts[target] = upcasts
+ out.allow_missing_docs = "missing_docs" in settings["file"].get("mod",
{})
+ return out
+
+
+def _enum_spec(name: str, keys: dict[str, bool | str | list[str]]) -> EnumSpec:
+ """Build the ``enum`` ``name``: a ``repr``, members ``Name = <int>``, and
``closed``."""
+ members = tuple((k, v) for k, v in keys.items() if k not in ("repr",
"closed"))
+ if "repr" not in keys:
+ raise ValueError(f"`enum` directive `{name}` needs `repr =
<{'|'.join(_INT_TYPES)}>`")
+ try:
+ values = tuple((member, int(str(value))) for member, value in members)
+ except ValueError:
+ raise ValueError(f"`enum` directive `{name}`: a member value is not an
integer") from None
+ closed = "closed" in keys
+ if closed and all(value != 0 for _, value in values):
raise ValueError(
- f"Closed `enum` directive at line {lineno} has no member equal to
0, the value of "
+ f"Closed `enum` directive `{name}` has no member equal to 0, the
value of "
"a field an object zero-filled at creation never set"
)
- target = _field_target("enum", match.group("target"), lineno)
- spec = EnumSpec(match.group("name"), match.group("repr"), tuple(members),
closed)
- return target, spec
+ return EnumSpec(name, str(keys["repr"]), values, closed)
diff --git a/python/tvm_ffi/stub/rust_generator/generator.py
b/python/tvm_ffi/stub/rust_generator/generator.py
index c7d0a92c..290f15a1 100644
--- a/python/tvm_ffi/stub/rust_generator/generator.py
+++ b/python/tvm_ffi/stub/rust_generator/generator.py
@@ -33,11 +33,13 @@ from .codegen import (
generate_rust_import_section,
generate_rust_object,
)
+from .directives import RUST_DIRECTIVE_KEYS, RustDirectives
from .utils import RustImports, RustUse, check_tokens, is_crate_type_key,
rust_module_path
if TYPE_CHECKING:
from pathlib import Path
+ from ..directives import Directives
from ..file_utils import CodeBlock
from ..utils import FuncInfo, InitConfig, ObjectInfo, Options
@@ -67,7 +69,7 @@ class RustGenerator:
name = "rust"
syntax = C.RUST_SYNTAX
source_exts = frozenset({".rs"})
- directive_kinds = C_RUST.RUST_DIRECTIVE_KINDS
+ directive_keys = RUST_DIRECTIVE_KEYS
def default_ty_map(self) -> dict[str, str]:
"""Return the default FFI-origin -> Rust-type name map."""
@@ -75,16 +77,12 @@ class RustGenerator:
# --- per-file collection
--------------------------------------------------
- def new_imports(self) -> RustImports:
- """Create an empty per-file collector (``use`` items and
directives)."""
- return RustImports()
-
- def add_directive(self, imports: RustImports, name: str, payload: str,
lineno: int) -> None:
- """Record ``import-object`` as a ``use`` (its path is the first ``;``
field); parse the rest."""
- if name == "import-object":
- imports.record(payload.split(";", 1)[0].strip())
- else:
- imports.directives.add(name, payload, lineno)
+ def new_imports(self, directives: Directives) -> RustImports:
+ """Create the per-file collector: the ``import`` paths as ``use``
items, and the settings."""
+ imports =
RustImports(directives=RustDirectives.from_directives(directives))
+ for path in directives.settings["import"]:
+ imports.record(path)
+ return imports
def canonical_type_name(self, type_key: str) -> str:
"""Return the Rust path for a defined type key (matches
:attr:`RustUse.path`)."""
diff --git a/python/tvm_ffi/stub/rust_generator/utils.py
b/python/tvm_ffi/stub/rust_generator/utils.py
index 7a5e0667..ec36bf90 100644
--- a/python/tvm_ffi/stub/rust_generator/utils.py
+++ b/python/tvm_ffi/stub/rust_generator/utils.py
@@ -25,7 +25,7 @@ from pathlib import Path
from typing import TYPE_CHECKING, Callable
from . import consts as C
-from .directives import Directives
+from .directives import RustDirectives
if TYPE_CHECKING:
from collections.abc import Iterable
@@ -105,7 +105,7 @@ class RustImports:
"""The per-file collector: the ``use`` items and the Rust directives of
one file."""
items: list[RustUse] = dataclasses.field(default_factory=list)
- directives: Directives = dataclasses.field(default_factory=Directives)
+ directives: RustDirectives =
dataclasses.field(default_factory=RustDirectives)
builtin_mirrors: dict[str, str] = dataclasses.field(default_factory=dict)
"""Builtin ancestors mirrored in this file, root first: type key -> the
base each embeds."""
reserved: set[str] = dataclasses.field(default_factory=set)
diff --git a/python/tvm_ffi/testing/testing.py
b/python/tvm_ffi/testing/testing.py
index b4797d67..eb847a04 100644
--- a/python/tvm_ffi/testing/testing.py
+++ b/python/tvm_ffi/testing/testing.py
@@ -161,7 +161,7 @@ class TestNonCopyable(Object):
@c_class("testing.SchemaAllTypes")
class _SchemaAllTypes:
- # tvm-ffi-stubgen(ty-map): testing.SchemaAllTypes ->
testing._SchemaAllTypes
+ # tvm-ffi-stubgen(object): testing.SchemaAllTypes(ty_map =
testing._SchemaAllTypes)
# tvm-ffi-stubgen(begin): object/testing.SchemaAllTypes
# fmt: off
v_bool: bool
@@ -317,7 +317,7 @@ class TestEqWithoutHash(Object):
@c_class("testing.TestCxxClassBase")
class _TestCxxClassBase(Object):
- # tvm-ffi-stubgen(ty-map): testing.TestCxxClassBase ->
testing._TestCxxClassBase
+ # tvm-ffi-stubgen(object): testing.TestCxxClassBase(ty_map =
testing._TestCxxClassBase)
# tvm-ffi-stubgen(begin): object/testing.TestCxxClassBase
# fmt: off
v_i64: int
@@ -338,7 +338,7 @@ class _TestCxxClassBase(Object):
@c_class("testing.TestCxxClassDerived", eq=True, order=True, unsafe_hash=True)
class _TestCxxClassDerived(_TestCxxClassBase):
- # tvm-ffi-stubgen(ty-map): testing.TestCxxClassDerived ->
testing._TestCxxClassDerived
+ # tvm-ffi-stubgen(object): testing.TestCxxClassDerived(ty_map =
testing._TestCxxClassDerived)
# tvm-ffi-stubgen(begin): object/testing.TestCxxClassDerived
# fmt: off
v_f64: float
@@ -352,7 +352,7 @@ class _TestCxxClassDerived(_TestCxxClassBase):
@c_class("testing.TestCxxClassDerivedDerived")
class _TestCxxClassDerivedDerived(_TestCxxClassDerived):
- # tvm-ffi-stubgen(ty-map): testing.TestCxxClassDerivedDerived ->
testing._TestCxxClassDerivedDerived
+ # tvm-ffi-stubgen(object): testing.TestCxxClassDerivedDerived(ty_map =
testing._TestCxxClassDerivedDerived)
# tvm-ffi-stubgen(begin): object/testing.TestCxxClassDerivedDerived
# fmt: off
v_str: str
@@ -396,7 +396,7 @@ class _TestCxxEnumHolder(Object):
@c_class("testing.TestCxxInitSubset")
class _TestCxxInitSubset(Object):
- # tvm-ffi-stubgen(ty-map): testing.TestCxxInitSubset ->
testing._TestCxxInitSubset
+ # tvm-ffi-stubgen(object): testing.TestCxxInitSubset(ty_map =
testing._TestCxxInitSubset)
# tvm-ffi-stubgen(begin): object/testing.TestCxxInitSubset
# fmt: off
required_field: int
@@ -411,7 +411,7 @@ class _TestCxxInitSubset(Object):
@c_class("testing.TestCxxKwOnly")
class _TestCxxKwOnly(Object):
- # tvm-ffi-stubgen(ty-map): testing.TestCxxKwOnly -> testing._TestCxxKwOnly
+ # tvm-ffi-stubgen(object): testing.TestCxxKwOnly(ty_map =
testing._TestCxxKwOnly)
# tvm-ffi-stubgen(begin): object/testing.TestCxxKwOnly
# fmt: off
x: int
@@ -431,7 +431,7 @@ class _TestCxxAutoInit(Object):
__test__ = False
- # tvm-ffi-stubgen(ty-map): testing.TestCxxAutoInit ->
testing._TestCxxAutoInit
+ # tvm-ffi-stubgen(object): testing.TestCxxAutoInit(ty_map =
testing._TestCxxAutoInit)
# tvm-ffi-stubgen(begin): object/testing.TestCxxAutoInit
# fmt: off
a: int
@@ -451,7 +451,7 @@ class _TestCxxAutoInitSimple(Object):
__test__ = False
- # tvm-ffi-stubgen(ty-map): testing.TestCxxAutoInitSimple ->
testing._TestCxxAutoInitSimple
+ # tvm-ffi-stubgen(object): testing.TestCxxAutoInitSimple(ty_map =
testing._TestCxxAutoInitSimple)
# tvm-ffi-stubgen(begin): object/testing.TestCxxAutoInitSimple
# fmt: off
x: int
@@ -469,7 +469,7 @@ class _TestCxxAutoInitAllInitOff(Object):
__test__ = False
- # tvm-ffi-stubgen(ty-map): testing.TestCxxAutoInitAllInitOff ->
testing._TestCxxAutoInitAllInitOff
+ # tvm-ffi-stubgen(object): testing.TestCxxAutoInitAllInitOff(ty_map =
testing._TestCxxAutoInitAllInitOff)
# tvm-ffi-stubgen(begin): object/testing.TestCxxAutoInitAllInitOff
# fmt: off
x: int
@@ -488,7 +488,7 @@ class _TestCxxAutoInitKwOnlyDefaults(Object):
__test__ = False
- # tvm-ffi-stubgen(ty-map): testing.TestCxxAutoInitKwOnlyDefaults ->
testing._TestCxxAutoInitKwOnlyDefaults
+ # tvm-ffi-stubgen(object): testing.TestCxxAutoInitKwOnlyDefaults(ty_map =
testing._TestCxxAutoInitKwOnlyDefaults)
# tvm-ffi-stubgen(begin): object/testing.TestCxxAutoInitKwOnlyDefaults
# fmt: off
p_required: int
@@ -509,7 +509,7 @@ class _TestCxxNoAutoInit(Object):
__test__ = False
- # tvm-ffi-stubgen(ty-map): testing.TestCxxNoAutoInit ->
testing._TestCxxNoAutoInit
+ # tvm-ffi-stubgen(object): testing.TestCxxNoAutoInit(ty_map =
testing._TestCxxNoAutoInit)
# tvm-ffi-stubgen(begin): object/testing.TestCxxNoAutoInit
# fmt: off
x: int
@@ -524,7 +524,7 @@ class _TestCxxAutoInitParent(Object):
__test__ = False
- # tvm-ffi-stubgen(ty-map): testing.TestCxxAutoInitParent ->
testing._TestCxxAutoInitParent
+ # tvm-ffi-stubgen(object): testing.TestCxxAutoInitParent(ty_map =
testing._TestCxxAutoInitParent)
# tvm-ffi-stubgen(begin): object/testing.TestCxxAutoInitParent
# fmt: off
parent_required: int
@@ -542,7 +542,7 @@ class _TestCxxAutoInitChild(_TestCxxAutoInitParent):
__test__ = False
- # tvm-ffi-stubgen(ty-map): testing.TestCxxAutoInitChild ->
testing._TestCxxAutoInitChild
+ # tvm-ffi-stubgen(object): testing.TestCxxAutoInitChild(ty_map =
testing._TestCxxAutoInitChild)
# tvm-ffi-stubgen(begin): object/testing.TestCxxAutoInitChild
# fmt: off
child_required: int
diff --git a/tests/python/test_stubgen.py b/tests/python/test_stubgen.py
index 0de76e91..aba0aeee 100644
--- a/tests/python/test_stubgen.py
+++ b/tests/python/test_stubgen.py
@@ -27,6 +27,7 @@ from tvm_ffi.core import MISSING, TypeSchema,
_lookup_or_register_type_info_from
from tvm_ffi.dataclasses import py_class
from tvm_ffi.stub import consts as C
from tvm_ffi.stub.cli import _stage_1, _stage_2, _stage_3
+from tvm_ffi.stub.directives import Directives, Key
from tvm_ffi.stub.file_utils import CodeBlock, FileInfo, collect_files,
syntax_for
from tvm_ffi.stub.generator import generator_names, get_generator
from tvm_ffi.stub.python_generator import PythonGenerator
@@ -45,7 +46,7 @@ from tvm_ffi.stub.python_generator.codegen import (
render_object_init,
render_object_methods,
)
-from tvm_ffi.stub.python_generator.utils import ImportItem
+from tvm_ffi.stub.python_generator.utils import ImportItem, PythonImports
from tvm_ffi.stub.utils import (
FuncInfo,
InitConfig,
@@ -101,10 +102,10 @@ def test_codeblock_from_begin_line_variants() -> None:
def test_codeblock_from_begin_line_ty_map_and_unknown() -> None:
- line = f"{C.PYTHON_SYNTAX.ty_map} custom -> mapped"
+ line = f"{C.PYTHON_SYNTAX.directive('object')} custom(ty_map = mapped)"
block = CodeBlock.from_begin_line(5, line, C.PYTHON_SYNTAX)
assert block.kind == "directive"
- assert block.param == ("ty-map", "custom -> mapped")
+ assert block.param == ("object", "custom(ty_map = mapped)")
assert block.lineno_start == 5
assert block.lineno_end == 5
@@ -129,7 +130,7 @@ def test_fileinfo_from_file_parses_blocks(tmp_path: Path)
-> None:
f"{C.PYTHON_SYNTAX.begin} global/demo.func",
"in_stub = True",
C.PYTHON_SYNTAX.end,
- f"{C.PYTHON_SYNTAX.ty_map} x -> y",
+ f"{C.PYTHON_SYNTAX.directive('object')} x(ty_map = y)",
]
)
path = tmp_path / "demo.py"
@@ -154,9 +155,9 @@ def test_fileinfo_from_file_parses_blocks(tmp_path: Path)
-> None:
]
assert ty_map.kind == "directive"
- assert ty_map.param == ("ty-map", "x -> y")
+ assert ty_map.param == ("object", "x(ty_map = y)")
assert ty_map.lineno_start == ty_map.lineno_end == 5
- assert ty_map.lines == [f"{C.PYTHON_SYNTAX.ty_map} x -> y"]
+ assert ty_map.lines == [f"{C.PYTHON_SYNTAX.directive('object')} x(ty_map =
y)"]
def test_fileinfo_from_file_error_paths(tmp_path: Path) -> None:
@@ -835,11 +836,12 @@ def
test_stage_3_adds_LIB_when_load_lib_imported(tmp_path: Path) -> None:
)
import_obj_block = CodeBlock(
kind="directive",
- param=("import-object",
"tvm_ffi.libinfo.load_lib_module;False;_FFI_LOAD_LIB"),
+ param=("import", "tvm_ffi.libinfo.load_lib_module(alias =
_FFI_LOAD_LIB)"),
lineno_start=1,
lineno_end=1,
lines=[
- f"{C.PYTHON_SYNTAX.import_object}
tvm_ffi.libinfo.load_lib_module;False;_FFI_LOAD_LIB"
+ f"{C.PYTHON_SYNTAX.directive('import')} "
+ "tvm_ffi.libinfo.load_lib_module(alias = _FFI_LOAD_LIB)"
],
)
all_block = CodeBlock(
@@ -945,11 +947,12 @@ def test_generate_ffi_api_with_objects_imports_parents()
-> None:
is_root=False,
syntax=C.PYTHON_SYNTAX,
)
- assert C.PYTHON_SYNTAX.import_object in code # register_object prompt
+ assert C.PYTHON_SYNTAX.directive("import") in code # register_object
prompt
assert f"{C.PYTHON_SYNTAX.begin} object/{obj_info.type_key}" in code
assert parent_key is not None
parent_import_prompt = (
- f"{C.PYTHON_SYNTAX.import_object}
{parent_key};False;_{parent_key.replace('.', '_')}"
+ f"{C.PYTHON_SYNTAX.directive('import')} {parent_key}"
+ f"(alias = _{parent_key.replace('.', '_')})"
)
assert parent_import_prompt in code
@@ -1095,7 +1098,7 @@ def test_rust_marker_syntax_parses_rs_file(tmp_path:
Path) -> None:
f"{C.RUST_SYNTAX.begin} object/demo.Foo",
"pub struct Foo;",
C.RUST_SYNTAX.end,
- f"{C.RUST_SYNTAX.ty_map} a -> b",
+ f"{C.RUST_SYNTAX.directive('object')} a(ty_map = b)",
"",
]
),
@@ -1107,7 +1110,7 @@ def test_rust_marker_syntax_parses_rs_file(tmp_path:
Path) -> None:
assert info.syntax is C.RUST_SYNTAX
assert [block.kind for block in info.code_blocks] == ["object",
"directive"]
assert info.code_blocks[0].param == "demo.Foo"
- assert info.code_blocks[1].param == ("ty-map", "a -> b")
+ assert info.code_blocks[1].param == ("object", "a(ty_map = b)")
assert info.code_blocks[0].lines[1] == "pub struct Foo;"
# Python markers are plain text inside a Rust file.
@@ -1141,15 +1144,17 @@ def test_generator_registry_names() -> None:
def test_codeblock_from_begin_line_directive() -> None:
- """``<comment> tvm-ffi-stubgen(<name>): <payload>`` parses into a one-line
directive block."""
+ """``<comment> tvm-ffi-stubgen(<subject>): <payload>`` parses into a
one-line directive block."""
for syntax in (C.PYTHON_SYNTAX, C.RUST_SYNTAX):
- block = CodeBlock.from_begin_line(7, f"{syntax.directive('field')}
a.B.c -> D ", syntax)
+ block = CodeBlock.from_begin_line(
+ 7, f"{syntax.directive('field')} a.B.c(type = D) ", syntax
+ )
assert block.kind == "directive"
- assert block.param == ("field", "a.B.c -> D")
+ assert block.param == ("field", "a.B.c(type = D)")
assert (block.lineno_start, block.lineno_end) == (7, 7)
assert block.lines == []
- empty = CodeBlock.from_begin_line(1, C.PYTHON_SYNTAX.directive("opaque"),
C.PYTHON_SYNTAX)
- assert empty.param == ("opaque", "")
+ empty = CodeBlock.from_begin_line(1, C.PYTHON_SYNTAX.directive("object"),
C.PYTHON_SYNTAX)
+ assert empty.param == ("object", "")
# The `name):` shape is required; anything else stays an unknown marker.
for line in (f"{C.PYTHON_SYNTAX.prefix}field)",
f"{C.PYTHON_SYNTAX.prefix}two words): x"):
@@ -1161,7 +1166,7 @@ def
test_fileinfo_from_file_keeps_directive_lines(tmp_path: Path) -> None:
"""A directive is parsed as its own block and written back untouched."""
src = tmp_path / "mod.rs"
lines = [
- " // tvm-ffi-stubgen(nullable): ir.Expr.span",
+ " // tvm-ffi-stubgen(field): ir.Expr.span(nullable)",
f"{C.RUST_SYNTAX.begin} object/demo.Foo",
C.RUST_SYNTAX.end,
f"{C.RUST_SYNTAX.prefix}bogus)",
@@ -1174,28 +1179,28 @@ def
test_fileinfo_from_file_keeps_directive_lines(tmp_path: Path) -> None:
info = FileInfo.from_file(src)
assert info is not None
assert [block.kind for block in info.code_blocks] == ["directive",
"object"]
- assert info.code_blocks[0].param == ("nullable", "ir.Expr.span")
+ assert info.code_blocks[0].param == ("field", "ir.Expr.span(nullable)")
assert info.code_blocks[0].lines == [lines[0]]
assert info.update(verbose=False, dry_run=False) is False
assert src.read_text(encoding="utf-8") == "\n".join(lines[:3]) + "\n"
def test_stage_3_routes_directives_by_generator_declaration(tmp_path: Path) ->
None:
- """Pipeline-owned names stay in the pipeline; declared names reach the
generator."""
+ """Pipeline keys stay in the pipeline; declared subjects and keys reach
the generator."""
path = tmp_path / "demo.py"
ty_map_directive = CodeBlock(
kind="directive",
- param=("ty-map", "demo.Foo -> mapped.Foo"),
+ param=("object", "demo.Foo(ty_map = mapped.Foo)"),
lineno_start=1,
lineno_end=1,
- lines=[f"{C.PYTHON_SYNTAX.ty_map} demo.Foo -> mapped.Foo"],
+ lines=[f"{C.PYTHON_SYNTAX.directive('object')} demo.Foo(ty_map =
mapped.Foo)"],
)
directive = CodeBlock(
kind="directive",
- param=("field", "demo.Foo.x -> Bar"),
+ param=("field", "demo.Foo.x(type = Bar)"),
lineno_start=2,
lineno_end=2,
- lines=[f"{C.PYTHON_SYNTAX.directive('field')} demo.Foo.x -> Bar"],
+ lines=[f"{C.PYTHON_SYNTAX.directive('field')} demo.Foo.x(type = Bar)"],
)
all_block = CodeBlock(
kind="__all__",
@@ -1214,26 +1219,28 @@ def
test_stage_3_routes_directives_by_generator_declaration(tmp_path: Path) -> N
syntax=C.PYTHON_SYNTAX,
)
- # `ty-map` is pipeline-owned: `_stage_1` consumes it and `_stage_3` skips
it.
- assert "ty-map" in C.PIPELINE_DIRECTIVE_KINDS
+ seen: list[Directives] = []
+
+ keys = {**PythonGenerator.directive_keys, "field": {"type": Key(True)}}
+
+ class FieldAware(PythonGenerator):
+ directive_keys = keys
+
+ def new_imports(self, directives: Directives) -> PythonImports:
+ seen.append(directives)
+ return super().new_imports(directives)
+
+ # `ty_map` is a pipeline key: `_stage_1` reads it.
+ assert "ty_map" in C.PIPELINE_DIRECTIVE_KEYS["object"]
ty_map: dict[str, str] = {}
- _stage_1(_file_info(), ty_map)
+ _stage_1(_file_info(), ty_map, FieldAware())
assert ty_map == {"demo.Foo": "mapped.Foo"}
- # The Python generator does not declare `field`, so the name is unknown to
it.
+ # The Python generator does not declare `field`, so the subject is unknown
to it.
python = get_generator("python")
- assert python.directive_kinds == frozenset({"import-object"})
+ assert set(python.directive_keys) == {"import"}
with pytest.raises(ValueError, match="Unknown directive `field` at line
2"):
_stage_3(_file_info(), Options(dry_run=True), _default_ty_map(), {},
python)
- seen: list[tuple[object, str, str, int]] = []
-
- class FieldAware(PythonGenerator):
- directive_kinds = frozenset({"field"})
-
- def add_directive(self, imports: object, name: str, payload: str,
lineno: int) -> None:
- seen.append((imports, name, payload, lineno))
-
_stage_3(_file_info(), Options(dry_run=True), _default_ty_map(), {},
FieldAware())
- assert [entry[1:] for entry in seen] == [("field", "demo.Foo.x -> Bar", 2)]
- assert isinstance(seen[0][0], type(python.new_imports()))
+ assert seen[0].settings["field"] == {"demo.Foo.x": {"type": "Bar"}}
diff --git a/tests/python/test_stubgen_rust.py
b/tests/python/test_stubgen_rust.py
index b257de53..ae9cd487 100644
--- a/tests/python/test_stubgen_rust.py
+++ b/tests/python/test_stubgen_rust.py
@@ -29,6 +29,7 @@ import tvm_ffi.testing # loads the `testing.*` fixture types
from tvm_ffi.core import TypeSchema
from tvm_ffi.stub import consts as C
from tvm_ffi.stub.cli import _stage_1, _stage_3
+from tvm_ffi.stub.directives import Directives
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
@@ -40,7 +41,7 @@ from tvm_ffi.stub.rust_generator.codegen import (
generate_rust_import_section,
generate_rust_object,
)
-from tvm_ffi.stub.rust_generator.directives import Directives, EnumSpec
+from tvm_ffi.stub.rust_generator.directives import RUST_DIRECTIVE_KEYS,
EnumSpec
from tvm_ffi.stub.rust_generator.utils import (
RustImports,
RustUse,
@@ -298,16 +299,28 @@ def test_check_tokens_ignores_spelling() -> None:
# ---------------------------------------------------------------------------
+def _imports(*lines: str) -> RustImports:
+ """Seed a collector from directive lines ``<subject>: <payload>``,
numbered from 1."""
+ directives = Directives(RUST_DIRECTIVE_KEYS)
+ for lineno, line in enumerate(lines, 1):
+ subject, _, payload = line.partition(": ")
+ directives.add(subject, payload, lineno)
+ return RUST.new_imports(directives)
+
+
def test_directives_parse() -> None:
- directives = Directives()
- directives.add("field", " tirx.Add.a -> PrimExpr ", 1)
- 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)
+ directives = _imports(
+ "field: tirx.Add.a( type = PrimExpr )",
+ "field: ir.Expr.span(nullable)",
+ "enum: ForKind(repr = i32, Serial = 0, Parallel = 1)",
+ "field: tirx.For.kind(type = ForKind)",
+ "enum: Mode(repr = u8)",
+ "field: tirx.For.mode(type = Mode)",
+ "object: ir.SourceName(opaque) ",
+ "object: tirx.Add(upcast = PrimExpr)",
+ "object: tirx.Add(upcast = crate::typed::TypedExpr, custom_new)",
+ "object: tirx.Add",
+ ).directives
assert directives.field_types == {"tirx.Add.a": "PrimExpr"}
assert directives.nullable == {"ir.Expr.span"}
assert directives.enums == {
@@ -320,44 +333,35 @@ def test_directives_parse() -> None:
@pytest.mark.parametrize(
- ("name", "payload", "expected"),
+ ("line", "expected"),
[
- ("field", "tirx.Add.a", "-> <RustType>"),
- ("field", "tirx.Add.a ->", "-> <RustType>"),
- ("field", "Add -> PrimExpr", "<type_key>.<field>"),
- ("nullable", "ir.Expr.span extra", "<type_key>.<field>"),
- ("enum", "tirx.For.kind -> ForKind", "Name(i32)"),
- ("enum", "tirx.For.kind -> ForKind(i128)", "Name(i32)"),
- ("enum", "tirx.For.kind -> ForKind(i32) { Serial }", "Name(i32)"),
- ("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"),
+ ("field: tirx.Add.a(type)", "`type` takes a value"),
+ ("field: tirx.Add.a(type =)", "`type` takes a value"),
+ ("field: tirx.Add.a(type = PrimType)", "`type` conflicts with line 6"),
+ ("field: Add(type = PrimExpr)", "<type_key>.<field>"),
+ ("field: ir.Expr.span extra", "bad target"),
+ ("field: ir.Expr.span(nullable", "expected `<target>("),
+ ("enum: ForKind(repr = i128)", "`repr` is one of"),
+ ("enum: ForKind(repr = i32, Serial)", "`Serial` takes a value"),
+ ("object: ir.SourceName ir.Source", "bad target"),
+ ("object: tirx.Add(upcast)", "`upcast` takes a value"),
+ ("object: tirx.Add(custom_new = yes)", "`custom_new` is a flag"),
+ ("object: tirx.Add(typed_view = PrimExpr)", "unknown key
`typed_view`"),
+ ("typed-view: tirx.Add", "Unknown directive"),
],
)
-def test_directives_reject_malformed(name: str, payload: str, expected: str)
-> None:
+def test_directives_reject_malformed(line: str, expected: str) -> None:
+ directives = Directives(RUST_DIRECTIVE_KEYS)
+ directives.add("field", "tirx.Add.a(type = PrimExpr)", 6)
+ subject, _, payload = line.partition(": ")
with pytest.raises(ValueError, match=re.escape(expected)) as exc:
- Directives().add(name, payload, 7)
+ directives.add(subject, payload, 7)
assert "at line 7" in str(exc.value)
def test_generator_declares_its_directives_and_records_imports() -> None:
- assert RUST.directive_kinds == {
- "import-object",
- "field",
- "nullable",
- "visibility",
- "enum",
- "opaque",
- "upcast",
- "custom-new",
- "no-new",
- "missing-docs",
- }
- 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)
+ assert set(RUST.directive_keys) == {"object", "field", "enum", "file",
"import"}
+ imports = _imports("import: tvm_ffi::libinfo::Foo", "field:
demo.Node.span(nullable)")
assert imports.items == [RustUse("tvm_ffi::libinfo::Foo")]
assert imports.directives.nullable == {"demo.Node.span"}
@@ -640,15 +644,12 @@ def test_render_iter_var_golden() -> None:
parent="ir.PrimExprConvertible",
is_final=True,
)
- imports = RUST.new_imports()
- RUST.add_directive(imports, "nullable", "tirx.IterVar.dom", 1)
- RUST.add_directive(imports, "field", "tirx.IterVar.var -> PrimVar", 2)
- RUST.add_directive(imports, "nullable", "tirx.IterVar.span", 3)
- RUST.add_directive(
- imports,
- "enum",
- "tirx.IterVar.iter_type -> IterVarType(i32) { kDataPar=0,
kThreadIndex=1 }",
- 4,
+ imports = _imports(
+ "field: tirx.IterVar.dom(nullable)",
+ "field: tirx.IterVar.var(type = PrimVar)",
+ "field: tirx.IterVar.span(nullable)",
+ "enum: IterVarType(repr = i32, kDataPar = 0, kThreadIndex = 1)",
+ "field: tirx.IterVar.iter_type(type = IterVarType)",
)
text, imports = _render(info, imports)
assert text == ITER_VAR_EXPECTED
@@ -668,9 +669,10 @@ def
test_field_directive_with_path_records_use_and_nullable_does_not_double_wrap
_field("dom", TypeSchema("Optional", (TypeSchema("int"),))),
),
)
- imports = RUST.new_imports()
- RUST.add_directive(imports, "field", "demo.Node.buffer ->
crate::typed::BufferVar", 1)
- RUST.add_directive(imports, "nullable", "demo.Node.dom", 2)
+ imports = _imports(
+ "field: demo.Node.buffer(type = crate::typed::BufferVar)",
+ "field: demo.Node.dom(nullable)",
+ )
text, imports = _render(info, imports)
assert "pub fn buffer(&self) -> Result<BufferVar> {" in text
assert "pub fn dom(&self) -> Result<Option<i64>> {" in text
@@ -696,12 +698,10 @@ def _span() -> ObjectInfo:
)
-def _span_imports() -> RustImports:
+def _span_imports(*extra: str) -> RustImports:
"""Type the narrow integer fields by directive: the schema erases
signedness."""
- imports = RUST.new_imports()
- for lineno, name in enumerate(("line", "column", "end_line",
"end_column"), 1):
- RUST.add_directive(imports, "field", f"ir.Span.{name} -> i32", lineno)
- return imports
+ names = ("line", "column", "end_line", "end_column")
+ return _imports(*(f"field: ir.Span.{name}(type = i32)" for name in names),
*extra)
def _expr() -> ObjectInfo:
@@ -816,8 +816,7 @@ tvm_ffi::impl_object_ref_upcast!(unsafe 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)
+ imports = _imports("field: ir.Expr.span(nullable)")
text, imports = _render(_expr(), imports)
assert text == EXPR_EXPECTED
assert _uses(imports) == {"tvm_ffi::Object", "tvm_ffi::IntoArg",
"tvm_ffi::ObjectArc"}
@@ -831,12 +830,13 @@ def test_render_complete_add_golden() -> None:
as it is); `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)
+ imports = _imports(
+ "field: ir.Expr.span(nullable)",
+ "field: tirx.Add.a(type = PrimExpr)",
+ "field: tirx.Add.b(type = PrimExpr)",
+ "field: tirx.Add.ty(type = PrimType)",
+ "object: tirx.Add(upcast = PrimExpr)",
+ )
text, imports = _render(_add(), imports)
assert text == ADD_EXPECTED
assert _uses(imports) == {
@@ -895,23 +895,21 @@ def test_reflected_base_and_data_keep_their_names() ->
None:
total_size=48,
is_final=True,
)
- imports = RUST.new_imports()
- RUST.add_directive(imports, "visibility", "demo.Ramp.cache_ ->
pub(crate)", 1)
+ imports = _imports("field: demo.Ramp.cache_(visibility = pub(crate))")
text, _ = _render(info, imports)
assert (
" base_: Object,\n pub base: i64,\n pub data: i64,\n
pub(crate) cache_: i64,\n"
) in text
assert " pub fn new(base: i64, data: i64, cache_: i64) -> Self {" in
text
assert " Self { base_, base, data, cache_ }" in text
- imports = RUST.new_imports()
- RUST.add_directive(imports, "visibility", "demo.Node.cache_ ->
pub(crate)", 1)
+ imports = _imports("field: demo.Node.cache_(visibility = pub(crate))")
text, _ = _render(_info("demo.Node", (_field("base", "int"),
_field("cache_", "int"))), imports)
assert (
'pub fn base(&self) -> Result<i64> {\n
cached_field_getter!(NodeObj, "base")' in text
)
assert " pub(crate) fn cache_(&self) -> Result<i64> {" in text
- with pytest.raises(ValueError, match=r"`visibility` directive at line 2"):
- Directives().add("visibility", "demo.Node.cache_ -> pub(super)", 2)
+ with pytest.raises(ValueError, match=r"line 2: `visibility` is one of
pub\(crate\)"):
+ Directives(RUST_DIRECTIVE_KEYS).add("field",
"demo.Node.cache_(visibility = pub(super))", 2)
def test_render_complete_enum_field() -> None:
@@ -922,8 +920,7 @@ def test_render_complete_enum_field() -> None:
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)
+ imports = _imports("enum: Kind(repr = i32, A = 0, B = 1)", "field:
demo.Pair.kind(type = Kind)")
text, imports = _render(info, imports)
assert " pub kind: Kind,\n" in text
assert " pub fn new(a: i64, kind: impl Into<Kind>) -> Self {" in text
@@ -932,16 +929,18 @@ def test_render_complete_enum_field() -> None:
def test_allow_missing_docs_and_closed_enum() -> None:
- """`missing-docs: allow` marks every generated type and `impl`; `closed`
emits a Rust enum."""
+ """`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)
+ imports = _imports(
+ "file: mod(missing_docs = allow)",
+ "enum: Kind(repr = i32, closed, A = 0, B = -1)",
+ "field: demo.Pair.kind(type = Kind)",
+ )
text, _ = _render(info, imports)
assert text.startswith(
"#[allow(missing_docs)]\n"
@@ -962,15 +961,12 @@ def test_allow_missing_docs_and_closed_enum() -> None:
) 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)
- with pytest.raises(ValueError, match="no member equal to 0"):
- Directives().add("enum", "demo.Pair.kind -> Kind(i32) closed { A=1 }",
4)
- for payload in ("", "warn"):
- with pytest.raises(
- ValueError, match=r"`missing-docs` directive at line 4\. Expected
`allow`"
- ):
- Directives().add("missing-docs", payload, 4)
+ for members in ("", ", A = 1"):
+ with pytest.raises(ValueError, match="no member equal to 0"):
+ _imports(f"enum: Kind(repr = i32, closed{members})")
+ for payload, expected in (("", "takes a value"), (" = warn", "is one of
allow")):
+ with pytest.raises(ValueError, match=f"line 4: `missing_docs`
{expected}"):
+ Directives(RUST_DIRECTIVE_KEYS).add("file",
f"mod(missing_docs{payload})", 4)
def test_complete_optional_field_mirrors() -> None:
@@ -1046,7 +1042,7 @@ def test_complete_big_int_field_mirrors() -> None:
def test_ty_map_origin_override_survives_lossless_fold() -> None:
- """A `ty-map` for `str` still wins for a reflected `ffi.String`: the
lossless fold keeps no key."""
+ """A `ty_map` for `str` still wins for a reflected `ffi.String`: the
lossless fold keeps no key."""
name = TypeSchema.from_json_str('{"type":"ffi.String"}')
assert (name.origin, name.type_key) == ("str", None)
info = _info("demo.Named", (_field("name", name, 24, 16),), total_size=40,
is_final=True)
@@ -1149,10 +1145,9 @@ def test_unrenderable_field_keeps_the_type_opaque(field:
NamedTypeSchema) -> Non
def test_custom_new_renames_the_wrapper_allocator() -> None:
- """`custom-new`: `Add::new` stays hand-written, the allocator is
`from_fields`."""
+ """`custom_new`: `Add::new` stays hand-written, the allocator is
`from_fields`."""
_register(_expr())
- imports = RUST.new_imports()
- RUST.add_directive(imports, "custom-new", "tirx.Add", 1)
+ imports = _imports("object: tirx.Add(custom_new)")
text, _ = _render(_add(), imports)
assert (
"impl AddObj {\n pub(crate) fn new(span: Span, ty: Type, a: Expr,
b: Expr) -> Self {"
@@ -1203,11 +1198,13 @@ def
test_wrapper_allocator_takes_object_references_as_into_arg() -> None:
total_size=160,
is_final=True,
)
- imports = RUST.new_imports()
- RUST.add_directive(imports, "field", "demo.Node.buffer ->
crate::typed::BufferVar", 1)
- RUST.add_directive(imports, "nullable", "demo.Node.span", 2)
- RUST.add_directive(imports, "enum", "demo.Node.kind -> Kind(i64) { A=0 }",
3)
- RUST.add_directive(imports, "field", "demo.Node.count -> i32", 4)
+ imports = _imports(
+ "field: demo.Node.buffer(type = crate::typed::BufferVar)",
+ "field: demo.Node.span(nullable)",
+ "enum: Kind(repr = i64, A = 0)",
+ "field: demo.Node.kind(type = Kind)",
+ "field: demo.Node.count(type = i32)",
+ )
text, imports = _render(info, imports)
assert (
" pub fn new(\n"
@@ -1284,9 +1281,10 @@ def test_allocator_body_wraps_like_rustfmt() -> None:
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)
+ imports = _imports(
+ "object: demo.Leaf(upcast = crate::typed::LeafView)",
+ "object: demo.Leaf(upcast = Other)",
+ )
text, imports = _render(info, imports)
assert text.endswith(
"tvm_ffi::impl_object_ref_upcast!(Leaf => Base);\n"
@@ -1297,8 +1295,7 @@ def test_upcast_directive_adds_typed_views() -> None:
def test_opaque_directive_vetoes_a_complete_type() -> None:
- imports = _span_imports()
- RUST.add_directive(imports, "opaque", "ir.Span", 5)
+ imports = _span_imports("object: ir.Span(opaque)")
text, _ = _render(_span(), imports)
assert "// Opaque: vetoed by directive although the layout is
reproducible." in text
assert "pub fn line(&self) -> Result<i32> {" in text
@@ -1335,19 +1332,19 @@ def test_opaque_parent_keeps_the_child_opaque() -> None:
@pytest.mark.parametrize(
- ("name", "payload", "message"),
+ ("lines", "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"),
+ (("field: demo.Pair.count(type = i64)",), "maps a 4-byte field to
`i64` (8 bytes)"),
+ (
+ ("enum: Kind(repr = i64)", "field: demo.Pair.count(type = Kind)"),
+ "maps a 4-byte field to `i64` (8 bytes)",
+ ),
+ (("field: demo.Pair.count(nullable)",), "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:
+def test_directive_disagreeing_with_bytes_is_an_error(lines: tuple[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)
+ imports = _imports(*lines)
with pytest.raises(ValueError, match=re.escape(message)):
_render(info, imports)
@@ -1407,8 +1404,7 @@ def test_finalize_module_tree(tmp_path: Path) -> None:
def _cxx_class_imports() -> RustImports:
"""Type the narrow integer ``v_i32`` by directive."""
- imports = RUST.new_imports()
- RUST.add_directive(imports, "field", "testing.TestCxxClassBase.v_i32 ->
i32", 1)
+ imports = _imports("field: testing.TestCxxClassBase.v_i32(type = i32)")
return imports
@@ -1483,7 +1479,8 @@ def
test_stage_3_applies_directives_to_a_registered_type(tmp_path: Path) -> None
[
f"{C.RUST_SYNTAX.begin} import-section",
C.RUST_SYNTAX.end,
- f"{C.RUST_SYNTAX.directive('enum')}
testing.TestCxxClassBase.v_i32 -> Kind(i32) {{ A=0 }}",
+ f"{C.RUST_SYNTAX.directive('enum')} Kind(repr = i32, A = 0)",
+ f"{C.RUST_SYNTAX.directive('field')}
testing.TestCxxClassBase.v_i32(type = Kind)",
f"{C.RUST_SYNTAX.begin} object/testing.TestCxxClassBase",
C.RUST_SYNTAX.end,
"",
@@ -1799,9 +1796,7 @@ def test_allocator_hands_a_ty_map_parent_its_own_types()
-> None:
is_final=True,
)
ty_map = {**RUST.default_ty_map(), "demo.Base": "crate::hand::Base"}
- imports = RUST.new_imports()
- RUST.add_directive(imports, "field", "demo.Base.kind -> i32", 1)
- RUST.add_directive(imports, "field", "demo.Child.kind -> Kind", 2)
+ imports = _imports("field: demo.Base.kind(type = i32)", "field:
demo.Child.kind(type = Kind)")
block = _object_block("demo.Child")
generate_rust_object(block, ty_map, imports, Options(), info,
_declared("demo.Child"))
text = "\n".join(block.lines[1:-1])
@@ -1833,14 +1828,17 @@ def
test_stage_3_checks_dependencies_across_the_run(tmp_path: Path) -> None:
assert info is not None
with pytest.raises(ValueError, match=r"depends on
`testing\.TestCxxClassBase`"):
_stage_3(info, Options(dry_run=True), RUST.default_ty_map(), {}, RUST,
declared)
- # A `ty-map` names a hand-written parent; its `<Name>Obj` becomes the base
slot.
- mapped = f"{C.RUST_SYNTAX.ty_map} testing.TestCxxClassBase ->
crate::hand::TestCxxClassBase"
- narrow = f"{C.RUST_SYNTAX.directive('field')}
testing.TestCxxClassBase.v_i32 -> i32"
+ # A `ty_map` names a hand-written parent; its `<Name>Obj` becomes the base
slot.
+ mapped = (
+ f"{C.RUST_SYNTAX.directive('object')} "
+ "testing.TestCxxClassBase(ty_map = crate::hand::TestCxxClassBase)"
+ )
+ narrow = f"{C.RUST_SYNTAX.directive('field')}
testing.TestCxxClassBase.v_i32(type = i32)"
src.write_text("\n".join([mapped, narrow, *blocks]), encoding="utf-8")
info = FileInfo.from_file(src)
assert info is not None
ty_map = RUST.default_ty_map()
- _stage_1(info, ty_map)
+ _stage_1(info, ty_map, RUST)
_stage_3(info, Options(dry_run=True), ty_map, {}, RUST, declared)
text = "\n".join(line for block in info.code_blocks for line in
block.lines)
assert " base_: TestCxxClassBaseObj," in text
@@ -1856,7 +1854,7 @@ def
test_stage_3_checks_dependencies_across_the_run(tmp_path: Path) -> None:
def test_ffi_field_type_off_the_default_ty_map_reads_as_any() -> None:
- """Of the `ffi.*` keys, only those on the default `ty-map` name a type the
crate binds."""
+ """Of the `ffi.*` keys, only those on the default `ty_map` name a type the
crate binds."""
text, imports = _render(_info("demo.Doc", (_field("path",
"ffi.reflection.AccessPath"),)))
assert " pub fn path(&self) -> Result<AccessPath> {" in text
assert "tvm_ffi::reflection::AccessPath" in _uses(imports)
@@ -1935,8 +1933,8 @@ def test_prefix_directive_rolls_out_a_namespace(
"\n".join(
[
"//! Hand-written skeleton.",
- f"{C.RUST_SYNTAX.directive('prefix')} testing",
- f"{C.RUST_SYNTAX.directive('skip')}
testing.TestCxxClassDerivedDerived",
+ f"{C.RUST_SYNTAX.directive('file')} mod(prefix = testing)",
+ f"{C.RUST_SYNTAX.directive('object')}
testing.TestCxxClassDerivedDerived(skip)",
"",
f"{C.RUST_SYNTAX.begin} object/testing.TestCxxClassBase",
C.RUST_SYNTAX.end,
@@ -1973,7 +1971,9 @@ def test_prefix_declared_twice_is_a_failure(
) -> None:
root = _crate(tmp_path)
for name in ("a.rs", "b.rs"):
- (root / name).write_text(f"{C.RUST_SYNTAX.directive('prefix')}
testing\n", encoding="utf-8")
+ (root / name).write_text(
+ f"{C.RUST_SYNTAX.directive('file')} mod(prefix = testing)\n",
encoding="utf-8"
+ )
monkeypatch.setattr("sys.argv", ["tvm-ffi-stubgen", "--target", "rust",
"--check", str(root)])
assert stub_cli.__main__() == 2
@@ -1997,7 +1997,7 @@ def test_roll_out_matches_the_prefix_exactly(
src.write_text(
"\n".join(
[
- f"{C.RUST_SYNTAX.directive('prefix')} a.",
+ f"{C.RUST_SYNTAX.directive('file')} mod(prefix = a.)",
f"{begin} import-section",
end,
"mod tail {}",
@@ -2010,7 +2010,7 @@ def test_roll_out_matches_the_prefix_exactly(
assert info is not None
assert stub_cli._roll_out_prefixes([info], RUST) == 0
assert [line for block in info.code_blocks for line in block.lines] == [
- f"{C.RUST_SYNTAX.directive('prefix')} a.",
+ f"{C.RUST_SYNTAX.directive('file')} mod(prefix = a.)",
f"{begin} import-section",
end,
"",
@@ -2023,7 +2023,7 @@ def test_roll_out_matches_the_prefix_exactly(
]
# Every `ffi.*` key lives in the crate and is never rolled out,
`ffi.Array` included: a
# generated non-generic `Array` would shadow the crate's `Array<T>`.
- src.write_text(f"{C.RUST_SYNTAX.directive('prefix')} ffi\n",
encoding="utf-8")
+ src.write_text(f"{C.RUST_SYNTAX.directive('file')} mod(prefix = ffi)\n",
encoding="utf-8")
info = FileInfo.from_file(src)
assert info is not None
assert stub_cli._roll_out_prefixes([info], RUST) == 0
@@ -2035,13 +2035,15 @@ def test_prefix_survives_init(tmp_path: Path,
monkeypatch: pytest.MonkeyPatch) -
root = _crate(tmp_path)
(root / "testing").mkdir()
mod_rs = root / "testing" / "mod.rs"
- skip = f"{C.RUST_SYNTAX.directive('skip')}
testing.TestCxxClassDerivedDerived"
- mod_rs.write_text(f"{C.RUST_SYNTAX.directive('prefix')}
testing\n{skip}\n", encoding="utf-8")
+ skip = f"{C.RUST_SYNTAX.directive('object')}
testing.TestCxxClassDerivedDerived(skip)"
+ mod_rs.write_text(
+ f"{C.RUST_SYNTAX.directive('file')} mod(prefix = testing)\n{skip}\n",
encoding="utf-8"
+ )
init = ["--init-pypkg", "demo", "--init-lib", "demo_shared",
"--init-prefix", "testing."]
monkeypatch.setattr("sys.argv", ["tvm-ffi-stubgen", "--target", "rust",
*init, str(root)])
assert stub_cli.__main__() == 0
text = mod_rs.read_text(encoding="utf-8")
- assert text.startswith(f"{C.RUST_SYNTAX.directive('prefix')} testing\n")
+ assert text.startswith(f"{C.RUST_SYNTAX.directive('file')} mod(prefix =
testing)\n")
assert "pub struct TestCxxClassDerivedObj {" in text
assert "object/testing.TestCxxClassDerivedDerived" not in text # `--init`
honours `skip`
monkeypatch.setattr("sys.argv", ["tvm-ffi-stubgen", "--target", "rust",
"--check", str(root)])
@@ -2061,7 +2063,13 @@ def test_prefix_removes_blocks_of_unregistered_types(
gone = f"{C.RUST_SYNTAX.begin} object/testing.NoLongerRegistered"
src.write_text(
"\n".join(
- [f"{C.RUST_SYNTAX.directive('prefix')} testing", "", gone,
C.RUST_SYNTAX.end, ""]
+ [
+ f"{C.RUST_SYNTAX.directive('file')} mod(prefix = testing)",
+ "",
+ gone,
+ C.RUST_SYNTAX.end,
+ "",
+ ]
),
encoding="utf-8",
)