Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions crates/red_knot_python_semantic/resources/mdtest/expression/lambda.md
Original file line number Diff line number Diff line change
@@ -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)
```
104 changes: 90 additions & 14 deletions crates/red_knot_python_semantic/src/types/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Comment on lines +122 to +129
Copy link
Member Author

Choose a reason for hiding this comment

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

This is similar to infer_definition_types and it allowed me to quickly understand the origin of the cycle and what was causing it.

TypeInference::cycle_fallback(input.scope(db), todo_type!("cycle recovery"))
}

Expand Down Expand Up @@ -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",
)
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
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> {
Expand Down
8 changes: 8 additions & 0 deletions crates/red_knot_python_semantic/src/types/signatures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
Loading