TVMScript#

TVMScript expresses TVM IR with Python syntax. A shared frontend translates source into a Python builder program; executing that program constructs IR. A separate printer converts IR back into readable, parseable source.

Source + definition context
    → Python AST → syntax transpiler → Python builder program → TVM IR
TVM IR
    → DocTranslator → Doc tree + origins → diagnostic paths → Python text

Source and frontend#

tvm.script.parser accepts Python functions, classes and source strings through public decorators, tvm.script.parse and tvm.script.from_source. The frontend acquires source and locations, captures the definition’s globals and closure bindings, and composes the generated callable with that environment. String callers provide external bindings through extra_vars.

Standalone function decorators construct IR immediately. Within an IR-module class, function decorators retain definitions until module construction, which can declare signatures before building bodies. Captured Python values belong to the source context; symbolic IR values are created and resolved by builders.

Function construction, JIT and macro decorators require @ application at the function definition site. Later application to an existing function is rejected. Use a registered namespace, such as @T.function or @Ts.function(private=True). Python definitions support namespace aliases such as Alias = T; bare callable aliases and preconfigured decorator aliases are unsupported. Source strings resolve namespace aliases from imports or extra_vars, not executable prefix assignments.

GeneratedBuilder records connect generated body helpers to original functions and identify bindings preserved during recomposition. Original-name wrapper parameters capture definition values as defaults. Execution globals remain separate from the locals that evaluate those defaults. Declaration and annotation helpers bind snapshot values under their original names, so Python handles parameter and local shadowing. Missing snapshot values raise only when their annotation expressions read them. Class setup and declaration snapshots execute in source order before function bodies. Body globals, closures and local shadowing retain their Python scope. Missing values are read only when needed, including conditional and optional annotations.

Annotations read concrete values from their definition scope, preserving missing-name errors when a value is used. Create external symbols with n = I.dynamic("n") or the identical T.dynamic and Ts.dynamic constructors. Each call creates a fresh native variable, defaulting to int64. Reuse that object in ordinary Python shape, stride and offset expressions to share identity; strings in those fields are not parsed as expressions. On Python 3.12+, explicit headers such as def f[n, k: T.int32](...) declare local symbols; n: int retains the int64 default. Use from __future__ import annotations to defer eager Python evaluation of header symbols. Explicit whole quoted annotations are rejected in script source; normal string arguments inside annotation constructors remain valid. Captured runtime typing.TypeVar objects are not script symbols; ordinary Python typing uses remain unaffected.

An explicit scalar annotation n: n preserves a captured native symbol’s identity. An independently typed parameter such as n: T.int32 and ordinary body locals shadow definition captures normally. Annotation classes, Python unions and deferred return-constructor evaluation retain their builder behavior.

Syntax and construction protocol#

The syntax transpiler rewrites a fresh Python AST using scope and declaration facts from a prescan. A registered decorator selects the construction namespace. Assignments call binding hooks, expression statements call emission hooks, and control flow opens builder frames. The namespace owns the meaning of these operations and the supported IR constructs. Standalone decorators pass already-evaluated root_function_kwargs so option expressions execute once. Nested functions and module members use their own decorator options. Builder hooks own option defaults; check_well_formed remains a separate parser setting.

tvm.script.parser.protocol_registry records syntax policies under registered canonical namespace paths rooted at tirx, relax, ir and s_tir. Source code selects its own aliases through imports or explicit environments; the parser does not provide implicit T, R, I or Ts bindings. For example, from tvm.script import tirx as X makes X.function resolve to tirx.function without changing the callable. Printed scripts show suggested imports as comments: include those imports in executable source or pass equivalent extra_vars when parsing a printed script. The public registration helpers identify scalar annotations and mutable declarations. Entry factories populate DEFINITION_KIND with DefinitionKind values: FUNCTION for regular and JIT IR definitions, MACRO for inline and macro expansion, and PYTHON for retained I.pyfunc runtime callables. Macro bodies construct IR in the caller’s context; Python bodies retain ordinary execution and do not need builder loc context. All calls retain their source context, with binding hooks owning result attachment. Symbolic shapes use concrete expressions. Source aliases resolve to those paths; ordinary Python calls remain calls in the generated program. Explicit constexpr markers from the shared builder select host control flow during construction.

JIT supplies const_args, a mapping from parameter names to fixed values. Explicit None values mark absent optional arguments and skip their annotation evaluation. Generated code reads the mapping through a fresh _const_args name. An empty map still selects root JIT construction; an absent map selects ordinary parsing. Syntax translation does not inspect these values to select a branch.

Generated operations retain source locations. Syntax restrictions raise source-located SyntaxError exceptions; builder and Python helper errors retain their original exception types. Temporary parse state is released when construction finishes or fails.

Frame-based construction#

The IR builder maintains an active stack of frames, such as module, function and loop frames. Entering a frame establishes its scope; exiting finalizes its IR and attaches it to its parent. Frames own parameters, symbols and construction state. Builder hooks own value binding, type rules and validation of completed IR. The shared resolve_global_info_args decorator resolves named metadata arguments after ordinary Python argument evaluation; dialect callbacks own selector syntax and lookup in the active module.

Public script namespaces expose decorators and construction operations. The same underlying builders can also be used directly from Python. The transpiler therefore needs no separate mutable IR representation: it generates calls to this construction protocol.

Ordinary IR constructors can be used directly in parsed source. Shared exports such as I.Call and T.Range use the same constructor contracts as tvm.ir.Call and tvm.ir.Range, including keyword arguments, source locations and validation. Call(..., ty=...) preserves an explicit result type, including Type.missing(). Omitted or None results use available inference and retain Type.missing() when no deduction is available; inference errors propagate. Construction permits provisional IR, and call.validate() checks the registered operator contract separately. Raw printed calls use I.Call with an explicit stored result type to preserve every field. Canonical named operations use the same args, attrs, ty_args and ty construction contract. Concrete printer hooks handle representation-changing syntax; ordinary calls share one formatter.

Operations likewise retain their normal argument contracts. A dtype inferred from operands is not an extra dtype keyword; operations with an explicit dtype parameter accept it normally. Annotation shorthand, module metadata selectors, frame construction and variadic dtype positioning remain explicit builder adapters. They do not change the underlying IR constructor’s validation or consult builder state from ordinary construction.

Printing and round trips#

DocTranslator invokes native type and operation hooks to produce a Doc tree of expressions and statements. The translation engine tracks scopes, names and each Doc’s original IR object. The script entry point maps these origins to diagnostic paths; the private Doc printer formats the tree, annotations and underlines as Python text. Printer configuration stays read-only throughout. The Python document printer helper also formats an existing Doc directly for document-level tests. This tree is separate from the parser’s Python AST.

The public tvm::Script text entry point is declared in tvm/script/printer/printer.h. Its orchestration and diagnostic path mapping live in src/script/printer/printer.cc; doc_translator.h exposes the IR-to-Doc translation protocol.

For example, a small function can be authored, printed and parsed again:

import tvm
from tvm.script import tirx as T

@T.function
def increment(A: T.Tensor((4,), "float32")):
    for i in T.serial(4):
        A[i] = A[i] + 1.0

text = increment.script()
reparsed = tvm.script.from_source(text, extra_vars={"T": T})
tvm.ir.assert_structural_equal(increment, reparsed)

Printed source uses canonical forms rather than preserving the original spelling. Round trips require printable IR and any external objects needed by the source; the text does not serialize arbitrary Python state.

Namespace extension#

A language variant supplies construction hooks and a public script namespace, then registers its aliases with register_namespace. register_namespace_initializer supports lazy setup. tvm.script.register_dialect exposes a package through the public script namespace. Syntax policies are registered beside the operations that need them. IR printing uses FDocTranslate hooks from tvm/script/printer/doc_translator.h, registered through the existing type or operation attributes. A hook returns an expression Doc or emits completed statements through its translator context.

This division keeps source acquisition, syntax translation and formatting shared. Language-specific construction and validation remain with the namespace and its builders.