Skip to content

Conversation

AaronBallman
Copy link
Collaborator

@AaronBallman AaronBallman commented Oct 9, 2025

This implements the parts of https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3457.htm which were adopted at the recent meeting in Brno.

Clang already implemented __COUNTER__, but needed some changes for conformance. Specifically, we now diagnose when the macro is expanded more than 2147483647 times. Additionally, we now give the expected extension and pre-compat warnings for the feature.

To support testing the limits, this also adds a -cc1-only option, -finitial-counter-value=, which lets you specify the initial value the __COUNTER__ macro should expand to.

This implements the parts of https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3457.htm
which were adopted at the recent meeting in Brno.

Clang already implemented __COUNTER__, but needed some changes for
conformance. Specifically, we now diagnose when the macro is expanded
more than 2147483647 times. Additionally, we now give the expected
extension and pre-compat warnings for the feature.

To support testing the limits, this also adds a -cc1-only option,
-finitial-counter-value=, which lets you specify the initial value the
__COUNTER__ macro should expand to.
@AaronBallman AaronBallman added clang Clang issues not falling into any other category clang:frontend Language frontend issues, e.g. anything involving "Sema" c2y labels Oct 9, 2025
@llvmbot llvmbot added the clang:modules C++20 modules and Clang Header Modules label Oct 9, 2025
@llvmbot
Copy link
Member

llvmbot commented Oct 9, 2025

@llvm/pr-subscribers-clang-modules

@llvm/pr-subscribers-clang

Author: Aaron Ballman (AaronBallman)

Changes

This implements the parts of https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3457.htm which were adopted at the recent meeting in Brno.

Clang already implemented COUNTER, but needed some changes for conformance. Specifically, we now diagnose when the macro is expanded more than 2147483647 times. Additionally, we now give the expected extension and pre-compat warnings for the feature.

To support testing the limits, this also adds a -cc1-only option, -finitial-counter-value=, which lets you specify the initial value the COUNTER macro should expand to.


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

15 Files Affected:

  • (modified) clang/docs/LanguageExtensions.rst (+3-1)
  • (modified) clang/docs/ReleaseNotes.rst (+5)
  • (modified) clang/include/clang/Basic/DiagnosticLexKinds.td (+8)
  • (modified) clang/include/clang/Driver/Options.td (+4)
  • (modified) clang/include/clang/Lex/Preprocessor.h (+3-3)
  • (modified) clang/include/clang/Lex/PreprocessorOptions.h (+4)
  • (modified) clang/include/clang/Serialization/ASTReader.h (+5-3)
  • (modified) clang/lib/Frontend/ASTUnit.cpp (+5-4)
  • (modified) clang/lib/Frontend/InitPreprocessor.cpp (+3)
  • (modified) clang/lib/Lex/PPMacroExpansion.cpp (+12-1)
  • (modified) clang/lib/Serialization/ASTReader.cpp (+2-2)
  • (added) clang/test/C/C2y/n3457.c (+37)
  • (added) clang/test/C/C2y/n3457_1.c (+20)
  • (added) clang/test/C/C2y/n3457_2.c (+10)
  • (modified) clang/www/c_status.html (+1-1)
diff --git a/clang/docs/LanguageExtensions.rst b/clang/docs/LanguageExtensions.rst
index 6bb99c757cd19..54c215e9ccfaa 100644
--- a/clang/docs/LanguageExtensions.rst
+++ b/clang/docs/LanguageExtensions.rst
@@ -385,7 +385,9 @@ Builtin Macros
 
 ``__COUNTER__``
   Defined to an integer value that starts at zero and is incremented each time
-  the ``__COUNTER__`` macro is expanded.
+  the ``__COUNTER__`` macro is expanded. This is a standard feature in C2y but
+  is an extension in earlier language modes and in C++. This macro can only be
+  expanded 2147483647 times at most.
 
 ``__INCLUDE_LEVEL__``
   Defined to an integral value that is the include depth of the file currently
diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst
index 5e9a71e1e74d6..37e02e9638296 100644
--- a/clang/docs/ReleaseNotes.rst
+++ b/clang/docs/ReleaseNotes.rst
@@ -181,6 +181,11 @@ C Language Changes
 C2y Feature Support
 ^^^^^^^^^^^^^^^^^^^
 - Clang now supports `N3355 <https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3355.htm>`_ Named Loops.
+- Clang's implementation of ``__COUNTER__`` was updated to conform to
+  `WG14 N3457 <https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3457.htm>`_.
+  This includes adding pedantic warnings for the feature being an extension in
+  other language modes as well as an error when the counter is expanded more
+  than 2147483647 times.
 
 C23 Feature Support
 ^^^^^^^^^^^^^^^^^^^
diff --git a/clang/include/clang/Basic/DiagnosticLexKinds.td b/clang/include/clang/Basic/DiagnosticLexKinds.td
index c7fe6e1db6d1f..46a7a88f7a50b 100644
--- a/clang/include/clang/Basic/DiagnosticLexKinds.td
+++ b/clang/include/clang/Basic/DiagnosticLexKinds.td
@@ -90,6 +90,14 @@ def err_unterminated___pragma : Error<"missing terminating ')' character">;
 
 def err_conflict_marker : Error<"version control conflict marker in file">;
 
+def err_counter_overflow : Error<
+  "'__COUNTER__' value cannot exceed 2147483647">;
+def ext_counter : Extension<
+  "'__COUNTER__' is a C2y extension">, InGroup<C2y>;
+def warn_counter : Warning<
+  "'__COUNTER__' is incompatible with standards before C2y">,
+  InGroup<CPre2yCompat>, DefaultIgnore;
+
 def err_raw_delim_too_long : Error<
   "raw string delimiter longer than 16 characters"
   "; use PREFIX( )PREFIX to delimit raw string">;
diff --git a/clang/include/clang/Driver/Options.td b/clang/include/clang/Driver/Options.td
index c8e96e125733c..2511f88f4e53c 100644
--- a/clang/include/clang/Driver/Options.td
+++ b/clang/include/clang/Driver/Options.td
@@ -8417,6 +8417,10 @@ def aligned_alloc_unavailable : Flag<["-"], "faligned-alloc-unavailable">,
   MarshallingInfoFlag<LangOpts<"AlignedAllocationUnavailable">>,
   ShouldParseIf<faligned_allocation.KeyPath>;
 
+def finitial_counter_value_EQ : Joined<["-"], "finitial-counter-value=">,
+  HelpText<"Sets the initial value for __COUNTER__, defaults to 0.">,
+  MarshallingInfoInt<PreprocessorOpts<"InitialCounterValue">, "0">;
+
 } // let Visibility = [CC1Option]
 
 //===----------------------------------------------------------------------===//
