This is an archive of the discontinued LLVM Phabricator instance.

Fix PR18792 - Bad diagnostic messages for std::enable_if_t uses
Needs ReviewPublic

Authored by stephant on Mar 12 2014, 11:14 AM.

Details

Reviewers
rsmith
Summary

Fix PR18792 - Bad diagnostic messages for std::enable_if_t uses

This patch improves the diagnostic messages for overloads disabled with
enable_if, in particular those where the failing typename enable_if<...>::type
lookup happens during the instantiation of an alias template like
std::enable_if_t.

Previously the diagnostic note for an overload disabled with std::enable_if_t
(or a similar alias template) only contained the location of the typename
enable_if<...>::type lookup and did not contain the location of the disabled
function overload, which could be quite annoying when you had many overloads.

This patch makes clang generate a note with the name and location of the
outermost alias template in the SFINAE context in which the enable_if<...>::type
lookup failed. Typically this location will be inside the source range for the
function template declaration, i.e. exactly where we want it. In the rare
circumstances where this is not the case, see e.g. the new test case with the
FIXME in overload-candidates.cpp, the implementation makes sure that the
diagnostic is placed at the disabled overload.

I'm not certain whether using the "disabled by %0" message even for template
aliases whose names don't start with "enable" or "disable" is optimal, but I'm
pretty sure that in the vast majority of cases the generated message should
be quite understandable and certainly better than the one generated when
not using the enable_if logic. Though, if someone has a good idea for the
wording, it would be easy to use a different message for such aliases.

This patch also improves the source range highlighting for regular enable_if
uses and always prints out the fully qualified name of the enable_if template
(as was requested by a FIXME in SemaOverload.cpp).

Diff Detail

Event Timeline

rsmith added inline comments.Mar 25 2014, 11:17 AM
include/clang/Basic/SourceLocation.h
217–218

Rather than essentially duplicating the implementation in the body, how about an English description, "Indicates whether another source range is the same as, or contained within, this range."

Also, the implementation doesn't appear to be correct; < on SourceLocations does not in general give source order (though it happens to if the locations are within the same FileID). Use SourceManager::isBeforeInTranslationUnit for this check (and it shouldn't be a member of SourceRange to avoid layering problems).

Maybe this should be a SourceManager member function?

include/clang/Sema/Sema.h
6055–6056

This seems a bit arbitrary. Could you add a TemplateArgumentListInfo * instead? (Maybe make TemplateArgs above a PointerUnion?)

lib/Sema/SemaOverload.cpp
8905–8918

I don't like inspecting the source ranges on a PartialDiagnostic -- they're not part of the numbered argument set for the diagnostic, so someone emitting the diagnostic is within their rights to change the set of ranges they provide. More generally, I'm not really a fan of digging into the implementation of the PartialDiagnostic to find its arguments.

