[red-knot] Implicit instance attributes#15811
Conversation
49bec2f to
362eb8a
Compare
carljm
left a comment
There was a problem hiding this comment.
Review not complete yet, just submitting partway through
| reveal_type(b"foo".endswith) # revealed: @Todo(bound method) | ||
| ``` | ||
|
|
||
| ## Instance attribute edge cases |
There was a problem hiding this comment.
What about assignments to attributes of the first argument of a classmethod or staticmethod?
There was a problem hiding this comment.
Those are not (properly) supported yet. We do have one pre-existing test for attribute assignments in @classmethods in this file, but we do not yet find these implicitly-declared attributes when calling .member on the class object. With this PR, we're potentially in a weird state where we find those attributes on instances, but not on the class object. If that's acceptable, I'll open an issue and add that to my goals for next week. Otherwise, I can also include it in this PR.
There was a problem hiding this comment.
I don't think the functionality scope of this PR needs to be expanded, this can definitely be a follow-up. I was more just thinking that some TODO tests around it would clarify the intention and fit in well with the existing tests in this PR, which are already a mix of "complete" and "TODO". But there's certainly no need for that, the tests can come with the feature.
There was a problem hiding this comment.
I'll note this down as a task for a follow-up.
|
|
||
| /// Tries to find declarations/bindings of an instance attribute named `name` that are only | ||
| /// "implicitly" defined in a method of the class that corresponds to `class_body_scope`. | ||
| fn implicit_instance_attribute( |
There was a problem hiding this comment.
I guess at this point we do attribute lookup fully lazily, and without any Salsa caching. I wonder if there's enough work repeated here to make some Salsa caching of attribute types worth it? But we can evaluate this separately.
There was a problem hiding this comment.
We may want to have salsa caching here to avoid invalidating types from module a if the class is defined in module b. Or is it guaranteed that this method is only ever called from within the same module (or if it is called across-modules, that it goes through a salsa query?)
There was a problem hiding this comment.
I think I need some help answering these questions.
Or is it guaranteed that this method is only ever called from within the same module
I don't think so.
or if it is called across-modules, that it goes through a salsa query?
This function is only called from Class::instance_member (via own_instance_member), which in turn can be called from:
Type::memberTypeInferenceBuilder::infer_attribute_expression
The former (Type::member) is called from all sorts of different places.
There was a problem hiding this comment.
Thanks. Yeah, in that case, I think implicit_instance_attribute or own_instance_member or member should be a salsa query because: semantic_index changes whenever the file where the class is declared changes, and nothing is constraining member calls to be from the same file.
An alternative is to introduce a attribute_assignments(db, class_body_scope) query that returns the assignments just for that scope. That might be cheaper but it still has the benefit that implicit_instance_attribute only gets invalidated when the attribute assignments in that specific scope changed.
We use a similar pattern for use_def_map and symbol_table where those queries return a specific "slice" of the SemanticIndex which is less likely to change.
Let me know if that helps or if you need some more input (also happy to discuss on Discord/sync)
There was a problem hiding this comment.
For other reviewers: I implemented something that I discussed with Micha.
Removing salsa::tracked from either attribute_assignments or the inline infer_expression_type query makes the newly added test fail.
carljm
left a comment
There was a problem hiding this comment.
Ok, finished review, only had one more comment.
This is, as usual, awesome work.
|
|
||
| /// Tries to find declarations/bindings of an instance attribute named `name` that are only | ||
| /// "implicitly" defined in a method of the class that corresponds to `class_body_scope`. | ||
| fn implicit_instance_attribute( |
There was a problem hiding this comment.
We may want to have salsa caching here to avoid invalidating types from module a if the class is defined in module b. Or is it guaranteed that this method is only ever called from within the same module (or if it is called across-modules, that it goes through a salsa query?)
128679e to
4ed94c6
Compare
…ted changes to external files
| // We use a separate salsa query here to prevent unrelated changes in the AST of an external | ||
| // file from triggering re-evaluations of downstream queries. | ||
| // See the `dependency_implicit_instance_attribute` test for more information. | ||
| #[salsa::tracked] | ||
| fn infer_expression_type<'db>(db: &'db dyn Db, expression: Expression<'db>) -> Type<'db> { | ||
| let inference = infer_expression_types(db, expression); | ||
| let expr_scope = expression.scope(db); | ||
| inference.expression_type(expression.node_ref(db).scoped_expression_id(db, expr_scope)) | ||
| } |
There was a problem hiding this comment.
I wonder if this would be useful to have as a general query and if it should be defined next to infer_expression_types instead.
There was a problem hiding this comment.
I had a similar thought. It could certainly be used in more places, but I wasn't sure if we want the additional query in those places? I'll note it down as a task and open a small PR after this has been merged.
There was a problem hiding this comment.
It is intuitive to me why attribute_assignments should be a query, but it's less intuitive to me why this should be a query. I don't see what additional dependencies are introduced here above those that the underlying infer_expression_types query would have anyway. So I guess this is not about dependencies, but rather about a smaller returned value so backdating can be more effective?
It feels intuitively to me that we would get more caching value with fewer cached memos if we placed the salsa query caching at the level of Class::own_instance_member or Type::member, rather than at such a fine-grained level that does such little work over the underlying infer_expression_types. But we should of course validate any such changes with observed performance differences.
There was a problem hiding this comment.
The problem is the expression.node_ref. It accesses the AST node from the module where the class is declared, meaning the enclosing query has to re-run whenever the declaring module changes -- which we don't want.
Doing it here is mainly about avoiding calling a query (and caching the value which is expensive) in cases where we don't need to. Although I admit, I don't have any numbers on whether caching here is a significantly lower number than caching at the own_instance_member level.
* main: (66 commits) [red-knot] Use ternary decision diagrams (TDDs) for visibility constraints (#15861) [`pyupgrade`] Rename private type parameters in PEP 695 generics (`UP049`) (#15862) Simplify the `StringFlags` trait (#15944) [`flake8-pyi`] Make `PYI019` autofixable for `.py` files in preview mode as well as stubs (#15889) Docs (`linter.md`): clarify that Python files are always searched for in subdirectories (#15882) [`flake8-pyi`] Make PEP-695 functions with multiple type parameters fixable by PYI019 again (#15938) [red-knot] Use unambiguous invalid-syntax-construct for suppression comment test (#15933) Make `Binding::range()` point to the range of a type parameter's name, not the full type parameter (#15935) Update black deviations (#15928) [red-knot] MDTest: Fix line numbers in error messages (#15932) Preserve triple quotes and prefixes for strings (#15818) [red-knot] Hand-written MDTest parser (#15926) [`pylint`] Fix missing parens in unsafe fix for `unnecessary-dunder-call` (`PLC2801`) (#15762) nit: docs for ignore & select (#15883) [airflow] `BashOperator` has been moved to `airflow.providers.standard.operators.bash.BashOperator` (AIR302) (#15922) [`flake8-logging`] `.exception()` and `exc_info=` outside exception handlers (`LOG004`, `LOG014`) (#15799) [red-knot] Enforce specifying paths for mdtest code blocks in a separate preceding line (#15890) [red-knot] Internal refactoring of visibility constraints API (#15913) [red-knot] Implicit instance attributes (#15811) [`flake8-comprehensions`] Handle extraneous parentheses around list comprehension (`C403`) (#15877) ...
Summary
Add support for implicitly-defined instance attributes, i.e. support type inference for cases like this:
A lot of things have been intentionally left out of this PR:
selfself.x = param)self.x, self.y = …Part of: #14164
Benchmarks
Codspeed reports no change in a cold-cache benchmark, and a -1% regression in the incremental benchmark. On
black'ssrcfolder, I don't see a statistically significant difference between the branches (Reminder to self: always set the CPU frequency scaling governor toperformancewhen benchmarking on a laptop):./red_knot_main check --project /home/shark/black/src./red_knot_feature check --project /home/shark/black/srcTest Plan
Updated and new Markdown tests