Skip to content
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

Merged
merged 27 commits into from
May 28, 2024

Conversation

plredmond
Copy link
Contributor

@plredmond plredmond commented May 14, 2024

  • Add a module type, ModuleTypeId
  • Add an attribute lookup method get_member for Type
    • Only implemented for ModuleTypeId and ClassTypeId
    • Should this be a trait?
      Answer: no
    • Uses unwrap, but we should remove that. Maybe add a new variant to QueryError?
      Answer: Return Option<Type> as is done elsewhere
  • Add infer_definition_type case for Import
  • Add infer_expr_type case for Attribute
  • Add a test to exercise these
  • remove all NOTE/FIXME/TODO after discussing with reviewers

Copy link
Contributor

github-actions bot commented May 14, 2024

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

@plredmond plredmond changed the title [redknot] attr chain lookup for unspecified-encoding rule [redknot] module attr chain lookup for unspecified-encoding rule May 14, 2024
@plredmond plredmond changed the title [redknot] module attr chain lookup for unspecified-encoding rule [redknot] module type, attribute type lookup May 24, 2024
@plredmond plredmond changed the title [redknot] module type, attribute type lookup [redknot] module type, attribute lookup May 24, 2024
@plredmond plredmond changed the title [redknot] module type, attribute lookup [redknot] add module type and attribute lookup for some types May 24, 2024
@plredmond plredmond marked this pull request as ready for review May 24, 2024 21:49
@plredmond
Copy link
Contributor Author

Since you folks were ooo, I added questions that I had as NOTE/TODO/FIXME comments in the diff.

@MichaReiser MichaReiser added the internal An internal refactor or improvement label May 27, 2024
Copy link
Member

@MichaReiser MichaReiser left a 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.

crates/red_knot/src/types.rs Outdated Show resolved Hide resolved
crates/red_knot/src/types.rs Outdated Show resolved Hide resolved
crates/red_knot/src/types.rs Show resolved Hide resolved
Copy link
Contributor

@carljm carljm left a 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 Show resolved Hide resolved
crates/red_knot/src/types.rs Outdated Show resolved Hide resolved
crates/red_knot/src/types.rs Outdated Show resolved Hide resolved
Comment on lines 82 to 83
// TODO return a type FF at least one of the unioned-types have the member; return
// the union over those member types
Copy link
Contributor

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.

Copy link
Member

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.

Copy link
Contributor

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.

Copy link
Contributor Author

@plredmond plredmond May 28, 2024

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)

Copy link
Contributor

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.)

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Comment on lines 87 to 88
// TODO return a type IFF all of the intersected-types have the member; but what
// type?
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

crates/red_knot/src/types.rs Outdated Show resolved Hide resolved
@plredmond plredmond requested a review from carljm May 28, 2024 17:17
@plredmond
Copy link
Contributor Author

plredmond commented May 28, 2024

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.

Copy link
Contributor

@carljm carljm left a 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.

crates/red_knot/src/types.rs Outdated Show resolved Hide resolved
crates/red_knot/src/types.rs Outdated Show resolved Hide resolved
@carljm
Copy link
Contributor

carljm commented May 28, 2024

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?

@plredmond
Copy link
Contributor Author

  • Clippy: rename an unused variable which we'll use after completing the get_member case for unions
  • Added case to infer_definition_type for AnnotatedAssignment variant which returns Type::Unknown when the value isn't present; includes a TODO to look at the annotation (which presumably will have a type expression)

@@ -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:?}")
Copy link
Contributor Author

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'>

Copy link
Contributor

@carljm carljm May 28, 2024

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.

Copy link
Contributor Author

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.

Copy link
Contributor

@carljm carljm left a 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.

@plredmond
Copy link
Contributor Author

plredmond commented May 28, 2024

Oh weird. Maybe the CI-clippy is getting some stricter options than my local one.[Edit: No, I just forgot to run it] Ty

@plredmond plredmond merged commit 9a3b9f9 into main May 28, 2024
19 checks passed
@plredmond plredmond deleted the redknot.attrchain branch May 28, 2024 20:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
internal An internal refactor or improvement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants