[ty] Add support for narrowing on tuple match cases#25493
Conversation
d5ead3f to
974d660
Compare
Typing conformance resultsThe percentage of diagnostics emitted that were expected errors held steady at 92.16%. The percentage of expected errors that received a diagnostic held steady at 87.31%. The number of fully passing files held steady at 92/134. SummaryHow are test cases classified?Each test case represents one expected error annotation or a group of annotations sharing a tag. Counts are per test case, not per diagnostic — multiple diagnostics on the same line count as one. Required annotations (
True positives changed (5)5 diagnostics
|
Memory usage reportMemory usage unchanged ✅ |
|
84e2755 to
26466d9
Compare
aea236d to
06e0ff9
Compare
2af8f75 to
4a1649a
Compare
90ab3f9 to
c87d1de
Compare
c87d1de to
256ced0
Compare
|
(In draft due to performance regression...) |
AlexWaygood
left a comment
There was a problem hiding this comment.
Only taken a pass at the tests so far
This comment was marked as resolved.
This comment was marked as resolved.
37e0039 to
6ab34ac
Compare
| /// whether the pattern accepts additional sequence elements. | ||
| #[derive(Debug, Clone, Hash, PartialEq, salsa::Update, get_size2::GetSize)] | ||
| pub struct SequencePatternPredicateKind<'db> { | ||
| pub patterns: Vec<PatternPredicateKind<'db>>, |
There was a problem hiding this comment.
| pub patterns: Vec<PatternPredicateKind<'db>>, | |
| pub patterns: Box<[PatternPredicateKind<'db>]>, |
Feels like a boxed slice here would use less memory and better reflects the fact that this is immutable data. (No other code needs to change -- you can .collect() directly into a boxed slice)
| let star_index = self | ||
| .patterns | ||
| .iter() | ||
| .position(|pattern| matches!(pattern, PatternPredicateKind::Unsupported))?; |
There was a problem hiding this comment.
| .position(|pattern| matches!(pattern, PatternPredicateKind::Unsupported))?; | |
| .position(|pattern| pattern == &PatternPredicateKind::Unsupported)?; |
There was a problem hiding this comment.
It seems like the only kind of match pattern that remains unsupported is match-star patterns, so I think we could rename PatternPredicateKind::Unsupported to PatternPredicateKind::MatchStar at this point. It would make this function more "obviously correct" (I had to look through the code to see whether there were other still-unsupported kinds of match patterns to check whether this logic was accurate)
| self.patterns.len() == 1 && self.has_star | ||
| } | ||
|
|
||
| pub fn is_exact_length(&self) -> bool { |
There was a problem hiding this comment.
| pub fn is_exact_length(&self) -> bool { | |
| pub const fn is_exact_length(&self) -> bool { |
| PatternPredicateKind::Sequence(SequencePatternPredicateKind { | ||
| patterns: pattern | ||
| .patterns | ||
| .iter() | ||
| .map(|pattern| self.predicate_kind(pattern)) | ||
| .collect(), | ||
| has_star: pattern | ||
| .patterns | ||
| .iter() | ||
| .any(|pattern| matches!(pattern, ast::Pattern::MatchStar(_))), | ||
| }) |
There was a problem hiding this comment.
we can do this with one iteration
| PatternPredicateKind::Sequence(SequencePatternPredicateKind { | |
| patterns: pattern | |
| .patterns | |
| .iter() | |
| .map(|pattern| self.predicate_kind(pattern)) | |
| .collect(), | |
| has_star: pattern | |
| .patterns | |
| .iter() | |
| .any(|pattern| matches!(pattern, ast::Pattern::MatchStar(_))), | |
| }) | |
| let mut has_star = false; | |
| let patterns = pattern | |
| .patterns | |
| .iter() | |
| .map(|pattern| { | |
| if matches!(pattern, ast::Pattern::MatchStar(_)) { | |
| has_star = true; | |
| } | |
| self.predicate_kind(pattern) | |
| }) | |
| .collect(); | |
| PatternPredicateKind::Sequence(SequencePatternPredicateKind { patterns, has_star }) |
There was a problem hiding this comment.
It doesn't look like we yet have any tests for the & ~str & ~bytes & ~bytearray part of the intersection. E.g.
def f(x: str | tuple[int, int]):
match x:
case (a, b):
reveal_type(a) # revealed: @Todo
reveal_type(b) # revealed: @Todo
case _:
reveal_type(x) # revealed: strWe wouldn't get the correct answer in the second branch there if it weren't for the & ~str & ~bytes & ~bytearray part of the intersection. We should add similar tests for unions with bytes and bytearray too
There was a problem hiding this comment.
I think this part doesn't work yet since we don't support narrowing on the assigned names?
There was a problem hiding this comment.
(Gah, sorry, I missed the @Todo in your own example...)
There was a problem hiding this comment.
it's the revealed: str in the second branch that already works on your PR! and it wouldn't work unless we intersected the type of x with & ~str & ~bytes & ~bytearray in the first branch
| /// Positive narrowing uses this as a sound over-approximation: the result | ||
| /// may include values that fail nested refutable checks, but matching | ||
| /// values cannot fall outside it. |
There was a problem hiding this comment.
This is somewhat jargony. It would be great to add an example of Python code that demonstrates what "sound over-approximation" and "nested refutable checks" mean
6ab34ac to
afa86eb
Compare
| /// For example, consider: | ||
| /// | ||
| /// ```python | ||
| /// case [int(real=0)]: |
There was a problem hiding this comment.
| /// case [int(real=0)]: | |
| /// def f(x: list[object]): | |
| /// match x: | |
| /// case [int(real=0)]: | |
| /// reveal_type(x) |
| /// Every match is a one-element sequence containing an `int`, so the | ||
| /// returned type records those facts. It does not record the nested | ||
| /// `real=0` check, so it can also contain values such as `[1]` that fail | ||
| /// the pattern at runtime. This is intentional for positive narrowing: | ||
| /// the result may retain extra values, but cannot exclude a possible match. |
There was a problem hiding this comment.
(I think I understand what this is trying to say, but the wording here still feels a bit awkward to me FWIW... codex is not good at writing prose)
afa86eb to
210840d
Compare
Summary
This PR adds precise narrowing for fixed-length sequence match patterns.
For a pattern like
[int(), str()], we synthesize a protocol whose__len__and indexed__getitem__methodsencode the pattern’s length and element types:
We distinguish between the type necessary to match a pattern and the type guaranteed to match it. Positive
narrowing uses the necessary type, while negative narrowing and reachability only subtract values guaranteed to
match. This allows exact sequence patterns to participate in exhaustiveness analysis without incorrectly treating
refutable nested patterns as exhaustive.
Closes astral-sh/ty#561.