Skip to content

Conversation

@Sirraide
Copy link
Member

@Sirraide Sirraide commented Nov 26, 2025

This implements a limit on the maximum number of expansions; this isn’t in the standard (there is nothing on this in [implimits]), but my reasoning is that if someone makes a mistake and inadvertently causes e.g. 1 000 000 000 instantiations, Clang is going to hang... basically for ever (for comparison, 1 million instantiations takes about 3 seconds provided the body of the expansion statement is empty).

For now, I’ve set the limit to 256, but that’s probably too low. I don’t have strong opinions on the exact value; I definitely do think that there should be a limit though. We could in theory reuse the constexpr-steps value, but it feels a bit weird because this isn’t really doing constant evaluation so much as just template instantiation.

@Sirraide Sirraide changed the title [Clang] [C++26] Expansion Statements (Part 10) [Clang] [C++26] Expansion Statements (Part 10: Expansion Limits) Nov 26, 2025
@Sirraide Sirraide changed the title [Clang] [C++26] Expansion Statements (Part 10: Expansion Limits) [Clang] [C++26] Expansion Statements (Part 10: Expansion Limit) Nov 26, 2025
@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-driver

@llvm/pr-subscribers-clang-codegen

Author: None (Sirraide)

Changes

Full diff: https://github.com/llvm/llvm-project/pull/169689.diff

7 Files Affected:

  • (modified) clang/include/clang/Basic/DiagnosticSemaKinds.td (+4)
  • (modified) clang/include/clang/Basic/LangOptions.def (+1)
  • (modified) clang/include/clang/Options/Options.td (+4)
  • (modified) clang/lib/Driver/ToolChains/Clang.cpp (+1)
  • (modified) clang/lib/Sema/SemaExpand.cpp (+18)
  • (added) clang/test/SemaCXX/cxx2c-expansion-stmts-limit.cpp (+68)
  • (added) clang/test/SemaCXX/cxx2c-fexpansion-statements.cpp (+9)
diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td
index 5115c175849e1..222d587b48a0b 100644
--- a/clang/include/clang/Basic/DiagnosticSemaKinds.td
+++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td
@@ -169,6 +169,10 @@ def err_expansion_size_expr_not_ice : Error<
   "expansion size is not a constant expression">;
 def err_expansion_size_negative : Error<
   "expansion size must not be negative (was %0)">;
+def err_expansion_too_big : Error<
+  "expansion size %0 exceeds maximum configured size %1">;
+def note_use_fexpansion_limit : Note<
+  "use -fexpansion-limit=N to adjust this limit">;
 
 // Semantic analysis of constant literals.
 def ext_predef_outside_function : Warning<
diff --git a/clang/include/clang/Basic/LangOptions.def b/clang/include/clang/Basic/LangOptions.def
index 40fc66ea12e34..315cb4dc5e1cf 100644
--- a/clang/include/clang/Basic/LangOptions.def
+++ b/clang/include/clang/Basic/LangOptions.def
@@ -377,6 +377,7 @@ LANGOPT(ConstexprCallDepth, 32, 512, Benign,
         "maximum constexpr call depth")
 LANGOPT(ConstexprStepLimit, 32, 1048576, Benign,
         "maximum constexpr evaluation steps")
