This is an archive of the discontinued LLVM Phabricator instance.

[clangd] add inlay hints for std::forward-ed parameter packs
ClosedPublic

Authored by upsj on Apr 29 2022, 11:10 AM.

Details

Summary

This adds special-case treatment for parameter packs in make_unique-like functions to forward parameter names to inlay hints.

Diff Detail

Event Timeline

There are a very large number of changes, so older changes are hidden. Show Older Changes
sammccall added inline comments.Apr 30 2022, 4:07 AM
clang-tools-extra/clangd/InlayHints.cpp
386

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.

upsj updated this revision to Diff 426224.Apr 30 2022, 6:25 AM

remove dependency on D124359 and add tests

upsj updated this revision to Diff 426225.Apr 30 2022, 6:27 AM

remove unnecessary changes

upsj published this revision for review.Apr 30 2022, 6:40 AM
upsj added inline comments.
clang-tools-extra/clangd/InlayHints.cpp
386

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
255

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.

270

This is a bit of an unfortunate side-effect of looking at instantiated templates.

upsj marked 3 inline comments as not done.Apr 30 2022, 6:40 AM
nridge added inline comments.May 2 2022, 12:11 AM
clang-tools-extra/clangd/InlayHints.cpp
386

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);
}
474

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
178

What does "converted into builtin" mean here?

203

This comment seems out of sync with the testcase (same for a few others)

209

The wrapped range doesn't seem to be used anywhere (same for a few other testcases)

270

Perhaps it would make sense to exclude arguments which are pack expansion expressions from getting parameter hints?

upsj added inline comments.May 2 2022, 7:32 AM
clang-tools-extra/clangd/InlayHints.cpp
474

The function we are looking at is an already instantiated template. The check Args.size() == Params.size() is only necessary because of va_args

nridge added inline comments.May 4 2022, 12:04 AM
clang-tools-extra/clangd/InlayHints.cpp
474

Ah, thanks, I overlooked that. A lot of things in this patch that initially confused me make a lot more sense now :)

upsj planned changes to this revision.May 4 2022, 11:23 PM

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.

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
255

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.

upsj added a comment.May 5 2022, 1:59 AM

! 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
255

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.

sammccall added inline comments.May 5 2022, 4:00 AM
clang-tools-extra/clangd/unittests/InlayHintTests.cpp
255

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

upsj updated this revision to Diff 427851.May 7 2022, 5:56 AM

use an RecursiveASTVisitor instead of ASTMatcher

upsj marked 10 inline comments as done.May 7 2022, 6:03 AM

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
178

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%.

upsj updated this revision to Diff 427853.May 7 2022, 6:13 AM
upsj marked an inline comment as done.

Work around the currently broken isExpandedParameter, also fix the broken diff

upsj updated this revision to Diff 427855.May 7 2022, 6:21 AM

updated patch baseline

upsj updated this revision to Diff 427861.May 7 2022, 7:24 AM

fix iterator invalidation issue, handle UnresolvedLookupExpr and test recursive and split variadic lookup

upsj updated this revision to Diff 427928.May 8 2022, 6:07 AM
  • add test for emplace-like functions
  • fix RecursiveASTVisitor early exit
  • fix handling of skipped parameters
upsj updated this revision to Diff 427930.May 8 2022, 6:18 AM

attempt to fix the patch issue

upsj added inline comments.May 8 2022, 6:24 AM
clang-tools-extra/clangd/InlayHints.cpp
214

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

320

There is probably more generic functionality available for this?

clang-tools-extra/clangd/unittests/InlayHintTests.cpp
305

This is not portable, but I don't have access to size_t

upsj added a comment.May 8 2022, 7:43 AM

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
350

unwrap? "unpack" sounds like something related to parameter packs

380

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:

  • Factor the first set out into a ParameterMappingCache struct
  • Do not keep a ForwardingParameterVisitor as a member of InlayHintVisitor, only the ParameterMappingCache
  • Create the ForwardingParameterVisitor as a local for each call, and pass it a reference to the ParameterMappingCache in the constructor

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 :)

nridge added inline comments.May 16 2022, 1:48 AM
clang-tools-extra/clangd/InlayHints.cpp
214

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.

nridge added inline comments.May 16 2022, 2:18 AM
clang-tools-extra/clangd/InlayHints.cpp
214

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
195

nit: isPackExpandedParameter
(usually "expanded" means macro expansion)

203

