diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index e8f3e7f739..a286cacd04 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -1907,9 +1907,6 @@ impl<'db> Type<'db> { match self.call_dunder(db, "__len__", &CallArguments::positional([*self])) { CallDunderOutcome::MethodNotAvailable => CallDunderLenOutcome::MethodNotAvailable, CallDunderOutcome::Call(outcome) => CallDunderLenOutcome::Call(outcome), - CallDunderOutcome::PossiblyUnbound(outcome) => { - CallDunderLenOutcome::PossiblyUnbound(outcome) - } } } @@ -1918,9 +1915,8 @@ impl<'db> Type<'db> { /// /// In the second case, the return type of `len()` in `typeshed` (`int`) /// is used as a fallback. + #[must_use] fn len(&self, db: &'db dyn Db) -> Option> { - let len = self.__len__(db).return_type(db)?; - fn non_negative_int_literal<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option> { match ty { Type::IntLiteral(value) => (value >= 0).then_some(ty), @@ -1936,11 +1932,12 @@ impl<'db> Type<'db> { } } + let len = self.__len__(db).return_type(db)?; + non_negative_int_literal(db, len) } /// Return the outcome of calling an object of this type. - #[must_use] fn call(self, db: &'db dyn Db, arguments: &CallArguments<'_, 'db>) -> CallOutcome<'db> { match self { Type::FunctionLiteral(function_type) => { @@ -2068,15 +2065,18 @@ impl<'db> Type<'db> { not_callable_ty: self, } } + // Turn "possibly unbound object of type `Literal['__call__']`" + // into "`X` not callable (possibly unbound `__call__` method)" + CallDunderOutcome::Call(CallOutcome::PossiblyUnboundDunderCall { + called_ty: _, + call_outcome, + }) => CallOutcome::PossiblyUnboundDunderCall { + called_ty: self, + call_outcome, + }, + CallDunderOutcome::Call(outcome) => outcome, - CallDunderOutcome::PossiblyUnbound(call_outcome) => { - // Turn "possibly unbound object of type `Literal['__call__']`" - // into "`X` not callable (possibly unbound `__call__` method)" - CallOutcome::PossiblyUnboundDunderCall { - called_ty: self, - call_outcome: Box::new(call_outcome), - } - } + CallDunderOutcome::MethodNotAvailable => { // Turn "`X.__call__` unbound" into "`X` not callable" CallOutcome::NotCallable { @@ -2111,7 +2111,6 @@ impl<'db> Type<'db> { /// `receiver_ty` must be `Type::Instance(_)` or `Type::ClassLiteral`. /// /// TODO: handle `super()` objects properly - #[must_use] fn call_bound( self, db: &'db dyn Db, @@ -2163,7 +2162,10 @@ impl<'db> Type<'db> { CallDunderOutcome::Call(callable_ty.call(db, arguments)) } Symbol::Type(callable_ty, Boundness::PossiblyUnbound) => { - CallDunderOutcome::PossiblyUnbound(callable_ty.call(db, arguments)) + CallDunderOutcome::Call(CallOutcome::PossiblyUnboundDunderCall { + call_outcome: Box::new(callable_ty.call(db, arguments)), + called_ty: callable_ty, + }) } Symbol::Unbound => CallDunderOutcome::MethodNotAvailable, } @@ -2187,8 +2189,7 @@ impl<'db> Type<'db> { let dunder_iter_result = self.call_dunder(db, "__iter__", &CallArguments::positional([self])); match dunder_iter_result { - CallDunderOutcome::Call(ref call_outcome) - | CallDunderOutcome::PossiblyUnbound(ref call_outcome) => { + CallDunderOutcome::Call(ref call_outcome) => { let Some(iterator_ty) = call_outcome.return_type(db) else { return IterationOutcome::NotIterable { not_iterable_ty: self, @@ -2199,7 +2200,7 @@ impl<'db> Type<'db> { .call_dunder(db, "__next__", &CallArguments::positional([iterator_ty])) .return_type(db) { - if matches!(dunder_iter_result, CallDunderOutcome::PossiblyUnbound(..)) { + if call_outcome.is_possibly_unbound() { IterationOutcome::PossiblyUnboundDunderIter { iterable_ty: self, element_ty, diff --git a/crates/red_knot_python_semantic/src/types/call.rs b/crates/red_knot_python_semantic/src/types/call.rs index 59f03f4044..ca535f2c0f 100644 --- a/crates/red_knot_python_semantic/src/types/call.rs +++ b/crates/red_knot_python_semantic/src/types/call.rs @@ -10,6 +10,7 @@ mod bind; pub(super) use arguments::{Argument, CallArguments}; pub(super) use bind::{bind_call, CallBinding}; +#[must_use] #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum CallOutcome<'db> { Callable { @@ -50,6 +51,10 @@ impl<'db> CallOutcome<'db> { } } + pub(super) fn is_possibly_unbound(&self) -> bool { + matches!(self, Self::PossiblyUnboundDunderCall { .. }) + } + /// Get the return type of the call, or `None` if not callable. pub(super) fn return_type(&self, db: &'db dyn Db) -> Option> { match self { @@ -215,24 +220,23 @@ impl<'db> CallOutcome<'db> { } } +#[must_use] +#[derive(Debug)] pub(super) enum CallDunderOutcome<'db> { Call(CallOutcome<'db>), - PossiblyUnbound(CallOutcome<'db>), MethodNotAvailable, } impl<'db> CallDunderOutcome<'db> { - pub(super) fn call_outcome(&self) -> Option<&CallOutcome<'db>> { - match self { - Self::Call(outcome) | Self::PossiblyUnbound(outcome) => Some(outcome), - Self::MethodNotAvailable => None, - } - } - pub(super) fn return_type(&self, db: &'db dyn Db) -> Option> { match self { - Self::Call(outcome) => outcome.return_type(db), - Self::PossiblyUnbound { .. } => None, + Self::Call(outcome) => { + if outcome.is_possibly_unbound() { + None + } else { + outcome.return_type(db) + } + } Self::MethodNotAvailable => None, } } @@ -293,6 +297,7 @@ impl<'db> NotCallableError<'db> { } } +#[must_use] #[derive(Debug)] pub(super) enum CallDunderLenOutcome<'db> { /// The length is statically known. @@ -301,9 +306,6 @@ pub(super) enum CallDunderLenOutcome<'db> { /// The length is determined by calling `__len__`. Call(CallOutcome<'db>), - /// The length is determined by calling `__len__` but it isn't always bound. - PossiblyUnbound(CallOutcome<'db>), - /// The object doesn't have a `__len__` method and, thus, doesn't implement sized. MethodNotAvailable, } @@ -315,8 +317,7 @@ impl<'db> CallDunderLenOutcome<'db> { // TODO: Fall back to `int` if value is too large? i64::try_from(*len).ok().map(Type::IntLiteral) } - CallDunderLenOutcome::Call(call_outcome) - | CallDunderLenOutcome::PossiblyUnbound(call_outcome) => call_outcome.return_type(db), + CallDunderLenOutcome::Call(call_outcome) => call_outcome.return_type(db), CallDunderLenOutcome::MethodNotAvailable => None, } } diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index e8c507e612..95088111b5 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -50,7 +50,9 @@ use crate::semantic_index::semantic_index; use crate::semantic_index::symbol::{NodeWithScopeKind, NodeWithScopeRef, ScopeId}; use crate::semantic_index::SemanticIndex; use crate::stdlib::builtins_module_scope; -use crate::types::call::{Argument, CallArguments, CallOutcome}; +use crate::types::call::{ + Argument, CallArguments, CallDunderLenOutcome, CallDunderOutcome, CallOutcome, +}; use crate::types::diagnostic::{ report_invalid_arguments_to_annotated, report_invalid_assignment, report_invalid_attribute_assignment, report_unresolved_module, TypeCheckDiagnostics, @@ -3346,7 +3348,13 @@ impl<'db> TypeInferenceBuilder<'db> { } KnownFunction::Len => { - // TODO: Assert that the argument implements the `Sized` protocol (correctly). + let Some(sized) = binding.one_parameter_type() else { + return call; + }; + + if let CallDunderLenOutcome::Call(_) = sized.__len__(self.db()) { + // TODO: Assert that the argument implements the `Sized` protocol (correctly). + } } _ => {} } @@ -3671,14 +3679,11 @@ impl<'db> TypeInferenceBuilder<'db> { } }; - if let Some(call) = operand_type - .call_dunder( - self.db(), - unary_dunder_method, - &CallArguments::positional([operand_type]), - ) - .call_outcome() - { + if let CallDunderOutcome::Call(call) = operand_type.call_dunder( + self.db(), + unary_dunder_method, + &CallArguments::positional([operand_type]), + ) { match call.return_type_result(&self.context, AnyNodeRef::ExprUnaryOp(unary)) { Ok(t) => t, Err(e) => {