Skip to content

Conversation

@Sirraide
Copy link
Member

@Sirraide Sirraide commented Nov 26, 2025

This implements compile-time evaluation of expansion statements. With this implemented, we can finally test expansion statements properly, so this also adds a bunch of tests for all three kinds of expansion statements.

I didn’t add any Sema tests before this because I felt that just testing that we don’t emit any diagnostics would not have been too useful; with this patch, we can also test that the behaviour of the generated AST is correct by evaluating it.

I still have to add support to the new constant interpreter; I’ll be adding that to this patch as well.

@github-actions
Copy link

github-actions bot commented Nov 26, 2025

🐧 Linux x64 Test Results

  • 111408 tests passed
  • 4449 tests skipped

@Sirraide Sirraide added c++26 clang:codegen IR generation bugs: mangling, exceptions, etc. clang:frontend Language frontend issues, e.g. anything involving "Sema" labels Nov 26, 2025 — with Graphite App
@llvmbot
Copy link
Member

llvmbot commented Nov 26, 2025

@llvm/pr-subscribers-clang

@llvm/pr-subscribers-clang-codegen

Author: None (Sirraide)

Changes

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

4 Files Affected:

  • (modified) clang/lib/AST/ExprConstant.cpp (+40)
  • (modified) clang/lib/CodeGen/CGDecl.cpp (+7)
  • (modified) clang/lib/Sema/SemaDeclCXX.cpp (+3)
  • (added) clang/test/SemaCXX/cxx2c-expansion-stmts.cpp (+1042)
