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

tqchen pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tvm.git


The following commit(s) were added to refs/heads/main by this push:
     new 615d506e0e [REFACTOR][Script] Simplify annotation scopes and signature 
variables (#20456)
615d506e0e is described below

commit 615d506e0eac781284f472bbf3c185235dc1c76e
Author: Tianqi Chen <[email protected]>
AuthorDate: Fri Sep 25 15:18:57 2026 -0400

    [REFACTOR][Script] Simplify annotation scopes and signature variables 
(#20456)
---
 include/tvm/relax/script/ir_builder/frame.h        |   3 -
 include/tvm/script/ir_builder/base.h               |   5 -
 include/tvm/tirx/script/ir_builder/frame.h         |   3 -
 python/tvm/relax/script/ir_builder/frame.py        |  10 +-
 python/tvm/relax/script/ir_builder/ir.py           |   4 +-
 .../tvm/relax/script/ir_builder/parser_protocol.py |  18 +-
 python/tvm/script/ir_builder/base.py               |  26 --
 python/tvm/script/ir_builder/parser_protocol.py    |  52 +---
 python/tvm/script/parser/entry.py                  |   7 +-
 python/tvm/script/parser/inspect_source.py         |   6 +-
 python/tvm/script/parser/prescan.py                |  95 +++---
 python/tvm/script/parser/protocol_registry.py      |   2 +-
 python/tvm/script/parser/transpile.py              | 336 +++++++++++----------
 python/tvm/tirx/script/ir_builder/frame.py         |   8 +-
 .../tvm/tirx/script/ir_builder/parser_protocol.py  |  16 +-
 src/relax/analysis/well_formed.cc                  |  14 +-
 src/relax/script/ir_builder/ir.cc                  |   4 -
 src/script/ir_builder/base.cc                      |  13 -
 src/tirx/script/ir_builder/ir.cc                   |   4 -
 tests/python/relax/test_tvmscript_parser.py        |  24 +-
 tests/python/script/minilang.py                    |  21 +-
 tests/python/script/test_parser_entry.py           |   3 +-
 .../python/tvmscript/test_tvmscript_parser_tir.py  |  14 +-
 23 files changed, 292 insertions(+), 396 deletions(-)

diff --git a/include/tvm/relax/script/ir_builder/frame.h 
b/include/tvm/relax/script/ir_builder/frame.h
index 8ddba3659d..ad63178dbd 100644
--- a/include/tvm/relax/script/ir_builder/frame.h
+++ b/include/tvm/relax/script/ir_builder/frame.h
@@ -97,8 +97,6 @@ class FunctionFrameNode : public SeqExprFrameNode {
    *       However, we must specify the name by `R.func_name` before exit this 
frame.
    */
   ffi::Optional<ffi::String> name;
-  /*! \brief Function-local symbols retained across declaration and body 
entry. */
-  ffi::Map<ffi::String, tvm::Var> type_var_map;
   /*! \brief The function params. */
   ffi::Array<tvm::Var> params;
   /*!
@@ -130,7 +128,6 @@ class FunctionFrameNode : public SeqExprFrameNode {
   static void RegisterReflection() {
     namespace refl = tvm::ffi::reflection;
     refl::ObjectDef<FunctionFrameNode>()
-        .def_ro("type_var_map", &FunctionFrameNode::type_var_map)
         .def_ro("name", &FunctionFrameNode::name)
         .def_ro("params", &FunctionFrameNode::params)
         .def_ro("ret_ty", &FunctionFrameNode::ret_ty)
diff --git a/include/tvm/script/ir_builder/base.h 
b/include/tvm/script/ir_builder/base.h
index ff9c798618..ee7cd9d72b 100644
--- a/include/tvm/script/ir_builder/base.h
+++ b/include/tvm/script/ir_builder/base.h
@@ -111,11 +111,6 @@ class IRBuilderFrame : public ffi::ObjectRef {
   }
 };
 
-/*! \brief Resolve one symbol in a native function frame's retained map. */
-TVM_DLL tvm::Var ResolveTypeVar(ffi::Map<ffi::String, tvm::Var>* symbols, 
const ffi::String& name,
-                                ffi::Optional<PrimType> dtype, 
ffi::Optional<tvm::Var> value,
-                                Span span);
-
 ////////////////////////////// IRBuilder //////////////////////////////
 
 /*!
diff --git a/include/tvm/tirx/script/ir_builder/frame.h 
b/include/tvm/tirx/script/ir_builder/frame.h
index 7836276b56..72defaff79 100644
--- a/include/tvm/tirx/script/ir_builder/frame.h
+++ b/include/tvm/tirx/script/ir_builder/frame.h
@@ -74,8 +74,6 @@ class TIRFrame : public IRBuilderFrame {
  */
 class PrimFuncFrameNode : public TIRFrameNode {
  public:
-  /*! \brief Function-local symbols retained across declaration and body 
entry. */
-  ffi::Map<ffi::String, tvm::Var> type_var_map;
   /*! \brief The name of the block. */
   ffi::Optional<ffi::String> name;
   /*! \brief Function parameters. */
@@ -101,7 +99,6 @@ class PrimFuncFrameNode : public TIRFrameNode {
   static void RegisterReflection() {
     namespace refl = tvm::ffi::reflection;
     refl::ObjectDef<PrimFuncFrameNode>()
-        .def_ro("type_var_map", &PrimFuncFrameNode::type_var_map)
         .def_ro("name", &PrimFuncFrameNode::name)
         .def_ro("args", &PrimFuncFrameNode::args)
         .def_ro("is_private", &PrimFuncFrameNode::is_private)
diff --git a/python/tvm/relax/script/ir_builder/frame.py 
b/python/tvm/relax/script/ir_builder/frame.py
index e81d11808b..3ffc0bc2aa 100644
--- a/python/tvm/relax/script/ir_builder/frame.py
+++ b/python/tvm/relax/script/ir_builder/frame.py
@@ -18,9 +18,7 @@
 
 from tvm_ffi import register_object as _register_object
 
-from tvm.script.ir_builder.base import IRBuilderFrame, _resolve_type_var
-
-from . import _ffi_api
+from tvm.script.ir_builder.base import IRBuilderFrame
 
 
 @_register_object("script.ir_builder.relax.RelaxFrame")
@@ -34,11 +32,7 @@ class SeqExprFrame(RelaxFrame): ...
 
 @_register_object("script.ir_builder.relax.FunctionFrame")
 class FunctionFrame(SeqExprFrame):
-    """Native function frame retaining signature, symbols and finalized 
results."""
-
-    def resolve_type_var(self, name, dtype=None, *, value=None, span=None):
-        """Resolve a primitive symbol in this function's native map."""
-        return _resolve_type_var(self, _ffi_api.ResolveTypeVar, name, dtype, 
value=value, span=span)
+    """Native function frame retaining signature and finalized results."""
 
 
 @_register_object("script.ir_builder.relax.BindingBlockFrame")
diff --git a/python/tvm/relax/script/ir_builder/ir.py 
b/python/tvm/relax/script/ir_builder/ir.py
index c39005fa36..481fc74afc 100644
--- a/python/tvm/relax/script/ir_builder/ir.py
+++ b/python/tvm/relax/script/ir_builder/ir.py
@@ -526,8 +526,8 @@ def type_var(name, *, dtype=None, span=None):
 
     Notes
     -----
-    This constructor creates a new symbol on each call. Use the language 
variant
-    resolver for symbols shared by name within a function signature.
+    This constructor creates a new symbol on each call. Reuse the returned
+    variable to share its identity across annotations and function parameters.
     """
     return _ir.Var(
         name,
diff --git a/python/tvm/relax/script/ir_builder/parser_protocol.py 
b/python/tvm/relax/script/ir_builder/parser_protocol.py
index 1513cae10a..d23f748a11 100644
--- a/python/tvm/relax/script/ir_builder/parser_protocol.py
+++ b/python/tvm/relax/script/ir_builder/parser_protocol.py
@@ -106,8 +106,8 @@ def arg_(name: str, ty: Any, *, span: _Span = None) -> 
_ir.Var:
     ty : Type, Var, or callable
         Parameter annotation; corresponds to ``annotation`` in the shared
         :func:`tvm.script.ir_builder.parser_protocol.arg_` contract. Primitive
-        annotations resolve through the active signature symbol context; an
-        existing variable retains its identity.
+        annotations create a fresh parameter; an existing variable retains
+        its identity.
     span : SpanEntry, Span or None, optional
         Source location attached to the constructed IR; None leaves it 
unspecified.
 
@@ -118,8 +118,6 @@ def arg_(name: str, ty: Any, *, span: _Span = None) -> 
_ir.Var:
     """
     if not isinstance(ty, _ir.Var):
         ty = _native._type(ty)
-    if isinstance(ty, _ir.PrimType) or _ir.is_prim_var(ty):
-        ty = resolve_type_var_(name, ty, span=span)
     if isinstance(ty, _ir.Var):
         return _ffi_api.ArgVar(name, ty)
     return _base.at_(span, _ffi_api.Arg(name, _native._type(ty)))
@@ -195,17 +193,6 @@ def _check_module_well_formed(module: _ir.IRModule) -> 
None:
 # --------------------------------------
 
 
-def resolve_type_var_(
-    name: str,
-    dtype: str | _ir.Type | _ir.Var | None = None,
-    *,
-    value: _ir.Var | None = None,
-    span: _Span = None,
-) -> _ir.Var:
-    """Implements 
:func:`tvm.script.ir_builder.parser_protocol.resolve_type_var_`."""
-    return _base._current_function_frame().resolve_type_var(name, dtype, 
value=value, span=span)
-
-
 def call_global_var_(function: _ir.GlobalVar, args: Sequence[Any]) -> _ir.Expr:
     """Implements 
:func:`tvm.script.ir_builder.parser_protocol.call_global_var_`."""
     return _relax.Call(function, [_relax.utils.convert_to_expr(value) for 
value in args])
@@ -605,7 +592,6 @@ __all__ = [
     "output",
     "range_",
     "resolve_global_info_",
-    "resolve_type_var_",
     "return_",
     "seq_expr",
     "set_mutable_cell_",
diff --git a/python/tvm/script/ir_builder/base.py 
b/python/tvm/script/ir_builder/base.py
index 0543fd8613..3234dc0960 100644
--- a/python/tvm/script/ir_builder/base.py
+++ b/python/tvm/script/ir_builder/base.py
@@ -407,32 +407,6 @@ def with_at_group_(
 at_ = at
 
 
-def _resolve_type_var(frame, ffi_resolver, name, dtype=None, *, value=None, 
span=None):
-    """Validate a native function resolver's inputs, leaving its map owned by 
C++."""
-    if not isinstance(name, str) or not name:
-        raise ValueError("A symbolic variable requires a nonempty string name")
-    if isinstance(dtype, ir.Var):
-        value, dtype = dtype, dtype.ty
-    if isinstance(dtype, str):
-        dtype = ir.PrimType(dtype)
-    if dtype is not None and not isinstance(dtype, ir.PrimType):
-        raise TypeError("A symbolic variable requires a primitive type")
-    if value is not None and not ir.is_prim_var(value):
-        raise TypeError("A symbolic binding requires a primitive Var")
-    return ffi_resolver(
-        frame, name, dtype, value, span.span if isinstance(span, SpanEntry) 
else span
-    )
-
-
-def _current_function_frame():
-    """Find the nearest native function frame for explicit symbol 
declarations."""
-    if IRBuilder.is_in_scope():
-        for frame in reversed(IRBuilder.current().frames):
-            if callable(getattr(frame, "resolve_type_var", None)):
-                return frame
-    raise ValueError("Symbol resolution requires an active function frame")
-
-
 def _return_annotation(annotation):
     """Evaluate the deferred return expression before normalizing its 
annotation value."""
     if callable(annotation) and not isinstance(annotation, ir.Expr | ir.Type):
diff --git a/python/tvm/script/ir_builder/parser_protocol.py 
b/python/tvm/script/ir_builder/parser_protocol.py
index df66a680c4..6a16ef8146 100644
--- a/python/tvm/script/ir_builder/parser_protocol.py
+++ b/python/tvm/script/ir_builder/parser_protocol.py
@@ -162,10 +162,10 @@ def arg_(name: str, annotation: Any, *, span: _Span = 
None) -> _ir.Var:
 
     Notes
     -----
-    Requires an active function signature frame. Primitive symbols use that 
frame's
-    resolver, preserving annotation/body identity. TIRx handles buffer layout 
according to
-    its function policy; Relax converts its type annotation. Invalid 
annotations or context
-    raise TypeError/native builder errors.
+    Requires an active function signature frame. Existing variables retain 
identity;
+    type annotations create fresh variables. TIRx handles buffer layout 
according to
+    its function policy; Relax converts its type annotation. Invalid 
annotations or
+    context raise TypeError/native builder errors.
 
     .. code:: python
 
@@ -333,50 +333,6 @@ def resolve_global_info_(content: str) -> _ir.GlobalInfo:
     return _global_infos()[name][int(index)]
 
 
-def resolve_type_var_(
-    name: str,
-    dtype: str | _ir.Type | _ir.Var | None = None,
-    *,
-    value: _ir.Var | None = None,
-    span: _Span = None,
-) -> _ir.Var:
-    """Resolve or declare a symbolic variable in the nearest native function.
-
-    Parameters
-    ----------
-    name : str
-        Function-local lookup key for explicit header parameters and captured 
symbols.
-    dtype : str, Type or Var, optional
-        Explicit primitive type or supplied variable. None defaults new 
symbols to
-        int64; existing symbols must agree with an explicit dtype.
-    value : Var, optional
-        Existing primitive variable to register without replacement. None 
creates a
-        variable only if the name is not already registered.
-    span : SpanEntry, Span or None, optional
-        Location for the constructed result. None (the default) leaves 
explicit location
-        unspecified; active source-call provenance is composed by the builder. 
Frames
-        retain their location until finalization.
-
-    Returns
-    -------
-    _ir.Var
-        The exact existing or newly registered variable.
-
-    Notes
-    -----
-    Requires an active native function frame. The signature and resumed body 
use the same
-    symbol map; nested functions use separate maps. Conflicting dtype/variable 
declarations
-    or missing context raise a builder error. No statement is emitted.
-
-    .. code:: python
-
-        # Source: def f[n: T.int32](...)
-        # Generated builder
-        n = X.resolve_type_var_("n", dtype="int32")
-    """
-    raise NotImplementedError
-
-
 def call_global_var_(function: _ir.GlobalVar, args: Sequence[Any]) -> _ir.Expr:
     """Construct a call to a declared module function.
 
diff --git a/python/tvm/script/parser/entry.py 
b/python/tvm/script/parser/entry.py
index bae99d679d..537a22abaf 100644
--- a/python/tvm/script/parser/entry.py
+++ b/python/tvm/script/parser/entry.py
@@ -688,13 +688,12 @@ def _run_statements(
     # Macros enter through their body instead of function declaration lowering.
     # Snapshot annotations before defining the helper, in the definition scope.
     # Bound arguments remain original-name parameters of the macro body.
-    _, _, annotation_names = transformer._read_function_annotations(
-        node, [], transformer.module.prescan_ctx.bindings.get(node, ())
-    )
     captures = transformer.module.make_fresh_name("_definition")
     transformer.function.definition_captures = captures
-    statements = transformer._create_definition_bindings(node, 
annotation_names, captures=captures)
     body = transformer.transform_statements(node.body)
+    statements = transformer._create_definition_bindings(
+        node, transformer.function.capture_names, captures=captures
+    )
     names = sorted(name for name in bound_names if name in namespace)
     helper_name = transformer.module.make_fresh_name("_macro")
     helper = ast.copy_location(
diff --git a/python/tvm/script/parser/inspect_source.py 
b/python/tvm/script/parser/inspect_source.py
index 8ced5a8bba..8ec89704f1 100644
--- a/python/tvm/script/parser/inspect_source.py
+++ b/python/tvm/script/parser/inspect_source.py
@@ -40,7 +40,7 @@ from tvm_ffi.dataclasses import MISSING
 from tvm.ir import SourceName, Span
 
 from . import protocol_registry
-from .prescan import collect_annotation_free_names, resolve_namespace_key, 
resolve_namespace_value
+from .prescan import collect_annotation_free_reads, resolve_namespace_key, 
resolve_namespace_value
 
 
 class _AnnotationScope(dict):
@@ -238,7 +238,7 @@ def capture_annotation_bindings(
             protocol_registry.DefinitionKind.MACRO,
             protocol_registry.DefinitionKind.PYTHON,
         ):
-            names.update(collect_annotation_free_names(target.value))
+            names.update(read.id for read in 
collect_annotation_free_reads(target.value))
     for node in ast.walk(tree):
         annotation = (
             node.annotation
@@ -248,7 +248,7 @@ def capture_annotation_bindings(
             else None
         )
         if annotation is not None:
-            names.update(collect_annotation_free_names(annotation))
+            names.update(read.id for read in 
collect_annotation_free_reads(annotation))
     return {name: definition_scope[name] for name in names if name in 
definition_scope}
 
 
diff --git a/python/tvm/script/parser/prescan.py 
b/python/tvm/script/parser/prescan.py
index f10344e471..1d7b43f8dd 100644
--- a/python/tvm/script/parser/prescan.py
+++ b/python/tvm/script/parser/prescan.py
@@ -22,19 +22,15 @@ import ast
 import builtins
 import inspect
 from collections.abc import Mapping
+from enum import IntEnum, auto
 from types import ModuleType
 from typing import NamedTuple, NoReturn
 
 from . import protocol_registry
 
 
-def collect_annotation_free_names(node: ast.expr) -> dict[str, ast.Name]:
-    """Find annotation captures and their source introduction locations."""
-    return {reference.id: reference for reference in 
collect_annotation_free_reads(node)}
-
-
 def collect_annotation_free_reads(node: ast.expr, bound: set[str] | None = 
None) -> list[ast.Name]:
-    """Collect original free reads for captures and lazy missing-name 
checks."""
+    """Collect lexical free reads for definition capture and shadowing 
checks."""
     bound = set() if bound is None else bound
     if isinstance(node, ast.Name):
         return [node] if isinstance(node.ctx, ast.Load) and node.id not in 
bound else []
@@ -155,14 +151,26 @@ def _match_special_func(node: ast.AST | None, namespaces: 
Mapping[str, str]) ->
     return None
 
 
+class BindingKind(IntEnum):
+    """Syntax categories used by binding lowering and explicit-symbol 
checks."""
+
+    ORDINARY = auto()
+    PARAMETER = auto()
+    FORWARD_PARAMETER = auto()
+    MUTABLE_PARAMETER = auto()
+    SYMBOL = auto()
+    MUTABLE = auto()
+    LOOP = auto()
+    MODULE_ALIAS = auto()
+    MUTABLE_UPDATE = auto()
+
+
 class Binding(NamedTuple):
     """A source binding site, retained through rewriting without value 
state."""
 
     name: str
     node: ast.AST
-    kind: str
-    annotation: ast.expr | None = None
-    dtype: object = None
+    kind: BindingKind
 
 
 class PrescanContext:
@@ -197,7 +205,11 @@ class PrescanContext:
         # Derive mutable-name sets once for assignment dispatch, then read 
them only;
         # these are syntax categories, not independently updated runtime value 
state.
         self.mutable_names = {
-            scope: {item.name for item in items if item.kind in ("mutable", 
"mutable_parameter")}
+            scope: {
+                item.name
+                for item in items
+                if item.kind in (BindingKind.MUTABLE, 
BindingKind.MUTABLE_PARAMETER)
+            }
             for scope, items in bindings.items()
         }
         # Collected branch-ending names select conditional results without 
later updates.
@@ -343,13 +355,11 @@ class PrescanCollector(ast.NodeVisitor):
         self,
         name: str,
         node: ast.AST,
-        kind: str = "ordinary",
-        annotation: ast.expr | None = None,
-        dtype: object = None,
+        kind: BindingKind = BindingKind.ORDINARY,
     ) -> None:
         self._check_reserved(name, node)
         self.names.add(name)
-        item = Binding(name, node, kind, annotation, dtype)
+        item = Binding(name, node, kind)
         self.bindings[self.scope].append(item)
         self.sites[node] = item
 
@@ -470,7 +480,7 @@ class PrescanCollector(ast.NodeVisitor):
         #         x = X.arg_("x", ty)
         # -------------------------------------------------
         # Collect this function separately from its enclosing scope.
-        self._record_binding(node.name, node, "function")
+        self._record_binding(node.name, node)
         old_scope, old_builder = self.scope, self.builder
         self.scope = node
         self.functions.append(node)
@@ -495,17 +505,11 @@ class PrescanCollector(ast.NodeVisitor):
             #         body(n)
             #
             # Builder:
-            #     n = X.resolve_type_var_("n")
+            #     n = I.dynamic("n")
             # -------------------------------------------------
-            bound = getattr(parameter, "bound", None)
-            dtype = (
-                "int64"
-                if bound is None or (isinstance(bound, ast.Name) and bound.id 
== "int")
-                else protocol_registry.SCALAR_ANNOTATION_DTYPE.get(
-                    _match_special_func(bound, self.namespaces)
-                )
-            )
-            self._record_binding(parameter.name, parameter, "symbol", 
dtype=dtype)
+            self._record_binding(parameter.name, parameter, BindingKind.SYMBOL)
+        earlier_reads: set[str] = set()
+        header_names = {parameter.name for parameter in getattr(node, 
"type_params", ())}
         for arg in [*node.args.posonlyargs, *node.args.args, 
*node.args.kwonlyargs]:
             # -------------------- Pattern --------------------
             # Python source:
@@ -526,13 +530,21 @@ class PrescanCollector(ast.NodeVisitor):
             self._record_binding(
                 arg.arg,
                 arg,
-                "mutable_parameter"
+                BindingKind.MUTABLE_PARAMETER
                 if inspect.getattr_static(self.builder, 
"supports_mutable_declarations", True)
                 is True
                 and "parameter" in 
protocol_registry.MUTABLE_CELL_DECL.get(constructor, ())
-                else "parameter",
-                arg.annotation,
+                else BindingKind.FORWARD_PARAMETER
+                if arg.arg in earlier_reads
+                and arg.arg not in header_names
+                and protocol_registry.SCALAR_ANNOTATION_DTYPE.get(
+                    _match_special_func(annotation, self.namespaces)
+                )
+                is not None
+                else BindingKind.PARAMETER,
             )
+            if annotation is not None:
+                earlier_reads.update(read.id for read in 
collect_annotation_free_reads(annotation))
         # Parameters, defaults and annotation-local binders are part of the 
same
         # fixed-name invariant, even when signature lowering handles them 
separately.
         self.visit(node.args)
@@ -560,19 +572,26 @@ class PrescanCollector(ast.NodeVisitor):
         assignable = {
             item.name
             for item in facts
-            if item.kind in ("parameter", "mutable_parameter", "mutable", 
"loop")
+            if item.kind
+            in (
+                BindingKind.PARAMETER,
+                BindingKind.FORWARD_PARAMETER,
+                BindingKind.MUTABLE_PARAMETER,
+                BindingKind.MUTABLE,
+                BindingKind.LOOP,
+            )
         }
         # This temporary diagnostic index is derived from existing source 
facts;
         # it is not retained in the prescan result or used as value state.
         origins: dict[str, ast.AST] = {}
         for item in facts:
-            if item.kind == "symbol":
+            if item.kind == BindingKind.SYMBOL:
                 origin = origins.get(item.name)
                 if origin is None or item.node.lineno < origin.lineno:
                     origins[item.name] = item.node
         for item in facts:
             # Explicit declarations and mutable updates are not symbol 
rebindings.
-            if item.kind == "symbol" or item.name in assignable:
+            if item.kind == BindingKind.SYMBOL or item.name in assignable:
                 continue
             origin = origins.get(item.name)
             if origin is not None and (item.node.lineno, item.node.col_offset) 
> (
@@ -591,7 +610,7 @@ class PrescanCollector(ast.NodeVisitor):
         value: ast.expr | None = None,
         annotation: ast.expr | None = None,
         *,
-        kind: str = "ordinary",
+        kind: BindingKind = BindingKind.ORDINARY,
     ) -> None:
         if self.functions:
             values = [value]
@@ -625,11 +644,11 @@ class PrescanCollector(ast.NodeVisitor):
                     )
                 )
             ):
-                self._record_binding(target.id, target, "mutable", annotation)
+                self._record_binding(target.id, target, BindingKind.MUTABLE)
             elif isinstance(value, ast.Name) and value.id == self.module_name:
-                self._record_binding(target.id, target, "module_alias")
+                self._record_binding(target.id, target, 
BindingKind.MODULE_ALIAS)
             elif isinstance(value, ast.Name) and any(
-                item.name == value.id and item.kind == "module_alias"
+                item.name == value.id and item.kind == BindingKind.MODULE_ALIAS
                 for item in self.bindings[self.scope]
             ):
                 # -------------------- Pattern --------------------
@@ -640,9 +659,9 @@ class PrescanCollector(ast.NodeVisitor):
                 #     second_alias = first_alias
                 # -------------------------------------------------
                 # Preserve the active module frame identity.
-                self._record_binding(target.id, target, "module_alias")
+                self._record_binding(target.id, target, 
BindingKind.MODULE_ALIAS)
             else:
-                self._record_binding(target.id, target, kind, 
annotation=annotation)
+                self._record_binding(target.id, target, kind)
         elif isinstance(target, ast.Tuple | ast.List):
             values = (
                 value.elts
@@ -760,7 +779,7 @@ class PrescanCollector(ast.NodeVisitor):
         #         X.emit_(body(i))
         # -------------------------------------------------
         # Loop binders remain assignable and are never signature declarations.
-        self._collect_target(node.target, kind="loop")
+        self._collect_target(node.target, kind=BindingKind.LOOP)
         self.visit(node.iter)
         for statement in node.body + node.orelse:
             self.visit(statement)
diff --git a/python/tvm/script/parser/protocol_registry.py 
b/python/tvm/script/parser/protocol_registry.py
index b8f745f419..67cfce82be 100644
--- a/python/tvm/script/parser/protocol_registry.py
+++ b/python/tvm/script/parser/protocol_registry.py
@@ -89,7 +89,7 @@ def register_scalar_annotation(
 
         register_scalar_annotation("tirx.int32", T.int32, dtype="int32")
         # Source: def f[n: T.int32](...):
-        # Builder: n = X.resolve_type_var_("n", dtype="int32")
+        # Builder: n = I.dynamic("n", "int32")
     """
     SCALAR_ANNOTATION_DTYPE[namespace_path] = dtype
     return constructor
diff --git a/python/tvm/script/parser/transpile.py 
b/python/tvm/script/parser/transpile.py
index 50a7fc9440..1f5afefe20 100644
--- a/python/tvm/script/parser/transpile.py
+++ b/python/tvm/script/parser/transpile.py
@@ -30,8 +30,8 @@ entry-owned definition scope and hand generated helpers to 
private recomposition
 in one direction, without back-references to the rewriter. Entry executes the
 recomposed builder and releases temporary captures. Short-lived frame names,
 statement lists and assembly results stay local to the methods that need them.
-Native frames own symbols, declarations, parameters, region results and final
-IR; the Python contexts do not mirror that construction state.
+Native frames own declarations, parameters, region results and final IR;
+the Python contexts do not mirror that construction state.
 """
 
 from __future__ import annotations
@@ -46,9 +46,8 @@ from typing import Any, NamedTuple, NoReturn, TypeVar
 from . import protocol_registry
 from .inspect_source import _AnnotationScope
 from .prescan import (
-    Binding,
+    BindingKind,
     PrescanContext,
-    collect_annotation_free_names,
     collect_annotation_free_reads,
 )
 
@@ -84,15 +83,6 @@ def _require_constexpr_arg(value: _Value, name: str) -> 
_Value:
     return value
 
 
-def _require_annotation_value(value: _Value, name: str) -> _Value:
-    """Keep captured values unchanged, reporting missing names only when 
read."""
-    from tvm.script.ir_builder.base import MISSING
-
-    if value is MISSING:
-        raise NameError(f"name {name!r} is not defined")
-    return value
-
-
 def _unwrap_optional_annotation(
     annotation: Any, const_args: Mapping[str, Any] | None = None
 ) -> Any:
@@ -237,8 +227,9 @@ class FunctionContext:
         self.current_scope = current_scope
         # Namespace identifier used by _call_dialect for this function's 
builder operations.
         self.dialect_prefix = dialect_prefix
-        # One snapshot supplies original-name helper defaults at the 
definition site.
+        # Only reads that cross a Python scope boundary need definition 
snapshots.
         self.definition_captures: str | None = None
+        self.capture_names: set[str] = set()
 
 
 class IRBuilderTranspiler(ast.NodeTransformer):
@@ -262,7 +253,7 @@ class IRBuilderTranspiler(ast.NodeTransformer):
         # Bypass syntax-to-builder lowering.
         # Still traverse children and instrument source calls with spans.
         self.bypass_ast_rewrite = False
-        # Only free reads of the active annotation need lazy missing-name 
checks.
+        # Only free reads that cross a scope boundary use the definition 
snapshot.
         self.annotation_reads: set[ast.Name] = set()
         # Only this expression's result is already located by its enclosing 
emit_.
         # Child operations retain their locations; restore the borrowed node 
on exit.
@@ -418,10 +409,11 @@ class IRBuilderTranspiler(ast.NodeTransformer):
             self.bypass_ast_rewrite = old
 
     @contextmanager
-    def _rewrite_annotation(self, node: ast.expr) -> Iterator[None]:
-        """Check missing values only at free reads, without renaming Python 
bindings."""
+    def _rewrite_annotation(self, reads: set[ast.Name]) -> Iterator[None]:
+        """Keep Python reads direct unless their definition scope would be 
shadowed."""
         previous_reads = self.annotation_reads
-        self.annotation_reads = set(collect_annotation_free_reads(node))
+        self.annotation_reads = reads
+        self.function.capture_names.update(read.id for read in 
self.annotation_reads)
         try:
             yield
         finally:
@@ -448,16 +440,16 @@ class IRBuilderTranspiler(ast.NodeTransformer):
         #     value: X.Tensor((n,))
         #
         # Builder:
-        #     value_type = X.Tensor((_require_annotation_value(n, "n"),))
+        #     value_type = X.Tensor((n,))
         # -------------------------------------------------
-        # Helpers bind captured values under their original names. Python 
handles
-        # lambda/comprehension locals and preceding signature parameters 
directly.
+        # Python handles ordinary names, lambda/comprehension locals, and 
earlier
+        # parameters. A selected cross-scope read keeps lazy NameError 
semantics.
         if node in self.annotation_reads:
-            if node.id in self.module.prescan_ctx.namespaces:
-                return node
             return ast.copy_location(
-                ast.Call(
-                    self._inject(_require_annotation_value), [node, 
ast.Constant(node.id)], []
+                ast.Subscript(
+                    ast.Name(self.function.definition_captures, ast.Load()),
+                    ast.Constant(node.id),
+                    ast.Load(),
                 ),
                 node,
             )
@@ -553,7 +545,7 @@ class IRBuilderTranspiler(ast.NodeTransformer):
             for item in 
self.module.prescan_ctx.bindings.get(self.function.current_scope, ())
             if item.name == node.id
         ]
-        return bool(records) and all(item.kind == "module_alias" for item in 
records)
+        return bool(records) and all(item.kind == BindingKind.MODULE_ALIAS for 
item in records)
 
     def _visit_direct_operand(self, node: ast.expr) -> ast.expr:
         """Preserve an existing payload's span without bypassing child 
operations."""
@@ -834,21 +826,21 @@ class IRBuilderTranspiler(ast.NodeTransformer):
             )
         return self.visit(node)
 
-    def _binding_kind(self, target: ast.Name, *, frame_value: bool = False) -> 
str:
+    def _binding_kind(self, target: ast.Name, *, frame_value: bool = False) -> 
BindingKind:
         """Select the declaration/store/binding operation from existing syntax 
facts."""
         if frame_value:
-            return "ordinary"
+            return BindingKind.ORDINARY
         site = self.module.prescan_ctx.sites.get(target)
-        kind = site.kind if site is not None else "ordinary"
-        if kind in ("mutable", "module_alias"):
+        kind = site.kind if site is not None else BindingKind.ORDINARY
+        if kind in (BindingKind.MUTABLE, BindingKind.MODULE_ALIAS):
             return kind
         mutable = 
self.module.prescan_ctx.mutable_names.get(self.function.current_scope, ())
-        return "mutable_update" if target.id in mutable else "ordinary"
+        return BindingKind.MUTABLE_UPDATE if target.id in mutable else 
BindingKind.ORDINARY
 
     def _uses_ordinary_binding(self, target: ast.expr) -> bool:
         """Test assignment targets without evaluating or inspecting their RHS 
values."""
         if isinstance(target, ast.Name):
-            return self._binding_kind(target) == "ordinary"
+            return self._binding_kind(target) == BindingKind.ORDINARY
         if isinstance(target, ast.Starred):
             return self._uses_ordinary_binding(target.value)
         if isinstance(target, ast.Tuple | ast.List):
@@ -881,7 +873,7 @@ class IRBuilderTranspiler(ast.NodeTransformer):
                 keywords["ty"] = ty
             if frame_value:
                 keywords["frame_value"] = ast.Constant(True)
-            if kind == "mutable" and not frame_value:
+            if kind == BindingKind.MUTABLE and not frame_value:
                 # -------------------- Pattern --------------------
                 # Python source:
                 #     x = X.local_scalar(initial)
@@ -896,7 +888,7 @@ class IRBuilderTranspiler(ast.NodeTransformer):
                     name_span=self.module.make_span_expr(target),
                     keywords=keywords,
                 )
-            elif kind == "module_alias" and not frame_value:
+            elif kind == BindingKind.MODULE_ALIAS and not frame_value:
                 # -------------------- Pattern --------------------
                 # Python source:
                 #     alias = Module
@@ -905,7 +897,7 @@ class IRBuilderTranspiler(ast.NodeTransformer):
                 #     alias = Module
                 # -------------------------------------------------
                 pass
-            elif kind == "mutable_update":
+            elif kind == BindingKind.MUTABLE_UPDATE:
                 # -------------------- Pattern --------------------
                 # Python source:
                 #     x = value
@@ -1103,12 +1095,8 @@ class IRBuilderTranspiler(ast.NodeTransformer):
             if node.value
             else ast.Attribute(ast.Name(self.module.ir_prefix, ast.Load()), 
"MISSING", ast.Load())
         )
-        annotation = node.annotation
-        names = set(collect_annotation_free_names(annotation))
-        with self._rewrite_annotation(annotation):
-            helper = self._create_lambda([], self.visit(annotation))
-        self._capture_defaults(helper, names, node.annotation, 
body_locals=True)
-        annotation = ast.copy_location(ast.Call(helper, [], []), 
node.annotation)
+        with self._rewrite_annotation(self._body_annotation_captures(node)):
+            annotation = self.visit(node.annotation)
         return self._bind(node.target, value, node, ty=annotation, 
value_span=value_span)
 
     def visit_AugAssign(self, node: ast.AugAssign) -> ast.AugAssign | 
list[ast.stmt]:
@@ -1627,31 +1615,31 @@ class IRBuilderTranspiler(ast.NodeTransformer):
         # Conflicting definition and execution namespaces need an injected 
name.
         return self._inject(namespace, "_X").id
 
-    def _read_function_annotations(
-        self, node: ast.FunctionDef, parameters: list[ast.arg], facts: 
list[Binding]
-    ) -> tuple[list[ast.expr | None], ast.expr | None, set[str]]:
-        """Find definition-scope names needed by signatures and body 
annotations."""
-        declared_names = {item.name for item in getattr(node, "type_params", 
())}
-        annotations = [parameter.annotation for parameter in parameters]
-        returns = node.returns
-        annotation_names = {
-            name
-            for annotation in [*annotations, returns]
-            if annotation is not None
-            for name in collect_annotation_free_names(annotation)
-        }
-        body_annotations = [
-            item.annotation
-            for item in facts
-            if item.annotation is not None
-            and item.kind not in ("parameter", "mutable_parameter", "symbol")
-        ]
-        annotation_names.update(
-            name
-            for annotation in body_annotations
-            for name in collect_annotation_free_names(annotation)
-        )
-        return annotations, returns, annotation_names - declared_names
+    def _body_annotation_captures(self, node: ast.AnnAssign) -> set[ast.Name]:
+        """Retain definition reads only where the body has a different Python 
scope."""
+        facts = 
self.module.prescan_ctx.bindings.get(self.function.current_scope, ())
+        captures = set()
+        for read in collect_annotation_free_reads(node.annotation):
+            name = read.id
+            if name in self.module.prescan_ctx.namespaces:
+                continue
+            bindings = [item for item in facts if item.name == name]
+            if any(
+                (item.node.lineno, item.node.col_offset)
+                < (node.annotation.lineno, node.annotation.col_offset)
+                for item in bindings
+            ):
+                continue
+            if (
+                bindings
+                or self.module.module_name is not None
+                or (
+                    name in self.module.definition_scope
+                    and self.module.definition_scope[name] is not 
self.module.exec_globals.get(name)
+                )
+            ):
+                captures.add(read)
+        return captures
 
     def _create_definition_bindings(
         self,
@@ -1660,12 +1648,9 @@ class IRBuilderTranspiler(ast.NodeTransformer):
         *,
         captures: str,
     ) -> list[ast.stmt]:
-        """Snapshot only lexical annotation/constexpr names at their 
definition site."""
-        names = names | {
-            parameter.arg
-            for parameter in [*node.args.posonlyargs, *node.args.args, 
*node.args.kwonlyargs]
-            if self._is_constexpr_annotation(parameter.annotation)
-        }
+        """Snapshot only consumed cross-scope reads at their definition 
site."""
+        if not names:
+            return []
         # Inject builtin operations so same-named source bindings cannot 
replace them.
         # Acquire each scope once; the snapshot retains selected values, never 
frames.
         scopes: list[ast.expr] = [ast.Call(self._inject(locals), [], [])]
@@ -1680,47 +1665,6 @@ class IRBuilderTranspiler(ast.NodeTransformer):
         )
         return [self._assign(captures, captured, node)]
 
-    def _capture_defaults(
-        self,
-        helper: ast.FunctionDef | ast.Lambda,
-        names: set[str],
-        node: ast.AST,
-        *,
-        body_locals: bool = False,
-    ) -> None:
-        """Give a helper original-name captures; Python owns all lexical 
shadowing."""
-        for name in sorted(names):
-            value = self._call(
-                self.function.definition_captures,
-                "get",
-                [
-                    ast.Constant(name),
-                    ast.Attribute(
-                        ast.Name(self.module.ir_prefix, ast.Load()), 
"MISSING", ast.Load()
-                    ),
-                ],
-                node,
-            )
-            if body_locals and any(
-                item.name == name
-                and (item.node.lineno, item.node.col_offset) < (node.lineno, 
node.col_offset)
-                for item in 
self.module.prescan_ctx.bindings.get(self.function.current_scope, ())
-            ):
-                # Keep a real name read so Python captures enclosing helper 
cells.
-                # A prior conditional binding may not have executed: snapshot 
its
-                # absence without reading it until the annotation selects that 
name.
-                value = ast.IfExp(
-                    ast.Compare(
-                        ast.Constant(name), [ast.In()], 
[ast.Call(self._inject(locals), [], [])]
-                    ),
-                    ast.Name(name, ast.Load()),
-                    ast.Attribute(
-                        ast.Name(self.module.ir_prefix, ast.Load()), 
"MISSING", ast.Load()
-                    ),
-                )
-            helper.args.kwonlyargs.append(ast.arg(name))
-            helper.args.kw_defaults.append(value)
-
     def _create_const_args(self, node: ast.FunctionDef, *, const_args: str) -> 
list[ast.stmt]:
         """Read root JIT inputs; nested functions keep ordinary runtime 
parameters."""
         from . import jit_support
@@ -1733,22 +1677,31 @@ class IRBuilderTranspiler(ast.NodeTransformer):
         )
         return [self._assign(const_args, const_args_expr, node)]
 
-    def _create_symbol_declarations(self, node: ast.FunctionDef) -> 
list[ast.stmt]:
-        """Bind explicit signature type parameters with their declared 
dtypes."""
+    def _create_symbol_declarations(
+        self, node: ast.FunctionDef
+    ) -> tuple[list[ast.stmt], dict[str, str]]:
+        """Create explicit header symbols and carry their identities into the 
body."""
         declaration: list[ast.stmt] = []
+        aliases: dict[str, str] = {}
         # -------------------- Pattern --------------------
         # Python source:
         #     def f[n]():
         #         body(n)
         #
         # Builder:
-        #     n = X.resolve_type_var_("n")
+        #     n = I.dynamic("n")
         # -------------------------------------------------
         for parameter in getattr(node, "type_params", ()):
             if not isinstance(parameter, getattr(ast, "TypeVar", ())):
                 self._raise_error(parameter, "Only scalar type parameters are 
supported")
             bound = getattr(parameter, "bound", None)
-            dtype = self.module.prescan_ctx.sites[parameter].dtype
+            dtype = (
+                "int64"
+                if bound is None or (isinstance(bound, ast.Name) and bound.id 
== "int")
+                else protocol_registry.SCALAR_ANNOTATION_DTYPE.get(
+                    self.module.prescan_ctx._match_special_func(bound)
+                )
+            )
             if bound is not None and not (isinstance(bound, ast.Name) and 
bound.id == "int"):
                 if (
                     protocol_registry.SCALAR_ANNOTATION_DTYPE.get(
@@ -1762,19 +1715,23 @@ class IRBuilderTranspiler(ast.NodeTransformer):
                     )
             if getattr(parameter, "default_value", None) is not None:
                 self._raise_error(parameter, "A symbolic type parameter cannot 
have a default")
+            alias = self.module.make_fresh_name("_symbol")
+            aliases[parameter.name] = alias
             declaration.append(
                 self._assign(
-                    parameter.name,
-                    self._call_dialect(
-                        "resolve_type_var_",
-                        [ast.Constant(parameter.name)],
+                    alias,
+                    self._call(
+                        self.module.ir_prefix,
+                        "dynamic",
+                        [ast.Constant(parameter.name), ast.Constant(dtype)],
                         parameter,
-                        keywords={"dtype": ast.Constant(dtype)},
+                        span=self.module.make_span_expr(parameter),
                     ),
                     parameter,
                 )
             )
-        return declaration
+            declaration.append(self._assign(parameter.name, ast.Name(alias, 
ast.Load()), parameter))
+        return declaration, aliases
 
     @staticmethod
     def _select_specialized_value(
@@ -1810,6 +1767,7 @@ class IRBuilderTranspiler(ast.NodeTransformer):
         *,
         captures: str,
         const_args: str | None,
+        symbols: dict[str, str],
     ) -> tuple[list[ast.stmt], dict[str, str]]:
         """Declare signature parameters, making constexpr values available 
first."""
         declaration: list[ast.stmt] = []
@@ -1822,6 +1780,47 @@ class IRBuilderTranspiler(ast.NodeTransformer):
             is_constexpr = self._is_constexpr_annotation(annotation)
             group = constexpr_params if is_constexpr else other_params
             group.append((parameter, annotation, is_constexpr))
+        forward: dict[str, str] = {}
+        for parameter, annotation, _ in other_params:
+            if self.module.prescan_ctx.sites[parameter].kind != 
BindingKind.FORWARD_PARAMETER:
+                continue
+            name = parameter.arg
+            self.function.capture_names.add(name)
+            alias = self.module.make_fresh_name("_parameter")
+            forward[name] = alias
+            dtype = protocol_registry.SCALAR_ANNOTATION_DTYPE[
+                self.module.prescan_ctx._match_special_func(annotation)
+            ]
+            created = self._call(
+                self.module.ir_prefix,
+                "dynamic",
+                [ast.Constant(name), ast.Constant(dtype)],
+                parameter,
+                span=self.module.make_span_expr(parameter),
+            )
+            if const_args is not None:
+                created = self._select_specialized_value(
+                    name, created, parameter, const_args=const_args
+                )
+            # An outer dimension still belongs to earlier annotations. The 
later
+            # scalar gets a fresh parameter unless this declaration supplied 
it.
+            supplied = ast.Compare(ast.Constant(name), [ast.In()], 
[ast.Name(captures, ast.Load())])
+            declaration.append(
+                self._assign(alias, ast.IfExp(supplied, 
self.visit(annotation), created), parameter)
+            )
+            declaration.append(
+                self._assign(
+                    name,
+                    self._call(
+                        captures,
+                        "get",
+                        [ast.Constant(name), ast.Name(alias, ast.Load())],
+                        parameter,
+                    ),
+                    parameter,
+                )
+            )
+        unbound = {parameter.arg for parameter in parameters} - symbols.keys() 
- forward.keys()
         for parameter, annotation, is_constexpr in [*constexpr_params, 
*other_params]:
             if annotation is None:
                 self._raise_error(parameter, f"Parameter {parameter.arg!r} 
requires an annotation")
@@ -1832,6 +1831,7 @@ class IRBuilderTranspiler(ast.NodeTransformer):
                 # alias so that binding does not become a local `name = name`.
                 alias = self.module.make_fresh_name("_parameter")
                 constexpr_aliases[name] = alias
+                self.function.capture_names.add(name)
                 fallback = ast.Call(
                     self._inject(_require_constexpr_arg),
                     [
@@ -1853,8 +1853,24 @@ class IRBuilderTranspiler(ast.NodeTransformer):
                     [],
                 )
             else:
-                with self._rewrite_annotation(annotation):
-                    translated = self.visit(annotation)
+                if name in forward:
+                    translated = ast.Name(forward[name], ast.Load())
+                elif (
+                    name in symbols
+                    and protocol_registry.SCALAR_ANNOTATION_DTYPE.get(
+                        self.module.prescan_ctx._match_special_func(annotation)
+                    )
+                    is not None
+                ):
+                    translated = ast.Name(name, ast.Load())
+                else:
+                    reads = {
+                        read
+                        for read in collect_annotation_free_reads(annotation)
+                        if read.id in unbound
+                    }
+                    with self._rewrite_annotation(reads):
+                        translated = self.visit(annotation)
                 if const_args is not None:
                     # Optional annotations are unwrapped only for 
specialization;
                     # ordinary annotation validation belongs to the language 
variant arg.
@@ -1875,6 +1891,7 @@ class IRBuilderTranspiler(ast.NodeTransformer):
                 )
             )
             declaration.append(self._assign(alias, value, parameter))
+            unbound.discard(name)
             if alias != name:
                 declaration.append(self._assign(name, ast.Name(alias, 
ast.Load()), parameter))
         return declaration, constexpr_aliases
@@ -1943,6 +1960,7 @@ class IRBuilderTranspiler(ast.NodeTransformer):
         node: ast.FunctionDef,
         parameters: list[ast.arg],
         constexpr_aliases: dict[str, str],
+        symbol_aliases: dict[str, str],
         *,
         frame: str,
         const_args: str | None,
@@ -2001,16 +2019,8 @@ class IRBuilderTranspiler(ast.NodeTransformer):
                     )
                 )
                 body.append(self._assign(name, value, parameter))
-        for parameter in getattr(node, "type_params", ()):
-            body.append(
-                self._assign(
-                    parameter.name,
-                    self._call_dialect(
-                        "resolve_type_var_", [ast.Constant(parameter.name)], 
parameter
-                    ),
-                    parameter,
-                )
-            )
+        for name, alias in symbol_aliases.items():
+            body.append(self._assign(name, ast.Name(alias, ast.Load()), node))
         return body
 
     def _create_split_declaration(
@@ -2019,26 +2029,24 @@ class IRBuilderTranspiler(ast.NodeTransformer):
         frame: str,
         definition: ast.FunctionDef,
         frame_declaration: list[ast.stmt],
-        capture_names: set[str],
-        constexpr_aliases: dict[str, str],
+        declaration_values: list[str],
     ) -> list[ast.stmt]:
         """Keep signature snapshots separate from the body's enclosing Python 
scope."""
-        # The declaration helper returns only the frame and selected host 
values.
-        # Runtime parameters and explicit symbols are recovered from the frame 
by
-        # the sibling body helper, whose closures still observe source class 
setup.
+        # The declaration helper returns its frame, explicit symbols and host 
values.
+        # The sibling body reads runtime parameters from the frame and receives
+        # explicit symbol identities directly, without resolving names again.
         declare_name = self.module.make_fresh_name("_declare")
         signature = frame_declaration[0]
         # Decorator options evaluate outside the signature's same-named 
parameters.
         constructor = signature.items[0].context_expr
         signature.items[0].context_expr = ast.copy_location(ast.Name(frame, 
ast.Load()), node)
-        outputs = [frame, *constexpr_aliases.values()]
+        outputs = [frame, *declaration_values]
         returned = ast.copy_location(
             ast.Return(ast.Tuple([ast.Name(name, ast.Load()) for name in 
outputs], ast.Load())),
             node,
         )
         declare = self._create_definition(declare_name, [signature, returned], 
node)
         declare.args.args.append(ast.arg(frame))
-        self._capture_defaults(declare, capture_names, node)
         return [
             declare,
             ast.copy_location(
@@ -2057,9 +2065,9 @@ class IRBuilderTranspiler(ast.NodeTransformer):
     ) -> tuple[list[ast.stmt], str, ast.With]:
         """Declare a native frame and emit a lexical body helper inside its 
scope.
 
-        Original-name helper defaults retain definition values. Subsequent 
Python
-        assignments expose each signature parameter to later annotations. Body
-        helpers preserve source globals/closures, independently of annotation 
captures.
+        Python closures supply annotation values; only shadowed reads need a
+        definition snapshot. Signature assignments expose each parameter to 
later
+        annotations, while body helpers preserve source globals and closures.
         """
         kind, options = self.read_function_metadata(node)
         builder = self._function_namespace(node, kind)
@@ -2072,13 +2080,11 @@ class IRBuilderTranspiler(ast.NodeTransformer):
             parameters = [*node.args.posonlyargs, *node.args.args, 
*node.args.kwonlyargs]
             if node.args.vararg or node.args.kwarg:
                 self._raise_error(node, "IR signatures require ordinary named 
parameters")
-            facts = self.module.prescan_ctx.bindings.get(node, [])
-            annotations, returns, capture_names = 
self._read_function_annotations(
-                node, parameters, facts
-            )
+            annotations = [parameter.annotation for parameter in parameters]
+            returns = node.returns
             captures = self.module.make_fresh_name("_definition")
             self.function.definition_captures = captures
-            statements = self._create_definition_bindings(node, capture_names, 
captures=captures)
+            statements: list[ast.stmt] = []
             const_args = None
             if self.module.enable_jit_map and not local_function:
                 const_args = self.module.make_fresh_name("_const_args")
@@ -2089,14 +2095,18 @@ class IRBuilderTranspiler(ast.NodeTransformer):
                     node,
                 )
             ]
-            declaration.extend(self._create_symbol_declarations(node))
+            symbols, symbol_aliases = self._create_symbol_declarations(node)
+            declaration.extend(symbols)
             arguments, constexpr_aliases = self._rewrite_parameters(
-                parameters, annotations, captures=captures, 
const_args=const_args
+                parameters,
+                annotations,
+                captures=captures,
+                const_args=const_args,
+                symbols=symbol_aliases,
             )
             declaration.extend(arguments)
             if returns is not None:
-                with self._rewrite_annotation(returns):
-                    translated_return = self.visit(returns)
+                translated_return = self.visit(returns)
                 declaration.append(
                     ast.copy_location(
                         ast.Expr(
@@ -2121,7 +2131,12 @@ class IRBuilderTranspiler(ast.NodeTransformer):
                 split_declare=split_declare,
             )
             body = self._create_body_parameters(
-                node, parameters, constexpr_aliases, frame=frame, 
const_args=const_args
+                node,
+                parameters,
+                constexpr_aliases,
+                symbol_aliases,
+                frame=frame,
+                const_args=const_args,
             )
             body.extend(self.transform_statements(node.body))
             definition = self._create_definition(body_name, body, node)
@@ -2153,14 +2168,15 @@ class IRBuilderTranspiler(ast.NodeTransformer):
             invocation = ast.copy_location(
                 ast.Expr(ast.Call(ast.Name(body_name, ast.Load()), [], [])), 
node
             )
+            declaration_values = [*symbol_aliases.values(), 
*constexpr_aliases.values()]
             if split_declare:
                 statements.extend(
                     self._create_split_declaration(
-                        node, frame, definition, frame_declaration, 
capture_names, constexpr_aliases
+                        node, frame, definition, frame_declaration, 
declaration_values
                     )
                 )
             else:
-                outputs = list(constexpr_aliases.values())
+                outputs = declaration_values
                 if outputs:
                     declaration.append(
                         ast.copy_location(
@@ -2175,7 +2191,6 @@ class IRBuilderTranspiler(ast.NodeTransformer):
                 signature = self._create_definition(
                     self.module.make_fresh_name("_declare"), declaration, node
                 )
-                self._capture_defaults(signature, capture_names, node)
                 call = ast.Call(ast.Name(signature.name, ast.Load()), [], [])
                 signature_call = ast.copy_location(
                     ast.Assign(
@@ -2195,7 +2210,10 @@ class IRBuilderTranspiler(ast.NodeTransformer):
                 ),
                 node,
             )
-            return statements, frame, resumed
+            snapshots = self._create_definition_bindings(
+                node, self.function.capture_names, captures=captures
+            )
+            return [*snapshots, *statements], frame, resumed
         finally:
             # Restore the enclosing function before returning fragments or 
propagating
             # an error; partially rewritten nested functions cannot leave 
active state.
diff --git a/python/tvm/tirx/script/ir_builder/frame.py 
b/python/tvm/tirx/script/ir_builder/frame.py
index 870dd25d16..2aaedb4e71 100644
--- a/python/tvm/tirx/script/ir_builder/frame.py
+++ b/python/tvm/tirx/script/ir_builder/frame.py
@@ -21,7 +21,7 @@ from collections.abc import Sequence
 from tvm_ffi import Array
 from tvm_ffi import register_object as _register_object
 
-from tvm.script.ir_builder.base import IRBuilderFrame, _resolve_type_var
+from tvm.script.ir_builder.base import IRBuilderFrame
 from tvm.tirx import Buffer, Var
 
 from . import _ffi_api
@@ -33,7 +33,7 @@ class TIRFrame(IRBuilderFrame): ...
 
 @_register_object("script.ir_builder.tirx.PrimFuncFrame")
 class PrimFuncFrame(TIRFrame):
-    """Native function frame retaining signature, symbols and finalized 
results."""
+    """Native function frame retaining signature and finalized results."""
 
     def default_buffer_layout(self, shape, scope):
         """Select the default layout for buffers constructed in this 
function."""
@@ -46,10 +46,6 @@ class PrimFuncFrame(TIRFrame):
         """The native declared parameters, shared with the resumed body."""
         return self.args
 
-    def resolve_type_var(self, name, dtype=None, *, value=None, span=None):
-        """Resolve a primitive symbol in this function's native map."""
-        return _resolve_type_var(self, _ffi_api.ResolveTypeVar, name, dtype, 
value=value, span=span)
-
 
 @_register_object("script.ir_builder.tirx.ForFrame")
 class ForFrame(TIRFrame):
diff --git a/python/tvm/tirx/script/ir_builder/parser_protocol.py 
b/python/tvm/tirx/script/ir_builder/parser_protocol.py
index aab9fa4147..3be37d2dbc 100644
--- a/python/tvm/tirx/script/ir_builder/parser_protocol.py
+++ b/python/tvm/tirx/script/ir_builder/parser_protocol.py
@@ -121,9 +121,7 @@ def arg_(name: str, annotation: Any, *, span: _Span = None) 
-> _ir.Var:
         raise TypeError("T.Optional is only supported by @T.jit")
     if callable(annotation) and not isinstance(annotation, _ir.Expr):
         annotation = annotation()
-    if isinstance(annotation, _ir.PrimType) or _ir.is_prim_var(annotation):
-        annotation = resolve_type_var_(name, annotation, span=span)
-    elif isinstance(annotation, _ir.Type):
+    if isinstance(annotation, _ir.Type):
         annotation = _ir.Var(name, annotation)
     return _ffi_api.Arg(name, _base.at_(span, annotation))
 
@@ -209,17 +207,6 @@ def resolve_global_info_(content: Any) -> Any:
     raise NotImplementedError("TIRx does not support global-info lookup")
 
 
-def resolve_type_var_(
-    name: str,
-    dtype: str | _ir.Type | _ir.Var | None = None,
-    *,
-    value: _ir.Var | None = None,
-    span: _Span = None,
-) -> _ir.Var:
-    """Implements 
:func:`tvm.script.ir_builder.parser_protocol.resolve_type_var_`."""
-    return _base._current_function_frame().resolve_type_var(name, dtype, 
value=value, span=span)
-
-
 def call_global_var_(function: _ir.GlobalVar, args: Sequence[Any]) -> _ir.Expr:
     """Implements 
:func:`tvm.script.ir_builder.parser_protocol.call_global_var_`."""
     return _op._call_global(function, *args)
@@ -1107,7 +1094,6 @@ __all__ = [
     "prim_func",
     "range_",
     "resolve_global_info_",
-    "resolve_type_var_",
     "return_",
     "serial",
     "set_mutable_cell_",
diff --git a/src/relax/analysis/well_formed.cc 
b/src/relax/analysis/well_formed.cc
index aa5b443b40..6146707eb8 100644
--- a/src/relax/analysis/well_formed.cc
+++ b/src/relax/analysis/well_formed.cc
@@ -295,13 +295,23 @@ class WellFormedChecker : public relax::ExprVisitor, 
public relax::TypeVisitor {
       }
     }
 
-    // Runtime parameters themselves are definitions only in source order.
+    // Primitive parameters may supply dimensions anywhere in the signature.
+    // Register each once so duplicate parameters remain an error.
+    for (Var param : op->params) {
+      if (GetType(param).as<PrimTypeNode>()) {
+        RegisterVarDefinition(param);
+      }
+    }
+
+    // Other runtime parameters retain source-order definition scope.
     for (Var param : op->params) {
       if (auto* dataflow_var = param.as<DataflowVarNode>()) {
         TVM_FFI_VISIT_THROW(ValueError, ffi::GetRef<DataflowVar>(dataflow_var))
             << "DataflowVar " << param << " is defined outside DataflowBlock.";
       }
-      RegisterVarDefinition(param);
+      if (!GetType(param).as<PrimTypeNode>()) {
+        RegisterVarDefinition(param);
+      }
 
       auto it = param_var_func_map_.find(param);
       if (it != param_var_func_map_.end() && it->second != cur_visited_func_) {
diff --git a/src/relax/script/ir_builder/ir.cc 
b/src/relax/script/ir_builder/ir.cc
index 4104523ac9..4316b9937c 100644
--- a/src/relax/script/ir_builder/ir.cc
+++ b/src/relax/script/ir_builder/ir.cc
@@ -203,10 +203,6 @@ void FuncRetValue(const tvm::relax::Expr& value) {
 TVM_FFI_STATIC_INIT_BLOCK() {
   namespace refl = tvm::ffi::reflection;
   refl::GlobalDef()
-      .def("script.ir_builder.relax.ResolveTypeVar",
-           [](FunctionFrame frame, ffi::String name, ffi::Optional<PrimType> 
dtype,
-              ffi::Optional<tvm::Var> value,
-              Span span) { return ResolveTypeVar(&frame->type_var_map, name, 
dtype, value, span); })
       .def("script.ir_builder.relax.Function", Function)
       .def("script.ir_builder.relax.DeclFunction", DeclFunction)
       .def("script.ir_builder.relax.LocalFunction", LocalFunction)
diff --git a/src/script/ir_builder/base.cc b/src/script/ir_builder/base.cc
index 6231a9581d..8a98ab2fb1 100644
--- a/src/script/ir_builder/base.cc
+++ b/src/script/ir_builder/base.cc
@@ -29,19 +29,6 @@ namespace tvm {
 namespace script {
 namespace ir_builder {
 
-tvm::Var ResolveTypeVar(ffi::Map<ffi::String, tvm::Var>* symbols, const 
ffi::String& name,
-                        ffi::Optional<PrimType> dtype, ffi::Optional<tvm::Var> 
value, Span span) {
-  TVM_FFI_CHECK(!name.empty(), ValueError) << "A symbolic variable requires a 
nonempty name";
-  if (auto existing = symbols->Get(name)) return existing.value();
-  tvm::Var symbol =
-      value.has_value() ? value.value() : tvm::Var(name, 
dtype.value_or(PrimType::Int(64)), span);
-  TVM_FFI_CHECK(symbol->ty.as<PrimTypeNode>(), TypeError)
-      << "A symbolic variable requires a primitive type";
-  if (symbol->name.empty()) details::Namer::Name(symbol, name);
-  symbols->Set(name, symbol);
-  return symbol;
-}
-
 namespace {
 
 bool PositionLessEqual(int lhs_line, int lhs_column, int rhs_line, int 
rhs_column) {
diff --git a/src/tirx/script/ir_builder/ir.cc b/src/tirx/script/ir_builder/ir.cc
index e569af59f4..fbe17af68f 100644
--- a/src/tirx/script/ir_builder/ir.cc
+++ b/src/tirx/script/ir_builder/ir.cc
@@ -726,10 +726,6 @@ TVM_FFI_STATIC_INIT_BLOCK() {
 TVM_FFI_STATIC_INIT_BLOCK() {
   namespace refl = tvm::ffi::reflection;
   refl::GlobalDef()
-      .def("script.ir_builder.tirx.ResolveTypeVar",
-           [](PrimFuncFrame frame, ffi::String name, ffi::Optional<PrimType> 
dtype,
-              ffi::Optional<tvm::Var> value,
-              Span span) { return ResolveTypeVar(&frame->type_var_map, name, 
dtype, value, span); })
       .def("script.ir_builder.tirx.Buffer",
            static_cast<BufferVar (*)(ffi::Array<PrimExpr>, PrimType, 
ffi::String,
                                      ffi::Optional<Expr>, 
ffi::Optional<ffi::Array<PrimExpr>>,
diff --git a/tests/python/relax/test_tvmscript_parser.py 
b/tests/python/relax/test_tvmscript_parser.py
index 3a88563bc4..488a3a5e75 100644
--- a/tests/python/relax/test_tvmscript_parser.py
+++ b/tests/python/relax/test_tvmscript_parser.py
@@ -1695,21 +1695,23 @@ class Module:
     _check(mod)
 
 
-def test_later_prim_param_requires_external_shape_symbol():
-    with pytest.raises(NameError):
-        tvm.script.from_source(
-            """
+def test_later_prim_param_reuses_shape_symbol():
+    function = tvm.script.from_source(
+        """
 @R.function
 def main(x: R.Tensor([n], "float32"), n: T.int64):
     return x
 """,
-            extra_vars={
-                "I": tvm.script.ir,
-                "R": tvm.script.relax,
-                "T": tvm.script.tirx,
-                "Ts": tvm.script.s_tir,
-            },
-        )
+        extra_vars={
+            "I": tvm.script.ir,
+            "R": tvm.script.relax,
+            "T": tvm.script.tirx,
+            "Ts": tvm.script.s_tir,
+        },
+    )
+    x, n = function.params
+    assert x.ty.shape[0].same_as(n)
+    assert str(n.ty.dtype) == "int64"
 
 
 def test_non_int64_prim_param_rejected_in_shape_annotation():
diff --git a/tests/python/script/minilang.py b/tests/python/script/minilang.py
index 6439b299c9..a0f2686a0e 100644
--- a/tests/python/script/minilang.py
+++ b/tests/python/script/minilang.py
@@ -78,7 +78,6 @@ class Frame:
         )
         self.function = Function() if kind == "function" else None
         self.params = []
-        self.type_var_map = {}
         self.global_var = Value("global", (self,))
         self.local_var = Value("local", (self,))
         self.result = None
@@ -108,15 +107,6 @@ class Frame:
             self.language.result = Module(self.language.functions)
         return False
 
-    def resolve_type_var(self, name, dtype=None, *, value=None, **kwargs):
-        if name not in self.type_var_map:
-            self.type_var_map[name] = (
-                value
-                if value is not None
-                else Value("symbol", ("int64" if dtype is None else dtype,), 
name)
-            )
-        return self.type_var_map[name]
-
     def __getitem__(self, name):
         return self.language.functions[name]
 
@@ -180,7 +170,6 @@ class Language:
             ),
             break_=lambda **kwargs: self.statement("break"),
             continue_=lambda **kwargs: self.statement("continue"),
-            resolve_type_var_=self.resolve_type_var,
             resolve_global_info_=self.resolve_global_info,
             bind_=self.bind,
             check_well_formed_=lambda result: None,
@@ -217,14 +206,14 @@ class Language:
         def Tensor(shape=None, dtype="float32", device=None, placement="S[0]"):
             return Value("tensor", (shape, dtype, device, placement))
 
-        def dynamic(name, dtype="int64"):
-            return Value("symbol", (dtype,), name)
+        def dynamic(name, dtype="int64", *, span=None):
+            return self.at(span, Value("symbol", (dtype,), name))
 
         def cell(value=None):
             return Value("cell", (value,))
 
         self.M.Tensor = Tensor
-        self.M.dynamic = dynamic
+        self.M.dynamic = self.I.dynamic = dynamic
         self.M.int32 = protocol_registry.register_scalar_annotation(
             "M.int32", lambda: None, dtype="int32"
         )
@@ -262,10 +251,6 @@ class Language:
         self.frame().function.body.append(statement)
         return AlreadyEmitted(statement)
 
-    def resolve_type_var(self, name, dtype=None, **kwargs):
-        value = self.frame().resolve_type_var(name, dtype, **kwargs)
-        return value
-
     def resolve_global_info(self, name):
         return self.global_infos[name]
 
diff --git a/tests/python/script/test_parser_entry.py 
b/tests/python/script/test_parser_entry.py
index 931a4535d3..e0f4f44f34 100644
--- a/tests/python/script/test_parser_entry.py
+++ b/tests/python/script/test_parser_entry.py
@@ -73,10 +73,9 @@ def 
test_callable_entry_emits_the_expected_builder_program(language, monkeypatch
     expected = dedent(
         """
         with _I0.IRBuilder() as _builder0:
-            _definition0 = _host2(('M',), _host0(), _host1())
             with M.function_() as _fn0:
 
-                def _declare0(*, M=_definition0.get('M', _I0.MISSING)):
+                def _declare0():
                     M.func_name_('identity')
                     x = M.arg_('x', M.Tensor((4,)))
 
diff --git a/tests/python/tvmscript/test_tvmscript_parser_tir.py 
b/tests/python/tvmscript/test_tvmscript_parser_tir.py
index 59002c571e..14e5fa6415 100644
--- a/tests/python/tvmscript/test_tvmscript_parser_tir.py
+++ b/tests/python/tvmscript/test_tvmscript_parser_tir.py
@@ -172,11 +172,15 @@ class Module:
 """,
     ],
 )
-def test_tir_direct_later_prim_param_is_undefined(source):
-    with pytest.raises(NameError):
-        tvm.script.from_source(
-            source, extra_vars={"I": tvm.script.ir, "T": tvm.script.tirx, 
"Ts": tvm.script.s_tir}
-        )
+def test_tir_direct_later_prim_param_reuses_shape_symbol(source):
+    result = tvm.script.from_source(
+        source, extra_vars={"I": tvm.script.ir, "T": tvm.script.tirx, "Ts": 
tvm.script.s_tir}
+    )
+    function = result["main"] if isinstance(result, tvm.IRModule) else result
+    A, n = function.params
+    assert A.ty.shape[0].same_as(n)
+    assert str(n.ty.dtype) == "int32"
+    assert function.body.value.same_as(n)
 
 
 def test_tir_return_annotation_does_not_define_symbolic_var():

Reply via email to