-
Notifications
You must be signed in to change notification settings - Fork 10.5k
[Sema] Diagnostic: improve diagnostics for ApplyExpr. #17093
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
Conversation
@xedin Want to take a look? |
// the first place, the resolved type may not provide the best diagnostic. | ||
// We consider the number of arguments to decide whether we'd go with it or | ||
// stay with the original one. | ||
if (fnExpr != callExpr->getFn()) { |
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.
Just counting arguments isn't necessarily going to work for default arguments and variadics.
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.
I agree with @CodaFi here, unfortunately it's not as simple as only matching number of parameters, it needs to account for defaults and variadics as well.
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.
As an alternative approach - just add all of the overloads from original function to existing calleeInfo
and filter via evaluateCloseness
with the rest?
@@ -19,19 +19,17 @@ func fg<T>(_ f: (T) -> T) -> Void {} // expected-note {{in call to function 'fg' | |||
fg({x in x}) // expected-error {{generic parameter 'T' could not be inferred}} | |||
|
|||
|
|||
// FIXME: Both f & g should complain about ambiguity of arg1, but in both cases the generic member | |||
// never gets into the list of candidates in the first place. | |||
struct S { |
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.
You can look into why we aren't picking up these callees by examining the overload set passed to the constructor for CalleeCandidateInfo.
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.
This is because the "fnExpr" passed to the constructor of CalleeCandidateInfo is redefined in this line, where the function subexpression is handled INDEPENDENTLY (i.e., without considering the actual argument information). In this case, the CS resolves an easy form of the function (e.g., the less formal parameters it has, the greater it is ranked). It may not be the best choice for diagnostics.
The motivation of this PR is to give it a chance to decide if we want to use the redefined "fnExpr" or the original one, taking into account the number of arguments.
After applying this PR, such FIXME would have been fixed, with the compiler raising better diagnostics.
Thanks for the review. I updated the code to handle parameters with default values and variadics, as you suggested. |
lib/Sema/CSDiag.cpp
Outdated
@@ -5599,6 +5599,60 @@ static bool shouldTypeCheckFunctionExpr(TypeChecker &TC, DeclContext *DC, | |||
return true; | |||
} | |||
|
|||
// Check if any of the candidates can accept a specified number of arguments, | |||
// regardless of parameter type or label information. | |||
static bool acceptNumArgs(const CalleeCandidateInfo &CCI, size_t numArgs) { |
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.
I think it would be great if logic that we already have in CSRanking could be extracted and re-used here as well, instead - https://github.com/apple/swift/blob/master/lib/Sema/CSRanking.cpp#L598-L701 it also handles trailing closures.
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.
Thanks for leading the way to the relevant code, Pavel! It is not straight forward to reuse, though.
The logic seems to be different. Let's say we have a candidate with variadic parameter foo(_: Int...)
, and it accepts the argument list (1, 2, 3)
. The acceptNumArgs
function here returns true, while the code snippet in CSRanking.cpp
would not due to this line.
In addition, the code snippet is a bit tightly coupled, with some variables defined in outer scope, and some control flow statements, like here and here).
The problem is that do we really have to handle trailing closure? If the the argument number mismatch happens in the closure, this PR does not handle it and compiler's behaviour remains unchanged. I mean, it does not make things worse.
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.
Sorry @dingobye I didn't mean that it has to be quiet literally the same, but I think it makes sense to extract common logic and leave irrelevant checks in their original places. I think the logic could be - given array of parameters can it match given array of arguments, maybe with additional callback to process each accepted argument/parameter pair further, this way it could be used by filter* functions in CalleeCandidateInfo as well.
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.
I am trying to extract common logic, but have some troubles. It seems to produce an opposite result for the following case:
Let's say we have params1 = (_: Int, _: Int)
and params2 = (_: Int...)
. The code in CSRank.cpp concludes params1
is NOT a subtype of params2
. Please check this line. However, I would expect the opposite, i.e., a given array of parameters params2
can match a given array of arguments with params1
's type (e.g., (0, 0)
). Because a variadic Int parameter should match a list of Int arguments.
I get confused here. Did I miss something?
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.
@dingobye I think it's correct behavior in this case because (according to comment) it tries to figure out if parameters from first function could be forwarded to the second, which in case of one having 2 parameters and another having a single variadic can't happen, same goes for the arguments with labels as well, they can't be forwarded to a single variadic parameter e.g.
func foo(_ params: Int...) {} // signature of parameters is (params: Int...)
foo(x: 0, y: 1) // fail - signature of arguments is (x: Int, y: Int)
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.
Hi Pavel, I updated the code as per your suggestions. Some logics are extracted, with variadic and defaulted parameters handled in general, and specific matching behaviours (e.g., type checking) are done via callback.
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.
Thank you, @dingobye! I'll try to take a look over the weekend.
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.
Thank you, Pavel! Please take your time.
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.
Do you think it makes sense to rename this function to something more verbose like isViableOverloadSet
?
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.
Good idea! I was not so happy with the name.
test/Constraints/diagnostics.swift
Outdated
@@ -376,7 +376,7 @@ CurriedClass.method3(c)(32, b: 1) | |||
_ = CurriedClass.method3(c) | |||
_ = CurriedClass.method3(c)(1, 2) // expected-error {{missing argument label 'b:' in call}} {{32-32=b: }} | |||
_ = CurriedClass.method3(c)(1, b: 2)(32) // expected-error {{cannot call value of non-function type '()'}} | |||
_ = CurriedClass.method3(1, 2) // expected-error {{instance member 'method3' cannot be used on type 'CurriedClass'; did you mean to use a value of this type instead?}} | |||
_ = CurriedClass.method3(1, 2) // expected-error {{missing argument label 'b:' in call}} |
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.
I'm not actually sure if this is better diagnostic - it's indeed pointing to missing b:
but the main problem here is that method3
can't be called on metatype like that...
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.
I agree with your, Pavel. The original error message makes more sense. I updated the code to properly handle the case of instance method as curried member on type. Now the PR does not change this file.
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.
I think new changes look much better! Thank you for all the work @dingobye! Sorry I've been traveling so I couldn't take a look sooner, I have left some comments and questions, but I think overall everything looks good, I just need to think some more about this. Going to run tests anyway to see if there is anything broken so you are aware.
lib/Sema/CSRanking.cpp
Outdated
} | ||
|
||
InputMatcher::Result InputMatcher::solve(int numInputs, | ||
bool ignoreLastPair, |
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.
It looks like ignoreLastPair
parameter is something specific to ranking it probably shouldn't be specially cased in the matcher itself, because it could be modeled outside by callee itself.
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.
Agree. I removed such ignoreLastPair
. In this case, the parameters should be properly prepared from outside by caller. If we intend to ignore trailing closure, we can slice params
by dropping the last element like this, before passing it to InputMatcher
's constructor.
As a side effect, however, computeDefaultMap
does not produce expected results if we pass the sliced params
to it. It means that we cannot directly compute defaultMap
inside InputMatcher
. As a result, defaultMap
should be prepared outside by caller as well. So I added the corresponding updates.
lib/Sema/CSRanking.cpp
Outdated
funcDecl->getDeclContext()->isTypeContext()); | ||
} | ||
|
||
InputMatcher::Result InputMatcher::solve(int numInputs, |
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.
Maybe it makes sense to rename this to match
instead of solve
?
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.
Yep!
lib/Sema/CSRanking.cpp
Outdated
|
||
InputMatcher::Result InputMatcher::solve(int numInputs, | ||
bool ignoreLastPair, | ||
std::function<bool(unsigned, unsigned)> pairMatcher) { |
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.
It would be great if instead of indices callback received references to AnyFunctionType::Parameter
, because index itself is inconsequential here.
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.
I was thinking the same, Pavel. However, I found it not easy to check for IUO without indices. I think indices is not a bad idea, since it allows pairMatcher
to take more control. What do you think?
lib/Sema/ConstraintSystem.h
Outdated
@@ -3487,6 +3487,52 @@ class OverloadSetCounter : public ASTWalker { | |||
return { true, expr }; | |||
} | |||
}; | |||
|
|||
|
|||
/// \brief Matching an array of input to an array of parameter. |
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.
Alternative phrasing - Matches array of function parameters to candidate arguments.
@@ -1,4 +1,4 @@ | |||
// RUN: %target-typecheck-verify-swift | |||
// RUN: %target-typecheck-verify-swift -swift-version 4 |
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.
Sorry I didn't actually comment on this right away but we can't really switch version to 4 here, we'd have to add additional test/Sema/diag_ambiguous_overloads_swift4.swift
and have it specify -swift-version 4
there.
More interesting question is why -swift-version 4
is even required here, which tests change?
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.
lib/Sema/CSDiag.cpp
Outdated
@@ -5599,6 +5599,60 @@ static bool shouldTypeCheckFunctionExpr(TypeChecker &TC, DeclContext *DC, | |||
return true; | |||
} | |||
|
|||
// Check if any of the candidates can accept a specified number of arguments, | |||
// regardless of parameter type or label information. | |||
static bool acceptNumArgs(const CalleeCandidateInfo &CCI, size_t numArgs) { |
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.
Do you think it makes sense to rename this function to something more verbose like isViableOverloadSet
?
@swift-ci please test |
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.
Thank you so much for your thorough review, Pavel! I updated the code as per your suggestions. swift-ci
completed successfully before this update.
lib/Sema/CSDiag.cpp
Outdated
@@ -5599,6 +5599,60 @@ static bool shouldTypeCheckFunctionExpr(TypeChecker &TC, DeclContext *DC, | |||
return true; | |||
} | |||
|
|||
// Check if any of the candidates can accept a specified number of arguments, | |||
// regardless of parameter type or label information. | |||
static bool acceptNumArgs(const CalleeCandidateInfo &CCI, size_t numArgs) { |
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.
Good idea! I was not so happy with the name.
lib/Sema/CSRanking.cpp
Outdated
funcDecl->getDeclContext()->isTypeContext()); | ||
} | ||
|
||
InputMatcher::Result InputMatcher::solve(int numInputs, |
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.
Yep!
lib/Sema/CSRanking.cpp
Outdated
} | ||
|
||
InputMatcher::Result InputMatcher::solve(int numInputs, | ||
bool ignoreLastPair, |
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.
Agree. I removed such ignoreLastPair
. In this case, the parameters should be properly prepared from outside by caller. If we intend to ignore trailing closure, we can slice params
by dropping the last element like this, before passing it to InputMatcher
's constructor.
As a side effect, however, computeDefaultMap
does not produce expected results if we pass the sliced params
to it. It means that we cannot directly compute defaultMap
inside InputMatcher
. As a result, defaultMap
should be prepared outside by caller as well. So I added the corresponding updates.
lib/Sema/CSRanking.cpp
Outdated
|
||
InputMatcher::Result InputMatcher::solve(int numInputs, | ||
bool ignoreLastPair, | ||
std::function<bool(unsigned, unsigned)> pairMatcher) { |
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.
I was thinking the same, Pavel. However, I found it not easy to check for IUO without indices. I think indices is not a bad idea, since it allows pairMatcher
to take more control. What do you think?
@@ -1,4 +1,4 @@ | |||
// RUN: %target-typecheck-verify-swift | |||
// RUN: %target-typecheck-verify-swift -swift-version 4 |
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.
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.
@dingobye Sorry it's taking this long, I'll try to take a look again soon, unfortunately I'm currently on vacation...
Regarding paramIsIUO
, as an idea, you could change constructor of the InputMatcher
to pass original function/subscript declaration, extract parameter list from it, and modify paramIsIUO
to accept ParamDecl
directly, how does that sound? It's seems unnecessary for that function to always go through parent function/subscript anyway. This way you don't have to pass indices around, the reason why I don't like that - is because the same information has to be kept in two different places.
I think this might be the last request I have, but I will take one more look at it.
Also could you do me a favor and run clang-format
on the modified files? You can do it paired with git as well.
Hi Pavel, thank you for the reply. I feel bad if this bothers you during your vocation. Actually, we can continue our discussion after you return to work. In the case of |
@xedin Hi Pavel, how are you? Sorry for having troubled you during your vocation period. I just had the code rebased to recent master. It would be great if you can help finish this patch. Hope now is an appropriate time to bring you back to this conversation. As per your latest comments, using indices as callback's parameters is not ideal. However, this callback function uses both What do you think? |
@dingobye Sorry, I'm back and will take a look ASAP. I think the topic you mentioned is the only outstanding thing which needs improvement, I'll see if I can come up with a good way to solve this, otherwise we can keep on using indices. |
lib/Sema/CalleeCandidateInfo.cpp
Outdated
CalleeCandidateInfo &CalleeCandidateInfo:: | ||
operator=(const CalleeCandidateInfo &CCI) { | ||
if (this != &CCI) { | ||
// If the reference member (i.e., CS) is identical, just copy the rest |
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.
I think the rest
could be changed to remaining
here.
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.
Fixed.
lib/Sema/ConstraintSystem.h
Outdated
}; | ||
|
||
InputMatcher(const ArrayRef<AnyFunctionType::Param> params, | ||
const llvm::SmallBitVector &defaultValueMap); |
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.
Ok, here is an idea - instead of accepting a list of params here, let's give InputMatcher
AbstractFunctionDecl
where ParamDecl
s could be extracted via getParameterList
(just like what paramisIUO
does today), and let's make it so matcher callback uses ParamDecl
everywhere, these decls should have all the information needed -label, type and flags. Let's try that, if it doesn't work out we'll go with current approach, but if it does that would be a very nice cleanup!
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.
I tried and it seems not easy to achieve this...
The list of params passed to InputMatcher
in CSRanking.cpp is from funcTy2 / openType2, which is not directly computed from decl2
and the computation is non-trivial (involving cs
intermediately). If we'd like to compute such list of params locally in InputMatcher
, the code for such computation would be bulky and some extra stuffs (e.g., cs
) should also be passed to InputMatcher
.
In addition, if the list of params is not prepared by the caller, we would have to pass additional information to InputMatcher
(like ignoreLastPair in a previous version of this patch), which you dislike as in your previous comment on 19th June.
It seems we do not have much choice here. Shall we just use indices?
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.
Alright, fair enough!
@swift-ci please test |
@dingobye One last thing, could you please run |
I updated code with git-clang-format applied. Thank you for everything, Pavel! I couldn't have gone this far without your continued help. |
No problem, thanks for all the work! |
@swift-ci please test |
Build failed |
I'm not sure what this |
Looks like that should be addressed by #17872 |
Build failed |
When diagnosing ApplyExpr, the existing implementation tries to resolve the function subexpression independently in the first place, without considering the argument information. As a result, such resolved function type may not produce the best diagnostic message. This patch adds condideration of the number of arguments to decide the better function subexpression for diagnostic purpose. Resolves: SR-7918, SR-7786, SR-7440, SR-7295, SR-5154.
instance method as curried member on type.
and reuse it to match parameters and arguments.
split test cases into different files for different swift versions.
Ugh... Thank you for letting me know such information, Pavel! Just had it rebased to include that patch. Would you fire a test again, please? |
@swift-ci please test |
Build failed |
Build failed |
@swift-ci please test source compatibility |
Ok source compatibility is still busted, let's wait a bit until it's back online, I just want to make sure that everything is green before merging since we touched ranking logic. |
@swift-ci please test source compatibility |
Sure, Pavel! I'd feel bad if this patch became a problem maker. |
@dingobye Could you please now resolve all of the fixed issues? :) |
Issues tickets are resolved :) |
Awesome! Thanks again for all the work! |
When diagnosing ApplyExpr, the existing implementation tries to resolve the function subexpression independently in the first place, without considering the argument information. As a result, such resolved function type may not produce the best diagnostic message.
This PR adds consideration of the number of arguments to decide the better function subexpression for diagnostic purpose.
It also improves a primary test case by proving better diagnostic to fix a FIXME.
Resolves SR-7918, SR-7786, SR-7440, SR-7295, SR-5154.