diff --git a/clang/include/clang/Lex/Preprocessor.h b/clang/include/clang/Lex/Preprocessor.h
index 39754847a93e4..b79e6072cff15 100644
--- a/clang/include/clang/Lex/Preprocessor.h
+++ b/clang/include/clang/Lex/Preprocessor.h
@@ -226,7 +226,7 @@ class Preprocessor {
       LangOptions::FPEvalMethodKind::FEM_UnsetOnCommandLine;
 
   // Next __COUNTER__ value, starts at 0.
-  unsigned CounterValue = 0;
+  unsigned long CounterValue = 0;
 
   enum {
     /// Maximum depth of \#includes.
@@ -2421,8 +2421,8 @@ class Preprocessor {
   bool SawDateOrTime() const {
     return DATELoc != SourceLocation() || TIMELoc != SourceLocation();
   }
-  unsigned getCounterValue() const { return CounterValue; }
-  void setCounterValue(unsigned V) { CounterValue = V; }
+  unsigned long getCounterValue() const { return CounterValue; }
+  void setCounterValue(unsigned long V) { CounterValue = V; }
 
   LangOptions::FPEvalMethodKind getCurrentFPEvalMethod() const {
     assert(CurrentFPEvalMethod != LangOptions::FEM_UnsetOnCommandLine &&
diff --git a/clang/include/clang/Lex/PreprocessorOptions.h b/clang/include/clang/Lex/PreprocessorOptions.h
index d4c4e1ccbf2c4..2b65e9422a5e5 100644
--- a/clang/include/clang/Lex/PreprocessorOptions.h
+++ b/clang/include/clang/Lex/PreprocessorOptions.h
@@ -198,6 +198,10 @@ class PreprocessorOptions {
   /// If set, the UNIX timestamp specified by SOURCE_DATE_EPOCH.
   std::optional<uint64_t> SourceDateEpoch;
 
+  /// The initial value for __COUNTER__; typically is zero but can be set via a
+  /// -cc1 flag for testing purposes.
+  unsigned long InitialCounterValue = 0;
+
 public:
   PreprocessorOptions() : PrecompiledPreambleBytes(0, false) {}
 
diff --git a/clang/include/clang/Serialization/ASTReader.h b/clang/include/clang/Serialization/ASTReader.h
index af856a8097ab1..485bfad8c9e1a 100644
--- a/clang/include/clang/Serialization/ASTReader.h
+++ b/clang/include/clang/Serialization/ASTReader.h
@@ -221,7 +221,7 @@ class ASTReaderListener {
 
   /// Receives __COUNTER__ value.
   virtual void ReadCounter(const serialization::ModuleFile &M,
-                           unsigned Value) {}
+                           unsigned long Value) {}
 
   /// This is called for each AST file loaded.
   virtual void visitModuleFile(StringRef Filename,
@@ -312,7 +312,8 @@ class ChainedASTReaderListener : public ASTReaderListener {
                                bool Complain,
                                std::string &SuggestedPredefines) override;
 
-  void ReadCounter(const serialization::ModuleFile &M, unsigned Value) override;
+  void ReadCounter(const serialization::ModuleFile &M,
+                   unsigned long Value) override;
   bool needsInputFileVisitation() override;
   bool needsSystemInputFileVisitation() override;
   void visitModuleFile(StringRef Filename,
@@ -352,7 +353,8 @@ class PCHValidator : public ASTReaderListener {
                                StringRef ModuleFilename,
                                StringRef SpecificModuleCachePath,
                                bool Complain) override;
-  void ReadCounter(const serialization::ModuleFile &M, unsigned Value) override;
+  void ReadCounter(const serialization::ModuleFile &M,
+                   unsigned long Value) override;
 };
 
 /// ASTReaderListenter implementation to set SuggestedPredefines of
diff --git a/clang/lib/Frontend/ASTUnit.cpp b/clang/lib/Frontend/ASTUnit.cpp
index cb445682ac48b..ff48faa02f593 100644
--- a/clang/lib/Frontend/ASTUnit.cpp
+++ b/clang/lib/Frontend/ASTUnit.cpp
@@ -520,7 +520,7 @@ class ASTInfoCollector : public ASTReaderListener {
   CodeGenOptions &CodeGenOpts;
   std::shared_ptr<TargetOptions> &TargetOpts;
   IntrusiveRefCntPtr<TargetInfo> &Target;
-  unsigned &Counter;
+  unsigned long &Counter;
   bool InitializedLanguage = false;
   bool InitializedHeaderSearchPaths = false;
 
@@ -529,7 +529,8 @@ class ASTInfoCollector : public ASTReaderListener {
                    HeaderSearchOptions &HSOpts, PreprocessorOptions &PPOpts,
                    LangOptions &LangOpt, CodeGenOptions &CodeGenOpts,
                    std::shared_ptr<TargetOptions> &TargetOpts,
-                   IntrusiveRefCntPtr<TargetInfo> &Target, unsigned &Counter)
+                   IntrusiveRefCntPtr<TargetInfo> &Target,
+                   unsigned long &Counter)
       : PP(PP), Context(Context), HSOpts(HSOpts), PPOpts(PPOpts),
         LangOpt(LangOpt), CodeGenOpts(CodeGenOpts), TargetOpts(TargetOpts),
         Target(Target), Counter(Counter) {}
@@ -627,7 +628,7 @@ class ASTInfoCollector : public ASTReaderListener {
   }
 
   void ReadCounter(const serialization::ModuleFile &M,
-                   unsigned Value) override {
+                   unsigned long Value) override {
     Counter = Value;
   }
 
@@ -873,7 +874,7 @@ std::unique_ptr<ASTUnit> ASTUnit::LoadFromASTFile(
       /*isysroot=*/"",
       /*DisableValidationKind=*/disableValid, AllowASTWithCompilerErrors);
 
-  unsigned Counter = 0;
+  unsigned long Counter = 0;
   AST->Reader->setListener(std::make_unique<ASTInfoCollector>(
       *AST->PP, AST->Ctx.get(), *AST->HSOpts, *AST->PPOpts, *AST->LangOpts,
       *AST->CodeGenOpts, AST->TargetOpts, AST->Target, Counter));
diff --git a/clang/lib/Frontend/InitPreprocessor.cpp b/clang/lib/Frontend/InitPreprocessor.cpp
index 877ab02850667..aaf01705caa5e 100644
--- a/clang/lib/Frontend/InitPreprocessor.cpp
+++ b/clang/lib/Frontend/InitPreprocessor.cpp
@@ -1569,6 +1569,9 @@ void clang::InitializePreprocessor(Preprocessor &PP,
   llvm::raw_string_ostream Predefines(PredefineBuffer);
   MacroBuilder Builder(Predefines);
 
+  // Ensure that the initial value of __COUNTER__ is hooked up.
+  PP.setCounterValue(InitOpts.InitialCounterValue);
+
   // Emit line markers for various builtin sections of the file. The 3 here
   // marks <built-in> as being a system header, which suppresses warnings when
   // the same macro is defined multiple times.
diff --git a/clang/lib/Lex/PPMacroExpansion.cpp b/clang/lib/Lex/PPMacroExpansion.cpp
index dec1956ea0f9a..37eef9060d4b5 100644
--- a/clang/lib/Lex/PPMacroExpansion.cpp
+++ b/clang/lib/Lex/PPMacroExpansion.cpp
@@ -1737,7 +1737,18 @@ void Preprocessor::ExpandBuiltinMacro(Token &Tok) {
       Diag(getLastFPEvalPragmaLocation(), diag::note_pragma_entered_here);
     }
   } else if (II == Ident__COUNTER__) {
-    // __COUNTER__ expands to a simple numeric value.
+    Diag(Tok.getLocation(),
+         getLangOpts().C2y ? diag::warn_counter : diag::ext_counter);
+    // __COUNTER__ expands to a simple numeric value that must be less than
+    // 2147483647.
+    if (CounterValue > 2147483647) {
+      Diag(Tok.getLocation(), diag::err_counter_overflow);
+      // Retain the maximal value so we don't issue conversion-related
+      // diagnostics by overflowing into a long long. While this does produce
+      // a duplicate value, there's no way to ignore this error so there's no
+      // translation anyway.
+      CounterValue = 2147483647;
+    }
     OS << CounterValue++;
     Tok.setKind(tok::numeric_constant);
   } else if (II == Ident__has_feature) {
diff --git a/clang/lib/Serialization/ASTReader.cpp b/clang/lib/Serialization/ASTReader.cpp
index 6acf79acea111..32d0f36d17459 100644
--- a/clang/lib/Serialization/ASTReader.cpp
+++ b/clang/lib/Serialization/ASTReader.cpp
@@ -225,7 +225,7 @@ bool ChainedASTReaderListener::ReadPreprocessorOptions(
 }
 
 void ChainedASTReaderListener::ReadCounter(const serialization::ModuleFile &M,
-                                           unsigned Value) {
+                                           unsigned long Value) {
   First->ReadCounter(M, Value);
   Second->ReadCounter(M, Value);
 }
@@ -973,7 +973,7 @@ bool PCHValidator::ReadHeaderSearchOptions(const HeaderSearchOptions &HSOpts,
       PP.getPreprocessorOpts());
 }
 
-void PCHValidator::ReadCounter(const ModuleFile &M, unsigned Value) {
+void PCHValidator::ReadCounter(const ModuleFile &M, unsigned long Value) {
   PP.setCounterValue(Value);
 }
 
diff --git a/clang/test/C/C2y/n3457.c b/clang/test/C/C2y/n3457.c
new file mode 100644
index 0000000000000..77e7b7ed5ecfe
--- /dev/null
+++ b/clang/test/C/C2y/n3457.c
@@ -0,0 +1,37 @@
+// RUN: %clang_cc1 -verify=ext -std=c23 -pedantic %s
+// RUN: %clang_cc1 -verify=pre -std=c2y -pedantic -Wpre-c2y-compat %s
+
+/* WG14 N3457: Clang 22
+ * The __COUNTER__ predefined macro
+ *
+ * This predefined macro was supported as an extension in earlier versions of
+ * Clang, but the required diagnostics for the limits were not added until 22.
+ */
+
+// Ensure that __COUNTER__ starts from 0.
+static_assert(__COUNTER__ == 0); /* ext-warning {{'__COUNTER__' is a C2y extension}}
+                                    pre-warning {{'__COUNTER__' is incompatible with standards before C2y}}
+                                  */
+
+// Ensure that the produced value can be used with token concatenation.
+#define CAT_IMPL(a, b) a ## b
+#define CAT(a, b) CAT_IMPL(a, b)
+#define NAME_WITH_COUNTER(a) CAT(a, __COUNTER__)
+void test() {
+  // Because this is the 2nd expansion, this defines test1.
+  int NAME_WITH_COUNTER(test); /* ext-warning {{'__COUNTER__' is a C2y extension}}
+                                  pre-warning {{'__COUNTER__' is incompatible with standards before C2y}}
+                                */
+  int other_test = test1;      // Ok
+}
+
+// Ensure that __COUNTER__ increments each time you mention it.
+static_assert(__COUNTER__ == 2); /* ext-warning {{'__COUNTER__' is a C2y extension}}
+                                    pre-warning {{'__COUNTER__' is incompatible with standards before C2y}}
+                                 */
+static_assert(__COUNTER__ == 3); /* ext-warning {{'__COUNTER__' is a C2y extension}}
+                                    pre-warning {{'__COUNTER__' is incompatible with standards before C2y}}
+                                 */
+static_assert(__COUNTER__ == 4); /* ext-warning {{'__COUNTER__' is a C2y extension}}
+                                    pre-warning {{'__COUNTER__' is incompatible with standards before C2y}}
+                                 */
diff --git a/clang/test/C/C2y/n3457_1.c b/clang/test/C/C2y/n3457_1.c
new file mode 100644
index 0000000000000..2612d096e23fc
--- /dev/null
+++ b/clang/test/C/C2y/n3457_1.c
@@ -0,0 +1,20 @@
+// RUN: %clang_cc1 -verify -std=c2y -finitial-counter-value=2147483646 %s
+
+// The value produced needs to be a type that's representable with a signed
+// long. However, the actual type it expands to does *not* need to be forced to
+// be signed long because that would generally mean suffixing the value with L,
+// which would be very surprising for folks using this to generate unique ids.
+// We'll test this by ensuring the largest value can be expanded properly and
+// an assertion that signed long is always at least four bytes wide (which is
+// what's required to represent that maximal value).
+//
+// So we set the initial counter value to 2147483646, we'll validate that,
+// increment it once to get to the maximal value and ensure there's no
+// diagnostic, then increment again to ensure we get the constraint violation.
+
+static_assert(__COUNTER__ == 2147483646); // Test and increment
+static_assert(__COUNTER__ == 2147483647); // Test and increment
+
+// This one should fail.
+signed long i = __COUNTER__; // expected-error {{'__COUNTER__' value cannot exceed 2147483647}}
+
diff --git a/clang/test/C/C2y/n3457_2.c b/clang/test/C/C2y/n3457_2.c
new file mode 100644
index 0000000000000..cf268283e11c6
--- /dev/null
+++ b/clang/test/C/C2y/n3457_2.c
@@ -0,0 +1,10 @@
+// RUN: %clang_cc1 -verify=good -std=c2y -finitial-counter-value=2147483648 %s
+// RUN: %clang_cc1 -verify -std=c2y -finitial-counter-value=2147483648 -DEXPAND_IT %s
+// good-no-diagnostics
+
+// This sets the intial __COUNTER__ value to something that's too big. Setting
+// the value too large is fine. Expanding to a too-large value is not.
+#ifdef EXPAND_IT
+  // This one should fail.
+  signed long i = __COUNTER__; // expected-error {{'__COUNTER__' value cannot exceed 2147483647}}
+#endif
diff --git a/clang/www/c_status.html b/clang/www/c_status.html
index 380f66495a367..96dcaf14aae02 100644
--- a/clang/www/c_status.html
+++ b/clang/www/c_status.html
@@ -329,7 +329,7 @@ <h2 id="c2y">C2y implementation status</h2>
     <tr>
       <td>The __COUNTER__ predefined macro</td>
       <td><a href="https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3457.htm">N3457</a></td>
-      <td class="unknown" align="center">Unknown</td>
+      <td class="full" align="center">Clang 22</td>
 	</tr>
     <tr>
       <td>Chasing Ghosts I: constant expressions v2</td>

@AaronBallman
Copy link
Collaborator Author

The CI failures are because third-party/benchmark are compiled with -pedantic -Werror and they use __COUNTER__, which is an extension outside of C2y that's now being diagnosed.

I think the correct fix there is to check for C2y in:

#if defined(__COUNTER__) && (__COUNTER__ + 1 == __COUNTER__ + 0)

However, this is in third-party and I'm not certain whether fixing it upstream will cause other issues when we pull the changes down (it's been over a year since we updated this dependency). Certainly expanding BENCHMARK_PRIVATE_UNIQUE_ID to __COUNTER__ will emit the same diagnostic. So it's a bit odd for the project to use -pedantic -Werror while using extensions like __COUNTER__ in the first place. CC @rnk for some opinions on the right way to proceed here.

@cor3ntin
Copy link
Contributor

@AaronBallman This is exactly what i was afraid of would happen when adding an extra pedantic compatibility diagnostic!

@@ -0,0 +1,20 @@
// RUN: %clang_cc1 -verify -std=c2y -finitial-counter-value=2147483646 %s
Copy link
Collaborator

Choose a reason for hiding this comment

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

What happens if we set -finitial-counter-value to something larger than unsinged 32 max? Do we obtain a diagnostic?

Copy link
Collaborator

@hubert-reinterpretcast hubert-reinterpretcast left a comment

Choose a reason for hiding this comment

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

TMK, the wording says that the third number in the diagnostic message from the static_assert below should be less than the second because replacement of __VA_OPT__ with its expanded replacement occurs before the other instance of __COUNTER__ in the replacement list is encountered during rescanning and further replacement.

Clang generates "0 2 3 1". The 2 and 3 is transposed compared to expectations.

#define STR0(X)  #X
#define STR(X) STR0(X)
#define F(X, Y, ...) STR(Y __COUNTER__ __VA_OPT__(__COUNTER__) X)
static_assert(0, F(__COUNTER__, __COUNTER__, hi));

https://godbolt.org/z/aYGEq94va

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

Labels

c2y clang:frontend Language frontend issues, e.g. anything involving "Sema" clang:modules C++20 modules and Clang Header Modules clang Clang issues not falling into any other category

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants