-
Notifications
You must be signed in to change notification settings - Fork 60
feat[next]: Add support for tuple comprehensions #2833
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e5b676a
8643e6d
7b21e9c
4a117af
666c8b4
0f11a59
681ce77
5da530e
dc655ee
c1ccbad
68de5f4
840b85e
bbb5099
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| --- | ||
| tags: [] | ||
| --- | ||
|
|
||
| # Homogeneous Tuple Comprehensions | ||
|
|
||
| - **Status**: valid | ||
| - **Authors**: Till Ehrengruber (@tehrengruber), Sara Faghih-Naini (@SF-N) | ||
| - **Created**: 2026-08-27 | ||
| - **Updated**: 2026-08-27 | ||
|
|
||
| In the context of tuple comprehensions in the field-view frontend, facing the constraint that every FOAST node carries exactly one type, we decided to support only homogeneous iterables — all elements of the same type — and reject heterogeneous ones in the type deduction. | ||
|
|
||
| ## Context | ||
|
|
||
| Tuple comprehensions, e.g. `tuple(2.0 * el for el in (a, b))`, are typed and lowered with a single mapper: one target (`el`) and one element expression (`2.0 * el`), shared by all elements of the iterable. In FOAST every node has a single `type` attribute, so the target symbol — and consequently every node in the element expression — can only be typed once. If the iterable's elements had different types, the mapper would need a different type per element, i.e. per-element re-typing (monomorphization) of the element expression, which the FOAST type system does not support. | ||
|
|
||
| The same constraint exists at the GTIR level: the ITIR type inference also stores a single type per node (and asserts on conflicting re-assignment), so the single `map_tuple` lambda used to lower comprehensions over variable-length tuples can only have one function type. For variable-length iterables heterogeneity cannot occur in the first place, since `VarArgType` describes all elements with a single element type. Rejecting heterogeneous iterables in the FOAST type deduction just surfaces the error earliest, with a source location. | ||
|
|
||
| ## Decision | ||
|
|
||
| Only homogeneous iterables are supported, both fixed-length and variable-length. Heterogeneous ones are rejected in the type deduction. E.g., with `a`, `b`, `c`, `d` of equal type: | ||
|
|
||
| ```python | ||
| tuple(2.0 * el for el in (a, b)) # supported | ||
| tuple(2.0 * el for el in (a(V2E), b(V2E))) # supported | ||
| tuple(local_el + el for local_el, el in ((a(V2E), b), (c(V2E), d))) # supported | ||
| tuple(2.0 * el for el in (a(V2E), b)) # rejected: local vs. non-local element | ||
| ``` | ||
|
|
||
| Note that homogeneity applies to the iterable's elements as a whole: in the third example each element is a pair of a local and a non-local field, but all elements share that same tuple type, so each target symbol still has a single consistent type. | ||
|
|
||
| ## Consequences | ||
|
|
||
| - Typing and lowering stay simple: the element expression is visited once, with one type per node. | ||
| - Computations over differently-typed elements cannot be written as a comprehension; they must be spelled out per element. | ||
| - The restriction could be lifted for fixed-length iterables by typing the mapper generically: the target symbol gets a type variable bounded by the valid element types, so a single type per node still suffices during type deduction. After `map_tuple` expansion the mapper is instantiated once per element, and each instance can then be specialized to its concrete element type. This is possible as a follow-up without breaking existing code, since it only widens the set of accepted programs. For variable-length iterables there is nothing to lift: heterogeneity cannot occur, as `VarArgType` has a single element type by construction. | ||
|
|
||
| ## References | ||
|
|
||
| - PR [#2833](https://github.com/GridTools/gt4py/pull/2833) (tuple comprehension support) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -113,9 +113,12 @@ def __call__(self, inp: ConcreteFOASTOperatorDef) -> ConcretePASTProgramDef: | |
| *partial_program_type.definition.kw_only_args.keys(), | ||
| ] | ||
| assert isinstance(type_, ts.CallableType) | ||
| assert arg_types[-1] == type_info.return_type( | ||
| return_type = type_info.return_type( | ||
| type_, with_args=list(arg_types), with_kwargs=kwarg_types | ||
| ) | ||
| # Not equality: variadic comprehensions give a `VarArg[...]` return type, | ||
| # while 'out' is a concrete tuple. | ||
| assert type_info.is_concretizable(return_type, arg_types[-1]) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
@gtx.field_operator
def fo(a: tuple[IField, ...], b: IField) -> tuple[tuple[IField, ...], IField]:
return tuple(x * 2 for x in a), b
fo((f1, f2), f3, out=((o1, o2), o3))On roundtrip this raises a bare
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @tehrengruber-ai Makes sense. Add a unit test. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 68de5f4 (pending push): 'is_concretizable' now recurses elementwise for TupleType-to-TupleType (equal lengths required), so a variable-length tuple nested inside a fixed return tuple concretizes. Unit test 'test_is_concretizable_tuple_with_nested_vararg' in test_type_info.py, plus the integration test 'test_var_len_tuple_comprehension_in_fixed_tuple_return' in test_tuples.py exercising exactly your '(new_tracers, rho)' direct-call repro — verified end-to-end on roundtrip (result ((2, 4), 7)). |
||
| assert args_names[-1] == "out" | ||
|
|
||
| params_decl: list[past.Symbol] = [ | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The element expression's type isn't validated before it goes into
VarArgType(element_type=...)here orTupleType(types=[...])at line 840, so a non-data element raises a raw eveTypeErrorwithout a location. Forgetting the call parentheses,tuple(helper for t in a)withhelpera field operator, givesTypeError: 'VarArgType.element_type' must be <class '...DataType'> (got 'FieldOperatorType(...'.tuple(TDim for t in a)raises for a variadic iterable but type-checks for a fixed-length one.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The whole DataType concept is not well designed so I'd rather keep it the way it is right now (which gives an understandable error message) instead of complicating things even further.