[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

View file

@ -6308,64 +6308,54 @@ impl<'db> FunctionType<'db> {
db: &'db dyn Db,
function: FunctionType<'db>,
) -> Option<OverloadedFunction<'db>> {
// 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 = function.definition(db).scope(db);
let use_def = semantic_index(db, scope.file(db)).use_def_map(scope.file_scope_id(db));
let use_id = function
.body_scope(db)
.node(db)
.expect_function()
.name
.scoped_use_id(db, scope);
let mut current = function;
let mut overloads = vec![];
if let Symbol::Type(Type::FunctionLiteral(function_literal), Boundness::Bound) =
symbol_from_bindings(db, use_def.bindings_at_use(use_id))
{
match function_literal.to_overloaded(db) {
None => {
debug_assert!(
!function_literal.has_known_decorator(db, FunctionDecorators::OVERLOAD),
"Expected `Some(OverloadedFunction)` if the previous function was an overload"
);
}
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 {
OverloadedFunction {
overloads: overloads.clone(),
implementation: Some(function),
}
},
);
}
loop {
// 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)
.node(db)
.expect_function()
.name
.scoped_use_id(db, scope);
let Symbol::Type(Type::FunctionLiteral(previous), Boundness::Bound) =
symbol_from_bindings(db, use_def.bindings_at_use(use_id))
else {
break;
};
if previous.has_known_decorator(db, FunctionDecorators::OVERLOAD) {
overloads.push(previous);
} else {
break;
}
current = previous;
}
if function.has_known_decorator(db, FunctionDecorators::OVERLOAD) {
Some(OverloadedFunction {
overloads: vec![function],
implementation: None,
})
} else {
// Overloads are inserted in reverse order, from bottom to top.
overloads.reverse();
let implementation = if function.has_known_decorator(db, FunctionDecorators::OVERLOAD) {
overloads.push(function);
None
} else {
Some(function)
};
if overloads.is_empty() {
None
} else {
Some(OverloadedFunction {
overloads,
implementation,
})
}
}