diff --git a/clang/lib/AST/ExprConstant.cpp b/clang/lib/AST/ExprConstant.cpp
index d93f87a27e68d..2de1641f6b46a 100644
--- a/clang/lib/AST/ExprConstant.cpp
+++ b/clang/lib/AST/ExprConstant.cpp
@@ -6037,6 +6037,12 @@ static EvalStmtResult EvaluateStmt(StmtResult &Result, EvalInfo &Info,
       const VarDecl *VD = dyn_cast_or_null<VarDecl>(D);
       if (VD && !CheckLocalVariableDeclaration(Info, VD))
         return ESR_Failed;
+
+      if (const auto *ESD = dyn_cast<CXXExpansionStmtDecl>(D)) {
+        assert(ESD->getInstantiations() && "not expanded?");
+        return EvaluateStmt(Result, Info, ESD->getInstantiations(), Case);
+      }
+
       // Each declaration initialization is its own full-expression.
       FullExpressionRAII Scope(Info);
       if (!EvaluateDecl(Info, D, /*EvaluateConditionDecl=*/true) &&
@@ -6309,6 +6315,40 @@ static EvalStmtResult EvaluateStmt(StmtResult &Result, EvalInfo &Info,
     return Scope.destroy() ? ESR_Succeeded : ESR_Failed;
   }
 
+  case Stmt::CXXExpansionStmtInstantiationClass: {
+    BlockScopeRAII Scope(Info);
+    const auto *Expansion = cast<CXXExpansionStmtInstantiation>(S);
+    for (const Stmt *Shared : Expansion->getSharedStmts()) {
+      EvalStmtResult ESR = EvaluateStmt(Result, Info, Shared);
+      if (ESR != ESR_Succeeded) {
+        if (ESR != ESR_Failed && !Scope.destroy())
+          return ESR_Failed;
+        return ESR;
+      }
+    }
+
+    // No need to push an extra scope for these since they're already
+    // CompoundStmts.
+    EvalStmtResult ESR = ESR_Succeeded;
+    for (const Stmt *Instantiation : Expansion->getInstantiations()) {
+      ESR = EvaluateStmt(Result, Info, Instantiation);
+      if (ESR == ESR_Failed ||
+          ShouldPropagateBreakContinue(Info, Expansion, &Scope, ESR))
+        return ESR;
+      if (ESR != ESR_Continue) {
+        // Succeeded here actually means we encountered a 'break'.
+        assert(ESR == ESR_Succeeded || ESR == ESR_Returned);
+        break;
+      }
+    }
+
+    // Map Continue back to Succeeded if we fell off the end of the loop.
+    if (ESR == ESR_Continue)
+      ESR = ESR_Succeeded;
+
+    return Scope.destroy() ? ESR : ESR_Failed;
+  }
+
   case Stmt::SwitchStmtClass:
     return EvaluateSwitch(Result, Info, cast<SwitchStmt>(S));
 
diff --git a/clang/lib/CodeGen/CGDecl.cpp b/clang/lib/CodeGen/CGDecl.cpp
index 8b1cd83af2396..678d2e9fa743a 100644
--- a/clang/lib/CodeGen/CGDecl.cpp
+++ b/clang/lib/CodeGen/CGDecl.cpp
@@ -143,6 +143,13 @@ void CodeGenFunction::EmitDecl(const Decl &D, bool EvaluateConditionDecl) {
     // None of these decls require codegen support.
     return;
 
+  case Decl::CXXExpansionStmt: {
+    const auto *ESD = cast<CXXExpansionStmtDecl>(&D);
+    assert(ESD->getInstantiations() && "expansion statement not expanded?");
+    EmitStmt(ESD->getInstantiations());
+    return;
+  }
+
   case Decl::NamespaceAlias:
     if (CGDebugInfo *DI = getDebugInfo())
         DI->EmitNamespaceAlias(cast<NamespaceAliasDecl>(D));
diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp
index 8030aac3d8771..e398710ace63b 100644
--- a/clang/lib/Sema/SemaDeclCXX.cpp
+++ b/clang/lib/Sema/SemaDeclCXX.cpp
@@ -2027,6 +2027,9 @@ static bool CheckConstexprDeclStmt(Sema &SemaRef, const FunctionDecl *Dcl,
       //   - using-enum-declaration
       continue;
 
+    case Decl::CXXExpansionStmt:
+      continue;
+
     case Decl::Typedef:
     case Decl::TypeAlias: {
       //   - typedef declarations and alias-declarations that do not define
diff --git a/clang/test/SemaCXX/cxx2c-expansion-stmts.cpp b/clang/test/SemaCXX/cxx2c-expansion-stmts.cpp
new file mode 100644
index 0000000000000..71ce71c4f69fe
--- /dev/null
+++ b/clang/test/SemaCXX/cxx2c-expansion-stmts.cpp
@@ -0,0 +1,1042 @@
+// RUN: %clang_cc1 %s -std=c++2c -fsyntax-only -fdeclspec -fblocks -verify
+namespace std {
+template <typename T>
+struct initializer_list {
+  const T* a;
+  const T* b;
+  initializer_list(T* a, T* b): a{a}, b{b} {}
+};
+}
+
+struct S {
+  int x;
+  constexpr S(int x) : x{x} {}
+};
+
+void g(int); // #g
+template <int n> constexpr int tg() { return n; }
+
+void f1() {
+  template for (auto x : {}) static_assert(false, "discarded");
+  template for (constexpr auto x : {}) static_assert(false, "discarded");
+  template for (auto x : {1}) g(x);
+  template for (auto x : {1, 2, 3}) g(x);
+  template for (constexpr auto x : {1}) g(x);
+  template for (constexpr auto x : {1, 2, 3}) g(x);
+  template for (constexpr auto x : {1}) tg<x>();
+  template for (constexpr auto x : {1, 2, 3})
+    static_assert(tg<x>());
+
+  template for (int x : {1, 2, 3}) g(x);
+  template for (S x : {1, 2, 3}) g(x.x);
+  template for (constexpr S x : {1, 2, 3}) tg<x.x>();
+
+  template for (int x : {"1", S(1), {1, 2}}) { // expected-error {{cannot initialize a variable of type 'int' with an lvalue of type 'const char[2]'}} \
+                                                  expected-error {{no viable conversion from 'S' to 'int'}} \
+                                                  expected-error {{excess elements in scalar initializer}} \
+                                                  expected-note 3 {{in instantiation of expansion statement requested here}}
+    g(x);
+  }
+
+  template for (constexpr auto x : {1, 2, 3, 4}) { // expected-note 3 {{in instantiation of expansion statement requested here}}
+    static_assert(tg<x>() == 4); // expected-error 3 {{static assertion failed due to requirement 'tg<x>() == 4'}} \
+                                    expected-note {{expression evaluates to '1 == 4'}} \
+                                    expected-note {{expression evaluates to '2 == 4'}} \
+                                    expected-note {{expression evaluates to '3 == 4'}}
+  }
+
+
+  template for (constexpr auto x : {1, 2}) { // expected-note 2 {{in instantiation of expansion statement requested here}}
+    static_assert(false, "not discarded"); // expected-error 2 {{static assertion failed: not discarded}}
+  }
+}
+
+template <typename T>
+void t1() {
+  template for (T x : {}) g(x);
+  template for (constexpr T x : {}) g(x);
+  template for (auto x : {}) g(x);
+  template for (constexpr auto x : {}) g(x);
+  template for (T x : {1, 2}) g(x);
+  template for (T x : {T(1), T(2)}) g(x);
+  template for (auto x : {T(1), T(2)}) g(x);
+  template for (constexpr T x : {T(1), T(2)}) static_assert(tg<x>());
+  template for (constexpr auto x : {T(1), T(2)}) static_assert(tg<x>());
+}
+
+template <typename U>
+struct s1 {
+  template <typename T>
+  void tf() {
+      template for (T x : {}) g(x);
+      template for (constexpr T x : {}) g(x);
+      template for (U x : {}) g(x);
+      template for (constexpr U x : {}) g(x);
+      template for (auto x : {}) g(x);
+      template for (constexpr auto x : {}) g(x);
+      template for (T x : {1, 2}) g(x);
+      template for (U x : {1, 2}) g(x);
+      template for (U x : {T(1), T(2)}) g(x);
+      template for (T x : {U(1), U(2)}) g(x);
+      template for (auto x : {T(1), T(2)}) g(x);
+      template for (auto x : {U(1), T(2)}) g(x);
+      template for (constexpr U x : {T(1), T(2)}) static_assert(tg<x>());
+      template for (constexpr T x : {U(1), U(2)}) static_assert(tg<x>());
+      template for (constexpr auto x : {T(1), U(2)}) static_assert(tg<x>());
+    }
+};
+
+template <typename T>
+void t2() {
+  template for (T x : {}) g(x);
+}
+
+void f2() {
+  t1<int>();
+  t1<long>();
+  s1<long>().tf<long>();
+  s1<int>().tf<int>();
+  s1<int>().tf<long>();
+  s1<long>().tf<int>();
+  t2<S>();
+  t2<S[1231]>();
+  t2<S***>();
+}
+
+template <__SIZE_TYPE__ size>
+struct String {
+  char data[size];
+
+  template <__SIZE_TYPE__ n>
+  constexpr String(const char (&str)[n]) { __builtin_memcpy(data, str, n); }
+
+  constexpr const char* begin() const { return data; }
+  constexpr const char* end() const { return data + size - 1; }
+};
+
+template <__SIZE_TYPE__ n>
+String(const char (&str)[n]) -> String<n>;
+
+constexpr int f3() {
+  static constexpr String s{"abcd"};
+  int count = 0;
+  template for (constexpr auto x : s) count++;
+  return count;
+}
+
+template <String s>
+constexpr int tf3() {
+  int count = 0;
+  template for (constexpr auto x : s) count++;
+  return count;
+}
+
+static_assert(f3() == 4);
+static_assert(tf3<"1">() == 1);
+static_assert(tf3<"12">() == 2);
+static_assert(tf3<"123">() == 3);
+static_assert(tf3<"1234">() == 4);
+
+void f4() {
+  static constexpr String empty{""};
+  static constexpr String s{"abcd"};
+  template for (auto x : empty) static_assert(false, "not expanded");
+  template for (constexpr auto x : s) g(x);
+  template for (auto x : s) g(x);
+}
+
+struct NegativeSize {
+  static constexpr const char* str = "123";
+  constexpr const char* begin() const { return str + 3; }
+  constexpr const char* end() const { return str; }
+};
+
+void negative_size() {
+  static constexpr NegativeSize n;
+  template for (auto x : n) g(x); // expected-error {{expansion size must not be negative (was -3)}}
+  template for (constexpr auto x : n) g(x); // expected-error {{expansion size must not be negative (was -3)}}
+}
+
+template <typename T, __SIZE_TYPE__ size>
+struct Array {
+  T data[size]{};
+  constexpr const T* begin() const { return data; }
+  constexpr const T* end() const { return data + size; }
+};
+
+struct NotInt {
+  struct iterator {};
+  constexpr iterator begin() const { return {}; }
+  constexpr iterator end() const { return {}; }
+};
+
+void not_int() {
+  static constexpr NotInt ni;
+  template for (auto x : ni) g(x); // expected-error {{invalid operands to binary expression}}
+}
+
+static constexpr Array<int, 3> integers{1, 2, 3};
+
+constexpr int friend_func();
+
+struct Private {
+  friend constexpr int friend_func();
+
+private:
+  constexpr const int* begin() const { return integers.begin(); } // expected-note 2 {{declared private here}}
+  constexpr const int* end() const { return integers.end(); } // expected-note 2 {{declared private here}}
+
+public:
+  static constexpr int member_func() {
+    int sum = 0;
+    static constexpr Private p1;
+    template for (auto x : p1) sum += x;
+    return sum;
+  }
+};
+
+struct Protected {
+  friend constexpr int friend_func();
+
+protected:
+  constexpr const int* begin() const { return integers.begin(); } // expected-note 2 {{declared protected here}}
+  constexpr const int* end() const { return integers.end(); } // expected-note 2 {{declared protected here}}
+
+public:
+  static constexpr int member_func() {
+    int sum = 0;
+    static constexpr Protected p1;
+    template for (auto x : p1) sum += x;
+    return sum;
+  }
+};
+
+void access_control() {
+  static constexpr Private p1;
+  template for (auto x : p1) g(x); // expected-error 2 {{'begin' is a private member of 'Private'}} expected-error 2 {{'end' is a private member of 'Private'}}
+
+  static constexpr Protected p2;
+  template for (auto x : p2) g(x); // expected-error 2 {{'begin' is a protected member of 'Protected'}} expected-error 2 {{'end' is a protected member of 'Protected'}}
+}
+
+constexpr int friend_func() {
+  int sum = 0;
+  static constexpr Private p1;
+  template for (auto x : p1) sum += x;
+
+  static constexpr Protected p2;
+  template for (auto x : p2) sum += x;
+  return sum;
+}
+
+static_assert(friend_func() == 12);
+static_assert(Private::member_func() == 6);
+static_assert(Protected::member_func() == 6);
+
+struct SizeNotICE {
+  struct iterator {
+    friend constexpr iterator operator+(iterator a, __PTRDIFF_TYPE__) { return a; }
+    int constexpr operator*() const { return 7; }
+
+    // NOT constexpr!
+    friend int operator-(iterator, iterator) { return 7; } // expected-note {{declared here}}
+    friend int operator!=(iterator, iterator) { return 7; }
+  };
+  constexpr iterator begin() const { return {}; }
+  constexpr iterator end() const { return {}; }
+};
+
+struct PlusMissing {
+  struct iterator {
+    int constexpr operator*() const { return 7; }
+  };
+  constexpr iterator begin() const { return {}; }
+  constexpr iterator end() const { return {}; }
+};
+
+struct DerefMissing {
+  struct iterator {
+    friend constexpr iterator operator+(iterator a, __PTRDIFF_TYPE__) { return a; }
+  };
+  constexpr iterator begin() const { return {}; }
+  constexpr iterator end() const { return {}; }
+};
+
+void missing_funcs() {
+  static constexpr SizeNotICE s1;
+  static constexpr PlusMissing s2;
+  static constexpr DerefMissing s3;
+
+  // TODO: This message should start complaining about '!=' once we support the
+  // proper way of computing the size.
+  template for (auto x : s1) g(x); // expected-error {{expansion size is not a constant expression}} \
+                                      expected-note {{non-constexpr function 'operator-' cannot be used in a constant expression}}
+
+  template for (auto x : s2) g(x); // expected-error {{invalid operands to binary expression}}
+  template for (auto x : s3) g(x); // expected-error {{indirection requires pointer operand ('iterator' invalid)}}
+}
+
+namespace adl {
+struct ADL {
+
+};
+
+constexpr const int* begin(const ADL&) { return integers.begin(); }
+constexpr const int* end(const ADL&) { return integers.end(); }
+}
+
+namespace adl_error {
+struct ADLError1 {
+  constexpr const int* begin() const { return integers.begin(); }
+};
+
+struct ADLError2 {
+  constexpr const int* end() const { return integers.end(); }
+};
+
+constexpr const int* begin(const ADLError2&) { return integers.begin(); }
+constexpr const int* end(const ADLError1&) { return integers.end(); }
+}
+
+namespace adl_both {
+static constexpr Array<int, 5> integers2{1, 2, 3, 4, 5};
+struct ADLBoth {
+  // Test that member begin/end are preferred over ADl begin/end. These return
+  // pointers to a different array.
+  constexpr const int* begin() const { return integers2.begin(); }
+  constexpr const int* end() const { return integers2.end(); }
+};
+
+constexpr const int* begin(const ADLBoth&) { return integers.begin(); }
+constexpr const int* end(const ADLBoth&) { return integers.end(); }
+}
+
+constexpr int adl_begin_end() {
+  static constexpr adl::ADL a;
+  int sum = 0;
+  template for (auto x : a) sum += x;
+  template for (constexpr auto x : a) sum += x;
+  return sum;
+}
+
+static_assert(adl_begin_end() == 12);
+
+void adl_mixed() {
+  static constexpr adl_error::ADLError1 a1;
+  static constexpr adl_error::ADLError2 a2;
+
+  // These are actually destructuring because there is no
+  // valid begin/end pair.
+  template for (auto x : a1) g(x);
+  template for (auto x : a2) g(x);
+}
+
+constexpr int adl_both_test() {
+  static constexpr adl_both::ADLBoth a;
+  int sum = 0;
+  template for (auto x : a) sum += x;
+  return sum;
+}
+
+static_assert(adl_both_test() == 15);
+
+struct A {};
+struct B { int x = 1; };
+struct C { int a = 1, b = 2, c = 3; };
+struct D {
+  int a = 1;
+  int* b = nullptr;
+  const char* c = "3";
+};
+
+struct Nested {
+  A a;
+  B b;
+  C c;
+};
+
+struct PrivateDestructurable {
+  friend void destructurable_friend();
+private:
+  int a, b; // expected-note 4 {{declared private here}}
+};
+
+struct ProtectedDestructurable {
+  friend void destructurable_friend();
+protected:
+  int a, b; // expected-note 4 {{declared protected here}}
+};
+
+void destructuring() {
+  static constexpr A a;
+  static constexpr B b;
+  static constexpr C c;
+  static constexpr D d;
+
+  template for (auto x : a) static_assert(false, "not expanded");
+  template for (constexpr auto x : a) static_assert(false, "not expanded");
+
+  template for (auto x : b) g(x);
+  template for (constexpr auto x : b) g(x);
+
+  template for (auto x : c) g(x);
+  template for (constexpr auto x : c) g(x);
+
+  template for (auto x : d) { // expected-note 2 {{in instantiation of expansion statement requested here}}
+    // expected-note@#g {{candidate function not viable: no known conversion from 'int *' to 'int' for 1st argument}}
+    // expected-note@#g {{candidate function not viable: no known conversion from 'const char *' to 'int' for 1st argument}}
+    g(x); // expected-error 2 {{no matching function for call to 'g'}}
+
+  }
+
+  template for (constexpr auto x : d) { // expected-note 2 {{in instantiation of expansion statement requested here}}
+    // expected-note@#g {{candidate function not viable: no known conversion from 'int *const' to 'int' for 1st argument}}
+    // expected-note@#g {{candidate function not viable: no known conversion from 'const char *const' to 'int' for 1st argument}}
+    g(x); // expected-error 2 {{no matching function for call to 'g'}}
+  }
+}
+
+constexpr int array() {
+  static constexpr int x[4]{1, 2, 3, 4};
+  int sum = 0;
+  template for (auto y : x) sum += y;
+  template for (constexpr auto y : x) sum += y;
+  return sum;
+}
+
+static_assert(array() == 20);
+
+template <auto v>
+constexpr int destructure() {
+  int sum = 0;
+  template for (auto x : v) sum += x;
+  template for (constexpr auto x : v) sum += x;
+  return sum;
+}
+
+static_assert(destructure<B{10}>() == 20);
+static_assert(destructure<C{}>() == 12);
+static_assert(destructure<C{3, 4, 5}>() == 24);
+
+constexpr int nested() {
+  static constexpr Nested n;
+  int sum = 0;
+  template for (constexpr auto x : n) {
+    static constexpr auto val = x;
+    template for (auto y : val) {
+      sum += y;
+    }
+  }
+  template for (constexpr auto x : n) {
+    static constexpr auto val = x;
+    template for (constexpr auto y : val) {
+      sum += y;
+    }
+  }
+  return sum;
+}
+
+static_assert(nested() == 14);
+
+void access_control_destructurable() {
+  template for (auto x : PrivateDestructurable()) {} // expected-error 2 {{cannot bind private member 'a' of 'PrivateDestructurable'}} \
+                                                        expected-error 2 {{cannot bind private member 'b' of 'PrivateDestructurable'}}
+
+  template for (auto x : ProtectedDestructurable()) {} // expected-error 2 {{cannot bind protected member 'a' of 'ProtectedDestructurable'}} \
+                                                          expected-error 2 {{cannot bind protected member 'b' of 'ProtectedDestructurable'}}
+}
+
+void destructurable_friend() {
+  template for (auto x : PrivateDestructurable()) {}
+  template for (auto x : ProtectedDestructurable()) {}
+}
+
+struct Placeholder {
+  A get_value() const { return {}; }
+  __declspec(property(get = get_value)) A a;
+};
+
+void placeholder() {
+  template for (auto x: Placeholder().a) {}
+}
+
+union Union { int a; long b;};
+
+struct MemberPtr {
+  void f() {}
+};
+
+void overload_set(int); // expected-note 2 {{possible target for call}}
+void overload_set(long); // expected-note 2 {{possible target for call}}
+
+void invalid_types() {
+  template for (auto x : void()) {} // expected-error {{cannot expand expression of type 'void'}}
+  template for (auto x : 1) {} // expected-error {{cannot expand expression of type 'int'}}
+  template for (auto x : 1.f) {} // expected-error {{cannot expand expression of type 'float'}}
+  template for (auto x : 'c') {} // expected-error {{cannot expand expression of type 'char'}}
+  template for (auto x : invalid_types) {} // expected-error {{cannot expand expression of type 'void ()'}}
+  template for (auto x : &invalid_types) {} // expected-error {{cannot expand expression of type 'void (*)()'}}
+  template for (auto x : &MemberPtr::f) {} // expected-error {{cannot expand expression of type 'void (MemberPtr::*)()'}}
+  template for (auto x : overload_set) {} // expected-error{{reference to overloaded function could not be resolved; did you mean to call it?}}
+  template for (auto x : &overload_set) {} // expected-error{{reference to overloaded function could not be resolved; did you mean to call it?}}
+  template for (auto x : nullptr) {} // expected-error {{cannot expand expression of type 'std::nullptr_t'}}
+  template for (auto x : __builtin_strlen) {} // expected-error {{builtin functions must be directly called}}
+  template for (auto x : Union()) {} // expected-error {{cannot expand expression of type 'Union'}}
+  template for (auto x : (char*)nullptr) {} // expected-error {{cannot expand expression of type 'char *'}}
+  template for (auto x : []{}) {} // expected-error {{cannot expand lambda closure type}}
+  template for (auto x : [x=3]{}) {} // expected-error {{cannot expand lambda closure type}}
+}
+
+struct BeginOnly {
+  int x{1};
+  constexpr const int* begin() const { r...
[truncated]

@Sirraide Sirraide marked this pull request as ready for review November 26, 2025 17:46
@llvmbot llvmbot added the clang Clang issues not falling into any other category label Nov 26, 2025
@Sirraide Sirraide force-pushed the users/Sirraide/expansion-stmts-7-constexpr-and-tests branch from d3761e5 to f1a507f Compare November 26, 2025 18:05
@Sirraide Sirraide force-pushed the users/Sirraide/expansion-stmts-6-destructuring branch from 43c4a23 to 45151d1 Compare November 26, 2025 18:05
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 Clang issues not falling into any other category

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants