Skip to content

[ty] Fix invariant gradual tuple materialization ranges - #27946

Open
carljm wants to merge 4 commits into
mainfrom
cjm/ty-invariant-gradual-tuple-materialization
Open

[ty] Fix invariant gradual tuple materialization ranges#27946
carljm wants to merge 4 commits into
mainfrom
cjm/ty-invariant-gradual-tuple-materialization

Conversation

@carljm

@carljm carljm commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

tuple[Any, ...] can materialize to an exact built-in tuple of any length. Currently we bottom-materialize this type incorrectly. We give its bottom materialization as tuple[Never, ...], which we currently simplify to tuple[()], but neither of these types is a subtype of every possible tuple type, which the bottom materialization of tuple[Any, ...] should be. As a result, valid specializations can be rejected, and an exhaustive variadic-generic class pattern can appear non-exhaustive.

The full fix here (a correct bottom materialization of tuple[Any, ...]) will be more invasive, and I prefer to delay that until we've made some more general decisions about how we want to treat bottom materializations and their (non-?)equivalence to Never, and fixed the property tests.

This limited fix just special-cases tuple[Any, ...] inside invariant specialization type relations, avoiding using the bottom materialization of tuple[Any, ...] rather than fixing it. This is a pre-requisite to avoid regression in my next steps on fixing the property tests (and another fix I'm working on). The added tests here are correct, so at least this is moving us in the right direction and locking in some better behaviors, even if the implementation isn't what it should be yet.

This is an independent prerequisite for both #27920 and #27943. It does not change Never tuple normalization or static tuple disjointness.

Test plan

  • Add mdtests for empty, nonempty, and symbolic tuple specializations; top and bottom bounds; symmetric overlap; aliases; and tuple-subclass, static-tuple, and mixed-prefix exclusions.
  • Verify class-pattern exhaustiveness for empty, nonempty, and symbolic variadic specializations, including the minimized regression exposed by [ty] Reject fixed tuples for non-inferable TypeVarTuples #27943. Check the combined changes against [ty] Reject fixed tuples for non-inferable TypeVarTuples #27943's fixed-tuple rejection tests.
  • Cover union simplification with nested cycle-recovery approximations and ensure unresolved divergence markers do not count as unrestricted gradual elements.

The stable property suite has two existing failures involving tuple[Never] disjointness, reproduced both with and without this change. Those are addressed by the separate tuple-disjointness work in #27920.

@carljm carljm added the ty Multi-file analysis & type inference label Aug 20, 2026
@astral-sh-bot

astral-sh-bot Bot commented Aug 20, 2026

Copy link
Copy Markdown

Typing conformance results

No changes detected ✅

Current numbers
The percentage of diagnostics emitted that were expected errors held steady at 97.68%. The percentage of expected errors that received a diagnostic held steady at 93.44%. The number of fully passing files held steady at 109/136.

@astral-sh-bot

astral-sh-bot Bot commented Aug 20, 2026

Copy link
Copy Markdown

Memory usage report

Memory usage unchanged ✅

@astral-sh-bot

astral-sh-bot Bot commented Aug 20, 2026

Copy link
Copy Markdown

ecosystem-analyzer results

No diagnostic changes detected ✅

Full report with detailed diff (timing results)

@carljm
carljm marked this pull request as ready for review August 21, 2026 00:59
@carljm
carljm requested a review from a team as a code owner August 21, 2026 00:59
@astral-sh-bot
astral-sh-bot Bot requested a review from dcreager August 21, 2026 00:59
@carljm
carljm requested review from dhruvmanila and removed request for dcreager August 21, 2026 01:00
Comment on lines +628 to +632
def symbolic(value: Variadic[*Ts]) -> int:
match value:
case Variadic():
reveal_type(value) # revealed: Variadic[*tuple[*Ts@symbolic]]
return 1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should we use assert_never instead?

def symbolic(value: Variadic[*Ts]) -> None:
    match value:
        case Variadic():
            reveal_type(value)  # revealed: Variadic[*tuple[*Ts@symbolic]]
        case _:
            assert_never(value)

Comment on lines +638 to +642
def empty(value: Variadic[()]) -> int:
match value:
case Variadic():
reveal_type(value) # revealed: Variadic[()]
return 1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

same as above

Comment on lines +648 to +652
def nonempty(value: Variadic[int]) -> int:
match value:
case Variadic():
reveal_type(value) # revealed: Variadic[int]
return 1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

and here

matches!(tuple, TupleSpec::Variable(tuple)
if tuple.prefix_elements().is_empty()
&& tuple.suffix_elements().is_empty()
&& matches!(tuple.variable(), VariableSegment::Homogeneous(Type::Dynamic(_))))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should we resolve type aliases for the variable type before comparing it to the dynamic type? So, something like following is also correct:

type Dynamic = Any

# Passes.
static_assert(is_subtype_of(Box[tuple[int]], Top[Box[tuple[Any, ...]]]))

# Incorrectly fails: Dynamic is an alias for Any.
static_assert(is_subtype_of(Box[tuple[int]], Top[Box[tuple[Dynamic, ...]]]))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants