Conversation
9d7b8dd to
8e97da6
Compare
crates/red_knot_python_semantic/resources/mdtest/scopes/moduletype_attrs.md
Show resolved
Hide resolved
edb4d6e to
0175a1b
Compare
|
7468f67 to
54998b3
Compare
CodSpeed Performance ReportMerging #13980 will improve performances by 4.17%Comparing Summary
Benchmarks breakdown
|
crates/red_knot_python_semantic/resources/mdtest/assignment/augmented.md
Outdated
Show resolved
Hide resolved
crates/red_knot_python_semantic/resources/mdtest/assignment/augmented.md
Outdated
Show resolved
Hide resolved
crates/red_knot_python_semantic/resources/mdtest/boolean/short_circuit.md
Outdated
Show resolved
Hide resolved
a933cf9 to
39c1906
Compare
MichaReiser
left a comment
There was a problem hiding this comment.
Nice. Do we understand where the perf improvement is coming from? Is it because we now create fewer union types?
| fn as_type(&self) -> Option<Type<'db>> { | ||
| match self { | ||
| Symbol::Type(ty, _) => Some(*ty), | ||
| Symbol::Unbound => None, | ||
| } | ||
| } |
There was a problem hiding this comment.
I know that @MichaReiser asked for this method and @carljm liked Micha's comment... I worry that this blurs the line between bound symbols and possibly-but-not-necessarily bound symbols. In some ways, possibly-unbound symbols are more similar to unbound symbols than they are similar to always-bound symbols, in that attempts to read them should always cause us to emit a diagnostic. The fact that we currently have a blurred line between possibly-bound and always-bound symbols is exactly what causes the bugs outlined in #14012.
If there are situations where we really need to ignore that a symbol might possibly be unbound, I think I'd honestly prefer to be explicit about that by manually matching on the variants, rather than using this method that collapses it into a binary "either you have a Type, or you don't" enum.
There was a problem hiding this comment.
(I agree with what you said in standup earlier about merging this PR sooner rather than later to avoid merge conflicts. Don't let this comment block that from happening! This can always be tackled as a followup.)
There was a problem hiding this comment.
I actually had the same thought. And considered calling this function something like Symbol::ignore_possibly_unbound instead of the more innocent Symbol::as_type. I'll think about it and probably do a follow-up.
There was a problem hiding this comment.
I mentioned this in another comment, but this issue isn't specific to as_type. Basically all of the methods on Symbol currently ignore possibly-unboundness in some form.
There was a problem hiding this comment.
in that attempts to read them should always cause us to emit a diagnostic
I don't think this is always true. For example, if __iadd__ is possibly unbound we should union its return type with the return type from __add__, and only emit a diagnostic if __add__ doesn't exist.
But I do agree that callers should be the ones make the decision about suppressing such diagnostics, and the APIs should be very clear if they do that.
There was a problem hiding this comment.
I don't think this is always true. For example, if
__iadd__is possibly unbound we should union its return type with the return type from__add__, and only emit a diagnostic if__add__doesn't exist.
Right, that's a useful correction. But even here, we still need to account for the possibly-unbound nature and treat it differently to the case where it was definitely bound.
| pub(crate) fn unwrap_or(&self, other: Type<'db>) -> Type<'db> { | ||
| match self { | ||
| Symbol::Type(ty, _) => *ty, | ||
| Symbol::Unbound => other, | ||
| } | ||
| } | ||
|
|
||
| pub(crate) fn unwrap_or_unknown(&self) -> Type<'db> { | ||
| self.unwrap_or(Type::Unknown) | ||
| } | ||
|
|
||
| pub(crate) fn as_type(&self) -> Option<Type<'db>> { | ||
| match self { | ||
| Symbol::Type(ty, _) => Some(*ty), | ||
| Symbol::Unbound => None, | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| #[track_caller] | ||
| pub(crate) fn expect_type(self) -> Type<'db> { | ||
| self.as_type() | ||
| .expect("Expected a (possibly unbound) type, not an unbound symbol") | ||
| } | ||
|
|
||
| #[must_use] | ||
| pub(crate) fn replace_unbound_with( | ||
| self, | ||
| db: &'db dyn Db, | ||
| replacement: &Symbol<'db>, | ||
| ) -> Symbol<'db> { | ||
| match replacement { | ||
| Symbol::Type(replacement, _) => Symbol::Type( | ||
| match self { | ||
| Symbol::Type(ty, Boundness::Bound) => ty, | ||
| Symbol::Type(ty, Boundness::MayBeUnbound) => { | ||
| UnionType::from_elements(db, [*replacement, ty]) | ||
| } | ||
| Symbol::Unbound => *replacement, | ||
| }, | ||
| Boundness::Bound, | ||
| ), | ||
| Symbol::Unbound => self, | ||
| } | ||
| } |
There was a problem hiding this comment.
I do think as a follow-up (not in this PR) we should carefully consider the naming of all these APIs.
Currently unwrap_or, unwrap_or_unknown, and as_type all three ignore possibly-unboundness of this symbol (the "other" type in the unwrap methods is only used in case of full-unboundness). And replace_unbound_with ignores possibly-unboundness of the replacement symbol.
I'm not totally confident that any of these choices are right (or if they are right, perhaps the name should clarify the behavior better), but it would require analysis of all the usage sites to reconsider how we should name/structure the APIs. Given this PR generally seems to maintain current behavior reasonably well, I think it makes sense to merge it now to avoid rebase conflicts and then consider this as follow-up.
| Some(Symbol::Type(ty, boundedness)) => Symbol::Type( | ||
| declarations_ty(db, declarations, Some(ty)).unwrap_or_else(|(ty, _)| ty), | ||
| boundedness, |
There was a problem hiding this comment.
I think what boundness value we should apply to a symbol that is declared in some paths (but not all) and bound in some paths (but not all) is pretty tricky and requires further attention in future. Right now here we are always just using the boundness from bindings and ignoring declarations for purposes of boundness, but that's weird, because we only even look at bindings if the symbol may be undeclared, which means in this example:
x: int
if flag:
y: int
else
y = 3If we import from this module, we will currently report x as a definitely-bound symbol (even though it has no bindings at all!) but report y as possibly-unbound (even though every path has either a binding or a declaration for it.)
Possibly the behavior here should differ depending on whether we are importing from a stub or a regular Python module? But an argument could be made that even in a regular Python module, if it declares x: int at module level, we should just trust that and not require that we can see the binding of it.
This is not for this PR to resolve (the conclusion may mean starting to include declaredness info in Symbol as well), but it may be worth a TODO comment here.
|
Oh shoot, sorry for merging #14016 :( It didn't even occur to me that this would give you more tests to update. I won't merge anymore red-knot PRs until this lands! 😄 Edit: And... it looks like you magically fixed a bunch of the TODOs I added in those tests? Thank you!! |
481b2f4 to
13bbeb2
Compare
Summary
Type::UnboundSymboltype)closes #13671
Test Plan