Instead, how about switching the diagnostic ID from err_typename_nested_not_found_enable_if to note_ovl_candidate_disabled_by_enable_if (with comments in the .td file saying that they're required to take the same arguments)?

Instead of using the Range, below, to determine the location for the diagnostic, you could use something like TemplDecl->getSourceRange().contains(PDiag->first) ? PDiag->first : Templated->getLocation().

lib/Sema/SemaTemplate.cpp
7887–7919

Having reflected on this a bit more, I think we should *only* do this if the context surrounding the enable_if is an alias template named enable_if_t (and we should only walk through one such alias template). If we walk too far away from the enable_if expression itself, it will no longer be obvious what we're complaining about.

7918

Please also change the literal 'enable_if' in err_typename_nested_not_found_enable_if to a %1.

Thanks for the review!

include/clang/Basic/SourceLocation.h
217–218

I used the short technical definition in the description because I wasn't sure how to describe the behaviour verbally in a terse and accurate way.

I didn't use isBeforeInTranslationUnit for the implementation because I wanted to test whether one source range is visually contained by the other in the original source file. However, I just tested the behaviour for macro expansions and noticed that clang does print both the spelling and the expansion location for macros, so using isBeforeInTranslationUnit indeed seems preferable here.

Do you maybe have a suggestion for the name of the new method in SourceManager? Maybe rangeContains?

include/clang/Sema/Sema.h
6055–6056

Using the InstantiationRange for storing the source range seemed like the minimally invasive way to make the source information for the enable_if diagnostic available.

I could store a TemplateArgumentListInfo pointer instead, but that would also require a new InstantiatingTemplate constructor.

lib/Sema/SemaOverload.cpp
8905–8918

In my view a PartialDiagnostic (or Diagnostic) is not just the input to some formatting routine but a strongly typed container of an error id and additional pieces of information about the error. So, accessing the arguments to the diagnostic through a proper API (introduced by D3060) in order to generate a better error message doesn't seem that objectionable to me. Also, I interpreted your FIXME as a request to do exactly this.

I used a second source range and the err_typename_nested_not_found_enable_if id because I assumed that in Sema::CheckTypenameType we should always generate an error that would be appropriate if the error message is not suppressed by the SFINAE logic. If this is not the case, it might be cleaner to always prepare the enable_if diagnostic in CheckTypenameType and then just reemit it in DiagnoseBadDeduction (adding the missing TemplateArgString).

If you'd like this code to not fail in the hypothetical case where someone generates a err_typename_nested_not_found_enable_if diagnostic with unexpected contents, I could replace the assert with some always-on fallback logic.

lib/Sema/SemaTemplate.cpp
7887–7919

If we don't walk up all alias template instantiations in the current SFINAE context on the instantiation stack, the SFINAE diagnostic will only state no type named 'type' in 'enable_if<false>' or disabled by 'enable_if' without any indication which alias template instantiation caused this error, since the SFINAE diagnostic doesn't contain the stack of instantiated alias templates for the suppressed error. (Would it maybe be an alternative to store and print this stack when we don't have a straightforward enable_if case?)

In my opinion such a diagnostic is significantly worse than a diagnostic that states something like e.g. disabled by 'enable_if_is_callback' or disabled by an error during the substitution of the alias template 'enable_if_is_callback' and that points to the source location of the enable_if_is_callback instantiation in the function declaration triggering the error.

Could you maybe give an example for a situation where you'd find the proposed diagnostic confusing? Would rewording the diagnostic in that situation be an alternative to falling back to the basic SFINAE diagnostic?

rsmith added inline comments.Mar 26 2014, 2:12 PM
include/clang/Basic/SourceLocation.h
217–218

How about SourceManager::isWithinInTranslationUnit(SourceRange, SourceLocation)?

lib/Sema/SemaOverload.cpp
8905–8918

I don't think a PartialDiagnostic is even notionally strongly typed (you can use the same diagnostic with a string or a Decl for the same argument, for instance, and we do that sort of thing in some cases). The approach you're taking is fragile in the presence of refactoring or people reusing the same diagnostic message in other circumstances (where they may not provide the same SourceRanges). I'm also not particularly enthusiastic about a fallback codepath -- I'd much rather have a clean way to pass the necessary information through. I'm not yet sure what that mechanism is, though.

(Yes, the diagnostic produced in CheckTypenameType should be appropriate in the case where we're not in a SFINAE condition.)

lib/Sema/SemaTemplate.cpp
7887–7919

Hmm, suppose I have:

template<typename T> void f(Iterator<T> iter);

with Iterator an alias template. If substitution fails for any reason within Iterator<T>, I don't want to be told disabled by 'Iterator'. I think in this case I actually want to produce two notes for the substitution failure: one indicating that the candidate was ignored due to a substitution failure (pointing at the candidate), and another pointing at the location within the alias template where we hit the problem.

I think we can do this without any alias template special-casing: when we come to emit a delayed 'substitution failure' diagnostic, check whether the location of the diagnostic is within the source range of the declaration of the candidate. If so, do what we do today, and if not, produce two notes (one pointing at the candidate and the other pointing at the location where we hit the error.

stephant added inline comments.Mar 27 2014, 1:04 PM
lib/Sema/SemaOverload.cpp
8905–8918

What I meant with strongly typed is that you can access the diagnostic arguments in a type safe way, as the types of the arguments are recorded and can be queried. Documenting the extra arguments in the diagnostic .td file should minimize any fragility.

If I want to pass on additional information about an error, putting it into the diagnostic data structure still seems rather natural to me. In this case the only alternative I can think of is to put this as an additional field into the TemplateDeductionInfo, but I don't think that would be cleaner.

lib/Sema/SemaTemplate.cpp
7887–7919

It would be really nice if we could tell the user that the substitution failure occurred within Iterator<T>. In this case there is no other possible place, but in more complicated situations it would make the error easier to understand, especially if the error occurred not directly within the Iterator<T> declaration but inside a nested alias template use. When you work with with an IDE that highlights the source ranges in an editor, this can be a real improvement. (And, of course, we can use a different wording than "disabled by xxx" for this and similar cases.)

Without any alias template special-casing the location of the original diagnostic for an error inside an alias template instantiation will never be within the source range of the declaration of the candidate and enable_if_t uses will generate arguably worse error messages than enable_if uses.