Skip to content

[clang] Fix discarded non-ODR-uses in lambdas - #215603

Open
tiagomacarios wants to merge 4 commits into
llvm:mainfrom
tiagomacarios:user/copilot/fix-gh127086-github
Open

[clang] Fix discarded non-ODR-uses in lambdas#215603
tiagomacarios wants to merge 4 commits into
llvm:mainfrom
tiagomacarios:user/copilot/fix-gh127086-github

Conversation

@tiagomacarios

@tiagomacarios tiagomacarios commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Fixes #127086.

Summary

  • Apply discarded-value non-ODR-use handling to ignored C++ expressions, allowing uncaptured constexpr and non-constant locals when their value or address is not required.
  • Preserve implicit captures for [=] and [&], including dependent lambdas, while keeping discarded constexpr objects non-capturing.
  • Scope pending lambda captures to the expression evaluation context so nested template-argument processing does not consume or diagnose captures belonging to an enclosing full-expression.
  • Preserve selected branches when rebuilding GNU __builtin_choose_expr, and handle comma folds plus GNU binary conditional potential results.
  • Add Sema, ODR, and code-generation regression coverage for ordinary locals, runtime-initialized enums, potential-result chains, templates, parameter packs, nested generic folds, dependent parameters, generic lambdas, and capture-default layout.

Commit structure

  1. Fix the pre-existing GNU choose-expression RHS rebuild bug.
  2. Implement discarded-value non-ODR-use and lambda capture semantics.
  3. Isolate pending captures by expression-evaluation context.
  4. Cover comma folds and GNU binary conditional expressions.

Each commit message links the relevant wording in https://eel.is/c++draft/.

Validation

  • Every commit builds independently with Debug Clang and passes its available focused tests.
  • The broader 1,385-test ODR/SemaCXX run has only the seven pre-existing baseline failures.
  • Built the patch as RelWithDebInfo/LTCG ship Clang; the exact enum reproducer compiles and all focused ship tests pass.
  • Clean-built LLVM with the resulting ship clang-cl binaries (2,768 build steps).
  • Independently reviewed by GPT-5.6 Sol xhigh and Gemini 3.1 Pro; all confirmed findings were resolved.
  • The four commits were replayed cleanly onto GitHub's main; one patch has adjusted surrounding assertion-guard context from upstream changes, with the fix logic unchanged.

@llvmorg-github-actions llvmorg-github-actions Bot added clang Clang issues not falling into any other category clang:frontend Language frontend issues, e.g. anything involving "Sema" labels Aug 11, 2026
@llvmorg-github-actions

Copy link
Copy Markdown

@llvm/pr-subscribers-clang

Author: Tiago (tiagomacarios)

Changes

Fixes #127086.

Summary

  • Apply discarded-value non-ODR-use handling to ignored C++ expressions, allowing uncaptured constexpr and non-constant locals when their value or address is not required.
  • Preserve implicit captures for [=] and [&], including dependent lambdas, while keeping discarded constexpr objects non-capturing.
  • Scope pending lambda captures to the expression evaluation context so nested template-argument processing does not consume or diagnose captures belonging to an enclosing full-expression.
  • Preserve selected branches when rebuilding GNU __builtin_choose_expr, and handle comma folds plus GNU binary conditional potential results.
  • Add Sema, ODR, and code-generation regression coverage for ordinary locals, runtime-initialized enums, potential-result chains, templates, parameter packs, nested generic folds, dependent parameters, generic lambdas, and capture-default layout.

Commit structure

  1. Fix the pre-existing GNU choose-expression RHS rebuild bug.
  2. Implement discarded-value non-ODR-use and lambda capture semantics.
  3. Isolate pending captures by expression-evaluation context.
  4. Cover comma folds and GNU binary conditional expressions.

Each commit message links the relevant wording in https://eel.is/c++draft/.

Validation

  • Every commit builds independently with Debug Clang and passes its available focused tests.
  • The broader 1,385-test ODR/SemaCXX run has only the seven pre-existing baseline failures.
  • Built the patch as RelWithDebInfo/LTCG ship Clang; the exact enum reproducer compiles and all focused ship tests pass.
  • Clean-built LLVM with the resulting ship clang-cl binaries (2,768 build steps).
  • Independently reviewed by GPT-5.6 Sol xhigh and Gemini 3.1 Pro; all confirmed findings were resolved.
  • The four commits were replayed cleanly onto GitHub's release/22.x; git range-diff reports all four patches as unchanged.

Patch is 26.48 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/215603.diff

8 Files Affected:

  • (modified) clang/include/clang/Sema/ScopeInfo.h (+36-9)
  • (modified) clang/include/clang/Sema/Sema.h (+6)
  • (modified) clang/lib/Sema/ScopeInfo.cpp (+3-2)
  • (modified) clang/lib/Sema/SemaExpr.cpp (+122-34)
  • (modified) clang/lib/Sema/SemaExprCXX.cpp (+53-14)
  • (modified) clang/test/CXX/basic/basic.def.odr/p2.cpp (+4-5)
  • (added) clang/test/CodeGenCXX/choose-expr-discarded.cpp (+15)
  • (added) clang/test/SemaCXX/lambda-expressions-gh127086.cpp (+154)
diff --git a/clang/include/clang/Sema/ScopeInfo.h b/clang/include/clang/Sema/ScopeInfo.h
index 4f4d38c961140..9c0b55dca2b9c 100644
--- a/clang/include/clang/Sema/ScopeInfo.h
+++ b/clang/include/clang/Sema/ScopeInfo.h
@@ -935,6 +935,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;
 
@@ -948,6 +953,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;
@@ -997,11 +1003,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.
   ///
@@ -1042,11 +1047,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) ||
@@ -1054,24 +1062,43 @@ 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);
   }
   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 d68ed7c75b86f..74857e9ccb7ee 100644
--- a/clang/include/clang/Sema/Sema.h
+++ b/clang/include/clang/Sema/Sema.h
@@ -6770,6 +6770,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;
@@ -7069,6 +7074,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/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 12681a1f0c73d..c478ffd4520e3 100644
--- a/clang/lib/Sema/SemaExpr.cpp
+++ b/clang/lib/Sema/SemaExpr.cpp
@@ -17878,6 +17878,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);
@@ -19902,9 +19912,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);
     }
   };
 
@@ -20041,6 +20068,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: {
@@ -20098,7 +20163,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();
 
@@ -20178,6 +20243,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;
@@ -20214,35 +20287,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());
   }
 }
 
@@ -20378,12 +20459,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))
@@ -20407,14 +20490,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 91967a7a9ff97..2b9c1aea8df3a 100644
--- a/clang/lib/Sema/SemaExprCXX.cpp
+++ b/clang/lib/Sema/SemaExprCXX.cpp
@@ -7459,6 +7459,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);
@@ -7566,7 +7571,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());
   assert(S.CurContext->isDependentContext());
@@ -7584,7 +7591,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.
@@ -7596,13 +7603,31 @@ static void CheckIfAnyEnclosingLambdasMustCaptureAnyPotentialCaptures(
     //     (void) +x + a;
     //   };
     // }
-    if (CurrentLSI->isVariableExprMarkedAsNonODRUsed(VarExpr) &&
-        !IsFullExprInstantiationDependent)
-      return;
+      VarDecl *UnderlyingVar = Var->getPotentiallyDecomposedVarDecl();
+      if (!UnderlyingVar)
+        return;
 
-    VarDecl *UnderlyingVar = Var->getPotentiallyDecomposedVarDecl();
-    if (!UnderlyingVar)
+      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).
@@ -7633,10 +7658,11 @@ static void CheckIfAnyEnclosingLambdasMustCaptureAnyPotentialCaptures(
                              DeclRefType, nullptr);
       }
     }
-  });
+  };
+  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 =
@@ -7650,7 +7676,8 @@ static void CheckIfAnyEnclosingLambdasMustCaptureAnyPotentialCaptures(
   }
 
   // Reset all the potential captures at the end of each full-expression.
-  CurrentLSI->clearPotentialCaptures();
+  CurrentLSI->clearPotentialCaptures(FirstVariableCapture, ...
[truncated]

tiagomacarios and others added 4 commits August 11, 2026 09:16
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 <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c3177956-d545-4f54-92ee-af0a5a8e5046
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 <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c3177956-d545-4f54-92ee-af0a5a8e5046
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 <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c3177956-d545-4f54-92ee-af0a5a8e5046
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 <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c3177956-d545-4f54-92ee-af0a5a8e5046
@tiagomacarios
tiagomacarios force-pushed the user/copilot/fix-gh127086-github branch from 93cea00 to 4940911 Compare August 11, 2026 16:17
@tiagomacarios
tiagomacarios changed the base branch from release/22.x to main August 11, 2026 16:18
@github-actions

Copy link
Copy Markdown

⚠️ C/C++ code formatter, clang-format found issues in your code. ⚠️

You can test this locally with the following command:
git-clang-format --diff origin/main HEAD --extensions cpp,h -- clang/test/CodeGenCXX/choose-expr-discarded.cpp clang/test/SemaCXX/lambda-expressions-gh127086.cpp clang/include/clang/Sema/ScopeInfo.h clang/include/clang/Sema/Sema.h clang/lib/Sema/ScopeInfo.cpp clang/lib/Sema/SemaExpr.cpp clang/lib/Sema/SemaExprCXX.cpp clang/test/CXX/basic/basic.def.odr/p2.cpp --diff_from_common_commit

⚠️
The reproduction instructions above might return results for more than one PR
in a stack if you are using a stacked PR workflow. You can limit the results by
changing origin/main to the base branch/commit you want to compare against.
⚠️

View the diff from clang-format here.
diff --git a/clang/lib/Sema/SemaExpr.cpp b/clang/lib/Sema/SemaExpr.cpp
index cec6d6b57..d6af8a7fc 100644
--- a/clang/lib/Sema/SemaExpr.cpp
+++ b/clang/lib/Sema/SemaExpr.cpp
@@ -20486,9 +20486,9 @@ static ExprResult rebuildPotentialResultsAsNonOdrUsed(Sema &S, Expr *E,
       RHS = Sub.get();
     else
       LHS = Sub.get();
-    return S.BuildCXXFoldExpr(
-        FE->getCallee(), FE->getLParenLoc(), LHS, FE->getOperator(),
-        FE->getEllipsisLoc(), RHS, FE->getRParenLoc(), FE->getNumExpansions());
+    return S.BuildCXXFoldExpr(FE->getCallee(), FE->getLParenLoc(), LHS,
+                              FE->getOperator(), FE->getEllipsisLoc(), RHS,
+                              FE->getRParenLoc(), FE->getNumExpansions());
   }
 
   // [Clang extension]
diff --git a/clang/lib/Sema/SemaExprCXX.cpp b/clang/lib/Sema/SemaExprCXX.cpp
index 2cb43e88e..a7bfc394e 100644
--- a/clang/lib/Sema/SemaExprCXX.cpp
+++ b/clang/lib/Sema/SemaExprCXX.cpp
@@ -7787,13 +7787,13 @@ static void CheckIfAnyEnclosingLambdasMustCaptureAnyPotentialCaptures(
     //     (void) +x + a;
     //   };
     // }
-      VarDecl *UnderlyingVar = Var->getPotentiallyDecomposedVarDecl();
-      if (!UnderlyingVar)
-        return;
+    VarDecl *UnderlyingVar = Var->getPotentiallyDecomposedVarDecl();
+    if (!UnderlyingVar)
+      return;
 
-      if (CurrentLSI->isVariableExprMarkedAsNonODRUsed(VarExpr) &&
-          (!IsFullExprInstantiationDependent ||
-           CurrentLSI->isVariableExprMarkedAsDiscarded(VarExpr))) {
+    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.
@@ -7805,8 +7805,7 @@ static void CheckIfAnyEnclosingLambdasMustCaptureAnyPotentialCaptures(
         return;
 
       QualType CaptureType, DeclRefType;
-      S.tryCaptureVariable(Var, VarExpr->getExprLoc(),
-                           TryCaptureKind::Implicit,
+      S.tryCaptureVariable(Var, VarExpr->getExprLoc(), TryCaptureKind::Implicit,
                            /*EllipsisLoc=*/SourceLocation(),
                            /*BuildAndDiagnose=*/true, CaptureType, DeclRefType,
                            nullptr);

@hubert-reinterpretcast

Copy link
Copy Markdown
Member
  1. Fix the pre-existing GNU choose-expression RHS rebuild bug.

    1. Implement discarded-value non-ODR-use and lambda capture semantics.

    2. Isolate pending captures by expression-evaluation context.

    3. Cover comma folds and GNU binary conditional expressions.

Each commit message links the relevant wording in https://eel.is/c++draft/.

@tiagomacarios, please confirm whether you have read https://llvm.org/docs/AIToolPolicy.html.
Note that, in the LLVM workflow, commit messages on the PR branch are transient in the Git history outside of the PR branch. References in the PR description to the transient commit messages are not sufficient.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

clang:frontend Language frontend issues, e.g. anything involving "Sema" clang Clang issues not falling into any other category

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Various issues with using uncaptured constexpr variable in lambda

2 participants