Skip to content

[ConstraintSystem] Diagnose missing conformance requirements via "fixes" #18484

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

Merged
merged 8 commits into from
Aug 4, 2018

Conversation

xedin
Copy link
Contributor

@xedin xedin commented Aug 3, 2018

If fixes are allowed let solver record missing protocol conformance
requirements and assume that conformsTo constraint is successfully
solved, this helps to diagnose such errors without involving
heavy-weight expression based diagnostics.

Resolves: rdar://problem/40537858

xedin added 5 commits August 2, 2018 16:40
If fixes are allowed let solver record missing protocol conformance
requirements and assume that `conformsTo` constraint is successfully
solved, this helps to diagnose such errors without involving
heavy-weight expression based diagnostics.

Resolves: rdar://problem/40537858
Any solution which doesn't require source changes should be considered
better than any other solution which does.

Otherwise solutions with fixes are always considered better than
solutions with unavailable overloads, which isn't correct especially
considering that different types of errors are now diagnosed via
forming solutions with fixes.
…pr is available

While trying to diagnose missing conformances don't attempt
to lookup parameter position if the parent apply expression
is not available, which could happen when fixes are produced
as part of the sub-expression diagnostics.
@xedin xedin requested review from rudkx and DougGregor August 3, 2018 05:02
Copy link
Contributor

@slavapestov slavapestov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of the diagnostic changes look like clear improvements, nice work! I have a few small suggestions and some ideas for future work.

}

var arr: [S] = []
_ = List(arr, id: \.id) // expected-error {{'List<[S], S.Id>' requires that 'S.Id' conform to 'Hashable'}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eventually, we should say something like

List<T, E> requires that 'E' conform to 'Hashable' (with 'E' = 'S.Id')

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can definitely add requirement information to the diagnostic message, since owner is known I can get generic signature and extract the original type from requirement.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. Including the name of the decl and it’s generic signature would make the diagnostic clearer, unless it becomes too long; but in that case we can split off some notes to spell out the substitutions, for example

@@ -75,18 +75,18 @@ let _ = Conforms<X>.Decl3.self
let _ = Conforms<X>.Decl4<X>.self
let _ = Conforms<X>.Decl5<X>.self

let _ = Conforms<Y>.TypeAlias1.self // expected-error {{type 'Y' does not conform to protocol 'P'}}
let _ = Conforms<Y>.TypeAlias2.self // expected-error {{type 'Y' does not conform to protocol 'P'}}
let _ = Conforms<Y>.TypeAlias1.self // expected-error {{'Conforms<Y>.TypeAlias1.Type' (aka 'Y.Type') requires that 'Y' conform to 'P'}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The '.Type' is just noise. It might be better to refer to the declaration here -- something like

typealias 'TypeAlias1' requires that 'Y'  conform to 'P'