I think this visitor has too many responsibilities:

  • it controls the lifetime of caches for reuse of partial results. (FWIW, I'm skeptical there's any serious benefit of this on real code, and would prefer to avoid paying any complexity costs for it)
  • it begins a traversal over function bodies, hooking interesting nodes to find forwarding calls and analyzing them
  • it owns the queues that are used for recursive resolution. (Again, it's unclear if these are providing value, but it's hard to separate them out from the design).
  • (via inheritance) it implements the mechanics of traversal itself

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.

207

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.

214

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 = ...)
267

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.
Maybe the added complexity is justifiable if this logic can be shared with different functions (hover, signature help) but I don't think it belongs in this patch.

292

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.
The contract of these void handleXXX functions is pretty vague.

I think it would be clearer if these functions were asking questions.
We got here by investigating the ParmVarDecls associated with a *particular* pack.
So if we were more direct here, we'd verify that the param here is actually tied to *that* pack, and reporting the call (i.e. this function should return Optional<ForwardingCall> for some struct we define, and not also worrying about recursion).

309

(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)

361

this seems like an unnecessarily clever optimization, and I can't really imagine a situation where it might matter.
(This whole analysis only lives for a single main-file call, so this only kicks in if multiple arguments eventually end up forwarded to the same parameter!)

I think this can be simplified to:

while (const ParamVarDecl *F = ForwardedParams.lookup(Param))
  Param = F;

and inlined into its caller

380

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).
If the storage is for performance, can we give some intuitive explanation of what is being reused and how many times in common cases?

clang-tools-extra/clangd/unittests/InlayHintTests.cpp
178

Ah, right!
Maybe say "This prototype of std::forward is sufficient for clang to recognize it"?

305

Hmm, I think you can use using size_t = decltype(sizeof(0))?

upsj planned changes to this revision.May 22 2022, 5:41 AM
upsj marked 15 inline comments as done.

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
267

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.

upsj marked an inline comment as done.May 22 2022, 5:44 AM

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.

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
267

You're right, sorry!
I think the the version we have is pretty opportunistic: this is a couple of lines of code, so why not?
I don't think we should aim for parity with it, but rather just handle whatever cases are both useful & trivial to implement (in this patch)

I can get the FunctionDecl from a ParmVarDecl, so this can stay in its previous place.

This sounds good to me - means we can drop this map right? That was the main piece I was concerned with.

320

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.

upsj added a comment.May 23 2022, 3:16 AM

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
267

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

upsj added a comment.May 26 2022, 11:20 AM

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]]);
    }

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())

upsj updated this revision to Diff 432518.May 27 2022, 4:07 AM

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

upsj planned changes to this revision.May 27 2022, 4:08 AM

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.

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.

upsj updated this revision to Diff 432900.May 30 2022, 6:00 AM
upsj marked an inline comment as done.

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.

upsj updated this revision to Diff 433732.Jun 2 2022, 7:06 AM
  • fix varargs, again :)
  • remove parameter names entirely if they occur multiple times
upsj updated this revision to Diff 434257.Jun 4 2022, 3:51 AM

This is now ready to review, only needed to fix a formatting issue

upsj updated this revision to Diff 434262.Jun 4 2022, 5:23 AM

add test for varargs function called from forwarding function

upsj added inline comments.Jun 4 2022, 5:24 AM
clang-tools-extra/clangd/AST.cpp
772 ↗(On Diff #434257)

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.

upsj added a comment.Jun 10 2022, 5:45 AM

@nridge @sammccall can you give this another look?

I will do my best to take a look this weekend. Your patience is appreciated :)

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 ↗(On Diff #434262)

nit: this include seems unnecessary given the code being added

210 ↗(On Diff #434262)

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
164

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.)

320

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 ↗(On Diff #434262)

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 ↗(On Diff #434262)

These names (FullyResolved, PartiallyResolved, NextTarget) sound like they might be leftovers from a previous implementation?

831 ↗(On Diff #434262)

nit: D is an odd name for a CallExpr, maybe Call or E?

888 ↗(On Diff #434262)

can just capture TTPT here

899 ↗(On Diff #434262)

The second argument is presumably meant to be Tail.rend()

clang-tools-extra/clangd/InlayHints.cpp
594

let's add // will not be hinted for clarity

600

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?

upsj updated this revision to Diff 437194.Jun 15 2022, 8:42 AM
upsj marked 9 inline comments as done.
  • improve documentation
  • add more edge case tests
  • fix reference hints for parameter packs
  • remove unnecessary headers
  • fix bug in handling of tail parameters
upsj added inline comments.Jun 15 2022, 8:44 AM
clang-tools-extra/clangd/AST.cpp
682 ↗(On Diff #434262)

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
600

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
164
320

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 ↗(On Diff #434262)

Do you have a suggestion for how to find this pack in general?

Just iterate backwards through TemplateParams rather than only considering TemplateParams.back(), I suppose.

I would like to keep this function as efficient as possible, since it's used everywhere

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
600

One idea is that we could return the original Parameters from resolveFowardingParameters() if we encounter recursion.

upsj updated this revision to Diff 437508.Jun 16 2022, 5:09 AM
upsj marked 4 inline comments as done.
  • 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 ↗(On Diff #434262)

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.

upsj added a comment.Jun 28 2022, 1:18 AM

can you give this another look, if you have some time @sammccall?

sammccall accepted this revision.Jun 28 2022, 6:37 AM

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
677 ↗(On Diff #437508)

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;"

680 ↗(On Diff #437508)

TTPD->isParameterPack() does the same thing with less code

689 ↗(On Diff #437508)

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?
(not get*TemplateParameter as they return a type, not the parameter decl)

690 ↗(On Diff #437508)

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.

720 ↗(On Diff #437508)

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
212 ↗(On Diff #437508)

nit: I think isExpandedFromParameterPack might be clearer, up to you

clang-tools-extra/clangd/InlayHints.cpp
464

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:
"If we're passing a parameter pack into this call, we need to give up matching arguments to parameters at that point as we don't know how long it is".


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 ...
}
533

why is this check needed if we already decline to provide a name for the parameter on line 534 in chooseParameterNames?

594

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.
601

factor out a function getParamDefinition?

624

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.

629

isa<PackExpansionExpr>(E)

This revision is now accepted and ready to land.Jun 28 2022, 6:37 AM
nridge added inline comments.Jun 28 2022, 3:59 PM
clang-tools-extra/clangd/InlayHints.cpp
533

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)

sammccall added inline comments.Jun 29 2022, 2:14 AM
clang-tools-extra/clangd/InlayHints.cpp
532–533

The new condition is nonobvious, but the new comment just echoes the code.

Instead add a second sentence explaining *why*.
(I can't provide a suggestion, because I don't understand why)

533

Thanks, that makes sense! I just hadn't understood that change.

533

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)

upsj updated this revision to Diff 440921.Jun 29 2022, 3:13 AM
upsj marked 14 inline comments as done.
  • 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
690 ↗(On Diff #437508)

Do the new tests VariadicNameFromSpecialization(Recursive)? match what you had in mind?

clang-tools-extra/clangd/InlayHints.cpp
533

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.

sammccall accepted this revision.Jun 29 2022, 3:56 AM

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
690 ↗(On Diff #437508)

Yes, that's fantastic, thank you!

clang-tools-extra/clangd/InlayHints.cpp
533

This makes sense, the comment explains well, thank you!
I have a couple of quibbles, up to you whether to change the logic.

#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).
But maybe that's more complexity and what you have is a good heuristic - I think at least we should call out that it's a heuristic for the true condition.

upsj marked an inline comment as done.Jun 29 2022, 5:08 AM

yes, I have commit access

clang-tools-extra/clangd/InlayHints.cpp
533

#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?

sammccall added inline comments.Jun 29 2022, 7:26 AM
clang-tools-extra/clangd/InlayHints.cpp
533

parameters could lose their reference-ness via Args... instead of Args&&...

(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)

and their rvalue-ness by not using std::forward

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.
It's a bunch of work and I'm not sure it's worth it (and I'm certainly not asking you to add it!)

Two of these three cases probably shouldn't have this annotation?

Yes. Claiming we're passing a numeric literal (prvalue) by mutable reference doesn't pass the laugh test.

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)

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:

  • if we're passing as (deduced) T then that's by value and no & is needed
  • if we're passing as (deduced) T& then that's explicitly by reference (will only bind to a mutable lvalue) and & is needed
  • if we're passing as (deduced) T&& then we're going to conservatively not include the &

I don't think packs need to be involved in this logic at all.

upsj updated this revision to Diff 441520.Jun 30 2022, 2:18 PM
upsj marked 3 inline comments as done.

detect whether forwarding functions preserve reference-ness and value categories of their arguments

upsj added inline comments.Jun 30 2022, 2:23 PM
clang-tools-extra/clangd/InlayHints.cpp
533

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.

upsj updated this revision to Diff 441646.Jul 1 2022, 2:21 AM

don't add reference hints to unresolved parameter packs

upsj added a comment.Jul 5 2022, 7:21 AM

@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 :)

This revision was automatically updated to reflect the committed changes.