[clang] [Sema][Serialization] Emit unused local typedefs in a deterministic order (PR #209639)
Steven Wu via cfe-commits
cfe-commits at lists.llvm.org
Tue Jul 14 14:48:56 PDT 2026
https://github.com/cachemeifyoucan created https://github.com/llvm/llvm-project/pull/209639
Sema::UnusedLocalTypedefNameCandidates is populated while iterating a Scope's
DeclsInScope, which is a SmallPtrSet whose iteration order depends on pointer
values and is therefore not stable across runs. The candidates are serialized
into the AST file -- both to assign declaration IDs and to write the
UNUSED_LOCAL_TYPEDEF_NAME_CANDIDATES record -- and are also used to emit the
deferred -Wunused-local-typedef warnings, so neither the emitted PCH/AST file
nor the diagnostics were reproducible. With deterministic compilation caching
this surfaces as a "cache poisoned" error, because two builds of the same PCH
produce different bytes.
Sort the candidates by source location at the point they are consumed, via
Sema::getSortedUnusedLocalTypedefNameCandidates(), so that both the diagnostics
and the serialized declarations are deterministic.
>From d48deb7cdf823690c5d035571fcca99ccb3a89fb Mon Sep 17 00:00:00 2001
From: Steven Wu <stevenwu at apple.com>
Date: Tue, 14 Jul 2026 14:48:43 -0700
Subject: [PATCH] =?UTF-8?q?[=F0=9D=98=80=F0=9D=97=BD=F0=9D=97=BF]=20initia?=
=?UTF-8?q?l=20version?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Created using spr 1.3.7
---
clang/include/clang/Sema/ExternalSemaSource.h | 2 +-
.../clang/Sema/MultiplexExternalSemaSource.h | 2 +-
clang/include/clang/Sema/Sema.h | 7 +-
clang/include/clang/Serialization/ASTReader.h | 2 +-
.../lib/Sema/MultiplexExternalSemaSource.cpp | 2 +-
clang/lib/Sema/Sema.cpp | 17 +++-
clang/lib/Serialization/ASTReader.cpp | 2 +-
clang/lib/Serialization/ASTWriter.cpp | 12 ++-
.../PCH/unused-local-typedef-determinism.cpp | 27 ++++++
...used-local-typedef-deterministic-order.cpp | 92 +++++++++++++++++++
10 files changed, 153 insertions(+), 12 deletions(-)
create mode 100644 clang/test/PCH/unused-local-typedef-determinism.cpp
create mode 100644 clang/test/SemaCXX/warn-unused-local-typedef-deterministic-order.cpp
diff --git a/clang/include/clang/Sema/ExternalSemaSource.h b/clang/include/clang/Sema/ExternalSemaSource.h
index e7e66bea8c6bd..f98b0f3585c03 100644
--- a/clang/include/clang/Sema/ExternalSemaSource.h
+++ b/clang/include/clang/Sema/ExternalSemaSource.h
@@ -141,7 +141,7 @@ class ExternalSemaSource : public ExternalASTSource {
/// be invoked multiple times; the external source should take care not to
/// introduce the same declarations repeatedly.
virtual void ReadUnusedLocalTypedefNameCandidates(
- llvm::SmallSetVector<const TypedefNameDecl *, 4> &Decls) {}
+ llvm::SmallPtrSetImpl<const TypedefNameDecl *> &Decls) {}
/// Read the set of referenced selectors known to the
/// external Sema source.
diff --git a/clang/include/clang/Sema/MultiplexExternalSemaSource.h b/clang/include/clang/Sema/MultiplexExternalSemaSource.h
index 1395470401b5d..6c117feaa47e0 100644
--- a/clang/include/clang/Sema/MultiplexExternalSemaSource.h
+++ b/clang/include/clang/Sema/MultiplexExternalSemaSource.h
@@ -301,7 +301,7 @@ class MultiplexExternalSemaSource : public ExternalSemaSource {
/// be invoked multiple times; the external source should take care not to
/// introduce the same declarations repeatedly.
void ReadUnusedLocalTypedefNameCandidates(
- llvm::SmallSetVector<const TypedefNameDecl *, 4> &Decls) override;
+ llvm::SmallPtrSetImpl<const TypedefNameDecl *> &Decls) override;
/// Read the set of referenced selectors known to the
/// external Sema source.
diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h
index bb5697a16e090..1c343af6008ec 100644
--- a/clang/include/clang/Sema/Sema.h
+++ b/clang/include/clang/Sema/Sema.h
@@ -3609,8 +3609,11 @@ class Sema final : public SemaBase {
ExtnameUndeclaredIdentifiers;
/// Set containing all typedefs that are likely unused.
- llvm::SmallSetVector<const TypedefNameDecl *, 4>
- UnusedLocalTypedefNameCandidates;
+ llvm::SmallPtrSet<const TypedefNameDecl *, 4> UnusedLocalTypedefNameCandidates;
+
+ /// Return UnusedLocalTypedefNameCandidates in a deterministic order.
+ SmallVector<const TypedefNameDecl *, 4>
+ getSortedUnusedLocalTypedefNameCandidates() const;
typedef LazyVector<const DeclaratorDecl *, ExternalSemaSource,
&ExternalSemaSource::ReadUnusedFileScopedDecls, 2, 2>
diff --git a/clang/include/clang/Serialization/ASTReader.h b/clang/include/clang/Serialization/ASTReader.h
index 61bd3d0748988..d800af83d350b 100644
--- a/clang/include/clang/Serialization/ASTReader.h
+++ b/clang/include/clang/Serialization/ASTReader.h
@@ -2317,7 +2317,7 @@ class ASTReader : public ExternalPreprocessorSource,
void ReadExtVectorDecls(SmallVectorImpl<TypedefNameDecl *> &Decls) override;
void ReadUnusedLocalTypedefNameCandidates(
- llvm::SmallSetVector<const TypedefNameDecl *, 4> &Decls) override;
+ llvm::SmallPtrSetImpl<const TypedefNameDecl *> &Decls) override;
void ReadDeclsToCheckForDeferredDiags(
llvm::SmallSetVector<Decl *, 4> &Decls) override;
diff --git a/clang/lib/Sema/MultiplexExternalSemaSource.cpp b/clang/lib/Sema/MultiplexExternalSemaSource.cpp
index 4306143025c80..49bd9596f77a7 100644
--- a/clang/lib/Sema/MultiplexExternalSemaSource.cpp
+++ b/clang/lib/Sema/MultiplexExternalSemaSource.cpp
@@ -303,7 +303,7 @@ void MultiplexExternalSemaSource::ReadDeclsToCheckForDeferredDiags(
}
void MultiplexExternalSemaSource::ReadUnusedLocalTypedefNameCandidates(
- llvm::SmallSetVector<const TypedefNameDecl *, 4> &Decls) {
+ llvm::SmallPtrSetImpl<const TypedefNameDecl *> &Decls) {
for(size_t i = 0; i < Sources.size(); ++i)
Sources[i]->ReadUnusedLocalTypedefNameCandidates(Decls);
}
diff --git a/clang/lib/Sema/Sema.cpp b/clang/lib/Sema/Sema.cpp
index e689c75eb566c..86e84db546394 100644
--- a/clang/lib/Sema/Sema.cpp
+++ b/clang/lib/Sema/Sema.cpp
@@ -1192,11 +1192,26 @@ static bool IsRecordFullyDefined(const CXXRecordDecl *RD,
return Complete;
}
+SmallVector<const TypedefNameDecl *, 4>
+Sema::getSortedUnusedLocalTypedefNameCandidates() const {
+ // The candidates are collected while iterating a Scope's SmallPtrSet, so sort
+ // by source location for a deterministic order.
+ SmallVector<const TypedefNameDecl *, 4> Sorted(
+ UnusedLocalTypedefNameCandidates.begin(),
+ UnusedLocalTypedefNameCandidates.end());
+ llvm::sort(Sorted, [](const TypedefNameDecl *LHS, const TypedefNameDecl *RHS) {
+ return LHS->getLocation().getRawEncoding() <
+ RHS->getLocation().getRawEncoding();
+ });
+ return Sorted;
+}
+
void Sema::emitAndClearUnusedLocalTypedefWarnings() {
if (ExternalSource)
ExternalSource->ReadUnusedLocalTypedefNameCandidates(
UnusedLocalTypedefNameCandidates);
- for (const TypedefNameDecl *TD : UnusedLocalTypedefNameCandidates) {
+ for (const TypedefNameDecl *TD :
+ getSortedUnusedLocalTypedefNameCandidates()) {
if (TD->isReferenced())
continue;
Diag(TD->getLocation(), diag::warn_unused_local_typedef)
diff --git a/clang/lib/Serialization/ASTReader.cpp b/clang/lib/Serialization/ASTReader.cpp
index 080cb7df04406..4d3816d686730 100644
--- a/clang/lib/Serialization/ASTReader.cpp
+++ b/clang/lib/Serialization/ASTReader.cpp
@@ -9721,7 +9721,7 @@ void ASTReader::ReadExtVectorDecls(SmallVectorImpl<TypedefNameDecl *> &Decls) {
}
void ASTReader::ReadUnusedLocalTypedefNameCandidates(
- llvm::SmallSetVector<const TypedefNameDecl *, 4> &Decls) {
+ llvm::SmallPtrSetImpl<const TypedefNameDecl *> &Decls) {
for (unsigned I = 0, N = UnusedLocalTypedefNameCandidates.size(); I != N;
++I) {
TypedefNameDecl *D = dyn_cast_or_null<TypedefNameDecl>(
diff --git a/clang/lib/Serialization/ASTWriter.cpp b/clang/lib/Serialization/ASTWriter.cpp
index b6b576833babb..76a6c99ea5e6f 100644
--- a/clang/lib/Serialization/ASTWriter.cpp
+++ b/clang/lib/Serialization/ASTWriter.cpp
@@ -5796,8 +5796,10 @@ void ASTWriter::PrepareWritingSpecialDecls(Sema &SemaRef) {
for (unsigned I = 0, N = SemaRef.VTableUses.size(); I != N; ++I)
GetDeclRef(SemaRef.VTableUses[I].first);
- // Writing all of the UnusedLocalTypedefNameCandidates.
- for (const TypedefNameDecl *TD : SemaRef.UnusedLocalTypedefNameCandidates)
+ // Writing all of the UnusedLocalTypedefNameCandidates in a deterministic
+ // order.
+ for (const TypedefNameDecl *TD :
+ SemaRef.getSortedUnusedLocalTypedefNameCandidates())
GetDeclRef(TD);
// Writing all of pending implicit instantiations.
@@ -5924,9 +5926,11 @@ void ASTWriter::WriteSpecialDeclRecords(Sema &SemaRef) {
Stream.EmitRecord(VTABLE_USES, VTableUses);
}
- // Write the record containing potentially unused local typedefs.
+ // Write the record containing potentially unused local typedefs, in a
+ // deterministic order.
RecordData UnusedLocalTypedefNameCandidates;
- for (const TypedefNameDecl *TD : SemaRef.UnusedLocalTypedefNameCandidates)
+ for (const TypedefNameDecl *TD :
+ SemaRef.getSortedUnusedLocalTypedefNameCandidates())
AddEmittedDeclRef(TD, UnusedLocalTypedefNameCandidates);
if (!UnusedLocalTypedefNameCandidates.empty())
Stream.EmitRecord(UNUSED_LOCAL_TYPEDEF_NAME_CANDIDATES,
diff --git a/clang/test/PCH/unused-local-typedef-determinism.cpp b/clang/test/PCH/unused-local-typedef-determinism.cpp
new file mode 100644
index 0000000000000..dc4753b9490a8
--- /dev/null
+++ b/clang/test/PCH/unused-local-typedef-determinism.cpp
@@ -0,0 +1,27 @@
+// Check that emitting a PCH is deterministic even when a scope contains many
+// unused local typedefs. These are collected into
+// Sema::UnusedLocalTypedefNameCandidates while iterating a Scope's SmallPtrSet
+// (a pointer-order, run-to-run unstable container) and are then serialized into
+// the AST file, so without a stable order the two PCHs below would differ.
+//
+// RUN: rm -rf %t
+// RUN: mkdir %t
+// RUN: %clang_cc1 -x c++-header %s -emit-pch -o %t/a.pch
+// RUN: %clang_cc1 -x c++-header %s -emit-pch -o %t/b.pch
+// RUN: cmp %t/a.pch %t/b.pch
+
+inline void f() {
+ typedef int t00; typedef int t01; typedef int t02; typedef int t03;
+ typedef int t04; typedef int t05; typedef int t06; typedef int t07;
+ typedef int t08; typedef int t09; typedef int t10; typedef int t11;
+ typedef int t12; typedef int t13; typedef int t14; typedef int t15;
+ typedef int t16; typedef int t17; typedef int t18; typedef int t19;
+ typedef int t20; typedef int t21; typedef int t22; typedef int t23;
+ typedef int t24; typedef int t25; typedef int t26; typedef int t27;
+ typedef int t28; typedef int t29; typedef int t30; typedef int t31;
+ typedef int t32; typedef int t33; typedef int t34; typedef int t35;
+ typedef int t36; typedef int t37; typedef int t38; typedef int t39;
+ typedef int t40; typedef int t41; typedef int t42; typedef int t43;
+ typedef int t44; typedef int t45; typedef int t46; typedef int t47;
+ typedef int t48; typedef int t49;
+}
diff --git a/clang/test/SemaCXX/warn-unused-local-typedef-deterministic-order.cpp b/clang/test/SemaCXX/warn-unused-local-typedef-deterministic-order.cpp
new file mode 100644
index 0000000000000..ef0eb25bcbd8d
--- /dev/null
+++ b/clang/test/SemaCXX/warn-unused-local-typedef-deterministic-order.cpp
@@ -0,0 +1,92 @@
+// Verify that -Wunused-local-typedef diagnostics are emitted in a deterministic
+// (source) order even when a scope contains many unused local typedefs. The
+// candidates are collected while iterating a Scope's DeclsInScope, which is a
+// SmallPtrSet, so without sorting the order would depend on pointer values and
+// vary across runs.
+//
+// RUN: %clang_cc1 %s -fsyntax-only -Wunused-local-typedef 2>&1 | FileCheck %s
+
+inline void f() {
+ // Enough typedefs to exceed the small storage of Scope::DeclSetTy.
+ typedef int t01;
+ typedef int t02;
+ typedef int t03;
+ typedef int t04;
+ typedef int t05;
+ typedef int t06;
+ typedef int t07;
+ typedef int t08;
+ typedef int t09;
+ typedef int t10;
+ typedef int t11;
+ typedef int t12;
+ typedef int t13;
+ typedef int t14;
+ typedef int t15;
+ typedef int t16;
+ typedef int t17;
+ typedef int t18;
+ typedef int t19;
+ typedef int t20;
+ typedef int t21;
+ typedef int t22;
+ typedef int t23;
+ typedef int t24;
+ typedef int t25;
+ typedef int t26;
+ typedef int t27;
+ typedef int t28;
+ typedef int t29;
+ typedef int t30;
+ typedef int t31;
+ typedef int t32;
+ typedef int t33;
+ typedef int t34;
+ typedef int t35;
+ typedef int t36;
+ typedef int t37;
+ typedef int t38;
+ typedef int t39;
+ typedef int t40;
+}
+
+// CHECK: warning: unused typedef 't01'
+// CHECK: warning: unused typedef 't02'
+// CHECK: warning: unused typedef 't03'
+// CHECK: warning: unused typedef 't04'
+// CHECK: warning: unused typedef 't05'
+// CHECK: warning: unused typedef 't06'
+// CHECK: warning: unused typedef 't07'
+// CHECK: warning: unused typedef 't08'
+// CHECK: warning: unused typedef 't09'
+// CHECK: warning: unused typedef 't10'
+// CHECK: warning: unused typedef 't11'
+// CHECK: warning: unused typedef 't12'
+// CHECK: warning: unused typedef 't13'
+// CHECK: warning: unused typedef 't14'
+// CHECK: warning: unused typedef 't15'
+// CHECK: warning: unused typedef 't16'
+// CHECK: warning: unused typedef 't17'
+// CHECK: warning: unused typedef 't18'
+// CHECK: warning: unused typedef 't19'
+// CHECK: warning: unused typedef 't20'
+// CHECK: warning: unused typedef 't21'
+// CHECK: warning: unused typedef 't22'
+// CHECK: warning: unused typedef 't23'
+// CHECK: warning: unused typedef 't24'
+// CHECK: warning: unused typedef 't25'
+// CHECK: warning: unused typedef 't26'
+// CHECK: warning: unused typedef 't27'
+// CHECK: warning: unused typedef 't28'
+// CHECK: warning: unused typedef 't29'
+// CHECK: warning: unused typedef 't30'
+// CHECK: warning: unused typedef 't31'
+// CHECK: warning: unused typedef 't32'
+// CHECK: warning: unused typedef 't33'
+// CHECK: warning: unused typedef 't34'
+// CHECK: warning: unused typedef 't35'
+// CHECK: warning: unused typedef 't36'
+// CHECK: warning: unused typedef 't37'
+// CHECK: warning: unused typedef 't38'
+// CHECK: warning: unused typedef 't39'
+// CHECK: warning: unused typedef 't40'
More information about the cfe-commits
mailing list