Skip to content

Commit

Permalink
[red-knot] function signature representation (#14304)
Browse files Browse the repository at this point in the history
## Summary

Add a typed representation of function signatures (parameters and return
type) and infer it correctly from a function.

Convert existing usage of function return types to use the signature
representation.

This does not yet add inferred types for parameters within function body
scopes based on the annotations, but it should be easy to add as a next
step.

Part of #14161 and #13693.

## Test Plan

Added tests.
  • Loading branch information
carljm authored Nov 14, 2024
1 parent ba6c7f6 commit a48d779
Show file tree
Hide file tree
Showing 8 changed files with 559 additions and 67 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ async def get_int_async() -> int:
reveal_type(get_int_async()) # revealed: @Todo
```

## Generic

```py
def get_int[T]() -> int:
return 42

reveal_type(get_int()) # revealed: int
```

## Decorated

```py
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,10 @@ except EXCEPTIONS as f:
## Dynamic exception types

```py
# TODO: we should not emit these `call-possibly-unbound-method` errors for `tuple.__class_getitem__`
def foo(
x: type[AttributeError],
y: tuple[type[OSError], type[RuntimeError]], # error: [call-possibly-unbound-method]
z: tuple[type[BaseException], ...], # error: [call-possibly-unbound-method]
y: tuple[type[OSError], type[RuntimeError]],
z: tuple[type[BaseException], ...],
):
try:
help()
Expand Down
10 changes: 5 additions & 5 deletions crates/red_knot_python_semantic/resources/mdtest/generics.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,31 +65,31 @@ A PEP695 type variable defines a value of type `typing.TypeVar` with attributes

```py
def f[T, U: A, V: (A, B), W = A, X: A = A1]():
reveal_type(T) # revealed: TypeVar
reveal_type(T) # revealed: T
reveal_type(T.__name__) # revealed: Literal["T"]
reveal_type(T.__bound__) # revealed: None
reveal_type(T.__constraints__) # revealed: tuple[()]
reveal_type(T.__default__) # revealed: NoDefault

reveal_type(U) # revealed: TypeVar
reveal_type(U) # revealed: U
reveal_type(U.__name__) # revealed: Literal["U"]
reveal_type(U.__bound__) # revealed: type[A]
reveal_type(U.__constraints__) # revealed: tuple[()]
reveal_type(U.__default__) # revealed: NoDefault

reveal_type(V) # revealed: TypeVar
reveal_type(V) # revealed: V
reveal_type(V.__name__) # revealed: Literal["V"]
reveal_type(V.__bound__) # revealed: None
reveal_type(V.__constraints__) # revealed: tuple[type[A], type[B]]
reveal_type(V.__default__) # revealed: NoDefault

reveal_type(W) # revealed: TypeVar
reveal_type(W) # revealed: W
reveal_type(W.__name__) # revealed: Literal["W"]
reveal_type(W.__bound__) # revealed: None
reveal_type(W.__constraints__) # revealed: tuple[()]
reveal_type(W.__default__) # revealed: type[A]

reveal_type(X) # revealed: TypeVar
reveal_type(X) # revealed: X
reveal_type(X.__name__) # revealed: Literal["X"]
reveal_type(X.__bound__) # revealed: type[A]
reveal_type(X.__constraints__) # revealed: tuple[()]
Expand Down
79 changes: 50 additions & 29 deletions crates/red_knot_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub(crate) use self::display::TypeArrayDisplay;
pub(crate) use self::infer::{
infer_deferred_types, infer_definition_types, infer_expression_types, infer_scope_types,
};
pub(crate) use self::signatures::Signature;
use crate::module_resolver::file_to_module;
use crate::semantic_index::ast_ids::HasScopedAstId;
use crate::semantic_index::definition::Definition;
Expand All @@ -35,6 +36,7 @@ mod display;
mod infer;
mod mro;
mod narrow;
mod signatures;
mod unpacker;

#[salsa::tracked(return_ref)]
Expand Down Expand Up @@ -1271,11 +1273,11 @@ impl<'db> Type<'db> {
Type::FunctionLiteral(function_type) => {
if function_type.is_known(db, KnownFunction::RevealType) {
CallOutcome::revealed(
function_type.return_ty(db),
function_type.signature(db).return_ty,
*arg_types.first().unwrap_or(&Type::Unknown),
)
} else {
CallOutcome::callable(function_type.return_ty(db))
CallOutcome::callable(function_type.signature(db).return_ty)
}
}

Expand Down Expand Up @@ -1461,6 +1463,24 @@ impl<'db> Type<'db> {
}
}

/// If we see a value of this type used as a type expression, what type does it name?
///
/// For example, the builtin `int` as a value expression is of type
/// `Type::ClassLiteral(builtins.int)`, that is, it is the `int` class itself. As a type
/// expression, it names the type `Type::Instance(builtins.int)`, that is, all objects whose
/// `__class__` is `int`.
#[must_use]
pub fn in_type_expression(&self, db: &'db dyn Db) -> Type<'db> {
match self {
Type::ClassLiteral(_) | Type::SubclassOf(_) => self.to_instance(db),
Type::Union(union) => union.map(db, |element| element.in_type_expression(db)),
Type::Unknown => Type::Unknown,
// TODO map this to a new `Type::TypeVar` variant
Type::KnownInstance(KnownInstanceType::TypeVar(_)) => *self,
_ => Type::Todo,
}
}

/// The type `NoneType` / `None`
pub fn none(db: &'db dyn Db) -> Type<'db> {
KnownClass::NoneType.to_instance(db)
Expand Down Expand Up @@ -2322,7 +2342,10 @@ impl<'db> FunctionType<'db> {
self.decorators(db).contains(&decorator)
}

/// inferred return type for this function
/// Typed externally-visible signature for this function.
///
/// This is the signature as seen by external callers, possibly modified by decorators and/or
/// overloaded.
///
/// ## Why is this a salsa query?
///
Expand All @@ -2331,34 +2354,32 @@ impl<'db> FunctionType<'db> {
///
/// Were this not a salsa query, then the calling query
/// would depend on the function's AST and rerun for every change in that file.
#[salsa::tracked]
pub fn return_ty(self, db: &'db dyn Db) -> Type<'db> {
#[salsa::tracked(return_ref)]
pub fn signature(self, db: &'db dyn Db) -> Signature<'db> {
let function_stmt_node = self.body_scope(db).node(db).expect_function();
let internal_signature = self.internal_signature(db);
if function_stmt_node.decorator_list.is_empty() {
return internal_signature;
}
// TODO process the effect of decorators on the signature
Signature::todo()
}

/// Typed internally-visible signature for this function.
///
/// This represents the annotations on the function itself, unmodified by decorators and
/// overloads.
///
/// These are the parameter and return types that should be used for type checking the body of
/// the function.
///
/// Don't call this when checking any other file; only when type-checking the function body
/// scope.
fn internal_signature(self, db: &'db dyn Db) -> Signature<'db> {
let scope = self.body_scope(db);
let function_stmt_node = scope.node(db).expect_function();

// TODO if a function `bar` is decorated by `foo`,
// where `foo` is annotated as returning a type `X` that is a subtype of `Callable`,
// we need to infer the return type from `X`'s return annotation
// rather than from `bar`'s return annotation
// in order to determine the type that `bar` returns
if !function_stmt_node.decorator_list.is_empty() {
return Type::Todo;
}

function_stmt_node
.returns
.as_ref()
.map(|returns| {
if function_stmt_node.is_async {
// TODO: generic `types.CoroutineType`!
Type::Todo
} else {
let definition =
semantic_index(db, scope.file(db)).definition(function_stmt_node);
definition_expression_ty(db, definition, returns.as_ref())
}
})
.unwrap_or(Type::Unknown)
let definition = semantic_index(db, scope.file(db)).definition(function_stmt_node);
Signature::from_function(db, definition, function_stmt_node)
}

pub fn is_known(self, db: &'db dyn Db, known_function: KnownFunction) -> bool {
Expand Down
2 changes: 1 addition & 1 deletion crates/red_knot_python_semantic/src/types/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ impl Display for DisplayRepresentation<'_> {
Type::SubclassOf(SubclassOfType { class }) => {
write!(f, "type[{}]", class.name(self.db))
}
Type::KnownInstance(known_instance) => f.write_str(known_instance.as_str()),
Type::KnownInstance(known_instance) => f.write_str(known_instance.repr(self.db)),
Type::FunctionLiteral(function) => f.write_str(function.name(self.db)),
Type::Union(union) => union.display(self.db).fmt(f),
Type::Intersection(intersection) => intersection.display(self.db).fmt(f),
Expand Down
40 changes: 12 additions & 28 deletions crates/red_knot_python_semantic/src/types/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -822,8 +822,7 @@ impl<'db> TypeInferenceBuilder<'db> {
.as_deref()
.expect("function type params scope without type params");

// TODO: defer annotation resolution in stubs, with __future__.annotations, or stringified
self.infer_optional_expression(function.returns.as_deref());
self.infer_optional_annotation_expression(function.returns.as_deref());
self.infer_type_parameters(type_params);
self.infer_parameters(&function.parameters);
}
Expand Down Expand Up @@ -915,13 +914,11 @@ impl<'db> TypeInferenceBuilder<'db> {
// If there are type params, parameters and returns are evaluated in that scope, that is, in
// `infer_function_type_params`, rather than here.
if type_params.is_none() {
self.infer_parameters(parameters);

// TODO: this should also be applied to parameter annotations.
if self.are_all_types_deferred() {
self.types.has_deferred = true;
} else {
self.infer_optional_annotation_expression(returns.as_deref());
self.infer_parameters(parameters);
}
}

Expand Down Expand Up @@ -971,7 +968,7 @@ impl<'db> TypeInferenceBuilder<'db> {
default: _,
} = parameter_with_default;

self.infer_optional_expression(parameter.annotation.as_deref());
self.infer_optional_annotation_expression(parameter.annotation.as_deref());
}

fn infer_parameter(&mut self, parameter: &ast::Parameter) {
Expand All @@ -981,7 +978,7 @@ impl<'db> TypeInferenceBuilder<'db> {
annotation,
} = parameter;

self.infer_optional_expression(annotation.as_deref());
self.infer_optional_annotation_expression(annotation.as_deref());
}

fn infer_parameter_with_default_definition(
Expand Down Expand Up @@ -1069,6 +1066,7 @@ impl<'db> TypeInferenceBuilder<'db> {

fn infer_function_deferred(&mut self, function: &ast::StmtFunctionDef) {
self.infer_optional_annotation_expression(function.returns.as_deref());
self.infer_parameters(function.parameters.as_ref());
}

fn infer_class_deferred(&mut self, class: &ast::StmtClassDef) {
Expand Down Expand Up @@ -4099,15 +4097,17 @@ impl<'db> TypeInferenceBuilder<'db> {

match expression {
ast::Expr::Name(name) => match name.ctx {
ast::ExprContext::Load => self.infer_name_expression(name).to_instance(self.db),
ast::ExprContext::Load => {
self.infer_name_expression(name).in_type_expression(self.db)
}
ast::ExprContext::Invalid => Type::Unknown,
ast::ExprContext::Store | ast::ExprContext::Del => Type::Todo,
},

ast::Expr::Attribute(attribute_expression) => match attribute_expression.ctx {
ast::ExprContext::Load => self
.infer_attribute_expression(attribute_expression)
.to_instance(self.db),
.in_type_expression(self.db),
ast::ExprContext::Invalid => Type::Unknown,
ast::ExprContext::Store | ast::ExprContext::Del => Type::Todo,
},
Expand Down Expand Up @@ -5019,24 +5019,8 @@ mod tests {
",
)?;

// TODO: sys.version_info, and need to understand @final and @type_check_only
assert_public_ty(&db, "src/a.py", "x", "EllipsisType | Unknown");

Ok(())
}

#[test]
fn function_return_type() -> anyhow::Result<()> {
let mut db = setup_db();

db.write_file("src/a.py", "def example() -> int: return 42")?;

let mod_file = system_path_to_file(&db, "src/a.py").unwrap();
let function = global_symbol(&db, mod_file, "example")
.expect_type()
.expect_function_literal();
let returns = function.return_ty(&db);
assert_eq!(returns.display(&db).to_string(), "int");
// TODO: sys.version_info
assert_public_ty(&db, "src/a.py", "x", "EllipsisType | ellipsis");

Ok(())
}
Expand Down Expand Up @@ -5251,7 +5235,7 @@ mod tests {
fn deferred_annotations_regular_source_fails() -> anyhow::Result<()> {
let mut db = setup_db();

// In (regular) source files, deferred annotations are *not* resolved
// In (regular) source files, annotations are *not* deferred
// Also tests imports from `__future__` that are not annotations
db.write_dedented(
"/src/source.py",
Expand Down
Loading

0 comments on commit a48d779

Please sign in to comment.