Skip to content

[ty] Add error context for intersection types - #24772

Merged
sharkdp merged 1 commit into
mainfrom
david/intersection-ctx
Apr 23, 2026
Merged

sharkdp merged 1 commit into
mainfrom
david/intersection-ctx

Conversation

@sharkdp

@sharkdp sharkdp commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Summary

This implementation is basically the dual of what we do for unions (with the assignability direction flipped).

Test Plan

Updated mdtests.

@sharkdp sharkdp added the ty Multi-file analysis & type inference label Apr 21, 2026
@astral-sh-bot

astral-sh-bot Bot commented Apr 21, 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 87.94%. The percentage of expected errors that received a diagnostic held steady at 83.36%. The number of fully passing files held steady at 79/133.

@astral-sh-bot

astral-sh-bot Bot commented Apr 21, 2026

Copy link
Copy Markdown

Memory usage report

Memory usage unchanged ✅

@astral-sh-bot

astral-sh-bot Bot commented Apr 21, 2026

Copy link
Copy Markdown

ecosystem-analyzer results

No diagnostic changes detected ✅

Flaky changes detected. This PR summary excludes flaky changes; see the HTML report for details.

Full report with detailed diff (timing results)

@sharkdp
sharkdp force-pushed the david/intersection-ctx branch 2 times, most recently from 19addde to 5b2f0cc Compare April 21, 2026 17:50
@sharkdp
sharkdp marked this pull request as ready for review April 21, 2026 17:52
| |
| Declared type
|
info: no element of intersection `HasBar & HasNeither` is assignable to `SupportsFooAndBar`

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.

hmmm... when Protocol types are involved, I don't think it's necessarily true that at least one element of the intersection type must be assignable to the declared type. E.g. this feels like it should be fine?

from typing import Protocol, runtime_checkable

@runtime_checkable
class Foo(Protocol):
    def foo(self): ...

class Bar(Protocol):
    def bar(self): ...

class FooAndBar(Foo, Bar, Protocol): ...

def f(x: Bar):
    if isinstance(x, Foo):
        reveal_type(x)  # Bar & Foo
        y: FooAndBar = x

although... we currently emit a diagnostic on the last line there 🙈 not sure why yet https://play.ty.dev/3d1b9f3f-cbf9-423f-9c0c-495550da8d57

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.

astral-sh/ty#338 feels related conceptually

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.

not sure why yet

Isn't that the expected behavior based on our implementation of type relations for intersections, which (for assignability from an intersection) is precisely "break it apart and require at least one element to be assignable to the target"?

I don't disagree that Foo & Bar should be assignable to FooAndBar, but to support that we would need to add a new relation arm for intersection vs protocol that is higher priority than the main intersection arm.

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.

but to support that we would need to add a new relation arm for intersection vs protocol that is higher priority than the main intersection arm.

Right, or just move the existing "anything vs a protocol" arm higher in relation.rs? I can experiment tomorrow

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.

Interesting observation!

Does this suggest that we should change the "no element of intersection …" message here for the common case? I would think it's still fine?

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.

Strictly speaking I think the "no element of interesection ..." message is only accurate if the right-hand side is a nominal type. Most types are nominal types, so your message is usually accurate. But if the right-hand side is a Callable/Protocol, or a union/intersection of Callable/Protocol types, I think the message would be inaccurate. Detection of those cases wouldn't be hard -- but perhaps then we need to figure out what to say instead in those cases

@carljm carljm Apr 22, 2026

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.

If we make changes in a future PR to allow intersections to be assignable to structural types without breaking them apart to test individual elements, then the error context collection for those assignability checks will naturally happen in a different code path and use a different error context variant. We wouldn't use NoIntersectionElementAssignableToTarget context at all; instead we'd give (e.g.) Protocol oriented error context about the members of Foo & Bar vs FooAndBar; we wouldn't even enter the assignability code path touched in this PR.

So I think we should consider that part of that (future) feature. NoIntersectionElementAssignableToTarget is clearly named, and is specific to a code path where that is exactly what we check, so the current error message is usefully precise about the actual assignability requirements in this code path.

Put differently: any "detection of those cases" that we would add in this PR would be duplicative of the actual reordering of check-pair cases discussed above, and would become dead code once we actually do that reordering.

So it seems best to me to land this PR without making changes to this error message.

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.

If we make changes in a future PR to allow intersections to be assignable to structural types without breaking them apart to test individual elements, then the error context collection for those assignability checks will naturally happen in a different code path and use a different error context variant.

I don't think that's necessarily the case at all — fundamentally whether an intersection satisfies a protocol should be answerable in the same way as the question of whether a nominal instance type satisfies a protocol. Just check whether all protocol members are available on the intersection type with the correct type. Currently we have a separate, special-cased code path for Protocol <-> Protocol assignability, but ideally we wouldn't, there's no particularly principled reason for it being that way currently

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.

@carljm pointed out in person that we're vigorously agreeing with each other, so yes, I think this PR is fine 😄

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.

#24796 to fix the Intersection <-> Protocol assignability bug

Comment on lines +758 to +761
info: ├── type `DoesNotSupportFoo1` is not assignable to protocol `SupportsFoo`
info: │ └── protocol member `foo` is not defined on type `DoesNotSupportFoo1`
info: └── type `DoesNotSupportFoo2` is not assignable to protocol `SupportsFoo`
info: └── protocol member `foo` is not defined on type `DoesNotSupportFoo2`

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.

ideally we could possibly look at making these more concise, in the future? e.g.

Suggested change
info: ├── type `DoesNotSupportFoo1` is not assignable to protocol `SupportsFoo`
info: │ └── protocol member `foo` is not defined on type `DoesNotSupportFoo1`
info: └── type `DoesNotSupportFoo2` is not assignable to protocol `SupportsFoo`
info: └── protocol member `foo` is not defined on type `DoesNotSupportFoo2`
info: └── protocol member `foo` is not defined on any intersection element

this is obviously a big improvement as-is, though

@sharkdp
sharkdp force-pushed the david/intersection-ctx branch from 5b2f0cc to 3f7bbef Compare April 23, 2026 16:52
@sharkdp
sharkdp merged commit 4f449ae into main Apr 23, 2026
56 checks passed
@sharkdp
sharkdp deleted the david/intersection-ctx branch April 23, 2026 17:29
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.

3 participants