This adds special-case treatment for parameter packs in make_unique-like functions to forward parameter names to inlay hints.
Details
Diff Detail
- Repository
- rG LLVM Github Monorepo
Event Timeline
clang-tools-extra/clangd/InlayHints.cpp | ||
---|---|---|
381 | We tend not to use ASTMatchers for these kind of tasks in clangd (except when embedding clang-tidy checks of course). I guess the biggest technical reason is it's hard to reason about their performance so they end up slow and opaque (and indeed the clang-tidy checks are slow and we haven't got a clear idea how to fix it). It's also easy to match too much. But part of it is just convention - because we have more RecursiveASTVisitors, the maintainers are more familiar with the quirks there than the quirks of matchers. To be clear, I don't see any specific problems with this code! But we still might ask to change it as there are costs to mixing styles, and we don't want to end up in a situation where a bug fix requires choosing between an expensive hasAncestor() matcher and rewriting the logic. |
clang-tools-extra/clangd/InlayHints.cpp | ||
---|---|---|
381 | That makes sense. From the Visitor standpoint, do I understand correctly that you mean something like remembering the top-level templated FunctionDecl (or stack of FunctionDecls if we have something like nested Lambdas?) and check every CallExpr and CXXConstructExpr for ParmVarDecls or std::forward(ParmVarDecl) matching the parameters of the higher-level FunctionDecls? That means basically lazily evaluating the parameter names, so more storage inside the Visitor, but allows us to do recursive resolution in a rather straightforward fashion. | |
clang-tools-extra/clangd/unittests/InlayHintTests.cpp | ||
342 | I haven't been able to figure out why, but for some reason the CXXConstructExpr and CXXTemporaryObjectExpr are not being picked up by the RecursiveASTVisitor, while in a similar situation below, the CallExpr gets picked up by VisitCallExpr. | |
357 | This is a bit of an unfortunate side-effect of looking at instantiated templates. |
clang-tools-extra/clangd/InlayHints.cpp | ||
---|---|---|
381 | I imagine something like running a RecursiveASTVisitor on Callee->getBody(), and overriding VisitCallExpr() and VisitCXXConstructExpr() to check whether it is a call of the right form. I don't think theres a need to worry about lambdas nested in the body, I think we may still be able to get useful parameter names out of them, as in e.g.: struct S { S(int a, int b); }; template <typename T, typename... Args> auto Make(Args... args) { auto ConstructTask = [&](){ return T(args...); }; return ConstructTask(); } int main() { auto s = Make<S>(1, 2); } | |
409 | What is the reason for the Args.size() == Params.size() condition? Doesn't this break cases where there is more than one argument matching the variadic parameter? For example: void foo(int a, int b); template <typename... Args> void bar(Args&&... args) { foo(args...); } int main() { bar(1, 2); } Here, I expect we'd have Args.size() == 2 and Params.size() == 1. (We should probably have test coverage for such multiple-arguments cases as well. We probably don't need it for all combinations, but we should have at least one test case exercising it.) | |
clang-tools-extra/clangd/unittests/InlayHintTests.cpp | ||
265 | What does "converted into builtin" mean here? | |
290 | This comment seems out of sync with the testcase (same for a few others) | |
296 | The wrapped range doesn't seem to be used anywhere (same for a few other testcases) | |
357 | Perhaps it would make sense to exclude arguments which are pack expansion expressions from getting parameter hints? |
clang-tools-extra/clangd/InlayHints.cpp | ||
---|---|---|
409 | The function we are looking at is an already instantiated template. The check Args.size() == Params.size() is only necessary because of va_args |
clang-tools-extra/clangd/InlayHints.cpp | ||
---|---|---|
409 | Ah, thanks, I overlooked that. A lot of things in this patch that initially confused me make a lot more sense now :) |
I will take a different approach to the matchers: For each CallExpr involving parameter packs, record the arguments (if they are a ParmVarDecl or stdForward(ParmVarDecl)) and the matching ParmVarDecl of the FunctionDecl in a map. Then I will compact that map to skip intermediate forwarding functions like emplace_back -> allocator::construct -> constructor and in a second pass resolve all previously unresolved forwarded parameters.
This makes sense to me.
This sounds more complicated, but if I'm reading correctly this multi-level forwarding isn't handled in the current version of the patch either?
At some point we'll want to extract this logic out so it can be used by hover etc, but not yet.
clang-tools-extra/clangd/unittests/InlayHintTests.cpp | ||
---|---|---|
342 | The AST here of the instantiated bar looks like -FunctionDecl <line:5:1, line:7:1> line:5:4 used bar 'S *(int &&)' |-TemplateArgument type 'S' |-TemplateArgument pack | `-TemplateArgument type 'int' |-ParmVarDecl <col:8, col:18> col:18 used args 'int &&' `-CompoundStmt <col:24, line:7:1> `-ReturnStmt <line:6:3, col:23> `-CXXNewExpr <col:10, col:23> 'S *' Function 'operator new' 'void *(unsigned long)' `-CXXConstructExpr <col:14, col:23> 'S':'S' 'void (int)' list `-ImplicitCastExpr <col:16> 'int':'int' <LValueToRValue> `-DeclRefExpr <col:16> 'int':'int' lvalue ParmVar 'args' 'int &&' So there's no CXXTemporaryObjectExpr (the value here is a pointer), but should be a CXXConstructExpr. Are you sure you're traversing bar's instantiation/specializaion, as opposed to its templated/generic FunctionDecl? The AST of the templated FunctionDecl indeed has an InitListExpr in place of the CXXConstructExpr because it's not resolved yet. |
! In D124690#3493246, @sammccall wrote:
This makes sense to me.
This sounds more complicated, but if I'm reading correctly this multi-level forwarding isn't handled in the current version of the patch either?
At some point we'll want to extract this logic out so it can be used by hover etc, but not yet.
Yes, currently only direct forwarding works. I originally looked at this when inlay hints were not yet available, and it seemed possible to do this for signature help as well, only most likely based on the template instantiation pattern instead of the instantiated function declaration.
clang-tools-extra/clangd/unittests/InlayHintTests.cpp | ||
---|---|---|
342 | It's quite possible this is related to the visitor not/only inconsistently traversing template instantiations due to shouldVisitTemplateInstantiations returning false. But I need to implement the new approach first, anyways. |
clang-tools-extra/clangd/unittests/InlayHintTests.cpp | ||
---|---|---|
342 | Ah, I think I see the confusion. We don't want to gather all the information in a single traversal of the whole AST. Such a traversal would need to walk all instantiated templates including those from headers, which is far too much/too slow. Instead, you want to analyze particular forwarding functions by walking through their bodies looking for the forwarding call. RecursiveASTVisitor is the tool for this job - you just call TraverseDecl(FunctionDecl) to limit the scope. The visitor should not visit template instantiations, but the traversal can still be rooted at an instantiation! So something roughly like: Optional<vector<ParmVarDecl*>> ultimateParams(FunctionDecl* Callee) { if (!Callee || !isa<FunctionProtoType>(Callee->getType())) return None; if (not instantiated from template || Pattern doesn't use a pack) return Callee->parameters(); for (P : Pattern->parameters()) { if (!P->isPack()) Result.push_back(Callee->parameters[Pos++]); else { if (PackExpansionSize = ...) { if (ForwardedParams = forwardedParams(Callee, P)) Result.append(*ForwardedParams); else // failed, no param info for forwarded args Result.append(nullptr, PackExpansionSize); Pos += PackExpansionSize; } } } } Optional<vector<ParmVarDecl*>> forwardedParams(FunctionDecl *Callee, ParmVarDecl *Pack) { class ForwardFinder : RAV<ForwardFinder> { bool VisitCallExpr(CallExpr *E) { if (E->params() expands Pack) { if (CallParams = ultimateParams(E->getCallee()->getAsFunctionDecl())) Result = CallParams.slice(section corresponding to Pack); } } }; ForwardFinder Finder; if (Callee->hasBody()) return Finder.VisitCallExpr(Callee); } you could also memoize with a map, but I suspect simple recursion is going to be good enough |
This almost works now, the only thing that needs to be fixed is isExpandedParameter, which for some reason picks up the first call, but not the second: (Compare ParameterHints.Forwarded and ParameterHints.VariadicPlain)
cpp void foo(int a); template <typename... Args> void bar(Args&&... args) { return foo(args...); } void baz() { int b; bar(42); // This parameter is recognized as expanded bar(b); // This one doesn't, so its inlay hint is @args: instead of a: }
clang-tools-extra/clangd/unittests/InlayHintTests.cpp | ||
---|---|---|
265 | I am talking about std::forward being a builtin function, which can be detected by its BuiltinID, so it doesn't matter if the signature doesn't match 100%. |
fix iterator invalidation issue, handle UnresolvedLookupExpr and test recursive and split variadic lookup
- add test for emplace-like functions
- fix RecursiveASTVisitor early exit
- fix handling of skipped parameters
clang-tools-extra/clangd/InlayHints.cpp | ||
---|---|---|
207 | This needs to be fixed, see ParameterHints.VariadicPlain vs. ParameterHints.VariadicForwarded if uncommented - I'd need some input from somebody with more knowledge about the AST | |
313 | There is probably more generic functionality available for this? | |
clang-tools-extra/clangd/unittests/InlayHintTests.cpp | ||
452 | This is not portable, but I don't have access to size_t |
It seems like except for the caveats I listed before, all the obvious cases seem to work: make_unique, make_shared, emplace_back with exact type matches. One point that still needs some work is if the parameter needs to be converted inside one of the forwarding functions (probably just needs another unpack inside ForwardingParameterVisitor), as well as a way to remove duplicate parameters that come from recursive templates like std::tuple. One obvious way would be removing inlay hints for duplicate parameters altogether, but that may not be enough/too heuristic? Alternatively, we could inspect the template instantiation pattern.
Had a read through this. I'm still digesting it, but the high-level approach seems reasonable to me.
Could we add a test case for the recursive scenario that came up during chat:
void foo(); template <typename Head, typename... Tail> void foo(Head head, Tail... tail) { foo(tail...); } int main() { foo(1, 2, 3); } `
(even if the behaviour on this testcase isn't what we want yet, it's useful to have it in the test suite as a reminder)
clang-tools-extra/clangd/InlayHints.cpp | ||
---|---|---|
343 | unwrap? "unpack" sounds like something related to parameter packs | |
373 | I think it would be more readable if the state that persists across calls (TraversedFunctions, ForwardedParams, ParamDeclToDef) would be separate from the state that does not (UnresolvedParams, TraversalQueue). A concrete suggestion:
|
Another test case that comes to mind is:
void f1(int a, int b); void f2(int c, int d); template <typename... Args> void foo(Args... args) { if (cond) { f1(args...); } else { f2(args...); } } int main() { foo(1, 2); }
I guess in this case it will use the parameters names from f1, because that's the first call that appears in the function body in traversal order?
I think that's fine, functions written like this are probably not very common. Still good to add to the test suite to document the behaviour.
One more testcase:
template <typename... Args> void foo(Args...); template <typename... Args> void bar(Args... args) { foo(args...); } template <typename... Args> void foo(Args... args) { bar(args...); } int main() { foo(1, 2); }
Sure, this is a stack overflow at runtime, but there's no excuse for it to be one in clangd :)
clang-tools-extra/clangd/InlayHints.cpp | ||
---|---|---|
207 | It looks like isExpandedParameter() relies on the SubstTemplateTypeParmType type sugar being present in the ParmVarDecl's type, but in some cases, the ParmVarDecl's type points to the canonical type directly. I'm not sure what sort of guarantees the AST intends to make about the presence of type sugar. Based on past experience with Eclipse CDT, it's very easy to lose type sugar and maintaining it in all the right places takes some effort. |
clang-tools-extra/clangd/InlayHints.cpp | ||
---|---|---|
207 | Upon further investigation, it looks like the ParmVarDecl is retaining the type sugar fine, it's the getNonReferenceType() call in isExpandedParameter() that loses it. What happens with perfect forwarding when the argument is an lvalue is a bit subtle. In this testcase: template <typename... Args> void bar(Args&&... args) { return foo(std::forward<Args>(args)...); } void baz() { int b; bar($param[[b]]); } the template argument that bar() is instantiated with is Args = [int &]. Substituting into Args&&, that then becomes int& && which collapses into int&, leaving the instantiated parameter type an lvalue reference type. Clang does in fact model this accurately, which means the type structure is: BuiltinType ReferenceType SubstTemplateTypeParmType ReferenceType The outer reference type is the && that's outside the Args, the SubstTemplateTypeParmType reflects the substitution Args = int&, and the inner ReferenceType is the int&. The problem is, getNonReferenceType() unpacks _all_ the reference types, skipping past the SubstTemplateTypeParmType and giving you the BuiltinType. |
Glad this is working! It looks exciting...
My high level comments are:
- the core "what are the ultimate param vars for this call" function is large and reusable enough that we should give this a public interface in AST.h
- the implementation does a few "extra" things and it's not clear they're worth their complexity cost
- the RecursiveASTVisitor is a bit of a "god object" at the moment, and should probably be split up
(Sorry if the comments below are duplicative)
clang-tools-extra/clangd/InlayHints.cpp | ||
---|---|---|
188 | nit: isPackExpandedParameter | |
196 | I think this visitor has too many responsibilities:
Could you try to reduce the scope of the visitor down to its very minimum: the inherited implementation of traversal + recording of forwarding calls? e.g. // Traverses a function body, recording locations where a particular // parameter pack was forwarded to another call. class FindForwardingCalls : public RecursiveASTVisitor { FindForwardingCalls(ParmVarDecl Pack); struct ForwardingCall { FunctionDecl *Callee; unsigned PackOffset; // e.g. 0 for make_unique, 1 for map::try_emplace }; vector<pair<FunctionDecl*, ArrayRef<Expr*>> ForwardingCalls; }; Then the other pieces can be decoupled and structured in ways that make the most sense individually. | |
200 | this is only called once, and the Params is always Callee->params(). Can this be a function instead that takes only Callee and returns the params? If possible, I'd declare this function in AST.h and hide the RecursiveASTVisitor in AST.cpp. | |
207 | Ah, great catch. I think it's fine to assume that ParmVarDecls in particular will carry their sugar. It's more the types of expressions where these things get lost. I think for our purposes we can assume that the *only* sugar present is the &&, and you can simply do const Type PlainType* = Param->getType().getTypePtr(); if (auto *RT = dyn_cast<ReferenceType>(PlainType)) // not get! no desugaring PlainType = RT; if (auto *SubstType = ...) | |
260 | Unless I'm missing something, going looking in the redecls of the function for a parameter name doesn't seem in scope for this patch. We don't support it in inlay hints elsewhere, and it's not clear it has anything to do with forwarding functions. | |
285 | The data flow here is uncomfortably implicit. We're traversing a bunch of functions according to logic A, populating a big map, and then querying the map with logic B, and hoping/asserting that A provides all the information that B needs. I think it would be clearer if these functions were asking questions. | |
302 | (this looks like the wrong place to test that the function hasn't been visited yet: it would be possible to enqueue the same callee multiple times) | |
354 | this seems like an unnecessarily clever optimization, and I can't really imagine a situation where it might matter. I think this can be simplified to: while (const ParamVarDecl *F = ForwardedParams.lookup(Param)) Param = F; and inlined into its caller | |
373 | I agree that these are too tangled, but I wouldn't think there's a strong performance reason for having a cache here. If the storage is needed for correctness, I think we should give it the minimum lifetime needed for correctness, and try to express the algorithm more directly if possible (e.g. recursion rather than a work queue). | |
clang-tools-extra/clangd/unittests/InlayHintTests.cpp | ||
265 | Ah, right! | |
452 | Hmm, I think you can use using size_t = decltype(sizeof(0))? |
I will update this to split up the control flow into a visitor just collecting a single recursion level and the high-level mapping. A general question here would be whether we should do the resolution by parameter or by parameter pack. Doing it by parameter (like I did it right now) is much easier for inlay hints, especially if the pack gets split up into different parts, but that may not necessarily be the case for template functions and more fuzzy cases like signature help/autocomplete.
clang-tools-extra/clangd/InlayHints.cpp | ||
---|---|---|
260 | This was already functionality previously available in chooseParameterNames, I thought I would need to do the same thing here, but turns out that I can get the FunctionDecl from a ParmVarDecl, so this can stay in its previous place. |
I'm not sure I have perfectly grasped the distinction you're drawing, but IIUC the dilemma:
- we could query separately where each arg is forwarded to. For multiple forwards, the recursive step is "what param is arg N of this callee ultimately bound to"
- we could query where a range of args is forwarded to. The recursive step is "what params are args [N-M] of this callee ultimately bound to"
Seems like the first is simpler but the second is more efficient. The second also more clearly ensures we don't give inconsistent results for adjacent parameters.
The approach you take now where you walk->cache->query does manage to combine the best of both (though I find it complex in other ways).
This is a tricky tradeoff. My guess is that it's fine to start with the simple param-by-param query, and leave a FIXME on the recursive function that querying a range at a time would be more efficient for large packs.
clang-tools-extra/clangd/InlayHints.cpp | ||
---|---|---|
260 | You're right, sorry!
This sounds good to me - means we can drop this map right? That was the main piece I was concerned with. | |
313 | Not AFAIK. There's a bunch of rich info about overload sets and their quality, but it's discarded after Sema and not preserved in the AST I think. |
Yes, I think that's a good summary. Only a small clarification:
we could query separately where each arg is forwarded to. For multiple forwards, the recursive step is "what param is arg N of this callee ultimately bound to"
I was thinking of implementing this as "what parameters are the following arguments bound to in a direct call", which doesn't have the aforementioned efficiency issues, and is basically a single recursion level of what I had previously implemented. Since (at least afaik) there is no way to have a parameter pack split up between different function calls (at least from the AST standpoint where we here only allow std::forward or plain parameters), the input would be a FunctionDecl and vector/set of ParmVarDecl and the output would be a map ParmVarDecl -> ParmVarDecl plus an Optional<FunctionDecl> telling us whether we need to recurse further. Unrolling the recursion would also make it easier to limit the depth to which we unpack forwarded parameters.
clang-tools-extra/clangd/InlayHints.cpp | ||
---|---|---|
260 | Yes, we can do the same in the InlayHintVisitor, though it becomes a bit more complex there, since we don't have the index of the argument, which might require a linear search, as different forwarded ParmVarDecls can come from different FunctionDecls |
I've hit a roadblock I'm not sure how to proceed with. It seems that in some cases, a ParmVarDecl of an instantiated function template indeed doesn't preserve the SubstTemplateTypeParmType type sugar allowing me to check which template parameter it was instantiated from. This might be related to reference collapsing on type deduction, since Args... preserves the sugar, but Args&&... doesn't. Without this ability to distinguish between expanded packs and normal parameters, I risk recursing into other functions and pulling up unrelated type names. Any ideas how to proceed here? Essentially, it's the same issue that @nridge already provided a partial solution for by not using getNonReferenceType(), but more complex. I would really like to avoid having to reconstruct parameter indices from arbitrarily many levels of template parameters packs from surrounding scopes.
Example where this pops up:
cpp namespace std { template <typename T> T&& forward(T&); } struct S { S(int a); }; template <typename T, typename... Args> T bar(Args&&... args) { return T{std::forward<Args>($fwd[[args]])...}; } void baz() { int b; bar<S>($param[[b]]); }
I'm happy to take a look at this, but I would need some updated code to run (including in particular the update to the call site that currently uses getNonReferenceType())
Of course, I forgot to create a new diff. There is some debug output and logic errors for some of the tests, but the simple ones already show the issue.
The function in question is getPackTemplateParameter, which provides similar functionality to isExpandedParameter before
Haven't had a chance to try it yet, but based on a quick glance, my suspicion is that the problem is the use of ReferenceType::getPointeeType(), which may do more unwrapping than we want (its implementation contains a loop).
I would try using getPointeeTypeAsWritten() instead and see if that helps.
Indeed, this change makes two tests (VariadicPlainNewConstructor and VariadicForwardedNewConstructor) pass. I assume the other failures are due to other issues in the patch.
Thanks Nathan, that was exactly it. I thought I looked at getTypePtr() directly, but I must have been mistaken.
After a small fix (only adding inlay hints until the first unexpanded pack expression, it seems to work now).
Short explanation of what is going on: I split the parameters into head, pack and tail where pack are all parameters matching the variadic template type parameter we are expanding.
This way, I need to make no allocations inside the visitor.
The only thing to be fixed is a make_tuple-like call, where each parameter is called head, probably simple deduplication makes most sense.
clang-tools-extra/clangd/AST.cpp | ||
---|---|---|
772 | Similar to processCall in InlayHints.cpp, this may have issues with varargs functions. Maybe on top of checking for unexpanded pack expression arguments, I should add a check Callee->getNumParams() == Args.size(). A corresponding test fails by not forwarding a fixed parameter right now. |
Will continue looking at the implementation tomorrow, for now a few minor comments and a suggested variation on a testcase.
clang-tools-extra/clangd/AST.h | ||
---|---|---|
19 | nit: this include seems unnecessary given the code being added | |
210 | This function does something a bit more specific than what this description suggests: given a type parameter pack typename... T, it will return true for function parameters instantiated from a parameter pack of type T... or T&... or T&&..., but not T*... or vector<T>... or anything else. Suggested description: /// Checks whether D is instantiated from a function parameter pack /// whose type is a bare type parameter pack (e.g. `Args...`), or a /// reference to one (e.g. `Args&...` or `Args&&...`). | |
clang-tools-extra/clangd/unittests/InlayHintTests.cpp | ||
200 | As an aside, & does not seem like a useful hint to show for std::forward -- should we try to omit it? (We don't need to do it in this patch as it's not really related.) | |
467 | I think a variation of this test case where foo is also variadic, would be valuable to add: template <typename... Args> void foo(int fixed, Args&&... args); template <typename... Args> void bar(Args&&... args) { foo(args...); } void baz() { bar($fixed[[41]], 42, 43); } This case does seem to already work with the current patch, and I think it's the more common case; I'm happy to defer the varargs one as a less important edge case. |
Finished looking through the patch; I found it nicely organized and fairly easy to understand (as easy as code involving the analysis of C++ variadic templates can be, anyways :-D)!
clang-tools-extra/clangd/AST.cpp | ||
---|---|---|
682 | I don't think there is any requirement that a pack be a trailing template parameter. For example, the following is valid: template <typename... B, typename A> void foo(A, B...); void bar() { foo(1, 2, 3); } | |
761 | These names (FullyResolved, PartiallyResolved, NextTarget) sound like they might be leftovers from a previous implementation? | |
831 | nit: D is an odd name for a CallExpr, maybe Call or E? | |
888 | can just capture TTPT here | |
899 | The second argument is presumably meant to be Tail.rend() | |
clang-tools-extra/clangd/InlayHints.cpp | ||
514 | let's add // will not be hinted for clarity | |
525 | This is an interesting approach for handling VariadicRecursive. I had in mind a different approach, something like keeping a std::set<FunctionTemplateDecl*> SeenFunctionTemplates in resolveForwardingParameters(), populating it with CurrentFunction->getPrimaryTemplate() on each iteration, and bailing if the same function template is seen more than once (indicating recursion). But this approach seems to work too, as a parameter name can't legitimately appear twice in a function declaration. That said, maybe having such a SeenFunctionTemplates recursion guard would be helpful anyways, so that e.g. in VariadicInfinite, we would bail after a single recursion rather than going until MaxDepth? |
- improve documentation
- add more edge case tests
- fix reference hints for parameter packs
- remove unnecessary headers
- fix bug in handling of tail parameters
clang-tools-extra/clangd/AST.cpp | ||
---|---|---|
682 | Do you have a suggestion for how to find this pack in general? I would like to keep this function as efficient as possible, since it's used everywhere | |
clang-tools-extra/clangd/InlayHints.cpp | ||
525 | I see your point here - I would also like an AST based approach more than this purely string-based one. The main issue is that if I deduplicate based on the function templates, I would still be left with the first parameter being named, which doesn't make much sense in something like make_tuple. | |
clang-tools-extra/clangd/unittests/InlayHintTests.cpp | ||
200 | ||
467 | The main reason I added this is to test that the visitor doesn't break on varargs, the output is not that important anyways. I added your suggestion as well, which highlighted another issue, thanks :) |
Thanks for the update and the additional test cases!
clang-tools-extra/clangd/AST.cpp | ||
---|---|---|
682 |
Just iterate backwards through TemplateParams rather than only considering TemplateParams.back(), I suppose.
The ParmVarDecl* overload of getPackTemplateParameter() is called a lot via the IsExpandedPack lambdas, but this overload is only called once per depth level. | |
clang-tools-extra/clangd/InlayHints.cpp | ||
525 | One idea is that we could return the original Parameters from resolveFowardingParameters() if we encounter recursion. |
- remove inlay hints from std::forward in tests
- identify recursive variadic calls from template, not post-processing
- allow template parameter packs to appear at any place
- improve documentation
clang-tools-extra/clangd/AST.cpp | ||
---|---|---|
682 | Ah thanks, of course! I got tricked by my own overloads. |
Thanks -- the patch looks quite good to me now!
I will defer to @sammccall for final approval in case he has any additional feedback.
Sorry about the delay. This is complicated stuff and every time I put it down for a day I forget how it works!
Thanks for your hard work here.
I have some more suggestions but hopefully fairly mechanical.
Feel free to land once you're done, or send it back over to Nathan or I to commit.
clang-tools-extra/clangd/AST.cpp | ||
---|---|---|
674 | nit: this comment doesn't really say anything that the method name doesn't say already. Maybe replace with an example like "returns true for X in template <class...X> int m;" | |
677 | TTPD->isParameterPack() does the same thing with less code | |
686 | it seems confusing and unneccesary to use the same name for the two versions of this function. The name applies to both, but they don't really do the same thing. Maybe call this one getFunctionPackType and the other getUnderylingPackType? | |
687 | This is doing something pretty strange if Callee is a function template specialization. It's not clear to me whether this function should be handling that case (which AFAICS it doesn't, but could inspect the specialization kind), or whether resolveForwardingParameters is responsible for not calling this function in that case (in which case we should probably have an assert here). Can you also add a test case that function template specialization doesn't confuse us? i.e. it should return the parmvardecls from the specialization's definition. | |
717 | This class could use a high-level comment explaining what it does. e.g. This visitor walks over the body of an instantiated function template. The template accepts a parameter pack and the visitor records whether the pack parameters were forwarded to another call. For example, given: template <class T, class...Args> auto make_unique(Args..args) { return unique_ptr<T>(new T(args...)); } When called as `make_unique<std::string>(2, 'x')` this yields a function `make_unique<std::string, int, char>` with two parameters. The visitor records that those two parameters are forwarded to the `constructor std::string(int, char);`. This information is recorded in the `ForwardingInfo` (in general, more complicated scenarios are also possible). | |
clang-tools-extra/clangd/AST.h | ||
213 | nit: I think isExpandedFromParameterPack might be clearer, up to you | |
clang-tools-extra/clangd/InlayHints.cpp | ||
397 | I can't understand what this comment is saying, can you rephrase it or provide an example? From looking at the code, I'm guessing something like: I think the interaction between getExpandedArgCount and chooseParameterNames is unclear and brittle here. (i.e. the invariant that getExpandedArgCount(Args).size() < chooseParameterNames(Params).size() is very non-local) I'd suggest writing this more directly as: for (I = 0; I < ParameterNames.size() && I < Args.size(); ++I) { // ... explanation ... if (isa<PackExpansionExpr>(Args[I])) break; // ... generate param hint ... } | |
464 | why is this check needed if we already decline to provide a name for the parameter on line 534 in chooseParameterNames? | |
514 | say why, not what // If we haven't resolved a pack paramater (e.g. foo(Args... args)) then // hinting as foo(args: 1, args: 2, args: 3) is unlikely to be useful. | |
521 | factor out a function getParamDefinition? | |
532–534 | the name of this function is very confusing - even after understanding what it does, I can't understand how that corresponds to the name. However as noted above I don't think we need this function returning an index at all, instead we can just check the condition while looping. | |
537 | isa<PackExpansionExpr>(E) |
clang-tools-extra/clangd/InlayHints.cpp | ||
---|---|---|
464 | shouldHintName and shouldHintReference are two independent conditions governing whether we show the parameter name and/or a & indicating pass-by-mutable-ref, respectively (I did approve the patch that introduced shouldHintReference myself, hope that's ok) |
clang-tools-extra/clangd/InlayHints.cpp | ||
---|---|---|
460 | The new condition is nonobvious, but the new comment just echoes the code. Instead add a second sentence explaining *why*. | |
464 | Thanks, that makes sense! I just hadn't understood that change. | |
464 | What exactly *is* the motivation for suppressing reference hints in the pack case? (I can imagine there are cases where they're annoying, but it's hard to know if the condition is right without knowing what those are) |
- simplify parameter pack detection
- improve function naming
- make handling of unexpanded packs and varargs more visible
- add tests involving template specializations
- make documentation more descriptive
clang-tools-extra/clangd/AST.cpp | ||
---|---|---|
687 | Do the new tests VariadicNameFromSpecialization(Recursive)? match what you had in mind? | |
clang-tools-extra/clangd/InlayHints.cpp | ||
464 | I added an explanation. Basically, if we are unable to figure out which parameter the arguments are being forwarded to, the type of the ParmVarDecl for Args&&... gets deduced as T& or T&&, so that would mean even though we don't know whether the argument will eventually be forwarded to a reference parameter, we still claim all mutable lvalue arguments will be mutated, which IMO introduces more noise than necessary. But I think there are also good arguments for adding them to be safe. There is another detail here, which is that we don't record whether we used std::forward, so the corresponding rvalue-to-lvalue conversions may lead to some unnecessary & annotations for rvalue arguments. |
Thanks for the readability improvements! I've forgotten if you have commit access?
This stuff is complicated and I'm definitely going to forget the reasoning, the intuitive explanations are going to help a lot if changes are needed in future.
(Maybe it's just me, but the formal language around instantiations, deductions, etc makes my eyes glaze over - as if it's the words themselves rather than how you use them!)
clang-tools-extra/clangd/AST.cpp | ||
---|---|---|
687 | Yes, that's fantastic, thank you! | |
clang-tools-extra/clangd/InlayHints.cpp | ||
464 | This makes sense, the comment explains well, thank you! #1: There's an unstated assumption that pack arguments *will* be forwarded (there are other things we can do with them, like use them in fold-expressions). It's a pretty good assumption but if the comment talks about forwarding, it should probably mention explicitly ("it's likely the params will be somehow forwarded, and...") #2: the idea is that if the reference-ness is deduced from the callsite, then it's not meaningful as an "is the param modified" signal, it's just "is this arg modifiable". Fair enough, but this is a property of universal/forwarding references (T&& where T is a template param), not of packs. So I *think* this check should rather be !isInstantiatedFromForwardingReference(Param). |
yes, I have commit access
clang-tools-extra/clangd/InlayHints.cpp | ||
---|---|---|
464 | #1: I agree, I'll make that more clear before committing. #2: Now that I think about it, there are actually two things we don't keep track of: parameters could lose their reference-ness via Args... instead of Args&&... and their rvalue-ness by not using std::forward. We only look at whether the innermost call takes a reference parameter, but as I said, we may lose some of that information on the way, claiming the function may modify the argument when it actually creates a copy on the way (losing reference-ness). I think the case of an rvalue being mistaken for an lvalue should not be much of an issue, since the reference annotation almost makes sense. To visualize the situation: These three snippets all add &: hints to the parameter of bar void foo(int&); template <typename... Args> void bar(Args... args) { return foo(args...); } void baz() { bar(1); } void foo(int&); template <typename... Args> void bar(Args&&... args) { return foo(args...); } void baz() { bar(1); } void foo(int&); template <typename... Args> void bar(Args&&... args) { return foo(std::forward<Args>(args)...); } void baz() { int a; bar(a); } Two of these three cases probably shouldn't have this annotation? |
clang-tools-extra/clangd/InlayHints.cpp | ||
---|---|---|
464 |
(I'm not quite following what you mean here: if we deduce as Args rather than Args&& then the parameters are not references in the first place, we're passing by value)
Yes. Fundamentally if we're deducing the ref type then we should be looking for a concrete signal of how the value is ultimately used, which involves tracking casts like std::forward. This is true whether it's a pack or not.
Yes. Claiming we're passing a numeric literal (prvalue) by mutable reference doesn't pass the laugh test.
Right, there can be copies on the way for various reasons (passing by value, but also e.g. forwarding a T as a const U& where U is constructible from T). I'm starting to think the simplest answer for now is never to include & if there's a forwarding T&& reference involved, as it's not always clear what it means and it's hard to do reliably. This means we're accepting that make_unique<Foo>(RefParamToConstructor) will lack its &. (It already does today - this patch would need extensions to do this robustly). I think we only need to check the outer call in the usual way, not any inner forwarded calls:
I don't think packs need to be involved in this logic at all. |
detect whether forwarding functions preserve reference-ness and value categories of their arguments
clang-tools-extra/clangd/InlayHints.cpp | ||
---|---|---|
464 | I think I found a suitable proxy for figuring out whether the function uses Args&&... and/or std::forward, can you take a final look at shouldHintReference whether this makes sense to you or I may have missed any important edge cases? I also added a few tests for this behavior (VariadicReferenceHint...) that should show that it works. |
@nridge @sammccall if you don't have any further comments, I will commit this tomorrow, and maybe think about how to extend it to signature help/code completion :)
nit: this include seems unnecessary given the code being added