This is an automated email from the ASF dual-hosted git repository.

tqchen pushed a commit to branch tvmscript-ast-only-transpiler
in repository https://gitbox.apache.org/repos/asf/tvm.git


The following commit(s) were added to refs/heads/tvmscript-ast-only-transpiler 
by this push:
     new 5b7969b327 Own syntax registration in the parser protocol
5b7969b327 is described below

commit 5b7969b3279b8fda3cdefcc2521e2b5ece29ddd5
Author: Tianqi Chen <[email protected]>
AuthorDate: Mon Sep 21 17:16:36 2026 +0000

    Own syntax registration in the parser protocol
    
    Keep expression-string and function-decorator metadata in one parser-owned 
registry, with explicit flat NamedTuple fields. Delegate concrete eager 
annotation behavior to builders and load parser entry points lazily to preserve 
the import boundary.
---
 python/tvm/relax/script/builder/__init__.py |   7 +-
 python/tvm/script/ir_builder/__init__.py    |   6 +-
 python/tvm/script/ir_builder/ir/__init__.py |   3 +-
 python/tvm/script/ir_builder/protocol.py    | 300 +++++++++-------------------
 python/tvm/script/parser/__init__.py        |  37 ++--
 python/tvm/script/parser/expression.py      |   2 +-
 python/tvm/script/parser/frontend.py        |  12 +-
 python/tvm/script/parser/protocol.py        | 192 ++++++++++++++++++
 python/tvm/script/parser/transpile.py       |  23 +--
 python/tvm/tirx/script/builder/__init__.py  |   2 +-
 tests/python/tvmscript/test_parser.py       |  33 +++
 11 files changed, 371 insertions(+), 246 deletions(-)

diff --git a/python/tvm/relax/script/builder/__init__.py 
b/python/tvm/relax/script/builder/__init__.py
index 16e6386287..ac22ef94df 100644
--- a/python/tvm/relax/script/builder/__init__.py
+++ b/python/tvm/relax/script/builder/__init__.py
@@ -37,6 +37,7 @@ from tvm.script.ir_builder import protocol as _protocol
 from tvm.script.ir_builder.type_var_frame import TypeVarDecl as _TypeVarDecl
 from tvm.script.ir_builder.type_var_frame import TypeVarFrame as _TypeVarFrame
 from tvm.script.ir_builder.type_var_frame import resolve_type_var
+from tvm.script.parser.protocol import expr_str_args as _expr_str_args
 
 from . import _ffi_api, distributed, frame, ir
 from . import distributed as dist
@@ -46,7 +47,7 @@ from .distributed.ir import _lookup_device_mesh
 from .ir import *
 
 
-@_protocol.expression_args("shape", introduce=True, dtype="int64", 
scalar_strings=False)
+@_expr_str_args("shape", introduce=True, dtype="int64", scalar_strings=False)
 def Tensor(shape=None, dtype=None, vdevice=None, ndim=-1, *, span=None):
     """Construct a tensor type from resolved shape dimensions.
 
@@ -69,7 +70,7 @@ def Tensor(shape=None, dtype=None, vdevice=None, ndim=-1, *, 
span=None):
     return _relax.TensorType(shape, dtype, vdevice, ndim, 
_protocol.source_span(span))
 
 
-@_protocol.expression_args("shape", introduce=True, dtype="int64", 
scalar_strings=False)
+@_expr_str_args("shape", introduce=True, dtype="int64", scalar_strings=False)
 def DTensor(shape=None, dtype=None, device_mesh=None, placement="", *, 
ndim=-1, span=None):
     """Construct a concrete distributed tensor type with resolved shape 
symbols."""
     if device_mesh is None:
@@ -86,7 +87,7 @@ def DTensor(shape=None, dtype=None, device_mesh=None, 
placement="", *, ndim=-1,
 Range = _ir.Range
 
 
-@_protocol.expression_args("values", introduce=True, dtype="int64")
+@_expr_str_args("values", introduce=True, dtype="int64")
 def Shape(values=None, ndim=-1, *, span=None):
     """Construct a shape type from concrete dimension expressions.
 
diff --git a/python/tvm/script/ir_builder/__init__.py 
b/python/tvm/script/ir_builder/__init__.py
index 30f34d0a51..398759b181 100644
--- a/python/tvm/script/ir_builder/__init__.py
+++ b/python/tvm/script/ir_builder/__init__.py
@@ -35,11 +35,15 @@ import importlib
 from typing import Any
 
 from .base import IRBuilder
-from .protocol import MISSING, ExprStrPolicy, _at, expr_str_args, 
expression_args
+from .protocol import MISSING, _at
 from .type_var_frame import TypeVarDecl, TypeVarFrame
 
 
 def __getattr__(name: str) -> Any:
+    if name in {"ExprStrPolicy", "expr_str_args", "expression_args"}:
+        from tvm.script.parser import protocol
+
+        return getattr(protocol, name)
     # Lazy import to avoid loading tvm.script during dialect bootstrap.
     from tvm.script import _DIALECT_REGISTRY  # pylint: 
disable=import-outside-toplevel
 
diff --git a/python/tvm/script/ir_builder/ir/__init__.py 
b/python/tvm/script/ir_builder/ir/__init__.py
index 04a973c307..16608098a7 100644
--- a/python/tvm/script/ir_builder/ir/__init__.py
+++ b/python/tvm/script/ir_builder/ir/__init__.py
@@ -17,7 +17,8 @@
 # under the License.
 """Package tvm.script.ir_builder.ir"""
 
-from ..protocol import MISSING, _at, expression_args
+from ..protocol import MISSING, _at
+from tvm.script.parser.protocol import expr_str_args as expression_args
 from .frame import IRModuleFrame
 from .ir import (
     reserve_function,
diff --git a/python/tvm/script/ir_builder/protocol.py 
b/python/tvm/script/ir_builder/protocol.py
index b6247e3760..a22153c176 100644
--- a/python/tvm/script/ir_builder/protocol.py
+++ b/python/tvm/script/ir_builder/protocol.py
@@ -14,20 +14,18 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-"""Shared construction metadata for explicit TVMScript builders.
+"""Concrete runtime helpers for explicit TVMScript builders.
 
-Metadata describes source syntax. Decorated constructors return concrete values
-or a composable missing type during unresolved eager annotation evaluation.
-Dialects register their own policies here, so translation reads metadata 
without
-importing dialects or evaluating annotations.
+Expression-string and function-decorator registration live in parser.protocol.
+This module implements their eager-call adapter, concrete source spans and
+construction-time control helpers. Unresolved eager annotations return a
+composable missing type; handwritten builder calls require concrete arguments.
 """
 
 from builtins import locals as locals
 from builtins import slice as slice
 from contextlib import contextmanager, nullcontext
-from dataclasses import dataclass
 from functools import wraps
-from inspect import signature
 from typing import Any, NamedTuple, TypeVar
 
 from tvm import ir
@@ -45,173 +43,77 @@ class _Missing:
 MISSING = _Missing()
 
 
-class ExprStrPolicy(NamedTuple):
-    """Immutable syntax policy for registered constructor arguments.
+def wrap_expression_constructor(constructor, call_signature, policy, *, 
as_type=False):
+    """Create the concrete eager-call adapter for parser-owned syntax metadata.
 
-    ``fields`` names parameters whose strings represent expressions.
-    ``scalar_strings`` controls bare strings; nested strings are always marked.
-    fields is a tuple of parameter-name strings; scalar_strings defaults True.
-    introduce (False), dtype (None), implicit_dtype (None), and
-    compound_declarations (False) retain legacy registration metadata only.
-    They never drive transpiler symbol construction or type validation; those
-    operations belong exclusively to builders. Construction returns this tuple
-    record and has no builder effects; NamedTuple enforces field arity only.
-    Instances live as long as their registered callable, across all 
transpilation
-    passes, and hold no symbols, eager results, or function-local state.
-    """
-
-    fields: tuple[str, ...]
-    introduce: bool = False
-    dtype: Any = None
-    scalar_strings: bool = True
-    implicit_dtype: Any = None
-    compound_declarations: bool = False
-
-
-# Process-wide registry: callable identity -> immutable syntax policy. Dialect
-# imports register once; aliases share identities. No per-function entries or
-# evaluation results are cached, and transpilers only read this table.
-_EXPR_STR_POLICIES = {}
+    constructor is the registered callable; call_signature is its inspected
+    Python signature; policy is immutable metadata with marked field names and
+    scalar_strings. as_type defaults False; True preserves annotation-class and
+    type-union use. Returns a wrapped callable/class with the original 
signature.
+    Calling it resolves host TypeVars in active function frames, returns a
+    composable MissingType for unresolved eager annotations, and rejects 
strings
+    in handwritten builders. Constructor/type/binding errors propagate.
 
-
-def expr_str_policy(constructor):
-    """Return a callable's ExprStrPolicy, or None when it is unregistered.
-
-    ``constructor`` may be any Python value. This read-only lookup neither
-    imports dialects nor evaluates the value and has no builder/frame effects.
-    """
-    try:
-        return _EXPR_STR_POLICIES.get(constructor)
-    except TypeError:
-        return None
-
-
-def expr_str_args(
-    *fields,
-    introduce=False,
-    dtype=None,
-    scalar_strings=True,
-    implicit_dtype=None,
-    compound_declarations=False,
-    as_type=False,
-):
-    """Register expression-string fields and wrap eager constructor calls.
-
-    Parameters
-    ----------
-    fields : str
-        Named parameters whose string values denote source expressions. Unknown
-        parameter names raise ValueError when applying the decorator.
-    introduce : bool, optional
-        Legacy syntax metadata, default False. Builders own symbol 
introduction.
-    dtype : object, optional
-        Legacy explicit symbol-type metadata, default None.
-    scalar_strings : bool, optional
-        Whether bare strings are expressions, default True. False preserves
-        literal shorthand such as ``Tensor("float32")``; nested strings in
-        tuples/lists remain expressions.
-    implicit_dtype : object, optional
-        Legacy implicit-type metadata, default None; newly resolved symbols
-        default to int32 in TypeVarFrame.
-    compound_declarations : bool, optional
-        Legacy expression metadata, default False.
-    as_type : bool, optional
-        Preserve use as an annotation class (including Python type unions),
-        default False. True returns a class whose construction invokes the
-        wrapped callable; its instances are still the callable's return values.
-
-    Returns
-    -------
-    decorator : callable
-        Registers and wraps a constructor. Unresolved strings or typing.TypeVar
-        values in marked fields return a concrete ``ir.MissingType()`` during
-        eager Python annotation evaluation. Concrete arguments call the 
original
-        constructor. Inside an active builder, unresolved inputs raise 
TypeError:
-        handwritten builders require concrete expressions. Calls are never
-        cached and annotations must be safe to re-evaluate. Ordinary 
call-binding
-        errors propagate from inspect.Signature.bind.
+    This factory owns no registry. Its closure retains only callable/signature/
+    policy configuration for that callable's lifetime, never annotation results
+    or symbols. Each invocation uses the current native builder frame stack.
     """
-
-    def decorate(constructor):
-        call_signature = signature(constructor)
-        unknown = set(fields).difference(call_signature.parameters)
-        if unknown:
-            raise ValueError(f"Unknown expression argument fields: 
{sorted(unknown)}")
-        policy = ExprStrPolicy(
-            tuple(fields),
-            bool(introduce),
-            dtype,
-            bool(scalar_strings),
-            implicit_dtype,
-            bool(compound_declarations),
-        )
-
-        def unresolved(value, nested=False):
-            if isinstance(value, str):
-                return nested or policy.scalar_strings
-            if isinstance(value, TypeVar):
-                return True
-            if isinstance(value, tuple | list):
-                return any(unresolved(item, True) for item in value)
-            return False
-
-        @wraps(constructor)
-        def invoke(*args, **kwargs):
-            bound = call_signature.bind(*args, **kwargs)
+    fields = policy.fields
+
+    def unresolved(value, nested=False):
+        if isinstance(value, str):
+            return nested or policy.scalar_strings
+        if isinstance(value, TypeVar):
+            return True
+        if isinstance(value, tuple | list):
+            return any(unresolved(item, True) for item in value)
+        return False
+
+    @wraps(constructor)
+    def invoke(*args, **kwargs):
+        bound = call_signature.bind(*args, **kwargs)
+        if IRBuilder.is_in_scope():
+            # typing.TypeVar is ordinary eager Python metadata. Resolve it
+            # here, never in the syntax-only transpiler.
+            from .type_var_frame import TypeVarFrame
+
+            def resolve(value):
+                if isinstance(value, TypeVar):
+                    if value.__bound__ is not None or value.__constraints__:
+                        raise TypeError("A symbolic TypeVar cannot have 
constraints or a bound")
+                    return TypeVarFrame.current().resolve(value.__name__)
+                if isinstance(value, tuple):
+                    return tuple(resolve(item) for item in value)
+                if isinstance(value, list):
+                    return [resolve(item) for item in value]
+                return value
+
+            for field in fields:
+                if field in bound.arguments:
+                    bound.arguments[field] = resolve(bound.arguments[field])
+        if any(unresolved(bound.arguments[field]) for field in fields if field 
in bound.arguments):
             if IRBuilder.is_in_scope():
-                # typing.TypeVar is ordinary eager Python metadata. Resolve it
-                # here, never in the syntax-only transpiler.
-                from .type_var_frame import TypeVarFrame
-
-                def resolve(value):
-                    if isinstance(value, TypeVar):
-                        if value.__bound__ is not None or 
value.__constraints__:
-                            raise TypeError("A symbolic TypeVar cannot have 
constraints or a bound")
-                        return TypeVarFrame.current().resolve(value.__name__)
-                    if isinstance(value, tuple):
-                        return tuple(resolve(item) for item in value)
-                    if isinstance(value, list):
-                        return [resolve(item) for item in value]
-                    return value
-
-                for field in fields:
-                    if field in bound.arguments:
-                        bound.arguments[field] = 
resolve(bound.arguments[field])
-            if any(
-                unresolved(bound.arguments[field]) for field in fields if 
field in bound.arguments
-            ):
-                if IRBuilder.is_in_scope():
-                    raise TypeError(
-                        "Builder expression arguments require concrete 
symbols, not strings"
-                    )
-                return ir.MissingType()
-            return constructor(*bound.args, **bound.kwargs)
-
-        result = invoke
-        if as_type:
-            # The class is an annotation surface, not an IR or proxy type.
-            # __new__ returns the concrete construction result (or 
MissingType).
-            result = type(
-                constructor.__name__,
-                (),
-                {
-                    "__new__": lambda cls, *args, **kwargs: invoke(*args, 
**kwargs),
-                    "__signature__": call_signature,
-                    "__doc__": constructor.__doc__,
-                    "__module__": constructor.__module__,
-                },
-            )
-        result.__tvm_expression_args__ = policy
-        _EXPR_STR_POLICIES[result] = policy
-        _EXPR_STR_POLICIES[constructor] = policy
-        return result
-
-    return decorate
-
-
-# Compatibility spellings share the registry and wrapper implementation.
-ExpressionArguments = ExprStrPolicy
-expression_args = expr_str_args
+                raise TypeError(
+                    "Builder expression arguments require concrete symbols, 
not strings"
+                )
+            return ir.MissingType()
+        return constructor(*bound.args, **bound.kwargs)
+
+    result = invoke
+    if as_type:
+        # The class is an annotation surface, not an IR or proxy type.
+        # __new__ returns the concrete construction result (or MissingType).
+        result = type(
+            constructor.__name__,
+            (),
+            {
+                "__new__": lambda cls, *args, **kwargs: invoke(*args, 
**kwargs),
+                "__signature__": call_signature,
+                "__doc__": constructor.__doc__,
+                "__module__": constructor.__module__,
+            },
+        )
+    return result
 
 
 class DeclarationArguments(NamedTuple):
@@ -241,44 +143,6 @@ def register_declaration(constructor, *, 
value_parameter="expr", dtype=None):
     return constructor
 
 
-@dataclass(frozen=True)
-class FunctionKind:
-    """Builder namespace and syntax options attached to a function decorator.
-
-    builder is the dialect construction namespace; metadata maps string policy
-    names to values (for example option_map/defaults). Registration owns this
-    record for the decorator's lifetime; transpilers read it across functions.
-    No concrete function, frame, annotation result, or symbol is stored here.
-    """
-
-    builder: Any
-    metadata: dict
-
-
-def register_function(decorator, builder, **metadata):
-    """Associate a decorator with its builder namespace and syntax policies.
-
-    ``decorator`` must support attribute assignment; ``builder`` is its dialect
-    construction namespace and ``metadata`` contains named syntax policies.
-    Returns the same decorator after replacing its FunctionKind record. No
-    construction or annotation evaluation occurs. Unsupported attribute
-    assignment raises the ordinary Python error; metadata is retained for the
-    decorator's lifetime and must not be mutated by a transpilation pass.
-    """
-    decorator.__tvm_function_kind__ = FunctionKind(builder, metadata)
-    return decorator
-
-
-def function_kind(decorator):
-    """Return a decorator's FunctionKind record, or None when unregistered.
-
-    ``decorator`` is any already resolved Python value. This lookup neither
-    evaluates it nor enters frames; ordinary custom attribute-access errors
-    propagate. Returned metadata is shared and must be treated as read-only.
-    """
-    return getattr(decorator, "__tvm_function_kind__", None)
-
-
 def source_span(location):
     """Materialize an IR span from a source location, or preserve an existing 
span.
 
@@ -458,3 +322,23 @@ def callee(builder, value):
         if isinstance(value, value_type):
             return adapter(value)
     return value
+
+
+def __getattr__(name):
+    # Compatibility exports forward to the single parser-owned registry. Lazy
+    # lookup avoids importing parser entry points during builder 
initialization.
+    aliases = {"function_kind": "function_info"}
+    exported = {
+        "ExprStrPolicy",
+        "ExpressionArguments",
+        "expr_str_args",
+        "expression_args",
+        "expr_str_policy",
+        "register_function",
+        "function_kind",
+    }
+    if name in exported:
+        from tvm.script.parser import protocol as syntax_protocol
+
+        return getattr(syntax_protocol, aliases.get(name, name))
+    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
diff --git a/python/tvm/script/parser/__init__.py 
b/python/tvm/script/parser/__init__.py
index 6d9941886d..22e8585bb4 100644
--- a/python/tvm/script/parser/__init__.py
+++ b/python/tvm/script/parser/__init__.py
@@ -23,21 +23,30 @@ those of the original source. Entry modules register 
policies; this parser never
 imports their namespaces.
 """
 
-from .frontend import (
-    _NAMESPACES,
-    from_source,
-    ir_module,
-    make_decorator,
-    make_helper,
-    parse,
-    pyfunc,
-    register_namespace,
+import importlib
+import sys
+
+# Process-wide fixed export names, never per-function construction state. Lazy
+# entry loading permits builder imports to register syntax policy without a
+# parser/frontend/builder-construction import cycle.
+_FRONTEND_EXPORTS = (
+    "_NAMESPACES",
+    "from_source",
+    "ir_module",
+    "make_decorator",
+    "make_helper",
+    "parse",
+    "pyfunc",
+    "register_namespace",
 )
+__all__ = [name for name in _FRONTEND_EXPORTS if not name.startswith("_")]
 
 
 def __getattr__(name):
-    # Entry modules register source aliases; parsing never imports their 
owners.
-    try:
-        return _NAMESPACES[name]
-    except KeyError:
-        raise AttributeError(f"module {__name__!r} has no attribute {name!r}") 
from None
+    if name in _FRONTEND_EXPORTS:
+        frontend = importlib.import_module(f"{__name__}.frontend")
+        return getattr(frontend, name)
+    frontend = sys.modules.get(f"{__name__}.frontend")
+    if frontend is not None and name in getattr(frontend, "_NAMESPACES", {}):
+        return frontend._NAMESPACES[name]
+    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
diff --git a/python/tvm/script/parser/expression.py 
b/python/tvm/script/parser/expression.py
index c8b31c0669..0efc15d966 100644
--- a/python/tvm/script/parser/expression.py
+++ b/python/tvm/script/parser/expression.py
@@ -29,7 +29,7 @@ import inspect
 import linecache
 import re
 
-from tvm.script.ir_builder import protocol
+from . import protocol
 
 
 class _LiteralParser:
diff --git a/python/tvm/script/parser/frontend.py 
b/python/tvm/script/parser/frontend.py
index c613835855..ea6516cff7 100644
--- a/python/tvm/script/parser/frontend.py
+++ b/python/tvm/script/parser/frontend.py
@@ -30,6 +30,7 @@ from typing import TypeVar
 from tvm.error import DiagnosticError
 from tvm.script.ir_builder import construction, protocol
 
+from . import protocol as syntax_protocol
 from .diagnostics import diagnostic_error
 from .transpile import IRBuilderTranspiler, NameCollector
 
@@ -117,7 +118,7 @@ def make_decorator(builder, *, option_map=None, 
defaults=None):
             raise ValueError("Construction decorators require a function or 
keyword options")
 
         def apply(function):
-            function.__tvm_function_kind__ = decorator.__tvm_function_kind__
+            function.__tvm_function_info__ = decorator.__tvm_function_info__
             function.__tvm_function_options__ = options
             if _inside_class(function):
                 return function
@@ -125,7 +126,7 @@ def make_decorator(builder, *, option_map=None, 
defaults=None):
 
         return apply(function) if function is not None else apply
 
-    return protocol.register_function(
+    return syntax_protocol.register_function(
         decorator, builder, option_map=mapping, defaults=default_options
     )
 
@@ -191,7 +192,7 @@ def pyfunc(function):
     return function
 
 
-protocol.register_function(pyfunc, None, python=True)
+syntax_protocol.register_function(pyfunc, None, python=True)
 
 
 class Compiler:
@@ -383,7 +384,10 @@ class Compiler:
             exec(
                 compile(ast.Module(imports, []), self.filename, "exec", 
dont_inherit=True), self.env
             )
-        if inspect.isfunction(self.original) and 
protocol.function_kind(self.original) is not None:
+        if (
+            inspect.isfunction(self.original)
+            and syntax_protocol.function_info(self.original) is not None
+        ):
             # Direct decorator application (T.prim_func(host_function)) has no
             # decorator in source AST. Inject only its registered host 
metadata;
             # already evaluated option values stay opaque execution bindings.
diff --git a/python/tvm/script/parser/protocol.py 
b/python/tvm/script/parser/protocol.py
new file mode 100644
index 0000000000..82f38d9eb9
--- /dev/null
+++ b/python/tvm/script/parser/protocol.py
@@ -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.
+"""Parser-owned syntax policies and function-decorator registration.
+
+This module stores only host callable identities and flat syntax metadata.
+Concrete eager annotation behavior is delegated to a builder-owned adapter;
+no IR definition, concrete annotation result, symbol or frame is stored here.
+"""
+
+from inspect import signature
+from typing import Any, NamedTuple
+
+
+class ExprStrPolicy(NamedTuple):
+    """Immutable syntax policy for registered constructor arguments.
+
+    ``fields`` names parameters whose strings represent expressions.
+    ``scalar_strings`` controls bare strings; nested strings are always marked.
+    fields is a tuple of parameter-name strings; scalar_strings defaults True.
+    introduce (False), dtype (None), implicit_dtype (None), and
+    compound_declarations (False) retain legacy registration metadata only.
+    They never drive transpiler symbol construction or type validation; those
+    operations belong exclusively to builders. Construction returns this tuple
+    record and has no builder effects; NamedTuple enforces field arity only.
+    Instances live as long as their registered callable, across all 
transpilation
+    passes, and hold no symbols, eager results, or function-local state.
+    """
+
+    fields: tuple[str, ...]
+    introduce: bool = False
+    dtype: Any = None
+    scalar_strings: bool = True
+    implicit_dtype: Any = None
+    compound_declarations: bool = False
+
+
+# Process-wide registry: callable identity -> immutable syntax policy. Dialect
+# imports register once; aliases share identities. No per-function entries or
+# evaluation results are cached, and transpilers only read this table.
+_EXPR_STR_POLICIES = {}
+
+
+def expr_str_policy(constructor):
+    """Return a callable's ExprStrPolicy, or None when it is unregistered.
+
+    ``constructor`` may be any Python value. This read-only lookup neither
+    imports dialects nor evaluates the value and has no builder/frame effects.
+    """
+    try:
+        return _EXPR_STR_POLICIES.get(constructor)
+    except TypeError:
+        return None
+
+
+def expr_str_args(
+    *fields,
+    introduce=False,
+    dtype=None,
+    scalar_strings=True,
+    implicit_dtype=None,
+    compound_declarations=False,
+    as_type=False,
+):
+    """Register expression-string fields and wrap eager constructor calls.
+
+    Parameters
+    ----------
+    fields : str
+        Named parameters whose string values denote source expressions. Unknown
+        parameter names raise ValueError when applying the decorator.
+    introduce : bool, optional
+        Legacy syntax metadata, default False. Builders own symbol 
introduction.
+    dtype : object, optional
+        Legacy explicit symbol-type metadata, default None.
+    scalar_strings : bool, optional
+        Whether bare strings are expressions, default True. False preserves
+        literal shorthand such as ``Tensor("float32")``; nested strings in
+        tuples/lists remain expressions.
+    implicit_dtype : object, optional
+        Legacy implicit-type metadata, default None; newly resolved symbols
+        default to int32 in TypeVarFrame.
+    compound_declarations : bool, optional
+        Legacy expression metadata, default False.
+    as_type : bool, optional
+        Preserve use as an annotation class (including Python type unions),
+        default False. True returns a class whose construction invokes the
+        wrapped callable; its instances are still the callable's return values.
+
+    Returns
+    -------
+    decorator : callable
+        Registers and wraps a constructor. Unresolved strings or typing.TypeVar
+        values in marked fields return a concrete ``ir.MissingType()`` during
+        eager Python annotation evaluation. Concrete arguments call the 
original
+        constructor. Inside an active builder, unresolved inputs raise 
TypeError:
+        handwritten builders require concrete expressions. Calls are never
+        cached and annotations must be safe to re-evaluate. Ordinary 
call-binding
+        errors propagate from inspect.Signature.bind.
+    """
+
+    def decorate(constructor):
+        call_signature = signature(constructor)
+        unknown = set(fields).difference(call_signature.parameters)
+        if unknown:
+            raise ValueError(f"Unknown expression argument fields: 
{sorted(unknown)}")
+        policy = ExprStrPolicy(
+            tuple(fields),
+            bool(introduce),
+            dtype,
+            bool(scalar_strings),
+            implicit_dtype,
+            bool(compound_declarations),
+        )
+
+        # Registration is syntax-only. Builders own eager construction and the
+        # active-frame/MissingType decisions behind this generic wrapper 
factory.
+        from tvm.script.ir_builder.protocol import wrap_expression_constructor
+
+        result = wrap_expression_constructor(constructor, call_signature, 
policy, as_type=as_type)
+        result.__tvm_expression_args__ = policy
+        _EXPR_STR_POLICIES[result] = policy
+        _EXPR_STR_POLICIES[constructor] = policy
+        return result
+
+    return decorate
+
+
+# Compatibility spellings share the registry and wrapper implementation.
+ExpressionArguments = ExprStrPolicy
+expression_args = expr_str_args
+
+
+class FunctionDecoratorInfo(NamedTuple):
+    """Flat syntax registration for a source function decorator.
+
+    builder is the opaque construction namespace, or None for ordinary Python.
+    option_map optionally maps public option names to builder option names;
+    defaults optionally maps builder option names to default Python constants.
+    Both default None (empty) and are copied by register_function. python 
defaults
+    False; True preserves the source function body as ordinary Python. These 
are
+    the complete supported fields: no generic metadata dictionary is retained.
+    Records live with their decorators across compilations; mappings are 
read-only
+    to consumers and contain no frame, function result, annotation or symbol 
state.
+    NamedTuple enforces field arity; construction enters no builder frames.
+    """
+
+    builder: Any
+    option_map: dict | None = None
+    defaults: dict | None = None
+    python: bool = False
+
+
+def register_function(decorator, builder, *, option_map=None, defaults=None, 
python=False):
+    """Register and return a decorator with explicit supported syntax options.
+
+    decorator must allow attribute assignment. builder is an opaque 
construction
+    namespace, or None with python=True. option_map/defaults are optional 
mappings
+    copied into the flat FunctionDecoratorInfo; omitted mappings mean empty.
+    python defaults False and selects ordinary host-function preservation when
+    True. Registration replaces __tvm_function_info__ for the callable's 
lifetime.
+    No constructor or annotation executes and no frame is entered. Invalid
+    mappings/attribute writes raise ordinary Python errors; unknown keywords 
are
+    rejected by this explicit signature rather than retained as arbitrary 
policy.
+    """
+    decorator.__tvm_function_info__ = FunctionDecoratorInfo(
+        builder, dict(option_map or {}), dict(defaults or {}), bool(python)
+    )
+    return decorator
+
+
+def function_info(decorator):
+    """Return FunctionDecoratorInfo for a resolved callable, or None.
+
+    decorator may be any already-resolved host value. This read-only lookup
+    neither evaluates it nor enters frames. Custom attribute errors propagate;
+    returned option mappings are shared registration state and must not be 
edited.
+    """
+    return getattr(decorator, "__tvm_function_info__", None)
diff --git a/python/tvm/script/parser/transpile.py 
b/python/tvm/script/parser/transpile.py
index ea15baea82..b90adb965c 100644
--- a/python/tvm/script/parser/transpile.py
+++ b/python/tvm/script/parser/transpile.py
@@ -26,8 +26,7 @@ import builtins
 import copy
 import inspect
 
-from tvm.script.ir_builder import protocol
-
+from . import protocol
 from .expression import rewrite_expression
 
 
@@ -884,7 +883,7 @@ class IRBuilderTranspiler(ast.NodeTransformer):
         # Ordinary construction helpers retain normal Python execution. 
Registered
         # function-kind entry points are resolved by the enclosing compiler 
callback.
         if any(
-            getattr(self._resolve(decorator), "__tvm_function_kind__", None)
+            getattr(self._resolve(decorator), "__tvm_function_info__", None)
             for decorator in node.decorator_list
         ):
             self._error(node, "A registered nested function requires a 
function compiler")
@@ -903,18 +902,16 @@ class IRBuilderTranspiler(ast.NodeTransformer):
         """Read registered decorator metadata without evaluating annotations.
 
         node is an original FunctionDef; allow_python permits unregistered 
nested
-        Python helpers. Returns (FunctionKind, options AST). Raises SyntaxError
+        Python helpers. Returns (FunctionDecoratorInfo, options AST). Raises 
SyntaxError
         for an undecorated IR function or positional construction options.
         """
         for decorator in node.decorator_list:
             target = decorator.func if isinstance(decorator, ast.Call) else 
decorator
-            kind = protocol.function_kind(self._resolve(target))
+            kind = protocol.function_info(self._resolve(target))
             if kind is None:
                 continue
-            mapping = kind.metadata.get("option_map", {})
-            values = {
-                key: ast.Constant(value) for key, value in 
kind.metadata.get("defaults", {}).items()
-            }
+            mapping = kind.option_map or {}
+            values = {key: ast.Constant(value) for key, value in 
(kind.defaults or {}).items()}
             expansions = []
             if isinstance(decorator, ast.Call):
                 if decorator.args:
@@ -930,7 +927,7 @@ class IRBuilderTranspiler(ast.NodeTransformer):
             )
             return kind, ast.copy_location(result, node)
         if allow_python:
-            return protocol.FunctionKind(None, {"python": True}), ast.Dict([], 
[])
+            return protocol.FunctionDecoratorInfo(None, python=True), 
ast.Dict([], [])
         self._error(node, f"Function {node.name!r} has no registered 
construction kind")
 
     def function_program(self, node, runtime, bindings, *, local=False):
@@ -950,7 +947,7 @@ class IRBuilderTranspiler(ast.NodeTransformer):
         # A factory gives each function an ordinary Python closure: parameters 
and
         # symbols from sibling signatures never overwrite one another's 
captures.
         kind, options = self.function_metadata(node)
-        if kind.metadata.get("python"):
+        if kind.python:
             return None
         builder = self.fresh("_t")
         bindings[builder] = kind.builder
@@ -1099,7 +1096,7 @@ class IRBuilderTranspiler(ast.NodeTransformer):
 
         def nested(child):
             nested_kind, _ = self.function_metadata(child, allow_python=True)
-            if nested_kind.metadata.get("python"):
+            if nested_kind.python:
                 return copy.deepcopy(child)
             statements, child_record, child_body = self.function_program(
                 child, runtime, bindings, local=True
@@ -1287,7 +1284,7 @@ class IRBuilderTranspiler(ast.NodeTransformer):
         records = []
         for function in functions:
             kind, _ = self.function_metadata(function)
-            if kind.metadata.get("python"):
+            if kind.python:
                 # Pattern: @I.pyfunc def f(...): body -> unchanged host 
function
                 # plus opaque builder registration.
                 host = copy.deepcopy(function)
diff --git a/python/tvm/tirx/script/builder/__init__.py 
b/python/tvm/tirx/script/builder/__init__.py
index 57b8c8b98a..ae63a3535d 100644
--- a/python/tvm/tirx/script/builder/__init__.py
+++ b/python/tvm/tirx/script/builder/__init__.py
@@ -32,7 +32,6 @@ from tvm.script.ir_builder import ir as _I
 from tvm.script.ir_builder.base import IRBuilderFrame as _NativeFrame
 from tvm.script.ir_builder.protocol import MISSING as _MISSING
 from tvm.script.ir_builder.protocol import at as _at
-from tvm.script.ir_builder.protocol import expression_args as _expression_args
 from tvm.script.ir_builder.protocol import register_call_kind as 
_register_call_kind
 from tvm.script.ir_builder.protocol import register_declaration as 
_register_declaration
 from tvm.script.ir_builder.protocol import source_span as _source_span
@@ -40,6 +39,7 @@ from tvm.script.ir_builder.protocol import span_context as 
_span_context
 from tvm.script.ir_builder.type_var_frame import TypeVarDecl as _TypeVarDecl
 from tvm.script.ir_builder.type_var_frame import TypeVarFrame as _TypeVarFrame
 from tvm.script.ir_builder.type_var_frame import resolve_type_var
+from tvm.script.parser.protocol import expr_str_args as _expression_args
 from tvm.tirx.lang.alloc_pool import SMEMPool, TMEMPool
 
 from . import _ffi_api
diff --git a/tests/python/tvmscript/test_parser.py 
b/tests/python/tvmscript/test_parser.py
index 61195c72f2..c1ca3e4207 100644
--- a/tests/python/tvmscript/test_parser.py
+++ b/tests/python/tvmscript/test_parser.py
@@ -33,6 +33,7 @@ from tvm import ir
 from tvm.ir.prim import Cast
 from tvm.script import parser
 from tvm.script.ir_builder import IRBuilder, protocol
+from tvm.script.parser import protocol as syntax_protocol
 from tvm.script.parser.frontend import Compiler, make_decorator
 from tvm.script.parser.transpile import IRBuilderTranspiler
 
@@ -461,3 +462,35 @@ def test_shared_allocator_preserves_source_names():
     transformer = compiler.transformer()
     assert transformer.fresh_unique_name("value") != "value0"
     assert transformer.fresh_unique_name() not in {"_t0", "_t1"}
+
+
+def test_parser_metadata_registration_boundary():
+    options, defaults = {"private": "local"}, {"local": False}
+
+    def decorator():
+        pass
+
+    syntax_protocol.register_function(decorator, _Recorder(), 
option_map=options, defaults=defaults)
+    info = syntax_protocol.function_info(decorator)
+    assert isinstance(info, tuple)
+    assert info._fields == ("builder", "option_map", "defaults", "python")
+    assert not hasattr(info, "metadata")
+    options["private"] = "changed"
+    assert info.option_map == {"private": "local"}
+    with pytest.raises(TypeError, match="unexpected keyword"):
+        syntax_protocol.register_function(decorator, None, unknown=True)
+
+    @syntax_protocol.expr_str_args("shape")
+    def constructor(shape):
+        return shape
+
+    policy = syntax_protocol.expr_str_policy(constructor)
+    assert isinstance(policy, tuple)
+    assert policy.fields == ("shape",)
+    assert policy.__class__.__module__ == "tvm.script.parser.protocol"
+    assert constructor((4,)) == (4,)
+    assert constructor(("n",)).is_missing()
+    # Existing metadata imports forward to the same parser-owned registration.
+    assert protocol.expr_str_policy(constructor) is policy
+    with IRBuilder(), pytest.raises(TypeError, match="concrete symbols"):
+        constructor(("n",))

Reply via email to