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

[red-knot] infer basic (name-based) annotation expressions #13130

Merged
merged 14 commits into from
Aug 30, 2024
Merged

[red-knot] infer basic (name-based) annotation expressions #13130

merged 14 commits into from
Aug 30, 2024

Conversation

chriskrycho
Copy link
Contributor

Summary

  • Introduce methods for inferring annotation and type expressions.
  • Correctly infer explicit return types from functions where they are simple names that can be resolved in scope.

Contributes to #12701 by way of helping unlock call expressions (this does not remotely finish that, as it stands, but it gets us moving that direction).

Test Plan

Added a test for function return types which use the name form of an annotation expression, since this is aiming toward call expressions. When we extend this to working for other annotation and type expression positions, we should add explicit tests for those as well.

@carljm carljm added the red-knot Multi-file analysis & type inference label Aug 27, 2024
Copy link
Member

@AlexWaygood AlexWaygood left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! A couple of nitpicks below.

Here's some high-level thoughts that shouldn't block this PR, just things we could think about as followups:

  • it might be nice if infer_annotation_expression took a custom TypeExpression enum as its input argument rather than the ast::Expr enum, to codify the fact that a valid type expression necessarily excludes certain Python expression nodes entirely, such as Expr::Calls, Expr::Lambdas, Expr::Named, etc.
  • currently we're not looking at any qualifiers an annotation expression might or might not include that wrap the annotation's type expression. But in the long run, we'll want to take note of those and record them somewhere. It might make sense for infer_annotation_expression to return something like (HasSet<TypeQualifier>, Type<'db>) rather than simply Type<'db>.

Again, these high-level comments are just me thinking out loud about how we might want this code to work in the long run; not really anything you need to respond to in this PR!

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

carljm commented Aug 28, 2024

Replying to a couple of the high-level thoughts here, though I agree that neither of them should be implemented in this PR:

  • it might be nice if infer_annotation_expression took a custom TypeExpression enum as its input argument rather than the ast::Expr enum, to codify the fact that a valid type expression necessarily excludes certain Python expression nodes entirely, such as Expr::Calls, Expr::Lambdas, Expr::Named, etc.

I'm not sure about this. It implies that we would then need to have some other method that takes an ast::Expr, emits diagnostics for invalid forms, and returns the custom enum. But it seems to me that it will be simpler, clearer, and more efficient if that is the job of infer_annotation_expression itself. Why match twice when we can match once, emit diagnostics on invalid forms and perform correct resolution of valid forms?

It seems to me like this is an idea we can keep in mind if at some point things reach a level of complexity where it really concretely improves type safety (for instance, if we have built up a large collection of methods that are subsidiary to infer_annotation_expression and all should operate only on a valid type expression form -- but I'm not convinced this will ever happen, because I think the subsidiary methods are more likely to operate on a specific expression kind instead), but this isn't something we should be a priori aiming for, or introducing for its own sake if its only taken by a single method.

currently we're not looking at any qualifiers an annotation expression might or might not include that wrap the annotation's type expression. But in the long run, we'll want to take note of those and record them somewhere. It might make sense for infer_annotation_expression to return something like (HasSet<TypeQualifier>, Type<'db>) rather than simply Type<'db>.

Yes, agreed. Probably best to sort out the details here once we are adding support for a type qualifier, so we can design it in context with how the information is used.

Copy link
Contributor

github-actions bot commented Aug 28, 2024

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

chriskrycho and others added 9 commits August 29, 2024 14:43
- Introduce methods for inferring annotation and type expressions.
- Correctly infer explicit return types from functions where they are
  simple names that can be resolved in scope.
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
Co-authored-by: Carl Meyer <carl@astral.sh>
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
This does not yet *actually* fix the underlying issue we were hoping it
would fix, i.e. a panic when inferring types in `typing.pyi` (as seen
when testing against `tomllib`), but it should be the first step in the
right direction to enabling that.
This suggests that we are going to end up with a lot of this code
scattered around. Once we have enough examples, we may want to take a
step back and work out how to refactor this to be less ad hoc.
We require that all nodes have a type, even if that type is `Unknown` or
`Unbound`, so in the case of annotation and type expressions, we must
recurse through all child expressions to identify their types, even if
we know they are actually “nonsense”. This will also be helpful in the
future when we have a language server, where we would want to show as
much valid information as possible even in invalid contexts.
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.

