-
Notifications
You must be signed in to change notification settings - Fork 1.6k
[ty] Avoid unnecessary argument type expansion #19999
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
89f1298
d52ac51
e89ae92
2e00f75
5345af0
1c99150
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,6 +16,7 @@ use crate::Program; | |
use crate::db::Db; | ||
use crate::dunder_all::dunder_all_names; | ||
use crate::place::{Boundness, Place}; | ||
use crate::types::call::arguments::is_expandable_type; | ||
use crate::types::diagnostic::{ | ||
CALL_NON_CALLABLE, CONFLICTING_ARGUMENT_FORMS, INVALID_ARGUMENT_TYPE, MISSING_ARGUMENT, | ||
NO_MATCHING_OVERLOAD, PARAMETER_ALREADY_ASSIGNED, TOO_MANY_POSITIONAL_ARGUMENTS, | ||
|
@@ -1327,6 +1328,48 @@ impl<'db> CallableBinding<'db> { | |
// for evaluating the expanded argument lists. | ||
snapshotter.restore(self, pre_evaluation_snapshot); | ||
|
||
// At this point, there's at least one argument that can be expanded. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it make sense to first explicitly check if there are any expandable arguments (using the new method you added), then do this check, then actually expand the arguments? This way even if the heuristic applies, we've already generated the expansion, just haven't used the expansions yet. But maybe generating the expansions is very cheap relative to actually trying the call with new argument types, so this doesn't matter? And I guess we'd slow down the fast path slightly if we first check all arguments for expandability, then separately expand them? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I don't think this would really matter in practice mainly because there are only a few differences between There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah right, so the main reason I did it at this location is so that the bindings state is the one before the type checking step, so that we only skip the overloads that have been filtered by the arity check. So, we'd either have to pay the cost of either
I think going with the latter sounds reasonable i.e., instead of always allocating the first expansion, we'd always allocate the bindings snapshot. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, but then there's complication on restoring the snapshots at the correct places. I think I'll leave this as is for now. |
||
// | ||
// This heuristic tries to detect if there's any need to perform argument type expansion or | ||
// not by checking whether there are any non-expandable argument type that cannot be | ||
// assigned to any of the remaining overloads. | ||
// | ||
// This heuristic needs to be applied after restoring the bindings state to the one before | ||
// type checking as argument type expansion would evaluate it from that point on. | ||
for (argument_index, (argument, argument_type)) in argument_types.iter().enumerate() { | ||
// TODO: Remove `Keywords` once `**kwargs` support is added | ||
if matches!(argument, Argument::Synthetic | Argument::Keywords) { | ||
continue; | ||
} | ||
let Some(argument_type) = argument_type else { | ||
continue; | ||
}; | ||
if is_expandable_type(db, argument_type) { | ||
continue; | ||
} | ||
let mut is_argument_assignable_to_any_overload = false; | ||
'overload: for (_, overload) in self.matching_overloads() { | ||
for parameter_index in &overload.argument_matches[argument_index].parameters { | ||
let parameter_type = overload.signature.parameters()[*parameter_index] | ||
.annotated_type() | ||
.unwrap_or(Type::unknown()); | ||
if argument_type.is_assignable_to(db, parameter_type) { | ||
is_argument_assignable_to_any_overload = true; | ||
break 'overload; | ||
} | ||
} | ||
} | ||
if !is_argument_assignable_to_any_overload { | ||
tracing::debug!( | ||
"Argument at {argument_index} (`{}`) is not assignable to any of the \ | ||
remaining overloads, skipping argument type expansion", | ||
argument_type.display(db) | ||
); | ||
snapshotter.restore(self, post_evaluation_snapshot); | ||
return; | ||
} | ||
} | ||
|
||
for expanded_argument_lists in expansions { | ||
// This is the merged state of the bindings after evaluating all of the expanded | ||
// argument lists. This will be the final state to restore the bindings to if all of | ||
|
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Considering the same example, but pass in
A()
as the first argument and usea=None
instead ofa=1
for the parameter above, this would still continue with the expansion. The reason being thatA()
matches parameter of at least one overload which means we cannot skip argument type expansion.I'm trying to think through what kind of heuristic to have that would avoid the expansion in this case as well. This would still lead to no-matching-overload because even though
Unknown
is assignable toint
, the spec says that all argument lists resulting from an expansion should evaluate successfully and there's no such expansion as expandingUnknown | None
leads to argument lists where at least one would haveNone
which isn't assignable toint
.Regardless, I think it'd be best to set a higher limit on the expansion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I think there will be cases where we can't avoid actually trying all the expansions and see if any of them work -- best we can do there is optimize the expansions as best we can (e.g. iterator instead of eagerly materialized?) and then set a reasonable limit.