func platypus<T>(a: [T]) {
_ = elephant(a) // expected-error {{generic parameter 'U' could not be inferred}}
_ = elephant(a) // expected-error {{'([T]) -> ()' requires that 'T' conform to 'Hashable'}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another case where we diagnose the type but not the decl -- shouldn't this say

function 'elephant' requires that 'T' conform to 'Hashable'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that definitely would be great! I left that for last, but I think it shouldn't be hard to do because anchor point to either DeclRefExpr or UnresolvedDot which should have a kind/name attach to it.

Just trying to get protocol compositions diagnose correctly first...

@@ -188,7 +188,7 @@ func r22459135() {

// <rdar://problem/19710848> QoI: Friendlier error message for "[] as Set"
// <rdar://problem/22326930> QoI: "argument for generic parameter 'Element' could not be inferred" lacks context
_ = [] as Set // expected-error {{generic parameter 'Element' could not be inferred in cast to 'Set'}} expected-note {{explicitly specify the generic arguments to fix this issue}} {{14-14=<<#Element: Hashable#>>}}
_ = [] as Set // expected-error {{using 'Any' as a concrete type conforming to protocol 'Hashable' is not supported}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is more accurate now, awesome!

I know this is an existing diagnostic, but in a subsequent patch, do you mind changing it to not say "not supported". Instead, I would just say "protocol type 'Any' cannot conform to 'Hashable' because only concrete types can conform to protocols" or something like that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure! There are currently 2 existing diagnostics - one for objc and one for swift types, should I modify both or just the one related to swift types?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the objc one already diagnoses the correct cause - static or initializer requirements. The swift one just says “not supported” which suggests it might be supported, but we don’t know if we’re going to implement self conforming existentials yet. Instead of promising a behavior, the diagnostic should explain the language rule - only concrete types can satisfy protocol requirements.

@@ -214,7 +214,7 @@ func rangeOfIsBefore<R : IteratorProtocol>(_ range: R) where R.Element : IsBefor

func callRangeOfIsBefore(_ ia: [Int], da: [Double]) {
rangeOfIsBefore(ia.makeIterator())
rangeOfIsBefore(da.makeIterator()) // expected-error{{type 'Double' does not conform to protocol 'IsBefore'}}
rangeOfIsBefore(da.makeIterator()) // expected-error{{'(IndexingIterator<[Double]>) -> ()' requires that 'Double' conform to 'IsBefore'}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another case where the decl and not the type would be most useful

@@ -17,7 +17,7 @@ func fAOE(_ t: AnyObject) { }
func fT<T>(_ t: T) { }

func testPassExistential(_ p: P, op: OP, opp: OP & P, cp: CP, sp: SP, any: Any, ao: AnyObject) {
fP(p) // expected-error{{cannot invoke 'fP' with an argument list of type '(P)'}} // expected-note{{expected an argument list of type '(T)'}}
fP(p) // expected-error{{using 'P' as a concrete type conforming to protocol 'P' is not supported}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent!

return foor20792596(x) // expected-error {{generic parameter 'T' could not be inferred}}
return foor20792596(x)
// expected-error@-1 {{missing argument label 'x:' in call}}
// expected-error@-2 {{argument type 'T' does not conform to expected type 'r20792596P'}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1!

@@ -11,5 +11,4 @@ var a : TheBuiltinInt64

// Check that it really is Builtin.Int64.
var wrapped = Int64(a) // okay
var badWrapped = Int32(a) // expected-error{{cannot invoke initializer for type 'Int32' with an argument list of type '(TheBuiltinInt64)'}}
// expected-note @-1 {{overloads for 'Int32' exist with}}
var badWrapped = Int32(a) // expected-error{{'Int32' requires that 'TheBuiltinInt64' conform to 'BinaryInteger'}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be clearer if it said "initializer for 'Int32'"

@slavapestov
Copy link
Contributor

Note that I didn’t review the code in detail, just the diagnostic changes. @rudkx should do that. But I think this and the other change to diagnose argument label mismatches as fixes are the right approach overall. Do you think soon you’ll be able to start ripping out code from CSDiag?

@xedin
Copy link
Contributor Author

xedin commented Aug 3, 2018

Yes, I hope to start removing logic from CSDiag soon. For example, I want to get out-of-order and missing arguments diagnosed via fixes, which would give it parity with what we have in CSDiag and, in theory, would allow us to remove ArgumentMatcher and related logic. Once we get all of the requirement kinds diagnosed via fixes, we can remove some of the logic related to failing constraint diagnostics fallback. I also have some ideas how to diagnose contextual errors with arguments/result types but that would require more changes to constraint system.

Copy link
Member

@DougGregor DougGregor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are great improvements to the diagnostics! I added a few comments/questions, but none that should block merging.

@@ -8148,6 +8149,95 @@ bool ConstraintSystem::applySolutionFix(
fix.first.getArgumentLabels(*this),
isa<SubscriptExpr>(call->getFn()));
}

case FixKind::AddConformace: {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we extract this case into a different function? It's a ton of code

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking about doing that in the separate PR because I think we need to refactor applySolutionFixes into Solution itself, add diagnoseFix instead of applySolutionFix, and extract this logic into fixConformance, WDYT?

auto protocolType = conformance.second->getDeclaredType();

// Find `ApplyExpr` based on a function expression attached to it.
auto findApplyExpr = [](Expr *parent, Expr *fnExpr) -> ApplyExpr * {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little surprised that we have to go searching for the ApplyExpr and matching up the parameter. Doesn't the (pre-simplified) locator contain the ApplyExpr, as well as information about which parameter we're talking about? Or is that still hard to get to because of the matchTypes/function call mess?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, unfortunately that exactly the reason, currently generic requirements are opened from function expression, which means that it would have DeclRefExpr or UnresolvedDotExpr or OverloadedDeclRefExpr as its anchor and opened generic -> type requirement # as its path...

// If this is a static, initializer or operator call,
// let's not try to diagnose it here, but refer to expression
// diagnostics.
if (isa<BinaryExpr>(applyExpr) || isa<TypeExpr>(anchor))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have expected that these diagnostics would be better for operators (at least), but okay.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are some problems with them currently, where current way of listing overload choices is better IMO, otherwise it would just say something like Int does not conform to RangeReplaceableCollection :(

auto *argExpr = getArgumentAt(applyExpr, *atParameterPos);
TC.diagnose(argExpr->getLoc(),
diag::cannot_convert_argument_value_protocol, type,
protocolType);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh! Oh! In many cases, we can probably resolveapplyExpr->getFn() down to a declaration, in which case we can produce a note pointing to the declaration we are trying to call. Bonus points for finding the type variable bindings when that declaration is generic. It would be super amazing if we could do something like this:

note: in call to function 'foo(blah:)' [with T=Int, U=String]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed! I've mentioned that in reply to Slava's comment - I'm going to change the diagnostic to <decl-kind> <name> requires that 'X' conform to 'Y' [with T = X]

auto typeRequirement = path.back();
std::pair<Expr *, unsigned> reqLoc = {anchor, typeRequirement.getValue()};
MissingConformances[reqLoc] = {type.getPointer(), protocol};
// Let's strip all of the uncessary information from locator,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo "uncessary"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

// Let's strip all of the uncessary information from locator,
// diagnostics only care about anchor - to lookup type,
// and what was the requirement# which is not satisfied.
ConstraintLocatorBuilder requirement(getConstraintLocator(anchor));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, so this is what helps us be sure we can find the missing conformance information later. Makes sense.

@@ -1009,6 +1009,12 @@ class ConstraintSystem {
/// Argument labels fixed by the constraint solver.
SmallVector<std::vector<Identifier>, 4> FixedArgLabels;

/// Conformances which solver "fixed" to exist to help with
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo? "to exist to help", perhaps just "to help"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

Copy link
Contributor

@rudkx rudkx left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM - very nice improvement.

@xedin
Copy link
Contributor Author

xedin commented Aug 3, 2018

I'll split the rest of the changes into multiple separate PR refactoring and extraction of diagnostics could be done separately.

@xedin
Copy link
Contributor Author

xedin commented Aug 3, 2018

@swift-ci please test

@swiftlang swiftlang deleted a comment from swift-ci Aug 3, 2018
@xedin xedin merged commit 913f371 into swiftlang:master Aug 4, 2018
@DougGregor
Copy link
Member

Excellent, thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants