[red-knot] Use iterative approach to collect overloads (#17607)

## Summary

This PR updates the `to_overloaded` method to use an iterative approach
instead of a recursive one.

Refer to
https://github.com/astral-sh/ruff/pull/17585#discussion_r2056804587 for
context.

The main benefit here is that it avoids calling the `to_overloaded`
function in a recursive manner which is a salsa query. So, this is a bit
hand wavy but we should also see less memory used because the cache will
only contain a single entry which should be the entire overload chain.
Previously, the recursive approach would mean that each of the function
involved in an overload chain would have a cache entry. This reduce in
memory shouldn't be too much and I haven't looked at the actual data for
it.

## Test Plan

Existing test cases should pass.
This commit is contained in:
Dhruv Manilawala 2025-04-24 22:23:50 +05:30 committed by GitHub
parent 8d2c79276d
commit 9937064761
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 43 additions and 53 deletions

View File

@ -6308,64 +6308,54 @@ impl<'db> FunctionType<'db> {
db: &'db dyn Db, db: &'db dyn Db,
function: FunctionType<'db>, function: FunctionType<'db>,
) -> Option<OverloadedFunction<'db>> { ) -> Option<OverloadedFunction<'db>> {
// The semantic model records a use for each function on the name node. This is used here let mut current = function;
// to get the previous function definition with the same name. let mut overloads = vec![];
let scope = function.definition(db).scope(db);
let use_def = semantic_index(db, scope.file(db)).use_def_map(scope.file_scope_id(db)); loop {
let use_id = function // The semantic model records a use for each function on the name node. This is used
// here to get the previous function definition with the same name.
let scope = current.definition(db).scope(db);
let use_def =
semantic_index(db, scope.file(db)).use_def_map(scope.file_scope_id(db));
let use_id = current
.body_scope(db) .body_scope(db)
.node(db) .node(db)
.expect_function() .expect_function()
.name .name
.scoped_use_id(db, scope); .scoped_use_id(db, scope);
if let Symbol::Type(Type::FunctionLiteral(function_literal), Boundness::Bound) = let Symbol::Type(Type::FunctionLiteral(previous), Boundness::Bound) =
symbol_from_bindings(db, use_def.bindings_at_use(use_id)) symbol_from_bindings(db, use_def.bindings_at_use(use_id))
{ else {
match function_literal.to_overloaded(db) { break;
None => { };
debug_assert!(
!function_literal.has_known_decorator(db, FunctionDecorators::OVERLOAD), if previous.has_known_decorator(db, FunctionDecorators::OVERLOAD) {
"Expected `Some(OverloadedFunction)` if the previous function was an overload" overloads.push(previous);
);
}
Some(OverloadedFunction {
implementation: Some(_),
..
}) => {
// If the previous overloaded function already has an implementation, then this
// new signature completely replaces it.
}
Some(OverloadedFunction {
overloads,
implementation: None,
}) => {
return Some(
if function.has_known_decorator(db, FunctionDecorators::OVERLOAD) {
let mut overloads = overloads.clone();
overloads.push(function);
OverloadedFunction {
overloads,
implementation: None,
}
} else { } else {
OverloadedFunction { break;
overloads: overloads.clone(),
implementation: Some(function),
}
},
);
}
}
} }
if function.has_known_decorator(db, FunctionDecorators::OVERLOAD) { current = previous;
Some(OverloadedFunction { }
overloads: vec![function],
implementation: None, // Overloads are inserted in reverse order, from bottom to top.
}) overloads.reverse();
} else {
let implementation = if function.has_known_decorator(db, FunctionDecorators::OVERLOAD) {
overloads.push(function);
None None
} else {
Some(function)
};
if overloads.is_empty() {
None
} else {
Some(OverloadedFunction {
overloads,
implementation,
})
} }
} }