This is an automated email from the ASF dual-hosted git repository.
spectrometerHBH pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tvm.git
The following commit(s) were added to refs/heads/main by this push:
new bb9bc20a82 [SCRIPT] Support PEP 695 symbolic variables in Relax and
TIR (#20107)
bb9bc20a82 is described below
commit bb9bc20a8294fb19a2a40f029fe57baa546a8206
Author: Tianqi Chen <[email protected]>
AuthorDate: Sun Aug 9 22:53:09 2026 +0800
[SCRIPT] Support PEP 695 symbolic variables in Relax and TIR (#20107)
Relax and TIRx dependent type fields need symbolic integer variables to
be declared in a function signature without persisting parser-only
metadata in IR.
- Accept standard module-level `TypeVar` declarations on Python 3.10+
and bare or int-bound PEP 695 parameters on Python 3.12+.
- Treat declarations as syntax sugar for fresh int64 TIR variables used
by dependent signature fields; unused declarations disappear and no
function attrs, type-parameter fields, or runtime parameters are added.
- Parse both versioned forms through shared signature plumbing while
retaining dialect-specific annotation and body handling.
Representative TIRx Python 3.12+ output uses PEP 695 and postponed
annotations, so dependent expressions are unquoted:
```python
from __future__ import annotations
@T.prim_func(private=True)
def main[M](A: T.Buffer((M, M * T.int64(2)), "float32")):
A[0, 0] = T.float32(1.0)
```
Before Python 3.12, the portable TIRx output uses a module-level
`TypeVar` and quotes compound dependent expressions:
```python
M = TypeVar("M")
@T.prim_func(private=True)
def main(A: T.Buffer((M, "M * T.int64(2)"), "float32")):
A[0, 0] = T.float32(1.0)
```
The same declaration and printing rules apply to Relax tensor
signatures.
Validation:
- Latest-main native build completed successfully.
- Consolidated Relax and TIRx TypeVar round-trip tests passed.
- Related Relax/TIRx parser, printer, and well-formedness suites passed
661 tests with 4 expected failures.
- Touched-file pre-commit hooks and diff checks passed.
- The implementation does not change Relax well-formedness analysis.
---
include/tvm/script/printer/doc.h | 15 +++-
python/tvm/relax/script/parser/parser.py | 6 +-
python/tvm/runtime/script_printer.py | 12 ++-
python/tvm/script/parser/_core.py | 2 +-
python/tvm/script/parser/core/diagnostics.py | 2 +-
python/tvm/script/parser/core/doc.py | 7 +-
python/tvm/script/parser/core/doc_core.py | 68 +++++++++++++++++
python/tvm/script/parser/core/entry.py | 3 +-
python/tvm/script/parser/core/parser.py | 85 +++++++++++++++++++++-
python/tvm/script/printer/doc.py | 3 +
python/tvm/tirx/script/parser/parser.py | 18 ++++-
src/relax/script/printer/dependent_type.cc | 10 ++-
src/relax/script/printer/function.cc | 52 +++++++------
src/relax/script/printer/tir.cc | 7 +-
src/relax/script/printer/utils.h | 4 +
src/script/printer/doc.cc | 10 ++-
.../printer/doc_printer/python_doc_printer.cc | 5 ++
src/script/printer/ir/ir.cc | 34 ++++++++-
src/script/printer/utils.h | 62 +++++++++++++++-
src/tirx/script/printer/buffer.cc | 20 +++--
src/tirx/script/printer/function.cc | 61 ++++++++++++++--
src/tirx/script/printer/utils.h | 5 +-
tests/python/relax/test_tvmscript_parser.py | 2 +
tests/python/relax/test_tvmscript_type_vars.py | 68 +++++++++++++++++
tests/python/tirx/test_tvmscript_type_vars.py | 67 +++++++++++++++++
.../python/tvmscript/test_tvmscript_printer_doc.py | 1 +
.../test_tvmscript_printer_python_doc_printer.py | 20 +++++
27 files changed, 588 insertions(+), 61 deletions(-)
diff --git a/include/tvm/script/printer/doc.h b/include/tvm/script/printer/doc.h
index 0442852692..5e877db958 100644
--- a/include/tvm/script/printer/doc.h
+++ b/include/tvm/script/printer/doc.h
@@ -1212,6 +1212,14 @@ class FunctionDocNode : public StmtDocNode {
ffi::Optional<ExprDoc> return_type{std::nullopt};
/*! \brief The body of function. */
ffi::Array<StmtDoc> body;
+ /*!
+ * \brief The PEP 695 type parameters of the function.
+ *
+ * Possible actual types:
+ * - ExprDoc (a bare parameter like ``T``)
+ * - AssignDoc (an annotated parameter like ``T: int``)
+ */
+ ffi::Array<Doc> type_params;
static void RegisterReflection() {
namespace refl = tvm::ffi::reflection;
@@ -1220,7 +1228,8 @@ class FunctionDocNode : public StmtDocNode {
.def_ro("args", &FunctionDocNode::args)
.def_ro("decorators", &FunctionDocNode::decorators)
.def_ro("return_type", &FunctionDocNode::return_type)
- .def_ro("body", &FunctionDocNode::body);
+ .def_ro("body", &FunctionDocNode::body)
+ .def_ro("type_params", &FunctionDocNode::type_params);
}
TVM_FFI_DECLARE_OBJECT_INFO_FINAL("script.printer.FunctionDoc",
FunctionDocNode, StmtDocNode);
};
@@ -1239,9 +1248,11 @@ class FunctionDoc : public StmtDoc {
* \param decorators The decorator of function.
* \param return_type The return type of function.
* \param body The body of function.
+ * \param type_params The PEP 695 type parameters of the function.
*/
explicit FunctionDoc(IdDoc name, ffi::Array<AssignDoc> args,
ffi::Array<ExprDoc> decorators,
- ffi::Optional<ExprDoc> return_type, ffi::Array<StmtDoc>
body);
+ ffi::Optional<ExprDoc> return_type, ffi::Array<StmtDoc>
body,
+ ffi::Array<Doc> type_params = {});
TVM_FFI_DEFINE_OBJECT_REF_METHODS_NOTNULLABLE(FunctionDoc, StmtDoc,
FunctionDocNode);
};
diff --git a/python/tvm/relax/script/parser/parser.py
b/python/tvm/relax/script/parser/parser.py
index bef126a884..083aa3ea93 100644
--- a/python/tvm/relax/script/parser/parser.py
+++ b/python/tvm/relax/script/parser/parser.py
@@ -32,7 +32,7 @@ from tvm.relax.script.builder.frame import BindingBlockFrame
from tvm.relax.utils import convert_to_expr
from tvm.script.ir_builder import ir as I
from tvm.script.ir_builder.base import IRBuilder
-from tvm.script.parser._core import Parser, dispatch, doc
+from tvm.script.parser._core import Parser, collect_signature_type_vars,
dispatch, doc
from tvm.tirx.script import builder as T
from .entry import (
@@ -231,9 +231,11 @@ def collect_symbolic_var_from_prelude(
def collect_symbolic_var_from_params(self: Parser, node: doc.FunctionDef) ->
None:
- symbolic_vars = {}
+ symbolic_vars = collect_signature_type_vars(self, node)
prim_params = set()
with self.var_table.with_frame():
+ for var_name, var in symbolic_vars.items():
+ self.var_table.add(var_name, var)
for arg in node.args.args:
if arg.annotation is None:
self.report_error(arg, "Type annotation is required for
function parameters.")
diff --git a/python/tvm/runtime/script_printer.py
b/python/tvm/runtime/script_printer.py
index 3f67e285de..973069021a 100644
--- a/python/tvm/runtime/script_printer.py
+++ b/python/tvm/runtime/script_printer.py
@@ -17,6 +17,7 @@
"""Configuration of TVMScript printer"""
import os
+import sys
from collections.abc import Sequence
from tvm_ffi import get_global_func, register_object
@@ -199,6 +200,10 @@ class Scriptable:
merged_extra: dict = {}
if extra_config is not None:
merged_extra.update(extra_config)
+ if "script.use_pep695" not in merged_extra:
+ merged_extra["script.use_pep695"] = merged_extra.get(
+ "relax.use_pep695", sys.version_info >= (3, 12)
+ )
# Only auto-switch if the caller has not already set a tirx.prefix
override.
if "tirx.prefix" not in merged_extra:
@@ -271,6 +276,11 @@ class Scriptable:
obj_to_underline: list[Object] | None = None,
obj_to_annotate: dict[Object, str] | None = None,
) -> str:
+ merged_extra = dict(extra_config or {})
+ if "script.use_pep695" not in merged_extra:
+ merged_extra["script.use_pep695"] = merged_extra.get(
+ "relax.use_pep695", sys.version_info >= (3, 12)
+ )
return _relax_script(
self,
PrinterConfig(
@@ -286,7 +296,7 @@ class Scriptable:
num_context_lines=num_context_lines,
syntax_sugar=syntax_sugar,
show_object_address=show_object_address,
- extra_config=extra_config,
+ extra_config=merged_extra,
path_to_underline=path_to_underline,
path_to_annotate=path_to_annotate,
obj_to_underline=obj_to_underline,
diff --git a/python/tvm/script/parser/_core.py
b/python/tvm/script/parser/_core.py
index b1d29d0623..d87214f22f 100644
--- a/python/tvm/script/parser/_core.py
+++ b/python/tvm/script/parser/_core.py
@@ -21,4 +21,4 @@
from .core import dispatch, doc, utils
from .core.dispatch import OpMethod, register_op
from .core.entry import parse, scan_macro
-from .core.parser import Parser
+from .core.parser import Parser, collect_signature_type_vars
diff --git a/python/tvm/script/parser/core/diagnostics.py
b/python/tvm/script/parser/core/diagnostics.py
index 4cc7c3a48b..974c5c593f 100644
--- a/python/tvm/script/parser/core/diagnostics.py
+++ b/python/tvm/script/parser/core/diagnostics.py
@@ -186,7 +186,7 @@ def findsource(obj):
if len(tokens) > 1:
name = None
if tokens[0] == "def":
- name = tokens[1].split(":")[0].split("(")[0] + "<locals>"
+ name = tokens[1].split(":")[0].split("(")[0].split("[")[0] +
"<locals>"
elif tokens[0] == "class":
name = tokens[1].split(":")[0].split("(")[0]
# pop scope if we are less indented
diff --git a/python/tvm/script/parser/core/doc.py
b/python/tvm/script/parser/core/doc.py
index 8c94d445c2..7d44dd50d6 100644
--- a/python/tvm/script/parser/core/doc.py
+++ b/python/tvm/script/parser/core/doc.py
@@ -307,7 +307,12 @@ def _register_default():
DefaultTranslator(
getattr(ast, cls_name),
from_doc,
- doc_cls._FIELDS, # pylint: disable=protected-access
+ [
+ field
+ for field in doc_cls._FIELDS # pylint:
disable=protected-access
+ if field in getattr(getattr(ast, cls_name), "_fields",
())
+ or field in {"lineno", "col_offset", "end_lineno",
"end_col_offset"}
+ ],
)
)
diff --git a/python/tvm/script/parser/core/doc_core.py
b/python/tvm/script/parser/core/doc_core.py
index aa05629d5c..74b7e57e3d 100644
--- a/python/tvm/script/parser/core/doc_core.py
+++ b/python/tvm/script/parser/core/doc_core.py
@@ -77,6 +77,7 @@ class FunctionDef(stmt):
"body",
"decorator_list",
"returns",
+ "type_params",
"lineno",
"col_offset",
"end_lineno",
@@ -90,6 +91,7 @@ class FunctionDef(stmt):
body,
decorator_list,
returns,
+ type_params,
lineno,
col_offset,
end_lineno,
@@ -101,6 +103,7 @@ class FunctionDef(stmt):
self.body = body
self.decorator_list = decorator_list
self.returns = returns
+ self.type_params = type_params
class ClassDef(stmt):
@@ -356,6 +359,67 @@ class expr(AST):
self.end_col_offset = end_col_offset
+class type_param(AST):
+ _FIELDS = ["lineno", "col_offset", "end_lineno", "end_col_offset"]
+
+ def __init__(self, lineno, col_offset, end_lineno, end_col_offset):
+ super().__init__()
+ self.lineno = lineno
+ self.col_offset = col_offset
+ self.end_lineno = end_lineno
+ self.end_col_offset = end_col_offset
+
+
+class TypeVar(type_param):
+ _FIELDS = [
+ "name",
+ "bound",
+ "default_value",
+ "lineno",
+ "col_offset",
+ "end_lineno",
+ "end_col_offset",
+ ]
+
+ def __init__(self, name, bound, default_value, lineno, col_offset,
end_lineno, end_col_offset):
+ super().__init__(lineno, col_offset, end_lineno, end_col_offset)
+ self.name = name
+ self.bound = bound
+ self.default_value = default_value
+
+
+class ParamSpec(type_param):
+ _FIELDS = [
+ "name",
+ "default_value",
+ "lineno",
+ "col_offset",
+ "end_lineno",
+ "end_col_offset",
+ ]
+
+ def __init__(self, name, default_value, lineno, col_offset, end_lineno,
end_col_offset):
+ super().__init__(lineno, col_offset, end_lineno, end_col_offset)
+ self.name = name
+ self.default_value = default_value
+
+
+class TypeVarTuple(type_param):
+ _FIELDS = [
+ "name",
+ "default_value",
+ "lineno",
+ "col_offset",
+ "end_lineno",
+ "end_col_offset",
+ ]
+
+ def __init__(self, name, default_value, lineno, col_offset, end_lineno,
end_col_offset):
+ super().__init__(lineno, col_offset, end_lineno, end_col_offset)
+ self.name = name
+ self.default_value = default_value
+
+
class BoolOp(expr):
_FIELDS = ["op", "values", "lineno", "col_offset", "end_lineno",
"end_col_offset"]
@@ -1016,6 +1080,7 @@ __all__ = [
"NotEq",
"NotIn",
"Or",
+ "ParamSpec",
"Pass",
"Pow",
"RShift",
@@ -1030,6 +1095,8 @@ __all__ = [
"Subscript",
"Try",
"Tuple",
+ "TypeVar",
+ "TypeVarTuple",
"UAdd",
"USub",
"UnaryOp",
@@ -1050,6 +1117,7 @@ __all__ = [
"mod",
"operator",
"stmt",
+ "type_param",
"unaryop",
"withitem",
]
diff --git a/python/tvm/script/parser/core/entry.py
b/python/tvm/script/parser/core/entry.py
index 418639db29..ec5b5d7ab1 100644
--- a/python/tvm/script/parser/core/entry.py
+++ b/python/tvm/script/parser/core/entry.py
@@ -17,7 +17,7 @@
"""The entry point of TVM parser."""
import inspect
-from typing import Any
+from typing import Any, TypeVar
import tvm
@@ -62,6 +62,7 @@ def _default_globals() -> dict[str, Any]:
"Tx": _tirx_tile,
"tirx": _tirx_dsl,
"Axis": _tirx_layout.Axis,
+ "TypeVar": TypeVar,
}
diff --git a/python/tvm/script/parser/core/parser.py
b/python/tvm/script/parser/core/parser.py
index 36349b26a4..771be8f3d1 100644
--- a/python/tvm/script/parser/core/parser.py
+++ b/python/tvm/script/parser/core/parser.py
@@ -17,16 +17,17 @@
"""The core parser"""
import abc
+import ast
import inspect
from collections import defaultdict
from collections.abc import Callable
from contextlib import contextmanager
-from typing import Any
+from typing import Any, TypeVar
import numpy as np
from tvm.error import DiagnosticError
-from tvm.ir import GlobalVar
+from tvm.ir import GlobalVar, Var
from tvm.runtime import Object
from ...ir_builder import IRBuilder
@@ -42,6 +43,74 @@ DEFAULT_VISIT = {
}
+def collect_signature_type_vars(parser: "Parser", node: doc.FunctionDef) ->
dict[str, Var]:
+ """Collect symbolic variables declared by function type-parameter
syntax."""
+
+ annotation_names = set()
+ parameter_names = {arg.arg for arg in node.args.args}
+
+ class AnnotationNameCollector(doc.NodeVisitor):
+ def visit_Name(self, name): # pylint: disable=invalid-name
+ annotation_names.add(name.id)
+
+ def visit_Constant(self, constant): # pylint: disable=invalid-name
+ if not isinstance(constant.value, str):
+ return
+ try:
+ expression = ast.parse(constant.value, mode="eval")
+ except SyntaxError:
+ return
+ annotation_names.update(
+ child.id for child in ast.walk(expression) if
isinstance(child, ast.Name)
+ )
+
+ name_collector = AnnotationNameCollector()
+ for arg in node.args.args:
+ name_collector.visit(arg.annotation)
+ name_collector.visit(node.returns)
+
+ symbolic_vars = {}
+ for binding_name, value in parser.var_table.get().items():
+ if (
+ binding_name not in annotation_names
+ or binding_name in parameter_names
+ or not isinstance(value, TypeVar)
+ ):
+ continue
+ if value.__name__ != binding_name:
+ parser.report_error(
+ node,
+ f"TypeVar binding {binding_name!r} must match its declared
name {value.__name__!r}",
+ )
+ if value.__constraints__ or value.__bound__ is not None:
+ parser.report_error(
+ node,
+ f"Symbolic TypeVar {binding_name!r} must not have constraints
or a bound",
+ )
+ symbolic_vars[binding_name] = Var(binding_name, "int64")
+
+ for type_param in node.type_params or []:
+ if not isinstance(type_param, doc.TypeVar):
+ parser.report_error(type_param, "Only PEP 695 TypeVar parameters
are supported")
+ # Both a bare parameter and the optional ``int`` bound declare the
+ # int64 PrimVar represented by this type parameter. The printer uses
+ # the bare spelling, while accepting the bound as explicit input.
+ if type_param.bound is not None and not (
+ isinstance(type_param.bound, doc.Name) and type_param.bound.id ==
"int"
+ ):
+ parser.report_error(
+ type_param,
+ f"Symbolic TypeVar {type_param.name!r} must be unannotated or
annotated as int",
+ )
+ if type_param.default_value is not None:
+ parser.report_error(
+ type_param,
+ f"Symbolic TypeVar {type_param.name!r} must not have a
default",
+ )
+ symbolic_vars[type_param.name] = Var(type_param.name, "int64")
+ return symbolic_vars
+
+
def _deferred(exit_f: Callable[[], None]):
"""Created context with certain exit function.
@@ -777,6 +846,18 @@ class Parser(doc.NodeVisitor):
self.report_error(node, "The parser does not understand the
decorator")
_dispatch_wrapper(func)(self, node)
+ def visit_ImportFrom(self, node: doc.ImportFrom) -> None: # pylint:
disable=invalid-name
+ """Accept postponed annotations emitted by the PEP 695 printer."""
+ is_future_annotations = (
+ node.module == "__future__"
+ and node.level == 0
+ and len(node.names) == 1
+ and node.names[0].name == "annotations"
+ and node.names[0].asname is None
+ )
+ if not is_future_annotations:
+ self.report_error(node, "Only 'from __future__ import annotations'
is supported")
+
def visit_arguments(self, node: doc.arguments) -> Any:
"""The general arguments visiting method.
diff --git a/python/tvm/script/printer/doc.py b/python/tvm/script/printer/doc.py
index 819a24a431..dd4d635e8f 100644
--- a/python/tvm/script/printer/doc.py
+++ b/python/tvm/script/printer/doc.py
@@ -462,6 +462,7 @@ class FunctionDoc(StmtDoc):
decorators: Sequence[ExprDoc]
return_type: ExprDoc | None
body: Sequence[StmtDoc]
+ type_params: Sequence[Doc]
def __init__(
self,
@@ -470,6 +471,7 @@ class FunctionDoc(StmtDoc):
decorators: list[ExprDoc],
return_type: ExprDoc | None,
body: list[StmtDoc],
+ type_params: list[Doc] | None = None,
):
self.__init_handle_by_constructor__(
_ffi_api.FunctionDoc, # type: ignore # pylint: disable=no-member
@@ -478,6 +480,7 @@ class FunctionDoc(StmtDoc):
decorators,
return_type,
body,
+ type_params or [],
)
diff --git a/python/tvm/tirx/script/parser/parser.py
b/python/tvm/tirx/script/parser/parser.py
index 324f59cdb5..ca881ac1c7 100644
--- a/python/tvm/tirx/script/parser/parser.py
+++ b/python/tvm/tirx/script/parser/parser.py
@@ -20,14 +20,14 @@ import ast
import contextlib
from copy import deepcopy
from functools import partial
-from typing import Any
+from typing import Any, TypeVar
import tvm
from tvm.ir import Expr, GlobalVar, PointerType, PrimType
from tvm.script.ir_builder import ir as I
from tvm.script.ir_builder.base import IRBuilder
from tvm.script.ir_builder.base import IRBuilderFrame as Frame
-from tvm.script.parser._core import Parser, dispatch, doc
+from tvm.script.parser._core import Parser, collect_signature_type_vars,
dispatch, doc
from tvm.script.parser.core.doc import from_doc
from tvm.tirx import Buffer, IterVar, Layout, buffer_data, is_buffer_var
from tvm.tirx.script import builder as T
@@ -429,9 +429,15 @@ def _eval_signature_annotation(
for child in ast.walk(expression):
if not isinstance(child, ast.Name) or not
isinstance(child.ctx, ast.Load):
continue
- if define_missing and child.id not in
self_parser.var_table.get():
+ current_value = self_parser.var_table.get().get(child.id)
+ is_shadowed_type_var = child.id in signature_dtypes and
isinstance(
+ current_value, TypeVar
+ )
+ if define_missing and (current_value is None or
is_shadowed_type_var):
# TIR match-scope indices default to int32. A later scalar
- # parameter keeps its explicitly declared dtype.
+ # parameter keeps its explicitly declared dtype. That
+ # runtime parameter also shadows a module TypeVar with the
+ # same name, such as one emitted for another function.
var = tvm.tirx.Var(child.id,
signature_dtypes.get(child.id, "int32"))
self_parser.var_table.add(child.id, var,
allow_shadowing=False)
self_parser._signature_match_vars[child.id] = var
@@ -835,6 +841,8 @@ def visit_function_def(self: Parser, node: doc.FunctionDef)
-> None:
persistent = find_decorator_annotation(node, "persistent", default=False)
self.function_annotations = None
with self.var_table.with_frame(), _signature_match_var_scope(self):
+ for name, var in collect_signature_type_vars(self, node).items():
+ self.var_table.add(name, var, allow_shadowing=False)
prim_func_ctx = T.prim_func(is_private=privacy, s_tir=s_tir,
persistent=persistent)
with prim_func_ctx:
T.func_name(node.name)
@@ -1139,6 +1147,8 @@ def visit_tvm_declare_function(self: Parser, node:
doc.FunctionDef) -> GlobalVar
ret_type = None
with self.var_table.with_frame(), _signature_match_var_scope(self):
signature_dtypes = _signature_prim_var_dtypes(node)
+ for name, var in collect_signature_type_vars(self, node).items():
+ self.var_table.add(name, var, allow_shadowing=False)
arg_annotations = []
for arg in node.args.args:
diff --git a/src/relax/script/printer/dependent_type.cc
b/src/relax/script/printer/dependent_type.cc
index 87f158d94b..af1f126fce 100644
--- a/src/relax/script/printer/dependent_type.cc
+++ b/src/relax/script/printer/dependent_type.cc
@@ -53,7 +53,15 @@ ExprDoc PrintShapeVar(const PrimExpr& e, const AccessPath&
e_p, const IRDocsifie
});
}
// Step 3. Stringify the PrimExpr if func var exists
- if (func_var_mode) {
+ bool is_bare_type_var = false;
+ if (f != nullptr && f->type_vars != nullptr) {
+ if (auto var = e.as<tirx::PrimVar>()) {
+ is_bare_type_var = f->type_vars->count(var.value().get());
+ }
+ }
+ bool use_postponed_annotations =
+ UsePEP695TypeVars(d) && f != nullptr && f->type_vars != nullptr &&
!f->type_vars->empty();
+ if (func_var_mode && !is_bare_type_var && !use_postponed_annotations) {
return ExprStringDoc(expr_doc, e_p);
}
return expr_doc;
diff --git a/src/relax/script/printer/function.cc
b/src/relax/script/printer/function.cc
index fafdad135b..869b842a10 100644
--- a/src/relax/script/printer/function.cc
+++ b/src/relax/script/printer/function.cc
@@ -16,6 +16,8 @@
* specific language governing permissions and limitations
* under the License.
*/
+#include <algorithm>
+
#include "./utils.h"
namespace tvm {
@@ -51,6 +53,8 @@ TVM_FFI_STATIC_INIT_BLOCK() {
RelaxFrameNode::RegisterReflection(); }
TVM_STATIC_IR_FUNCTOR(IRDocsifier, vtable)
.set_dispatch<relax::Function>("", [](relax::Function n, AccessPath n_p,
IRDocsifier d) -> Doc {
std::unordered_set<const VarNode*> func_vars;
+ std::unordered_set<const VarNode*> type_vars;
+ std::unordered_set<const VarNode*> prim_params;
With<RelaxFrame> f(d);
IdDoc func_name("");
@@ -64,6 +68,13 @@ TVM_STATIC_IR_FUNCTOR(IRDocsifier, vtable)
(*f)->AddDispatchToken(d, "relax");
(*f)->is_func = true;
(*f)->func_vars = &func_vars;
+ (*f)->type_vars = &type_vars;
+ (*f)->prim_params = &prim_params;
+ for (const Var& param : n->params) {
+ if (param->ty.as<PrimTypeNode>()) {
+ prim_params.insert(param.get());
+ }
+ }
// Step 1. Print params
ffi::Array<AssignDoc> params;
{
@@ -79,31 +90,22 @@ TVM_STATIC_IR_FUNCTOR(IRDocsifier, vtable)
ffi::Optional<ExprDoc> ret_type = d->AsDoc<ExprDoc>(n->ret_ty,
n_p->Attr("ret_ty"));
// Step 3. Clean up func variables
(*f)->func_vars = nullptr;
+ (*f)->type_vars = nullptr;
+ (*f)->prim_params = nullptr;
// Step 4. Print attributes
- if (!n->attrs->dict.empty()) {
- // If the function is a global function and has a global symbol,
- // then don't print the global symbol (it will be implicit from not
being private).
- // For a function without an IR module whose global symbol
- // doesn't match the function name, we should still print the global
symbol attribute.
- if (AtTopLevelFunction(d) &&
n->attrs->dict.count(tvm::attr::kGlobalSymbol) &&
-
n->attrs->dict.at(tvm::attr::kGlobalSymbol).as_or_throw<ffi::String>() ==
- func_name->name) {
- ffi::Map<ffi::String, Any> new_attrs;
- for (auto kv : n->attrs->dict) {
- if (kv.first != tvm::attr::kGlobalSymbol) {
- new_attrs.Set(kv.first, kv.second);
- }
- }
- if (!new_attrs.empty()) {
- (*f)->stmts.push_back(ExprStmtDoc(
- Relax(d, "func_attr") //
- ->Call({d->AsDoc<ExprDoc>(DictAttrs(new_attrs),
n_p->Attr("attrs"))})));
- }
- } else {
- (*f)->stmts.push_back(
- ExprStmtDoc(Relax(d, "func_attr") //
- ->Call({d->AsDoc<ExprDoc>(n->attrs,
n_p->Attr("attrs"))})));
+ ffi::Map<ffi::String, Any> printable_attrs;
+ for (const auto& [key, value] : n->attrs->dict) {
+ // A matching global symbol is implicit for a top-level function.
+ if (key == tvm::attr::kGlobalSymbol && AtTopLevelFunction(d) &&
+ value.as_or_throw<ffi::String>() == func_name->name) {
+ continue;
}
+ printable_attrs.Set(key, value);
+ }
+ if (!printable_attrs.empty()) {
+ (*f)->stmts.push_back(ExprStmtDoc(
+ Relax(d, "func_attr") //
+ ->Call({d->AsDoc<ExprDoc>(DictAttrs(printable_attrs),
n_p->Attr("attrs"))})));
}
// Step 5. Prepare the decorator (include purity if it's impure)
ExprDoc decorator = Relax(d, "function");
@@ -127,7 +129,9 @@ TVM_STATIC_IR_FUNCTOR(IRDocsifier, vtable)
// Step 6. Print body
ffi::Array<StmtDoc> body = PrintSeqExpr(n->body, n_p->Attr("body"), d,
/*use_ret=*/true);
(*f)->stmts.insert((*f)->stmts.end(), body.begin(), body.end());
- return HeaderWrapper(d, FunctionDoc(func_name, params, {decorator},
ret_type, (*f)->stmts));
+ auto type_var_docs = DefineTypeVarDocs(type_vars,
ffi::GetRef<Frame>((*f).get()), d);
+ return WrapFunctionDocWithTypeVars(
+ d, FunctionDoc(func_name, params, {decorator}, ret_type,
(*f)->stmts), type_var_docs);
});
TVM_STATIC_IR_FUNCTOR(IRDocsifier, vtable)
diff --git a/src/relax/script/printer/tir.cc b/src/relax/script/printer/tir.cc
index dfd742a0b1..6ac8f11986 100644
--- a/src/relax/script/printer/tir.cc
+++ b/src/relax/script/printer/tir.cc
@@ -61,10 +61,15 @@ Doc PrintCanonicalVar(Var n, AccessPath n_p, IRDocsifier d)
{
if (f->func_vars) {
TVM_FFI_ICHECK(f->is_func);
f->func_vars->insert(n.get());
+ if (!f->prim_params->count(n.get())) {
+ f->type_vars->insert(n.get());
+ }
}
IdDoc var = d->Define(n, ffi::GetRef<Frame>(f), n->name.empty() ? "v" :
n->name);
var->source_paths.push_back(n_p);
- f->stmts.push_back(AssignDoc(var, PrintVarCreation(prim_var, n_p, d),
std::nullopt));
+ if (!f->func_vars || f->prim_params->count(n.get()) ||
!f->type_vars->count(n.get())) {
+ f->stmts.push_back(AssignDoc(var, PrintVarCreation(prim_var, n_p, d),
std::nullopt));
+ }
}
if (ffi::Optional<ExprDoc> doc = d->GetVarDoc(n)) {
return doc.value();
diff --git a/src/relax/script/printer/utils.h b/src/relax/script/printer/utils.h
index 6fe09a672b..cb7c8c4b46 100644
--- a/src/relax/script/printer/utils.h
+++ b/src/relax/script/printer/utils.h
@@ -42,6 +42,8 @@ class RelaxFrameNode : public FrameNode {
bool is_func = false;
bool module_alias_printed = false;
std::unordered_set<const VarNode*>* func_vars = nullptr;
+ std::unordered_set<const VarNode*>* type_vars = nullptr;
+ std::unordered_set<const VarNode*>* prim_params = nullptr;
static void RegisterReflection() {
namespace refl = tvm::ffi::reflection;
@@ -60,6 +62,8 @@ class RelaxFrame : public Frame {
n->d = d.get();
n->is_func = false;
n->func_vars = nullptr;
+ n->type_vars = nullptr;
+ n->prim_params = nullptr;
data_ = std::move(n);
}
diff --git a/src/script/printer/doc.cc b/src/script/printer/doc.cc
index a5a4e92c9a..53738361b9 100644
--- a/src/script/printer/doc.cc
+++ b/src/script/printer/doc.cc
@@ -261,13 +261,15 @@ ReturnDoc::ReturnDoc(ExprDoc value) {
}
FunctionDoc::FunctionDoc(IdDoc name, ffi::Array<AssignDoc> args,
ffi::Array<ExprDoc> decorators,
- ffi::Optional<ExprDoc> return_type,
ffi::Array<StmtDoc> body) {
+ ffi::Optional<ExprDoc> return_type,
ffi::Array<StmtDoc> body,
+ ffi::Array<Doc> type_params) {
ffi::ObjectPtr<FunctionDocNode> n = ffi::make_object<FunctionDocNode>();
n->name = name;
n->args = args;
n->decorators = decorators;
n->return_type = return_type;
n->body = body;
+ n->type_params = type_params;
this->data_ = std::move(n);
}
@@ -483,8 +485,10 @@ TVM_FFI_STATIC_INIT_BLOCK() {
namespace refl = tvm::ffi::reflection;
refl::GlobalDef().def("script.printer.FunctionDoc",
[](IdDoc name, ffi::Array<AssignDoc> args,
ffi::Array<ExprDoc> decorators,
- ffi::Optional<ExprDoc> return_type,
ffi::Array<StmtDoc> body) {
- return FunctionDoc(name, args, decorators,
return_type, body);
+ ffi::Optional<ExprDoc> return_type,
ffi::Array<StmtDoc> body,
+ ffi::Array<Doc> type_params = {}) {
+ return FunctionDoc(name, args, decorators,
return_type, body,
+ type_params);
});
}
diff --git a/src/script/printer/doc_printer/python_doc_printer.cc
b/src/script/printer/doc_printer/python_doc_printer.cc
index 13ccbde1ea..72fbbc1ec4 100644
--- a/src/script/printer/doc_printer/python_doc_printer.cc
+++ b/src/script/printer/doc_printer/python_doc_printer.cc
@@ -805,6 +805,11 @@ void PythonDocPrinter::PrintTypedDoc(const FunctionDoc&
doc) {
output_ << "def ";
PrintDoc(doc->name);
+ if (!doc->type_params.empty()) {
+ output_ << "[";
+ PrintJoinedDocs(doc->type_params, ", ");
+ output_ << "]";
+ }
output_ << "(";
PrintJoinedDocs(doc->args, ", ");
diff --git a/src/script/printer/ir/ir.cc b/src/script/printer/ir/ir.cc
index 640bc6c57e..67c42f0f31 100644
--- a/src/script/printer/ir/ir.cc
+++ b/src/script/printer/ir/ir.cc
@@ -64,9 +64,28 @@ struct SortableFunction {
}
};
+ffi::Optional<ffi::String> GetTypeVarDeclarationName(const StmtDoc& stmt) {
+ const auto* assign = stmt.as<AssignDocNode>();
+ if (assign == nullptr || !assign->rhs.has_value()) {
+ return std::nullopt;
+ }
+ const auto* lhs = assign->lhs.as<IdDocNode>();
+ const auto* call = assign->rhs.value().as<CallDocNode>();
+ if (lhs == nullptr || call == nullptr) {
+ return std::nullopt;
+ }
+ const auto* callee = call->callee.as<IdDocNode>();
+ if (callee == nullptr || callee->name != "TypeVar") {
+ return std::nullopt;
+ }
+ return lhs->name;
+}
+
TVM_STATIC_IR_FUNCTOR(IRDocsifier, vtable)
.set_dispatch<IRModule>("", [](IRModule mod, AccessPath p, IRDocsifier d)
-> Doc {
std::vector<SortableFunction> functions;
+ ffi::Array<StmtDoc> type_var_decls;
+ std::unordered_set<ffi::String> declared_type_vars;
for (const auto& kv : mod->functions) {
functions.push_back(SortableFunction(kv));
}
@@ -101,6 +120,14 @@ TVM_STATIC_IR_FUNCTOR(IRDocsifier, vtable)
Doc doc = d->AsDoc(base_func, p->Attr("functions")->MapItem(gv));
d->cfg->binding_names.pop_back();
if (const auto* stmt_block = doc.as<StmtBlockDocNode>()) {
+ for (const StmtDoc& stmt : stmt_block->stmts) {
+ if (ffi::Optional<ffi::String> name =
GetTypeVarDeclarationName(stmt)) {
+ if (!declared_type_vars.count(name.value())) {
+ declared_type_vars.insert(name.value());
+ type_var_decls.push_back(stmt);
+ }
+ }
+ }
(*f)->stmts.push_back(stmt_block->stmts.back());
(*f)->stmts.back()->source_paths = std::move(doc->source_paths);
} else if (auto stmt = doc.as<StmtDoc>()) {
@@ -118,7 +145,12 @@ TVM_STATIC_IR_FUNCTOR(IRDocsifier, vtable)
<< " produced Doc type of " << doc->GetTypeKey();
}
}
- return HeaderWrapper(d, ClassDoc(module_doc, {IR(d, "ir_module")},
(*f)->stmts));
+ ClassDoc class_doc(module_doc, {IR(d, "ir_module")}, (*f)->stmts);
+ if (type_var_decls.empty()) {
+ return HeaderWrapper(d, class_doc);
+ }
+ type_var_decls.push_back(class_doc);
+ return HeaderWrapper(d, StmtBlockDoc(type_var_decls));
});
TVM_STATIC_IR_FUNCTOR(IRDocsifier, vtable)
diff --git a/src/script/printer/utils.h b/src/script/printer/utils.h
index 99e98556b6..1ec51e3a43 100644
--- a/src/script/printer/utils.h
+++ b/src/script/printer/utils.h
@@ -29,6 +29,7 @@
#include <tvm/script/printer/ir_docsifier.h>
#include <tvm/script/printer/printer.h>
+#include <algorithm>
#include <sstream>
#include <string>
#include <unordered_set>
@@ -67,6 +68,9 @@ inline std::string Docsify(const ffi::ObjectRef& obj, const
IRDocsifier& d, cons
TVM_FFI_THROW(TypeError) << "Unexpected doc type: " << doc->GetTypeKey();
}
std::ostringstream os;
+ if (d->ir_usage.count("future_annotations")) {
+ os << "from __future__ import annotations\n\n";
+ }
if (!d->metadata.empty()) {
if (d->cfg->show_meta) {
os << "metadata = tvm.ir.load_json(\"\"\""
@@ -124,6 +128,9 @@ inline std::string DType2Str(DLDataType dtype) {
inline Doc HeaderWrapper(const IRDocsifier& d, const Doc& doc) {
if (d->ir_usage.size()) {
ffi::Array<StmtDoc> stmts;
+ if (d->ir_usage.count("type_var")) {
+ stmts.push_back(CommentDoc("from typing import TypeVar"));
+ }
if (d->ir_usage.count("ir")) {
stmts.push_back(CommentDoc("from tvm.script import ir as " +
d->cfg->ir_prefix));
}
@@ -140,12 +147,65 @@ inline Doc HeaderWrapper(const IRDocsifier& d, const Doc&
doc) {
d->cfg->GetExtraConfig<ffi::String>("relax.prefix", "R")));
}
stmts.push_back(CommentDoc(""));
- stmts.push_back(doc.as_or_throw<StmtDoc>());
+ if (const auto* stmt_block = doc.as<StmtBlockDocNode>()) {
+ stmts.insert(stmts.end(), stmt_block->stmts.begin(),
stmt_block->stmts.end());
+ } else {
+ stmts.push_back(doc.as_or_throw<StmtDoc>());
+ }
return StmtBlockDoc(stmts);
}
return doc;
}
+inline std::vector<std::pair<std::string, ExprDoc>> DefineTypeVarDocs(
+ const std::unordered_set<const VarNode*>& type_vars, const Frame& frame,
const IRDocsifier& d) {
+ std::vector<std::pair<std::string, ExprDoc>> type_var_docs;
+ type_var_docs.reserve(type_vars.size());
+ for (const VarNode* var_node : type_vars) {
+ Var var = ffi::GetRef<Var>(var_node);
+ ffi::Optional<ExprDoc> existing_doc = d->GetVarDoc(var);
+ ExprDoc var_doc = existing_doc.has_value()
+ ? existing_doc.value()
+ : d->Define(var, frame, var->name.empty() ? "v" :
var->name);
+ const auto* id_doc = var_doc.as<IdDocNode>();
+ TVM_FFI_ICHECK(id_doc != nullptr);
+ type_var_docs.emplace_back(id_doc->name, var_doc);
+ }
+ std::sort(type_var_docs.begin(), type_var_docs.end(),
+ [](const auto& lhs, const auto& rhs) { return lhs.first <
rhs.first; });
+ return type_var_docs;
+}
+
+inline bool UsePEP695TypeVars(const IRDocsifier& d) {
+ return d->cfg->GetExtraConfig<bool>("script.use_pep695",
+
d->cfg->GetExtraConfig<bool>("relax.use_pep695", false));
+}
+
+inline Doc WrapFunctionDocWithTypeVars(
+ const IRDocsifier& d, FunctionDoc function_doc,
+ const std::vector<std::pair<std::string, ExprDoc>>& type_var_docs) {
+ if (type_var_docs.empty()) {
+ return HeaderWrapper(d, function_doc);
+ }
+ if (UsePEP695TypeVars(d)) {
+ d->ir_usage.insert("future_annotations");
+ for (const auto& type_var_doc : type_var_docs) {
+ function_doc->type_params.push_back(type_var_doc.second);
+ }
+ return HeaderWrapper(d, function_doc);
+ }
+
+ d->ir_usage.insert("type_var");
+ ffi::Array<StmtDoc> stmts;
+ for (const auto& [name, var_doc] : type_var_docs) {
+ stmts.push_back(AssignDoc(
+ var_doc, IdDoc("TypeVar")->Call({LiteralDoc::Str(name,
ffi::Optional<AccessPath>())}),
+ std::nullopt));
+ }
+ stmts.push_back(function_doc);
+ return HeaderWrapper(d, StmtBlockDoc(stmts));
+}
+
/*! \brief Check if a string has multiple lines. */
inline bool HasMultipleLines(const std::string& str) {
return str.find_first_of('\n') != std::string::npos;
diff --git a/src/tirx/script/printer/buffer.cc
b/src/tirx/script/printer/buffer.cc
index e161a03868..950b5acc34 100644
--- a/src/tirx/script/printer/buffer.cc
+++ b/src/tirx/script/printer/buffer.cc
@@ -30,8 +30,8 @@ namespace printer {
ffi::Map<ffi::String, ExprDoc> BufferAttrs(
tirx::BufferVar buffer, const AccessPath& buffer_p, const Frame& frame,
const IRDocsifier& d,
BufferVarDefinition var_definitions, ffi::Optional<Expr> data =
std::nullopt,
- bool stringify_undefined_shape = false,
- std::unordered_set<tirx::Var> stringify_shape_vars = {}) {
+ bool stringify_undefined_shape = false, std::unordered_set<tirx::Var>
stringify_shape_vars = {},
+ std::unordered_set<tirx::Var> stringify_compound_shape_vars = {}) {
using tvm::tirx::Var;
using tvm::tirx::VarNode;
ffi::Map<ffi::String, ExprDoc> kwargs;
@@ -88,6 +88,7 @@ ffi::Map<ffi::String, ExprDoc> BufferAttrs(
PrimExpr e = shape[i];
AccessPath e_p = shape_p->ArrayItem(i);
bool contains_new_var = false;
+ bool contains_compound_shape_var = false;
std::unordered_set<Var> vars_in_shape;
tirx::PostOrderVisit(e, [&](const ffi::ObjectRef& obj) {
if (const auto* var_node = obj.as<VarNode>()) {
@@ -95,14 +96,20 @@ ffi::Map<ffi::String, ExprDoc> BufferAttrs(
vars_in_shape.insert(var);
contains_new_var =
contains_new_var || !d->IsVarDefined(var) ||
stringify_shape_vars.count(var);
+ contains_compound_shape_var =
+ contains_compound_shape_var ||
stringify_compound_shape_vars.count(var);
}
});
if (is_new_var(e)) {
add_out_of_line_var_def(e.as_or_throw<Var>(), e_p);
}
ExprDoc result = d->AsDoc<ExprDoc>(e, e_p);
- results.push_back(stringify_undefined_shape && contains_new_var ?
ExprStringDoc(result, e_p)
- :
result);
+ bool is_bare_compound_shape_var =
+ e.as<VarNode>() &&
stringify_compound_shape_vars.count(e.as_or_throw<Var>());
+ bool stringify_compound_expr = contains_compound_shape_var &&
!is_bare_compound_shape_var;
+ results.push_back((stringify_undefined_shape && contains_new_var) ||
stringify_compound_expr
+ ? ExprStringDoc(result, e_p)
+ : result);
// A quoted shape expression defines every Var it contains. Do not quote
// later dimensions merely because they reuse a Var introduced here.
for (const Var& var : vars_in_shape) {
@@ -335,10 +342,11 @@ ExprDoc BufferDecl(const tirx::BufferVar& buffer, const
ffi::String& method,
}
ExprDoc BufferAttn(const tirx::BufferVar& buffer, const AccessPath& p, const
Frame& frame,
- const IRDocsifier& d, std::unordered_set<tirx::Var>
stringify_shape_vars) {
+ const IRDocsifier& d, std::unordered_set<tirx::Var>
stringify_shape_vars,
+ std::unordered_set<tirx::Var>
stringify_compound_shape_vars) {
ffi::Map<ffi::String, ExprDoc> attrs =
BufferAttrs(buffer, p, frame, d, BufferVarDefinition::MatchBuffer,
std::nullopt, true,
- std::move(stringify_shape_vars));
+ std::move(stringify_shape_vars),
std::move(stringify_compound_shape_vars));
if (!attrs.count("dtype")) {
attrs.Set("dtype", LiteralDoc::DataType(buffer->dtype->dtype,
p->Attr("dtype")));
}
diff --git a/src/tirx/script/printer/function.cc
b/src/tirx/script/printer/function.cc
index f6ba953b86..554dba11c5 100644
--- a/src/tirx/script/printer/function.cc
+++ b/src/tirx/script/printer/function.cc
@@ -34,6 +34,42 @@ TVM_STATIC_IR_FUNCTOR(IRDocsifier, vtable)
d->SetCommonPrefix(func, [](const ffi::ObjectRef& obj) {
return obj->IsInstance<tirx::VarNode>() ||
obj->IsInstance<tirx::BufferTypeNode>();
});
+ std::unordered_set<const VarNode*> runtime_params;
+ for (const tirx::Var& param : func->params) {
+ runtime_params.insert(param.get());
+ }
+ std::unordered_set<const VarNode*> type_vars;
+ auto collect_type_vars = [&](const PrimExpr& expr) {
+ for (const tirx::Var& var : tirx::UndefinedVars(expr)) {
+ const auto* var_ty_node = var->ty.as<PrimTypeNode>();
+ if (var_ty_node == nullptr) {
+ continue;
+ }
+ PrimType var_ty(var_ty_node->dtype);
+ if (!runtime_params.count(var.get()) && var_ty.IsScalar() &&
+ var_ty.MatchesElementType(DLDataTypeCode::kDLInt, 64)) {
+ type_vars.insert(var.get());
+ }
+ }
+ };
+ for (const tirx::Var& param : func->params) {
+ if (!param->ty.as<tirx::BufferTypeNode>()) {
+ continue;
+ }
+ tirx::BufferVar buffer(param);
+ for (const PrimExpr& extent : buffer->shape) {
+ collect_type_vars(extent);
+ }
+ for (const PrimExpr& stride : buffer->strides) {
+ collect_type_vars(stride);
+ }
+ collect_type_vars(buffer->elem_offset);
+ for (const PrimExpr& address : buffer->allocated_addr) {
+ collect_type_vars(address);
+ }
+ }
+ auto type_var_docs = DefineTypeVarDocs(type_vars,
ffi::GetRef<Frame>((*f).get()), d);
+ bool use_postponed_annotations = UsePEP695TypeVars(d) &&
!type_vars.empty();
int n_args = func->params.size();
// Step 1. Handle `func->params`
ffi::Array<AssignDoc> args;
@@ -55,21 +91,28 @@ TVM_STATIC_IR_FUNCTOR(IRDocsifier, vtable)
if (var->ty.as<tirx::BufferTypeNode>()) {
tirx::BufferVar buffer(var);
std::unordered_set<tirx::Var> stringify_shape_vars;
+ std::unordered_set<tirx::Var> stringify_compound_shape_vars;
std::unordered_set<tirx::Var> shape_vars;
for (const PrimExpr& shape : buffer->shape) {
tirx::PostOrderVisit(shape, [&](const ffi::ObjectRef& obj) {
if (const auto* shape_var_node = obj.as<tirx::VarNode>()) {
tirx::Var shape_var = ffi::GetRef<tirx::Var>(shape_var_node);
shape_vars.insert(shape_var);
- if (!bound_signature_vars.count(shape_var)) {
+ bool is_type_var = type_vars.count(shape_var.get());
+ if (!use_postponed_annotations &&
!bound_signature_vars.count(shape_var) &&
+ !is_type_var) {
stringify_shape_vars.insert(shape_var);
}
+ if (!use_postponed_annotations && is_type_var) {
+ stringify_compound_shape_vars.insert(shape_var);
+ }
}
});
}
IdDoc lhs = DefineBuffer(buffer, *f, d);
ExprDoc annotation =
- BufferAttn(buffer, var_p->Attr("ty"), *f, d,
std::move(stringify_shape_vars));
+ BufferAttn(buffer, var_p->Attr("ty"), *f, d,
std::move(stringify_shape_vars),
+ std::move(stringify_compound_shape_vars));
args.push_back(AssignDoc(lhs, std::nullopt, annotation));
for (const tirx::Var& shape_var : shape_vars) {
bound_signature_vars.insert(shape_var);
@@ -177,12 +220,14 @@ TVM_STATIC_IR_FUNCTOR(IRDocsifier, vtable)
ffi::Array<ExprDoc> pos_args;
decorator = std::move(decorator->Call(pos_args, kwargs_keys,
kwargs_values));
}
- return HeaderWrapper(d, FunctionDoc(
- /*name=*/func_name,
- /*args=*/args,
- /*decorators=*/{decorator},
- /*return_type=*/ret_type,
- /*body=*/(*f)->stmts));
+ return WrapFunctionDocWithTypeVars(d,
+ FunctionDoc(
+ /*name=*/func_name,
+ /*args=*/args,
+ /*decorators=*/{decorator},
+ /*return_type=*/ret_type,
+ /*body=*/(*f)->stmts),
+ type_var_docs);
});
TVM_REGISTER_SCRIPT_AS_REPR(tirx::PrimFuncNode, ReprPrintTIR);
diff --git a/src/tirx/script/printer/utils.h b/src/tirx/script/printer/utils.h
index 2c38ec59e2..b2246a667f 100644
--- a/src/tirx/script/printer/utils.h
+++ b/src/tirx/script/printer/utils.h
@@ -316,10 +316,13 @@ ExprDoc BufferDecl(const tirx::BufferVar& buffer, const
ffi::String& method,
* \param d The IRDocsifier
* \param stringify_shape_vars Variables whose first shape use must be
stringified. The set is
* passed by value so entries can be consumed as dimensions are emitted.
+ * \param stringify_compound_shape_vars Variables whose compound shape
expressions must be
+ * stringified while their bare-name uses remain direct.
* \return The ExprDoc corresponding to the buffer declaration
*/
ExprDoc BufferAttn(const tirx::BufferVar& buffer, const AccessPath& p, const
Frame& frame,
- const IRDocsifier& d, std::unordered_set<tirx::Var>
stringify_shape_vars = {});
+ const IRDocsifier& d, std::unordered_set<tirx::Var>
stringify_shape_vars = {},
+ std::unordered_set<tirx::Var> stringify_compound_shape_vars
= {});
/*!
* \brief Print the creation of a Var
diff --git a/tests/python/relax/test_tvmscript_parser.py
b/tests/python/relax/test_tvmscript_parser.py
index b91a1e4544..1e7cddb116 100644
--- a/tests/python/relax/test_tvmscript_parser.py
+++ b/tests/python/relax/test_tvmscript_parser.py
@@ -995,6 +995,8 @@ def test_call_tir_with_tir_var():
Y[vi] = X[vi]
_check(Module)
+ portable = Module.script(show_meta=True,
extra_config={"script.use_pep695": False})
+ tvm.ir.assert_structural_equal(Module, tvm.script.from_source(portable))
def test_call_tir_with_grad():
diff --git a/tests/python/relax/test_tvmscript_type_vars.py
b/tests/python/relax/test_tvmscript_type_vars.py
new file mode 100644
index 0000000000..990a9582f7
--- /dev/null
+++ b/tests/python/relax/test_tvmscript_type_vars.py
@@ -0,0 +1,68 @@
+# 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.
+
+import sys
+from typing import TypeVar
+
+import tvm
+import tvm.testing
+from tvm.script import relax as R
+
+M = TypeVar("M")
+UNUSED_GENERIC = TypeVar("UNUSED_GENERIC", bound=int)
+
+
+def test_type_vars_roundtrip():
+ @R.function(private=True)
+ def func(
+ x: R.Tensor((M, "M * 2"), "float32"),
+ ) -> R.Tensor((M, "M * 2"), "float32"):
+ return x
+
+ script = func.script()
+ if sys.version_info >= (3, 12):
+ assert script.startswith("from __future__ import annotations\n\n")
+ assert "def main[M](" in script
+ assert 'R.Tensor((M, M * 2), dtype="float32")' in script
+ typed = tvm.script.from_source(
+ """
[email protected](private=True)
+def func[M: int](x: R.Tensor((M, M * 2), "float32")):
+ return x
+"""
+ )
+ tvm.ir.assert_structural_equal(func, typed)
+ else:
+ assert "from __future__ import annotations" not in script
+ assert 'M = TypeVar("M")' in script
+ assert 'R.Tensor((M, "M * 2"), dtype="float32")' in script
+
+ portable = func.script(extra_config={"relax.use_pep695": False})
+ assert "from __future__ import annotations" not in portable
+ assert 'M = TypeVar("M")' in portable
+ assert 'R.Tensor((M, "M * 2"), dtype="float32")' in portable
+ assert "M = T.int64()" not in script
+ assert "UNUSED_GENERIC" not in script
+ assert [param.name for param in func.params] == ["x"]
+ assert not hasattr(func, "type_params")
+ assert func.attrs.get("relax.type_vars") is None
+ tvm.ir.assert_structural_equal(func, tvm.script.from_source(script))
+ tvm.ir.assert_structural_equal(func, tvm.script.from_source(portable))
+
+
+if __name__ == "__main__":
+ tvm.testing.main()
diff --git a/tests/python/tirx/test_tvmscript_type_vars.py
b/tests/python/tirx/test_tvmscript_type_vars.py
new file mode 100644
index 0000000000..c3d13f122b
--- /dev/null
+++ b/tests/python/tirx/test_tvmscript_type_vars.py
@@ -0,0 +1,67 @@
+# 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.
+
+import sys
+
+import tvm
+import tvm.testing
+
+
+def test_type_vars_roundtrip():
+ func = tvm.script.from_source(
+ """
+M = TypeVar("M")
+UNUSED = TypeVar("UNUSED")
+
[email protected]_func(private=True)
+def func(A: T.Buffer((M, M * 2), "float32")):
+ A[0, 0] = T.float32(1)
+"""
+ )
+
+ script = func.script()
+ if sys.version_info >= (3, 12):
+ assert script.startswith("from __future__ import annotations\n\n")
+ assert "def main[M](" in script
+ assert 'T.Buffer((M, M * T.int64(2)), "float32")' in script
+ typed = tvm.script.from_source(
+ """
[email protected]_func(private=True)
+def func[M: int](A: T.Buffer((M, M * 2), "float32")):
+ A[0, 0] = T.float32(1)
+"""
+ )
+ tvm.ir.assert_structural_equal(func, typed)
+ else:
+ assert "from __future__ import annotations" not in script
+ assert 'M = TypeVar("M")' in script
+
+ portable = func.script(extra_config={"script.use_pep695": False})
+ assert "from __future__ import annotations" not in portable
+ assert 'M = TypeVar("M")' in portable
+ assert 'T.Buffer((M, "M * T.int64(2)"), "float32")' in portable
+ assert "UNUSED" not in script
+ assert "M = T.int64()" not in script
+ assert len(func.params) == 1
+ assert not hasattr(func, "type_params")
+ assert func.attrs.get("tirx.type_vars") is None
+ tvm.ir.assert_structural_equal(func, tvm.script.from_source(script))
+ tvm.ir.assert_structural_equal(func, tvm.script.from_source(portable))
+
+
+if __name__ == "__main__":
+ tvm.testing.main()
diff --git a/tests/python/tvmscript/test_tvmscript_printer_doc.py
b/tests/python/tvmscript/test_tvmscript_printer_doc.py
index 18c3cec267..571f75e35a 100644
--- a/tests/python/tvmscript/test_tvmscript_printer_doc.py
+++ b/tests/python/tvmscript/test_tvmscript_printer_doc.py
@@ -478,6 +478,7 @@ def test_function_doc(args, decorators, return_type, body):
assert list(doc.decorators) == decorators
assert doc.return_type == return_type
assert list(doc.body) == body
+ assert list(doc.type_params) == []
@pytest.mark.parametrize(
diff --git
a/tests/python/tvmscript/test_tvmscript_printer_python_doc_printer.py
b/tests/python/tvmscript/test_tvmscript_printer_python_doc_printer.py
index 9cf7baa302..0fe90a1a83 100644
--- a/tests/python/tvmscript/test_tvmscript_printer_python_doc_printer.py
+++ b/tests/python/tvmscript/test_tvmscript_printer_python_doc_printer.py
@@ -804,6 +804,26 @@ def test_print_function_doc(args, decorators, body,
return_type, expected):
assert to_python_script(doc) == format_script(expected) # test
+def test_print_function_doc_with_type_params():
+ doc = FunctionDoc(
+ IdDoc("func"),
+ [],
+ [],
+ None,
+ [ReturnDoc(IdDoc("T"))],
+ type_params=[
+ IdDoc("T"),
+ AssignDoc(IdDoc("U"), rhs=None, annotation=IdDoc("int")),
+ ],
+ )
+ assert to_python_script(doc) == format_script(
+ """
+ def func[T, U: int]():
+ return T
+ """
+ )
+
+
def get_func_doc_for_class(name):
args = [
AssignDoc(IdDoc("x"), rhs=None, annotation=IdDoc("int")),