[ty] narrow tagged unions of TypedDict#22104
Conversation
Diagnostic diff on typing conformance testsNo changes detected when running ty on typing conformance tests ✅ |
|
|
omg look at those pydantic diagnostics disappear |
|
Wow! |
|
| Lint rule | Added | Removed | Changed |
|---|---|---|---|
invalid-key |
0 | 3,124 | 0 |
invalid-assignment |
0 | 74 | 5 |
invalid-argument-type |
2 | 19 | 19 |
invalid-return-type |
2 | 0 | 13 |
possibly-missing-attribute |
0 | 2 | 0 |
type-assertion-failure |
0 | 1 | 0 |
| Total | 4 | 3,220 | 37 |
|
CodSpeed Performance ReportMerging #22104 will not alter performanceComparing Summary
Footnotes
|
Adopt PR astral-sh#22104's approach for narrowing tagged TypedDict unions, with a performance optimization that uses direct filtering for simple unions instead of intersection-based narrowing. The key insight is that for unions containing only TypedDict variants (no intersections or other types), we can directly filter to matching variants in O(n) time, rather than building up intersection types with negations which requires O(n²) simplification. The optimization falls back to the intersection-based approach for complex cases like unions with intersections or non-TypedDict types.
AlexWaygood
left a comment
There was a problem hiding this comment.
Nice! It would be great to rebase on main and see what the codspeed report is now #22102 has landed
| let known_equality_result = match (field.declared_ty, rhs_type) { | ||
| (Type::StringLiteral(lhs), Type::StringLiteral(rhs)) => Some(lhs == rhs), | ||
| (Type::IntLiteral(lhs), Type::IntLiteral(rhs)) => Some(lhs == rhs), | ||
| // It's unlikely that the user will mix int and string literals in the same | ||
| // union, but go ahead and handle it. | ||
| (Type::StringLiteral(_), Type::IntLiteral(_)) | ||
| | (Type::IntLiteral(_), Type::StringLiteral(_)) => Some(false), | ||
| _ => None, | ||
| }; |
There was a problem hiding this comment.
another important case to handle here is enum-literal types.
However, there's some additional complications with enum literals, so I think it might be good to postpone enums to a followup. Namely, an enum literal can't be used as a "tag" if the enum class has a custom __eq__ method. That's true if the enum class inherits from IntEnum or StrEnum (we handle this incorrectly in some other situations right now).
|
Unfortunately merging |
f1d30f5 to
ae2d96f
Compare
|
04ee4dc is a substantial refactoring. After staring at some profiles and adding logs and stuff, I'm pretty sure the problem is that I'm taking these unions of ~50 TypedDicts and intersecting them with ~49 negated TypedDicts in a lot of places, and that's slow. The new approach is to synthesize a TypedDict with a single field in both cases (the |
| if u["tag"] == "foo": | ||
| # TODO: `dict & ~<TypedDict ...>` should simplify to `dict` here, but that's currently a | ||
| # false negative in `is_disjoint_impl`. | ||
| reveal_type(u) # revealed: Foo | (dict[Unknown, Unknown] & ~<TypedDict with items 'tag'>) |
There was a problem hiding this comment.
The display here changed from ~Bar to ~<TypedDict with items 'tag'>. Probably that's fine?
There was a problem hiding this comment.
I think that's fine. I think the real fix to get rid of this is to implement disjointness between dict and TypedDict types... which theoretically isn't too hard, but I'm a bit worried it might lead to false positives due to missing pieces in our TypedDict/bidirectional-inference logic elsewhere. Best done as a standalone change, anyway.
04ee4dc to
b31d29b
Compare
AlexWaygood
left a comment
There was a problem hiding this comment.
Awesome work. Thank you!!
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
|
Awesome! |
Identify and narrow cases like this:
Fixes part of astral-sh/ty#1479.