Skip to content

fix[eve]: support type aliases recursing through a container - #2814

Merged
tehrengruber merged 2 commits into
mainfrom
fix-eve-tuple-comprehension-workaround
Aug 24, 2026
Merged

fix[eve]: support type aliases recursing through a container#2814
tehrengruber merged 2 commits into
mainfrom
fix-eve-tuple-comprehension-workaround

Conversation

@tehrengruber

@tehrengruber tehrengruber commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

NestedTuple and its siblings in gt4py.eve.extended_typing were defined as plain assignments holding a string forward reference to themselves:

NestedTuple = tuple[Union[_T_co, "NestedTuple[_T_co]"], ...]

Subscripting such an alias does not substitute the type parameter inside the forward reference, so NestedTuple[Foo] expands to tuple[Foo | ForwardRef('NestedTuple[_T_co]'), ...] and get_type_hints() fails with NameError: name '_T_co' is not defined when resolving it in the namespace of the annotated object. These aliases could therefore not be used as datamodel/node field annotations at all, which is why #2487 had to fall back to target: Any instead of NestedTuple[DataSymbol] in foast.TupleComprehensionMapper.

Changes:

  • The Nested* / MaybeNested* aliases are now defined with the PEP 695 type statement, which substitutes type parameters properly across the recursion.
  • SimpleTypeValidatorFactory breaks the resulting cycle. Such an alias is well founded, unlike the type A = A cycle eval_type_alias() rejects: a single resolution step already yields an annotation which is not an alias itself. What it does not yield is a finite one, so the alias occurring inside its own definition is now handed a deferred validator, filled in as soon as the definition has been processed.

test_recursive_type_alias_is_not_supported was renamed to test_cyclic_type_alias_is_not_supported, since only degenerate cycles remain unsupported.

Once this is merged, the target: Any workaround in #2487 can be removed.

Disclaimer: This PR and its description were written largely with the help of AI. Code was reviewed briefly by me and in more detail by the reviewer.

`NestedTuple` and friends were defined as plain assignments with a string
forward reference to themselves. Subscripting such an alias does not
substitute the type parameter inside the forward reference, so
`NestedTuple[Foo]` expands to `tuple[Foo | ForwardRef('NestedTuple[_T_co]'), ...]`,
which raises a `NameError` when `get_type_hints()` tries to resolve it in
the namespace of the annotated object. As a consequence these aliases
could not be used as datamodel/node field annotations at all.

They are now defined with the PEP 695 `type` statement, which supports
recursion and parameter substitution properly. Such an alias is well
founded, unlike the `type A = A` cycle `eval_type_alias()` rejects: a
single resolution step already yields an annotation which is not an alias
itself. What it does not yield is a *finite* one, so
`SimpleTypeValidatorFactory` now hands the alias occurring inside its own
definition a deferred validator, filled in as soon as the definition has
been processed.
@tehrengruber
tehrengruber requested a review from egparedes August 22, 2026 19:54

@egparedes egparedes left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a couple of comments to make the PR cleaner.

Comment thread src/gt4py/eve/extended_typing.py Outdated
Comment on lines +190 to +194
# Note: these recursive aliases have to be defined with the PEP 695 `type` statement. Written as
# plain assignments (`NestedTuple = tuple[Union[_T_co, "NestedTuple[_T_co]"], ...]`), subscription
# does not substitute the type parameter inside the string annotation, so `NestedTuple[int]` keeps
# an unparametrized `ForwardRef("NestedTuple[_T_co]")` which cannot be resolved in the namespace
# of the annotated object (see `get_partial_type_hints`).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this comment verbose and basically unnecessary since the codebase should be upgraded soon to >= 3.12 only, so all aliases will become PEP 695 type statements anyway.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tehrengruber-ai Remove comment.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

Comment thread src/gt4py/eve/extended_typing.py Outdated
Comment on lines +195 to +202
type NestedSequence[T] = Sequence[T | NestedSequence[T]]
type NestedList[T] = list[T | NestedList[T]]
type NestedTuple[T] = tuple[T | NestedTuple[T], ...]

type MaybeNested[T] = T | NestedSequence[T]
type MaybeNestedInSequence[T] = T | NestedSequence[T]
type MaybeNestedInList[T] = T | NestedList[T]
type MaybeNestedInTuple[T] = T | NestedTuple[T]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why changing from _T_co to T as type variable here? Shouldn't the covariant type var be used here since a tuple is an immutable container?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tehrengruber-ai: Keep the previous style as requested.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No good reason — that was collateral from switching to the PEP 695 syntax, not intentional. Reverted: the aliases take _T_co again.

Note that PEP 695 has no syntax for declaring variance on a type parameter (it is inferred at the use site), so unlike the old TypeVar("_T_co", covariant=True) the name is now the only thing carrying the intent. But you are right that it is the correct one for these containers, and keeping it also keeps the aliases consistent with is_nested_tuple_of / is_maybe_nested_in_tuple_of below, which still use the module-level _T_co.


# Implementations
@dataclasses.dataclass
class _DeferredTypeValidator:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be wrong but isn't this class a duplication of the datamodels.core.ForwardRefValidator? Would it be worth to refactor the code to define it only once in the proper place (probably defined here and used from datamodels)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tehrengruber-ai Investigate.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Investigated. They look alike, but the contract differs in both directions, so I kept them separate:

  • Different protocol. ForwardRefValidator is an attrs field validator, (instance, attribute, value) — that signature is what field_type_validator_factory has to hand back to attrs. _DeferredTypeValidator is a plain FixedTypeValidator, (value, **kwargs), which is what gets composed into the surrounding validators here. type_validation also has no notion of datamodels/attrs, and the dependency only runs the other way.
  • Different filling mechanism. ForwardRefValidator is lazy and self-resolving: on its first call it goes through instance to the model class, runs update_forward_refs(model_cls) and only then can look up the field annotation and build the real validator — it cannot do any of that at construction time. _DeferredTypeValidator resolves nothing: the annotation is already in hand, and the factory assigns .validator on the very next line, before the composed validator is returned and therefore before any value can reach it. Hence the assert where the other one has a resolution step.

A shared class would have to carry both call signatures and both filling modes, which is more machinery than the ~8 lines each costs today. The one unification that would type-check is ForwardRefValidator holding a _DeferredTypeValidator instead of its own validator field, but that is pure indirection — it removes no code and hides the lazy resolution one level deeper.

I did add a sentence to the _DeferredTypeValidator docstring pointing at the distinction, so the next reader does not have to redo this comparison. Happy to go the other way if you still prefer a single place.

- Drop the explanatory comment above the `Nested*` aliases.
- Name the type parameter of the `Nested*` / `MaybeNested*` aliases `_T_co`
  again, as before the switch to the PEP 695 `type` statement.
- Note in `_DeferredTypeValidator` how it differs from the similar-looking
  `datamodels.ForwardRefValidator`.

@egparedes egparedes left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@tehrengruber
tehrengruber merged commit 477e47c into main Aug 24, 2026
30 checks passed
tehrengruber added a commit to SF-N/gt4py that referenced this pull request Aug 25, 2026
Now that eve supports type aliases recursing through a container (GridTools#2814),
annotate the comprehension target precisely as
'MaybeNestedInTuple[DataSymbol]' — the 'Maybe' variant since a bare-name
target yields a lone symbol. Align 'func_to_foast.parse_target' and the
'type_deduction' helper signatures with the same spelling.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants