diff --git a/crates/red_knot_python_semantic/resources/mdtest/expression/lambda.md b/crates/red_knot_python_semantic/resources/mdtest/expression/lambda.md new file mode 100644 index 00000000000000..eaf9d5f08d51ef --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/expression/lambda.md @@ -0,0 +1,100 @@ +# `lambda` expression + +## No parameters + +`lambda` expressions can be defined without any parameters. + +```py +reveal_type(lambda: 1) # revealed: () -> @Todo(lambda return type) + +# error: [unresolved-reference] +reveal_type(lambda: a) # revealed: () -> @Todo(lambda return type) +``` + +## With parameters + +Unlike parameters in function definition, the parameters in a `lambda` expression cannot be +annotated. + +```py +reveal_type(lambda a: a) # revealed: (a) -> @Todo(lambda return type) +reveal_type(lambda a, b: a + b) # revealed: (a, b) -> @Todo(lambda return type) +``` + +But, it can have default values: + +```py +reveal_type(lambda a=1: a) # revealed: (a=Literal[1]) -> @Todo(lambda return type) +reveal_type(lambda a, b=2: a) # revealed: (a, b=Literal[2]) -> @Todo(lambda return type) +``` + +And, positional-only parameters: + +```py +reveal_type(lambda a, b, /, c: c) # revealed: (a, b, /, c) -> @Todo(lambda return type) +``` + +And, keyword-only parameters: + +```py +reveal_type(lambda a, *, b=2, c: b) # revealed: (a, *, b=Literal[2], c) -> @Todo(lambda return type) +``` + +And, variadic parameter: + +```py +reveal_type(lambda *args: args) # revealed: (*args) -> @Todo(lambda return type) +``` + +And, keyword-varidic parameter: + +```py +reveal_type(lambda **kwargs: kwargs) # revealed: (**kwargs) -> @Todo(lambda return type) +``` + +Mixing all of them together: + +```py +# revealed: (a, b, /, c=Literal[True], *args, *, d=Literal["default"], e=Literal[5], **kwargs) -> @Todo(lambda return type) +reveal_type(lambda a, b, /, c=True, *args, d="default", e=5, **kwargs: None) +``` + +## Parameter type + +In addition to correctly inferring the `lambda` expression, the parameters should also be inferred +correctly. + +Using a parameter with no default value: + +```py +lambda x: reveal_type(x) # revealed: Unknown +``` + +Using a parameter with default value: + +```py +lambda x=1: reveal_type(x) # revealed: Unknown | Literal[1] +``` + +Using a variadic paramter: + +```py +# TODO: should be `tuple[Unknown, ...]` (needs generics) +lambda *args: reveal_type(args) # revealed: tuple +``` + +Using a keyword-varidic parameter: + +```py +# TODO: should be `dict[str, Unknown]` (needs generics) +lambda **kwargs: reveal_type(kwargs) # revealed: dict +``` + +## Nested `lambda` expressions + +Here, a `lambda` expression is used as the default value for a parameter in another `lambda` +expression. + +```py +reveal_type(lambda a=lambda x, y: 0: 2) # revealed: (a=(x, y) -> @Todo(lambda return type)) -> @Todo(lambda return type) +``` diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 33bd0b201e14d7..634a1ca62854db 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -119,7 +119,14 @@ fn infer_definition_types_cycle_recovery<'db>( _cycle: &salsa::Cycle, input: Definition<'db>, ) -> TypeInference<'db> { - tracing::trace!("infer_definition_types_cycle_recovery"); + let file = input.file(db); + let _span = tracing::trace_span!( + "infer_definition_types_cycle_recovery", + range = ?input.kind(db).target_range(), + file = %file.path(db) + ) + .entered(); + TypeInference::cycle_fallback(input.scope(db), todo_type!("cycle recovery")) } @@ -317,7 +324,7 @@ impl<'db> TypeInference<'db> { #[track_caller] pub(crate) fn expression_type(&self, expression: ScopedExpressionId) -> Type<'db> { self.try_expression_type(expression).expect( - "expression should belong to this TypeInference region and + "expression should belong to this TypeInference region and \ TypeInferenceBuilder should have inferred a type for it", ) } @@ -1361,7 +1368,9 @@ impl<'db> TypeInferenceBuilder<'db> { /// /// The annotated type is implicitly wrapped in a homogeneous tuple. /// - /// See `infer_parameter_definition` doc comment for some relevant observations about scopes. + /// See [`infer_parameter_definition`] doc comment for some relevant observations about scopes. + /// + /// [`infer_parameter_definition`]: Self::infer_parameter_definition fn infer_variadic_positional_parameter_definition( &mut self, parameter: &ast::Parameter, @@ -1390,7 +1399,9 @@ impl<'db> TypeInferenceBuilder<'db> { /// /// The annotated type is implicitly wrapped in a string-keyed dictionary. /// - /// See `infer_parameter_definition` doc comment for some relevant observations about scopes. + /// See [`infer_parameter_definition`] doc comment for some relevant observations about scopes. + /// + /// [`infer_parameter_definition`]: Self::infer_parameter_definition fn infer_variadic_keyword_parameter_definition( &mut self, parameter: &ast::Parameter, @@ -3283,18 +3294,83 @@ impl<'db> TypeInferenceBuilder<'db> { body: _, } = lambda_expression; - if let Some(parameters) = parameters { - for default in parameters - .iter_non_variadic_params() - .filter_map(|param| param.default.as_deref()) - { - self.infer_expression(default); - } + let parameters = if let Some(parameters) = parameters { + let positional_only = parameters + .posonlyargs + .iter() + .map(|parameter| { + Parameter::new( + Some(parameter.name().id.clone()), + None, + ParameterKind::PositionalOnly { + default_ty: parameter + .default() + .map(|default| self.infer_expression(default)), + }, + ) + }) + .collect::>(); + let positional_or_keyword = parameters + .args + .iter() + .map(|parameter| { + Parameter::new( + Some(parameter.name().id.clone()), + None, + ParameterKind::PositionalOrKeyword { + default_ty: parameter + .default() + .map(|default| self.infer_expression(default)), + }, + ) + }) + .collect::>(); + let variadic = parameters.vararg.as_ref().map(|parameter| { + Parameter::new( + Some(parameter.name.id.clone()), + None, + ParameterKind::Variadic, + ) + }); + let keyword_only = parameters + .kwonlyargs + .iter() + .map(|parameter| { + Parameter::new( + Some(parameter.name().id.clone()), + None, + ParameterKind::KeywordOnly { + default_ty: parameter + .default() + .map(|default| self.infer_expression(default)), + }, + ) + }) + .collect::>(); + let keyword_variadic = parameters.kwarg.as_ref().map(|parameter| { + Parameter::new( + Some(parameter.name.id.clone()), + None, + ParameterKind::KeywordVariadic, + ) + }); - self.infer_parameters(parameters); - } + Parameters::new( + positional_only + .into_iter() + .chain(positional_or_keyword) + .chain(variadic) + .chain(keyword_only) + .chain(keyword_variadic), + ) + } else { + Parameters::empty() + }; - todo_type!("typing.Callable type") + Type::Callable(CallableType::General(GeneralCallableType::new( + self.db(), + Signature::new(parameters, Some(todo_type!("lambda return type"))), + ))) } fn infer_call_expression(&mut self, call_expression: &ast::ExprCall) -> Type<'db> { diff --git a/crates/red_knot_python_semantic/src/types/signatures.rs b/crates/red_knot_python_semantic/src/types/signatures.rs index 8468fd94099f0e..9acf3552f83890 100644 --- a/crates/red_knot_python_semantic/src/types/signatures.rs +++ b/crates/red_knot_python_semantic/src/types/signatures.rs @@ -94,6 +94,14 @@ impl<'db> Parameters<'db> { } } + /// Create an empty parameter list. + pub(crate) fn empty() -> Self { + Self { + value: Vec::new(), + is_gradual: false, + } + } + pub(crate) fn as_slice(&self) -> &[Parameter<'db>] { self.value.as_slice() }