Skip to content
Closed
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
8 changes: 4 additions & 4 deletions crates/ty_python_semantic/resources/mdtest/cycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,16 +129,16 @@ class C:
self.c = lambda positional_only=self.c, /: positional_only
self.d = lambda *, kw_only=self.d: kw_only

# revealed: (positional=...) -> Unknown
# revealed: (positional=...) -> Unknown | ((positional=...) -> Unknown) | ((positional=...) -> Divergent) | ((positional=...) -> Divergent)
reveal_type(self.a)

# revealed: (*, kw_only=...) -> Unknown
# revealed: (*, kw_only=...) -> Unknown | ((*, kw_only=...) -> Unknown) | ((*, kw_only=...) -> Divergent) | ((*, kw_only=...) -> Divergent)
reveal_type(self.b)

# revealed: (positional_only=..., /) -> Unknown
# revealed: (positional_only=..., /) -> Unknown | ((positional_only=..., /) -> Unknown) | ((positional_only=..., /) -> Divergent) | ((positional_only=..., /) -> Divergent)
reveal_type(self.c)

# revealed: (*, kw_only=...) -> Unknown
# revealed: (*, kw_only=...) -> Unknown | ((*, kw_only=...) -> Unknown) | ((*, kw_only=...) -> Divergent) | ((*, kw_only=...) -> Divergent)
reveal_type(self.d)
```

Expand Down
47 changes: 40 additions & 7 deletions crates/ty_python_semantic/resources/mdtest/expression/lambda.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
`lambda` expressions can be defined without any parameters.

```py
reveal_type(lambda: 1) # revealed: () -> Unknown
reveal_type(lambda: 1) # revealed: () -> Literal[1]

# error: [unresolved-reference]
reveal_type(lambda: a) # revealed: () -> Unknown
Expand All @@ -24,7 +24,7 @@ reveal_type(lambda a, b: a + b) # revealed: (a, b) -> Unknown
But, it can have default values:

```py
reveal_type(lambda a=1: a) # revealed: (a=1) -> Unknown
reveal_type(lambda a=1: a) # revealed: (a=1) -> Unknown | Literal[1]
reveal_type(lambda a, b=2: a) # revealed: (a, b=2) -> Unknown
```

Expand All @@ -37,25 +37,25 @@ reveal_type(lambda a, b, /, c: c) # revealed: (a, b, /, c) -> Unknown
And, keyword-only parameters:

```py
reveal_type(lambda a, *, b=2, c: b) # revealed: (a, *, b=2, c) -> Unknown
reveal_type(lambda a, *, b=2, c: b) # revealed: (a, *, b=2, c) -> Unknown | Literal[2]
```

And, variadic parameter:

```py
reveal_type(lambda *args: args) # revealed: (*args) -> Unknown
reveal_type(lambda *args: args) # revealed: (*args) -> tuple[Unknown, ...]
```

And, keyword-varidic parameter:

```py
reveal_type(lambda **kwargs: kwargs) # revealed: (**kwargs) -> Unknown
reveal_type(lambda **kwargs: kwargs) # revealed: (**kwargs) -> dict[str, Unknown]
```

Mixing all of them together:

```py
# revealed: (a, b, /, c=True, *args, *, d="default", e=5, **kwargs) -> Unknown
# revealed: (a, b, /, c=True, *args, *, d="default", e=5, **kwargs) -> None
reveal_type(lambda a, b, /, c=True, *args, d="default", e=5, **kwargs: None)
```

Expand Down Expand Up @@ -94,7 +94,40 @@ Here, a `lambda` expression is used as the default value for a parameter in anot
expression.

```py
reveal_type(lambda a=lambda x, y: 0: 2) # revealed: (a=...) -> Unknown
reveal_type(lambda a=lambda x, y: 0: 2) # revealed: (a=...) -> Literal[2]
```

## Return type inference

The return type of a lambda is inferred from the type of its body expression.

```py
reveal_type(lambda: 1) # revealed: () -> Literal[1]
reveal_type(lambda: "hello") # revealed: () -> Literal["hello"]
reveal_type(lambda: True) # revealed: () -> Literal[True]
reveal_type(lambda: None) # revealed: () -> None
reveal_type(lambda: (1, "a")) # revealed: () -> tuple[Literal[1], Literal["a"]]
```

When the body uses parameters with unknown types, the return type may be `Unknown`:

```py
reveal_type(lambda x: x) # revealed: (x) -> Unknown
reveal_type(lambda x, y: x + y) # revealed: (x, y) -> Unknown
```

When the body is a constant expression unrelated to parameters, the return type is inferred:

```py
reveal_type(lambda x: 42) # revealed: (x) -> Literal[42]
reveal_type(lambda x, y: "result") # revealed: (x, y) -> Literal["result"]
reveal_type(lambda x: None) # revealed: (x) -> None
```

Nested lambda return types are also inferred:

```py
reveal_type(lambda: lambda: 1) # revealed: () -> () -> Literal[1]
```

## Assignment
Expand Down
12 changes: 9 additions & 3 deletions crates/ty_python_semantic/src/place.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ use crate::semantic_index::{
};
use crate::semantic_index::{DeclarationWithConstraint, global_scope, use_def_map};
use crate::types::{
ApplyTypeMappingVisitor, DynamicType, KnownClass, MaterializationKind, MemberLookupPolicy,
Truthiness, Type, TypeAndQualifiers, TypeQualifiers, UnionBuilder, UnionType, binding_type,
declaration_type,
ApplyTypeMappingVisitor, DynamicType, KnownClass, MAX_CYCLE_RECOVERY_ITERATIONS,
MaterializationKind, MemberLookupPolicy, Truthiness, Type, TypeAndQualifiers, TypeQualifiers,
UnionBuilder, UnionType, binding_type, declaration_type,
};
use crate::{Db, FxOrderSet, Program};

Expand Down Expand Up @@ -810,6 +810,12 @@ impl<'db> PlaceAndQualifiers<'db> {
previous_place: Self,
cycle: &salsa::Cycle,
) -> Self {
// Force convergence when the cycle has been iterating for too many rounds.
// See [`MAX_CYCLE_RECOVERY_ITERATIONS`] for details.
if cycle.iteration() >= MAX_CYCLE_RECOVERY_ITERATIONS {
return previous_place;
}

let place = match (previous_place.place, self.place) {
// In fixed-point iteration of type inference, the member type must be monotonically widened and not "oscillate".
// Here, monotonicity is guaranteed by pre-unioning the type of the previous iteration into the current result.
Expand Down
12 changes: 12 additions & 0 deletions crates/ty_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,14 @@ mod definition;
mod property_tests;
mod subscript;

/// The maximum number of Salsa fixpoint iterations before `cycle_normalized` forces convergence
/// by returning the previous value unchanged. Salsa itself panics at 200 iterations;
/// this limit ensures we stop growing union types well before that point.
/// In practice, well-behaved cycles converge within ~10 iterations. Cycles that haven't
/// converged after this many iterations involve pathological code (e.g. classes with circular
/// decorators, bases, and type parameters) and are unlikely to ever converge.
pub(crate) const MAX_CYCLE_RECOVERY_ITERATIONS: u32 = 100;

pub fn check_types(db: &dyn Db, file: File) -> Vec<Diagnostic> {
let _span = tracing::trace_span!("check_types", ?file).entered();
tracing::debug!("Checking file '{path}'", path = file.path(db));
Expand Down Expand Up @@ -909,6 +917,10 @@ impl<'db> Type<'db> {
previous: Self,
cycle: &salsa::Cycle,
) -> Self {
if cycle.iteration() >= MAX_CYCLE_RECOVERY_ITERATIONS {
return previous;
}

// When we encounter a salsa cycle, we want to avoid oscillating between two or more types
// without converging on a fixed-point result. Most of the time, we union together the
// types from each cycle iteration to ensure that our result is monotonic, even if we
Expand Down
19 changes: 15 additions & 4 deletions crates/ty_python_semantic/src/types/infer/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12110,10 +12110,21 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {

self.deferred_state = previous_deferred_state;

// TODO: Useful inference of a lambda's return type will require a different approach,
// which does the inference of the body expression based on arguments at each call site,
// rather than eagerly computing a return type without knowing the argument types.
Type::function_like_callable(self.db(), Signature::new(parameters, Type::unknown()))
// Infer the return type from the lambda body expression. This eagerly computes
// a return type without knowing the argument types; more precise inference would
// require deferring to each call site, similar to how overloaded functions work.
let return_ty = if let Some(scope_id) = self
.index
.try_node_scope(NodeWithScopeRef::Lambda(lambda_expression))
{
let scope = scope_id.to_scope_id(self.db(), self.file());
let inference = infer_scope_types(self.db(), scope, TypeContext::default());
inference.expression_type(&*lambda_expression.body)
} else {
Type::unknown()
};

Type::function_like_callable(self.db(), Signature::new(parameters, return_ty))
}

fn infer_call_expression(
Expand Down
13 changes: 12 additions & 1 deletion crates/ty_python_semantic/src/types/relation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,18 @@ impl<'db> Type<'db> {
///
/// See [`TypeRelation::Redundancy`] for more details.
pub(super) fn is_redundant_with(self, db: &'db dyn Db, other: Type<'db>) -> bool {
#[salsa::tracked(cycle_initial=|_, _, _, _| true, heap_size=ruff_memory_usage::heap_size)]
#[salsa::tracked(
cycle_initial=|_, _, _, _| false,
cycle_fn=|_, _, previous: &bool, current: bool, _, _| {
if *previous == current {
current
} else {
// The cycle is not converging; conservatively assume not redundant.
false
}
},
heap_size=ruff_memory_usage::heap_size,
)]
fn is_redundant_with_impl<'db>(
db: &'db dyn Db,
self_ty: Type<'db>,
Expand Down