From 1014cd31a32970ec43bf9b2afe29818a8921ad18 Mon Sep 17 00:00:00 2001 From: Zheng QiHang Date: Mon, 31 Aug 2026 09:30:56 +0800 Subject: [PATCH 1/3] fix(parser): validate HIR return annotations --- docs/spec/hir.md | 10 ++ docs/spec/parser.md | 25 +++ src/tilefoundry/ir/types/utils.py | 9 + src/tilefoundry/parser/pattern_nodes.py | 155 +++++++++++++++++- tests/analysis/test_analysis_families.py | 4 +- .../placed/prefill_decode_attention.py | 6 +- tests/parser/test_dimensions.py | 2 +- tests/parser/test_functions.py | 83 +++++++++- tests/passes/test_hir_to_tir.py | 2 +- 9 files changed, 279 insertions(+), 17 deletions(-) diff --git a/docs/spec/hir.md b/docs/spec/hir.md index 7d7e72b3..ad5bdd27 100644 --- a/docs/spec/hir.md +++ b/docs/spec/hir.md @@ -116,6 +116,16 @@ declares, but it MUST NOT create a level or change one's extent. `return_type`. The projection is fixed at construction and stays consistent across construction sites. +**Parser return contract.** At the DSL boundary, a body with an authored return +annotation MUST satisfy `types_compatible(annotation, body.type)`. A normal +function keeps its inferred `body.type` as `return_type` after that check. A +`pass` dispatch prototype MUST declare its return type; it becomes the base +return type and every variant body MUST satisfy it before the variant receives +that exact base `return_type`. `Tensor[...]` without a storage slot is a GMEM +annotation, including here: it is not an unconstrained storage spelling. The +parser accepts `tuple[...]` and applies the same compatibility relation +recursively to its fields. See [parser §3.1](./parser.md#31-hir-return-contracts). + **Call typing — visitor-scoped inference.** A `Call` keeps its authored `Function` template as `target`. Its result type is inferred by seeding a new visitor memo with the actual argument types bound to the callee's formal diff --git a/docs/spec/parser.md b/docs/spec/parser.md index f0a4e3fb..7bcd07c3 100644 --- a/docs/spec/parser.md +++ b/docs/spec/parser.md @@ -105,8 +105,11 @@ tensor-optional-slot ::= layout tensor ::= tensor-head '[' '(' (tensor-shape-layout ',' dtype | tensor-shape-layout ',' dtype ',' tensor-optional-slot | tensor-shape-layout ',' dtype ',' tensor-optional-slot ',' tensor-optional-slot) ')' ']' +tuple-type ::= 'tuple' '[' '(' type-annotation (',' type-annotation)* ')' ']' + | 'tuple' '[' type-annotation ']' scalar-type ::= primary type-annotation ::= tensor + | tuple-type | scalar-type signature ::= (name ':' type-annotation (',' name ':' type-annotation)*)? return-type ::= type-annotation @@ -194,6 +197,7 @@ function ::= 'def' name '(' signature ')' ('->' return-type)? ':' b | explicit_layout, layout, placed_layout, plain_layout | layout_shape, tensor_optional_slot, tensor_shape | LayoutShapeRule | A layout must have a valid non-boolean shape. | src/tilefoundry/parser/ast_pattern.py | | function | function | FunctionDialectRule | A function kind and constructed value must agree with the active dialect. | src/tilefoundry/parser/pattern_nodes.py | | function | function | FunctionRegistrationRule | A validated function must be registered exactly once in its owning scope. | src/tilefoundry/parser/pattern_nodes.py | +| function | function | FunctionReturnCompatibilityRule | A HIR body with a return annotation must satisfy that annotation; a dispatch prototype must declare one, and each variant body must satisfy the prototype return contract. | src/tilefoundry/parser/pattern_nodes.py | | function | function | FunctionRoleValidationRule | A root, variant, or converter must satisfy its role before registration. | src/tilefoundry/parser/pattern_nodes.py | | function | function | FunctionSignatureRule | A function must construct an ordered parameter tuple. | src/tilefoundry/parser/pattern_nodes.py | | index_slice | subscript_index | TileWindowSliceBoundRule | A tile window cannot be used as a slice bound. | src/tilefoundry/parser/pattern_nodes.py | @@ -219,6 +223,27 @@ function ::= 'def' name '(' signature ')' ('->' return-type)? ':' b | Pattern Visitor | Traverses the same graph to render this section's generated grammar and constraints. | | Refusal | Carries the reason from the pattern that claimed a node and then refused it, so a report names a cause rather than the absence of a match. | +### 3.1 HIR Return Contracts + +An ordinary HIR function and a specialization variant MAY omit `-> return-type`; +an ordinary function then records its inferred `body.type` as `Function.return_type`. +When either declares an annotation, the parser MUST require +`types_compatible(annotation, body.type)`. This is directional compatibility, +not a parser-only equality relation: an annotation with `layout=None` leaves +layout unconstrained according to the shared type rule. + +A `pass` HIR function is a dispatch prototype and MUST declare `-> return-type`. +That annotation is its `Function.return_type` and callable return type. Every +variant body MUST be compatible with that base return type; its own IR return +type remains the exact base type so all variants share one dispatch signature. + +`Tensor[...]` without a storage slot constructs `storage=GMEM`, including in a +return annotation. It is not an unspecified-storage spelling. Consequently an +SMEM body under `-> Tensor[...]` must explicitly return a GMEM result; the +parser reports both the annotation and inferred body type, with the authored +function location, when they are incompatible. `tuple[...]` annotations are +accepted and apply this same compatibility rule recursively to every field. + ```mermaid classDiagram ParserAPI --> FuncParserContext diff --git a/src/tilefoundry/ir/types/utils.py b/src/tilefoundry/ir/types/utils.py index 92877bb0..7ae78fd9 100644 --- a/src/tilefoundry/ir/types/utils.py +++ b/src/tilefoundry/ir/types/utils.py @@ -61,6 +61,15 @@ def layout_compatible(declared_layout, actual_layout) -> bool: ) and layout_compatible(declared.layout, actual.layout) ) + if isinstance(declared, TupleType): + return ( + isinstance(actual, TupleType) + and len(declared.fields) == len(actual.fields) + and all( + types_compatible(declared_field, actual_field) + for declared_field, actual_field in zip(declared.fields, actual.fields) + ) + ) return actual == declared diff --git a/src/tilefoundry/parser/pattern_nodes.py b/src/tilefoundry/parser/pattern_nodes.py index 3ad41a86..3cf35439 100644 --- a/src/tilefoundry/parser/pattern_nodes.py +++ b/src/tilefoundry/parser/pattern_nodes.py @@ -34,6 +34,8 @@ from tilefoundry.ir.types import TensorType from tilefoundry.ir.types.dim import DimVar from tilefoundry.ir.types.shard import Broadcast, Layout, Partial, Split +from tilefoundry.ir.types.substitute import canonicalize_dims +from tilefoundry.ir.types.utils import types_compatible from .ast_pattern import ( _BINARY_OPERATORS, @@ -963,11 +965,94 @@ def construct(match, children, context): RULES: ClassVar[tuple[AstRule[Any], ...]] = () +class TupleTypePattern(ElementPattern): + """Parse ``tuple[T, U, ...]`` as a parser-only type annotation value.""" + + element_name = "tuple_type" + syntax = LazyPattern( + lambda: ChoicePattern( + BranchPattern( + "tuple", + AstNodePattern( + ast.Subscript, + FieldPattern( + "value", + AstNodePattern( + ast.Name, + FieldPattern("id", LiteralPattern("tuple")), + ), + ), + FieldPattern( + "slice", + AstNodePattern( + ast.Tuple, + CapturePattern("field_count", lambda node, context: len(node.elts)), + FieldPattern( + "elts", + RepeatPattern( + ChildPattern( + "field_{index}", + lambda: TypeAnnotationPattern(), + "type_annotation", + "tuple_field", + ), + minimum=1, + ), + ), + ), + ), + ), + pattern_id="tuple.annotation", + ), + BranchPattern( + "tuple_single", + AstNodePattern( + ast.Subscript, + FieldPattern( + "value", + AstNodePattern( + ast.Name, + FieldPattern("id", LiteralPattern("tuple")), + ), + ), + FieldPattern( + "slice", + ChildPattern( + "field_0", + lambda: TypeAnnotationPattern(), + "type_annotation", + "tuple_field", + ), + ), + ), + pattern_id="tuple.annotation", + ), + ) + ) + + @staticmethod + def construct(match, children, context): + fields = tuple( + children[f"field_{index}"] + for index in range(match.captures.get("field_count", 1)) + ) + if not all(isinstance(field, (runtime.TensorType, runtime.TupleType)) for field in fields): + raise ParseError.from_node( + match.node, + context, + "tuple annotation fields must resolve to Tensor or tuple types", + ) + return runtime.TupleType(fields=fields) + + RULES: ClassVar[tuple[AstRule[Any], ...]] = () + + class TypeAnnotationPattern(ElementPattern): element_name = "type_annotation" syntax = LazyPattern( lambda: ChoicePattern( TensorPattern(), + TupleTypePattern(), ScalarTypePattern(), ) ) @@ -4062,6 +4147,60 @@ def apply(self, value, *, match, context): return value +_AUTHORED_RETURN_ANNOTATION = "_tilefoundry_authored_return_annotation" +_AUTHORED_BODY_TYPE = "_tilefoundry_authored_body_type" + + +@dataclass(frozen=True) +class FunctionReturnCompatibilityRule: + STATEMENT: ClassVar[str] = ( + "A HIR body with a return annotation must satisfy that annotation; a " + "dispatch prototype must declare one, and each variant body must " + "satisfy the prototype return contract." + ) + + def apply(self, value, *, match, context): + if context.function is None: + raise ParseError.from_node(match.node, context, "function lacks parser context") + if context.function.dialect != "hir": + return value + declared = getattr(value, _AUTHORED_RETURN_ANNOTATION, None) + body_type = getattr(value, _AUTHORED_BODY_TYPE, None) + try: + if body_type is None: + if context.function.role is FunctionRole.ROOT and declared is None: + raise ParseError.from_node( + match.node, + context, + "HIR pass prototype requires a return annotation", + ) + return value + if declared is not None and not types_compatible(declared, body_type): + raise ParseError.from_node( + match.node, + context, + "return annotation is not compatible with the inferred body type: " + f"annotation {declared!r}, body {body_type!r}", + ) + if context.function.role is FunctionRole.VARIANT: + base = context.function.base + if not isinstance(base, runtime.Function): + raise ParseError.from_node( + match.node, context, "variant lacks a HIR dispatch prototype" + ) + if not types_compatible(base.return_type, body_type): + raise ParseError.from_node( + match.node, + context, + f"variant body type {body_type!r} is not compatible with " + f"dispatch return contract {base.return_type!r}", + ) + return value + finally: + delattr(value, _AUTHORED_RETURN_ANNOTATION) + delattr(value, _AUTHORED_BODY_TYPE) + + @dataclass(frozen=True) class FunctionRoleValidationRule: STATEMENT: ClassVar[str] = ( @@ -4196,14 +4335,11 @@ def construct(match, children, context): specializations = context.function.specializations converter = context.function.converter if context.function.dialect == "hir": + declared_return = ( + None if declared_return is None else canonicalize_dims(declared_return) + ) if body is None: - if declared_return is None: - raise ParseError.from_node( - match.node, - context, - "HIR pass prototype requires a return annotation", - ) - return_type = declared_return + return_type = declared_return or runtime.UnitType() elif context.function.role is FunctionRole.VARIANT: base = context.function.base assert isinstance(base, runtime.Function) @@ -4222,6 +4358,8 @@ def construct(match, children, context): return_type=return_type, specializations=specializations, ) + setattr(function, _AUTHORED_RETURN_ANNOTATION, declared_return) + setattr(function, _AUTHORED_BODY_TYPE, None if body is None else body.type) if context.function.role is FunctionRole.VARIANT: setattr(function, runtime.DISPLAY_NAME, match.captures["name"]) function.name = function_name @@ -4252,6 +4390,7 @@ def construct(match, children, context): RULES: ClassVar[tuple[AstRule[Any], ...]] = ( FunctionSignatureRule(), FunctionDialectRule(), + FunctionReturnCompatibilityRule(), FunctionRoleValidationRule(), FunctionRegistrationRule(), ) @@ -4286,6 +4425,7 @@ def _body_as_ast_module(body: object, *, strip_docstring: bool = False) -> ast.M "FunctionDialectRule", "FunctionPattern", "FunctionRegistrationRule", + "FunctionReturnCompatibilityRule", "FunctionRoleValidationRule", "FunctionSignatureRule", "IndexEndpointPattern", @@ -4324,6 +4464,7 @@ def _body_as_ast_module(body: object, *, strip_docstring: bool = False) -> ast.M "TensorShapeLayoutPattern", "TupleAssignmentPattern", "TupleExpressionPattern", + "TupleTypePattern", "TypeAnnotationPattern", "UnaryExpressionPattern", "CallVariadicInputFormRule", diff --git a/tests/analysis/test_analysis_families.py b/tests/analysis/test_analysis_families.py index ba3b4712..464c81e9 100644 --- a/tests/analysis/test_analysis_families.py +++ b/tests/analysis/test_analysis_families.py @@ -268,7 +268,7 @@ class _SplitLastAxis: def last_axis( x: Tensor[(1, _SPLIT_BLOCK, _SPLIT_HIDDEN), "bf16"], w: ConstTensor[(_SPLIT_HIDDEN, _SPLIT_OUT), "bf16"], - ) -> Tensor[(1, _SPLIT_BLOCK, _SPLIT_OUT), "bf16"]: + ): with Mesh(("cta",), layout=(_SPLIT_GRID,), names=("unit",)) as mesh: rows = tf.reshard( x[:, :, 0:_SPLIT_BLOCK], (1, _SPLIT_BLOCK, _SPLIT_BLOCK), "smem" @@ -287,7 +287,7 @@ class _SplitStripMajor: def strip_major( x: Tensor[(1, _SPLIT_BLOCK, _SPLIT_HIDDEN), "bf16"], w: ConstTensor[(_SPLIT_GRID, _SPLIT_HIDDEN, _SPLIT_PER), "bf16"], - ) -> Tensor[(_SPLIT_GRID, _SPLIT_BLOCK, _SPLIT_PER), "bf16"]: + ): with Mesh(("cta",), layout=(_SPLIT_GRID,), names=("unit",)) as mesh: rows = tf.reshard( x[:, :, 0:_SPLIT_BLOCK], (1, _SPLIT_BLOCK, _SPLIT_BLOCK), "smem" diff --git a/tests/fixtures/placed/prefill_decode_attention.py b/tests/fixtures/placed/prefill_decode_attention.py index c26a1fa8..393ee20b 100644 --- a/tests/fixtures/placed/prefill_decode_attention.py +++ b/tests/fixtures/placed/prefill_decode_attention.py @@ -106,7 +106,11 @@ def decode( running_out = next_out normalized = running_out / running_sum - return tf.transpose(tf.cast(normalized, dtype="bf16"), perm=(0, 2, 1, 3)) + return tf.reshard( + tf.transpose(tf.cast(normalized, dtype="bf16"), perm=(0, 2, 1, 3)), + (1, SEQ, HEADS, HEAD_DIM), + "gmem", + ) @attend.specialize(DimVarRangePat("seq", 2, 4097)) def prefill( diff --git a/tests/parser/test_dimensions.py b/tests/parser/test_dimensions.py index 36f371fc..88b1d5f1 100644 --- a/tests/parser/test_dimensions.py +++ b/tests/parser/test_dimensions.py @@ -19,7 +19,7 @@ class Model: @func def f( x: Tensor[(1, 16, 8192), "f32"], - ) -> Tensor[(1, 16, 8192), "f32"]: + ): with Mesh(("cta",), layout=(128,), names=("unit",)) as mesh: width = 4096 + 4096 return tf.reshard( diff --git a/tests/parser/test_functions.py b/tests/parser/test_functions.py index 158acb5d..3f40d136 100644 --- a/tests/parser/test_functions.py +++ b/tests/parser/test_functions.py @@ -13,7 +13,10 @@ from tilefoundry.inspection import as_script from tilefoundry.ir.core import Call, SourceSpanMetadata, get_metadata from tilefoundry.ir.core.module import Module, subtree +from tilefoundry.ir.core.pattern import DimVarRangePat from tilefoundry.ir.hir.function import Function +from tilefoundry.ir.types import TupleType +from tilefoundry.ir.types.dim import DimVar from tilefoundry.ir.types.storage import StorageKind from tilefoundry.ir.visitor import collect_exprs from tilefoundry.parser import ParseError @@ -45,19 +48,40 @@ def add(function: Function) -> None: return tuple(found) -def test_a_lying_return_annotation_is_ignored_not_rejected() -> None: - @func(target=CudaTarget("nvidia.h200_sxm"), topologies=(Topology("cta", 1),)) +def test_a_return_annotation_validates_a_layout_inferred_from_the_body() -> None: + @func(target=CudaTarget("nvidia.h200_sxm"), topologies=(Topology("cta", 2),)) def annotated( x: Tensor[(8, 16), "f32"], - ) -> Tensor[(8, 16), "f32", None, "smem"]: - return tf.mul(x, x) + ) -> Tensor[(8, 16), "f32"]: + with Mesh(("cta",), layout=(2,), names=("lane",)) as cta: + return tf.reshard(x, (8 @ cta.lane, 16), "gmem") fn = annotated.entry_function() assert fn.return_type == fn.body.type assert fn.return_type.storage is StorageKind.GMEM + assert fn.return_type.layout is not None + +def test_an_incompatible_return_annotation_reports_annotation_and_body_types() -> None: + with pytest.raises(ParseError) as raised: -def test_a_dispatch_prototype_still_requires_a_return_annotation() -> None: + @func(target=CudaTarget("nvidia.h200_sxm"), topologies=(Topology("cta", 1),)) + def annotated( + x: Tensor[(8, 16), "f32"], + ) -> Tensor[(8, 16), "f32", None, "smem"]: + return tf.mul(x, x) + + message = str(raised.value) + assert "return annotation is not compatible with the inferred body type" in message + assert "annotation TensorType" in message + assert "body TensorType" in message + assert "StorageKind.SMEM" in message + assert "StorageKind.GMEM" in message + assert "(role 'func')" in message + assert "tests/parser/test_functions.py:" in message + + +def test_a_dispatch_prototype_requires_a_return_annotation() -> None: with pytest.raises(ParseError, match="prototype requires a return annotation"): @module( @@ -71,6 +95,55 @@ def root(x: Tensor[(8, 16), "f32"]): pass +def test_a_dispatch_prototype_uses_its_tuple_annotation_as_a_variant_contract() -> None: + size = DimVar("prototype_size", 1, 9) + + @module( + entry="root", + target=CudaTarget("nvidia.h200_sxm"), + topologies=(Topology("cta", 1),), + ) + class TupleDispatch: + @func + def root( + x: Tensor[(size,), "f32"], + ) -> tuple[Tensor[(size,), "f32"], Tensor[(size,), "f32"]]: + pass + + @root.specialize(DimVarRangePat("prototype_size", 1, 9)) + def both( + x: Tensor[(size,), "f32"], + ) -> tuple[Tensor[(size,), "f32"], Tensor[(size,), "f32"]]: + return x, x + + prototype = TupleDispatch.entry_function() + assert isinstance(prototype.return_type, TupleType) + assert prototype.return_type == prototype.variants[0].return_type + assert prototype.type.return_type == prototype.return_type + + +def test_a_variant_body_must_satisfy_its_dispatch_return_contract() -> None: + size = DimVar("prototype_mismatch_size", 1, 9) + + with pytest.raises(ParseError, match="variant body type .*dispatch return contract"): + + @module( + entry="root", + target=CudaTarget("nvidia.h200_sxm"), + topologies=(Topology("cta", 1),), + ) + class IncompatibleVariant: + @func + def root( + x: Tensor[(size,), "f32"], + ) -> tuple[Tensor[(size,), "f32"], Tensor[(size,), "f32"]]: + pass + + @root.specialize(DimVarRangePat("prototype_mismatch_size", 1, 9)) + def scalar(x: Tensor[(size,), "f32"]): + return x + + def test_a_storage_the_target_does_not_have_is_refused() -> None: refusal = re.escape( "storage tmem is not allowed by hardware context ('gmem', 'smem', 'rmem', 'umat')" diff --git a/tests/passes/test_hir_to_tir.py b/tests/passes/test_hir_to_tir.py index 0c31e3a7..7a5a4de3 100644 --- a/tests/passes/test_hir_to_tir.py +++ b/tests/passes/test_hir_to_tir.py @@ -51,7 +51,7 @@ def test_umat_param_rejected_at_lowering() -> None: """ @func - def f(x: Tensor[(8,), "f32", None, StorageKind.UMAT]) -> Tensor[(8,), "f32"]: + def f(x: Tensor[(8,), "f32", None, StorageKind.UMAT]): return x fn = f From 443f92dde31f58302d3df8c59a0ccf41ccc8387f Mon Sep 17 00:00:00 2001 From: Zheng QiHang Date: Mon, 31 Aug 2026 09:53:09 +0800 Subject: [PATCH 2/3] docs(spec): locate return contracts with parser --- docs/spec/hir.md | 15 +++++---------- docs/spec/parser.md | 42 +++++++++++++++++++++--------------------- 2 files changed, 26 insertions(+), 31 deletions(-) diff --git a/docs/spec/hir.md b/docs/spec/hir.md index ad5bdd27..284f5cd7 100644 --- a/docs/spec/hir.md +++ b/docs/spec/hir.md @@ -111,21 +111,16 @@ parser-lexical mesh binding; `ShardLayout.mesh` MUST point at an active binding on the lexical path. A `Mesh` MAY map fewer levels than the domain declares, but it MUST NOT create a level or change one's extent. +**Return type.** `Function.return_type` is the HIR result `Type`: a +`TensorType` for one result or a `TupleType` for multiple results. It is part of +the function signature and is the result component projected into +`Function.type`. + **Value type.** `Function.type` is the IR-level `CallableType` ([types §7](./types.md#7-callabletype)) projected from `params` + `return_type`. The projection is fixed at construction and stays consistent across construction sites. -**Parser return contract.** At the DSL boundary, a body with an authored return -annotation MUST satisfy `types_compatible(annotation, body.type)`. A normal -function keeps its inferred `body.type` as `return_type` after that check. A -`pass` dispatch prototype MUST declare its return type; it becomes the base -return type and every variant body MUST satisfy it before the variant receives -that exact base `return_type`. `Tensor[...]` without a storage slot is a GMEM -annotation, including here: it is not an unconstrained storage spelling. The -parser accepts `tuple[...]` and applies the same compatibility relation -recursively to its fields. See [parser §3.1](./parser.md#31-hir-return-contracts). - **Call typing — visitor-scoped inference.** A `Call` keeps its authored `Function` template as `target`. Its result type is inferred by seeding a new visitor memo with the actual argument types bound to the callee's formal diff --git a/docs/spec/parser.md b/docs/spec/parser.md index 7bcd07c3..944305ae 100644 --- a/docs/spec/parser.md +++ b/docs/spec/parser.md @@ -17,6 +17,27 @@ def parse_function( ) -> hir.Function | tir.PrimFunction: ... ``` +### 1.1 HIR Return Contracts + +An ordinary HIR function and a specialization variant MAY omit `-> return-type`; +an ordinary function then records its inferred `body.type` as `Function.return_type`. +When either declares an annotation, the parser MUST require +`types_compatible(annotation, body.type)`. This is directional compatibility, +not a parser-only equality relation: an annotation with `layout=None` leaves +layout unconstrained according to the shared type rule. + +A `pass` HIR function is a dispatch prototype and MUST declare `-> return-type`. +That annotation is its `Function.return_type` and callable return type. Every +variant body MUST be compatible with that base return type; its own IR return +type remains the exact base type so all variants share one dispatch signature. + +`Tensor[...]` without a storage slot constructs `storage=GMEM`, including in a +return annotation. It is not an unspecified-storage spelling. Consequently an +SMEM body under `-> Tensor[...]` must explicitly return a GMEM result; the +parser reports both the annotation and inferred body type, with the authored +function location, when they are incompatible. `tuple[...]` annotations are +accepted and apply this same compatibility rule recursively to every field. + - Every parser-authored `Call` reachable from a Function body carries `SourceSpanMetadata` for the AST expression that constructed it. A parent match fills only Calls without a span, so it cannot replace a more precise child span. Traversal follows `Call` operands and IR `Tuple` @@ -223,27 +244,6 @@ function ::= 'def' name '(' signature ')' ('->' return-type)? ':' b | Pattern Visitor | Traverses the same graph to render this section's generated grammar and constraints. | | Refusal | Carries the reason from the pattern that claimed a node and then refused it, so a report names a cause rather than the absence of a match. | -### 3.1 HIR Return Contracts - -An ordinary HIR function and a specialization variant MAY omit `-> return-type`; -an ordinary function then records its inferred `body.type` as `Function.return_type`. -When either declares an annotation, the parser MUST require -`types_compatible(annotation, body.type)`. This is directional compatibility, -not a parser-only equality relation: an annotation with `layout=None` leaves -layout unconstrained according to the shared type rule. - -A `pass` HIR function is a dispatch prototype and MUST declare `-> return-type`. -That annotation is its `Function.return_type` and callable return type. Every -variant body MUST be compatible with that base return type; its own IR return -type remains the exact base type so all variants share one dispatch signature. - -`Tensor[...]` without a storage slot constructs `storage=GMEM`, including in a -return annotation. It is not an unspecified-storage spelling. Consequently an -SMEM body under `-> Tensor[...]` must explicitly return a GMEM result; the -parser reports both the annotation and inferred body type, with the authored -function location, when they are incompatible. `tuple[...]` annotations are -accepted and apply this same compatibility rule recursively to every field. - ```mermaid classDiagram ParserAPI --> FuncParserContext From 13fbaac65aa5d8d9244656e4fbcda76ee510a854 Mon Sep 17 00:00:00 2001 From: Zheng QiHang Date: Mon, 31 Aug 2026 10:03:18 +0800 Subject: [PATCH 3/3] docs(parser): section public parser contracts --- docs/spec/parser.md | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/docs/spec/parser.md b/docs/spec/parser.md index 944305ae..92c1645b 100644 --- a/docs/spec/parser.md +++ b/docs/spec/parser.md @@ -38,15 +38,22 @@ parser reports both the annotation and inferred body type, with the authored function location, when they are incompatible. `tuple[...]` annotations are accepted and apply this same compatibility rule recursively to every field. -- Every parser-authored `Call` reachable from a Function body carries `SourceSpanMetadata` for - the AST expression that constructed it. A parent match fills only Calls without a span, so it - cannot replace a more precise child span. Traversal follows `Call` operands and IR `Tuple` - values, but does not assign source identity to shared lexical `Var` values. Source spans use - physical source-file coordinates with a one-based start column. -- For `a, b = producer(...)`, detached `TupleGetItem(index=0)` and - `TupleGetItem(index=1)` lexical values carry the respective target Name spans (`a` and `b`) and - matching `BindingMetadata`; later reads do not replace that identity. A multi-carry loop's - derived projections carry the `for` statement span and their carry binding name. +### 1.2 Source Span Metadata + +Every parser-authored `Call` reachable from a Function body carries `SourceSpanMetadata` for +the AST expression that constructed it. A parent match fills only Calls without a span, so it +cannot replace a more precise child span. Traversal follows `Call` operands and IR `Tuple` +values, but does not assign source identity to shared lexical `Var` values. Source spans use +physical source-file coordinates with a one-based start column. + +### 1.3 Tuple Binding Metadata + +For `a, b = producer(...)`, detached `TupleGetItem(index=0)` and +`TupleGetItem(index=1)` lexical values carry the respective target Name spans (`a` and `b`) and +matching `BindingMetadata`; later reads do not replace that identity. A multi-carry loop's +derived projections carry the `for` statement span and their carry binding name. + +### 1.4 Context and Diagnostics `FuncParserContext` carries the dialect, Function role, closure, topology scope, target, and optional base/key for one parse. `FunctionRole` is `ROOT`, `VARIANT`, or `CONVERTER`.