[clang] [Clang][Parse] Fix assertion when annotating a failed decltype-specifier (PR #211221)
Akash Agrawal via cfe-commits
cfe-commits at lists.llvm.org
Sun Jul 26 22:09:36 PDT 2026
https://github.com/akashagrwl updated https://github.com/llvm/llvm-project/pull/211221
>From 91df3e236c2486c4c7c669cea3ea8dfcfae5d4df Mon Sep 17 00:00:00 2001
From: Akash Agrawal <akashag at qti.qualcomm.com>
Date: Wed, 22 Jul 2026 02:55:39 -0700
Subject: [PATCH 1/3] [Clang][Parse] Fix assertion when annotating a failed
decltype-specifier
When `ParseOptionalCXXScopeSpecifier` sees a leading `decltype`, it parses the
decltype-specifier and then calls `AnnotateExistingDecltypeSpecifier` to turn
the consumed tokens into a single `annot_decltype` token. If the
decltype-specifier fails to parse (e.g. `decltype` is not followed by `(`),
error recovery may skip tokens, leaving the `EndLoc` returned by
`ParseDecltypeSpecifier` stale. Annotating with that stale end location no
longer matches the most recent cached token and trips the invariant asserted
in `Preprocessor::AnnotatePreviousCachedTokens`:
```cpp
int decltype; // crashed
int *decltype = 0; // crashed
```
Fix it the same way the sibling pack-indexing path
(`AnnotateExistingIndexedTypeNamePack`) already does: on error, re-derive
`EndLoc` from the last cached token. Unlike the pack-indexing branch, this path
can be entered with a single `decltype` token, so the backtracking cache may be
empty after `RevertCachedTokens`; guard the query with a new
`Preprocessor::hasCachedTokenLocation()` helper so we never call
`getLastCachedTokenLocation()` (which asserts `CachedLexPos != 0`) on an empty
cache.
Fixes #211207
---
clang/include/clang/Lex/Preprocessor.h | 4 ++++
clang/lib/Parse/ParseDeclCXX.cpp | 14 ++++++++++++++
clang/test/Parser/decltype-crash.cpp | 14 ++++++++++++++
3 files changed, 32 insertions(+)
create mode 100644 clang/test/Parser/decltype-crash.cpp
diff --git a/clang/include/clang/Lex/Preprocessor.h b/clang/include/clang/Lex/Preprocessor.h
index 1c917dcfe7b7e..95a8074215014 100644
--- a/clang/include/clang/Lex/Preprocessor.h
+++ b/clang/include/clang/Lex/Preprocessor.h
@@ -1991,6 +1991,10 @@ class Preprocessor {
return CachedTokens[CachedLexPos-1].getLastLoc();
}
+ /// Whether there is a cached token whose location
+ /// getLastCachedTokenLocation() can return.
+ bool hasCachedTokenLocation() const { return CachedLexPos != 0; }
+
/// Whether \p Tok is the most recent token (`CachedLexPos - 1`) in
/// CachedTokens.
bool IsPreviousCachedToken(const Token &Tok) const;
diff --git a/clang/lib/Parse/ParseDeclCXX.cpp b/clang/lib/Parse/ParseDeclCXX.cpp
index 150987dae332d..dd57495634218 100644
--- a/clang/lib/Parse/ParseDeclCXX.cpp
+++ b/clang/lib/Parse/ParseDeclCXX.cpp
@@ -1136,6 +1136,20 @@ void Parser::AnnotateExistingDecltypeSpecifier(const DeclSpec &DS,
// make sure we have a token we can turn into an annotation token
if (PP.isBacktrackEnabled()) {
PP.RevertCachedTokens(1);
+ if (DS.getTypeSpecType() == TST_error && PP.hasCachedTokenLocation()) {
+ // The decltype-specifier failed to parse (e.g. 'decltype' not followed
+ // by '('), so error recovery may have skipped tokens to resynchronize.
+ // The EndLoc computed before that skip is now stale and no longer
+ // matches the most recent cached token, which would trip the invariant
+ // in Preprocessor::AnnotatePreviousCachedTokens. Annotate up to the last
+ // cached token instead, as AnnotateExistingIndexedTypeNamePack does.
+ // Unlike that pack-indexing path (which needs several lookahead tokens
+ // to be reached), this one can fire with just the 'decltype' token
+ // cached, so the cache may be empty after RevertCachedTokens; guard
+ // against that, since getLastCachedTokenLocation() requires a cached
+ // token.
+ EndLoc = PP.getLastCachedTokenLocation();
+ }
} else
PP.EnterToken(Tok, /*IsReinject*/ true);
diff --git a/clang/test/Parser/decltype-crash.cpp b/clang/test/Parser/decltype-crash.cpp
new file mode 100644
index 0000000000000..c892c18069d87
--- /dev/null
+++ b/clang/test/Parser/decltype-crash.cpp
@@ -0,0 +1,14 @@
+// RUN: %clang_cc1 -fsyntax-only -verify -std=c++11 %s
+// RUN: %clang_cc1 -fsyntax-only -verify -std=c++20 %s
+
+// These used to assert in Preprocessor::AnnotatePreviousCachedTokens instead
+// of just diagnosing the error: ParseOptionalCXXScopeSpecifier annotated a
+// decltype-specifier that failed to parse (no '(' after 'decltype'), using a
+// stale end-location left over from error recovery.
+int decltype = 0;
+// expected-error at -1 {{expected '(' after 'decltype'}}
+// expected-error at -2 {{expected unqualified-id}}
+
+int *decltype = 0;
+// expected-error at -1 {{expected '(' after 'decltype'}}
+// expected-error at -2 {{expected unqualified-id}}
>From d743aa4830ce451b09aec48fef11fbb9fc55f919 Mon Sep 17 00:00:00 2001
From: Akash Agrawal <akashag at qti.qualcomm.com>
Date: Thu, 23 Jul 2026 22:24:16 -0700
Subject: [PATCH 2/3] Reworked the fix as per the comments
---
clang/docs/ReleaseNotes.md | 4 ++++
clang/include/clang/Lex/Preprocessor.h | 4 ----
clang/lib/Parse/ParseDeclCXX.cpp | 14 --------------
clang/lib/Parse/ParseExprCXX.cpp | 8 ++++++++
clang/test/Parser/decltype-crash.cpp | 9 +++++----
5 files changed, 17 insertions(+), 22 deletions(-)
diff --git a/clang/docs/ReleaseNotes.md b/clang/docs/ReleaseNotes.md
index 50e1c4ce88797..1d410f8da145e 100644
--- a/clang/docs/ReleaseNotes.md
+++ b/clang/docs/ReleaseNotes.md
@@ -355,6 +355,10 @@ features cannot lower the translation-unit ABI level;
`[](Types... = args...) {}`). Clang now diagnoses the illegal default
argument instead of asserting. (#GH210714)
+- Fixed a crash on invalid code where a ``decltype`` not followed by ``(`` was
+ parsed where a nested-name-specifier could appear (e.g. ``int decltype = 0;``).
+ Clang now diagnoses the error instead of asserting. (#GH211207)
+
#### Bug Fixes to AST Handling
- Fixed a non-deterministic ordering of unused local typedefs that made
diff --git a/clang/include/clang/Lex/Preprocessor.h b/clang/include/clang/Lex/Preprocessor.h
index 95a8074215014..1c917dcfe7b7e 100644
--- a/clang/include/clang/Lex/Preprocessor.h
+++ b/clang/include/clang/Lex/Preprocessor.h
@@ -1991,10 +1991,6 @@ class Preprocessor {
return CachedTokens[CachedLexPos-1].getLastLoc();
}
- /// Whether there is a cached token whose location
- /// getLastCachedTokenLocation() can return.
- bool hasCachedTokenLocation() const { return CachedLexPos != 0; }
-
/// Whether \p Tok is the most recent token (`CachedLexPos - 1`) in
/// CachedTokens.
bool IsPreviousCachedToken(const Token &Tok) const;
diff --git a/clang/lib/Parse/ParseDeclCXX.cpp b/clang/lib/Parse/ParseDeclCXX.cpp
index dd57495634218..150987dae332d 100644
--- a/clang/lib/Parse/ParseDeclCXX.cpp
+++ b/clang/lib/Parse/ParseDeclCXX.cpp
@@ -1136,20 +1136,6 @@ void Parser::AnnotateExistingDecltypeSpecifier(const DeclSpec &DS,
// make sure we have a token we can turn into an annotation token
if (PP.isBacktrackEnabled()) {
PP.RevertCachedTokens(1);
- if (DS.getTypeSpecType() == TST_error && PP.hasCachedTokenLocation()) {
- // The decltype-specifier failed to parse (e.g. 'decltype' not followed
- // by '('), so error recovery may have skipped tokens to resynchronize.
- // The EndLoc computed before that skip is now stale and no longer
- // matches the most recent cached token, which would trip the invariant
- // in Preprocessor::AnnotatePreviousCachedTokens. Annotate up to the last
- // cached token instead, as AnnotateExistingIndexedTypeNamePack does.
- // Unlike that pack-indexing path (which needs several lookahead tokens
- // to be reached), this one can fire with just the 'decltype' token
- // cached, so the cache may be empty after RevertCachedTokens; guard
- // against that, since getLastCachedTokenLocation() requires a cached
- // token.
- EndLoc = PP.getLastCachedTokenLocation();
- }
} else
PP.EnterToken(Tok, /*IsReinject*/ true);
diff --git a/clang/lib/Parse/ParseExprCXX.cpp b/clang/lib/Parse/ParseExprCXX.cpp
index 06ff45da413ca..ea5aa447fe4fa 100644
--- a/clang/lib/Parse/ParseExprCXX.cpp
+++ b/clang/lib/Parse/ParseExprCXX.cpp
@@ -169,6 +169,14 @@ bool Parser::ParseOptionalCXXScopeSpecifier(
SourceLocation DeclLoc = Tok.getLocation();
SourceLocation EndLoc = ParseDecltypeSpecifier(DS);
+ // Drop an invalid decltype-specifier instead of annotating it as a
+ // nested-name-specifier: the EndLoc may be stale from error recovery,
+ // which would trip an assertion in
+ // Preprocessor::AnnotatePreviousCachedTokens. Mirrors the pack-indexing
+ // check below.
+ if (DS.getTypeSpecType() == DeclSpec::TST_error)
+ return false;
+
SourceLocation CCLoc;
// Work around a standard defect: 'decltype(auto)::' is not a
// nested-name-specifier.
diff --git a/clang/test/Parser/decltype-crash.cpp b/clang/test/Parser/decltype-crash.cpp
index c892c18069d87..af4622df5e54a 100644
--- a/clang/test/Parser/decltype-crash.cpp
+++ b/clang/test/Parser/decltype-crash.cpp
@@ -1,10 +1,11 @@
// RUN: %clang_cc1 -fsyntax-only -verify -std=c++11 %s
// RUN: %clang_cc1 -fsyntax-only -verify -std=c++20 %s
-// These used to assert in Preprocessor::AnnotatePreviousCachedTokens instead
-// of just diagnosing the error: ParseOptionalCXXScopeSpecifier annotated a
-// decltype-specifier that failed to parse (no '(' after 'decltype'), using a
-// stale end-location left over from error recovery.
+// A 'decltype' that is not followed by '(' fails to parse as a
+// decltype-specifier. ParseOptionalCXXScopeSpecifier used to still annotate it
+// and treat it as a nested-name-specifier, which tripped an assertion in
+// Preprocessor::AnnotatePreviousCachedTokens. It should just diagnose the
+// error instead.
int decltype = 0;
// expected-error at -1 {{expected '(' after 'decltype'}}
// expected-error at -2 {{expected unqualified-id}}
>From 2600220811849f48101e0f99d0e3e5340fbe45f6 Mon Sep 17 00:00:00 2001
From: Akash Agrawal <akashag at qti.qualcomm.com>
Date: Sun, 26 Jul 2026 22:09:00 -0700
Subject: [PATCH 3/3] fix the crash on the bot - add the annotation back
---
clang/include/clang/Lex/Preprocessor.h | 4 ++++
clang/lib/Parse/ParseDeclCXX.cpp | 13 +++++++++++++
clang/lib/Parse/ParseExprCXX.cpp | 8 --------
3 files changed, 17 insertions(+), 8 deletions(-)
diff --git a/clang/include/clang/Lex/Preprocessor.h b/clang/include/clang/Lex/Preprocessor.h
index 1c917dcfe7b7e..b22ff5b18fa41 100644
--- a/clang/include/clang/Lex/Preprocessor.h
+++ b/clang/include/clang/Lex/Preprocessor.h
@@ -1991,6 +1991,10 @@ class Preprocessor {
return CachedTokens[CachedLexPos-1].getLastLoc();
}
+ /// Whether there is a cached token that getLastCachedTokenLocation() can
+ /// return the location of (i.e. the backtracking cache is non-empty).
+ bool hasCachedTokenLocation() const { return CachedLexPos != 0; }
+
/// Whether \p Tok is the most recent token (`CachedLexPos - 1`) in
/// CachedTokens.
bool IsPreviousCachedToken(const Token &Tok) const;
diff --git a/clang/lib/Parse/ParseDeclCXX.cpp b/clang/lib/Parse/ParseDeclCXX.cpp
index 150987dae332d..a01460c9db3d2 100644
--- a/clang/lib/Parse/ParseDeclCXX.cpp
+++ b/clang/lib/Parse/ParseDeclCXX.cpp
@@ -1136,6 +1136,19 @@ void Parser::AnnotateExistingDecltypeSpecifier(const DeclSpec &DS,
// make sure we have a token we can turn into an annotation token
if (PP.isBacktrackEnabled()) {
PP.RevertCachedTokens(1);
+ if (DS.getTypeSpecType() == TST_error && PP.hasCachedTokenLocation()) {
+ // The decltype-specifier failed to parse (e.g. 'decltype' not followed
+ // by '('), so error recovery may have skipped tokens to resynchronize.
+ // The EndLoc computed before that skip is now stale and would no longer
+ // match the most recent cached token, tripping the invariant in
+ // Preprocessor::AnnotatePreviousCachedTokens. Annotate up to the last
+ // cached token instead, mirroring AnnotateExistingIndexedTypeNamePack.
+ // Unlike that pack-indexing path (which needs several lookahead tokens
+ // to be reached), this one can fire with just 'decltype' cached, so the
+ // cache may be empty after RevertCachedTokens; the guard avoids
+ // querying a location that does not exist.
+ EndLoc = PP.getLastCachedTokenLocation();
+ }
} else
PP.EnterToken(Tok, /*IsReinject*/ true);
diff --git a/clang/lib/Parse/ParseExprCXX.cpp b/clang/lib/Parse/ParseExprCXX.cpp
index ea5aa447fe4fa..06ff45da413ca 100644
--- a/clang/lib/Parse/ParseExprCXX.cpp
+++ b/clang/lib/Parse/ParseExprCXX.cpp
@@ -169,14 +169,6 @@ bool Parser::ParseOptionalCXXScopeSpecifier(
SourceLocation DeclLoc = Tok.getLocation();
SourceLocation EndLoc = ParseDecltypeSpecifier(DS);
- // Drop an invalid decltype-specifier instead of annotating it as a
- // nested-name-specifier: the EndLoc may be stale from error recovery,
- // which would trip an assertion in
- // Preprocessor::AnnotatePreviousCachedTokens. Mirrors the pack-indexing
- // check below.
- if (DS.getTypeSpecType() == DeclSpec::TST_error)
- return false;
-
SourceLocation CCLoc;
// Work around a standard defect: 'decltype(auto)::' is not a
// nested-name-specifier.
More information about the cfe-commits
mailing list