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 c6ba25159f Resolve named outputs through explicit completed builder
frames
c6ba25159f is described below
commit c6ba25159f25ea039172859667f5de2fbb78fe0e
Author: Tianqi Chen <[email protected]>
AuthorDate: Mon Sep 21 17:59:38 2026 +0000
Resolve named outputs through explicit completed builder frames
---
python/tvm/relax/script/builder/__init__.py | 20 +++++++++++
python/tvm/script/ir_builder/protocol.py | 16 +++++----
python/tvm/script/parser/transpile.py | 53 +++++++++++++----------------
python/tvm/tirx/script/builder/__init__.py | 18 ++++++++++
tests/python/tvmscript/test_parser.py | 36 ++++++++++++++++++++
5 files changed, 108 insertions(+), 35 deletions(-)
diff --git a/python/tvm/relax/script/builder/__init__.py
b/python/tvm/relax/script/builder/__init__.py
index 3a5b83058e..7607bebffa 100644
--- a/python/tvm/relax/script/builder/__init__.py
+++ b/python/tvm/relax/script/builder/__init__.py
@@ -223,6 +223,25 @@ class _Frame:
return False
+def frame_result(completed_frame, name):
+ """Return one supported output of an explicitly supplied completed region.
+
+ completed_frame is a completed Relax builder context or a host Python
branch result
+ dictionary; name is the original source variable-name string. A conditional
+ exports its native designated result, whose final binding name matches in
+ both branches. Dataflow frames export their declared outputs; local
function
+ frames export their function reference. Internal branch bindings and names
+ owned by other frames are not exposed. Host dictionaries contain only the
+ lexical outputs explicitly supplied by their executed branch.
+
+ Returns the exported value, including None, or protocol.MISSING when
absent.
+ A non-string name raises TypeError; custom frame/result access errors
+ propagate. The operation is read-only, enters no frames, and retains no
+ implicit completed-frame state, so nested frame results remain independent.
+ """
+ return _protocol._frame_result(completed_frame, name)
+
+
def function(is_pure=True, is_private=False, *, local=False, reference=None,
span=None):
"""Enter a definition using the native Relax function frame."""
if local:
@@ -447,6 +466,7 @@ __all__ = [
"DTensor",
"For",
"for_",
+ "frame_result",
"resolve_type_var",
"Object",
"Prim",
diff --git a/python/tvm/script/ir_builder/protocol.py
b/python/tvm/script/ir_builder/protocol.py
index ff70228b19..ebfd9d110f 100644
--- a/python/tvm/script/ir_builder/protocol.py
+++ b/python/tvm/script/ir_builder/protocol.py
@@ -200,14 +200,18 @@ def at(span, value):
_at = at
-def frame_result(frame):
- """Return frame.result, or an empty mapping when no result is exposed.
+def _frame_result(frame, name):
+ """Read one explicit export without consulting ambient construction state.
- frame is a finalized builder context object. The result maps source names
- to exported values and is owned by the frame. This read-only operation
- enters no frames; custom attribute-access errors propagate.
+ Dialects pass a completed frame whose result is its finalized export map,
+ or a Python-branch dictionary containing host lexical results. The map
stays
+ private to its owner; a missing name returns MISSING instead of leaking a
+ branch-local binding. No frame is entered and no result is cached globally.
"""
- return getattr(frame, "result", {})
+ if not isinstance(name, str):
+ raise TypeError("A frame result name must be a string")
+ exports = frame if isinstance(frame, dict) else getattr(frame, "result",
{})
+ return exports.get(name, MISSING)
def require_defined(value, name):
diff --git a/python/tvm/script/parser/transpile.py
b/python/tvm/script/parser/transpile.py
index e78d9fbec7..01394c8c59 100644
--- a/python/tvm/script/parser/transpile.py
+++ b/python/tvm/script/parser/transpile.py
@@ -148,7 +148,7 @@ class IRBuilderTranspiler(ast.NodeTransformer):
namespaces/callables; assignments and parameters shadow entries.
dialect_prefix
selects the generated current builder alias and is restored after
functions.
bound holds only Python identifier strings. optional maps
- conditionally exported names to AST reads of builder result dictionaries;
+ conditionally exported names to AST reads of cached named builder outputs;
for example a name defined only by one branch is checked when read. Each
lexical helper copies/restores bound/namespace_bindings/optional; no
concrete values
or IR identity are stored. preserve_return and syntax hooks configure one
@@ -726,43 +726,40 @@ class IRBuilderTranspiler(ast.NodeTransformer):
invocation = self._located(ast.Call(self._name(helper, original), [],
[]), original)
return [definition, self._statement(invocation, original)]
- def _exports(self, frame, candidates, original, mapping=None):
+ def _exports(self, frame, candidates, original):
+ # Pattern: names assigned in a completed region -> one named builder
+ # request per candidate, e.g. y = X.frame_result(frame, "y"). Builders
+ # decide which names are designated outputs; absent results remain
+ # MISSING so reads cannot expose branch-local bindings accidentally.
result = []
- if mapping is None:
- mapping_stmt, mapping = self._cache(
+ for name in sorted(candidates):
+ assignment, value = self._cache(
self._call(
- self.infrastructure_name,
+ self.dialect_prefix,
"frame_result",
- [self._name(frame, original)],
+ [self._name(frame, original), ast.Constant(name)],
original,
),
original,
- "exports",
+ "output",
)
- result.append(mapping_stmt)
- for name in sorted(candidates):
- key = ast.Constant(name)
+ result.append(assignment)
condition = self._located(
- ast.Compare(copy.deepcopy(key), [ast.In()],
[copy.deepcopy(mapping)]), original
+ ast.Compare(
+ copy.deepcopy(value),
+ [ast.IsNot()],
+ [self._attribute(self.infrastructure_name, "MISSING",
original)],
+ ),
+ original,
)
- value = self._located(ast.Subscript(copy.deepcopy(mapping), key,
ast.Load()), original)
result.append(
self._located(
- ast.If(condition, [self._assign(name, value, original)],
[]), original
+ ast.If(condition, [self._assign(name,
copy.deepcopy(value), original)], []),
+ original,
)
)
- for name in candidates - self.bound:
- self.optional[name] = self._located(
- ast.Call(
- ast.Attribute(copy.deepcopy(mapping), "get", ast.Load()),
- [
- ast.Constant(name),
- self._attribute(self.infrastructure_name, "MISSING",
original),
- ],
- [],
- ),
- original,
- )
+ if name not in self.bound:
+ self.optional[name] = copy.deepcopy(value)
return result
def _with(self, context, body, original, target=None):
@@ -814,13 +811,11 @@ class IRBuilderTranspiler(ast.NodeTransformer):
native.append(
self._assign(
mapping_name,
- self._call(
- self.infrastructure_name, "frame_result",
[self._name(frame, node)], node
- ),
+ self._name(frame, node),
node,
)
)
- tail = self._exports(frame, candidates, node,
self._name(mapping_name, node))
+ tail = self._exports(mapping_name, candidates, node)
dispatch = self._located(
ast.If(
self._call(
diff --git a/python/tvm/tirx/script/builder/__init__.py
b/python/tvm/tirx/script/builder/__init__.py
index 10537d2d08..fb332d95c1 100644
--- a/python/tvm/tirx/script/builder/__init__.py
+++ b/python/tvm/tirx/script/builder/__init__.py
@@ -32,6 +32,7 @@ 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 _construction_span
+from tvm.script.ir_builder.protocol import _frame_result as _named_frame_result
from tvm.script.ir_builder.protocol import at as _at
from tvm.script.ir_builder.protocol import register_call_kind as
_register_call_kind
from tvm.script.ir_builder.protocol import source_span as _source_span
@@ -175,6 +176,23 @@ class _Frame:
return getattr(self.native, name)
+def frame_result(completed_frame, name):
+ """Return one named export from an explicitly supplied completed region.
+
+ completed_frame is a completed builder context or the result dictionary
from a host
+ Python branch; name is the original source variable-name string. Returns
+ its exported value, including None, or protocol.MISSING when not exported.
+ TIR statement frames have no implicit lexical exports, so their internal
+ bindings remain unavailable through this operation. Host branch
dictionaries
+ expose only the names explicitly supplied by that branch.
+
+ A non-string name raises TypeError; custom frame/result access errors
+ propagate. This read-only operation enters no frames, modifies no result
+ maps, and retains no last-completed-frame or cross-region state.
+ """
+ return _named_frame_result(completed_frame, name)
+
+
def function(*, private=False, s_tir=False, persistent=False, span=None):
"""Enter a native primitive-function definition frame."""
with _construction_span(span):
diff --git a/tests/python/tvmscript/test_parser.py
b/tests/python/tvmscript/test_parser.py
index a029cadc52..46667cac02 100644
--- a/tests/python/tvmscript/test_parser.py
+++ b/tests/python/tvmscript/test_parser.py
@@ -45,6 +45,7 @@ class _Recorder:
def __init__(self):
self.bindings = {}
self.returned = []
+ self.frame_requests = []
def bind_(self, value=protocol.MISSING, **metadata):
self.bindings[metadata.get("name")] = (value, metadata)
@@ -65,6 +66,12 @@ class _Recorder:
def unpack(self, value):
return value
+ def frame_result(self, frame, name):
+ self.frame_requests.append((frame, name))
+ return getattr(frame, "result", frame if isinstance(frame, dict) else
{}).get(
+ name, protocol.MISSING
+ )
+
def _registered(source, env=None, **options):
recorder = _Recorder()
@@ -655,3 +662,32 @@ def f():
]
assert chains == [(1, 2, 3)]
assert builder.returned == [(False, True, 10, True, True)]
+
+
+def test_named_results_keep_nested_frame_identity():
+ class Scope:
+ def __init__(self, value):
+ self.value = value
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, *exc):
+ self.result = {"value": self.value}
+
+ outer, inner = Scope(30), Scope(20)
+ compiler, function, builder = _registered(
+ """
+ @D.function
+ def f():
+ with outer:
+ with inner:
+ value = 1
+ value = value + 1
+ return value
+ """,
+ {"outer": outer, "inner": inner},
+ )
+ _run(compiler, function, builder)
+ assert builder.frame_requests == [(inner, "value"), (outer, "value")]
+ assert builder.returned == [30]