Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[Clang] Introduce a trait to determine the structure binding size #131515

Merged
merged 23 commits into from
Mar 18, 2025

Conversation

cor3ntin
Copy link
Contributor

@cor3ntin cor3ntin commented Mar 16, 2025

Introduce a trait to determine the number of bindings that would be produced by

   auto [...p] = expr;

This is necessary to implement P2300 (https://eel.is/c++draft/exec#snd.concepts-5), but can also be used to implement a general get function that supports aggregates

__builtin_structured_binding_size is a unary type trait that evaluates to the number of bindings in a decomposition

If the argument cannot be destructured, a sfinae-friendly error is produced.

A type is considered a valid tuple if std::tuple_size_v<T> is a valid expression, even if there is no valid std::tuple_element specialization or suitable get function for that type.

This is modeled as a UnaryExprOrTypeTraitExpr, but it is wrapped in a ConstantExpr because the structured binding size can only be established during sema.

Fixes #46049

bindings that would be produced by

```cpp

   auto [...p] = expr;

```

This is necessary to implement P2300 (https://eel.is/c++draft/exec#snd.concepts-5),
but can also be used to implement a general get<N>
function that supports aggregates

__builtin_structured_binding_size works like sizeof in that it supports both
type and expression arguments.

If the argument cannot be destructured, a sfinae-friendly error is produced.

A type is considered a valid tuple if `std::tuple_size_v<T>`
is a valid expression, even if there is no valid `std::tuple_element`
specialization or suitable `get` function for that type.

This is modeled as a UnaryExprOrTypeTraitExpr, but it is wrapped
in a ConstantExpr because the structured binding size can only be
established during sema.
@cor3ntin cor3ntin added enhancement Improving things as opposed to bug fixing, e.g. new or missing feature clang:frontend Language frontend issues, e.g. anything involving "Sema" c++26 extension:clang labels Mar 16, 2025
@llvmbot llvmbot added the clang Clang issues not falling into any other category label Mar 16, 2025
@llvmbot
Copy link
Member

llvmbot commented Mar 16, 2025

@llvm/pr-subscribers-clang-static-analyzer-1
@llvm/pr-subscribers-clang-modules

@llvm/pr-subscribers-clang

Author: cor3ntin (cor3ntin)

Changes

bindings that would be produced by

   auto [...p] = expr;

This is necessary to implement P2300 (https://eel.is/c++draft/exec#snd.concepts-5), but can also be used to implement a general get<N> function that supports aggregates

__builtin_structured_binding_size works like sizeof in that it supports both type and expression arguments.

If the argument cannot be destructured, a sfinae-friendly error is produced.

A type is considered a valid tuple if std::tuple_size_v&lt;T&gt; is a valid expression, even if there is no valid std::tuple_element specialization or suitable get function for that type.

This is modeled as a UnaryExprOrTypeTraitExpr, but it is wrapped in a ConstantExpr because the structured binding size can only be established during sema.


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

14 Files Affected:

  • (modified) clang/docs/LanguageExtensions.rst (+30)
  • (modified) clang/docs/ReleaseNotes.rst (+3)
  • (modified) clang/include/clang/Basic/DiagnosticSemaKinds.td (+2)
  • (modified) clang/include/clang/Basic/TokenKinds.def (+1-1)
  • (modified) clang/include/clang/Sema/Sema.h (+2-1)
  • (modified) clang/lib/AST/ByteCode/Compiler.cpp (+2)
  • (modified) clang/lib/AST/ExprConstant.cpp (+5)
  • (modified) clang/lib/AST/ItaniumMangle.cpp (+9)
  • (modified) clang/lib/Parse/ParseExpr.cpp (+10-3)
  • (modified) clang/lib/Sema/SemaDeclCXX.cpp (+93-30)
  • (modified) clang/lib/Sema/SemaExpr.cpp (+62-7)
  • (modified) clang/test/CodeGenCXX/builtins.cpp (+6)
  • (added) clang/test/CodeGenCXX/mangle-structured-binding-size.cpp (+12)
  • (added) clang/test/SemaCXX/builtin-structured-binding-size.cpp (+168)
diff --git a/clang/docs/LanguageExtensions.rst b/clang/docs/LanguageExtensions.rst
index cc12ff5bad353..9a5cd8f1e5f5d 100644
--- a/clang/docs/LanguageExtensions.rst
+++ b/clang/docs/LanguageExtensions.rst
@@ -434,6 +434,36 @@ __datasizeof
 ``__datasizeof`` behaves like ``sizeof``, except that it returns the size of the
 type ignoring tail padding.
 
+.. _builtin_structured_binding_size-doc:
+
+__builtin_structured_binding_size (C++)
+---------------------------------------
+``__builtin_structured_binding_size`` returns the *structured binding size*
+([dcl.struct.bind]) of the type ``T`` (or unevaluate expression ``arg``)
+passed as argument.
+
+This is equivalent to the size of the pack ``p`` in ``auto&& [...p] = arg;``.
+If the argument is not destructurable (ie not an array, vector, complex,
+*tuple-like* type or destructurable class type), ``__builtin_structured_binding_size(T)``
+is not a valid expression (``__builtin_structured_binding_size`` is SFINEA-friendly).
+
+A type is considered a valid tuple if ``std::tuple_size_v<T>`` is a valid expression,
+even if there is no valid ``std::tuple_element`` specialization or suitable
+``get`` function for that type.
+
+.. code-block:: c++
+
+  template<std::size_t Idx, typename T>
+  requires (Idx < __builtin_structured_binding_size(T))
+  decltype(auto) constexpr get_binding(T&& obj) {
+      auto && [...p] = std::forward<T>(obj);
+      return p...[Idx];
+  }
+  struct S { int a = 0, b = 42; };
+  static_assert(__builtin_structured_binding_size(S) == 2);
+  static_assert(get_binding<1>(S{}) == 42);
+
+
 _BitInt, _ExtInt
 ----------------
 
diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst
index 2a1c5ee2d788e..f49e389773e4e 100644
--- a/clang/docs/ReleaseNotes.rst
+++ b/clang/docs/ReleaseNotes.rst
@@ -74,6 +74,9 @@ What's New in Clang |release|?
 C++ Language Changes
 --------------------
 
+- Added a :ref:`__builtin_structured_binding_size <builtin_structured_binding_size-doc>` (T)
+  builtin that returns the number of structured bindings that would be produced by destructuring ``T``.
+
 C++2c Feature Support
 ^^^^^^^^^^^^^^^^^^^^^
 
diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td
index 86c9c955c1c78..fad826c1c6336 100644
--- a/clang/include/clang/Basic/DiagnosticSemaKinds.td
+++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td
@@ -591,6 +591,8 @@ def err_decomp_decl_std_tuple_size_not_constant : Error<
   "is not a valid integral constant expression">;
 def note_in_binding_decl_init : Note<
   "in implicit initialization of binding declaration %0">;
+def err_arg_is_not_destructurable : Error<
+  "type %0 is not destructurable">;
 
 def err_std_type_trait_not_class_template : Error<
   "unsupported standard library implementation: "
diff --git a/clang/include/clang/Basic/TokenKinds.def b/clang/include/clang/Basic/TokenKinds.def
index 397a5d95709fb..bad9387673ef9 100644
--- a/clang/include/clang/Basic/TokenKinds.def
+++ b/clang/include/clang/Basic/TokenKinds.def
@@ -553,8 +553,8 @@ TYPE_TRAIT_2(__reference_converts_from_temporary, ReferenceConvertsFromTemporary
 // IsDeducible is only used internally by clang for CTAD implementation and
 // is not exposed to users.
 TYPE_TRAIT_2(/*EmptySpellingName*/, IsDeducible, KEYCXX)
-
 TYPE_TRAIT_1(__is_bitwise_cloneable, IsBitwiseCloneable, KEYALL)
+UNARY_EXPR_OR_TYPE_TRAIT(__builtin_structured_binding_size, StructuredBindingSize, KEYCXX)
 
 // Embarcadero Expression Traits
 EXPRESSION_TRAIT(__is_lvalue_expr, IsLValueExpr, KEYCXX)
diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h
index 657350fa843b9..cd7078b119712 100644
--- a/clang/include/clang/Sema/Sema.h
+++ b/clang/include/clang/Sema/Sema.h
@@ -6095,7 +6095,8 @@ class Sema final : public SemaBase {
                                          RecordDecl *ClassDecl,
                                          const IdentifierInfo *Name);
 
-  unsigned GetDecompositionElementCount(QualType DecompType);
+  std::optional<unsigned int> GetDecompositionElementCount(QualType DecompType,
+                                                           SourceLocation Loc);
   void CheckCompleteDecompositionDeclaration(DecompositionDecl *DD);
 
   /// Stack containing information needed when in C++2a an 'auto' is encountered
diff --git a/clang/lib/AST/ByteCode/Compiler.cpp b/clang/lib/AST/ByteCode/Compiler.cpp
index b9f88230007b5..0259605086b21 100644
--- a/clang/lib/AST/ByteCode/Compiler.cpp
+++ b/clang/lib/AST/ByteCode/Compiler.cpp
@@ -2154,6 +2154,8 @@ bool Compiler<Emitter>::VisitUnaryExprOrTypeTraitExpr(
             E->getArgumentType()),
         E);
   }
+  assert(Kind != UETT_StructuredBindingSize &&
+         "should have been evaluated in Sema");
 
   return false;
 }
diff --git a/clang/lib/AST/ExprConstant.cpp b/clang/lib/AST/ExprConstant.cpp
index f8e8aaddbfdbd..1763bbc18043d 100644
--- a/clang/lib/AST/ExprConstant.cpp
+++ b/clang/lib/AST/ExprConstant.cpp
@@ -14878,6 +14878,11 @@ bool IntExprEvaluator::VisitUnaryExprOrTypeTraitExpr(
     }
     return Success(Sizeof, E);
   }
+  case UETT_StructuredBindingSize:
+    // This can only be computed from Sema and has been cached.
+    // We can still get there from code that strips the outer ConstantExpr.
+    return false;
+
   case UETT_OpenMPRequiredSimdAlign:
     assert(E->isArgumentType());
     return Success(
diff --git a/clang/lib/AST/ItaniumMangle.cpp b/clang/lib/AST/ItaniumMangle.cpp
index b6ba36784f38a..12993d5cb35f1 100644
--- a/clang/lib/AST/ItaniumMangle.cpp
+++ b/clang/lib/AST/ItaniumMangle.cpp
@@ -5389,6 +5389,15 @@ void CXXNameMangler::mangleExpression(const Expr *E, unsigned Arity,
       Diags.Report(DiagID);
       return;
     }
+    case UETT_StructuredBindingSize:
+      Out << "u11__builtin_structured_binding_size";
+      if (SAE->isArgumentType())
+        mangleType(SAE->getArgumentType());
+      else
+        mangleTemplateArgExpr(SAE->getArgumentExpr());
+      Out << 'E';
+      break;
+      return;
     }
     break;
   }
diff --git a/clang/lib/Parse/ParseExpr.cpp b/clang/lib/Parse/ParseExpr.cpp
index 0c28972d6ed8f..2e3f0ce3194f5 100644
--- a/clang/lib/Parse/ParseExpr.cpp
+++ b/clang/lib/Parse/ParseExpr.cpp
@@ -1544,6 +1544,7 @@ ExprResult Parser::ParseCastExpression(CastParseKind ParseKind,
   // unary-expression: '__builtin_omp_required_simd_align' '(' type-name ')'
   case tok::kw___builtin_omp_required_simd_align:
   case tok::kw___builtin_vectorelements:
+  case tok::kw___builtin_structured_binding_size:
     if (NotPrimaryExpression)
       *NotPrimaryExpression = true;
     AllowSuffix = false;
@@ -2463,7 +2464,8 @@ Parser::ParseExprAfterUnaryExprOrTypeTrait(const Token &OpTok,
                        tok::kw___datasizeof, tok::kw___alignof, tok::kw_alignof,
                        tok::kw__Alignof, tok::kw_vec_step,
                        tok::kw___builtin_omp_required_simd_align,
-                       tok::kw___builtin_vectorelements) &&
+                       tok::kw___builtin_vectorelements,
+                       tok::kw___builtin_structured_binding_size) &&
          "Not a typeof/sizeof/alignof/vec_step expression!");
 
   ExprResult Operand;
