chore: import upstream snapshot with attribution
Lint / lint (push) Has been cancelled
CI / MacOS (push) Has been cancelled
CI / Windows (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:36:25 +08:00
commit 26446540fa
3151 changed files with 974126 additions and 0 deletions
+238
View File
@@ -0,0 +1,238 @@
# 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.
"""
TVMScript public namespace.
Dialect resolution mechanism
----------------------------
``tvm.script`` is a virtual namespace: dialect names like ``tirx`` and
``relax`` are not bound as static attributes here. Instead:
- ``register_dialect(name, module_path)`` writes an entry to
``_DIALECT_REGISTRY: dict[str, str]``. Each in-tree dialect's
``__init__.py`` calls this on import (e.g., ``tvm.tirx.__init__.py``
calls ``tvm.script.register_dialect("tirx", "tvm.tirx.script")``).
Out-of-tree dialects can register themselves the same way.
- ``__getattr__(name)`` (PEP 562) fires on missing attribute access.
If ``name`` is in ``_DIALECT_REGISTRY``, the listed module is imported
and cached as a normal module attribute. Subsequent accesses
skip ``__getattr__`` (cached in ``globals()``).
- Subpackages ``tvm.script.parser``, ``tvm.script.ir_builder``, etc.
each define their own ``__getattr__`` that consults the SAME
``_DIALECT_REGISTRY`` and appends their suffix. So
``tvm.script.parser.tirx`` resolves to ``tvm.tirx.script.parser`` via
the dialect registry + ``.parser`` suffix.
- For deep statement-form imports like
``from tvm.script.parser.tirx.entry import ObjectProxy``, PEP 562's
``__getattr__`` is not enough — it only handles one-level
``from X import Y``. A ``sys.meta_path`` finder (see
``_DialectRedirectFinder``) intercepts the import machinery to
register the real module under the legacy name in ``sys.modules``,
so subsequent attribute walks resolve correctly.
Each dialect's ``tvm.<dialect>.script`` package MUST expose ``parser``,
``ir_builder``, and (where applicable) ``printer`` as submodules. This
convention is what makes the suffix-append redirect work uniformly.
IR is foundational (script depends on ir) and is NOT a dialect; its
script handlers live in the shared core, not via this registry.
Bootstrap order
---------------
``python/tvm/__init__.py`` imports ``tvm.script`` BEFORE importing any
dialect package (``tvm.tirx``, ``tvm.relax``, …). This guarantees that
``tvm.script.register_dialect`` is reachable the moment a dialect's own
``__init__.py`` runs and calls it. The ``tvm.script`` module itself
stays dialect-agnostic at load time (no dialect submodules are eagerly
imported here), so there is no circular dependency.
"""
import importlib
import importlib.util
import sys
from typing import Any
_DIALECT_REGISTRY: dict[str, str] = {}
# Subpackages of `tvm.script` whose per-dialect children are redirected to a
# matching subpackage under `tvm.<dialect>.script`. The values are the
# subpackage name on the dialect side (e.g. `tvm.<dialect>.script.parser`).
_REDIRECTED_SUBPACKAGES = {
"tvm.script.parser": "parser",
"tvm.script.ir_builder": "builder",
}
def register_dialect(name: str, module_path: str) -> None:
"""Register a dialect's script package path.
Writes ``name -> module_path`` into ``_DIALECT_REGISTRY``. After
registration, ``tvm.script.<name>`` resolves to ``module_path`` via
``__getattr__``, and ``tvm.script.parser.<name>`` / ``tvm.script.ir_builder.<name>``
resolve to ``module_path + ".parser"`` / ``module_path + ".builder"`` etc.
via each subpackage's own ``__getattr__``. Deep statement-form imports
(e.g., ``from tvm.script.parser.<name>.entry import X``) are handled
by ``_DialectRedirectFinder`` on ``sys.meta_path``.
This function is idempotent — re-registering the same name with the same
path is harmless.
Each in-tree dialect calls this from its own ``__init__.py``::
import tvm.script
tvm.script.register_dialect("tirx", "tvm.tirx.script")
Out-of-tree dialects do the same in their own package init without
editing any in-tree file.
Parameters
----------
name : str
The short name exposed under ``tvm.script.<name>`` (e.g. ``"tirx"``).
module_path : str
The full dotted module path of the dialect's script package, e.g.
``"tvm.tirx.script"``. That package must expose ``parser`` and
``ir_builder`` as submodules (and ``printer`` where applicable) so
that the suffix-append redirect works uniformly.
"""
_DIALECT_REGISTRY[name] = module_path
def _redirect_target(fullname: str) -> str | None:
"""Return the target module path for a redirected ``tvm.script[...]`` name.
Returns ``None`` if ``fullname`` is not a redirected name.
"""
if fullname.startswith("tvm.script."):
# tvm.script.<dialect>[.subpath]
rest = fullname[len("tvm.script.") :]
head, _, tail = rest.partition(".")
if head in _DIALECT_REGISTRY and "." not in head:
target = _DIALECT_REGISTRY[head]
return f"{target}.{tail}" if tail else target
# tvm.script.parser.<dialect>[.subpath] / tvm.script.ir_builder.<dialect>[.subpath]
for prefix, sub in _REDIRECTED_SUBPACKAGES.items():
if fullname == prefix or not fullname.startswith(prefix + "."):
continue
rest = fullname[len(prefix) + 1 :]
head, _, tail = rest.partition(".")
if head in _DIALECT_REGISTRY:
target = f"{_DIALECT_REGISTRY[head]}.{sub}"
return f"{target}.{tail}" if tail else target
return None
class _DialectRedirectFinder:
"""``sys.meta_path`` finder that redirects ``tvm.script.<dialect>`` import paths.
PEP 562 ``__getattr__`` only handles one-level attribute lookups
(``from tvm.script import tirx``). It cannot intercept deep
statement-form imports such as::
from tvm.script.parser.tirx.entry import ObjectProxy
import tvm.script.ir_builder.relax.ir
This finder is installed on ``sys.meta_path`` to cover those cases.
When the import machinery asks for a module whose full name starts with
``tvm.script.<dialect>`` (or ``tvm.script.parser.<dialect>``, etc.) and
that dialect is in ``_DIALECT_REGISTRY``, :meth:`find_spec` imports the
real target module (e.g. ``tvm.tirx.script.parser.entry``) and returns an
alias spec whose loader hands back that module, so the import machinery
registers it in ``sys.modules`` under the legacy name and all subsequent
imports and attribute walks resolve without going through the redirect
again.
"""
@classmethod
def find_spec(cls, fullname, path, target=None):
redirected = _redirect_target(fullname)
if redirected is None:
return None
# Resolve the target module and return an alias spec for it. Do NOT
# pre-register ``sys.modules[fullname]`` here: if ``fullname`` is in
# ``sys.modules`` when find_spec returns, ``_bootstrap._find_spec``
# discards the returned spec in favor of ``module.__spec__`` — the
# target's original SourceFileLoader spec — and ``_load_unlocked``
# then RE-EXECUTES the target module and replaces its canonical
# ``sys.modules`` entry with the duplicate. The machinery registers
# the alias under ``fullname`` itself when loading the spec below.
module = importlib.import_module(redirected)
return importlib.util.spec_from_loader(fullname, _AliasLoader(module))
class _AliasLoader:
"""Loader that returns an already-resolved module for an alias spec."""
def __init__(self, module):
self._module = module
# ``module_from_spec`` unconditionally stamps the alias spec onto the
# module returned by ``create_module``; capture the canonical values
# so ``exec_module`` can restore them.
self._spec = getattr(module, "__spec__", None)
self._loader = getattr(module, "__loader__", None)
def create_module(self, spec):
return self._module
def exec_module(self, module):
# Module is already populated by the redirect target; just restore
# the canonical ``__spec__``/``__loader__`` that the import machinery
# overwrote with the alias spec (a stale alias ``__spec__.parent``
# breaks relative imports inside the module).
if self._spec is not None:
module.__spec__ = self._spec
if self._loader is not None:
module.__loader__ = self._loader
# Install the redirect finder once. Re-importing tvm.script (e.g. during a
# pytest reload) must not stack duplicates.
if not any(isinstance(f, _DialectRedirectFinder) for f in sys.meta_path):
sys.meta_path.append(_DialectRedirectFinder())
def __getattr__(name: str) -> Any:
if name in _DIALECT_REGISTRY:
module = importlib.import_module(_DIALECT_REGISTRY[name])
globals()[name] = module
return module
if name == "ir":
# IR is foundational — its parser is a real submodule under
# tvm.script.parser.ir, exposed here as `tvm.script.ir` for the
# legacy `from tvm.script import ir as I` pattern.
ir_parser = importlib.import_module("tvm.script.parser.ir")
globals()["ir"] = ir_parser
return ir_parser
if name in ("from_source", "parse"):
from .parser._core import parse # pylint: disable=import-outside-toplevel
globals()["from_source"] = parse
globals()["parse"] = parse
return parse
if name == "ir_module":
# ir_module lives in the IR parser at tvm.script.parser.ir; the IR
# layer is foundational, so we resolve it directly rather than via
# the dialect registry.
ir_parser = importlib.import_module("tvm.script.parser.ir")
ir_module_value = ir_parser.ir_module
globals()["ir_module"] = ir_module_value
return ir_module_value
raise AttributeError(f"module 'tvm.script' has no attribute {name!r}")
+37
View File
@@ -0,0 +1,37 @@
# 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.
# 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.
"""FFI APIs for tvm.script"""
import tvm_ffi
tvm_ffi.init_ffi_api("script", __name__)
+311
View File
@@ -0,0 +1,311 @@
# 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.
# ruff: noqa: F821, RUF012
"""Highlight printed TVM script."""
import functools
import os
import shutil
import subprocess
import sys
import warnings
from typing import Any, Union
def cprint(
printable: Any | str,
style: str | None = None,
black_format: bool = False,
) -> None:
"""Print TVMScript string with Pygments highlight and Black auto-formatting.
Parameters
----------
printable : Union[IRModule, PrimFunc, str]
The TVMScript to be printed
style : str, optional
Pygmentize printing style, auto-detected if None.
black_format: bool
If true, use the formatter Black to format the TVMScript
Notes
-----
The style parameter follows the Pygments style names or Style objects. Three
built-in styles are extended: "light", "dark" and "ansi". By default, "light"
will be used for notebook environment and terminal style will be "ansi" for
better style consistency. As an fallback when the optional Pygment library is
not installed, plain text will be printed with a one-time warning to suggest
installing the Pygment library. Other Pygment styles can be found in
https://pygments.org/styles/
The default pygmentize style can also be set with the environment
variable "TVM_PYGMENTIZE_STYLE".
"""
if hasattr(printable, "script") and callable(getattr(printable, "script")):
printable = printable.script()
elif not isinstance(printable, str):
raise TypeError(
f"Only can print strings or objects with `script` method, but got: {type(printable)}"
)
if black_format:
printable = _format(printable)
is_in_notebook = "ipykernel" in sys.modules # in notebook env (support html display).
style = _get_pygments_style(style, is_in_notebook)
if style is None:
print(printable)
return
# pylint: disable=import-outside-toplevel
from pygments import highlight
from pygments.formatters import HtmlFormatter, Terminal256Formatter
from pygments.lexers.python import Python3Lexer
if is_in_notebook:
from IPython import display # pylint: disable=import-outside-toplevel
formatter = HtmlFormatter(style=style)
formatter.noclasses = True # inline styles
html = highlight(printable, Python3Lexer(), formatter)
display.display(display.HTML(html))
else:
print(highlight(printable, Python3Lexer(), Terminal256Formatter(style=style)))
@functools.lru_cache
def _get_formatter(formatter: str | None = None):
def get_ruff_formatter():
if shutil.which("ruff") is None:
return None
def formatter(code_str):
proc = subprocess.Popen(
["ruff", "format", "--stdin-filename=TVMScript"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
encoding="utf-8",
)
stdout, _stderr = proc.communicate(code_str)
return stdout
return formatter
def get_black_formatter():
try:
# pylint: disable=import-outside-toplevel
import black
except ImportError:
return None
def formatter(code_str):
return black.format_str(code_str, mode=black.FileMode())
return formatter
def get_fallback_formatter():
def formatter(code_str):
with warnings.catch_warnings():
warnings.simplefilter("once", UserWarning)
ruff_install_cmd = sys.executable + " -m pip install ruff"
black_install_cmd = (
sys.executable + ' -m pip install "black==22.3.0" --upgrade --user'
)
warnings.warn(
f"Neither the 'ruff' formatter nor the 'black' formatter is available. "
f"To print formatted TVM script, please a formatter. \n"
f"To install ruff: {ruff_install_cmd}\n"
f"To install black: {black_install_cmd}",
category=UserWarning,
)
return code_str
return formatter
# formatter = "black"
if formatter is None:
options = [get_ruff_formatter, get_black_formatter]
elif formatter == "ruff":
options = [get_ruff_formatter]
elif formatter == "black":
options = [get_black_formatter]
else:
raise ValueError(f"Unknown formatter: {formatter}")
for option in options:
func = option()
if func is not None:
return func
return get_fallback_formatter()
def _format(code_str: str, formatter: str | None = None) -> str:
"""Format a code string using Black.
Parameters
----------
code_str: str
The string containing Python/TVMScript code to format
formatter: Optional[str]
The formatter to use. Can specify `ruff`, `black`, or
auto-select by passing `None`.
Returns
-------
formatted: str
The formatted Python/TVMScript code
"""
return _get_formatter(formatter)(code_str)
def _get_pygments_style(
style: str | None, is_in_notebook: bool
) -> Union["pygments.style.Style", str] | None:
"""Select a pygments style to use
Parameters
----------
style: str
The style specifier to use. If None, auto-select a style.
is_in_notebook: bool
Whether python is currently running in a jupyter notebook.
Used for automatic selection.
Returns
-------
style: Optional[Union['pygments.style.Style',str]]
If pygments is installed, the style object or string, suitable
for use as the "style" argument to pygments formatters. If
pygments is not installed, returns None.
"""
try:
# pylint: disable=import-outside-toplevel
import pygments
from packaging import version
from pygments.style import Style
from pygments.token import Comment, Keyword, Name, Number, Operator, String
if version.parse(pygments.__version__) < version.parse("2.4.0"):
raise ImportError("Required Pygments version >= 2.4.0 but got " + pygments.__version__)
except ImportError as err:
if err.name == "packaging":
name = "packaging"
elif err.name == "pygments":
name = "Pygments>=2.4.0"
else:
raise ValueError(f'Package "{err.name}" should not be used')
with warnings.catch_warnings():
warnings.simplefilter("once", UserWarning)
install_cmd = sys.executable + f' -m pip install "{name}" --upgrade --user'
warnings.warn(
str(err)
+ "\n"
+ f"To print highlighted TVM script, please install {name}:\n"
+ install_cmd,
category=UserWarning,
)
return None
class JupyterLight(Style):
"""A Jupyter-Notebook-like Pygments style configuration (aka. "light")"""
background_color = ""
styles = {
Keyword: "bold #008000",
Keyword.Type: "nobold #008000",
Name.Function: "#0000FF",
Name.Class: "bold #0000FF",
Name.Decorator: "#AA22FF",
String: "#BA2121",
Number: "#008000",
Operator: "bold #AA22FF",
Operator.Word: "bold #008000",
Comment: "italic #007979",
}
class VSCDark(Style):
"""A VSCode-Dark-like Pygments style configuration (aka. "dark")"""
background_color = ""
styles = {
Keyword: "bold #c586c0",
Keyword.Type: "#82aaff",
Keyword.Namespace: "#4ec9b0",
Name.Class: "bold #569cd6",
Name.Function: "bold #dcdcaa",
Name.Decorator: "italic #fe4ef3",
String: "#ce9178",
Number: "#b5cea8",
Operator: "#bbbbbb",
Operator.Word: "#569cd6",
Comment: "italic #6a9956",
}
class AnsiTerminalDefault(Style):
"""The default style for terminal display with ANSI colors (aka. "ansi")"""
background_color = ""
styles = {
Keyword: "bold ansigreen",
Keyword.Type: "nobold ansigreen",
Name.Class: "bold ansiblue",
Name.Function: "bold ansiblue",
Name.Decorator: "italic ansibrightmagenta",
String: "ansiyellow",
Number: "ansibrightgreen",
Operator: "bold ansimagenta",
Operator.Word: "bold ansigreen",
Comment: "italic ansibrightblack",
}
if style == "light":
return JupyterLight
elif style == "dark":
return VSCDark
elif style == "ansi":
return AnsiTerminalDefault
if style is not None:
return style
style_from_environment = os.environ.get("TVM_PYGMENTIZE_STYLE", "").strip()
if style_from_environment:
return style_from_environment
if is_in_notebook:
return JupyterLight
return AnsiTerminalDefault
+48
View File
@@ -0,0 +1,48 @@
# 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 ir_builder subpackage of TVMScript.
Per-dialect builder submodules (``tvm.script.ir_builder.tirx``, etc.) are
resolved lazily via :data:`tvm.script._DIALECT_REGISTRY`. When a dialect
is accessed (e.g. ``tvm.script.ir_builder.tirx``), this subpackage's
``__getattr__`` looks up the dialect in ``_DIALECT_REGISTRY`` and imports
``<dialect_module_path>.builder`` (e.g. ``tvm.tirx.script.builder``),
caching the result so subsequent accesses skip ``__getattr__``.
The IR layer is foundational and is NOT registered as a dialect — its
builder lives as a real submodule ``tvm.script.ir_builder.ir``.
See :mod:`tvm.script` for a full description of the dialect resolution
mechanism, including the ``_DialectRedirectFinder`` that handles
deep statement-form imports.
"""
import importlib
from typing import Any
from .base import IRBuilder
def __getattr__(name: str) -> Any:
# Lazy import to avoid loading tvm.script during dialect bootstrap.
from tvm.script import _DIALECT_REGISTRY # pylint: disable=import-outside-toplevel
if name in _DIALECT_REGISTRY:
module = importlib.import_module(f"{_DIALECT_REGISTRY[name]}.builder")
globals()[name] = module
return module
raise AttributeError(f"module 'tvm.script.ir_builder' has no attribute {name!r}")
+21
View File
@@ -0,0 +1,21 @@
# 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.
"""FFI APIs for tvm.script.ir_builder"""
import tvm_ffi
tvm_ffi.init_ffi_api("script.ir_builder", __name__) # pylint: disable=protected-access
+202
View File
@@ -0,0 +1,202 @@
# 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.
"""A generic IRBuilder across the TVM stack"""
from collections.abc import Callable
from typing import Any
from tvm_ffi import register_object as _register_object
from tvm.runtime import Object as _Object
from . import _ffi_api
@_register_object("script.ir_builder.IRBuilderFrame")
class IRBuilderFrame(_Object):
"""A stack frame of the IRBuilder used to keep track of the current scope.
Furthermore, the information stored in each stack frame can be useful for context-dependent
IR construction.
Examples
--------
The `T.match_buffer` below instead an element in the buffer map of `PrimFuncFrame`:
.. code-block:: python
from tvm.script.ir_builder import tirx as T
from tvm.script.ir_builder import IRBuilder
with IRBuilder() as builder:
with T.prim_func(...): # pushes a PrimFuncFrame (subclass of IRBuilderFrame)
# to `builder`'s stack of frames
buffer = T.match_buffer(...)
The `T.match_buffer` below instead generates `MatchBufferRegion` in a TIR block:
.. code-block:: python
from tvm.script.ir_builder import tirx as T
from tvm.script.ir_builder import IRBuilder
with IRBuilder() as builder:
with T.prim_func(...): # pushes a PrimFuncFrame (subclass of IRBuilderFrame)
# to `builder`'s stack of frames
with T.sblock(...): # pushes a BlockFrame (subclass of IRBuilderFrame)
# to `builder`'s stack of frames
buffer = T.match_buffer(...)
"""
def __enter__(self) -> "IRBuilderFrame":
_ffi_api.IRBuilderFrameEnter(self) # type: ignore[attr-defined] # pylint: disable=no-member
return self
def __exit__(self, exc_type, exc_value, trace) -> None: # pylint: disable=unused-argument
if exc_type is None and exc_value is None:
# Do not execute `FrameExit` if the with scope exits because of exceptions
_ffi_api.IRBuilderFrameExit(self) # type: ignore[attr-defined] # pylint: disable=no-member
def add_callback(self, callback: Callable[[], None]) -> None:
"""Add a callback method invoked when exiting the with-scope.
Parameters
----------
callback : Callable[[], None]
The callback method to be invoked.
"""
_ffi_api.IRBuilderFrameAddCallback( # type: ignore[attr-defined] # pylint: disable=no-member
self, callback
)
@_register_object("script.ir_builder.IRBuilder")
class IRBuilder(_Object):
"""A dialect-agnostic IRBuilder that constructs any IR of TVM.
Examples
--------
An idiomatic use of this class is to put this inside the with-scope,
call dialect-specific methods accordingly. Upon exiting the scope.
.. code-block:: python
from tvm.script.ir_builder import tirx as T
from tvm.script.ir_builder import IRBuilder
with IRBuilder() as builder:
with T.prim_func(...): # pushes a PrimFuncFrame (subclass of IRBuilderFrame)
# to `builder`'s stack of frames
buffer = T.match_buffer(...)
return builder.get() # returns the constructed IR, i.e. tirx.PrimFunc
"""
def __init__(self) -> None:
"""Construct an IRBuilder."""
self.__init_handle_by_constructor__(
_ffi_api.IRBuilder # type: ignore[attr-defined] # pylint: disable=no-member
)
def __enter__(self) -> "IRBuilder":
"""Enter the with-scope for IRBuilder, which allows the IRBuilder to be discoverable
using `IRBuilder.current()`.
Examples
--------
.. code-block:: python
from tvm.script.ir_builder import IRBuilder
with IRBuilder() as builder:
assert IRBuilder.current() == builder
"""
_ffi_api.IRBuilderEnter(self) # type: ignore[attr-defined] # pylint: disable=no-member
return self
def __exit__(self, ptype, value, trace) -> None: # pylint: disable=unused-argument
_ffi_api.IRBuilderExit(self) # type: ignore[attr-defined] # pylint: disable=no-member
@staticmethod
def current() -> "IRBuilder":
"""Get the current IRBuilder put in the with-scope.
Returns
-------
builder : IRBuilder
The current IRBuilder.
"""
return _ffi_api.IRBuilderCurrent() # type: ignore[attr-defined] # pylint: disable=no-member
@staticmethod
def is_in_scope() -> bool:
"""See if the current thread-local scope has an IRBuilder.
Returns
-------
bool
Whether the current thread-local scope has an IRBuilder
"""
return _ffi_api.IRBuilderIsInScope() # type: ignore[attr-defined] # pylint: disable=no-member
def get(self) -> _Object:
"""Get the constructed IR."""
return _ffi_api.IRBuilderGet(self) # type: ignore[attr-defined] # pylint: disable=no-member
@staticmethod
def name(s: str, v: Any) -> Any:
"""Set the name of an object.
Parameters
----------
s : str
The name of the object.
v : Any
The object to name.
Returns
-------
v : Any
The same object with the name set.
"""
return _ffi_api.IRBuilderName(s, v) # type: ignore[attr-defined] # pylint: disable=no-member
@staticmethod
def name_many( # pylint: disable=invalid-name
s: list[str],
vs: list[Any],
) -> list[Any]:
"""Set the name of a list of objects.
Parameters
----------
s : List[str]
The names of the objects.
vs : List[Any]
The objects to name.
Returns
-------
vs : List[Any]
The same objects with the names set.
"""
assert len(s) == len(vs)
return [IRBuilder.name(i, v) for i, v in zip(s, vs)]
@@ -0,0 +1,33 @@
# isort: skip_file
# 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.
"""Package tvm.script.ir_builder.ir"""
from .frame import IRModuleFrame
from .ir import (
decl_function,
def_function,
ir_module,
module_attrs,
module_get_attr,
module_set_attr,
module_global_infos,
lookup_vdevice,
lookup_name,
vdevice,
dummy_global_info,
)
@@ -0,0 +1,21 @@
# 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.
"""FFI APIs"""
import tvm_ffi
tvm_ffi.init_ffi_api("script.ir_builder.ir", __name__) # pylint: disable=protected-access
+25
View File
@@ -0,0 +1,25 @@
# 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.
"""Package tvm.script.ir_builder.ir.frame"""
from tvm_ffi import register_object as _register_object
from ..base import IRBuilderFrame
@_register_object("script.ir_builder.IRModuleFrame")
class IRModuleFrame(IRBuilderFrame): ...
+192
View File
@@ -0,0 +1,192 @@
# 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.
"""Package tvm.script.ir_builder.ir.ir"""
from tvm.ir import BaseFunc, DummyGlobalInfo, GlobalInfo, GlobalVar, VDevice
from tvm.runtime import Object as tvm_Object
from . import _ffi_api
from .frame import IRModuleFrame
def ir_module() -> IRModuleFrame:
"""Start a ir_module frame.
Returns
-------
frame: IRModuleFrame
The constructed frame.
"""
return _ffi_api.IRModule() # type: ignore[attr-defined] # pylint: disable=no-member
def decl_function(func_name: str, func_signature: BaseFunc) -> GlobalVar:
"""Declare a Function without given the specific function implementation.
Parameters
----------
func_name : str
The function unique name.
func_signature: BaseFunc
A Function w/o body, which used to specify the function signature
(i.e. func params and func return type/shape).
Note
----
It is usually used in cross-function call. And we can specify the function by `DefFunction`
Returns
-------
gv : GlobalVar
The corresponding GlobalVar.
"""
if not isinstance(func_signature, BaseFunc):
raise ValueError(
"decl_function expects an instance of BaseFunc, "
f"but {func_signature} is of type {type(func_signature)}"
)
return _ffi_api.DeclFunction( # type: ignore[attr-defined] # pylint: disable=no-member
func_name, func_signature
)
def def_function(func_name: str, func: BaseFunc) -> None:
"""Define the function which is declared before.
Parameters
----------
func_name : str
The function unique name.
func: BaseFunc
The given function implementation
"""
return _ffi_api.DefFunction(func_name, func) # type: ignore[attr-defined] # pylint: disable=no-member
def module_attrs(attrs: dict[str, tvm_Object], allow_overwrite=False) -> None:
"""Specify the attrs of the ir_module frame.
Parameters
----------
attrs: Dict[str, Object]
The module attrs.
allow_overwrite: bool
Whether allow overwrite the existing attrs.
"""
return _ffi_api.ModuleAttrs(attrs, allow_overwrite) # type: ignore[attr-defined] # pylint: disable=no-member
def module_get_attr(attr_key: str) -> tvm_Object | None:
"""Get the specified attr of the ir_module frame.
Parameters
----------
attr_key: str
The key of the attr to be retrieved.
Returns
-------
attr: Optional[Object]
The specified module attr or None if not found.
"""
return _ffi_api.ModuleGetAttr(attr_key) # type: ignore[attr-defined] # pylint: disable=no-member
def module_set_attr(
attr_key: str, attr_value: tvm_Object | None, allow_overwrite: bool = False
) -> None:
"""Set the specified attr of the ir_module frame.
Parameters
----------
attr_key: str
The key of the attr to be set.
attr_value: Optional[Object]
The value of the attr to be set.
allow_overwrite: bool
Whether allow overwrite the existing attr.
"""
return _ffi_api.ModuleSetAttr(attr_key, attr_value, allow_overwrite) # type: ignore[attr-defined] # pylint: disable=no-member
def module_global_infos(global_infos: dict[str, list[GlobalInfo]]) -> None:
"""Specify the global infos of the ir_module frame.
Parameters
----------
global_infos: Dict[str, List[GlobalInfo]]
The module global infos.
"""
return _ffi_api.ModuleGlobalInfos(global_infos) # type: ignore[attr-defined] # pylint: disable=no-member
############################### GlobalInfo ###############################
def dummy_global_info() -> DummyGlobalInfo:
"""Create a dummy global info expression.
Returns
-------
res : DummyGlobalInfo
The result dummy global info.
"""
return DummyGlobalInfo() # type: ignore[attr-defined] # pylint: disable=no-member
def vdevice(target=None, vdevice_id: int = 0, memory_scope: str = "global") -> VDevice:
"""Create a virtual device global info.
Parameters
----------
target
The target.
vdevice_id: int
The virtual device index.
memory_scope: str
The memory scope, default is "global"
Returns
-------
res : VDevice
The result virtual device.
"""
return VDevice(target, vdevice_id, memory_scope) # type: ignore[attr-defined] # pylint: disable=no-member
def lookup_vdevice(target_kind: str | None = None, device_index: int = -1) -> VDevice:
"""Retrieve a virtual device from the globalinfo vdevice list.
Parameters
----------
target_kind: str
The target device kind, for example 'llvm' or 'cuda'.
device_index: int
The virtual device index.
Returns
-------
res : VDevice
The result virtual device.
"""
return _ffi_api.LookupVDevice(target_kind, device_index) # type: ignore[attr-defined] # pylint: disable=no-member
def lookup_name(name: str) -> bool:
"""Check if a global variable with the given name exists.
Parameters
----------
name: str
The name of the global variable.
Returns
-------
res : bool
True if the global variable exists, False otherwise.
"""
return _ffi_api.LookupName(name) # type: ignore[attr-defined] # pylint: disable=no-member
+51
View File
@@ -0,0 +1,51 @@
# 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 parser subpackage of TVMScript.
Per-dialect parser submodules (``tvm.script.parser.tirx``, etc.) are
resolved lazily via :data:`tvm.script._DIALECT_REGISTRY`. When a dialect
is accessed (e.g. ``tvm.script.parser.tirx``), this subpackage's
``__getattr__`` looks up the dialect in ``_DIALECT_REGISTRY`` and imports
``<dialect_module_path>.parser`` (e.g. ``tvm.tirx.script.parser``),
caching the result so subsequent accesses skip ``__getattr__``.
The IR layer is foundational and is NOT registered as a dialect — its
parser lives as a real submodule ``tvm.script.parser.ir``, with
``ir_module`` re-exported at this level for convenience.
See :mod:`tvm.script` for a full description of the dialect resolution
mechanism, including the ``_DialectRedirectFinder`` that handles
deep statement-form imports.
"""
import importlib
from typing import Any
from . import _core, ir, tirx
from ._core import parse
from .ir import ir_module
def __getattr__(name: str) -> Any:
# Lazy import to avoid loading tvm.script during dialect bootstrap.
from tvm.script import _DIALECT_REGISTRY # pylint: disable=import-outside-toplevel
if name in _DIALECT_REGISTRY:
module = importlib.import_module(f"{_DIALECT_REGISTRY[name]}.parser")
globals()[name] = module
return module
raise AttributeError(f"module 'tvm.script.parser' has no attribute {name!r}")
+24
View File
@@ -0,0 +1,24 @@
# 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 Licens.
# ruff: noqa: F401
"""The core parser infra"""
# pylint: disable=unused-import
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
+20
View File
@@ -0,0 +1,20 @@
# isort: skip_file
# 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 core parser infra"""
from . import diagnostics, dispatch, doc, doc_core, entry, evaluator, parser, utils
@@ -0,0 +1,305 @@
# 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.
# ruff: noqa: E741
"""TVM Script Parser Source and diagnostics"""
import inspect
import sys
from tvm.error import DiagnosticError
from . import doc
class Source:
"""Source code class for TVMScript.
It is constructed by source code str or doc AST tree.
Parameters
----------
source_name : str
The filename of the file where the source code locates.
start_line : int
The first line number of the source code.
start_column : int
The first column number of the first line of the source code.
source : str
The source code str of source code.
full_source : str
The complete source code of the file where the source code locates.
"""
source_name: str
start_line: int
start_column: int
source: str
full_source: str
def __init__(self, program: str | doc.AST):
if isinstance(program, str):
self.source_name = "<str>"
self.start_line = 1
self.start_column = 0
self.source = program
self.full_source = program
return
self.source_name = inspect.getsourcefile(program) # type: ignore
lines, self.start_line = getsourcelines(program) # type: ignore
if lines:
self.start_column = len(lines[0]) - len(lines[0].lstrip())
else:
self.start_column = 0
if self.start_column and lines:
self.source = "\n".join([l[self.start_column :].rstrip() for l in lines])
else:
self.source = "".join(lines)
try:
# It will cause a problem when running in Jupyter Notebook.
# `mod` will be <module '__main__'>, which is a built-in module
# and `getsource` will throw a TypeError
mod = inspect.getmodule(program)
if mod:
self.full_source = inspect.getsource(mod)
else:
self.full_source = self.source
except TypeError:
# It's a work around for Jupyter problem.
# Since `findsource` is an internal API of inspect, we just use it
# as a fallback method.
src, _ = inspect.findsource(program) # type: ignore
self.full_source = "".join(src)
def as_ast(self) -> doc.AST:
"""Parse the source code into AST.
Returns
-------
res : doc.AST
The AST of source code.
"""
return doc.parse(self.source)
_getfile = inspect.getfile # pylint: disable=invalid-name
_findsource = inspect.findsource # pylint: disable=invalid-name
def _patched_inspect_getfile(obj):
"""Work out which source or compiled file an object was defined in."""
if not inspect.isclass(obj):
return _getfile(obj)
mod = getattr(obj, "__module__", None)
if mod is not None:
file = getattr(sys.modules[mod], "__file__", None)
if file is not None:
return file
for _, member in inspect.getmembers(obj):
if inspect.isfunction(member):
if obj.__qualname__ + "." + member.__name__ == member.__qualname__:
return inspect.getfile(member)
raise TypeError(f"Source for {obj!r} not found")
def findsource(obj):
"""Return the entire source file and starting line number for an object."""
import linecache # pylint: disable=import-outside-toplevel
if not inspect.isclass(obj):
return _findsource(obj)
file = inspect.getsourcefile(obj)
if file:
linecache.checkcache(file)
else:
file = inspect.getfile(obj)
if not (file.startswith("<") and file.endswith(">")):
raise OSError("source code not available")
module = inspect.getmodule(obj, file)
if module:
lines = linecache.getlines(file, module.__dict__)
else:
lines = linecache.getlines(file)
if not lines:
raise OSError("could not get source code")
qual_names = obj.__qualname__.replace(".<locals>", "<locals>").split(".")
in_comment = 0
scope_stack = []
indent_info = {}
for i, line in enumerate(lines):
n_comment = line.count('"""')
if n_comment:
# update multi-line comments status
in_comment = in_comment ^ (n_comment & 1)
continue
if in_comment:
# skip lines within multi-line comments
continue
indent = len(line) - len(line.lstrip())
tokens = line.split()
if len(tokens) > 1:
name = None
if tokens[0] == "def":
name = tokens[1].split(":")[0].split("(")[0] + "<locals>"
elif tokens[0] == "class":
name = tokens[1].split(":")[0].split("(")[0]
# pop scope if we are less indented
while scope_stack and indent_info[scope_stack[-1]] >= indent:
scope_stack.pop()
if name:
scope_stack.append(name)
indent_info[name] = indent
if scope_stack == qual_names:
return lines, i
raise OSError("could not find class definition")
def getsourcelines(obj):
"""Extract the block of code at the top of the given list of lines."""
obj = inspect.unwrap(obj)
lines, l_num = findsource(obj)
return inspect.getblock(lines[l_num:]), l_num + 1
inspect.getfile = _patched_inspect_getfile
def _format_source_snippet(
source_lines: list,
lineno: int,
col_offset: int,
end_lineno: int,
end_col_offset: int,
) -> str:
"""Format a source code snippet with a column/span marker.
Renders every source line spanned by the diagnostic (``lineno`` through
``end_lineno``, inclusive) with a per-line gutter, followed by a caret
underline covering the offending span. For a single-line span
(``end_lineno == lineno``) only the columns ``col_offset`` (inclusive)
through ``end_col_offset`` (exclusive) are underlined; for a multi-line
span the underline covers the start column to end-of-line on the first
line, the full text of interior lines, and the start of the final line up
to ``end_col_offset``.
Parameters
----------
source_lines : list of str
Lines of the source code.
lineno : int
1-based starting line number in the source.
col_offset : int
1-based starting column (inclusive) on ``lineno``.
end_lineno : int
1-based ending line number in the source (>= ``lineno``).
end_col_offset : int
1-based ending column (exclusive) on ``end_lineno``.
Returns
-------
snippet : str
Formatted source snippet with caret-marker line(s).
"""
if end_lineno < lineno:
end_lineno = lineno
# Determine the gutter width so that all line numbers line up.
header_width = len(f" {end_lineno} ")
no_line_header = " " * header_width
parts = [f"{no_line_header}| "]
for cur_lineno in range(lineno, end_lineno + 1):
idx = cur_lineno - 1
if not 0 <= idx < len(source_lines):
continue
line_text = source_lines[idx].rstrip("\n")
line_header = f" {cur_lineno} ".rjust(header_width)
# Compute the underline span [start_col, stop_col) for this line.
start_col = col_offset if cur_lineno == lineno else 1
stop_col = end_col_offset if cur_lineno == end_lineno else len(line_text) + 1
marker = ""
for i in range(1, len(line_text) + 1):
if start_col <= i < stop_col:
marker += "^"
else:
marker += " "
parts.append(f"{line_header}| {line_text}")
parts.append(f"{no_line_header}| {marker}")
if len(parts) == 1:
return ""
return "\n".join(parts)
class Diagnostics:
"""Diagnostics class for error reporting in parser.
Formats parse errors with source location context and raises directly,
without going through DiagnosticContext.
Parameters
----------
source : Source
The source code.
"""
source: Source
def __init__(self, source: Source):
self.source = source
def error(self, node: doc.AST, message: str) -> None:
"""Emit a diagnostic error by raising with source location context.
Parameters
----------
node : doc.AST
The node with diagnostic error.
message : str
The diagnostic message.
"""
lineno = getattr(node, "lineno", 1)
col_offset = getattr(node, "col_offset", self.source.start_column)
end_lineno = getattr(node, "end_lineno", lineno)
end_col_offset = getattr(node, "end_col_offset", col_offset)
lineno += self.source.start_line - 1
end_lineno += self.source.start_line - 1
col_offset += self.source.start_column + 1
end_col_offset += self.source.start_column + 1
source_lines = self.source.full_source.splitlines(keepends=True)
snippet = _format_source_snippet(
source_lines, lineno, col_offset, end_lineno, end_col_offset
)
location = f"{self.source.source_name}:{lineno}:{col_offset}"
formatted = f"error: {message}\n --> {location}\n{snippet}"
raise DiagnosticError(formatted)
+157
View File
@@ -0,0 +1,157 @@
# 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.
"""Parser dispatching infrastructure"""
from collections.abc import Callable
from typing import TYPE_CHECKING, Any
from .doc import AST
if TYPE_CHECKING:
from .parser import Parser
ParseMethod = Callable[["Parser", AST], None]
ParseVTable: dict[tuple[str, str], ParseMethod] = {}
OpMethod = Callable[..., Any]
OpVTable: dict[tuple[type, AST, int], OpMethod] = {}
def register(token: str, type_name: str):
"""Register a method for a dispatch token and type name.
Parameters
----------
token : str
The token for IR, e.g., T for TIR and R for Relax.
type_name : str
The type name of AST node, e.g., FunctionDef, With, For.
Returns
-------
func : callable
The function to register dispatched method of parsing
corresponding token and AST node type.
"""
def func(method: ParseMethod):
"""Register a method in parser virtual table.
Parameters
----------
method : ParseMethod
The dispatched method to be registered in parser virtual table.
"""
ParseVTable[(token, type_name)] = method
return func
def get(
token: str,
type_name: str,
default: ParseMethod | None = None,
) -> ParseMethod | None:
"""Get a registered method for a dispatch token and type name,
or return a default method if no registered methods with this dispatch token and type name.
Parameters
----------
token : str
The token for IR, e.g., T for TIR and R for Relax.
type_name : str
The type name of AST node, e.g., FunctionDef, With, For.
default : Optional[ParseMethod]
The default method when no registered methods with this dispatch token and type name.
Returns
-------
func : Optional[ParseMethod]
The dispatched method of parsing corresponding token and AST node type.
"""
return ParseVTable.get((token, type_name), default)
def register_op(operand_type: type, op_node_type: AST, operand_index: int):
"""Register a method for a operand type, AST operator node and operand index.
Parameters
----------
operand_type : Type
The type of operands, e.g., tirx.Expr, tirx.IterVar.
op_node_type : AST
The doc AST operator node type, e.g., doc.Add, doc.Eq.
operand_index : int
The operand index, i.e., 0 for left operand and 1 for right operand.
Returns
-------
func : callable
The function to register dispatched method of parsing
corresponding a operand type, AST operator node and operand index.
"""
def func(method: OpMethod):
"""Register a method in parser operator virtual table.
Parameters
----------
method : ParseMethod
The dispatched method to be registered in parser operator virtual table.
"""
OpVTable[(operand_type, op_node_type, operand_index)] = method
return func
def get_op(
operand_type: type,
op_node_type: type,
operand_index: int,
default: OpMethod | None = None,
) -> OpMethod | None:
"""Register a method for a operand type, AST operator node and operand index.
Parameters
----------
operand_type : Type
The type of operands, e.g., tirx.Expr, tirx.IterVar.
op_node_type : AST
The doc AST operator node type, e.g., doc.Add, doc.Eq.
operand_index : int
The operand index, i.e., 0 for left operand and 1 for right operand.
default : Optional[OpMethod]
The default method when no registered methods with this operand type,
AST operator node and operand index.
Returns
-------
func : Optional[OpMethod]
The function to register dispatched method of parsing
corresponding a operand type, AST operator node and operand index.
"""
return OpVTable.get((operand_type, op_node_type, operand_index), default)
+315
View File
@@ -0,0 +1,315 @@
# 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.
# ruff: noqa: E722, F403
"""TVM Script Parser doc AST"""
import ast
import inspect
import typing
from collections import defaultdict
from . import doc_core as doc
from .doc_core import * # pylint: disable=unused-import,wildcard-import,redefined-builtin,W0614
FnToDoc = typing.Callable[[ast.AST], doc.AST]
FnFromDoc = typing.Callable[[doc.AST], ast.AST]
class Entry:
"""Mapping entry between python AST node type str and doc AST.
Parameters
----------
to_doc : typing.Optional[FnToDoc]
The callable methods for converting python AST node to doc AST.
from_doc : typing.Optional[FnFromDoc]
The callable methods for converting doc AST to python AST node.
"""
to_doc: FnToDoc | None
from_doc: FnFromDoc | None
def __init__(self):
self.to_doc = None
self.from_doc = None
class Registry:
"""Registration map from python AST node type str to methods of conversion
between python AST node and doc AST node.
Parameters
----------
_inst : typing.Optional["Registry"]
The instance of Registry.
table : typing.Dict[str, Entry]
The registration map from python AST node type str to methods of conversion
between python AST node and doc AST node.
"""
_inst: typing.Optional["Registry"] = None
table: dict[str, Entry]
def __init__(self):
self.table = defaultdict(Entry)
def register_to_doc(name: str):
"""Register the to_doc method for python AST node type.
Parameters
----------
name : str
The type of python AST node.
Returns
-------
f : Callable[[FnToDoc], None]
The function of registering the to_doc method for python AST node type.
"""
def f(to_doc: FnToDoc): # pylint: disable=redefined-outer-name
reg = Registry._inst # pylint: disable=protected-access
reg.table[name].to_doc = to_doc
return f
def register_from_doc(name: str):
"""Register the from_doc method for python AST node type.
Parameters
----------
name : str
The type of python AST node.
Returns
-------
f : Callable[[FnFromDoc], None]
The function of registering the from_doc method for python AST node type.
"""
def f(to_doc: FnFromDoc): # pylint: disable=redefined-outer-name
reg = Registry._inst # pylint: disable=protected-access
reg.table[name].from_doc = to_doc
return f
def _is_atomic_type(node):
return (
node is None
or node in [..., True, False]
or isinstance(
node,
int | float | str | bool | bytes | complex,
)
)
def _get_registry_entry(cls_name, attr):
cls_name = cls_name.split(".")[-1]
reg = Registry._inst # pylint: disable=protected-access
if cls_name in reg.table:
entry = reg.table[cls_name]
return getattr(entry, attr, None)
return None
def from_doc(node):
"""Get original python AST node from doc AST node.
Parameters
----------
node : doc.AST
The doc AST node.
Returns
-------
res : ast.AST
The corresponding AST node.
"""
if _is_atomic_type(node):
return node
if isinstance(node, tuple):
return tuple(from_doc(n) for n in node)
if isinstance(node, list):
return [from_doc(n) for n in node]
func = _get_registry_entry(node.__class__.__name__, "from_doc")
if not func:
raise NotImplementedError(f"from_doc is not implemented for: {node.__class__.__name__}")
return func(node)
def to_doc(node):
"""Get doc AST node from python AST node.
Parameters
----------
node : ast.AST
The AST node.
Returns
-------
res : doc.AST
The corresponding doc AST node.
"""
if _is_atomic_type(node):
return node
if isinstance(node, tuple):
return tuple(to_doc(n) for n in node)
if isinstance(node, list):
return [to_doc(n) for n in node]
func = _get_registry_entry(node.__class__.__name__, "to_doc")
if not func:
raise NotImplementedError(f"to_doc is not implemented for: {node.__class__.__name__}")
return func(node)
def parse(
source: str,
filename: str = "<unknown>",
mode: str = "exec",
) -> doc.AST:
"""Parse TVMScript source code str to doc AST.
Its interface is consistent with python built-in ast.parse.
And it will parse by python 3.8 first if possible,
or it will parse with python version in current environment.
Parameters
----------
source : str
The TVMScript source code.
filename : str
The optional filename of the file where source code locates.
mode : str
The parsing mode for ast.parse.
Returns
-------
res : doc.AST
The parsed doc AST.
"""
try:
program = ast.parse( # pylint: disable=unexpected-keyword-arg
source=source,
filename=filename,
mode=mode,
feature_version=(3, 8),
)
except: # pylint: disable=bare-except
program = ast.parse(
source=source,
filename=filename,
mode=mode,
)
return to_doc(program)
class NodeVisitor:
"""Node visitor for doc AST"""
def visit(self, node: doc.AST) -> None:
if isinstance(node, list | tuple):
for item in node:
self.visit(item)
return
if not isinstance(node, doc.AST):
return
getattr(
self,
"visit_" + node.__class__.__name__.split(".")[-1],
self.generic_visit,
)(node)
def generic_visit(self, node: doc.AST) -> None:
for field in node.__class__._FIELDS: # pylint: disable=protected-access
value = getattr(node, field, None)
if value is None:
pass
elif isinstance(value, doc.AST | list | tuple):
self.visit(value)
class NodeTransformer:
"""Node transformer for doc AST"""
def visit(self, node: doc.AST) -> doc.AST:
if isinstance(node, list):
return [self.visit(item) for item in node]
if isinstance(node, tuple):
return tuple(self.visit(item) for item in node)
if not isinstance(node, doc.AST):
return node
return getattr(
self,
"visit_" + node.__class__.__name__.split(".")[-1],
self.generic_visit,
)(node)
def generic_visit(self, node: doc.AST) -> doc.AST:
kv: dict[str, typing.Any] = {}
for field in node.__class__._FIELDS: # pylint: disable=protected-access
value = getattr(node, field, None)
if value is None:
pass
elif isinstance(value, doc.AST | list | tuple):
value = self.visit(value)
kv[field] = value
return node.__class__(**kv)
def _register_default():
class DefaultTranslator:
def __init__(self, doc_cls, func, fields):
self.doc_cls = doc_cls # getattr(doc, name)
self.func = func
self.fields = fields
def __call__(self, node):
kv = {attr: self.func(getattr(node, attr, None)) for attr in self.fields}
return self.doc_cls(**kv)
Registry._inst = Registry() # pylint: disable=protected-access
for cls_name in dir(doc):
doc_cls = getattr(doc, cls_name)
if not hasattr(ast, cls_name):
continue
if inspect.isclass(doc_cls) and issubclass(doc_cls, doc.AST):
assert "." not in cls_name
register_to_doc(cls_name)(
DefaultTranslator(
getattr(doc, cls_name),
to_doc,
doc_cls._FIELDS, # pylint: disable=protected-access
)
)
register_from_doc(cls_name)(
DefaultTranslator(
getattr(ast, cls_name),
from_doc,
doc_cls._FIELDS, # pylint: disable=protected-access
)
)
_register_default()
File diff suppressed because it is too large Load Diff
+210
View File
@@ -0,0 +1,210 @@
# 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 entry point of TVM parser."""
import inspect
from typing import Any
import tvm
from ....ir.module import IRModule
from ...ir_builder import IRBuilder
from . import doc
from .diagnostics import Source
from .error import ParserError
from .parser import Parser
WELL_FORMED_ERROR_MESSAGE = (
"Program is not well-formed. If this is deliberate, consider "
"setting check_well_formed in the top-level decorator to False "
"(e.g., @I.ir_module(check_well_formed=False) or "
"@R.function(check_well_formed=False))."
)
def _default_globals() -> dict[str, Any]:
# lazy import here to avoid circular deps
from tvm.script import tirx as _tirx_dsl # pylint: disable=import-outside-toplevel
from tvm.script.parser import (
ir, # pylint: disable=import-outside-toplevel
relax, # pylint: disable=import-outside-toplevel
)
from tvm.script.parser import tirx as _tirx_parser # pylint: disable=import-outside-toplevel
from tvm.script.tirx import tile as _tirx_tile # pylint: disable=import-outside-toplevel
from tvm.tirx import layout as _tirx_layout # pylint: disable=import-outside-toplevel
# Expose the layout `Axis` class so printed layout sugar like
# `4 @ Axis.laneid` round-trips without per-script imports. Injecting just
# `Axis` (one short symbol) avoids name collisions with common user shape
# vars like `m`, `P`, `F` that registered axes happen to share names with.
return {
"tvm": tvm,
"I": ir,
"ir": ir,
"T": _tirx_parser,
"tir": _tirx_parser,
"R": relax,
"relax": relax,
"Tx": _tirx_tile,
"tirx": _tirx_dsl,
"Axis": _tirx_layout.Axis,
}
def scan_macro(program: Any | str, extra_vars: dict[str, Any] | None = None) -> Any:
"""Generate the AST, and the source code for __repr__."""
# The AST will be converted into TIR at the time of expansion.
source = Source(program)
closure_vars = extra_vars or _default_globals()
return source, closure_vars
def parse(
program: doc.AST | Any | str,
extra_vars: dict[str, Any] | None = None,
check_well_formed: bool = True,
s_tir: bool = False,
) -> Any:
"""Register a method for a operand type, AST operator node and operand index.
Parameters
----------
program : Union[doc.AST, Any, str]
The TVMScript code to parse.
extra_vars : Dict[str, Any]
The extra variable table for parsing.
check_well_formed : bool
Whether to check well-formedness after parsing.
Returns
-------
func : Any
The parsed TVMScript program.
"""
if extra_vars is None:
extra_vars = _default_globals()
ann = {}
all_pyfuncs = {}
if inspect.isfunction(program):
ann = {program.__name__: program.__annotations__}
elif inspect.isclass(program):
for name, func in program.__dict__.items():
if inspect.isfunction(func):
ann[name] = func.__annotations__
all_pyfuncs[name] = func
source = Source(program)
parser = Parser(source, ann)
with IRBuilder() as builder:
try:
parser.parse(extra_vars=extra_vars)
except ParserError as err:
parser.report_error(err.node, err.args[0])
ret = builder.get()
# Attach pyfuncs to the IRModule
if inspect.isclass(program) and isinstance(ret, IRModule):
_attach_pyfuncs_to_irmodule(ret, all_pyfuncs)
# check well-formedness in both Relax and TIR
if check_well_formed:
check_ret = ret
if not isinstance(check_ret, IRModule):
check_ret = IRModule.from_expr(ret)
source_ast = source.as_ast()
if isinstance(
ret, IRModule | tvm.relax.Function
) and not tvm.relax.analysis.check_well_formed(ret):
parser.report_error(source_ast, err=WELL_FORMED_ERROR_MESSAGE)
try:
if s_tir:
tvm.tirx.analysis.verify_well_formed(check_ret)
else:
tvm.tirx.analysis.verify_tirx_well_formed(check_ret)
except Exception as err: # pylint: disable=broad-exception-caught
parser.report_error(
source_ast,
err=f"{WELL_FORMED_ERROR_MESSAGE}\n\nTraceback: {err!s}",
)
return ret
def _create_python_packed_func(pyfunc):
"""Create a PackedFunc wrapper for a Python function.
This function creates a PackedFunc that can be called from TVM runtime
and will execute the original Python function.
Parameters
----------
pyfunc : Callable
The Python function to wrap.
Returns
-------
PackedFunc
A PackedFunc that wraps the Python function.
"""
def packed_func_wrapper(*args, **kwargs):
"""Wrapper function that calls the original Python function."""
try:
result = pyfunc(*args, **kwargs)
return result
except Exception as error:
print(f"Error calling Python function {pyfunc.__name__}: {error}")
raise
return packed_func_wrapper
def _attach_pyfuncs_to_irmodule(irmodule, all_pyfuncs):
"""Attach Python functions to IRModule with reduced nesting."""
if not all_pyfuncs:
return
if not hasattr(irmodule, "pyfuncs"):
irmodule.pyfuncs = {}
for global_var, func in irmodule.functions_items():
if not isinstance(func, tvm.relax.ExternFunc):
continue
if not func.attrs.get("is_pyfunc", False):
continue
pyfunc_name = global_var.name_hint
if pyfunc_name not in all_pyfuncs:
continue
pyfunc = all_pyfuncs[pyfunc_name]
irmodule.pyfuncs[pyfunc_name] = pyfunc
try:
source_code = inspect.getsource(pyfunc)
func = func.with_attr("python_source", source_code)
except (OSError, TypeError):
func = func.with_attr("python_source", f"# Source unavailable for {pyfunc_name}")
packed_func = _create_python_packed_func(pyfunc)
func = func.with_attr("python_packed_func", packed_func)
irmodule[global_var] = func
+27
View File
@@ -0,0 +1,27 @@
# 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.
"""Error classes for diagnostics."""
from . import doc
class ParserError(Exception):
"""Error class for diagnostics."""
def __init__(self, node: doc.AST, msg: str):
super().__init__(msg)
self.node = node
+604
View File
@@ -0,0 +1,604 @@
# 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.
"""AST Evaluation"""
import ast
from collections.abc import Callable
from typing import TYPE_CHECKING, Any
import tvm
from . import dispatch, doc
from .error import ParserError
if TYPE_CHECKING:
from .parser import Parser
DEFAULT_OP: dict[type, Callable[..., Any]] = {
doc.Add: lambda a, b: a + b,
doc.Sub: lambda a, b: a - b,
doc.Mult: lambda a, b: a * b,
doc.Div: lambda a, b: a / b,
doc.FloorDiv: lambda a, b: a // b,
doc.Mod: lambda a, b: a % b,
doc.LShift: lambda a, b: a << b,
doc.RShift: lambda a, b: a >> b,
doc.BitOr: lambda a, b: a | b,
doc.BitXor: lambda a, b: a ^ b,
doc.BitAnd: lambda a, b: a & b,
doc.MatMult: lambda a, b: a @ b,
doc.Pow: lambda a, b: a**b,
doc.Eq: lambda a, b: a == b,
doc.NotEq: lambda a, b: a != b,
doc.Lt: lambda a, b: a < b,
doc.LtE: lambda a, b: a <= b,
doc.Gt: lambda a, b: a > b,
doc.GtE: lambda a, b: a >= b,
doc.Is: lambda a, b: a is b,
doc.IsNot: lambda a, b: a is not b,
doc.In: lambda a, b: a in b,
doc.NotIn: lambda a, b: a not in b,
doc.And: lambda a, b: a and b,
doc.Or: lambda a, b: a or b,
doc.Invert: lambda a: ~a,
doc.Not: lambda a: not a,
doc.UAdd: lambda a: +a,
doc.USub: lambda a: -a,
}
def _get_builtin_or_none(name: str):
builtins = globals().get("__builtins__")
if not builtins:
return None
if not isinstance(builtins, dict) and hasattr(builtins, "__dict__"):
builtins = builtins.__dict__
if isinstance(builtins, dict):
return builtins.get(name)
return None
class ExprEvaluator:
"""Expression evaluator for TVMScript parser.
Parameters
----------
parser : Parser
The parser bound with the evaluator.
value_table : Dict[str, Any]
The value table for expression evaluation.
new_value_count : int
The count for intermediate result added during evaluation.
"""
parser: "Parser"
value_table: dict[str, Any]
new_value_count: int
def __init__(self, parser: "Parser", value_table: dict[str, Any]) -> None:
super().__init__()
self.parser = parser
self.value_table = value_table
self.new_value_count = 0
@staticmethod
def eval(parser: "Parser", value_table: dict[str, Any], node: doc.AST) -> Any:
"""Expression evaluation for TVMScript parser.
Parameters
----------
parser : Parser
The parser bound with the evaluator.
value_table : Dict[str, Any]
The value table for expression evaluation.
node : doc.AST
The root node of AST tree node of expression to evaluate.
Returns
-------
res : Any
The evaluation result.
"""
self = ExprEvaluator(parser, value_table)
result = self._visit(node) # pylint: disable=protected-access
if isinstance(result, doc.Name):
if result.id in self.value_table:
return self.value_table[result.id]
else:
builtin = _get_builtin_or_none(result.id)
if builtin:
return builtin
raise ParserError(result, f"Undefined variable: {result.id}")
if isinstance(result, doc.Constant):
return result.value
raise TypeError(f"Unexpected result type: {type(result)}")
def _add_intermediate_result(self, value: Any) -> doc.Name:
"""Add intermediate result during evaluation into value table.
Parameters
----------
value : Any
The intermediate result.
Returns
-------
name : doc.Name
The doc AST name node with intermediate name for intermediate result.
"""
name = f"__tvm_tmp_value_{self.new_value_count}"
self.new_value_count += 1
self.value_table[name] = value
lineno = 0
col_offset = 0
return doc.Name(
id=name,
ctx=doc.Load(),
lineno=lineno,
col_offset=col_offset,
end_lineno=None,
end_col_offset=None,
)
def _visit(self, node: doc.AST) -> Any:
"""General doc AST node visiting method for expression evaluation.
Parameters
----------
node : doc.AST
The root node of AST tree node of expression to evaluate.
Returns
-------
res : Any
The evaluation result.
"""
args = []
if (
isinstance(node, doc.Call)
and hasattr(node.func, "attr")
and node.func.attr not in ["reads", "writes", "match_buffer"]
) or isinstance(node, doc.BinOp | doc.UnaryOp | doc.Compare | doc.BoolOp | doc.IfExp):
if isinstance(node, doc.BinOp):
args = [node.left, node.right]
elif isinstance(node, doc.UnaryOp):
args = [node.operand]
elif isinstance(node, doc.Compare):
args = [node.left, *node.comparators]
elif isinstance(node, doc.IfExp):
args = [node.test, node.body, node.orelse]
elif isinstance(node, doc.Call):
args = node.args
elif isinstance(node, doc.BoolOp):
args = node.values
for arg in args:
if isinstance(arg, doc.Subscript) and isinstance(arg.slice, doc.Slice | doc.Tuple):
if isinstance(arg.slice, doc.Slice):
check_slices = [arg.slice]
else:
check_slices = []
for p in arg.slice.elts:
if isinstance(p, doc.Slice):
check_slices.append(p)
for s in check_slices:
if not s.step and s.upper and s.lower:
s.step = doc.Constant(
1,
None,
s.upper.lineno,
s.upper.end_col_offset + 1,
s.upper.lineno,
s.upper.end_col_offset + 2,
)
if isinstance(node, list):
return [self._visit(n) for n in node]
if isinstance(node, tuple):
return tuple(self._visit(n) for n in node)
assert isinstance(node, doc.AST)
if isinstance(node, doc.Name):
if node.id not in self.value_table and not _get_builtin_or_none(node.id):
raise ParserError(node, f"Undefined variable: {node.id}")
return node
if isinstance(
node,
doc.Constant | doc.expr_context | doc.operator | doc.boolop | doc.unaryop | doc.cmpop,
):
return node
if isinstance(node, doc.keyword):
return doc.keyword(arg=node.arg, value=self._visit(node.value))
if not isinstance(node, doc.expr | doc.Slice):
return node
if isinstance(node, doc.Lambda):
return self._eval_lambda(node)
if isinstance(node, doc.Starred):
value = self._visit(node.value)
return doc.Starred(
value=value,
ctx=node.ctx,
lineno=node.lineno,
col_offset=node.col_offset,
end_lineno=node.end_lineno,
end_col_offset=node.end_col_offset,
)
if isinstance(node, doc.ListComp | doc.SetComp | doc.DictComp):
value = self._eval_expr(node)
return self._add_intermediate_result(value)
fields = {}
for field in node.__class__._FIELDS: # pylint: disable=protected-access
attr = getattr(node, field)
if isinstance(attr, doc.AST | tuple | list):
fields[field] = self._visit(attr)
else:
fields[field] = attr
try:
if isinstance(node, doc.BoolOp):
value = self._eval_bool_op(fields)
elif isinstance(node, doc.Compare):
value = self._eval_compare(fields)
elif isinstance(node, doc.UnaryOp):
value = self._eval_unary_op(fields)
elif isinstance(node, doc.BinOp):
value = self._eval_bin_op(fields)
elif isinstance(node, doc.IfExp):
value = self._eval_if_exp(fields)
elif isinstance(node, doc.Slice):
value = self._eval_slice(fields)
else:
value = self._eval_expr(node.__class__(**fields))
except Exception as err: # pylint: disable=broad-except
self.parser.report_error(node, err)
return self._add_intermediate_result(value)
def _eval_lambda(self, node: doc.Lambda) -> Any:
"""The doc AST lambda node evaluating method.
Parameters
----------
node : doc.Lambda
The root node of AST tree node of expression to evaluate.
Returns
-------
res : Any
The evaluation result.
"""
try:
value = self._eval_expr(node)
except Exception as err: # pylint: disable=broad-except
self.parser.report_error(node, err)
return self._add_intermediate_result(value)
def _eval_bool_op(self, fields: dict[str, Any]) -> Any:
"""The doc AST boolean operator node evaluating method.
Parameters
----------
fields : Dict[str, Any]
The dictionary of boolean operation information,
e.g., operator types, operand values.
Returns
-------
res : Any
The evaluation result.
"""
op = fields["op"]
if not isinstance(op, doc.And | doc.Or):
raise TypeError(f"Unexpected operator: {op}")
value = self._eval_expr(fields["values"][0])
for rhs in fields["values"][1:]:
value = _eval_op(op, values=[value, self._eval_expr(rhs)])
return value
def _eval_compare(self, fields: dict[str, Any]) -> Any:
"""The doc AST comparison operation node evaluating method.
Parameters
----------
fields : Dict[str, Any]
The dictionary of comparison operation information,
e.g., operator types, operand values.
Returns
-------
res : Any
The evaluation result.
"""
values = [self._eval_expr(fields["left"])]
values.extend([self._eval_expr(rhs) for rhs in fields["comparators"]])
result = None
assert len(fields["ops"]) == len(values) - 1
for index, op in enumerate(fields["ops"]):
sub_result = _eval_op(op, values=[values[index], values[index + 1]])
if result is None:
result = sub_result
else:
result = _eval_op(doc.And(), values=[result, sub_result])
return result
def _eval_unary_op(self, fields: dict[str, Any]) -> Any:
"""The doc AST unary operation node evaluating method.
Parameters
----------
fields : Dict[str, Any]
The dictionary of unary operation information,
e.g., operator types, operand values.
Returns
-------
res : Any
The evaluation result.
"""
value = self._eval_expr(fields["operand"])
value = _eval_op(fields["op"], values=[value])
return value
def _eval_bin_op(self, fields: dict[str, Any]) -> Any:
"""The doc AST binary operation node evaluating method.
Parameters
----------
fields : Dict[str, Any]
The dictionary of binary operation information,
e.g., operator types, operand values.
Returns
-------
res : Any
The evaluation result.
"""
return _eval_op(
fields["op"],
values=[
self._eval_expr(fields["left"]),
self._eval_expr(fields["right"]),
],
)
def _eval_if_exp(self, fields: dict[str, Any]) -> Any:
"""The doc AST if-else expression node evaluating method.
Parameters
----------
fields : Dict[str, Any]
The dictionary of if-else expression information,
e.g., test, body, orelse.
Returns
-------
res : Any
The evaluation result.
"""
test = self._eval_expr(fields["test"])
body = self._eval_expr(fields["body"])
orelse = self._eval_expr(fields["orelse"])
if isinstance(test, bool):
return body if test else orelse
elif tvm.ir.is_prim_expr(test) and test.ty.matches_code(tvm.DataTypeCode.BOOL):
return tvm.tirx.op.if_then_else(test, body, orelse)
else:
raise TypeError(f"Expected Python bool or TIR bool, but got {type(test)}")
def _eval_slice(self, fields: dict[str, Any]) -> slice:
"""The doc AST slice node evaluating method.
Parameters
----------
fields : Dict[str, Any]
The dictionary of slice information,
e.g., lower bound, upper bound, step.
Returns
-------
res : slice
The evaluation result.
"""
lower, upper, step = fields["lower"], fields["upper"], fields["step"]
lower = self._eval_expr(lower) if lower is not None else None
upper = self._eval_expr(upper) if upper is not None else None
step = self._eval_expr(step) if step is not None else None
return slice(lower, upper, step)
def _eval_expr(self, v: Any) -> Any:
"""The doc AST expression node evaluating method.
Parameters
----------
v : Any
The root node of AST tree node of expression to evaluate.
Returns
-------
res : Any
The evaluation result.
"""
return _eval_expr(v, self.value_table)
def eval_expr(
parser: "Parser",
node: doc.expr | doc.Expression,
dict_globals: dict[str, Any] | None,
) -> Any:
"""Expression evaluation for TVMScript parser.
Parameters
----------
parser : Parser
The parser bound with the evaluator.
node : Union[doc.expr, doc.Expression]
The root node of AST tree node of expression to evaluate.
dict_globals : Optional[Dict[str, Any]]
The optional global value table for expression evaluation.
Returns
-------
res : Any
The evaluation result.
"""
value_table = {}
if dict_globals is not None:
value_table.update(dict_globals)
return ExprEvaluator.eval(parser, value_table, node)
def eval_assign(
parser: "Parser",
target: doc.expr,
source: Any,
) -> dict[str, Any]:
"""Expression assignment evaluation for TVMScript parser.
Parameters
----------
parser : Parser
The parser bound with the evaluator.
target : doc.expr
The root node of AST tree node of assigned expression to evaluate.
source : Any
The source to be assigned with evaluated expression.
Returns
-------
res : Any
The evaluation result.
"""
try:
return _eval_assign(target, source)
except Exception as err: # pylint: disable=broad-except
parser.report_error(target, err)
raise
def _eval_expr(
node: doc.expr | doc.Expression,
dict_globals: dict[str, Any] | None,
) -> Any:
"""Expression evaluation implementation for TVMScript parser.
Parameters
----------
node : Union[doc.expr, doc.Expression]
The root node of AST tree node of expression to evaluate.
dict_globals : Optional[Dict[str, Any]]
The optional global value table for expression evaluation.
Returns
-------
res : Any
The evaluation result.
"""
node = doc.from_doc(node)
if isinstance(node, ast.expr):
node = ast.Expression(body=node)
assert isinstance(node, ast.Expression), "Expects an ast.Expression, but gets: " + str(node)
if dict_globals is None:
dict_globals = {}
node = ast.fix_missing_locations(node)
exe = compile(node, filename="<ast>", mode="eval")
return eval(exe, dict_globals) # pylint: disable=eval-used
def _eval_op(
op: doc.AST,
values: list[Any],
):
"""Operation expression evaluation implementation for TVMScript parser.
Parameters
----------
op : doc.AST
The root node of AST tree node of operation expression to evaluate.
values : List[Any]
The list of values of operands.
Returns
-------
res : Any
The evaluation result.
"""
op_type = type(op) # pylint: disable=protected-access
for i, v in enumerate(values):
v_type = getattr(type(v), "_dispatch_type", None)
if v_type is None:
continue
f = dispatch.get_op(
operand_type=v_type, op_node_type=op_type, operand_index=i, default=None
)
if f is not None:
return f(*values)
return DEFAULT_OP[op_type](*values)
def _eval_assign(
target: doc.expr,
source: Any,
) -> dict[str, Any]:
"""Expression assignment evaluation implementation for TVMScript parser.
Parameters
----------
target : doc.expr
The root node of AST tree node of assigned expression to evaluate.
source : Any
The source to be assigned with evaluated expression.
Returns
-------
res : Any
The evaluation result.
"""
target = doc.from_doc(target)
assert isinstance(target, ast.expr)
RHS_VAR_NAME = "__tvm_rhs_var__" # pylint: disable=invalid-name
rhs_var_name = RHS_VAR_NAME
dict_locals = {rhs_var_name: source}
mod = ast.fix_missing_locations(
ast.Module(
body=[
ast.Assign(
targets=[target],
value=ast.Name(
id=rhs_var_name,
ctx=ast.Load(),
),
)
],
type_ignores=[],
)
)
exe = compile(mod, filename="<ast>", mode="exec")
exec(exe, {}, dict_locals) # pylint: disable=exec-used
del dict_locals[rhs_var_name]
return dict_locals
+952
View File
@@ -0,0 +1,952 @@
# 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 core parser"""
import abc
import inspect
from collections import defaultdict
from collections.abc import Callable
from contextlib import contextmanager
from typing import Any
import numpy as np
from tvm.error import DiagnosticError
from tvm.ir import GlobalVar
from . import dispatch, doc
from .diagnostics import Diagnostics, Source
from .evaluator import eval_assign, eval_expr
DEFAULT_VISIT = {
"Interactive",
"Module",
"Expression",
"Pass",
}
def _deferred(exit_f: Callable[[], None]):
"""Created context with certain exit function.
Parameters
----------
exit_f : Callable[[], None]
The function to call when exiting the context.
Returns
-------
res : Any
The created context.
"""
@contextmanager
def context():
try:
yield
finally:
exit_f()
return context()
def _do_nothing(*args, **kwargs): # pylint: disable=unused-argument
pass
class ScriptMacro(abc.ABC):
"""Representation of a script macro.
This is a callable object, intended to be called from the expression evaluator.
The evaluator is expected to insert the current parser into the environment
undef the name given by "parser_object_name".
Once called, the ScriptMacro object will locate the current parser, and use it
to parse the macro's body and produce the result.
There were two major considerations for this design:
1. Implementing hygienic and non-hygienic macros.
2. Implementing macros that return values.
Macro uses in TIR are only allowed at a statement-level, and they don't produce
any values. Parsing of such macros could easily be done by intercepting doc.Call
nodes in the TIR parser. If a macro is a value-producing expression, then there
may not be a direct way to intercept calls to it if it's embedded in a complex
expression. Because macros use function-call syntax, the evaluator will try to
call the macro object, which this design relies on to parse and evaluate the macro.
"""
parser_object_name = "__current_script_parser__"
def __init__(
self,
source: Source,
closure_vars: dict[str, Any],
func: Callable,
hygienic: bool,
) -> None:
self.source = source
self.closure_vars = closure_vars
self.func = func
self.hygienic = hygienic
def __repr__(self):
return self.source.source
@abc.abstractmethod
def parse_macro(self, parser: "Parser") -> Any:
"""The main macro parsing function. Different scripts may have different
ways to parse a macro, and to return a value to the evaluator.
Parameters
----------
parser : Parser
The parser with the appropriate frame already created and populated depending
macro's hygiene settings,
Returns
-------
The return value depends on the specifics of the particular script. It can be
"None" or any other value or any type.
"""
def _find_parser_def(self):
outer_frame_infos = inspect.getouterframes(inspect.currentframe())
for finfo in outer_frame_infos:
parser = finfo.frame.f_globals.get(ScriptMacro.parser_object_name)
if parser is not None:
return parser
raise RuntimeError(f"{ScriptMacro.parser_object_name} not available")
def get_macro_def(self):
ast_module = self.source.as_ast()
for decl in ast_module.body:
if isinstance(decl, doc.FunctionDef) and decl.name == self.func.__name__:
return decl
raise RuntimeError(f"cannot find macro definition for {self.func.__name__}")
def __call__(self, *args, **kwargs):
param_binding = inspect.signature(self.func).bind(*args, **kwargs)
param_binding.apply_defaults()
local_vars = param_binding.arguments
parser = self._find_parser_def()
with parser.with_diag_source(self.source):
if self.hygienic:
saved_var_table = parser.var_table
parser.var_table = VarTable()
with parser.var_table.with_frame():
for k, v in self.closure_vars.items():
parser.var_table.add(k, v)
for k, v in local_vars.items():
parser.var_table.add(k, v)
parse_result = self.parse_macro(parser)
parser.var_table = saved_var_table
else:
with parser.var_table.with_frame():
for k, v in local_vars.items():
parser.var_table.add(k, v)
parse_result = self.parse_macro(parser)
return parse_result
class VarTableFrame:
"""The variable table frame.
A frame of variable table stores the variables created in one block or scope.
Parameters
----------
vars : Set[str]
The set of variable names in the variable table frame.
"""
vars: set[str]
def __init__(self):
self.vars = set()
def add(self, var: str):
"""Add a new variable into variable table frame.
Parameters
----------
var : str
The name of new variable.
"""
if var in self.vars:
raise ValueError(f"Variable {var} already defined in current scope")
self.vars.add(var)
def pop_all(self, fn_pop: Callable[[str], None]):
"""Pop out all variable in variable table frame.
Parameters
----------
fn_pop : Callable[[str], None]
The methods to call when popping each variable.
"""
for var in self.vars:
fn_pop(var)
self.vars.clear()
class VarTable:
"""The variable table.
A variable table stores the all variables when parsing TVMScript.
Parameters
----------
frames : List[VarTableFrame]
The list or stack of variable table frame.
name2value : Dict[str, List[Any]]
The dictionary for variable table name-based query.
"""
frames: list[VarTableFrame]
name2value: dict[str, list[Any]]
def __init__(self):
self.frames = []
self.name2value = defaultdict(list)
def with_frame(self):
"""Create a new variable table frame as with statement.
Returns
-------
res : Any
The context with new variable table frame.
"""
def pop_frame():
frame = self.frames.pop()
frame.pop_all(lambda name: self.name2value[name].pop())
self.frames.append(VarTableFrame())
return _deferred(pop_frame)
def add(self, var: str, value: Any, allow_shadowing: bool = False):
"""Add a new variable to variable table.
Parameters
----------
var : str
The name of variable.
value : Any
The value of variable.
allow_shadowing : bool
The options of whether variable shadowing allowed for this variable.
"""
# Skip if the key and value are equal to those in the var_table
if self.name2value[var] and isinstance(self.name2value[var][-1], type(value)):
if isinstance(value, np.ndarray) and (self.name2value[var][-1] == value).all():
return
elif self.name2value[var][-1] == value:
return
if allow_shadowing and var in self.frames[-1].vars:
# Shadowing
self.name2value[var][-1] = value
else:
self.frames[-1].add(var)
self.name2value[var].append(value)
def get(self) -> dict[str, Any]:
"""Get a variable dictionary of latest variables.
Returns
-------
res : Any
The variable dictionary copy of latest variables.
"""
return {key: values[-1] for key, values in self.name2value.items() if values}
def get_at_depth(self, depth: int) -> dict[str, Any]:
"""Get variables visible at the given frame depth, using current values.
For each variable name that appears in frames 0..depth-1, count how many
times it was pushed (to handle shadowing), then index into name2value at
count-1 to retrieve the latest value visible at that depth.
Parameters
----------
depth : int
The frame depth (number of frames visible).
Returns
-------
res : dict[str, Any]
Variable dictionary of values visible at the given depth.
"""
result: dict[str, Any] = {}
name_count: dict[str, int] = defaultdict(int)
for frame_idx in range(min(depth, len(self.frames))):
for name in self.frames[frame_idx].vars:
name_count[name] += 1
for name, count in name_count.items():
if self.name2value[name]:
result[name] = self.name2value[name][count - 1]
return result
def exist(self, value: Any) -> bool:
"""Check if any value exists in variable table.
Parameters
----------
value : Any
The value of variable.
Returns
-------
res : bool
The existence of the value.
"""
return any(
value.same_as(known_value)
for known_value_stack in self.name2value.values()
for known_value in known_value_stack
)
def _dispatch_wrapper(func: dispatch.ParseMethod) -> dispatch.ParseMethod:
def _wrapper(self: "Parser", node: doc.AST) -> None:
try:
return func(self, node)
except Exception as err: # pylint: disable=broad-except
self.report_error(node, err)
raise
return _wrapper
def _dispatch(self: "Parser", type_name: str) -> dispatch.ParseMethod:
for token in [self.dispatch_tokens[-1], "default"]:
func = dispatch.get(token=token, type_name=type_name, default=None)
if func is not None:
return _dispatch_wrapper(func)
return _dispatch_wrapper(lambda self, node: self.generic_visit(node))
class Parser(doc.NodeVisitor):
"""The TVMScript parser
Parameters
----------
diag : Diagnostics
The diagnostics for error reporting.
dispatch_tokens : List[str]
The list of dispatching tokens to dispatching parsing method
of different IRs and different doc AST structure.
var_table : VarTable
The variable table for parsing.
"""
diag: Diagnostics
dispatch_tokens: list[str]
function_annotations: dict[str, dict[str, Any]] | None
var_table: VarTable
inside_function: bool # whether we are within a function
current_class: str | None = None # current class being parsed
base_py_module_context: bool = False # whether current class inherits from BasePyModule
def __init__(
self,
source: Source,
function_annotations: dict[str, dict[str, Any]],
) -> None:
self.diag = Diagnostics(source)
self.dispatch_tokens = ["default"]
self.function_annotations = function_annotations
self.var_table = VarTable()
self.inside_function = False
def parse(self, extra_vars: dict[str, Any] | None = None) -> Any:
"""The main parse method for parser.
Parameters
----------
extra_vars : Optional[Dict[str, Any]]
The optional global value table for parsing.
Returns
-------
res : Any
The doc AST node visiting result.
"""
if extra_vars is None:
extra_vars = {}
with self.var_table.with_frame():
for k, v in extra_vars.items():
self.var_table.add(k, v)
node = self.diag.source.as_ast()
self.visit(node)
def get_dispatch_token(self, node: doc.FunctionDef) -> str:
if not isinstance(node, doc.FunctionDef):
self.report_error(node, "Only can get dispatch token for function.")
if not node.decorator_list:
self.report_error(node, "Function must be decorated")
# TODO: only the last decorator is parsed
decorator = self.eval_expr(node.decorator_list[-1])
if not hasattr(decorator, "dispatch_token"):
self.report_error(node, "The parser does not understand the decorator")
return decorator.dispatch_token
def with_dispatch_token(self, token: str):
"""Add a new dispatching token as with statement.
Parameters
----------
token : str
The dispatching token.
Returns
-------
res : Any
The context with new dispatching token.
"""
self.dispatch_tokens.append(token)
enter_func = dispatch.get(token=token, type_name="enter_token", default=lambda *args: None)
context = enter_func(self)
def pop_token():
exit_func = dispatch.get(
token=token, type_name="exit_token", default=lambda *args: None
)
exit_func(self, context)
self.dispatch_tokens.pop()
return _deferred(pop_token)
def set_class_context(self, class_name: str, is_base_py_module: bool = False):
"""Set the current class context for parsing.
Parameters
----------
class_name : str
The name of the current class being parsed.
is_base_py_module : bool
Whether the current class inherits from BasePyModule.
"""
self.current_class = class_name
self.base_py_module_context = is_base_py_module
def _get_current_class_context(self) -> str | None:
"""Get the current class context.
Returns
-------
Optional[str]
The name of the current class, or None if not in a class context.
"""
return self.current_class
def _is_base_py_module_context(self) -> bool:
"""Check if the current class context allows Python functions.
Returns
-------
bool
True if Python functions are allowed in the current context.
"""
return self.base_py_module_context
def with_diag_source(self, source: Source):
"""Add a new source as with statement.
Parameters
----------
source : Source
The source for diagnostics.
Returns
-------
res : Any
The context with new source.
"""
last_diag = self.diag
self.diag = Diagnostics(source)
def pop_source():
self.diag = last_diag
return _deferred(pop_source)
def eval_expr(
self,
node: doc.Expression | doc.expr,
extra_vars: dict[str, Any] | None = None,
) -> Any:
"""Expression evaluation when parsing.
Parameters
----------
node : Union[doc.expr, doc.Expression]
The root node of AST tree node of expression to evaluate.
extra_vars : Optional[Dict[str, Any]]
The optional global value table for expression evaluation.
Returns
-------
res : Any
The evaluation result.
"""
var_values = self.var_table.get()
if extra_vars is not None:
for k, v in extra_vars.items():
var_values[k] = v
var_values[ScriptMacro.parser_object_name] = self
return eval_expr(self, node, var_values)
def _duplicate_lhs_check(self, target: doc.expr) -> bool | set[str]:
"""Check whether duplicate lhs exists in assignment.
Parameters
----------
target : doc.expr
The doc AST expr node for lhs.
Returns
-------
res : Union[bool, Set[str]]
The result of true if duplicate lhs exists,
or the set of lhs names if no duplicate lhs exists.
"""
if isinstance(target, doc.Tuple | doc.List):
vars: set[str] = set() # pylint: disable=redefined-builtin
for i in target.elts:
res = self._duplicate_lhs_check(i)
if isinstance(res, bool) and res:
return True
assert isinstance(res, set)
if vars & res:
return True
vars = vars.union(res)
return vars
elif isinstance(target, doc.Name):
return {target.id}
elif isinstance(target, doc.Starred):
return self._duplicate_lhs_check(target.value)
elif isinstance(target, doc.Attribute):
# Attribute assignment like packedB.data = ..., treated as rebinding.
return {target.attr}
else:
self.report_error(target, "Invalid type in assign statement")
raise NotImplementedError
def eval_assign(
self,
target: doc.expr,
source: Any,
bind_value: Callable[["Parser", doc.expr, str, Any], Any],
allow_shadowing: bool = False,
) -> dict[str, Any]:
"""Expression assignment evaluation when parsing.
Parameters
----------
target : doc.expr
The root node of AST tree node of assigned expression to evaluate.
source : Any
The source to be assigned with evaluated expression.
bind_value : Callable[["Parser", doc.expr, str, Any], Any]
The value binding method when assigning the values to variables.
allow_shadowing : bool
The options of whether variable shadowing allowed for assignment.
Returns
-------
res : Dict[str, Any]
The dictionary of assignment result.
"""
if self._duplicate_lhs_check(target) is True:
self.report_error(target, "Duplicate vars assigned.")
var_values = eval_assign(self, target, source)
for k, v in var_values.items():
var = bind_value(self, target, k, v)
self.var_table.add(k, var, allow_shadowing)
return var_values
def report_error(self, node: doc.AST, err: Exception | str) -> None: # pylint: disable=no-self-use
"""The error reporting when parsing.
Parameters
----------
node : doc.AST
The doc AST node with errors.
err: Union[Exception, str]
The error to report.
"""
# If the error is already being raised as a DiagnosticError,
# re-raise it without wrapping it in a DiagnosticContext.
if isinstance(err, DiagnosticError):
raise err
# Only take the last line of the error message
if isinstance(err, RuntimeError):
lines = list(filter(None, str(err).split("\n")))
msg = lines[-1] if lines else (str(err) or type(err).__name__)
elif isinstance(err, KeyError):
msg = "KeyError: " + str(err)
else:
msg = str(err)
try:
self.diag.error(node, msg)
except Exception as diag_err:
# Calling self.diag.error is guaranteed to throw an
# exception. When shown to a user, this error should
# reference the point of error within the provided
# TVMScript. However, when caught in pdb, the full
# traceback should be available for debugging.
if isinstance(err, Exception):
diag_err = diag_err.with_traceback(err.__traceback__)
raise diag_err
def visit(self, node: doc.AST) -> None:
"""The general visiting method.
Parameters
----------
node : doc.AST
The doc AST node.
Returns
-------
res : Any
The visiting result.
"""
if isinstance(node, list | tuple):
for item in node:
self.visit(item)
return
if not isinstance(node, doc.AST):
return
name = node.__class__.__name__.split(".")[-1]
if name in DEFAULT_VISIT:
func = self.generic_visit
else:
func = getattr(self, "visit_" + name, None)
if func is None:
raise NotImplementedError(f"Visitor of AST node is not implemented: {name}")
try:
func(node)
except Exception as err: # pylint: disable=broad-except
self.report_error(node, err)
def visit_body(self, node: list[doc.stmt]) -> Any:
"""The general body visiting method.
Parameters
----------
node : List[doc.stmt]
The list of statements in body.
Returns
-------
res : Any
The visiting result.
"""
for stmt in node:
self.visit(stmt)
def visit_tvm_annotation(self, node: doc.expr) -> Any:
"""The general TVM annotation visiting method.
Parameters
----------
node : doc.expr
The doc AST expr node.
Returns
-------
res : Any
The visiting result.
"""
return _dispatch(self, "tvm_annotation")(self, node)
def visit_FunctionDef(self, node: doc.FunctionDef) -> None: # pylint: disable=invalid-name
"""The general function definition visit method.
Parameters
----------
node : doc.FunctionDef
The doc FunctionDef node.
"""
token = self.get_dispatch_token(node)
func = dispatch.get(token=token, type_name="FunctionDef", default=None)
if func is None:
self.report_error(
node,
"""The parser does not understand the decorator,
or visit_FunctionDef is not implemented for the decorator with token: """
+ token,
)
_dispatch(self, "pre_visit_local_function")(self, node)
_dispatch_wrapper(func)(self, node)
_dispatch(self, "post_visit_local_function")(self, node)
def visit_tvm_declare_function(self, node: doc.FunctionDef) -> GlobalVar:
token = self.get_dispatch_token(node)
with self.with_dispatch_token(token):
return _dispatch(self, "tvm_declare_function")(self, node)
def visit_ClassDef(self, node: doc.ClassDef) -> Any: # pylint: disable=invalid-name
"""The general class definition visiting method.
Parameters
----------
node : doc.ClassDef
The doc AST class definition node.
Returns
-------
res : Any
The visiting result.
"""
func = dispatch.get(token="ir", type_name="ClassDef", default=None)
if func is None:
self.report_error(node, "The parser does not understand the decorator")
_dispatch_wrapper(func)(self, node)
def visit_arguments(self, node: doc.arguments) -> Any:
"""The general arguments visiting method.
Parameters
----------
node : doc.arguments
The doc AST arguments node.
Returns
-------
res : Any
The visiting result.
"""
return _dispatch(self, "arguments")(self, node)
def visit_For(self, node: doc.For) -> Any: # pylint: disable=invalid-name
"""The general for visiting method.
Parameters
----------
node : doc.For
The doc AST for node.
Returns
-------
res : Any
The visiting result.
"""
return _dispatch(self, "For")(self, node)
def visit_While(self, node: doc.While) -> Any: # pylint: disable=invalid-name
"""The general while visiting method.
Parameters
----------
node : doc.While
The doc AST while node.
Returns
-------
res : Any
The visiting result.
"""
return _dispatch(self, "While")(self, node)
def visit_With(self, node: doc.With) -> Any: # pylint: disable=invalid-name
"""The general with visiting method.
Parameters
----------
node : doc.With
The doc AST with node.
Returns
-------
res : Any
The visiting result.
"""
return _dispatch(self, "With")(self, node)
def visit_Assign(self, node: doc.Assign) -> Any: # pylint: disable=invalid-name
"""The general assign visiting method.
Parameters
----------
node : doc.Assign
The doc AST assign node.
Returns
-------
res : Any
The visiting result.
"""
return _dispatch(self, "Assign")(self, node)
def visit_AnnAssign(self, node: doc.AnnAssign) -> Any: # pylint: disable=invalid-name
"""The general annotated assign visiting method.
Parameters
----------
node : doc.Assign
The doc AST annotated assign node.
Returns
-------
res : Any
The visiting result.
"""
return _dispatch(self, "AnnAssign")(self, node)
def visit_Expr(self, node: doc.Expr) -> Any: # pylint: disable=invalid-name
"""The general expression visiting method.
Parameters
----------
node : doc.Expr
The doc AST expression node.
Returns
-------
res : Any
The visiting result.
"""
return _dispatch(self, "Expr")(self, node)
def visit_If(self, node: doc.If) -> Any: # pylint: disable=invalid-name
"""The general if visiting method.
Parameters
----------
node : doc.If
The doc AST if node.
Returns
-------
res : Any
The visiting result.
"""
return _dispatch(self, "If")(self, node)
def visit_AugAssign(self, node: doc.AugAssign) -> Any: # pylint: disable=invalid-name
"""The general augmented assignment visiting method.
Parameters
----------
node : doc.AugAssign
The doc AST augmented assignment node.
Returns
-------
res : Any
The visiting result.
"""
return _dispatch(self, "AugAssign")(self, node)
def visit_Assert(self, node: doc.Assert) -> Any: # pylint: disable=invalid-name
"""The general assert visiting method.
Parameters
----------
node : doc.Assert
The doc AST assert node.
Returns
-------
res : Any
The visiting result.
"""
return _dispatch(self, "Assert")(self, node)
def visit_Return(self, node: doc.Return) -> Any: # pylint: disable=invalid-name
"""The general return visiting method.
Parameters
----------
node : doc.Return
The doc AST return node.
Returns
-------
res : Any
The visiting result.
"""
return _dispatch(self, "Return")(self, node)
def visit_Continue(self, node: doc.Continue) -> Any: # pylint: disable=invalid-name
"""The general continue visiting method.
Parameters
----------
node : doc.Continue
The doc AST continue node.
Returns
-------
res : Any
The visiting result.
"""
return _dispatch(self, "Continue")(self, node)
def visit_Break(self, node: doc.Break) -> Any: # pylint: disable=invalid-name
"""The general break visiting method.
Parameters
----------
node : doc.Break
The doc AST break node.
Returns
-------
res : Any
The visiting result.
"""
return _dispatch(self, "Break")(self, node)
def visit_Nonlocal(self, node: doc.Nonlocal) -> Any: # pylint: disable=invalid-name
"""The general nonlocal visiting method.
Parameters
----------
node : doc.Nonlocal
The doc AST nonlocal node.
Returns
-------
res : Any
The visiting result.
"""
return _dispatch(self, "Nonlocal")(self, node)
+212
View File
@@ -0,0 +1,212 @@
# 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.
"""TVM Script Parser utils"""
import inspect
from collections.abc import Callable
from types import FrameType
from typing import Any
from .diagnostics import findsource
def get_func_nonlocals(func):
"""A modified version of `inspect.getclosurevars`"""
if inspect.ismethod(func):
func = func.__func__
if not inspect.isfunction(func):
raise TypeError(f"{func!r} is not a Python function")
code = func.__code__
# Nonlocal references are named in co_freevars and resolved
# by looking them up in __closure__ by positional index
nonlocal_vars = {}
if func.__closure__ is not None:
for var, cell in zip(code.co_freevars, func.__closure__):
try:
nonlocal_vars[var] = cell.cell_contents
except ValueError as err:
# cell_contents may raise ValueError if the cell is empty.
if "empty" not in str(err):
raise
return nonlocal_vars
def inspect_function_capture(func: Callable) -> dict[str, Any]:
"""Capture function non-locals and global variables.
Parameters
----------
func : Callable
The function to inspect.
Returns
-------
res : Dict[str, Any]
The function variables map with non-local or global variables.
"""
captured = {
**func.__globals__, # type: ignore
**get_func_nonlocals(func),
}
return captured
def inspect_class_capture(cls: type) -> dict[str, Any]:
"""Capture class non-locals and global variables.
Parameters
----------
cls : type
The class to inspect.
Returns
-------
res : Dict[str, Any]
The class variables map with non-local or global variables.
"""
result: dict[str, Any] = {}
for _, v in cls.__dict__.items():
if inspect.isfunction(v):
func_vars = inspect_function_capture(v)
result.update(**func_vars)
return result
def _collect_annotation_names(source_obj: type | Callable) -> set[str]:
"""Parse source AST to find names used in function annotations.
Returns the set of ``ast.Name`` identifiers found inside argument
annotations and return annotations of any function definitions in
*source_obj*.
"""
import ast
import textwrap
try:
source = textwrap.dedent(inspect.getsource(source_obj))
tree = ast.parse(source)
except (OSError, TypeError):
return set()
names: set[str] = set()
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef):
for arg in node.args.args + node.args.posonlyargs + node.args.kwonlyargs:
if arg.annotation:
for n in ast.walk(arg.annotation):
if isinstance(n, ast.Name):
names.add(n.id)
if node.returns:
for n in ast.walk(node.returns):
if isinstance(n, ast.Name):
names.add(n.id)
return names
def _has_string_annotations(source_obj: type | Callable) -> bool:
"""Check if *source_obj* has stringified annotations (PEP 563)."""
if inspect.isclass(source_obj):
return any(
isinstance(a, str)
for v in source_obj.__dict__.values()
if inspect.isfunction(v)
for a in v.__annotations__.values()
)
return any(isinstance(a, str) for a in getattr(source_obj, "__annotations__", {}).values())
def _get_enclosing_scope_names(qualname: str) -> set[str]:
"""Extract lexically enclosing scope names from ``__qualname__``.
For ``outer.<locals>.inner.<locals>.func`` this returns ``{"outer", "inner"}``.
"""
parts = qualname.split(".")
return {p for p in parts[:-1] if p != "<locals>"}
def resolve_closure_vars(
source_obj: type | Callable, extra_vars: dict[str, Any], outer_stack: list
) -> None:
"""Resolve closure variables hidden by PEP 563.
With ``from __future__ import annotations``, variables used only in
annotations are not captured in ``__closure__``. This function parses
the source AST to find names used in function annotations, then looks
them up in lexically enclosing scope frames identified via
``__qualname__``.
Only triggered when annotations are actually strings (PEP 563 active).
Only annotation-referenced names are added, and only from enclosing
scopes — not from arbitrary caller frames.
Works for both classes (``@I.ir_module``) and functions (``@T.prim_func``).
"""
if not _has_string_annotations(source_obj):
return
ann_names = _collect_annotation_names(source_obj)
enclosing = _get_enclosing_scope_names(source_obj.__qualname__)
for name in ann_names:
if name not in extra_vars:
for frame_info in outer_stack[1:]:
if frame_info.frame.f_code.co_name in enclosing:
if name in frame_info.frame.f_locals:
extra_vars[name] = frame_info.frame.f_locals[name]
break
def is_defined_in_class(frames: list[FrameType], obj: Any) -> bool:
"""Check whether a object is defined in a class scope.
Parameters
----------
frames : List[FrameType]
The frame stack of the object, obtained by `inspect.stack()`.
Returns
-------
res : bool
The result if the object is defined in a class scope.
"""
def _is_tvmscript_class_annotator(line: str) -> bool:
"""Checks if the line contains a TVMScript annotator for a class
These match either `@I.ir_module` or `@R.rewriter`, or their
imported names `@ir_module` or `@rewriter`.
"""
return line.startswith("@") and ("ir_module" in line or "rewriter" in line)
if len(frames) > 2:
frame_info = frames[2]
code_context = frame_info.code_context
if code_context is None:
return False
line = code_context[0].strip()
if _is_tvmscript_class_annotator(line):
return True
if line.startswith("class"):
lineno = frame_info.lineno
if lineno >= 2:
source, _ = findsource(obj)
line = source[lineno - 2].strip()
if _is_tvmscript_class_annotator(line):
return True
return False
+35
View File
@@ -0,0 +1,35 @@
# isort: skip_file
# 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 ir module parser"""
from tvm.ir import Range
from ...ir_builder.ir import * # pylint: disable=redefined-builtin
from . import parser as _parser
from .entry import ir_module, pyfunc
__all__ = [
"Range",
"dummy_global_info",
"ir_module",
"lookup_vdevice",
"module_attrs",
"module_global_infos",
"pyfunc",
"vdevice",
]
+163
View File
@@ -0,0 +1,163 @@
# 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.
# pylint: disable=import-outside-toplevel
"""The entry point of TVM parser for ir module."""
import inspect
from collections.abc import Callable
from tvm import cpu, ir
from tvm.ir import GlobalVar, IRModule
from .._core import parse, utils
# this formulation allows us to support having @I.ir_module
# appear as a decorator by itself or to have optional arguments
# like @I.ir_module(check_well_formed=False)
def ir_module(
mod: type | None = None, check_well_formed: bool = True, s_tir: bool = False
) -> IRModule:
"""The parsing method for ir module, by using `@ir_module` as decorator.
Parameters
----------
mod : Type
The class to be parsed as ir module.
check_well_formed : bool
Whether to check well-formedness during parsing.
Returns
-------
ir_module : IRModule
The parsed ir module.
"""
# Capture stack outside wrapper (wrapper adds to the stack)
outer_stack = inspect.stack()
def decorator_wrapper(mod):
if not inspect.isclass(mod):
raise TypeError(f"Expect a class, but got: {mod}")
# Check BasePyModule inheritance
base_py_module_inherited = any(base.__name__ == "BasePyModule" for base in mod.__bases__)
extra_vars = utils.inspect_class_capture(mod)
# Resolve closure variables hidden by PEP 563 (annotation-only names)
utils.resolve_closure_vars(mod, extra_vars, outer_stack)
m = parse(mod, extra_vars, check_well_formed=check_well_formed, s_tir=s_tir)
if base_py_module_inherited:
# Lazy import: tvm.relax cannot be imported at module level in tvm.script.parser
# because tvm.script is loaded before tvm.relax during tvm initialization.
from tvm.relax.base_py_module import BasePyModule
from tvm.relax.expr import ExternFunc # pylint: disable=import-outside-toplevel
# Collect pyfunc methods
pyfunc_methods = [
name
for name, attr in mod.__dict__.items()
if hasattr(attr, "dispatch_token") and attr.dispatch_token == "pyfunc"
]
mod._pyfunc_methods = pyfunc_methods
# Create ExternFunc nodes
for method_name in pyfunc_methods:
try:
existing_gvars = [
global_var
for global_var in m.get_global_vars()
if global_var.name_hint == method_name
]
extern_func = ExternFunc(method_name)
extern_func = extern_func.with_attr("is_pyfunc", True)
extern_func = extern_func.with_attr("function_type", "python")
extern_func = extern_func.with_attr("python_function_name", method_name)
extern_func = extern_func.with_attr(
"python_source", f"# Source for {method_name}"
)
extern_func = extern_func.with_attr("python_packed_func", None)
if existing_gvars:
m[existing_gvars[0]] = extern_func
else:
m[GlobalVar(method_name)] = extern_func
except Exception: # pylint: disable=broad-exception-caught
continue
class ModuleFactory:
"""Factory class for creating BasePyModule instances with Python functions."""
def __init__(self, module, pyfunc_methods, original_class):
self.ir_module = module
self.pyfunc_methods = pyfunc_methods
self.original_class = original_class
def __call__(self, device=None, target=None):
if device is None:
device = cpu(0)
instance_ir_mod = ir.IRModule()
for global_var, func in self.ir_module.functions_items():
instance_ir_mod[global_var] = func
instance = BasePyModule(instance_ir_mod, device, target)
for method_name in self.pyfunc_methods:
if hasattr(self.original_class, method_name):
method = getattr(self.original_class, method_name)
instance.add_python_function(method_name, method)
return instance
def __getattr__(self, name):
if hasattr(self.ir_module, name):
return getattr(self.ir_module, name)
raise AttributeError(
f"'{self.__class__.__name__}' object has no attribute '{name}'"
)
factory = ModuleFactory(m, pyfunc_methods, mod)
setattr(factory, "__name__", mod.__name__)
return factory
setattr(m, "__name__", mod.__name__)
return m
if mod is not None:
# if there are no optional args given, this will directly invoke the wrapper
return decorator_wrapper(mod)
else:
# if there is a optional arg given, it returns the wrapper function
# as a new decorator and applies it
setattr(decorator_wrapper, "dispatch_token", "ir")
return decorator_wrapper
def pyfunc(func: Callable):
# Set the dispatch_token on the decorated function
setattr(func, "dispatch_token", "pyfunc")
return func
setattr(pyfunc, "dispatch_token", "pyfunc")
+212
View File
@@ -0,0 +1,212 @@
# 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.
# pylint: disable=unused-argument
"""The base parser for ir module"""
from tvm.ir import GlobalVar
from ...ir_builder import ir as I
from .._core import Parser, dispatch, doc
class ModuleWithGlobalVars:
"""A Module that can add global vars during parsing, to support `Module.function` syntax."""
def __getattr__(self, attr):
# Customize the error message.
# NOTE: `__getattr__` is only called when the attribute access fails with an AttributeError
raise AttributeError(f"Cannot find the function `{attr}` in the current IRModule")
@dispatch.register(token="ir", type_name="ClassDef")
def _visit_class_def(self: Parser, node: doc.ClassDef) -> None:
"""The class definition visiting method for ir module.
Parameters
----------
self : Parser
The visiting parser.
node : doc.ClassDef
The doc AST class definition node.
"""
with self.var_table.with_frame():
with I.ir_module():
# Step 0. Add the class name to the var table
fake_module = ModuleWithGlobalVars()
self.var_table.add(node.name, fake_module)
# Step 1: Check if this class inherits from BasePyModule
is_base_py_module = _check_base_py_module_inheritance(node)
if is_base_py_module:
# Store this information in the IRModule for later use
I.module_attrs({"base_py_module": True})
# Set the parser context to allow Python functions
self.set_class_context(node.name, True)
else:
# Set the parser context to disallow Python functions
self.set_class_context(node.name, False)
# Step 2. Visit non-function stmts, including but not limited to
# 1. `I.module_attrs`
# 2. `I.module_global_infos`
with self.with_dispatch_token("ir"):
for stmt in node.body:
if not isinstance(stmt, doc.FunctionDef):
self.visit(stmt)
# Step 3. Visit function stmts to declare the global vars
for stmt in node.body:
if isinstance(stmt, doc.FunctionDef):
global_var = self.visit_tvm_declare_function(stmt)
fake_module.__setattr__(stmt.name, global_var)
# Step 4. Visit and parse the functions
with self.with_dispatch_token("ir"):
for stmt in node.body:
if isinstance(stmt, doc.FunctionDef):
self.visit(stmt)
@dispatch.register(token="ir", type_name="Assign")
def _visit_assign(self: Parser, node: doc.Assign) -> None:
"""The assign visiting method for ir module.
Parameters
----------
self : Parser
The visiting parser.
node : doc.ClassDef
The doc AST assign node.
"""
if len(node.targets) != 1:
self.report_error(node, "Consequential assignments like 'a = b = c' are not supported.")
lhs = node.targets[0].id
rhs = self.eval_expr(node.value)
I.decl_function(lhs, rhs)
I.def_function(lhs, rhs)
@dispatch.register(token="ir", type_name="Expr")
def _visit_expr(self: Parser, node: doc.Expr) -> None:
"""The expression visiting method for ir module.
Parameters
----------
self : Parser
The visiting parser.
node : doc.ClassDef
The doc AST expression node.
"""
self.eval_expr(node.value)
@dispatch.register(token="default", type_name="Assign")
def visit_assign(self: Parser, node: doc.Assign) -> None:
if len(node.targets) != 1:
self.report_error(node, "Consequential assignments like 'a = b = c' are not supported.")
lhs = node.targets[0]
rhs = self.eval_expr(node.value)
self.eval_assign(
target=lhs, source=rhs, bind_value=lambda _a, _b, _c, value: value, allow_shadowing=True
)
@dispatch.register(token="default", type_name="pre_visit_local_function")
def pre_visit_local_function(self: Parser, node: doc.Expr) -> None:
pass
@dispatch.register(token="default", type_name="post_visit_local_function")
def post_visit_local_function(self: Parser, node: doc.Expr) -> None:
pass
@dispatch.register(token="pyfunc", type_name="tvm_declare_function")
def visit_tvm_declare_function(self: Parser, node: doc.FunctionDef) -> GlobalVar:
"""Declare a Python function as an ExternFunc in the IRModule."""
# Check if Python functions are allowed in this context
# We need to check if we're in a class that inherits from BasePyModule
current_class = self._get_current_class_context()
if current_class and not self._is_base_py_module_context():
self.report_error(
node,
"@I.pyfunc are only allowed in classes that inherit from BasePyModule. "
f"Class '{current_class}' does not inherit from BasePyModule.",
)
# Lazy import: tvm.relax cannot be imported at module level in tvm.script.parser
# because tvm.script is loaded before tvm.relax during tvm initialization.
from tvm.relax import ExternFunc # pylint: disable=import-outside-toplevel
# Create ExternFunc with proper attributes for Python functions
func = ExternFunc(node.name)
func = func.with_attr("is_pyfunc", True)
func = func.with_attr("function_type", "python")
func = func.with_attr("python_function_name", node.name)
# Add placeholder attributes that will be filled in later
func = func.with_attr("python_source", f"# Source will be filled for {node.name}")
func = func.with_attr("python_packed_func", None) # Will be filled in entry.py
# Store the function name for later retrieval
return I.decl_function(node.name, func)
@dispatch.register(token="pyfunc", type_name="FunctionDef")
def visit_function_def(self: Parser, node: doc.FunctionDef) -> None:
"""Visit Python function definition - no need to parse the body."""
# Python function body is not parsed in TVMScript
def _check_base_py_module_inheritance(node: doc.ClassDef) -> bool:
"""Check if a class inherits from BasePyModule.
Parameters
----------
node : doc.ClassDef
The class definition node to check.
Returns
-------
bool
True if the class inherits from BasePyModule, False otherwise.
"""
if not node.bases:
return False
# Check each base class
for base in node.bases:
if hasattr(base, "id"):
if base.id == "BasePyModule":
return True
elif hasattr(base, "attr"):
if base.attr == "BasePyModule":
return True
elif hasattr(base, "value") and hasattr(base.value, "id"):
if (
base.value.id in ["BasePyModule", "tvm", "relax"]
and hasattr(base, "attr")
and base.attr == "BasePyModule"
):
return True
return False
+21
View File
@@ -0,0 +1,21 @@
# 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.
"""
TVMScript Unified Printer
This package provides a set of APIs to print supported TVM IR into TVMScript
in a roundtrippable way.
"""
+21
View File
@@ -0,0 +1,21 @@
# 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.
"""FFI APIs for tvm.script.printer"""
import tvm_ffi
tvm_ffi.init_ffi_api("script.printer", __name__) # pylint: disable=protected-access
+520
View File
@@ -0,0 +1,520 @@
# 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.
"""Doc types for TVMScript Unified Printer"""
from collections.abc import Sequence
from enum import IntEnum, unique
from typing import Union
from tvm_ffi import register_object
from tvm_ffi.access_path import AccessPath
from tvm.runtime import Object
from tvm.tirx import FloatImm, IntImm
from . import _ffi_api
@register_object("script.printer.Doc")
class Doc(Object):
"""Base class of all Docs"""
@register_object("script.printer.ExprDoc")
class ExprDoc(Doc):
"""Base class of all expression Docs"""
def attr(self, name: str) -> "AttrAccessDoc":
"""
Create a doc that represents attribute access on self.
Parameters
----------
name : str
The attribute name to access
Returns
-------
doc : AttrAccessDoc
"""
return _ffi_api.ExprDocAttr(self, name) # type: ignore # pylint: disable=no-member
def call(self, *args: tuple["ExprDoc"], **kwargs: dict[str, "ExprDoc"]) -> "CallDoc":
"""
Create a doc that represents function call, with self as callee.
Parameters
----------
*args : ExprDoc
The positional arguments of the function call.
**kwargs
The keyword arguments of the function call.
Returns
-------
doc : CallDoc
"""
kwargs_keys = list(kwargs.keys())
kwargs_values = list(kwargs.values())
return _ffi_api.ExprDocCall(self, args, kwargs_keys, kwargs_values) # type: ignore # pylint: disable=no-member
_IndexType = Union["ExprDoc", "SliceDoc"]
def __getitem__(self, indices: tuple[_IndexType] | _IndexType) -> "IndexDoc":
"""
Create a doc that represents index access on self.
Parameters
----------
indices : Union[Tuple[Union["ExprDoc", "SliceDoc"]], Union["ExprDoc", "SliceDoc"]]
The indices to access
Returns
-------
doc : IndexDoc
"""
if not isinstance(indices, tuple):
indices = (indices,)
return _ffi_api.ExprDocIndex(self, indices) # type: ignore # pylint: disable=no-member
def __iter__(self):
"""
This is implemented to prevent confusing error message when trying to use ExprDoc
as iterable. According to PEP-234, An object can be iterated over if it
implements __iter__() or __getitem__(). If an object has only __getitem__
but not __iter__, interpreter will iterate the object by calling
__getitem__ with 0, 1, 2, ..., until an IndexError is raised.
https://peps.python.org/pep-0234/#python-api-specification
"""
raise RuntimeError(f"{self.__class__} cannot be used as iterable.")
@register_object("script.printer.StmtDoc")
class StmtDoc(Doc):
"""Base class of statement doc"""
@register_object("script.printer.StmtBlockDoc")
class StmtBlockDoc(Doc):
"""The container doc that holds a list of StmtDoc.
Note: `StmtBlockDoc` is never used in the IR, but a temporary container that allows holding a
list of StmtDoc.
"""
stmts: Sequence[StmtDoc]
def __init__(self, stmts: list[StmtDoc]):
self.__init_handle_by_constructor__(_ffi_api.StmtBlockDoc, stmts) # type: ignore # pylint: disable=no-member
@register_object("script.printer.LiteralDoc")
class LiteralDoc(ExprDoc):
"""Doc that represents literal value"""
value: str | IntImm | FloatImm | None
def __init__(
self,
value: str | float | bool | int | None,
path: AccessPath | None = None,
):
if value is None:
self.__init_handle_by_constructor__(_ffi_api.LiteralDocNone, path) # type: ignore # pylint: disable=no-member
elif isinstance(value, str):
self.__init_handle_by_constructor__(
_ffi_api.LiteralDocStr, # type: ignore # pylint: disable=no-member
value,
path,
)
elif isinstance(value, float):
self.__init_handle_by_constructor__(
_ffi_api.LiteralDocFloat, # type: ignore # pylint: disable=no-member
value,
path,
)
elif isinstance(value, bool):
self.__init_handle_by_constructor__(
_ffi_api.LiteralDocBoolean, # type: ignore # pylint: disable=no-member
value,
path,
)
elif isinstance(value, int):
self.__init_handle_by_constructor__(
_ffi_api.LiteralDocInt, # type: ignore # pylint: disable=no-member
value,
path,
)
else:
raise TypeError(f"Unsupported type {type(value)} for LiteralDoc")
@register_object("script.printer.IdDoc")
class IdDoc(ExprDoc):
"""Doc that represents identifier"""
name: str
def __init__(self, name: str):
self.__init_handle_by_constructor__(_ffi_api.IdDoc, name) # type: ignore # pylint: disable=no-member
@register_object("script.printer.AttrAccessDoc")
class AttrAccessDoc(ExprDoc):
"""Doc that represents attribute access on an expression"""
value: ExprDoc
name: str
def __init__(self, value: ExprDoc, name: str):
self.__init_handle_by_constructor__(_ffi_api.AttrAccessDoc, value, name) # type: ignore # pylint: disable=no-member
@register_object("script.printer.IndexDoc")
class IndexDoc(ExprDoc):
"""Doc that represents index access on an expression"""
value: ExprDoc
indices: Sequence[Union[ExprDoc, "SliceDoc"]]
def __init__(self, value: ExprDoc, indices: list[Union[ExprDoc, "SliceDoc"]]):
self.__init_handle_by_constructor__(_ffi_api.IndexDoc, value, indices) # type: ignore # pylint: disable=no-member
@register_object("script.printer.CallDoc")
class CallDoc(ExprDoc):
"""Doc that represents function call"""
callee: ExprDoc
args: Sequence[ExprDoc]
kwargs_keys: Sequence[str]
kwargs_values: Sequence[ExprDoc]
def __init__(self, callee: ExprDoc, *args: tuple[ExprDoc], **kwargs: dict[str, ExprDoc]):
kwargs_keys = list(kwargs.keys())
kwargs_values = list(kwargs.values())
self.__init_handle_by_constructor__(
_ffi_api.CallDoc, # type: ignore # pylint: disable=no-member
callee,
args,
kwargs_keys,
kwargs_values,
)
@unique
class OperationKind(IntEnum):
"""
This enum represents the kind of operation (operator) in OperationDoc
It's mirrored from OperationDocNode::Kind at include/tvm/script/printer/doc.h
"""
# The name convention follows https://docs.python.org/3/library/ast.html
# pylint: disable=invalid-name
_UnaryStart = 0
USub = 1
Invert = 2
Not = 3
_UnaryEnd = 4
_BinaryStart = 5
Add = 6
Sub = 7
Mult = 8
Div = 9
FloorDiv = 10
Mod = 11
Pow = 12
LShift = 13
RShift = 14
BitAnd = 15
BitOr = 16
BitXor = 17
Lt = 18
LtE = 19
Eq = 20
NotEq = 21
Gt = 22
GtE = 23
And = 24
Or = 25
MatMul = 26
_BinaryEnd = 27
_SpecialStart = 28
IfThenElse = 29
_SpecialEnd = 30
# pylint: enable=invalid-name
@register_object("script.printer.OperationDoc")
class OperationDoc(ExprDoc):
"""
Doc that represents operation
It can be unary, binary and other special operators (for example, the
if-then-else expression).
"""
kind: OperationKind
operands: Sequence[ExprDoc]
def __init__(self, kind: OperationKind, operands: list[ExprDoc]):
self.__init_handle_by_constructor__(_ffi_api.OperationDoc, kind, operands) # type: ignore # pylint: disable=no-member
@register_object("script.printer.LambdaDoc")
class LambdaDoc(ExprDoc):
"""Doc that represents lambda function"""
args: Sequence[IdDoc]
body: ExprDoc
def __init__(self, args: list[IdDoc], body: ExprDoc):
self.__init_handle_by_constructor__(_ffi_api.LambdaDoc, args, body) # type: ignore # pylint: disable=no-member
@register_object("script.printer.TupleDoc")
class TupleDoc(ExprDoc):
"""Doc that represents tuple literal"""
elements: Sequence[ExprDoc]
def __init__(self, elements: list[ExprDoc]):
self.__init_handle_by_constructor__(_ffi_api.TupleDoc, elements) # type: ignore # pylint: disable=no-member
@register_object("script.printer.ListDoc")
class ListDoc(ExprDoc):
"""Doc that represents list literal"""
elements: Sequence[ExprDoc]
def __init__(self, elements: list[ExprDoc]):
self.__init_handle_by_constructor__(_ffi_api.ListDoc, elements) # type: ignore # pylint: disable=no-member
@register_object("script.printer.DictDoc")
class DictDoc(ExprDoc):
"""Doc that represents dict literal"""
keys: Sequence[ExprDoc]
values: Sequence[ExprDoc]
def __init__(self, content: dict[ExprDoc, ExprDoc]):
keys = list(content.keys())
values = list(content.values())
self.__init_handle_by_constructor__(_ffi_api.DictDoc, keys, values) # type: ignore # pylint: disable=no-member
@register_object("script.printer.SliceDoc")
class SliceDoc(ExprDoc):
"""
Doc that represents slice in Index expression
This doc can only appear in `IndexDoc.indices`.
"""
start: ExprDoc | None
stop: ExprDoc | None
step: ExprDoc | None
def __init__(
self,
start: ExprDoc | None = None,
stop: ExprDoc | None = None,
step: ExprDoc | None = None,
):
self.__init_handle_by_constructor__(_ffi_api.SliceDoc, start, stop, step) # type: ignore # pylint: disable=no-member
@register_object("script.printer.AssignDoc")
class AssignDoc(StmtDoc):
"""Doc that represents assign statement."""
lhs: ExprDoc
rhs: ExprDoc | None
annotation: ExprDoc | None
def __init__(self, lhs: ExprDoc, rhs: ExprDoc | None, annotation: ExprDoc | None = None):
self.__init_handle_by_constructor__(
_ffi_api.AssignDoc, # type: ignore # pylint: disable=no-member
lhs,
rhs,
annotation,
)
@register_object("script.printer.IfDoc")
class IfDoc(StmtDoc):
"""Doc that represent if-then-else statement."""
predicate: ExprDoc
then_branch: Sequence[StmtDoc]
else_branch: Sequence[StmtDoc]
def __init__(self, predicate: ExprDoc, then_branch: list[StmtDoc], else_branch: list[StmtDoc]):
self.__init_handle_by_constructor__(
_ffi_api.IfDoc, # type: ignore # pylint: disable=no-member
predicate,
then_branch,
else_branch,
)
@register_object("script.printer.WhileDoc")
class WhileDoc(StmtDoc):
"""Doc that represents while statement."""
predicate: ExprDoc
body: Sequence[StmtDoc]
def __init__(self, predicate: ExprDoc, body: list[StmtDoc]):
self.__init_handle_by_constructor__(_ffi_api.WhileDoc, predicate, body) # type: ignore # pylint: disable=no-member
@register_object("script.printer.ForDoc")
class ForDoc(StmtDoc):
"""Doc that represents for statement."""
lhs: ExprDoc
rhs: ExprDoc
body: Sequence[StmtDoc]
def __init__(self, lhs: ExprDoc, rhs: ExprDoc, body: list[StmtDoc]):
self.__init_handle_by_constructor__(_ffi_api.ForDoc, lhs, rhs, body) # type: ignore # pylint: disable=no-member
@register_object("script.printer.ScopeDoc")
class ScopeDoc(StmtDoc):
"""
Doc that represents special scopes.
Specifically, this means the with statement in Python:
with <rhs> as <lhs>:
<body...>
"""
lhs: ExprDoc | None
rhs: ExprDoc
body: Sequence[StmtDoc]
def __init__(self, lhs: ExprDoc | None, rhs: ExprDoc, body: list[StmtDoc]):
self.__init_handle_by_constructor__(_ffi_api.ScopeDoc, lhs, rhs, body) # type: ignore # pylint: disable=no-member
@register_object("script.printer.ExprStmtDoc")
class ExprStmtDoc(StmtDoc):
"""Doc that represents an expression as statement."""
expr: ExprDoc
def __init__(self, expr: ExprDoc):
self.__init_handle_by_constructor__(_ffi_api.ExprStmtDoc, expr) # type: ignore # pylint: disable=no-member
@register_object("script.printer.AssertDoc")
class AssertDoc(StmtDoc):
"""Doc that represents assert statement."""
test: ExprDoc
msg: ExprDoc | None
def __init__(self, test: ExprDoc, msg: ExprDoc | None = None):
self.__init_handle_by_constructor__(_ffi_api.AssertDoc, test, msg) # type: ignore # pylint: disable=no-member
@register_object("script.printer.ReturnDoc")
class ReturnDoc(StmtDoc):
"""Doc that represents return statement."""
value: ExprDoc
def __init__(self, value: ExprDoc):
self.__init_handle_by_constructor__(_ffi_api.ReturnDoc, value) # type: ignore # pylint: disable=no-member
@register_object("script.printer.FunctionDoc")
class FunctionDoc(StmtDoc):
"""Doc that represents function definition."""
name: IdDoc
args: Sequence[AssignDoc]
decorators: Sequence[ExprDoc]
return_type: ExprDoc | None
body: Sequence[StmtDoc]
def __init__(
self,
name: IdDoc,
args: list[AssignDoc],
decorators: list[ExprDoc],
return_type: ExprDoc | None,
body: list[StmtDoc],
):
self.__init_handle_by_constructor__(
_ffi_api.FunctionDoc, # type: ignore # pylint: disable=no-member
name,
args,
decorators,
return_type,
body,
)
@register_object("script.printer.ClassDoc")
class ClassDoc(StmtDoc):
"""Doc that represents class definition."""
name: IdDoc
decorators: Sequence[ExprDoc]
body: Sequence[StmtDoc]
def __init__(self, name: IdDoc, decorators: list[ExprDoc], body: list[StmtDoc]):
self.__init_handle_by_constructor__(
_ffi_api.ClassDoc, # type: ignore # pylint: disable=no-member
name,
decorators,
body,
)
@register_object("script.printer.CommentDoc")
class CommentDoc(StmtDoc):
"""Doc that represents comment."""
def __init__(self, comment: str):
self.__init_handle_by_constructor__(
_ffi_api.CommentDoc,
comment, # type: ignore # pylint: disable=no-member
)
@register_object("script.printer.DocStringDoc")
class DocStringDoc(StmtDoc):
"""Doc that represents docstring."""
def __init__(self, docs: str):
self.__init_handle_by_constructor__(
_ffi_api.DocStringDoc,
docs, # type: ignore # pylint: disable=no-member
)
+60
View File
@@ -0,0 +1,60 @@
# 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.
"""Functions to print doc into text format"""
from tvm_ffi.access_path import AccessPath
from tvm.runtime.script_printer import PrinterConfig
from . import _ffi_api
from .doc import Doc
def to_python_script(
doc: Doc,
indent_spaces: int = 4,
print_line_numbers: bool = False,
num_context_lines: int | None = None,
path_to_underline: list[AccessPath] | None = None,
) -> str:
"""Convert Doc into Python script.
Parameters
----------
doc : Doc
The doc to convert into Python script
indent_spaces : int
The number of indent spaces to use in the output
print_line_numbers: bool
Whether to print line numbers
num_context_lines : Optional[int]
Number of context lines to print around the underlined text
path_to_underline : Optional[AccessPath]
Object path to be underlined
Returns
-------
script : str
The text representation of Doc in Python syntax
"""
cfg = PrinterConfig(
indent_spaces=indent_spaces,
print_line_numbers=print_line_numbers,
num_context_lines=num_context_lines,
path_to_underline=path_to_underline,
)
return _ffi_api.DocToPythonScript(doc, cfg) # type: ignore # pylint: disable=no-member