https://github.com/tiagomacarios updated https://github.com/llvm/llvm-project/pull/215603
>From b553287e10f7df73a9aa5ae799b70ba6ab359ef1 Mon Sep 17 00:00:00 2001 From: Tiago Macarios <[email protected]> Date: Mon, 10 Aug 2026 15:30:15 -0700 Subject: [PATCH 1/4] [clang] Fix RHS rebuilding for GNU choose expressions GNU choose expressions are an extension modeled with potential-result rebuilding analogous to conditional expressions. The potential results of a conditional come from its second and third operands, while its first operand is evaluated as the condition: https://eel.is/c++draft/basic.def.odr#3.7 https://eel.is/c++draft/expr.cond#1 Rebuild the right operand from the right operand instead of transforming the left operand twice. Add a CodeGen regression with an explicit Itanium target triple so the selected branch and its side effects are stable across hosts. Co-authored-by: Copilot <[email protected]> Copilot-Session: c3177956-d545-4f54-92ee-af0a5a8e5046 --- clang/lib/Sema/SemaExpr.cpp | 2 +- clang/test/CodeGenCXX/choose-expr-discarded.cpp | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 clang/test/CodeGenCXX/choose-expr-discarded.cpp diff --git a/clang/lib/Sema/SemaExpr.cpp b/clang/lib/Sema/SemaExpr.cpp index 5f4af9debe91a..59dbbcd6dfd2c 100644 --- a/clang/lib/Sema/SemaExpr.cpp +++ b/clang/lib/Sema/SemaExpr.cpp @@ -20483,7 +20483,7 @@ static ExprResult rebuildPotentialResultsAsNonOdrUsed(Sema &S, Expr *E, if (LHS.isInvalid()) return ExprError(); - ExprResult RHS = Rebuild(CE->getLHS()); + ExprResult RHS = Rebuild(CE->getRHS()); if (RHS.isInvalid()) return ExprError(); diff --git a/clang/test/CodeGenCXX/choose-expr-discarded.cpp b/clang/test/CodeGenCXX/choose-expr-discarded.cpp new file mode 100644 index 0000000000000..9ad85ff923b31 --- /dev/null +++ b/clang/test/CodeGenCXX/choose-expr-discarded.cpp @@ -0,0 +1,15 @@ +// RUN: %clang_cc1 -std=c++20 -triple x86_64-unknown-linux-gnu -emit-llvm -o - %s | FileCheck %s + +int left(); +int right(); + +void test() { + const int a = 0; + const int b = 0; + __builtin_choose_expr(false, left() ? a : a, (right(), b)); +} + +// CHECK-LABEL: define{{.*}} void @_Z4testv() +// CHECK-NOT: call{{.*}} @_Z4leftv() +// CHECK: call{{.*}} @_Z5rightv() +// CHECK: ret void >From d6e0b0920947e56b93a59201fc74d412df852226 Mon Sep 17 00:00:00 2001 From: Tiago Macarios <[email protected]> Date: Mon, 10 Aug 2026 15:32:14 -0700 Subject: [PATCH 2/4] [clang] Handle discarded non-ODR-uses in lambdas A discarded-value expression is defined by [expr.context]/2. The potential-result rules cover id-expressions, conditional operands, and the right operand of comma expressions, and [basic.def.odr]/5.2.1 makes a non-reference variable non-ODR-used when such a potential result is discarded without an lvalue-to-rvalue conversion: https://eel.is/c++draft/expr.context#2 https://eel.is/c++draft/basic.def.odr#3.1 https://eel.is/c++draft/basic.def.odr#3.7 https://eel.is/c++draft/basic.def.odr#3.8 https://eel.is/c++draft/basic.def.odr#5.2.1 Process those potential results with NOUR_Discarded, retain that reason through dependent expressions, and delay ODR-use marking for constexpr and non-constant non-reference potential lambda captures. The lambda rules describe potentially referenced entities, reaching scope, and implicit capture under a capture-default: https://eel.is/c++draft/expr.prim.lambda.capture#7 https://eel.is/c++draft/expr.prim.lambda.capture#10 https://eel.is/c++draft/expr.prim.lambda.capture#11 https://eel.is/c++draft/expr.prim.lambda.capture#12 Accordingly, [] permits a discarded non-ODR-use, while [=] and [&] retain Clang's established implicit capture for non-constant entities. Constexpr non-ODR-used entities continue to omit capture. For [=], the tests verify the copy-capture layout; reference-capture storage is unspecified, so no [&] layout property is asserted. Co-authored-by: Copilot <[email protected]> Copilot-Session: c3177956-d545-4f54-92ee-af0a5a8e5046 --- clang/include/clang/Sema/ScopeInfo.h | 18 ++- clang/include/clang/Sema/Sema.h | 1 + clang/lib/Sema/SemaExpr.cpp | 106 ++++++++++++------ clang/lib/Sema/SemaExprCXX.cpp | 35 +++++- clang/test/CXX/basic/basic.def.odr/p2.cpp | 9 +- .../SemaCXX/lambda-expressions-gh127086.cpp | 88 +++++++++++++++ 6 files changed, 211 insertions(+), 46 deletions(-) create mode 100644 clang/test/SemaCXX/lambda-expressions-gh127086.cpp diff --git a/clang/include/clang/Sema/ScopeInfo.h b/clang/include/clang/Sema/ScopeInfo.h index 8dc40a5bc58bd..6e5da3cc84910 100644 --- a/clang/include/clang/Sema/ScopeInfo.h +++ b/clang/include/clang/Sema/ScopeInfo.h @@ -938,6 +938,11 @@ class LambdaScopeInfo final : /// if the enclosing full-expression is instantiation dependent). llvm::SmallPtrSet<Expr *, 8> NonODRUsedCapturingExprs; + /// Contains the subset of NonODRUsedCapturingExprs whose use is discarded. + /// These expressions remain non-odr-uses even if their full-expression is + /// instantiation-dependent. + llvm::SmallPtrSet<Expr *, 4> DiscardedValueCapturingExprs; + /// A map of explicit capture indices to their introducer source ranges. llvm::DenseMap<unsigned, SourceRange> ExplicitCaptureRanges; @@ -1045,11 +1050,14 @@ class LambdaScopeInfo final : /// seemingly harmless change elsewhere in Sema could cause us to start or stop /// building such a node. So we need a rule that anyone can implement and get /// exactly the same result". - void markVariableExprAsNonODRUsed(Expr *CapturingVarExpr) { + void markVariableExprAsNonODRUsed(Expr *CapturingVarExpr, + NonOdrUseReason NOUR) { assert(isa<DeclRefExpr>(CapturingVarExpr) || isa<MemberExpr>(CapturingVarExpr) || isa<FunctionParmPackExpr>(CapturingVarExpr)); NonODRUsedCapturingExprs.insert(CapturingVarExpr); + if (NOUR == NOUR_Discarded) + DiscardedValueCapturingExprs.insert(CapturingVarExpr); } bool isVariableExprMarkedAsNonODRUsed(Expr *CapturingVarExpr) const { assert(isa<DeclRefExpr>(CapturingVarExpr) || @@ -1057,6 +1065,12 @@ class LambdaScopeInfo final : isa<FunctionParmPackExpr>(CapturingVarExpr)); return NonODRUsedCapturingExprs.count(CapturingVarExpr); } + bool isVariableExprMarkedAsDiscarded(Expr *CapturingVarExpr) const { + assert(isa<DeclRefExpr>(CapturingVarExpr) || + isa<MemberExpr>(CapturingVarExpr) || + isa<FunctionParmPackExpr>(CapturingVarExpr)); + return DiscardedValueCapturingExprs.count(CapturingVarExpr); + } void removePotentialCapture(Expr *E) { llvm::erase(PotentiallyCapturingExprs, E); } @@ -1070,7 +1084,7 @@ class LambdaScopeInfo final : bool hasPotentialCaptures() const { return getNumPotentialVariableCaptures() || - PotentialThisCaptureLocation.isValid(); + PotentialThisCaptureLocation.isValid(); } void visitPotentialCaptures( diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index b1d2488d2163b..b05ba031b49cd 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -7183,6 +7183,7 @@ class Sema final : public SemaBase { unsigned CapturingScopeIndex); ExprResult CheckLValueToRValueConversionOperand(Expr *E); + ExprResult CheckDiscardedValueExpression(Expr *E); void CleanupVarDeclMarking(); /// Try to capture the given variable. diff --git a/clang/lib/Sema/SemaExpr.cpp b/clang/lib/Sema/SemaExpr.cpp index 59dbbcd6dfd2c..0168c7e886dc5 100644 --- a/clang/lib/Sema/SemaExpr.cpp +++ b/clang/lib/Sema/SemaExpr.cpp @@ -20287,9 +20287,26 @@ static ExprResult rebuildPotentialResultsAsNonOdrUsed(Sema &S, Expr *E, // Mark that this expression does not constitute an odr-use. auto MarkNotOdrUsed = [&] { if (!MaybeCUDAODRUsed()) { - S.MaybeODRUseExprs.remove(E); - if (LambdaScopeInfo *LSI = S.getCurLambda()) - LSI->markVariableExprAsNonODRUsed(E); + LambdaScopeInfo *LSI = S.getCurLambda(); + bool PreserveCaptureDefault = false; + if (NOUR == NOUR_Discarded && LSI && + LSI->ImpCaptureStyle != CapturingScopeInfo::ImpCap_None && + S.MaybeODRUseExprs.count(E)) { + if (auto *DRE = dyn_cast<DeclRefExpr>(E)) + PreserveCaptureDefault = + !cast<VarDecl>(DRE->getDecl()) + ->isUsableInConstantExpressions(S.Context); + else if (auto *ME = dyn_cast<MemberExpr>(E)) + PreserveCaptureDefault = + !cast<VarDecl>(ME->getMemberDecl()) + ->isUsableInConstantExpressions(S.Context); + else + PreserveCaptureDefault = isa<FunctionParmPackExpr>(E); + } + if (!PreserveCaptureDefault) + S.MaybeODRUseExprs.remove(E); + if (LSI) + LSI->markVariableExprAsNonODRUsed(E, NOUR); } }; @@ -20563,6 +20580,14 @@ ExprResult Sema::CheckLValueToRValueConversionOperand(Expr *E) { return Result.get() ? Result : E; } +ExprResult Sema::CheckDiscardedValueExpression(Expr *E) { + ExprResult Result = + rebuildPotentialResultsAsNonOdrUsed(*this, E, NOUR_Discarded); + if (Result.isInvalid()) + return ExprError(); + return Result.get() ? Result : E; +} + ExprResult Sema::ActOnConstantExpression(ExprResult Res) { if (!Res.isUsable()) return Res; @@ -20599,35 +20624,43 @@ void Sema::CleanupVarDeclMarking() { "MarkVarDeclODRUsed failed to cleanup MaybeODRUseExprs?"); } -static void DoMarkPotentialCapture(Sema &SemaRef, SourceLocation Loc, - ValueDecl *Var, Expr *E) { +static LambdaScopeInfo *getLambdaForPotentialCapture(Sema &SemaRef, + ValueDecl *Var) { VarDecl *VD = Var->getPotentiallyDecomposedVarDecl(); if (!VD) - return; + return nullptr; const bool RefersToEnclosingScope = (SemaRef.CurContext != VD->getDeclContext() && VD->getDeclContext()->isFunctionOrMethod() && VD->hasLocalStorage()); - if (RefersToEnclosingScope) { - LambdaScopeInfo *const LSI = - SemaRef.getCurLambda(/*IgnoreNonLambdaCapturingScope=*/true); - if (LSI && (!LSI->CallOperator || - !LSI->CallOperator->Encloses(Var->getDeclContext()))) { - // If a variable could potentially be odr-used, defer marking it so - // until we finish analyzing the full expression for any - // lvalue-to-rvalue - // or discarded value conversions that would obviate odr-use. - // Add it to the list of potential captures that will be analyzed - // later (ActOnFinishFullExpr) for eventual capture and odr-use marking - // unless the variable is a reference that was initialized by a constant - // expression (this will never need to be captured or odr-used). - // - // FIXME: We can simplify this a lot after implementing P0588R1. - assert(E && "Capture variable should be used in an expression."); - if (!Var->getType()->isReferenceType() || - !VD->isUsableInConstantExpressions(SemaRef.Context)) - LSI->addPotentialCapture(E->IgnoreParens()); - } + if (!RefersToEnclosingScope) + return nullptr; + + LambdaScopeInfo *LSI = + SemaRef.getCurLambda(/*IgnoreNonLambdaCapturingScope=*/true); + if (LSI && (!LSI->CallOperator || + !LSI->CallOperator->Encloses(Var->getDeclContext()))) + return LSI; + return nullptr; +} + +static void DoMarkPotentialCapture(Sema &SemaRef, SourceLocation Loc, + ValueDecl *Var, Expr *E) { + if (LambdaScopeInfo *LSI = getLambdaForPotentialCapture(SemaRef, Var)) { + // If a variable could potentially be odr-used, defer marking it so + // until we finish analyzing the full expression for any lvalue-to-rvalue + // or discarded value conversions that would obviate odr-use. + // Add it to the list of potential captures that will be analyzed + // later (ActOnFinishFullExpr) for eventual capture and odr-use marking + // unless the variable is a reference that was initialized by a constant + // expression (this will never need to be captured or odr-used). + // + // FIXME: We can simplify this a lot after implementing P0588R1. + assert(E && "Capture variable should be used in an expression."); + VarDecl *VD = Var->getPotentiallyDecomposedVarDecl(); + if (!Var->getType()->isReferenceType() || + !VD->isUsableInConstantExpressions(SemaRef.Context)) + LSI->addPotentialCapture(E->IgnoreParens()); } } @@ -20770,12 +20803,14 @@ static void DoMarkVarDeclReferenced( // conversion is applied // -- x is a variable of non-reference type, and e is an element of the set // of potential results of a discarded-value expression to which the - // lvalue-to-rvalue conversion is not applied [FIXME] + // lvalue-to-rvalue conversion is not applied // - // We check the first part of the second bullet here, and - // Sema::CheckLValueToRValueConversionOperand deals with the second part. - // FIXME: To get the third bullet right, we need to delay this even for - // variables that are not usable in constant expressions. + // Delay marking variables usable in constant expressions until the + // enclosing full-expression determines whether an lvalue-to-rvalue + // conversion is applied. Also delay non-reference variables that are + // potential lambda captures so a discarded-value expression can obviate + // their capture. + // FIXME: Implement the third bullet for non-capturing contexts too. // If we already know this isn't an odr-use, there's nothing more to do. if (DeclRefExpr *DRE = dyn_cast_or_null<DeclRefExpr>(E)) @@ -20799,14 +20834,19 @@ static void DoMarkVarDeclReferenced( // behavior. break; - case OdrUseContext::Used: + case OdrUseContext::Used: { // If we might later find that this expression isn't actually an odr-use, // delay the marking. - if (E && Var->isUsableInConstantExpressions(SemaRef.Context)) + LambdaScopeInfo *PotentialCaptureLSI = + getLambdaForPotentialCapture(SemaRef, Var); + if (E && (Var->isUsableInConstantExpressions(SemaRef.Context) || + (!Var->getType()->isReferenceType() && PotentialCaptureLSI && + PotentialCaptureLSI->AfterParameterList))) SemaRef.MaybeODRUseExprs.insert(E); else MarkVarDeclODRUsed(Var, Loc, SemaRef); break; + } case OdrUseContext::Dependent: // If this is a dependent context, we don't need to mark variables as diff --git a/clang/lib/Sema/SemaExprCXX.cpp b/clang/lib/Sema/SemaExprCXX.cpp index a76146a8d914f..907aab2e50d11 100644 --- a/clang/lib/Sema/SemaExprCXX.cpp +++ b/clang/lib/Sema/SemaExprCXX.cpp @@ -7644,6 +7644,11 @@ ExprResult Sema::IgnoredValueConversions(Expr *E) { return E; E = Res.get(); } else { + ExprResult Res = CheckDiscardedValueExpression(E); + if (Res.isInvalid()) + return E; + E = Res.get(); + // Per C++2a [expr.ass]p5, a volatile assignment is not deprecated if // it occurs as a discarded-value expression. CheckUnusedVolatileAssignment(E); @@ -7768,7 +7773,7 @@ static void CheckIfAnyEnclosingLambdasMustCaptureAnyPotentialCaptures( // All the potentially captureable variables in the current nested // lambda (within a generic outer lambda), must be captured by an // outer lambda that is enclosed within a non-dependent context. - CurrentLSI->visitPotentialCaptures([&](ValueDecl *Var, Expr *VarExpr) { + auto CheckCapture = [&](ValueDecl *Var, Expr *VarExpr) { // If the variable is clearly identified as non-odr-used and the full // expression is not instantiation dependent, only then do we not // need to check enclosing lambda's for speculative captures. @@ -7780,14 +7785,31 @@ static void CheckIfAnyEnclosingLambdasMustCaptureAnyPotentialCaptures( // (void) +x + a; // }; // } - if (CurrentLSI->isVariableExprMarkedAsNonODRUsed(VarExpr) && - !IsFullExprInstantiationDependent) - return; - VarDecl *UnderlyingVar = Var->getPotentiallyDecomposedVarDecl(); if (!UnderlyingVar) return; + if (CurrentLSI->isVariableExprMarkedAsNonODRUsed(VarExpr) && + (!IsFullExprInstantiationDependent || + CurrentLSI->isVariableExprMarkedAsDiscarded(VarExpr))) { + // Preserve Clang's existing implicit-capture behavior for lambdas with + // a capture-default. The discarded-use exception suppresses a capture + // diagnostic for [], but does not make [=] or [&] closures empty. + if (!CurrentLSI->isVariableExprMarkedAsDiscarded(VarExpr)) + return; + if (CurrentLSI->ImpCaptureStyle == CapturingScopeInfo::ImpCap_None) + return; + if (UnderlyingVar->isUsableInConstantExpressions(S.Context)) + return; + + QualType CaptureType, DeclRefType; + S.tryCaptureVariable(Var, VarExpr->getExprLoc(), TryCaptureKind::Implicit, + /*EllipsisLoc=*/SourceLocation(), + /*BuildAndDiagnose=*/true, CaptureType, DeclRefType, + nullptr); + return; + } + // If we have a capture-capable lambda for the variable, go ahead and // capture the variable in that lambda (and all its enclosing lambdas). if (const UnsignedOrNone Index = @@ -7817,7 +7839,8 @@ static void CheckIfAnyEnclosingLambdasMustCaptureAnyPotentialCaptures( DeclRefType, nullptr); } } - }); + }; + CurrentLSI->visitPotentialCaptures(CheckCapture); // Check if 'this' needs to be captured. if (CurrentLSI->hasPotentialThisCapture()) { diff --git a/clang/test/CXX/basic/basic.def.odr/p2.cpp b/clang/test/CXX/basic/basic.def.odr/p2.cpp index 0ffd08c924fbb..a57aed5f5f054 100644 --- a/clang/test/CXX/basic/basic.def.odr/p2.cpp +++ b/clang/test/CXX/basic/basic.def.odr/p2.cpp @@ -1,11 +1,12 @@ -// RUN: %clang_cc1 -std=c++98 %s -Wno-unused -verify +// RUN: %clang_cc1 -std=c++98 %s -Wno-unused -verify=cxx98 // RUN: %clang_cc1 -std=c++11 %s -Wno-unused -verify // RUN: %clang_cc1 -std=c++2a %s -Wno-unused -verify +// cxx98-no-diagnostics void use(int); void f() { - const int a = 1; // expected-note {{here}} + const int a = 1; #if __cplusplus >= 201103L constexpr int arr[3] = {1, 2, 3}; // expected-note 2{{here}} @@ -57,9 +58,7 @@ void f() { // comma expression use((i, a)); - // FIXME: This is not an odr-use because it is a discarded-value - // expression applied to an expression whose potential result is 'a'. - use((a, a)); // expected-error {{reference to local variable}} + use((a, a)); // (and combinations thereof) use(a ? (i, a) : a); diff --git a/clang/test/SemaCXX/lambda-expressions-gh127086.cpp b/clang/test/SemaCXX/lambda-expressions-gh127086.cpp new file mode 100644 index 0000000000000..7190af54283bd --- /dev/null +++ b/clang/test/SemaCXX/lambda-expressions-gh127086.cpp @@ -0,0 +1,88 @@ +// RUN: %clang_cc1 -std=c++20 -fsyntax-only -verify -Wno-unused-value %s +// expected-no-diagnostics + +void discarded_value() { + constexpr bool b = true; + [] { b; }; + [] { static_cast<void>(b); }; + [] { +b; }; + [] { static_cast<bool>(b); }; + + int i; + [] { i; }; + + enum E {}; + const auto e = static_cast<E>(42); + [] { e; }; +} + +enum NonConstantE {}; +NonConstantE make_enum(); + +void discarded_nonconstant_enum() { + const auto e = make_enum(); + [] { e; }; +} + +struct PotentialResults { + int x; + mutable int y; +}; + +void side_effect(); + +void discarded_potential_results() { + PotentialResults object{}; + int array[1]{}; + int i = 0; + + [] { (object); }; + [] { object.x; }; + [] { object.*&PotentialResults::x; }; + [] { true ? object.x : object.y; }; + [] { static_cast<void>(object.x); }; + [] { array[0]; }; + [] { (side_effect(), i); }; +} + +template <typename T> +void dependent_parameter(T t) { + [] { t; }; +} + +void generic_lambda_parameter() { + [](auto t) { + [] { t; }; + }(0); +} + +void capture_default(int i) { + auto by_copy = [=] { i; }; + static_assert(sizeof(by_copy) >= sizeof(i)); + + constexpr int constant = 42; + auto without_capture = [=] { constant; }; + static_assert(sizeof(without_capture) == 1); +} + +template <typename T> +void dependent_capture_default(T t) { + auto l = [=](auto) { t; }; + static_assert(sizeof(l) >= sizeof(T)); + l(0); +} + +struct Noncopyable { + constexpr Noncopyable() = default; + Noncopyable(const Noncopyable &) = delete; +}; + +template <typename T> +void dependent_discarded_constant() { + constexpr Noncopyable n; + [=](auto) { n; }(T()); +} + +template void dependent_parameter<int>(int); +template void dependent_capture_default<int>(int); +template void dependent_discarded_constant<int>(); >From 1a4d2d30290824b16fac276ce4642a1243f23299 Mon Sep 17 00:00:00 2001 From: Tiago Macarios <[email protected]> Date: Mon, 10 Aug 2026 15:33:10 -0700 Subject: [PATCH 3/4] [clang] Isolate potential captures by evaluation context A full-expression includes the conversions and initialization required by its surrounding construct, and its evaluation can include subexpressions that are not lexically part of it: https://eel.is/c++draft/intro.execution#5.2 Template-argument checking can therefore introduce nested full-expression boundaries while determining a constant template argument: https://eel.is/c++draft/temp.arg.nontype#2 Snapshot the potential variable captures and the location and count of repeated potential this captures in each ExpressionEvaluationContextRecord. When a nested context finishes, process only the suffix introduced by that context, then restore the outer prefix, location, and count. This keeps the local entities potentially referenced under the lambda rules associated with the correct full-expression: https://eel.is/c++draft/expr.prim.lambda.capture#7 Add ordinary template-call, user-defined-literal, dependent-initializer, and dependent-conditional regressions for these nested boundaries. Co-authored-by: Copilot <[email protected]> Copilot-Session: c3177956-d545-4f54-92ee-af0a5a8e5046 --- clang/include/clang/Sema/ScopeInfo.h | 29 ++++++++---- clang/include/clang/Sema/Sema.h | 5 ++ clang/lib/Sema/ScopeInfo.cpp | 5 +- clang/lib/Sema/SemaExpr.cpp | 10 ++++ clang/lib/Sema/SemaExprCXX.cpp | 31 +++++++++---- .../SemaCXX/lambda-expressions-gh127086.cpp | 46 +++++++++++++++++++ 6 files changed, 108 insertions(+), 18 deletions(-) diff --git a/clang/include/clang/Sema/ScopeInfo.h b/clang/include/clang/Sema/ScopeInfo.h index 6e5da3cc84910..33c9c64003db9 100644 --- a/clang/include/clang/Sema/ScopeInfo.h +++ b/clang/include/clang/Sema/ScopeInfo.h @@ -956,6 +956,7 @@ class LambdaScopeInfo final : llvm::SmallVector<ShadowedOuterDecl, 4> ShadowingDecls; SourceLocation PotentialThisCaptureLocation; + unsigned NumPotentialThisCaptures = 0; /// Variables that are potentially ODR-used in CUDA/HIP. llvm::SmallPtrSet<VarDecl *, 4> CUDAPotentialODRUsedVars; @@ -1005,11 +1006,10 @@ class LambdaScopeInfo final : void addPotentialThisCapture(SourceLocation Loc) { PotentialThisCaptureLocation = Loc; + ++NumPotentialThisCaptures; } - bool hasPotentialThisCapture() const { - return PotentialThisCaptureLocation.isValid(); - } + bool hasPotentialThisCapture() const { return NumPotentialThisCaptures != 0; } /// Mark a variable's reference in a lambda as non-odr using. /// @@ -1077,18 +1077,31 @@ class LambdaScopeInfo final : void clearPotentialCaptures() { PotentiallyCapturingExprs.clear(); PotentialThisCaptureLocation = SourceLocation(); + NumPotentialThisCaptures = 0; + } + void clearPotentialCaptures(unsigned NumVariableCaptures, + unsigned NumThisCaptures, + SourceLocation ThisCaptureLocation) { + PotentiallyCapturingExprs.resize(NumVariableCaptures); + NumPotentialThisCaptures = NumThisCaptures; + PotentialThisCaptureLocation = ThisCaptureLocation; } unsigned getNumPotentialVariableCaptures() const { return PotentiallyCapturingExprs.size(); } + unsigned getNumPotentialThisCaptures() const { + return NumPotentialThisCaptures; + } - bool hasPotentialCaptures() const { - return getNumPotentialVariableCaptures() || - PotentialThisCaptureLocation.isValid(); + bool hasPotentialCaptures(unsigned NumVariableCaptures = 0, + unsigned NumThisCaptures = 0) const { + return getNumPotentialVariableCaptures() != NumVariableCaptures || + getNumPotentialThisCaptures() != NumThisCaptures; } - void visitPotentialCaptures( - llvm::function_ref<void(ValueDecl *, Expr *)> Callback) const; + void + visitPotentialCaptures(llvm::function_ref<void(ValueDecl *, Expr *)> Callback, + unsigned FirstCapture = 0) const; bool lambdaCaptureShouldBeConst() const; }; diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index b05ba031b49cd..42c6850584dc1 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -6881,6 +6881,11 @@ class Sema final : public SemaBase { MaybeODRUseExprSet SavedMaybeODRUseExprs; + sema::LambdaScopeInfo *PotentialCaptureContext = nullptr; + unsigned NumPotentialVariableCaptures = 0; + unsigned NumPotentialThisCaptures = 0; + SourceLocation PotentialThisCaptureLocation; + /// The lambdas that are present within this context, if it /// is indeed an unevaluated context. SmallVector<LambdaExpr *, 2> Lambdas; diff --git a/clang/lib/Sema/ScopeInfo.cpp b/clang/lib/Sema/ScopeInfo.cpp index d089836fa36dd..b689a0ae2cbaf 100644 --- a/clang/lib/Sema/ScopeInfo.cpp +++ b/clang/lib/Sema/ScopeInfo.cpp @@ -233,8 +233,9 @@ bool CapturingScopeInfo::isVLATypeCaptured(const VariableArrayType *VAT) const { } void LambdaScopeInfo::visitPotentialCaptures( - llvm::function_ref<void(ValueDecl *, Expr *)> Callback) const { - for (Expr *E : PotentiallyCapturingExprs) { + llvm::function_ref<void(ValueDecl *, Expr *)> Callback, + unsigned FirstCapture) const { + for (Expr *E : llvm::drop_begin(PotentiallyCapturingExprs, FirstCapture)) { if (auto *DRE = dyn_cast<DeclRefExpr>(E)) { Callback(cast<ValueDecl>(DRE->getFoundDecl()), E); } else if (auto *ME = dyn_cast<MemberExpr>(E)) { diff --git a/clang/lib/Sema/SemaExpr.cpp b/clang/lib/Sema/SemaExpr.cpp index 0168c7e886dc5..f76858268e14d 100644 --- a/clang/lib/Sema/SemaExpr.cpp +++ b/clang/lib/Sema/SemaExpr.cpp @@ -18241,6 +18241,16 @@ Sema::PushExpressionEvaluationContext( ExprEvalContexts.back().InImmediateEscalatingFunctionContext = Prev.InImmediateEscalatingFunctionContext; + if (LambdaScopeInfo *LSI = getCurLambda(/*IgnoreCapturedRegions=*/true)) { + ExprEvalContexts.back().PotentialCaptureContext = LSI; + ExprEvalContexts.back().NumPotentialVariableCaptures = + LSI->getNumPotentialVariableCaptures(); + ExprEvalContexts.back().NumPotentialThisCaptures = + LSI->getNumPotentialThisCaptures(); + ExprEvalContexts.back().PotentialThisCaptureLocation = + LSI->PotentialThisCaptureLocation; + } + Cleanup.reset(); if (!MaybeODRUseExprs.empty()) std::swap(MaybeODRUseExprs, ExprEvalContexts.back().SavedMaybeODRUseExprs); diff --git a/clang/lib/Sema/SemaExprCXX.cpp b/clang/lib/Sema/SemaExprCXX.cpp index 907aab2e50d11..a7bfc394edfd3 100644 --- a/clang/lib/Sema/SemaExprCXX.cpp +++ b/clang/lib/Sema/SemaExprCXX.cpp @@ -7756,7 +7756,9 @@ static inline bool VariableCanNeverBeAConstantExpression(VarDecl *Var, /// need to be captured. static void CheckIfAnyEnclosingLambdasMustCaptureAnyPotentialCaptures( - Expr *const FE, LambdaScopeInfo *const CurrentLSI, Sema &S) { + Expr *const FE, LambdaScopeInfo *const CurrentLSI, Sema &S, + unsigned FirstVariableCapture, unsigned FirstThisCapture, + SourceLocation SavedThisCaptureLocation) { assert(!S.isUnevaluatedContext()); #ifndef NDEBUG @@ -7840,10 +7842,10 @@ static void CheckIfAnyEnclosingLambdasMustCaptureAnyPotentialCaptures( } } }; - CurrentLSI->visitPotentialCaptures(CheckCapture); + CurrentLSI->visitPotentialCaptures(CheckCapture, FirstVariableCapture); // Check if 'this' needs to be captured. - if (CurrentLSI->hasPotentialThisCapture()) { + if (CurrentLSI->getNumPotentialThisCaptures() != FirstThisCapture) { // If we have a capture-capable lambda for 'this', go ahead and capture // 'this' in that lambda (and all its enclosing lambdas). if (const UnsignedOrNone Index = @@ -7857,7 +7859,8 @@ static void CheckIfAnyEnclosingLambdasMustCaptureAnyPotentialCaptures( } // Reset all the potential captures at the end of each full-expression. - CurrentLSI->clearPotentialCaptures(); + CurrentLSI->clearPotentialCaptures(FirstVariableCapture, FirstThisCapture, + SavedThisCaptureLocation); } ExprResult Sema::ActOnFinishFullExpr(Expr *FE, SourceLocation CC, @@ -7946,10 +7949,22 @@ ExprResult Sema::ActOnFinishFullExpr(Expr *FE, SourceLocation CC, while (isa_and_nonnull<CapturedDecl>(DC)) DC = DC->getParent(); const bool IsInLambdaDeclContext = isLambdaCallOperator(DC); - if (IsInLambdaDeclContext && CurrentLSI && - CurrentLSI->hasPotentialCaptures() && !FullExpr.isInvalid()) - CheckIfAnyEnclosingLambdasMustCaptureAnyPotentialCaptures(FE, CurrentLSI, - *this); + if (IsInLambdaDeclContext && CurrentLSI && !FullExpr.isInvalid()) { + const ExpressionEvaluationContextRecord &Rec = currentEvaluationContext(); + const bool IsSameCaptureContext = Rec.PotentialCaptureContext == CurrentLSI; + const unsigned FirstVariableCapture = + IsSameCaptureContext ? Rec.NumPotentialVariableCaptures : 0; + const unsigned FirstThisCapture = + IsSameCaptureContext ? Rec.NumPotentialThisCaptures : 0; + const SourceLocation SavedThisCaptureLocation = + IsSameCaptureContext ? Rec.PotentialThisCaptureLocation + : SourceLocation(); + if (CurrentLSI->hasPotentialCaptures(FirstVariableCapture, + FirstThisCapture)) + CheckIfAnyEnclosingLambdasMustCaptureAnyPotentialCaptures( + FE, CurrentLSI, *this, FirstVariableCapture, FirstThisCapture, + SavedThisCaptureLocation); + } return MaybeCreateExprWithCleanups(FullExpr); } diff --git a/clang/test/SemaCXX/lambda-expressions-gh127086.cpp b/clang/test/SemaCXX/lambda-expressions-gh127086.cpp index 7190af54283bd..1aba12b612416 100644 --- a/clang/test/SemaCXX/lambda-expressions-gh127086.cpp +++ b/clang/test/SemaCXX/lambda-expressions-gh127086.cpp @@ -45,6 +45,23 @@ void discarded_potential_results() { [] { (side_effect(), i); }; } +template <int> +void templ() {} + +template <typename> +void template_argument() { + constexpr bool b = true; + [] { + b, templ<0>(); + }; +} + +template <typename T> +void dependent_initializer() { + constexpr bool b = T::b; + [] { b; }; +} + template <typename T> void dependent_parameter(T t) { [] { t; }; @@ -83,6 +100,35 @@ void dependent_discarded_constant() { [=](auto) { n; }(T()); } +template <char...> +int operator""_literal() { + return 0; +} + +template <typename T> +void literal_template_argument() { + constexpr bool b = T::b; + [] { + b, 0_literal; + }; +} + +template <typename T> +void dependent_conditional() { + constexpr bool b = T::b; + [] { + b ? 0_literal : 0; + }; +} + +struct S { + static constexpr bool b = true; +}; + +template void template_argument<void>(); +template void dependent_initializer<S>(); template void dependent_parameter<int>(int); template void dependent_capture_default<int>(int); template void dependent_discarded_constant<int>(); +template void literal_template_argument<S>(); +template void dependent_conditional<S>(); >From ce591f17a54e262a3ef5f3a0a29baf3f489784ce Mon Sep 17 00:00:00 2001 From: Tiago Macarios <[email protected]> Date: Mon, 10 Aug 2026 15:33:33 -0700 Subject: [PATCH 4/4] [clang] Handle discarded potential results in extension expressions Fold-expression syntax and expansion rules preserve which operand is syntactically rightmost: https://eel.is/c++draft/expr.prim.fold#1 https://eel.is/c++draft/expr.prim.fold#4 Because a comma expression evaluates left then right and has the right operand's result, and that right operand supplies its potential results, comma CXXFoldExpr rebuilding follows the syntactic rightmost operand: https://eel.is/c++draft/expr.comma#1 https://eel.is/c++draft/basic.def.odr#3.8 GNU a ?: b is an extension. Model its potential results analogously to a conditional, but rebuild only b: a is both the common result operand and the expression evaluated as the condition, so its use cannot be discarded: https://eel.is/c++draft/basic.def.odr#3.7 https://eel.is/c++draft/expr.cond#1 Add lambda regressions for comma folds and GNU binary conditional operators. Co-authored-by: Copilot <[email protected]> Copilot-Session: c3177956-d545-4f54-92ee-af0a5a8e5046 --- clang/docs/ReleaseNotes.md | 9 +++++ clang/lib/Sema/SemaExpr.cpp | 38 +++++++++++++++++++ .../SemaCXX/lambda-expressions-gh127086.cpp | 20 ++++++++++ 3 files changed, 67 insertions(+) diff --git a/clang/docs/ReleaseNotes.md b/clang/docs/ReleaseNotes.md index d9b9c92950c98..c045120a81713 100644 --- a/clang/docs/ReleaseNotes.md +++ b/clang/docs/ReleaseNotes.md @@ -455,6 +455,15 @@ features cannot lower the translation-unit ABI level; affect C++26 constexpr structured bindings and expansion statements, but also affects some uses of plain structured bindings. (#GH211930) +- A variable that appears as the potential result of a discarded-value + expression is no longer treated as odr-used, so naming it in a lambda with no + capture-default is accepted and requires no capture, for example + `constexpr bool b = true; [] { b; };` and `[] { static_cast<void>(b); };`. + Previously the same lambda body was rejected while `+b` was accepted. This + also fixes such a use inside a template being reported against an unrelated + construct in the same full-expression, and fixes `__builtin_choose_expr` + rebuilding its right operand from its left operand. (#GH127086) + #### Bug Fixes to AST Handling - Fixed a non-deterministic ordering of unused local typedefs that made diff --git a/clang/lib/Sema/SemaExpr.cpp b/clang/lib/Sema/SemaExpr.cpp index f76858268e14d..d6af8a7fc28ad 100644 --- a/clang/lib/Sema/SemaExpr.cpp +++ b/clang/lib/Sema/SemaExpr.cpp @@ -20453,6 +20453,44 @@ static ExprResult rebuildPotentialResultsAsNonOdrUsed(Sema &S, Expr *E, CO->getCond(), LHS.get(), RHS.get()); } + // [Clang extension] + // -- If e is a GNU binary conditional expression, its false operand is a + // potential result. The common operand is also used as the condition, + // so it remains an odr-use. + case Expr::BinaryConditionalOperatorClass: { + auto *BCO = cast<BinaryConditionalOperator>(E); + ExprResult RHS = Rebuild(BCO->getFalseExpr()); + if (!RHS.isUsable()) + return RHS; + return new (S.Context) BinaryConditionalOperator( + BCO->getCommon(), BCO->getOpaqueValue(), BCO->getCond(), + BCO->getTrueExpr(), RHS.get(), BCO->getQuestionLoc(), + BCO->getColonLoc(), BCO->getType(), BCO->getValueKind(), + BCO->getObjectKind()); + } + + // [Clang extension] + // -- If e is a comma fold-expression, its rightmost operand is a + // potential result. + case Expr::CXXFoldExprClass: { + auto *FE = cast<CXXFoldExpr>(E); + if (FE->getOperator() != BO_Comma) + break; + + Expr *LHS = FE->getLHS(); + Expr *RHS = FE->getRHS(); + ExprResult Sub = Rebuild(RHS ? RHS : LHS); + if (!Sub.isUsable()) + return Sub; + if (RHS) + RHS = Sub.get(); + else + LHS = Sub.get(); + return S.BuildCXXFoldExpr(FE->getCallee(), FE->getLParenLoc(), LHS, + FE->getOperator(), FE->getEllipsisLoc(), RHS, + FE->getRParenLoc(), FE->getNumExpansions()); + } + // [Clang extension] // -- If e has the form __extension__ e1... case Expr::UnaryOperatorClass: { diff --git a/clang/test/SemaCXX/lambda-expressions-gh127086.cpp b/clang/test/SemaCXX/lambda-expressions-gh127086.cpp index 1aba12b612416..bf4e7029cb7d5 100644 --- a/clang/test/SemaCXX/lambda-expressions-gh127086.cpp +++ b/clang/test/SemaCXX/lambda-expressions-gh127086.cpp @@ -100,6 +100,24 @@ void dependent_discarded_constant() { [=](auto) { n; }(T()); } +template <typename... T> +void discarded_fold(T... t) { + [] { (t, ...); }; +} + +template <typename... T> +void nested_discarded_fold(T... t) { + [](auto... u) { + [] { (u, ...); }; + }(t...); +} + +int global; + +void discarded_binary_conditional(int i) { + [] { global ?: i; }; +} + template <char...> int operator""_literal() { return 0; @@ -130,5 +148,7 @@ template void dependent_initializer<S>(); template void dependent_parameter<int>(int); template void dependent_capture_default<int>(int); template void dependent_discarded_constant<int>(); +template void discarded_fold<int, int>(int, int); +template void nested_discarded_fold<int, int>(int, int); template void literal_template_argument<S>(); template void dependent_conditional<S>(); _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
