Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Clang][Sema] Tweak tryCaptureVariable for unevaluated lambdas #93206

Merged
merged 4 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions clang/docs/ReleaseNotes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -824,6 +824,8 @@ Bug Fixes to C++ Support
- Fix a crash when a variable is captured by a block nested inside a lambda. (Fixes #GH93625).
- Fixed a type constraint substitution issue involving a generic lambda expression. (#GH93821)
- Fix a crash caused by improper use of ``__array_extent``. (#GH80474)
- Fixed several bugs in capturing variables within unevaluated contexts. (#GH63845), (#GH67260), (#GH69307),
(#GH88081), (#GH89496), (#GH90669) and (#GH91633).

Bug Fixes to AST Handling
^^^^^^^^^^^^^^^^^^^^^^^^^
Expand Down
4 changes: 4 additions & 0 deletions clang/include/clang/AST/DeclBase.h
Original file line number Diff line number Diff line change
Expand Up @@ -2148,6 +2148,10 @@ class DeclContext {
getDeclKind() <= Decl::lastRecord;
}

bool isRequiresExprBody() const {
return getDeclKind() == Decl::RequiresExprBody;
}

bool isNamespace() const { return getDeclKind() == Decl::Namespace; }

bool isStdNamespace() const;
Expand Down
5 changes: 4 additions & 1 deletion clang/lib/Parse/ParseExprCXX.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1581,7 +1581,10 @@ ExprResult Parser::ParseLambdaExpressionAfterIntroducer(
TrailingReturnTypeLoc, &DS),
std::move(Attributes), DeclEndLoc);

Actions.ActOnLambdaClosureQualifiers(Intro, MutableLoc);
// We have called ActOnLambdaClosureQualifiers for parentheses-less cases
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is there any good reason to do this in 2 places, rather than just delay the above one to here? @cor3ntin ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, looking into ActOnLambdaClosureQualifiers, I don't see anything that (currently) depends on lambda parameters. I guess we can combine these two calls? I'd love to open a separate NFC PR for doing so, before landing this patch. :)

@erichkeane I retrospected your comment in #78598 (comment) and I appreciate the diagnostics for non-ODR uses - however that's much like a QoI issue to me and would it be OK to leave it as a follow-up? Let me know what you think and I'll add a FIXME then.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think I'd be ok with this as a followup. But @cor3ntin is the one who knows the most about lambda stuff (particularly parsing, etc), so I'd like to see him approve this.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, we need to set whether the lambda has a mutable keyword as soon as the parameters, if present, are parsed, as that affect the meaning of subsequent id-expressions

When there are no parens, there can be a noexcept clause for example, and parsing that does requires knowing whether the lambda is mutable, so that we can adjust the type of any reference-to-capture.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, we need to set whether the lambda has a mutable keyword as soon as the parameters, if present, are parsed, as that affect the meaning of subsequent id-expressions
When there are no parens, there can be a noexcept clause for example, and parsing that does requires knowing whether the lambda is mutable, so that we can adjust the type of any reference-to-capture.

Thanks, that makes sense! We currently set LSI->Mutable in ActOnLambdaClosureQualifiers() , and while I think it's possible to move it out of the function, I'm not opposed to leaving it as-is.

// above.
if (HasParentheses)
Actions.ActOnLambdaClosureQualifiers(Intro, MutableLoc);

if (HasParentheses && Tok.is(tok::kw_requires))
ParseTrailingRequiresClause(D);
Expand Down
10 changes: 10 additions & 0 deletions clang/lib/Sema/SemaExpr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18768,13 +18768,23 @@ bool Sema::tryCaptureVariable(
DeclContext *VarDC = Var->getDeclContext();
DeclContext *DC = CurContext;

// Skip past RequiresExprBodys because they don't constitute function scopes.
while (DC->isRequiresExprBody())
DC = DC->getParent();

// tryCaptureVariable is called every time a DeclRef is formed,
// it can therefore have non-negigible impact on performances.
// For local variables and when there is no capturing scope,
// we can bailout early.
if (CapturingFunctionScopes == 0 && (!BuildAndDiagnose || VarDC == DC))
return true;

// Exception: Function parameters are not tied to the function's DeclContext
// until we enter the function definition. Capturing them anyway would result
// in an out-of-bounds error while traversing DC and its parents.
if (isa<ParmVarDecl>(Var) && !VarDC->isFunctionOrMethod())
return true;

const auto *VD = dyn_cast<VarDecl>(Var);
if (VD) {
if (VD->isInitCapture())
Expand Down
29 changes: 20 additions & 9 deletions clang/lib/Sema/SemaLambda.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1076,16 +1076,27 @@ void Sema::ActOnLambdaExpressionAfterIntroducer(LambdaIntroducer &Intro,
// be dependent, because there are template parameters in scope.
CXXRecordDecl::LambdaDependencyKind LambdaDependencyKind =
CXXRecordDecl::LDK_Unknown;
if (LSI->NumExplicitTemplateParams > 0) {
Scope *TemplateParamScope = CurScope->getTemplateParamParent();
assert(TemplateParamScope &&
"Lambda with explicit template param list should establish a "
"template param scope");
assert(TemplateParamScope->getParent());
if (TemplateParamScope->getParent()->getTemplateParamParent() != nullptr)
LambdaDependencyKind = CXXRecordDecl::LDK_AlwaysDependent;
} else if (CurScope->getTemplateParamParent() != nullptr) {
if (CurScope->getTemplateParamParent() != nullptr) {
LambdaDependencyKind = CXXRecordDecl::LDK_AlwaysDependent;
} else if (Scope *P = CurScope->getParent()) {
// Given a lambda defined inside a requires expression,
//
// struct S {
// S(auto var) requires requires { [&] -> decltype(var) { }; }
// {}
// };
//
// The parameter var is not injected into the function Decl at the point of
// parsing lambda. In such scenarios, perceiving it as dependent could
// result in the constraint being evaluated, which matches what GCC does.
while (P->getEntity() && P->getEntity()->isRequiresExprBody())
P = P->getParent();
if (P->isFunctionDeclarationScope() &&
llvm::any_of(P->decls(), [](Decl *D) {
return isa<ParmVarDecl>(D) &&
cast<ParmVarDecl>(D)->getType()->isTemplateTypeParmType();
}))
LambdaDependencyKind = CXXRecordDecl::LDK_AlwaysDependent;
}

CXXRecordDecl *Class = createLambdaClosureType(
Expand Down
2 changes: 1 addition & 1 deletion clang/lib/Sema/TreeTransform.h
Original file line number Diff line number Diff line change
Expand Up @@ -14247,7 +14247,7 @@ TreeTransform<Derived>::TransformLambdaExpr(LambdaExpr *E) {
// will be deemed as dependent even if there are no dependent template
// arguments.
// (A ClassTemplateSpecializationDecl is always a dependent context.)
while (DC->getDeclKind() == Decl::Kind::RequiresExprBody)
while (DC->isRequiresExprBody())
DC = DC->getParent();
if ((getSema().isUnevaluatedContext() ||
getSema().isConstantEvaluatedContext()) &&
Expand Down
75 changes: 75 additions & 0 deletions clang/test/SemaCXX/lambda-unevaluated.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -189,3 +189,78 @@ void recursive() {

}
}

// GH63845: Test if we have skipped past RequiresExprBodyDecls in tryCaptureVariable().
namespace GH63845 {

template <bool> struct A {};

struct true_type {
constexpr operator bool() noexcept { return true; }
};

constexpr bool foo() {
true_type x{};
return requires { typename A<x>; };
}

static_assert(foo());

} // namespace GH63845

// GH69307: Test if we can correctly handle param decls that have yet to get into the function scope.
namespace GH69307 {

constexpr auto ICE() {
constexpr auto b = 1;
return [=](auto c) -> int
requires requires { b + c; }
{ return 1; };
};

constexpr auto Ret = ICE()(1);

} // namespace GH69307

// GH88081: Test if we evaluate the requires expression with lambda captures properly.
namespace GH88081 {

// Test that ActOnLambdaClosureQualifiers() is called only once.
void foo(auto value)
requires requires { [&] -> decltype(value) {}; }
// expected-error@-1 {{non-local lambda expression cannot have a capture-default}}
{}

struct S { //#S
S(auto value) //#S-ctor
requires requires { [&] -> decltype(value) { return 2; }; } {} // #S-requires

static auto foo(auto value) -> decltype([&]() -> decltype(value) {}()) { return {}; } // #S-foo

// FIXME: 'value' does not constitute an ODR use here. Add a diagnostic for it.
static auto bar(auto value) -> decltype([&] { return value; }()) {
return "a"; // #bar-body
}
};

S s("a"); // #use
// expected-error@#S-requires {{cannot initialize return object of type 'decltype(value)' (aka 'const char *') with an rvalue of type 'int'}}
// expected-error@#use {{no matching constructor}}
// expected-note@#S-requires {{substituting into a lambda expression here}}
// expected-note@#S-requires {{substituting template arguments into constraint expression here}}
// expected-note@#S-requires {{in instantiation of requirement here}}
// expected-note@#use {{checking constraint satisfaction for template 'S<const char *>' required here}}
// expected-note@#use {{requested here}}
// expected-note-re@#S 2{{candidate constructor {{.*}} not viable}}
// expected-note@#S-ctor {{constraints not satisfied}}
// expected-note-re@#S-requires {{because {{.*}} would be invalid}}

void func() {
S::foo(42);
S::bar("str");
S::bar(0.618);
// expected-error-re@#bar-body {{cannot initialize return object of type {{.*}} (aka 'double') with an lvalue of type 'const char[2]'}}
// expected-note@-2 {{requested here}}
}

} // namespace GH88081
Loading