+LANGOPT(MaxTemplateForExpansions, 32, 256, Benign, "maximum template for expansions")
 LANGOPT(EnableNewConstInterp, 1, 0, Benign,
         "enable the experimental new constant interpreter")
 LANGOPT(BracketDepth, 32, 256, Benign,
diff --git a/clang/include/clang/Options/Options.td b/clang/include/clang/Options/Options.td
index 0e88656c5e1bc..86867b4814eeb 100644
--- a/clang/include/clang/Options/Options.td
+++ b/clang/include/clang/Options/Options.td
@@ -2054,6 +2054,10 @@ def fconstexpr_steps_EQ : Joined<["-"], "fconstexpr-steps=">, Group<f_Group>,
   Visibility<[ClangOption, CC1Option]>,
   HelpText<"Set the maximum number of steps in constexpr function evaluation (0 = no limit)">,
   MarshallingInfoInt<LangOpts<"ConstexprStepLimit">, "1048576">;
+def fexpansion_limit_EQ : Joined<["-"], "fexpansion-limit=">, Group<f_Group>,
+  Visibility<[ClangOption, CC1Option]>,
+  HelpText<"Set the maximum number of times a single expansion statement may be expanded (0 = no limit)">,
+  MarshallingInfoInt<LangOpts<"MaxTemplateForExpansions">, "256">;
 def fexperimental_new_constant_interpreter : Flag<["-"], "fexperimental-new-constant-interpreter">, Group<f_Group>,
   HelpText<"Enable the experimental new constant interpreter">,
   Visibility<[ClangOption, CC1Option]>,
diff --git a/clang/lib/Driver/ToolChains/Clang.cpp b/clang/lib/Driver/ToolChains/Clang.cpp
index c5d40c9825fab..649d4c3e4ca97 100644
--- a/clang/lib/Driver/ToolChains/Clang.cpp
+++ b/clang/lib/Driver/ToolChains/Clang.cpp
@@ -6358,6 +6358,7 @@ void Clang::ConstructJob(Compilation &C, const JobAction &JA,
   Args.AddLastArg(CmdArgs, options::OPT_foperator_arrow_depth_EQ);
   Args.AddLastArg(CmdArgs, options::OPT_fconstexpr_depth_EQ);
   Args.AddLastArg(CmdArgs, options::OPT_fconstexpr_steps_EQ);
+  Args.AddLastArg(CmdArgs, options::OPT_fexpansion_limit_EQ);
 
   Args.AddLastArg(CmdArgs, options::OPT_fexperimental_library);
 
diff --git a/clang/lib/Sema/SemaExpand.cpp b/clang/lib/Sema/SemaExpand.cpp
index fcc951503deb9..ad481df7e7d6a 100644
--- a/clang/lib/Sema/SemaExpand.cpp
+++ b/clang/lib/Sema/SemaExpand.cpp
@@ -43,6 +43,18 @@ struct IterableExpansionStmtData {
 };
 } // namespace
 
+static bool CheckExpansionSize(Sema &S, uint64_t NumInstantiations,
+                               SourceLocation Loc) {
+  unsigned Max = S.LangOpts.MaxTemplateForExpansions;
+  if (Max != 0 && NumInstantiations > Max) {
+    S.Diag(Loc, diag::err_expansion_too_big) << NumInstantiations << Max;
+    S.Diag(Loc, diag::note_use_fexpansion_limit);
+    return true;
+  }
+
+  return false;
+}
+
 // Build a 'DeclRefExpr' designating the template parameter '__N'.
 static DeclRefExpr *BuildIndexDRE(Sema &S, CXXExpansionStmtDecl *ESD) {
   return S.BuildDeclRefExpr(ESD->getIndexTemplateParm(),
@@ -198,6 +210,9 @@ static StmtResult BuildDestructuringCXXExpansionStmt(
     return StmtError();
   }
 
+  if (CheckExpansionSize(S, *Arity, ColonLoc))
+    return StmtError();
+
   QualType AutoRRef = S.Context.getAutoRRefDeductType();
   SmallVector<BindingDecl *> Bindings;
   for (unsigned I = 0; I < *Arity; ++I)
@@ -399,6 +414,9 @@ StmtResult Sema::FinishCXXExpansionStmt(Stmt *Exp, Stmt *Body) {
   if (!NumInstantiations)
     return StmtError();
 
+  if (CheckExpansionSize(*this, *NumInstantiations, Expansion->getColonLoc()))
+    return StmtError();
+
   // Collect shared statements.
   SmallVector<Stmt *, 1> Shared;
   if (Expansion->getInit())
diff --git a/clang/test/SemaCXX/cxx2c-expansion-stmts-limit.cpp b/clang/test/SemaCXX/cxx2c-expansion-stmts-limit.cpp
new file mode 100644
index 0000000000000..8411d6987565c
--- /dev/null
+++ b/clang/test/SemaCXX/cxx2c-expansion-stmts-limit.cpp
@@ -0,0 +1,68 @@
+// RUN: %clang_cc1 %s -std=c++2c -fsyntax-only -fexpansion-limit=32 -verify
+
+void g(int);
+
+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>;
+
+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; }
+};
+
+void expansion_size() {
+  static constexpr Array<int, 32> almost_too_big;
+  template for (auto x : almost_too_big) g(x);
+  template for (constexpr auto x : almost_too_big) g(x);
+
+  static constexpr Array<int, 33> too_big;
+  template for (auto x : too_big) g(x); // expected-error {{expansion size 33 exceeds maximum configured size 32}} expected-note {{use -fexpansion-limit=N to adjust this limit}}
+  template for (constexpr auto x : too_big) g(x); // expected-error {{expansion size 33 exceeds maximum configured size 32}} expected-note {{use -fexpansion-limit=N to adjust this limit}}
+
+  static constexpr String big{"1234567890123456789012345678901234567890234567890"};
+  template for (auto x : big) g(x); // expected-error {{expansion size 49 exceeds maximum configured size 32}} expected-note {{use -fexpansion-limit=N to adjust this limit}}
+  template for (constexpr auto x : big) g(x); // expected-error {{expansion size 49 exceeds maximum configured size 32}} expected-note {{use -fexpansion-limit=N to adjust this limit}}
+
+  template for (auto x : {1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
+    11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
+    21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
+    31, 32}) g(x);
+  template for (constexpr auto x : {1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
+    11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
+    21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
+    31, 32}) g(x);
+
+  template for (auto x : {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, // expected-error {{expansion size 33 exceeds maximum configured size 32}} expected-note {{use -fexpansion-limit=N to adjust this limit}}
+    11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
+    21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
+    31, 32, 33}) g(x);
+  template for (constexpr auto x : {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, // expected-error {{expansion size 33 exceeds maximum configured size 32}} expected-note {{use -fexpansion-limit=N to adjust this limit}}
+    11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
+    21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
+    31, 32, 33}) g(x);
+
+  int huge[1'000'000'000];
+  template for (auto x : huge) {} // expected-error {{expansion size 1000000000 exceeds maximum configured size 32}} expected-note {{use -fexpansion-limit=N to adjust this limit}}
+}
+
+void array_too_big() {
+  int ok[32];
+  int too_big[33];
+
+  template for (auto x : ok) {}
+  template for (auto x : too_big) {} // expected-error {{expansion size 33 exceeds maximum configured size 32}} \
+                                        expected-note {{use -fexpansion-limit=N to adjust this limit}}
+}
diff --git a/clang/test/SemaCXX/cxx2c-fexpansion-statements.cpp b/clang/test/SemaCXX/cxx2c-fexpansion-statements.cpp
new file mode 100644
index 0000000000000..2c80c392e400d
--- /dev/null
+++ b/clang/test/SemaCXX/cxx2c-fexpansion-statements.cpp
@@ -0,0 +1,9 @@
+// RUN: %clang_cc1 %s -std=c++2c -fsyntax-only -fexpansion-limit=0 -verify
+// expected-no-diagnostics
+
+// Test that passing =0 disables the limit.
+
+void big() {
+  int ok[500];
+  template for (auto x : ok) {}
+}

@Sirraide Sirraide marked this pull request as ready for review November 26, 2025 17:47
@llvmbot llvmbot added clang Clang issues not falling into any other category clang:driver 'clang' and 'clang++' user-facing binaries. Not 'clang-cl' labels Nov 26, 2025
@Sirraide Sirraide force-pushed the users/Sirraide/expansion-stmts-10-limits branch from 7314e79 to 9a24859 Compare November 26, 2025 18:05
@Sirraide Sirraide force-pushed the users/Sirraide/expansion-stmts-9-control-flow branch from 00062fb to 162cc15 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:driver 'clang' and 'clang++' user-facing binaries. Not 'clang-cl' 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