@@ -2473,7 +2475,8 @@ Parser::ParseExprAfterUnaryExprOrTypeTrait(const Token &OpTok,
     // If construct allows a form without parenthesis, user may forget to put
     // pathenthesis around type name.
     if (OpTok.isOneOf(tok::kw_sizeof, tok::kw___datasizeof, tok::kw___alignof,
-                      tok::kw_alignof, tok::kw__Alignof)) {
+                      tok::kw_alignof, tok::kw__Alignof,
+                      tok::kw___builtin_structured_binding_size)) {
       if (isTypeIdUnambiguously()) {
         DeclSpec DS(AttrFactory);
         ParseSpecifierQualifierList(DS);
@@ -2599,7 +2602,8 @@ ExprResult Parser::ParseUnaryExprOrTypeTraitExpression() {
   assert(Tok.isOneOf(tok::kw_sizeof, tok::kw___datasizeof, tok::kw___alignof,
                      tok::kw_alignof, tok::kw__Alignof, tok::kw_vec_step,
                      tok::kw___builtin_omp_required_simd_align,
-                     tok::kw___builtin_vectorelements) &&
+                     tok::kw___builtin_vectorelements,
+                     tok::kw___builtin_structured_binding_size) &&
          "Not a sizeof/alignof/vec_step expression!");
   Token OpTok = Tok;
   ConsumeToken();
@@ -2687,6 +2691,9 @@ ExprResult Parser::ParseUnaryExprOrTypeTraitExpression() {
   case tok::kw___datasizeof:
     ExprKind = UETT_DataSizeOf;
     break;
+  case tok::kw___builtin_structured_binding_size:
+    ExprKind = UETT_StructuredBindingSize;
+    break;
   case tok::kw___builtin_vectorelements:
     ExprKind = UETT_VectorElements;
     break;
diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp
index a02bd8335fa20..164e81e1cfa61 100644
--- a/clang/lib/Sema/SemaDeclCXX.cpp
+++ b/clang/lib/Sema/SemaDeclCXX.cpp
@@ -1475,6 +1475,48 @@ static DeclAccessPair findDecomposableBaseClass(Sema &S, SourceLocation Loc,
   return DeclAccessPair::make(const_cast<CXXRecordDecl*>(ClassWithFields), AS);
 }
 
+static bool CheckMemberDecompositionFields(Sema &S, SourceLocation Loc,
+                                           const CXXRecordDecl *OrigRD,
+                                           QualType DecompType,
+                                           DeclAccessPair BasePair) {
+  const CXXRecordDecl *RD = cast_or_null<CXXRecordDecl>(BasePair.getDecl());
+  if (!RD)
+    return true;
+
+  for (auto *FD : RD->fields()) {
+    if (FD->isUnnamedBitField())
+      continue;
+
+    // All the non-static data members are required to be nameable, so they
+    // must all have names.
+    if (!FD->getDeclName()) {
+      if (RD->isLambda()) {
+        S.Diag(Loc, diag::err_decomp_decl_lambda);
+        S.Diag(RD->getLocation(), diag::note_lambda_decl);
+        return true;
+      }
+
+      if (FD->isAnonymousStructOrUnion()) {
+        S.Diag(Loc, diag::err_decomp_decl_anon_union_member)
+            << DecompType << FD->getType()->isUnionType();
+        S.Diag(FD->getLocation(), diag::note_declared_at);
+        return true;
+      }
+
+      // FIXME: Are there any other ways we could have an anonymous member?
+    }
+    // The field must be accessible in the context of the structured binding.
+    // We already checked that the base class is accessible.
+    // FIXME: Add 'const' to AccessedEntity's classes so we can remove the
+    // const_cast here.
+    S.CheckStructuredBindingMemberAccess(
+        Loc, const_cast<CXXRecordDecl *>(OrigRD),
+        DeclAccessPair::make(FD, CXXRecordDecl::MergeAccess(
+                                     BasePair.getAccess(), FD->getAccess())));
+  }
+  return false;
+}
+
 static bool checkMemberDecomposition(Sema &S, ArrayRef<BindingDecl*> Bindings,
                                      ValueDecl *Src, QualType DecompType,
                                      const CXXRecordDecl *OrigRD) {
@@ -1503,43 +1545,20 @@ static bool checkMemberDecomposition(Sema &S, ArrayRef<BindingDecl*> Bindings,
   auto FlatBindings = DD->flat_bindings();
   assert(llvm::range_size(FlatBindings) == NumFields);
   auto FlatBindingsItr = FlatBindings.begin();
+
+  if (CheckMemberDecompositionFields(S, Src->getLocation(), OrigRD, DecompType,
+                                     BasePair))
+    return true;
+
   for (auto *FD : RD->fields()) {
     if (FD->isUnnamedBitField())
       continue;
 
-    // All the non-static data members are required to be nameable, so they
-    // must all have names.
-    if (!FD->getDeclName()) {
-      if (RD->isLambda()) {
-        S.Diag(Src->getLocation(), diag::err_decomp_decl_lambda);
-        S.Diag(RD->getLocation(), diag::note_lambda_decl);
-        return true;
-      }
-
-      if (FD->isAnonymousStructOrUnion()) {
-        S.Diag(Src->getLocation(), diag::err_decomp_decl_anon_union_member)
-          << DecompType << FD->getType()->isUnionType();
-        S.Diag(FD->getLocation(), diag::note_declared_at);
-        return true;
-      }
-
-      // FIXME: Are there any other ways we could have an anonymous member?
-    }
-
     // We have a real field to bind.
     assert(FlatBindingsItr != FlatBindings.end());
     BindingDecl *B = *(FlatBindingsItr++);
     SourceLocation Loc = B->getLocation();
 
-    // The field must be accessible in the context of the structured binding.
-    // We already checked that the base class is accessible.
-    // FIXME: Add 'const' to AccessedEntity's classes so we can remove the
-    // const_cast here.
-    S.CheckStructuredBindingMemberAccess(
-        Loc, const_cast<CXXRecordDecl *>(OrigRD),
-        DeclAccessPair::make(FD, CXXRecordDecl::MergeAccess(
-                                     BasePair.getAccess(), FD->getAccess())));
-
     // Initialize the binding to Src.FD.
     ExprResult E = S.BuildDeclRefExpr(Src, DecompType, VK_LValue, Loc);
     if (E.isInvalid())
@@ -1642,6 +1661,50 @@ void Sema::CheckCompleteDecompositionDeclaration(DecompositionDecl *DD) {
     DD->setInvalidDecl();
 }
 
+std::optional<unsigned> Sema::GetDecompositionElementCount(QualType T,
+                                                           SourceLocation Loc) {
+  const ASTContext &Ctx = getASTContext();
+  assert(!T->isDependentType());
+  if (auto *CAT = Ctx.getAsConstantArrayType(T))
+    return CAT->getSize().getZExtValue();
+  if (auto *VT = T->getAs<VectorType>())
+    return VT->getNumElements();
+  if (T->getAs<ComplexType>())
+    return 2;
+
+  llvm::APSInt TupleSize(Ctx.getTypeSize(Ctx.getSizeType()));
+  switch (isTupleLike(*this, Loc, T, TupleSize)) {
+  case IsTupleLike::Error:
+    return {};
+  case IsTupleLike::TupleLike:
+    return TupleSize.getExtValue();
+  case IsTupleLike::NotTupleLike:
+    break;
+  }
+  CXXRecordDecl *OrigRD = T->getAsCXXRecordDecl();
+  if (!OrigRD || OrigRD->isUnion()) {
+    return std::nullopt;
+  }
+
+  if (RequireCompleteType(Loc, T, diag::err_incomplete_type))
+    return std::nullopt;
+
+  CXXCastPath BasePath;
+  DeclAccessPair BasePair =
+      findDecomposableBaseClass(*this, Loc, OrigRD, BasePath);
+  const CXXRecordDecl *RD = cast_or_null<CXXRecordDecl>(BasePair.getDecl());
+  if (!RD)
+    return std::nullopt;
+
+  unsigned NumFields = llvm::count_if(
+      RD->fields(), [](FieldDecl *FD) { return !FD->isUnnamedBitField(); });
+
+  if (CheckMemberDecompositionFields(*this, Loc, OrigRD, T, BasePair))
+    return true;
+
+  return NumFields;
+}
+
 void Sema::MergeVarDeclExceptionSpecs(VarDecl *New, VarDecl *Old) {
   // Shortcut if exceptions are disabled.
   if (!getLangOpts().CXXExceptions)
@@ -17262,8 +17325,8 @@ void Sema::DiagnoseStaticAssertDetails(const Expr *E) {
       Expr::EvalResult Result;
       SmallString<12> ValueString;
       bool Print;
-    } DiagSide[2] = {{LHS, Expr::EvalResult(), {}, false},
-                     {RHS, Expr::EvalResult(), {}, false}};
+    } DiagSide[2] = {{Op->getLHS(), Expr::EvalResult(), {}, false},
+                     {Op->getRHS(), Expr::EvalResult(), {}, false}};
     for (unsigned I = 0; I < 2; I++) {
       const Expr *Side = DiagSide[I].Cond;
 
diff --git a/clang/lib/Sema/SemaExpr.cpp b/clang/lib/Sema/SemaExpr.cpp
index e19136b394800..8766331a0df59 100644
--- a/clang/lib/Sema/SemaExpr.cpp
+++ b/clang/lib/Sema/SemaExpr.cpp
@@ -4160,6 +4160,54 @@ static bool CheckVecStepTraitOperandType(Sema &S, QualType T,
   return false;
 }
 
+static ExprResult BuildStructuredBindingSizeTraitImpl(Sema &S, QualType T,
+                                                      Expr *E,
+                                                      TypeSourceInfo *TInfo,
+                                                      SourceLocation Loc,
+                                                      SourceRange ArgRange) {
+  assert(!!E != !!TInfo);
+  assert(!T->isDependentType());
+  std::optional<unsigned> Size =
+      S.GetDecompositionElementCount(T, ArgRange.getBegin());
+  if (!Size) {
+    return S.Diag(Loc, diag::err_arg_is_not_destructurable) << T << ArgRange;
+    return ExprError();
+  }
+  Expr *Inner;
+  if (E)
+    Inner = new (S.getASTContext()) UnaryExprOrTypeTraitExpr(
+        UnaryExprOrTypeTrait::UETT_StructuredBindingSize, E,
+        S.getASTContext().getSizeType(), Loc, E->getEndLoc());
+
+  else
+    Inner = new (S.getASTContext()) UnaryExprOrTypeTraitExpr(
+        UnaryExprOrTypeTrait::UETT_StructuredBindingSize, TInfo,
+        S.getASTContext().getSizeType(), Loc, ArgRange.getEnd());
+
+  // Computing the number of bindings requires Sema and is non-trivial,
+  // so we cache the result now.
+  llvm::APSInt V =
+      S.getASTContext().MakeIntValue(*Size, S.getASTContext().getSizeType());
+  return ConstantExpr::Create(S.getASTContext(), Inner, APValue{V});
+}
+
+static ExprResult BuildStructuredBindingSizeTrait(Sema &S,
+                                                  TypeSourceInfo *TInfo,
+                                                  SourceLocation Loc,
+                                                  SourceRange ArgRange) {
+  return BuildStructuredBindingSizeTraitImpl(S, TInfo->getType(),
+                                             /*Expr=*/nullptr, TInfo, Loc,
+                                             ArgRange);
+}
+
+static ExprResult BuildStructuredBindingSizeTrait(Sema &S, SourceLocation OpLoc,
+                                                  Expr *E) {
+
+  return BuildStructuredBindingSizeTraitImpl(S, E->getType(), E,
+                                             /*TInfo=*/nullptr, OpLoc,
+                                             E->getSourceRange());
+}
+
 static bool CheckVectorElementsTraitOperandType(Sema &S, QualType T,
                                                 SourceLocation Loc,
                                                 SourceRange ArgRange) {
@@ -4650,10 +4698,14 @@ ExprResult Sema::CreateUnaryExprOrTypeTraitExpr(TypeSourceInfo *TInfo,
 
   QualType T = TInfo->getType();
 
-  if (!T->isDependentType() &&
-      CheckUnaryExprOrTypeTraitOperand(T, OpLoc, R, ExprKind,
-                                       getTraitSpelling(ExprKind)))
-    return ExprError();
+  if (!T->isDependentType()) {
+    if (ExprKind == UETT_StructuredBindingSize)
+      return BuildStructuredBindingSizeTrait(*this, TInfo, OpLoc, R);
+
+    if (CheckUnaryExprOrTypeTraitOperand(T, OpLoc, R, ExprKind,
+                                         getTraitSpelling(ExprKind)))
+      return ExprError();
+  }
 
   // Adds overload of TransformToPotentiallyEvaluated for TypeSourceInfo to
   // properly deal with VLAs in nested calls of sizeof and typeof.
@@ -4680,14 +4732,17 @@ Sema::CreateUnaryExprOrTypeTraitExpr(Expr *E, SourceLocation OpLoc,
   bool isInvalid = false;
   if (E->isTypeDependent()) {
     // Delay type-checking for type-dependent expressions.
+  } else if (ExprKind == UETT_StructuredBindingSize) {
+    // Custom logic
+    return BuildStructuredBindingSizeTrait(*this, OpLoc, E);
   } else if (ExprKind == UETT_AlignOf || ExprKind == UETT_PreferredAlignOf) {
     isInvalid = CheckAlignOfExpr(*this, E, ExprKind);
   } else if (ExprKind == UETT_VecStep) {
     isInvalid = CheckVecStepExpr(E);
   } else if (ExprKind == UETT_OpenMPRequiredSimdAlign) {
-      Diag(E->getExprLoc(), diag::err_openmp_default_simd_align_expr);
-      isInvalid = true;
-  } else if (E->refersToBitField()) {  // C99 6.5.3.4p1.
+    Diag(E->getExprLoc(), diag::err_openmp_default_simd_align_expr);
+    isInvalid = true;
+  } else if (E->refersToBitField()) { // C99 6.5.3.4p1.
     Diag(E->getExprLoc(), diag::err_sizeof_alignof_typeof_bitfield) << 0;
     isInvalid = true;
   } else if (ExprKind == UETT_VectorElements) {
diff --git a/clang/test/CodeGenCXX/builtins.cpp b/clang/test/CodeGenCXX/builtins.cpp
index 37f9491d12d04..9169f3a3276d3 100644
--- a/clang/test/CodeGenCXX/builtins.cpp
+++ b/clang/test/CodeGenCXX/builtins.cpp
@@ -77,3 +77,9 @@ int constexpr_overflow_result() {
   // CHECK: [[RET_VAL:%.+]] = load i32, ptr [[Z]]
   // CHECK: ret i32 [[RET_VAL]]
 }
+
+int structured_binding_size() {
+  struct S2 {int a, b;};
+  return __builtin_structured_binding_size(S2);
+  // CHECK: ret i32 2
+}
diff --git a/clang/test/CodeGenCXX/mangle-structured-binding-size.cpp b/clang/test/CodeGenCXX/mangle-structured-binding-size.cpp
new file mode 100644
index 0000000000000..5b53ed8d7166d
--- /dev/null
+++ b/clang/test/CodeGenCXX/mangle-structured-binding-si...
[truncated]

@cor3ntin
Copy link
Contributor Author

Please look at the doc/tests and confirm this would be suitable to implement std::exec::tag_of_t / P2300

@ldionne @jwakely @StephanTLavavej @philnik777 @ericniebler

@cor3ntin cor3ntin changed the title [Clang][RFC] Intrododuce a builtin to determine the number of [Clang][RFC] Intrododuce a builtin to determine the structure binding size Mar 16, 2025
requires (__builtin_structured_binding_size(T) >=2)
consteval auto tag_of_impl(T& t) {
auto && [tag, ..._] = t;
return type_identity<decltype(auto(tag))>{};
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we test __builtin_structured_binding_size(decltype(t))?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

in the requires?

Copy link
Contributor

@zyn0217 zyn0217 Mar 18, 2025

Choose a reason for hiding this comment

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

in the function body :)
like, static_assert(__builtin_structured_binding_size(decltype(t)) == ...)

Copy link
Collaborator

@erichkeane erichkeane left a comment

Choose a reason for hiding this comment

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

A few drive-bys :)

@llvmbot llvmbot added clang:modules C++20 modules and Clang Header Modules clang:codegen IR generation bugs: mangling, exceptions, etc. clang:static analyzer labels Mar 17, 2025
Copy link
Collaborator

@erichkeane erichkeane left a comment

Choose a reason for hiding this comment

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

A few comments... this ends up being way nicer as a type trait IMO.

Copy link
Collaborator

@erichkeane erichkeane left a comment

Choose a reason for hiding this comment

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

I'm happy when the other reviewers are. 1 nit, else LGTM.

the *structured binding size* ([dcl.struct.bind]) of type ``T``

This is equivalent to the size of the pack ``p`` in ``auto&& [...p] = declval<T&>();``.
If the argument cannot be decomposed (ie not a builtin array, builtin SIMD vector,
Copy link
Collaborator

@shafik shafik Mar 18, 2025

Choose a reason for hiding this comment

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

I feel a bullet list would be better here. Also a positive form would read better, the following can be decomposed, otherwise it is not well-formed something like that.

It took me a few tries to make sense of this.

Comment on lines +1669 to +1672
Qualifiers Quals;
QualType Unqual = Context.getUnqualifiedArrayType(T, Quals);
Quals.removeCVRQualifiers();
T = Context.getQualifiedType(Unqual, Quals);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why do we need to do this dance? (Should the code be moved down to above the isTupleLike call?)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why do we need to do this dance

const objects of class types are decomposable

Should the code be moved down to above the isTupleLike call?

No, for tuple-like types we already handle their constness

@@ -5066,6 +5066,10 @@ static bool CheckUnaryTypeTraitTypeCompleteness(Sema &S, TypeTrait UTT,
case UTT_IsInterfaceClass:
return true;

// We diagnose incomplete class types later
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
// We diagnose incomplete class types later
// We diagnose incomplete class types later.

struct S0 {};
struct S1 {int a;};
struct S2 {int a; int b;};
struct S3 {double a; int b; int c;};
Copy link
Collaborator

Choose a reason for hiding this comment

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

Test with bit-fields? Test with unnamed bit-field? Test with an anonymous union member? Test with a flexible array member? Test where the structure being decomposed as a static_assert declaration in it (which wouldn't be a member as far as decomposition is concerned)?

What about a monstrosity like:

struct S {
  int x;
  int y;

  static_assert(__builtin_structured_binding_size(S) == 2); // ?

  int z;
  static_assert(__builtin_structured_binding_size(S) == 3);
};

Copy link
Collaborator

@shafik shafik Mar 18, 2025

Choose a reason for hiding this comment

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

I was just about to make a similar comment

static members? bit-field? anon bit-fields? Reference members?

I guess it will just failed w/ anon union members?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added tests for all of these

struct S0 {};
struct S1 {int a;};
struct S2 {int a; int b;};
struct S3 {double a; int b; int c;};
Copy link
Collaborator

@shafik shafik Mar 18, 2025

Choose a reason for hiding this comment

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

I was just about to make a similar comment

static members? bit-field? anon bit-fields? Reference members?

I guess it will just failed w/ anon union members?

@cor3ntin cor3ntin changed the title [Clang][RFC] Introduce a trait to determine the structure binding size [Clang] Introduce a trait to determine the structure binding size Mar 18, 2025
return makeTruthVal(TE->getBoolValue(), TE->getType());
if (TE->getType()->isIntegralOrEnumerationType())
return makeIntVal(TE->getAPValue().getInt());
return std::nullopt;
Copy link
Collaborator

Choose a reason for hiding this comment

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

How could we end up here? Is this tested?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I replaced by an assert.

@cor3ntin
Copy link
Contributor Author

@AaronBallman @shafik Hopefully all yiour feedback has been applied :)

Copy link
Collaborator

@AaronBallman AaronBallman left a comment

Choose a reason for hiding this comment

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

Just a few more test cases regarding edge cases, but in general LGTM assuming those tests don't find any surprises.

// expected-error@-1 {{static assertion expression is not an integral constant expression}} \
// expected-note@-4 {{definition of 'S' is not complete until the closing '}'}}
};

Copy link
Collaborator

@AaronBallman AaronBallman Mar 18, 2025

Choose a reason for hiding this comment

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

Some other edge case tests:

void func(int array[14], int x = __builtin_structured_binding_size(decltype(array))); // Is x 14?

struct S {
  static int array[14];
  int x = __builtin_structured_binding_size(decltype(array)); // Is x 14?
};

template <typename Ty, int N = __builtin_structured_binding_size(Ty)>
struct T {
  static constexpr int value = N;
};

T<int> t1; // Error
static_assert(T<S3>::value == 3);

@cor3ntin cor3ntin force-pushed the corentin/structured_binding_size branch from 1187752 to 2745db0 Compare March 18, 2025 18:54
@cor3ntin cor3ntin merged commit bc8b19c into llvm:main Mar 18, 2025
7 of 11 checks passed
RD->fields(), [](FieldDecl *FD) { return !FD->isUnnamedBitField(); });

if (CheckMemberDecompositionFields(*this, Loc, OrigRD, T, BasePair))
return true;
Copy link
Contributor

Choose a reason for hiding this comment

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

return true or return std::nullopt?

@ahatanak
Copy link
Collaborator

It looks like this PR introduced some undefined behavior. https://green.lab.llvm.org/job/llvm.org/job/clang-stage2-cmake-RgSan/767/testReport/

Could you take a look at the failing tests and fix them or revert the PR?

@erichkeane
Copy link
Collaborator

It looks like this PR introduced some undefined behavior. https://green.lab.llvm.org/job/llvm.org/job/clang-stage2-cmake-RgSan/767/testReport/

Could you take a look at the failing tests and fix them or revert the PR?

Huh, interesting! Looks like the APValue wasn't initialized in SOME path but we tried to get it. Corentin will be in tomorrow morning and will fix it, else I will revert tomorrow.

cor3ntin added a commit to cor3ntin/llvm-project that referenced this pull request Mar 19, 2025
It turns out trailing objects are uninitialized
and APValue assignment operator requires a fully initialized object.
@erichkeane
Copy link
Collaborator

See: #132091

cor3ntin added a commit to cor3ntin/llvm-project that referenced this pull request Mar 19, 2025
It turns out trailing objects are uninitialized
and APValue assignment operator requires a fully initialized object.
cor3ntin added a commit that referenced this pull request Mar 19, 2025
It turns out trailing objects are uninitialized
and APValue assignment operator requires a fully initialized object.

Additionally,  do some drive-by post-commit-review fixes.
@cor3ntin
Copy link
Contributor Author

It looks like this PR introduced some undefined behavior. https://green.lab.llvm.org/job/llvm.org/job/clang-stage2-cmake-RgSan/767/testReport/

Could you take a look at the failing tests and fix them or revert the PR?

Thanks for letting me know.
It should be fixed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c++26 clang:codegen IR generation bugs: mangling, exceptions, etc. clang:frontend Language frontend issues, e.g. anything involving "Sema" clang:modules C++20 modules and Clang Header Modules clang:static analyzer clang Clang issues not falling into any other category enhancement Improving things as opposed to bug fixing, e.g. new or missing feature extension:clang
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Enhancement: Please add a builtin to count bindings in [dcl.struct.bind]