Great work! In terms of the broad strokes, this looks excellent.

A lot of my comments are basically just a single comment that I don't think we should add such fine-grained tracing of type inference by default; if we did it everywhere, it'd be so noisy in any realistic case that you'd never be able to find anything. When debugging at that level, it works better IMO to manually add the specific dbg!() calls that are relevant to the case you're debugging. Open to discussing this if you think I'm missing the value of this tracing!

crates/red_knot_python_semantic/src/types/infer.rs Outdated Show resolved Hide resolved
crates/red_knot_python_semantic/src/types/infer.rs Outdated Show resolved Hide resolved
crates/red_knot_python_semantic/src/types/infer.rs Outdated Show resolved Hide resolved
crates/red_knot_python_semantic/src/types/infer.rs Outdated Show resolved Hide resolved
crates/red_knot_python_semantic/src/types/infer.rs Outdated Show resolved Hide resolved
crates/red_knot_python_semantic/src/types/infer.rs Outdated Show resolved Hide resolved
crates/red_knot_python_semantic/src/types/infer.rs Outdated Show resolved Hide resolved
crates/red_knot_python_semantic/src/types/infer.rs Outdated Show resolved Hide resolved
crates/red_knot_python_semantic/src/types/infer.rs Outdated Show resolved Hide resolved
crates/red_knot_python_semantic/src/types/infer.rs Outdated Show resolved Hide resolved
@chriskrycho
Copy link
Contributor Author

Ha, very strongly agreed re: tracing instrumentation – I was going to ask if you thought any of those were valuable to keep, and I think the answer here is basically “no”. I’ll double check on the annotation-vs.-type-expressions thing as well and respond on that specific comment.

Copy link

codspeed-hq bot commented Aug 29, 2024

CodSpeed Performance Report

Merging #13130 will not alter performance

Comparing chriskrycho:rk-basic-annotation-expressions (407c27e) with main (34b4732)

Summary

✅ 32 untouched benchmarks

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.

Looks great!

crates/red_knot_python_semantic/src/types/infer.rs Outdated Show resolved Hide resolved
@carljm carljm merged commit f8656ff into astral-sh:main Aug 30, 2024
20 checks passed
@chriskrycho chriskrycho deleted the rk-basic-annotation-expressions branch August 30, 2024 15:29
@@ -77,6 +77,7 @@ fn infer_definition_types_cycle_recovery<'db>(
_cycle: &salsa::Cycle,
input: Definition<'db>,
) -> TypeInference<'db> {
tracing::trace!("infer_definition_types_cycle_recovery");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should change this to an actual sentence rather than the method name. Tracing messages are user facing (not trace but we might decide to make them user facing in the future).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is a partially-broken temporary crutch just to get us through until we have fixpoint iteration and full deferred resolution of annotations in the necessary places; it should go away. So I'm not too worried about this particular trace message sticking around for a long time. But this is a good point.

@@ -2059,6 +2086,173 @@ impl<'db> TypeInferenceBuilder<'db> {
}
}

/// Annotation expressions.
impl<'db> TypeInferenceBuilder<'db> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the motivation for placing each method in its own impl block?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea was not "each method" but "each set of related methods", though at the moment it is just one method for annotation expressions and one for type expressions; that will change. It was just a way to more clearly separate inference of value expressions from inference of annotation expressions and type expressions.

This was Chris' idea and it seemed fine to me, but I'm also fine with just using comment headers to achieve the same separation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
red-knot Multi-file analysis & type inference
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants