[red-knot] Refactor: no mutability in call APIs (#17788)

## Summary

Remove mutability in parameter types for a few functions such as
`with_self` and `try_call`. I tried the `Rc`-approach with cheap cloning
[suggest
here](https://github.com/astral-sh/ruff/pull/17733#discussion_r2068722860)
first, but it turns out we need a whole stack of prepended arguments
(there can be [both `self` *and*
`cls`](3cf44e401a/crates/red_knot_python_semantic/resources/mdtest/call/constructor.md?plain=1#L113)),
and we would need the same construct not just for `CallArguments` but
also for `CallArgumentTypes`. At that point we're cloning `VecDeque`s
anyway, so the overhead of cloning the whole `VecDeque` with all
arguments didn't seem to justify the additional code complexity.

## Benchmarks

Benchmarks on tomllib, black, jinja, isort seem neutral.
This commit is contained in:
David Peter 2025-05-02 13:53:19 +02:00 committed by GitHub
parent 6d2c10cca2
commit ea3f4ac059
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 45 additions and 51 deletions

View File

@ -2649,10 +2649,7 @@ impl<'db> Type<'db> {
if let Symbol::Type(descr_get, descr_get_boundness) = descr_get {
let return_ty = descr_get
.try_call(
db,
&mut CallArgumentTypes::positional([self, instance, owner]),
)
.try_call(db, &CallArgumentTypes::positional([self, instance, owner]))
.map(|bindings| {
if descr_get_boundness == Boundness::Bound {
bindings.return_type(db)
@ -4207,7 +4204,7 @@ impl<'db> Type<'db> {
fn try_call(
self,
db: &'db dyn Db,
argument_types: &mut CallArgumentTypes<'_, 'db>,
argument_types: &CallArgumentTypes<'_, 'db>,
) -> Result<Bindings<'db>, CallError<'db>> {
let signatures = self.signatures(db);
Bindings::match_parameters(signatures, argument_types).check_types(db, argument_types)
@ -4420,7 +4417,7 @@ impl<'db> Type<'db> {
fn try_call_constructor(
self,
db: &'db dyn Db,
mut argument_types: CallArgumentTypes<'_, 'db>,
argument_types: CallArgumentTypes<'_, 'db>,
) -> Result<Type<'db>, ConstructorCallError<'db>> {
debug_assert!(matches!(
self,
@ -4486,7 +4483,7 @@ impl<'db> Type<'db> {
match new_method {
Symbol::Type(new_method, boundness) => {
let result = new_method.try_call(db, argument_types);
let result = new_method.try_call(db, &argument_types);
if boundness == Boundness::PossiblyUnbound {
return Some(Err(DunderNewCallError::PossiblyUnbound(result.err())));

View File

@ -11,16 +11,18 @@ impl<'a> CallArguments<'a> {
/// Invoke a function with an optional extra synthetic argument (for a `self` or `cls`
/// parameter) prepended to the front of this argument list. (If `bound_self` is none, the
/// function is invoked with the unmodified argument list.)
pub(crate) fn with_self<F, R>(&mut self, bound_self: Option<Type<'_>>, f: F) -> R
pub(crate) fn with_self<F, R>(&self, bound_self: Option<Type<'_>>, f: F) -> R
where
F: FnOnce(&mut Self) -> R,
F: FnOnce(&Self) -> R,
{
let mut call_arguments = self.clone();
if bound_self.is_some() {
self.0.push_front(Argument::Synthetic);
call_arguments.0.push_front(Argument::Synthetic);
}
let result = f(self);
let result = f(&call_arguments);
if bound_self.is_some() {
self.0.pop_front();
call_arguments.0.pop_front();
}
result
}
@ -55,6 +57,7 @@ pub(crate) enum Argument<'a> {
}
/// Arguments for a single call, in source order, along with inferred types for each argument.
#[derive(Clone, Debug)]
pub(crate) struct CallArgumentTypes<'a, 'db> {
arguments: CallArguments<'a>,
types: VecDeque<Type<'db>>,
@ -93,20 +96,20 @@ impl<'a, 'db> CallArgumentTypes<'a, 'db> {
/// Invoke a function with an optional extra synthetic argument (for a `self` or `cls`
/// parameter) prepended to the front of this argument list. (If `bound_self` is none, the
/// function is invoked with the unmodified argument list.)
pub(crate) fn with_self<F, R>(&mut self, bound_self: Option<Type<'db>>, f: F) -> R
pub(crate) fn with_self<F, R>(&self, bound_self: Option<Type<'db>>, f: F) -> R
where
F: FnOnce(&mut Self) -> R,
F: FnOnce(Self) -> R,
{
let mut call_argument_types = self.clone();
if let Some(bound_self) = bound_self {
self.arguments.0.push_front(Argument::Synthetic);
self.types.push_front(bound_self);
call_argument_types
.arguments
.0
.push_front(Argument::Synthetic);
call_argument_types.types.push_front(bound_self);
}
let result = f(self);
if bound_self.is_some() {
self.arguments.0.pop_front();
self.types.pop_front();
}
result
f(call_argument_types)
}
pub(crate) fn iter(&self) -> impl Iterator<Item = (Argument<'a>, Type<'db>)> + '_ {

View File

@ -56,7 +56,7 @@ impl<'db> Bindings<'db> {
/// verify that each argument type is assignable to the corresponding parameter type.
pub(crate) fn match_parameters(
signatures: Signatures<'db>,
arguments: &mut CallArguments<'_>,
arguments: &CallArguments<'_>,
) -> Self {
let mut argument_forms = vec![None; arguments.len()];
let mut conflicting_forms = vec![false; arguments.len()];
@ -92,7 +92,7 @@ impl<'db> Bindings<'db> {
pub(crate) fn check_types(
mut self,
db: &'db dyn Db,
argument_types: &mut CallArgumentTypes<'_, 'db>,
argument_types: &CallArgumentTypes<'_, 'db>,
) -> Result<Self, CallError<'db>> {
for (signature, element) in self.signatures.iter().zip(&mut self.elements) {
element.check_types(db, signature, argument_types);
@ -351,10 +351,7 @@ impl<'db> Bindings<'db> {
[Some(Type::PropertyInstance(property)), Some(instance), ..] => {
if let Some(getter) = property.getter(db) {
if let Ok(return_ty) = getter
.try_call(
db,
&mut CallArgumentTypes::positional([*instance]),
)
.try_call(db, &CallArgumentTypes::positional([*instance]))
.map(|binding| binding.return_type(db))
{
overload.set_return_type(return_ty);
@ -383,10 +380,7 @@ impl<'db> Bindings<'db> {
[Some(instance), ..] => {
if let Some(getter) = property.getter(db) {
if let Ok(return_ty) = getter
.try_call(
db,
&mut CallArgumentTypes::positional([*instance]),
)
.try_call(db, &CallArgumentTypes::positional([*instance]))
.map(|binding| binding.return_type(db))
{
overload.set_return_type(return_ty);
@ -414,7 +408,7 @@ impl<'db> Bindings<'db> {
if let Some(setter) = property.setter(db) {
if let Err(_call_error) = setter.try_call(
db,
&mut CallArgumentTypes::positional([*instance, *value]),
&CallArgumentTypes::positional([*instance, *value]),
) {
overload.errors.push(BindingError::InternalCallError(
"calling the setter failed",
@ -433,7 +427,7 @@ impl<'db> Bindings<'db> {
if let Some(setter) = property.setter(db) {
if let Err(_call_error) = setter.try_call(
db,
&mut CallArgumentTypes::positional([*instance, *value]),
&CallArgumentTypes::positional([*instance, *value]),
) {
overload.errors.push(BindingError::InternalCallError(
"calling the setter failed",
@ -874,7 +868,7 @@ pub(crate) struct CallableBinding<'db> {
impl<'db> CallableBinding<'db> {
fn match_parameters(
signature: &CallableSignature<'db>,
arguments: &mut CallArguments<'_>,
arguments: &CallArguments<'_>,
argument_forms: &mut [Option<ParameterForm>],
conflicting_forms: &mut [bool],
) -> Self {
@ -912,13 +906,13 @@ impl<'db> CallableBinding<'db> {
&mut self,
db: &'db dyn Db,
signature: &CallableSignature<'db>,
argument_types: &mut CallArgumentTypes<'_, 'db>,
argument_types: &CallArgumentTypes<'_, 'db>,
) {
// If this callable is a bound method, prepend the self instance onto the arguments list
// before checking.
argument_types.with_self(signature.bound_type, |argument_types| {
for (signature, overload) in signature.iter().zip(&mut self.overloads) {
overload.check_types(db, signature, argument_types);
overload.check_types(db, signature, &argument_types);
}
});
}

View File

@ -772,9 +772,9 @@ impl<'db> ClassLiteral<'db> {
let namespace = KnownClass::Dict.to_instance(db);
// TODO: Other keyword arguments?
let mut arguments = CallArgumentTypes::positional([name, bases, namespace]);
let arguments = CallArgumentTypes::positional([name, bases, namespace]);
let return_ty_result = match metaclass.try_call(db, &mut arguments) {
let return_ty_result = match metaclass.try_call(db, &arguments) {
Ok(bindings) => Ok(bindings.return_type(db)),
Err(CallError(CallErrorKind::NotCallable, bindings)) => Err(MetaclassError {

View File

@ -1815,7 +1815,7 @@ impl<'db> TypeInferenceBuilder<'db> {
for (decorator_ty, decorator_node) in decorator_types_and_nodes.iter().rev() {
inferred_ty = match decorator_ty
.try_call(self.db(), &mut CallArgumentTypes::positional([inferred_ty]))
.try_call(self.db(), &CallArgumentTypes::positional([inferred_ty]))
.map(|bindings| bindings.return_type(self.db()))
{
Ok(return_ty) => return_ty,
@ -2832,7 +2832,7 @@ impl<'db> TypeInferenceBuilder<'db> {
let successful_call = meta_dunder_set
.try_call(
db,
&mut CallArgumentTypes::positional([
&CallArgumentTypes::positional([
meta_attr_ty,
object_ty,
value_ty,
@ -2973,7 +2973,7 @@ impl<'db> TypeInferenceBuilder<'db> {
let successful_call = meta_dunder_set
.try_call(
db,
&mut CallArgumentTypes::positional([
&CallArgumentTypes::positional([
meta_attr_ty,
object_ty,
value_ty,
@ -4561,7 +4561,7 @@ impl<'db> TypeInferenceBuilder<'db> {
// We don't call `Type::try_call`, because we want to perform type inference on the
// arguments after matching them to parameters, but before checking that the argument types
// are assignable to any parameter annotations.
let mut call_arguments = Self::parse_arguments(arguments);
let call_arguments = Self::parse_arguments(arguments);
let callable_type = self.infer_expression(func);
if let Type::FunctionLiteral(function) = callable_type {
@ -4640,11 +4640,11 @@ impl<'db> TypeInferenceBuilder<'db> {
}
let signatures = callable_type.signatures(self.db());
let bindings = Bindings::match_parameters(signatures, &mut call_arguments);
let mut call_argument_types =
let bindings = Bindings::match_parameters(signatures, &call_arguments);
let call_argument_types =
self.infer_argument_types(arguments, call_arguments, &bindings.argument_forms);
match bindings.check_types(self.db(), &mut call_argument_types) {
match bindings.check_types(self.db(), &call_argument_types) {
Ok(mut bindings) => {
for binding in &mut bindings {
let binding_type = binding.callable_type;
@ -6486,7 +6486,7 @@ impl<'db> TypeInferenceBuilder<'db> {
Symbol::Type(contains_dunder, Boundness::Bound) => {
// If `__contains__` is available, it is used directly for the membership test.
contains_dunder
.try_call(db, &mut CallArgumentTypes::positional([right, left]))
.try_call(db, &CallArgumentTypes::positional([right, left]))
.map(|bindings| bindings.return_type(db))
.ok()
}
@ -6640,7 +6640,7 @@ impl<'db> TypeInferenceBuilder<'db> {
generic_context: GenericContext<'db>,
) -> Type<'db> {
let slice_node = subscript.slice.as_ref();
let mut call_argument_types = match slice_node {
let call_argument_types = match slice_node {
ast::Expr::Tuple(tuple) => CallArgumentTypes::positional(
tuple.elts.iter().map(|elt| self.infer_type_expression(elt)),
),
@ -6650,8 +6650,8 @@ impl<'db> TypeInferenceBuilder<'db> {
value_ty,
generic_context.signature(self.db()),
));
let bindings = match Bindings::match_parameters(signatures, &mut call_argument_types)
.check_types(self.db(), &mut call_argument_types)
let bindings = match Bindings::match_parameters(signatures, &call_argument_types)
.check_types(self.db(), &call_argument_types)
{
Ok(bindings) => bindings,
Err(CallError(_, bindings)) => {
@ -6893,7 +6893,7 @@ impl<'db> TypeInferenceBuilder<'db> {
match ty.try_call(
self.db(),
&mut CallArgumentTypes::positional([value_ty, slice_ty]),
&CallArgumentTypes::positional([value_ty, slice_ty]),
) {
Ok(bindings) => return bindings.return_type(self.db()),
Err(CallError(_, bindings)) => {