This is an archive of the discontinued LLVM Phabricator instance.

[Concepts] Recover properly from a RecoveryExpr in a concept
ClosedPublic

Authored by erichkeane on Sep 23 2022, 8:11 AM.

Details

Summary

Discovered by reducing a different problem, we currently assert because
we failed to make the constraint expressions not dependent, since a
RecoveryExpr cannot be transformed.

This patch fixes that, and gets reasonably nice diagnostics by
introducing a concept (hah!) of "ContainsErrors" to the Satisfaction
types, which causes us to treat the candidate as non-viable.

However, just making THAT candidate non-viable would result in choosing
the 'next best' canddiate, which can result in awkward errors, where we
start evaluating a candidate that is not intended to be selected.
Because of this, and to make diagnostics more relevant, we now just
cause the entire lookup to result in a 'no-viable-candidates'.

This means we will only emit the list of candidates, rather than any
cascading failures.

Diff Detail

Event Timeline

erichkeane created this revision.Sep 23 2022, 8:11 AM
Herald added a project: Restricted Project. ยท View Herald TranscriptSep 23 2022, 8:11 AM
erichkeane requested review of this revision.Sep 23 2022, 8:11 AM
ychen added a subscriber: ychen.Sep 23 2022, 11:01 AM

The patch looks good. Two high-level questions:

  • Does the similar thing happen for class templates? Like a constraint expr of a partial specialization has an error. Maybe we don't assert in this case?
  • I suppose the constraint error does not always affect the overload resolution results. When it does not, an alternative would be to assume the constraint is a satisfaction failure and the compilation continues. Do you see any value in this approach? Personally, I could go either way. Basically a trade-off between pessimistic and optimistic.
clang/docs/ReleaseNotes.rst
213
clang/include/clang/Sema/Overload.h
931

How about ConstraintExprHasError? Cascading sounds like more details than useful.

clang/lib/Sema/SemaOverload.cpp
10123โ€“10127

nit: might be easier to read

erichkeane marked 3 inline comments as done.Sep 23 2022, 11:22 AM
erichkeane added a subscriber: tahonermann.

The patch looks good. Two high-level questions:

  • Does the similar thing happen for class templates? Like a constraint expr of a partial specialization has an error. Maybe we don't assert in this case?

WE currently crash in that case as well: https://godbolt.org/z/MGMqz1x59 . This patch still crashes in that case, and we should fix that in a similar way. I'll put it on my list of things to do soon! I don't want to do it in the same patch, simply because the type resolution parts are going to be completely different, and would likely just double the size of this patch.

  • I suppose the constraint error does not always affect the overload resolution results. When it does not, an alternative would be to assume the constraint is a satisfaction failure and the compilation continues. Do you see any value in this approach? Personally, I could go either way. Basically a trade-off between pessimistic and optimistic.

In cases where the constraint error does not affect overload resolution (like with short-circuiting), this patch makes no changes, and will continue without it. ONLY when a constraint that references a RecoveryExpr in some way is used will we 'quit' overload resolution.

I ALSO considered just marking as a failure, and continuing, but @tahonermann made a point in a private chat that the result is that we'll end up getting wacky follow-up errors. Consider something like:

template<typename T> concept HasFoo = /*Test for has foo*/;
   template<typename T> concept HasBarAlternative = /*test for has bar, but with a typo!*/;

   template<typename T> requires HasFoo<T>
   void do_thing(T &t) {
     t.Foo();
     t.Bar();
   }
   template<typename T> requires HasFoo<T> && HasBarAlternative<T>
   void do_thing(T&t) {
     t.Foo();
     t.BarAlternative();
   }

The result of just marking HasBarAlternative' as not satisfied, is the 1st do_thing` will be called. THEN you'd get an error on instantiating because of the lack of Bar. This seems like a worse behavior to me, and results in just nonsense-errors/not useful errors most of the time.

clang/include/clang/Sema/Overload.h
931

Yeah, my name is awful here... I'm not sure ConstraintExprHasError is correc tin this case (since this is an OverloadCandidate), so the question is "Does this candidate fail because this is a constraint that contains an error". I'll try to come up with something better.

Feel free to help bikeshed/workshop something better!

clang/lib/Sema/SemaOverload.cpp
10123โ€“10127

yep! I like it. I tend to do early exit when I can, particularly on large functions, but this ended up being pretty small in the final version of this patch.

erichkeane marked 2 inline comments as done.Sep 23 2022, 11:35 AM

The patch looks good. Two high-level questions:

  • Does the similar thing happen for class templates? Like a constraint expr of a partial specialization has an error. Maybe we don't assert in this case?

WE currently crash in that case as well: https://godbolt.org/z/MGMqz1x59 . This patch still crashes in that case, and we should fix that in a similar way. I'll put it on my list of things to do soon! I don't want to do it in the same patch, simply because the type resolution parts are going to be completely different, and would likely just double the size of this patch.

  • I suppose the constraint error does not always affect the overload resolution results. When it does not, an alternative would be to assume the constraint is a satisfaction failure and the compilation continues. Do you see any value in this approach? Personally, I could go either way. Basically a trade-off between pessimistic and optimistic.

In cases where the constraint error does not affect overload resolution (like with short-circuiting), this patch makes no changes, and will continue without it. ONLY when a constraint that references a RecoveryExpr in some way is used will we 'quit' overload resolution.

I ALSO considered just marking as a failure, and continuing, but @tahonermann made a point in a private chat that the result is that we'll end up getting wacky follow-up errors. Consider something like:

template<typename T> concept HasFoo = /*Test for has foo*/;
   template<typename T> concept HasBarAlternative = /*test for has bar, but with a typo!*/;

   template<typename T> requires HasFoo<T>
   void do_thing(T &t) {
     t.Foo();
     t.Bar();
   }
   template<typename T> requires HasFoo<T> && HasBarAlternative<T>
   void do_thing(T&t) {
     t.Foo();
     t.BarAlternative();
   }

The result of just marking HasBarAlternative' as not satisfied, is the 1st do_thing` will be called. THEN you'd get an error on instantiating because of the lack of Bar. This seems like a worse behavior to me, and results in just nonsense-errors/not useful errors most of the time.

WOOPS! I was wrong about 'still crashes' here. I mis-applied one of your suggestions and it crashed for another reason! When properly done, this stops that crash, and the Class-template-partial-specializations are just invalid at that point. I think we should still do the same thing as far as failing-lookup for the same reasons, so I'll do that separately.

Changes as suggested by @ychen. Thanks!

ychen added a comment.Sep 23 2022, 3:14 PM

The patch looks good. Two high-level questions:

  • Does the similar thing happen for class templates? Like a constraint expr of a partial specialization has an error. Maybe we don't assert in this case?

WE currently crash in that case as well: https://godbolt.org/z/MGMqz1x59 . This patch still crashes in that case, and we should fix that in a similar way. I'll put it on my list of things to do soon! I don't want to do it in the same patch, simply because the type resolution parts are going to be completely different, and would likely just double the size of this patch.

  • I suppose the constraint error does not always affect the overload resolution results. When it does not, an alternative would be to assume the constraint is a satisfaction failure and the compilation continues. Do you see any value in this approach? Personally, I could go either way. Basically a trade-off between pessimistic and optimistic.

In cases where the constraint error does not affect overload resolution (like with short-circuiting), this patch makes no changes, and will continue without it. ONLY when a constraint that references a RecoveryExpr in some way is used will we 'quit' overload resolution.
I ALSO considered just marking as a failure, and continuing, but @tahonermann made a point in a private chat that the result is that we'll end up getting wacky follow-up errors. Consider something like:

template<typename T> concept HasFoo = /*Test for has foo*/;
   template<typename T> concept HasBarAlternative = /*test for has bar, but with a typo!*/;

   template<typename T> requires HasFoo<T>
   void do_thing(T &t) {
     t.Foo();
     t.Bar();
   }
   template<typename T> requires HasFoo<T> && HasBarAlternative<T>
   void do_thing(T&t) {
     t.Foo();
     t.BarAlternative();
   }

The result of just marking HasBarAlternative' as not satisfied, is the 1st do_thing` will be called. THEN you'd get an error on instantiating because of the lack of Bar. This seems like a worse behavior to me, and results in just nonsense-errors/not useful errors most of the time.

Agreed that short-circuiting cases would not hit this issue. I actually meant cases where a supposedly failed constraint has errors (https://clang.godbolt.org/z/5P7fYYvao). But that could be reconsidered in the future if use cases arise. With this patch, we already handle this better than GCC/MSVC.

constexpr bool True = true;
constexpr bool False = b;
template <typename T> concept C = True;
template <typename T> concept D = False;

template<D T>
void foo(T) = delete;
template<C T>
void foo(T);

void g() {
    foo(1);
}
clang/include/clang/Sema/Overload.h
931

Thanks. This looks good to me.

ychen accepted this revision as: ychen.Sep 23 2022, 3:15 PM

LGTM.

This revision is now accepted and ready to land.Sep 23 2022, 3:15 PM

The patch looks good. Two high-level questions:

  • Does the similar thing happen for class templates? Like a constraint expr of a partial specialization has an error. Maybe we don't assert in this case?

WE currently crash in that case as well: https://godbolt.org/z/MGMqz1x59 . This patch still crashes in that case, and we should fix that in a similar way. I'll put it on my list of things to do soon! I don't want to do it in the same patch, simply because the type resolution parts are going to be completely different, and would likely just double the size of this patch.

  • I suppose the constraint error does not always affect the overload resolution results. When it does not, an alternative would be to assume the constraint is a satisfaction failure and the compilation continues. Do you see any value in this approach? Personally, I could go either way. Basically a trade-off between pessimistic and optimistic.

In cases where the constraint error does not affect overload resolution (like with short-circuiting), this patch makes no changes, and will continue without it. ONLY when a constraint that references a RecoveryExpr in some way is used will we 'quit' overload resolution.
I ALSO considered just marking as a failure, and continuing, but @tahonermann made a point in a private chat that the result is that we'll end up getting wacky follow-up errors. Consider something like:

template<typename T> concept HasFoo = /*Test for has foo*/;
   template<typename T> concept HasBarAlternative = /*test for has bar, but with a typo!*/;

   template<typename T> requires HasFoo<T>
   void do_thing(T &t) {
     t.Foo();
     t.Bar();
   }
   template<typename T> requires HasFoo<T> && HasBarAlternative<T>
   void do_thing(T&t) {
     t.Foo();
     t.BarAlternative();
   }

The result of just marking HasBarAlternative' as not satisfied, is the 1st do_thing` will be called. THEN you'd get an error on instantiating because of the lack of Bar. This seems like a worse behavior to me, and results in just nonsense-errors/not useful errors most of the time.

Agreed that short-circuiting cases would not hit this issue. I actually meant cases where a supposedly failed constraint has errors (https://clang.godbolt.org/z/5P7fYYvao). But that could be reconsidered in the future if use cases arise. With this patch, we already handle this better than GCC/MSVC.

constexpr bool True = true;
constexpr bool False = b;
template <typename T> concept C = True;
template <typename T> concept D = False;

template<D T>
void foo(T) = delete;
template<C T>
void foo(T);

void g() {
    foo(1);
}

Thanks for the quick review! Yeah, that case is a tough one, we don't really have a good way of determining whether that was supposed to fail, or be the best match. GCC does the option of just treating it as failed, so it'll have the problem that I mentioned above. No idea what MSVC is doing though.... thats frightening.

ychen added a comment.Sep 25 2022, 5:22 PM

Thanks for the quick review! Yeah, that case is a tough one, we don't really have a good way of determining whether that was supposed to fail, or be the best match. GCC does the option of just treating it as failed, so it'll have the problem that I mentioned above. No idea what MSVC is doing though.... thats frightening.

This paper (https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p2429r0.pdf) makes me like the current approach more. Basically, we better strive for intuitive diagnostics instead of diagnosing as many errors as possible but run the risk of lower-quality diagnostic messages.

Herald added a project: Restricted Project. ยท View Herald TranscriptSep 26 2022, 8:39 AM