-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[redknot] add module type and attribute lookup for some types #11416
Conversation
|
…ment statements in lint_unspecified_encoding
Since you folks were ooo, I added questions that I had as NOTE/TODO/FIXME comments in the diff. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me overall but I let @carljm approve who has more insight on what the actual inference logic should be.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is excellent!
crates/red_knot/src/types.rs
Outdated
// TODO return a type FF at least one of the unioned-types have the member; return | ||
// the union over those member types |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accessing a member of a union should really be a type error if any of the unioned types lack the member. In inference-only mode we won't want to issue this error, though -- I think we should add Unknown
to the member type union instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Long-term it would be interesting to still return a "guessed" type when only some variants implement the specific member. It could allow for better recovery in some situations (e.g. intelli sense). The API would need to be explicit that the type isn't certain.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that's actually what my comment proposes, that e.g. if we have x: A | B | C
and we access x.foo
, and types A
and B
have a foo
attribute but C
does not, we would return A.foo | B.foo | Unknown
instead of erroring. Effectively this is our best guess of the type, and the union with Unknown
is the marker of our uncertainty.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok! This approach (returning the union of get_member
results for each type in the union, and adding Unknown
once if any of result is None
) makes more sense than what I was suggesting. I can do that in a followup PR if desired.
One question: Is there any case where get_member
of a union type doesn't return a result?
For example, if you have A | B
and neither has a .foo
attribute, would (A | B).get_member("foo")
be Type::Unknown
? (sorry abusing syntax here)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah this is a good question, and I don't have a clear answer yet. I need to think through more carefully when we should return None vs Unknown, and how that will interact with inference-only vs type-checking modes.
In principle a nice clean answer would to always use Unknown and never return None. But I worry that this doesn't allow us to distinguish "attribute doesn't exist" from "attribute exists but resolving its type gives us Unknown." But maybe it's ok to not distinguish these from the caller, if we're able to surface diagnostics wherever we create an Unknown. Which I think we should be able to. (It may require passing some kind of diagnostic-sink to all these methods.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On the other hand I can imagine that in some lint rules we might want to be able to distinguish these cases, without necessarily creating a diagnostic for the missing attribute.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that keeping those cases distinct is important at this level, but perhaps it should wait until we have a use case to drive their clarification.
crates/red_knot/src/types.rs
Outdated
// TODO return a type IFF all of the intersected-types have the member; but what | ||
// type? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In an intersection, the type must be all of the intersected types, so if any of those have the member, it has the member. So we can return a type if any of the types have it; if more than one has it, we should return the intersection of those types.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- No types have the member, return
None
- One type has the member, return the member type (or a singleton intersection containing it?).
- Two types have the member, return the intersection of the member types.
- Two types have the member, but one type doesn't have the member...? It seems to me that the intersection ought to be empty.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There never a need for singleton intersection (or union), it should always simplify to just the single type.
In the final case, the type that is missing the attribute should just contribute nothing to the intersection, it shouldn't contribute the bottom type (thus making the intersection empty). I would have to think a bit more about the right way to think about this formally, but I'm sure it's the right behavior. A & B & C must be an A and a B and a C, so if only A and B have the attribute, we know the attribute is present and its type is A.foo & B.foo; C contributes no information about its type.
Thanks for your review. I've made requested changes and asked some questions to describe the follow-up work in comments. Let me know when/if it's ok to merge. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think with the one method name change I suggested, this is ready to merge. The union and intersection TODOs don't specify all the details we discussed in those comment threads, but they don't need to, especially since we're not totally clear on all those details yet.
Oh there are a couple clippy issues to fix, too. Looks like you could easily support AnnotatedAssignment the same as Assignment for now (with a TODO to actually look at the annotation) and our Definition handling would cover all variants? |
…y adding a case for AnnotatedAssignment per @CARJM
|
@@ -529,6 +594,10 @@ impl std::fmt::Display for DisplayType<'_> { | |||
Type::Never => f.write_str("Never"), | |||
Type::Unknown => f.write_str("Unknown"), | |||
Type::Unbound => f.write_str("Unbound"), | |||
Type::Module(module_id) => { | |||
// NOTE: something like this?: "<module 'module-name' from 'path-from-fileid'>" | |||
todo!("{module_id:?}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I left this as a TODO for now, but could implement it if you tell me what you'd like. It's the type of a module, which doesn't have much info when python prints it out. Probably a bikeshed.
>>> import x
>>> x
<module 'x' from '/home/asteroid/code/redknot.attrchain/x.py'>
>>> type(x)
<class 'module'>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think copying the runtime output (for the module object, not the type, since this is not the general module type but the literal type for a particular module) is reasonable here; totally fine with you either doing it in this PR or leaving it as a todo for later, up to you.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, DisplayType
only has access to TypeStore
. We'd need a SemanticDb
to call ModuleTypeId::name
and/or a Files
to call Files::path
on FileId
. I don't think I can access either of these in this context. I'll leave it for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clippy still not happy with that variable name, but once you appease clippy this looks good.
Oh weird. |
ModuleTypeId
get_member
forType
ModuleTypeId
andClassTypeId
Answer: no
unwrap
, but we should remove that. Maybe add a new variant toQueryError
?Answer: Return
Option<Type>
as is done elsewhereinfer_definition_type
case forImport
infer_expr_type
case forAttribute