# 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