https://github.com/adams381 created
https://github.com/llvm/llvm-project/pull/219564
An enum that is still incomplete has no CIR type of its own to complete later
the way a record does, so it converts to a guessed `!u32i`.
`updateCompletedType` asserted that a definition could never disagree with that
guess. It can:
```c
extern enum E v;
void touch(void) { v; }
enum E { A = 0x100000000 };
unsigned long read(void) { return v; }
```
The definition makes `E` 64 bits and the assert fires. With assertions off the
guess survives instead, and `read` loads 32 bits of a 64-bit object and
zero-extends, where classic CodeGen loads 64.
Flushing the type cache when the definition contradicts the guess removes both
the abort and the truncation. Classic tests whether the completed type is
`i32`, which does not carry over because `!s32i` and `!u32i` are distinct CIR
types, so the comparison here is against the cached entry.
A signature converted while the enum was incomplete keeps the guess, so a
declaration can name a wrong width where classic leaves it unprototyped. No
emitted code reads that signature: call sites bitcast the callee to the
definition's type, and a definition of the function gets a fresh one. Removing
it needs CIR to reconcile placeholder function types at every conversion
boundary, since CIR function pointers are typed where LLVM's are opaque.
>From 71e148ac696dcac57cea382cbdabd9007f2bf513 Mon Sep 17 00:00:00 2001
From: Adam Smith <[email protected]>
Date: Fri, 28 Aug 2026 12:04:20 -0700
Subject: [PATCH] [CIR] Flush the type cache when an enum completes differently
An enum that is still incomplete converts to a guessed !u32i, since it has no
CIR type of its own to complete later the way a record does. If the definition
disagrees with the guess, everything already converted from it is wrong.
We now flush the cache when the definition contradicts the guess, so anything
derived from it is rebuilt.
A signature converted while the enum was incomplete keeps the guess, so a
declaration can still name the wrong width where classic leaves it
unprototyped. Nothing emitted reads that signature, since call sites bitcast
the callee to the type the definition gave the enum, and a definition of the
function gets a fresh signature.
Assisted-by: Cursor / claude-opus-5
---
clang/lib/CIR/CodeGen/CIRGenTypes.cpp | 18 ++---
clang/test/CIR/CodeGen/forward-enum.c | 109 +++++++++++++++++++++++++-
2 files changed, 112 insertions(+), 15 deletions(-)
diff --git a/clang/lib/CIR/CodeGen/CIRGenTypes.cpp
b/clang/lib/CIR/CodeGen/CIRGenTypes.cpp
index f1834d8fc7f37..82c69ad525674 100644
--- a/clang/lib/CIR/CodeGen/CIRGenTypes.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenTypes.cpp
@@ -818,17 +818,13 @@ void CIRGenTypes::updateCompletedType(const TagDecl *td) {
// If this is an enum being completed, then we flush all non-struct types
// from the cache. This allows function types and other things that may be
// derived from the enum to be recomputed.
- if ([[maybe_unused]] const auto *ed = dyn_cast<EnumDecl>(td)) {
- // Classic codegen clears the type cache if it contains an entry for this
- // enum type that doesn't use i32 as the underlying type, but I can't find
- // a test case that meets that condition. C++ doesn't allow forward
- // declaration of enums, and C doesn't allow an incomplete forward
- // declaration with a non-default type.
- assert(
- !typeCache.count(
- ed->getASTContext().getCanonicalTagType(ed)->getTypePtr()) ||
- (convertType(ed->getIntegerType()) ==
-
typeCache[ed->getASTContext().getCanonicalTagType(ed)->getTypePtr()]));
+ if (const auto *ed = dyn_cast<EnumDecl>(td)) {
+ const clang::Type *key = astContext.getCanonicalTagType(ed)->getTypePtr();
+ // Converting the enum before it was complete cached a guessed placeholder
+ // for its underlying type.
+ if (typeCache.count(key) &&
+ convertType(ed->getIntegerType()) != typeCache.lookup(key))
+ typeCache.clear();
// If necessary, provide the full definition of a type only used with a
// declaration so far.
assert(!cir::MissingFeatures::generateDebugInfo());
diff --git a/clang/test/CIR/CodeGen/forward-enum.c
b/clang/test/CIR/CodeGen/forward-enum.c
index cba5679485a38..6f3684321c565 100644
--- a/clang/test/CIR/CodeGen/forward-enum.c
+++ b/clang/test/CIR/CodeGen/forward-enum.c
@@ -1,9 +1,9 @@
// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -emit-cir %s -o
%t.cir
// RUN: FileCheck --check-prefix=CIR --input-file=%t.cir %s
// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -emit-llvm %s -o
%t-cir.ll
-// RUN: FileCheck --check-prefix=LLVM --input-file=%t-cir.ll %s
+// RUN: FileCheck --check-prefixes=LLVM,LLVMCIR --input-file=%t-cir.ll %s
// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -emit-llvm %s -o %t.ll
-// RUN: FileCheck --check-prefix=OGCG --input-file=%t.ll %s
+// RUN: FileCheck --check-prefixes=LLVM,OGCG --input-file=%t.ll %s
extern enum X x;
void f(void) {
@@ -22,5 +22,106 @@ enum X {
// LLVM: @x = external global i32
// LLVM: define {{.*}}void @f()
-// OGCG: @x = external global i32
-// OGCG: define {{.*}}void @f()
+// The same shape, but with a definition that contradicts the guess. touch_v
+// is what forces the conversion while the enum is incomplete, so uses after
+// the definition have to be given the definition's type instead.
+extern enum V v;
+void touch_v(void) { v; }
+enum V { VBig = 0x100000000 };
+unsigned long read_v(void) { return v; }
+
+// CIR: cir.func{{.*}} @touch_v()
+// CIR: cir.func{{.*}} @read_v() -> !u64i
+// CIR: %[[VP:.+]] = cir.get_global @v : !cir.ptr<!u32i>
+// CIR: %[[VCAST:.+]] = cir.cast bitcast %[[VP]] : !cir.ptr<!u32i> ->
!cir.ptr<!u64i>
+// CIR: %{{.+}} = cir.load align(8) %[[VCAST]] : !cir.ptr<!u64i>, !u64i
+
+// LLVM: define {{.*}}void @touch_v()
+// LLVM: define {{.*}}i64 @read_v()
+// LLVM: load i64, ptr @v, align 8
+
+// A fixed underlying type is not a guess, so nothing here needs invalidating.
+enum Y : long;
+extern enum Y y;
+void touch_y(void) { y; }
+enum Y : long { YOne = 1 };
+long read_y(void) { return y; }
+
+// CIR: cir.func{{.*}} @touch_y()
+// CIR: cir.get_global @y : !cir.ptr<!s64i>
+// CIR: cir.func{{.*}} @read_y() -> !s64i
+// CIR: cir.get_global @y : !cir.ptr<!s64i>
+
+// LLVM: define {{.*}}void @touch_y()
+// LLVM: load i64, ptr @y, align 8
+// LLVM: define {{.*}}i64 @read_y()
+// LLVM: load i64, ptr @y, align 8
+
+// The function pointer forces the signature to be converted while the enum is
+// incomplete, so the declaration keeps the guess. The calls are emitted after
+// the definition and use its type, bitcasting the callee to match.
+enum W;
+void takes_wider(enum W);
+void (*wider_ptr)(enum W) = takes_wider;
+enum W { WBig = 0x100000000 };
+void use_wider(void) { takes_wider(WBig); }
+void use_wider_ptr(void) { wider_ptr(WBig); }
+
+// CIR: cir.func private @takes_wider(!u32i)
+// CIR: cir.func{{.*}} @use_wider()
+// CIR: %[[WVAL:.+]] = cir.const #cir.int<4294967296> : !u64i
+// CIR: %[[WFN:.+]] = cir.get_global @takes_wider :
!cir.ptr<!cir.func<(!u32i)>>
+// CIR: %[[WCAST:.+]] = cir.cast bitcast %[[WFN]] :
!cir.ptr<!cir.func<(!u32i)>> -> !cir.ptr<!cir.func<(!u64i)>>
+// CIR: cir.call %[[WCAST]](%[[WVAL]]) : (!cir.ptr<!cir.func<(!u64i)>>,
!u64i {llvm.noundef}) -> ()
+
+// CIR: cir.func{{.*}} @use_wider_ptr()
+// CIR: %[[WPCAST:.+]] = cir.cast bitcast %{{.+}} :
!cir.ptr<!cir.ptr<!cir.func<(!u32i)>>> -> !cir.ptr<!cir.ptr<!cir.func<(!u64i)>>>
+// CIR: %[[WCALLEE:.+]] = cir.load align(8) %[[WPCAST]] :
!cir.ptr<!cir.ptr<!cir.func<(!u64i)>>>, !cir.ptr<!cir.func<(!u64i)>>
+// CIR: cir.call %[[WCALLEE]](%{{.+}}) : (!cir.ptr<!cir.func<(!u64i)>>,
!u64i {llvm.noundef}) -> ()
+
+// LLVMCIR: declare void @takes_wider(i32)
+// OGCG: declare void @takes_wider()
+// LLVM: define {{.*}}void @use_wider()
+// LLVM: call void @takes_wider(i64 noundef 4294967296)
+// LLVM: define {{.*}}void @use_wider_ptr()
+// LLVM: %[[WP:.+]] = load ptr, ptr @wider_ptr, align 8
+// LLVM: call void %[[WP]](i64 noundef 4294967296)
+
+// Same, for a definition that keeps the guess's width but not its signedness.
+enum S;
+void takes_signed(enum S);
+void (*signed_ptr)(enum S) = takes_signed;
+enum S { SNeg = -1 };
+void use_signed(void) { takes_signed(SNeg); }
+
+// CIR: cir.func private @takes_signed(!u32i)
+// CIR: cir.func{{.*}} @use_signed()
+// CIR: %[[SVAL:.+]] = cir.const #cir.int<-1> : !s32i
+// CIR: %[[SFN:.+]] = cir.get_global @takes_signed :
!cir.ptr<!cir.func<(!u32i)>>
+// CIR: %[[SCAST:.+]] = cir.cast bitcast %[[SFN]] :
!cir.ptr<!cir.func<(!u32i)>> -> !cir.ptr<!cir.func<(!s32i)>>
+// CIR: cir.call %[[SCAST]](%[[SVAL]]) : (!cir.ptr<!cir.func<(!s32i)>>,
!s32i {llvm.noundef}) -> ()
+
+// LLVMCIR: declare void @takes_signed(i32)
+// OGCG: declare void @takes_signed()
+// LLVM: define {{.*}}void @use_signed()
+// LLVM: call void @takes_signed(i32 noundef -1)
+
+// A definition emitted after the enum completes takes its parameter type from
+// the definition, so the call to it needs no bitcast. defined_ptr is again
+// what forces the early conversion.
+enum D;
+void takes_defined(enum D);
+void (*defined_ptr)(enum D) = takes_defined;
+enum D { DBig = 0x100000000 };
+unsigned long sink;
+void takes_defined(enum D d) { sink = d; }
+void use_defined(void) { takes_defined(DBig); }
+
+// CIR: cir.func{{.*}} @takes_defined(%arg0: !u64i {llvm.noundef}
+// CIR: cir.func{{.*}} @use_defined()
+// CIR: %[[DVAL:.+]] = cir.const #cir.int<4294967296> : !u64i
+// CIR: cir.call @takes_defined(%[[DVAL]]) : (!u64i {llvm.noundef}) -> ()
+
+// LLVM: define {{.*}}void @takes_defined(i64 noundef %{{.+}})
+// LLVM: define {{.*}}void @use_defined()
+// LLVM: call void @takes_defined(i64 noundef 4294967296)
_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits