[clang] [Clang] Fix spurious "satisfaction of constraint ... depends on itself" from nested synthesized comparisons (PR #223289)

via cfe-commits cfe-commits at lists.llvm.org
Sun Sep 13 19:43:42 PDT 2026


https://github.com/GEJXD created https://github.com/llvm/llvm-project/pull/223289

Fixes #223220

This re-lands #221707 with a minimal fix for the regression it introduced,
which made comparing two `std::map` objects fail with:

```text
error: satisfaction of constraint 'requires { { __t < __u } -> __boolean_testable; { __u < __t } -> __boolean_testable; }' depends on itself
```

## Root cause

`TemplateInstantiator::instantiateMissingDeclsToScopeForConcepts` (added by
#221707) resolves a missing declaration by walking the
`LocalInstantiationScope` chain. After #221707 this walk returns mappings for
*all* declaration kinds, including function parameters.

When the constraint of one specialization of a function template is checked
while another specialization of the *same* template is still being checked
(`std::__detail::__synth3way_t` reached from `std::pair`'s rewritten
`operator<=>` inside `std::map`'s), the parameters of the inner
specialization are looked up through the outer scope chain and find the outer
specialization's parameter mapping. The inner constraint expression then
substitutes the outer (pair-typed) parameters, so the check re-enters an
identical constraint check and is reported as "depends on itself" (the
constraint-satisfaction stack contains the same constraint ID twice).

## Fix

In the outer-scope walk, skip `ParmVarDecl`s and fall through to on-demand
instantiation (CWG2770), which always builds a parameter bound to the
*current* specialization. Non-parameter declarations (e.g. the typedefs that
#221707 intended to make visible in the current scope; see #GH198052,
#GH209632) still reuse outer-scope mappings as before.

This matches the intent stated in the original change ("we don't want clashes
of instantiated function parameters").

## Regression test

`clang/test/SemaTemplate/concepts-nested-synth.cpp` reproduces the
`std::__detail::__synth3way_t` structure instantiated from the `operator<=>`
of `std::map` and `std::pair` with no standard library, exercising the exact
nested-constraint path:

- `Map::operator<=>` returns `synth_t<Pair<const K, V>>` → outer
  `Synth::operator()<Pair,...>` constraint check active
- inside its requirement, `lhs < rhs` resolves through `Pair`'s rewritten
  `operator<=>` whose return type instantiates `synth_t<K, K>` → inner
  `Synth::operator()<K, K>` check nested in the outer one

## Validation

- Fails on the re-landed commit ("depends on itself"), passes with the fix
- `check-clang` (full clang test suite): 0 failures
- `git clang-format` and `git diff --check`: clean

>From 30bbf3987a97fee8d76fceffbb71ee394f6a49c2 Mon Sep 17 00:00:00 2001
From: Younan Zhang <zyn7109 at gmail.com>
Date: Sat, 12 Sep 2026 00:52:36 +0800
Subject: [PATCH 1/3] [Clang] Refactor instantiation of declarations within
 concepts (#221707)

After implementation of CWG2369, declarations are instantiated on demand
when evaluating a concept.

This was hinged on TemplateInstantiator, where we intercepted most calls
that need an instantiated declaration. This is almost identity to
SetupConstraintScope, which tries to re-instantiate any declarations to
the current scope. This patch removes SetupConstraintScope because we
can instantiate anything on demand.

The concept normalization patch brings us more troubles when we have to
deal with sugars: declarations instantiated outside of the scope (e.g. a
typedef declaration) are never added/instantiated during instantiation
of concepts: this patch makes that function also look up outer scopes
for those declarations. Note that we couldn't simply make the scope of
concepts 'transparent', since we don't want clashes of instantiated
function parameters.

Also also this added a call to instantiateMissingDeclsToScopeForConcepts
for C++26 fold expressions, before checking any pack sizes.

Fixes https://github.com/llvm/llvm-project/issues/198052
Fixes https://github.com/llvm/llvm-project/issues/209632
---
 clang/docs/ReleaseNotes.md                  |   3 +
 clang/include/clang/Sema/Sema.h             |  26 +--
 clang/include/clang/Sema/Template.h         |   2 +
 clang/lib/Sema/SemaConcept.cpp              | 176 ++++----------------
 clang/lib/Sema/SemaTemplateInstantiate.cpp  | 114 +++++++++----
 clang/test/SemaCXX/cxx2c-fold-exprs.cpp     |  25 +++
 clang/test/SemaTemplate/concepts-lambda.cpp |  16 ++
 7 files changed, 164 insertions(+), 198 deletions(-)

diff --git a/clang/docs/ReleaseNotes.md b/clang/docs/ReleaseNotes.md
index 043a0ddae2a6c..fc7ab3efa9731 100644
--- a/clang/docs/ReleaseNotes.md
+++ b/clang/docs/ReleaseNotes.md
@@ -589,6 +589,9 @@ features cannot lower the translation-unit ABI level;
 - Fixed a crash when module directive export module foo not following a
   semicolon and there are no rest pp-tokens in current module file. (#GH187771)
 
+- Fixed concept evaluation bugs where some declarations were not added to
+  the current instantiation scope. (#GH198052), (#GH209632)
+
 - Fixed a crash when a lambda parameter pack was given a default argument that
   is a pack expansion referencing an enclosing function's parameter pack (e.g.
   `[](Types... = args...) {}`). Clang now diagnoses the illegal default
diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h
index 4ff4c669a6b70..fc8da0ed56005 100644
--- a/clang/include/clang/Sema/Sema.h
+++ b/clang/include/clang/Sema/Sema.h
@@ -15129,11 +15129,17 @@ class Sema final : public SemaBase {
       const NamedDecl *D1, ArrayRef<AssociatedConstraint> AC1,
       const NamedDecl *D2, ArrayRef<AssociatedConstraint> AC2);
 
+private:
+  friend class ConstraintSatisfactionChecker;
+  friend class SubstituteParameterMappings;
+
+  UnsignedOrNone EvaluateFoldExpandedConstraintSize(
+      const Expr *Pattern, const MultiLevelTemplateArgumentList &MLTAL);
+
   /// Cache the satisfaction of an atomic constraint.
   /// The key is based on the unsubstituted expression and the parameter
   /// mapping. This lets us not substituting the mapping more than once,
   /// which is (very!) expensive.
-  /// FIXME: this should be private.
   llvm::DenseMap<llvm::FoldingSetNodeID,
                  UnsubstitutedConstraintSatisfactionCacheResult>
       UnsubstitutedConstraintSatisfactionCache;
@@ -15145,7 +15151,6 @@ class Sema final : public SemaBase {
   llvm::DenseMap<llvm::FoldingSetNodeID, TemplateArgumentLoc>
       *CurrentCachedTemplateArgs = nullptr;
 
-private:
   /// Caches pairs of template-like decls whose associated constraints were
   /// checked for subsumption and whether or not the first's constraints did in
   /// fact subsume the second's.
@@ -15166,23 +15171,6 @@ class Sema final : public SemaBase {
   // The current stack of constraint satisfactions, so we can exit-early.
   llvm::SmallVector<SatisfactionStackEntryTy, 10> SatisfactionStack;
 
-  /// Used by SetupConstraintCheckingTemplateArgumentsAndScope to set up the
-  /// LocalInstantiationScope of the current non-lambda function. For lambdas,
-  /// use LambdaScopeForCallOperatorInstantiationRAII.
-  bool
-  SetupConstraintScope(FunctionDecl *FD,
-                       std::optional<ArrayRef<TemplateArgument>> TemplateArgs,
-                       const MultiLevelTemplateArgumentList &MLTAL,
-                       LocalInstantiationScope &Scope);
-
-  /// Used during constraint checking, sets up the constraint template argument
-  /// lists, and calls SetupConstraintScope to set up the
-  /// LocalInstantiationScope to have the proper set of ParVarDecls configured.
-  std::optional<MultiLevelTemplateArgumentList>
-  SetupConstraintCheckingTemplateArgumentsAndScope(
-      FunctionDecl *FD, std::optional<ArrayRef<TemplateArgument>> TemplateArgs,
-      LocalInstantiationScope &Scope);
-
   ///@}
 
   //
diff --git a/clang/include/clang/Sema/Template.h b/clang/include/clang/Sema/Template.h
index 50e950e56c6ca..818528744511d 100644
--- a/clang/include/clang/Sema/Template.h
+++ b/clang/include/clang/Sema/Template.h
@@ -540,6 +540,8 @@ enum class TemplateSubstitutionKind : char {
     llvm::PointerUnion<Decl *, DeclArgumentPack *> *
     getInstantiationOfIfExists(const Decl *D);
 
+    LocalInstantiationScope *getOuterScope() const { return Outer; }
+
     void InstantiatedLocal(const Decl *D, Decl *Inst);
     void InstantiatedLocalPackArg(const Decl *D, VarDecl *Inst);
     void MakeInstantiatedLocalArgPack(const Decl *D);
diff --git a/clang/lib/Sema/SemaConcept.cpp b/clang/lib/Sema/SemaConcept.cpp
index ee17f826dc32c..0122e920e5d89 100644
--- a/clang/lib/Sema/SemaConcept.cpp
+++ b/clang/lib/Sema/SemaConcept.cpp
@@ -549,7 +549,9 @@ class HashParameterMapping : public RecursiveASTVisitor<HashParameterMapping> {
     }
   }
 };
+} // namespace
 
+namespace clang {
 class ConstraintSatisfactionChecker {
   Sema &S;
   const NamedDecl *Template;
@@ -652,10 +654,6 @@ class ConstraintSatisfactionChecker {
   EvaluateAtomicConstraint(const Expr *AtomicExpr,
                            const MultiLevelTemplateArgumentList &MLTAL);
 
-  UnsignedOrNone EvaluateFoldExpandedConstraintSize(
-      const FoldExpandedConstraint &FE,
-      const MultiLevelTemplateArgumentList &MLTAL);
-
   // XXX: It is SLOW! Use it very carefully.
   std::optional<MultiLevelTemplateArgumentList> SubstitutionInTemplateArguments(
       const NormalizedConstraintWithParamMapping &Constraint,
@@ -700,7 +698,7 @@ class ConstraintSatisfactionChecker {
                       const MultiLevelTemplateArgumentList &MLTAL);
 };
 
-} // namespace
+} // namespace clang
 
 ExprResult ConstraintSatisfactionChecker::EvaluateAtomicConstraint(
     const Expr *AtomicExpr, const MultiLevelTemplateArgumentList &MLTAL) {
@@ -948,31 +946,6 @@ ExprResult ConstraintSatisfactionChecker::Evaluate(
   return PMCache.cache(EvaluateSlow(Constraint, MLTAL));
 }
 
-UnsignedOrNone
-ConstraintSatisfactionChecker::EvaluateFoldExpandedConstraintSize(
-    const FoldExpandedConstraint &FE,
-    const MultiLevelTemplateArgumentList &MLTAL) {
-
-  Expr *Pattern = const_cast<Expr *>(FE.getPattern());
-
-  SmallVector<UnexpandedParameterPack, 2> Unexpanded;
-  S.collectUnexpandedParameterPacks(Pattern, Unexpanded);
-  assert(!Unexpanded.empty() && "Pack expansion without parameter packs?");
-  bool Expand = true;
-  bool RetainExpansion = false;
-  UnsignedOrNone NumExpansions(std::nullopt);
-  if (S.CheckParameterPacksForExpansion(
-          Pattern->getExprLoc(), Pattern->getSourceRange(), Unexpanded, MLTAL,
-          /*FailOnPackProducingTemplates=*/false, Expand, RetainExpansion,
-          NumExpansions, /*Diagnose=*/false) ||
-      !Expand || RetainExpansion)
-    return std::nullopt;
-
-  if (NumExpansions && S.getLangOpts().BracketDepth < *NumExpansions)
-    return std::nullopt;
-  return NumExpansions;
-}
-
 ExprResult ConstraintSatisfactionChecker::EvaluateSlow(
     const FoldExpandedConstraint &Constraint,
     const MultiLevelTemplateArgumentList &MLTAL) {
@@ -993,9 +966,15 @@ ExprResult ConstraintSatisfactionChecker::EvaluateSlow(
     return ExprError();
   }
 
-  ExprResult Out;
-  UnsignedOrNone NumExpansions =
-      EvaluateFoldExpandedConstraintSize(Constraint, *SubstitutedArgs);
+  UnsignedOrNone NumExpansions(std::nullopt);
+  {
+    Sema::InstantiatingTemplate InstTemplate(
+        S, TemplateNameLoc,
+        Sema::InstantiatingTemplate::ConstraintSubstitution{},
+        const_cast<NamedDecl *>(Template), Constraint.getSourceRange());
+    NumExpansions = S.EvaluateFoldExpandedConstraintSize(
+        Constraint.getPattern(), *SubstitutedArgs);
+  }
   if (!NumExpansions)
     return ExprEmpty();
 
@@ -1004,6 +983,7 @@ ExprResult ConstraintSatisfactionChecker::EvaluateSlow(
     return ExprEmpty();
   }
 
+  ExprResult Out;
   for (unsigned I = 0; I < *NumExpansions; I++) {
     Sema::ArgPackSubstIndexRAII SubstIndex(S, I);
     Satisfaction.IsSatisfied = false;
@@ -1412,98 +1392,6 @@ SubstituteConceptsInConstraintExpression(Sema &S, const NamedDecl *D,
                                          MLTAL);
 }
 
-bool Sema::SetupConstraintScope(
-    FunctionDecl *FD, std::optional<ArrayRef<TemplateArgument>> TemplateArgs,
-    const MultiLevelTemplateArgumentList &MLTAL,
-    LocalInstantiationScope &Scope) {
-  assert(!isLambdaCallOperator(FD) &&
-         "Use LambdaScopeForCallOperatorInstantiationRAII to handle lambda "
-         "instantiations");
-  if (FD->isTemplateInstantiation() && FD->getPrimaryTemplate()) {
-    FunctionTemplateDecl *PrimaryTemplate = FD->getPrimaryTemplate();
-    InstantiatingTemplate Inst(
-        *this, FD->getPointOfInstantiation(),
-        Sema::InstantiatingTemplate::ConstraintsCheck{}, PrimaryTemplate,
-        TemplateArgs ? *TemplateArgs : ArrayRef<TemplateArgument>{},
-        SourceRange());
-    if (Inst.isInvalid())
-      return true;
-
-    // addInstantiatedParametersToScope creates a map of 'uninstantiated' to
-    // 'instantiated' parameters and adds it to the context. For the case where
-    // this function is a template being instantiated NOW, we also need to add
-    // the list of current template arguments to the list so that they also can
-    // be picked out of the map.
-    if (auto *SpecArgs = FD->getTemplateSpecializationArgs()) {
-      MultiLevelTemplateArgumentList JustTemplArgs(FD, SpecArgs->asArray(),
-                                                   /*Final=*/false);
-      if (addInstantiatedParametersToScope(
-              FD, PrimaryTemplate->getTemplatedDecl(), Scope, JustTemplArgs))
-        return true;
-    }
-
-    // If this is a member function, make sure we get the parameters that
-    // reference the original primary template.
-    if (FunctionTemplateDecl *FromMemTempl =
-            PrimaryTemplate->getInstantiatedFromMemberTemplate()) {
-      if (addInstantiatedParametersToScope(FD, FromMemTempl->getTemplatedDecl(),
-                                           Scope, MLTAL))
-        return true;
-    }
-
-    return false;
-  }
-
-  if (FD->getTemplatedKind() == FunctionDecl::TK_MemberSpecialization ||
-      FD->getTemplatedKind() == FunctionDecl::TK_DependentNonTemplate) {
-    FunctionDecl *InstantiatedFrom =
-        FD->getTemplatedKind() == FunctionDecl::TK_MemberSpecialization
-            ? FD->getInstantiatedFromMemberFunction()
-            : FD->getInstantiatedFromDecl();
-
-    InstantiatingTemplate Inst(
-        *this, FD->getPointOfInstantiation(),
-        Sema::InstantiatingTemplate::ConstraintsCheck{}, InstantiatedFrom,
-        TemplateArgs ? *TemplateArgs : ArrayRef<TemplateArgument>{},
-        SourceRange());
-    if (Inst.isInvalid())
-      return true;
-
-    // Case where this was not a template, but instantiated as a
-    // child-function.
-    if (addInstantiatedParametersToScope(FD, InstantiatedFrom, Scope, MLTAL))
-      return true;
-  }
-
-  return false;
-}
-
-// This function collects all of the template arguments for the purposes of
-// constraint-instantiation and checking.
-std::optional<MultiLevelTemplateArgumentList>
-Sema::SetupConstraintCheckingTemplateArgumentsAndScope(
-    FunctionDecl *FD, std::optional<ArrayRef<TemplateArgument>> TemplateArgs,
-    LocalInstantiationScope &Scope) {
-  MultiLevelTemplateArgumentList MLTAL;
-
-  // Collect the list of template arguments relative to the 'primary' template.
-  // We need the entire list, since the constraint is completely uninstantiated
-  // at this point.
-  MLTAL =
-      getTemplateInstantiationArgs(FD, FD->getLexicalDeclContext(),
-                                   /*Final=*/false, /*Innermost=*/std::nullopt,
-                                   /*RelativeToPrimary=*/true,
-                                   /*Pattern=*/nullptr,
-                                   /*ForConstraintInstantiation=*/true);
-  // Lambdas are handled by LambdaScopeForCallOperatorInstantiationRAII.
-  if (isLambdaCallOperator(FD))
-    return MLTAL;
-  if (SetupConstraintScope(FD, TemplateArgs, MLTAL, Scope))
-    return std::nullopt;
-
-  return MLTAL;
-}
-
 bool Sema::CheckFunctionConstraints(const FunctionDecl *FD,
                                     ConstraintSatisfaction &Satisfaction,
                                     SourceLocation UsageLoc,
@@ -1543,12 +1431,12 @@ bool Sema::CheckFunctionConstraints(const FunctionDecl *FD,
 
   ContextRAII SavedContext{*this, CtxToSave};
   LocalInstantiationScope Scope(*this, !ForOverloadResolution);
-  std::optional<MultiLevelTemplateArgumentList> MLTAL =
-      SetupConstraintCheckingTemplateArgumentsAndScope(
-          const_cast<FunctionDecl *>(FD), {}, Scope);
-
-  if (!MLTAL)
-    return true;
+  MultiLevelTemplateArgumentList MLTAL =
+      getTemplateInstantiationArgs(FD, FD->getLexicalDeclContext(),
+                                   /*Final=*/false, /*Innermost=*/std::nullopt,
+                                   /*RelativeToPrimary=*/true,
+                                   /*Pattern=*/nullptr,
+                                   /*ForConstraintInstantiation=*/true);
 
   Qualifiers ThisQuals;
   CXXRecordDecl *Record = nullptr;
@@ -1559,11 +1447,11 @@ bool Sema::CheckFunctionConstraints(const FunctionDecl *FD,
   CXXThisScopeRAII ThisScope(*this, Record, ThisQuals, Record != nullptr);
 
   LambdaScopeForCallOperatorInstantiationRAII LambdaScope(
-      *this, const_cast<FunctionDecl *>(FD), *MLTAL, Scope,
+      *this, const_cast<FunctionDecl *>(FD), MLTAL, Scope,
       ForOverloadResolution);
 
   return CheckConstraintSatisfaction(
-      FD, FD->getTrailingRequiresClause(), *MLTAL,
+      FD, FD->getTrailingRequiresClause(), MLTAL,
       SourceRange(UsageLoc.isValid() ? UsageLoc : FD->getLocation()),
       Satisfaction);
 }
@@ -1796,12 +1684,12 @@ bool Sema::CheckFunctionTemplateConstraints(
   Sema::ContextRAII savedContext(*this, Decl);
   LocalInstantiationScope Scope(*this);
 
-  std::optional<MultiLevelTemplateArgumentList> MLTAL =
-      SetupConstraintCheckingTemplateArgumentsAndScope(Decl, TemplateArgs,
-                                                       Scope);
-
-  if (!MLTAL)
-    return true;
+  MultiLevelTemplateArgumentList MLTAL =
+      getTemplateInstantiationArgs(Decl, Decl->getLexicalDeclContext(),
+                                   /*Final=*/false, /*Innermost=*/std::nullopt,
+                                   /*RelativeToPrimary=*/true,
+                                   /*Pattern=*/nullptr,
+                                   /*ForConstraintInstantiation=*/true);
 
   Qualifiers ThisQuals;
   CXXRecordDecl *Record = nullptr;
@@ -1811,10 +1699,10 @@ bool Sema::CheckFunctionTemplateConstraints(
   }
 
   CXXThisScopeRAII ThisScope(*this, Record, ThisQuals, Record != nullptr);
-  LambdaScopeForCallOperatorInstantiationRAII LambdaScope(*this, Decl, *MLTAL,
+  LambdaScopeForCallOperatorInstantiationRAII LambdaScope(*this, Decl, MLTAL,
                                                           Scope);
 
-  return CheckConstraintSatisfaction(Template, TemplateAC, *MLTAL,
+  return CheckConstraintSatisfaction(Template, TemplateAC, MLTAL,
                                      PointOfInstantiation, Satisfaction);
 }
 
@@ -2087,7 +1975,7 @@ void Sema::DiagnoseUnsatisfiedConstraint(
                                   ConstraintExpr->getBeginLoc(), First);
 }
 
-namespace {
+namespace clang {
 
 class SubstituteParameterMappings {
   Sema &SemaRef;
@@ -2125,6 +2013,8 @@ class SubstituteParameterMappings {
   bool substitute(NormalizedConstraint &N);
 };
 
+} // namespace clang
+
 void SubstituteParameterMappings::buildParameterMapping(
     NormalizedConstraintWithParamMapping &N) {
   TemplateParameterList *TemplateParams =
@@ -2418,8 +2308,6 @@ bool SubstituteParameterMappings::substitute(NormalizedConstraint &N) {
   llvm_unreachable("Unknown ConstraintKind enum");
 }
 
-} // namespace
-
 NormalizedConstraint *NormalizedConstraint::fromAssociatedConstraints(
     Sema &S, const NamedDecl *D, ArrayRef<AssociatedConstraint> ACs) {
   assert(ACs.size() != 0);
diff --git a/clang/lib/Sema/SemaTemplateInstantiate.cpp b/clang/lib/Sema/SemaTemplateInstantiate.cpp
index ffbe8bb0506bc..cadc5689cebc2 100644
--- a/clang/lib/Sema/SemaTemplateInstantiate.cpp
+++ b/clang/lib/Sema/SemaTemplateInstantiate.cpp
@@ -1324,11 +1324,10 @@ namespace {
     bool BailOutOnIncomplete;
 
     std::optional<llvm::FoldingSetNodeID> TemplateArgsHashValue;
+    llvm::DenseMap<llvm::FoldingSetNodeID, TemplateArgumentLoc>
+        *CurrentCachedTemplateArgs = nullptr;
 
-    // CWG2770: Function parameters should be instantiated when they are
-    // needed by a satisfaction check of an atomic constraint or
-    // (recursively) by another function parameter.
-    bool maybeInstantiateFunctionParameterToScope(ParmVarDecl *OldParm);
+    bool instantiateMissingDeclsToScopeForConcepts(Decl *D);
 
   public:
     typedef TreeTransform<TemplateInstantiator> inherited;
@@ -1358,12 +1357,14 @@ namespace {
     inline static struct ForConstraintSubstitution_t {
     } ForConstraintSubstitution;
 
-    TemplateInstantiator(ForParameterMappingSubstitution_t, Sema &SemaRef,
-                         SourceLocation Loc,
-                         const MultiLevelTemplateArgumentList &TemplateArgs)
+    TemplateInstantiator(
+        ForParameterMappingSubstitution_t, Sema &SemaRef, SourceLocation Loc,
+        const MultiLevelTemplateArgumentList &TemplateArgs,
+        llvm::DenseMap<llvm::FoldingSetNodeID, TemplateArgumentLoc> *Cache)
         : inherited(SemaRef), TemplateArgs(TemplateArgs), Loc(Loc),
-          EvaluateLambdaConstraint(true), BailOutOnIncomplete(false) {
-      if (!SemaRef.CurrentCachedTemplateArgs)
+          EvaluateLambdaConstraint(true), BailOutOnIncomplete(false),
+          CurrentCachedTemplateArgs(Cache) {
+      if (!Cache)
         return;
       auto &V = TemplateArgsHashValue.emplace();
       for (auto &Level : TemplateArgs)
@@ -1410,22 +1411,18 @@ namespace {
                                  ArrayRef<UnexpandedParameterPack> Unexpanded,
                                  bool FailOnPackProducingTemplates,
                                  bool &ShouldExpand, bool &RetainExpansion,
-                                 UnsignedOrNone &NumExpansions) {
-      if (SemaRef.CurrentInstantiationScope &&
-          (SemaRef.inConstraintSubstitution() ||
-           SemaRef.inParameterMappingSubstitution())) {
-        for (UnexpandedParameterPack ParmPack : Unexpanded) {
-          NamedDecl *VD = ParmPack.first.dyn_cast<NamedDecl *>();
-          if (auto *PVD = dyn_cast_if_present<ParmVarDecl>(VD);
-              PVD && maybeInstantiateFunctionParameterToScope(PVD))
-            return true;
-        }
+                                 UnsignedOrNone &NumExpansions,
+                                 bool Diagnose = true) {
+      for (UnexpandedParameterPack ParmPack : Unexpanded) {
+        if (instantiateMissingDeclsToScopeForConcepts(
+                dyn_cast<NamedDecl *>(ParmPack.first)))
+          return true;
       }
 
       return getSema().CheckParameterPacksForExpansion(
           EllipsisLoc, PatternRange, Unexpanded, TemplateArgs,
           FailOnPackProducingTemplates, ShouldExpand, RetainExpansion,
-          NumExpansions);
+          NumExpansions, Diagnose);
     }
 
     void ExpandingFunctionParameterPack(ParmVarDecl *Pack) {
@@ -1637,7 +1634,7 @@ namespace {
                                    TemplateArgumentLoc &Output,
                                    bool Uneval = false) {
       const TemplateArgument &Arg = Input.getArgument();
-      if (auto *Cache = SemaRef.CurrentCachedTemplateArgs;
+      if (auto *Cache = CurrentCachedTemplateArgs;
           Cache && TemplateArgsHashValue) {
         llvm::FoldingSetNodeID ID = *TemplateArgsHashValue;
         ID.AddInteger(SemaRef.ArgPackSubstIndex.toInternalRepresentation());
@@ -1980,11 +1977,7 @@ Decl *TemplateInstantiator::TransformDecl(SourceLocation Loc, Decl *D) {
     // template parameter.
   }
 
-  if (ParmVarDecl *PVD = dyn_cast<ParmVarDecl>(D);
-      PVD && SemaRef.CurrentInstantiationScope &&
-      (SemaRef.inConstraintSubstitution() ||
-       SemaRef.inParameterMappingSubstitution()) &&
-      maybeInstantiateFunctionParameterToScope(PVD))
+  if (instantiateMissingDeclsToScopeForConcepts(D))
     return nullptr;
 
   if (isa<CXXExpansionStmtDecl>(D)) {
@@ -1996,9 +1989,39 @@ Decl *TemplateInstantiator::TransformDecl(SourceLocation Loc, Decl *D) {
   return SemaRef.FindInstantiatedDecl(Loc, cast<NamedDecl>(D), TemplateArgs);
 }
 
-bool TemplateInstantiator::maybeInstantiateFunctionParameterToScope(
-    ParmVarDecl *OldParm) {
-  if (SemaRef.CurrentInstantiationScope->getInstantiationOfIfExists(OldParm))
+bool TemplateInstantiator::instantiateMissingDeclsToScopeForConcepts(Decl *D) {
+  if (!(D && (SemaRef.inConstraintSubstitution() ||
+              SemaRef.inParameterMappingSubstitution())))
+    return false;
+
+  auto *Current = SemaRef.CurrentInstantiationScope;
+  if (!Current)
+    return false;
+  if (Current->getInstantiationOfIfExists(D))
+    return false;
+
+  for (auto *Outer = Current->getOuterScope(); Outer;
+       Outer = Outer->getOuterScope()) {
+    auto *Pair = Outer->getInstantiationOfIfExists(D);
+    if (!Pair)
+      continue;
+
+    if (auto *InstD = dyn_cast<Decl *>(*Pair)) {
+      Current->InstantiatedLocal(D, InstD);
+    } else {
+      Current->MakeInstantiatedLocalArgPack(D);
+      auto *Pack = cast<LocalInstantiationScope::DeclArgumentPack *>(*Pair);
+      for (auto *VD : *Pack)
+        Current->InstantiatedLocal(D, VD);
+    }
+    return false;
+  }
+
+  // CWG2770: Function parameters should be instantiated when they are
+  // needed by a satisfaction check of an atomic constraint or
+  // (recursively) by another function parameter.
+  auto *OldParm = dyn_cast<ParmVarDecl>(D);
+  if (!OldParm)
     return false;
 
   if (!OldParm->isParameterPack())
@@ -2459,11 +2482,7 @@ TemplateInstantiator::TransformDeclRefExpr(DeclRefExpr *E) {
   // Handle references to function parameter packs.
   if (VarDecl *PD = dyn_cast<VarDecl>(D))
     if (PD->isParameterPack()) {
-      if (ParmVarDecl *PVD = dyn_cast<ParmVarDecl>(PD);
-          PVD && SemaRef.CurrentInstantiationScope &&
-          (SemaRef.inConstraintSubstitution() ||
-           SemaRef.inParameterMappingSubstitution()) &&
-          maybeInstantiateFunctionParameterToScope(PVD))
+      if (instantiateMissingDeclsToScopeForConcepts(PD))
         return ExprError();
 
       return TransformFunctionParmPackRefExpr(E, PD);
@@ -4486,10 +4505,35 @@ bool Sema::SubstTemplateArgumentsInParameterMapping(
     TemplateArgumentListInfo &Out) {
   TemplateInstantiator Instantiator(
       TemplateInstantiator::ForParameterMappingSubstitution, *this, BaseLoc,
-      TemplateArgs);
+      TemplateArgs, CurrentCachedTemplateArgs);
   return Instantiator.TransformTemplateArguments(Args.begin(), Args.end(), Out);
 }
 
+UnsignedOrNone Sema::EvaluateFoldExpandedConstraintSize(
+    const Expr *Pattern, const MultiLevelTemplateArgumentList &TemplateArgs) {
+  TemplateInstantiator Instantiator(
+      TemplateInstantiator::ForConstraintSubstitution, *this, TemplateArgs,
+      SourceLocation(), DeclarationName());
+
+  SmallVector<UnexpandedParameterPack, 2> Unexpanded;
+  collectUnexpandedParameterPacks(const_cast<Expr *>(Pattern), Unexpanded);
+  assert(!Unexpanded.empty() && "Pack expansion without parameter packs?");
+
+  bool Expand = true;
+  bool RetainExpansion = false;
+  UnsignedOrNone NumExpansions(std::nullopt);
+  if (Instantiator.TryExpandParameterPacks(
+          Pattern->getExprLoc(), Pattern->getSourceRange(), Unexpanded,
+          /*FailOnPackProducingTemplates=*/false, Expand, RetainExpansion,
+          NumExpansions, /*Diagnose=*/false) ||
+      !Expand || RetainExpansion)
+    return std::nullopt;
+
+  if (NumExpansions && getLangOpts().BracketDepth < *NumExpansions)
+    return std::nullopt;
+  return NumExpansions;
+}
+
 ExprResult
 Sema::SubstExpr(Expr *E, const MultiLevelTemplateArgumentList &TemplateArgs) {
   if (!E)
diff --git a/clang/test/SemaCXX/cxx2c-fold-exprs.cpp b/clang/test/SemaCXX/cxx2c-fold-exprs.cpp
index d3e681f22ab26..80c97253653e3 100644
--- a/clang/test/SemaCXX/cxx2c-fold-exprs.cpp
+++ b/clang/test/SemaCXX/cxx2c-fold-exprs.cpp
@@ -692,3 +692,28 @@ void g() {
 }
 
 }
+
+namespace GH198052 {
+
+template <class T, class U>
+concept is_same = __is_same(T, U);
+
+constexpr int NumberOfTrueInstances(auto... booleans)
+  requires (is_same<bool, decltype(booleans)> && ...)
+{
+  bool the_booleans[] = {booleans...};
+  int nrvo = 0;
+  for (bool a_boolean : the_booleans) {
+    if (a_boolean) nrvo += 1;
+  }
+  return nrvo;
+}
+
+constexpr bool a = true;
+constexpr bool b = false;
+constexpr bool c = true;
+constexpr bool d = false;
+constexpr int count = NumberOfTrueInstances(a, b, c, d);
+static_assert(count == 2);
+
+}
diff --git a/clang/test/SemaTemplate/concepts-lambda.cpp b/clang/test/SemaTemplate/concepts-lambda.cpp
index 2010a028fce4a..26deec7ad7777 100644
--- a/clang/test/SemaTemplate/concepts-lambda.cpp
+++ b/clang/test/SemaTemplate/concepts-lambda.cpp
@@ -493,3 +493,19 @@ static_assert(count_if_v_bad_2<L, double> == 111);
 static_assert(count_if_v_bad_2<L, char> == 111);
 
 }
+
+namespace GH209632 {
+
+template <class A, class B> concept same_as = __is_same(A, B);
+
+template <class NR> void f(NR) {
+  using N = NR;
+  auto inner = [](same_as<N> auto) {};
+  inner(N{});
+}
+
+void main() {
+  f(0);
+}
+
+}

>From cbb799673477e047b897c10fbc6e6e1241723243 Mon Sep 17 00:00:00 2001
From: Hsin <genesisjxd at gmail.com>
Date: Mon, 14 Sep 2026 01:08:48 +0800
Subject: [PATCH 2/3] [clang] Fix satisfaction of 'depends on itself' with
 nested synthesized constraints

Re-land companion for #221707; fixes #223220.

When the constraint of one specialization of a function template is
checked while another specialization of the same template is already
being checked (as happens for std::__detail::__synth3way_t instantiated
from the operator<=> of std::map and std::pair), a missing ParmVarDecl
could be resolved through the LocalInstantiationScope chain into the
parameter mapping of the *outer* specialization. The inner constraint
expression then got the outer specialization's (pair-typed) parameters
substituted in, so the check re-entered an identical constraint check and
was reported as 'satisfaction of constraint ... depends on itself'.

Only reuse outer-scope mappings for non-parameter declarations (the
typedefs #221707 originally aimed to make available); for ParmVarDecls
fall through to on-demand instantiation (CWG2770), which always builds a
parameter bound to the current specialization.
---
 clang/docs/ReleaseNotes.md                 | 6 ++++++
 clang/lib/Sema/SemaTemplateInstantiate.cpp | 8 ++++++++
 2 files changed, 14 insertions(+)

diff --git a/clang/docs/ReleaseNotes.md b/clang/docs/ReleaseNotes.md
index fc7ab3efa9731..f55d5570da375 100644
--- a/clang/docs/ReleaseNotes.md
+++ b/clang/docs/ReleaseNotes.md
@@ -592,6 +592,12 @@ features cannot lower the translation-unit ABI level;
 - Fixed concept evaluation bugs where some declarations were not added to
   the current instantiation scope. (#GH198052), (#GH209632)
 
+- Fixed a regression where the constraint of one specialization of a function
+  template could reuse the function parameters of an outer (still active)
+  specialization of the same template, causing a spurious
+  "satisfaction of constraint ... depends on itself" error for synthesized
+  three-way comparisons such as ``std::map``'s ``operator<=>``. (#GH223220)
+
 - Fixed a crash when a lambda parameter pack was given a default argument that
   is a pack expansion referencing an enclosing function's parameter pack (e.g.
   `[](Types... = args...) {}`). Clang now diagnoses the illegal default
diff --git a/clang/lib/Sema/SemaTemplateInstantiate.cpp b/clang/lib/Sema/SemaTemplateInstantiate.cpp
index cadc5689cebc2..fe27b9ea1f323 100644
--- a/clang/lib/Sema/SemaTemplateInstantiate.cpp
+++ b/clang/lib/Sema/SemaTemplateInstantiate.cpp
@@ -2006,6 +2006,14 @@ bool TemplateInstantiator::instantiateMissingDeclsToScopeForConcepts(Decl *D) {
     if (!Pair)
       continue;
 
+    // Parameters belong to the current specialization. Reusing a mapping
+    // from an outer specialization can substitute the wrong arguments and
+    // recursively re-enter constraint satisfaction. Let CWG2770 instantiate
+    // parameters on demand instead; non-parameter declarations still reuse
+    // outer-scope mappings.
+    if (isa<ParmVarDecl>(D))
+      break;
+
     if (auto *InstD = dyn_cast<Decl *>(*Pair)) {
       Current->InstantiatedLocal(D, InstD);
     } else {

>From be6314a32e6c441e4b3b9ae4996fc4410cdd8fef Mon Sep 17 00:00:00 2001
From: Hsin <genesisjxd at gmail.com>
Date: Mon, 14 Sep 2026 01:08:51 +0800
Subject: [PATCH 3/3] [clang] Add regression test for nested synthesized
 constraint checks

Regression test for #223220, which since #221707 makes comparing two
std::map objects fail with 'satisfaction of constraint ... depends on
itself'. Reproduces the structure of std::__detail::__synth3way_t as
instantiated from the operator<=> of std::map and std::pair with no
standard library involved: the map's synthesis instantiates the
constraint check of the pair's operator<=> while the map-level check is
still active.
---
 .../SemaTemplate/concepts-nested-synth.cpp    | 82 +++++++++++++++++++
 1 file changed, 82 insertions(+)
 create mode 100644 clang/test/SemaTemplate/concepts-nested-synth.cpp

diff --git a/clang/test/SemaTemplate/concepts-nested-synth.cpp b/clang/test/SemaTemplate/concepts-nested-synth.cpp
new file mode 100644
index 0000000000000..ada5222e644e2
--- /dev/null
+++ b/clang/test/SemaTemplate/concepts-nested-synth.cpp
@@ -0,0 +1,82 @@
+// RUN: %clang_cc1 -std=c++20 -fsyntax-only -verify %s
+// expected-no-diagnostics
+//
+// Regression test for https://github.com/llvm/llvm-project/issues/223220.
+//
+// Mechanism: the constraint of a function template is checked for one
+// specialization while a constraint check of *another* specialization of the
+// same function template is still active. Both specializations share the same
+// original parameter declarations. When the parameters of the inner
+// specialization are looked up through the (outer) instantiation scope chain,
+// the parameter mapping of the outer specialization can be found and reused.
+// The inner constraint expression then ends up with parameters of the outer
+// specialization's type, which can re-enter the very same constraint check and
+// be reported as "satisfaction of constraint ... depends on itself".
+//
+// This test reproduces the structure of std::__detail::__synth3way_t
+// instantiated from the operator<=> of std::map and std::pair (no standard
+// library involved):
+//   Map::operator<=>  -> synth<Pair<const Key, Node>>   (outer check)
+//     `a < b` inside the outer check's requirement visits Pair's rewritten
+//     operator<=>, whose return type instantiates synth<const Key, Key>
+//     (the inner check) while the outer check is still active.
+
+namespace nested_synth {
+
+template <class T> T &&Declval() noexcept;
+
+struct Synth {
+  template <class T, class U>
+  constexpr auto operator()(T const &t, U const &u) const
+      requires requires {
+        { t < u };
+        { u < t };
+      } {
+    if (t < u)
+      return int{-1};
+    if (u < t)
+      return int{1};
+    return int{0};
+  }
+};
+constexpr Synth synth{};
+
+template <class T, class U = T>
+using synth_t = decltype(synth(Declval<T &>(), Declval<U &>()));
+
+struct Key {
+  int n;
+  constexpr bool operator<(Key const &O) const { return n < O.n; }
+};
+struct Node {
+  int m;
+  constexpr bool operator<(Node const &O) const { return m < O.m; }
+};
+
+template <class T1, class T2> struct Pair {
+  T1 first;
+  T2 second;
+};
+// Pair has no operator<, so `lhs < rhs` resolves through the rewritten
+// operator<=>, whose return type instantiates synth_t<T1, U1>.
+template <class T1, class T2, class U1, class U2>
+constexpr auto operator<=>(Pair<T1, T2> const &, Pair<U1, U2> const &)
+    -> synth_t<T1, U1> {
+  return {};
+}
+
+template <class K2, class V2> struct Map {};
+
+// The map's only ordering operator returns the synthesized three-way result
+// of its (const Key, V) element pairs.
+template <class K2, class V2>
+constexpr auto operator<=>(Map<K2, V2> const &, Map<K2, V2> const &)
+    -> synth_t<Pair<const K2, V2>> {
+  return {};
+}
+
+using NodeMap = Map<Key, Node>;
+
+bool compare(NodeMap const &A, NodeMap const &B) { return A < B; }
+
+} // namespace nested_synth



More information about the cfe-commits mailing list