[clang] [CIR] Preserve annotate attributes on record declarations (PR #207551)

via cfe-commits cfe-commits at lists.llvm.org
Sun Jul 26 09:27:31 PDT 2026


https://github.com/RedNicStone updated https://github.com/llvm/llvm-project/pull/207551

>From ee82950c006729160fe5d68e043f69233daaad37 Mon Sep 17 00:00:00 2001
From: RedNicStone <nic at struktur.de>
Date: Tue, 21 Jul 2026 19:25:40 +0100
Subject: [PATCH] CIR] Preserve annotate attributes on record declarations

__attribute__((annotate(...))) already round-trips through ClangIR for
functions and globals but is silently dropped for record (struct/class/
union) declarations. Unlike LLVM IR, CIR retains a structured record type
deep into the pipeline, so this metadata can be preserved.

Carry the annotations as an optional array attribute on !cir.struct and
!cir.union, populated once per annotated RecordDecl during record layout
using the existing #cir.annotation.

Annotations survive CXXABI lowering and are dropped during LLVM lowering,
which has no equivalent representation.
---
 clang/include/clang/CIR/Dialect/IR/CIRTypes.h |  3 +-
 .../include/clang/CIR/Dialect/IR/CIRTypes.td  |  8 ++-
 .../clang/CIR/Dialect/IR/CIRTypesDetails.h    | 16 ++++-
 clang/lib/CIR/CodeGen/CIRGenModule.cpp        |  9 +++
 clang/lib/CIR/CodeGen/CIRGenModule.h          |  4 ++
 .../CIR/CodeGen/CIRGenRecordLayoutBuilder.cpp |  2 +-
 clang/lib/CIR/Dialect/IR/CIRTypes.cpp         | 69 ++++++++++++++-----
 .../CIR/Dialect/Transforms/CXXABILowering.cpp |  2 +-
 clang/test/CIR/CodeGen/annotate-attribute.cpp | 19 +++++
 clang/test/CIR/IR/record-annotations.cir      | 20 ++++++
 10 files changed, 128 insertions(+), 24 deletions(-)
 create mode 100644 clang/test/CIR/IR/record-annotations.cir

diff --git a/clang/include/clang/CIR/Dialect/IR/CIRTypes.h b/clang/include/clang/CIR/Dialect/IR/CIRTypes.h
index c5f4127040ca0..a2bc2a8a05925 100644
--- a/clang/include/clang/CIR/Dialect/IR/CIRTypes.h
+++ b/clang/include/clang/CIR/Dialect/IR/CIRTypes.h
@@ -121,9 +121,10 @@ class RecordType : public mlir::Type {
   mlir::Type getElementType(size_t idx) const { return getMembers()[idx]; }
   std::string getKindAsStr() const;
   std::string getPrefixedName() const;
+  mlir::ArrayAttr getAnnotations() const;
 
   void complete(llvm::ArrayRef<mlir::Type> members, bool packed, bool padded,
-                mlir::Type padding = {});
+                mlir::Type padding = {}, mlir::ArrayAttr annotations = {});
   uint64_t getElementOffset(const mlir::DataLayout &dataLayout,
                             unsigned idx) const;
   bool isLayoutIdentical(const RecordType &other);
diff --git a/clang/include/clang/CIR/Dialect/IR/CIRTypes.td b/clang/include/clang/CIR/Dialect/IR/CIRTypes.td
index 9ed151392dc30..bcf2afe44b597 100644
--- a/clang/include/clang/CIR/Dialect/IR/CIRTypes.td
+++ b/clang/include/clang/CIR/Dialect/IR/CIRTypes.td
@@ -738,7 +738,9 @@ def CIR_StructType : CIR_Type<"Struct", "struct", [
     }
 
     void complete(llvm::ArrayRef<mlir::Type> members, bool packed,
-                  bool isPadded);
+                  bool isPadded, mlir::ArrayAttr annotations = {});
+
+    mlir::ArrayAttr getAnnotations() const;
 
     uint64_t getElementOffset(const mlir::DataLayout &dataLayout,
                               unsigned idx) const;
@@ -882,7 +884,9 @@ def CIR_UnionType : CIR_Type<"Union", "union", [
     mlir::Type getUnionStorageType(const mlir::DataLayout &dataLayout) const;
 
     void complete(llvm::ArrayRef<mlir::Type> members, bool packed,
-                  mlir::Type padding = {});
+                  mlir::Type padding = {}, mlir::ArrayAttr annotations = {});
+
+    mlir::ArrayAttr getAnnotations() const;
 
     uint64_t getElementOffset(const mlir::DataLayout &, unsigned) const {
       return 0;
diff --git a/clang/include/clang/CIR/Dialect/IR/CIRTypesDetails.h b/clang/include/clang/CIR/Dialect/IR/CIRTypesDetails.h
index e94e1d81ff4c6..f7774ee0ca5bf 100644
--- a/clang/include/clang/CIR/Dialect/IR/CIRTypesDetails.h
+++ b/clang/include/clang/CIR/Dialect/IR/CIRTypesDetails.h
@@ -48,6 +48,13 @@ struct StructTypeStorage : public mlir::TypeStorage {
   bool padded;
   bool is_class;
 
+  // Array of #cir.annotation attributes, set at completion (like
+  // members/layout), not part of the type's identity: named records are
+  // uniqued by name, so there is at most one annotation set per type. Keeping
+  // them out of the key is also required so an incomplete record can be
+  // completed in place.
+  mlir::ArrayAttr annotations;
+
   StructTypeStorage(llvm::ArrayRef<mlir::Type> members, mlir::StringAttr name,
                     bool incomplete, bool packed, bool padded, bool is_class)
       : members(members), name(name), incomplete(incomplete), packed(packed),
@@ -84,7 +91,7 @@ struct StructTypeStorage : public mlir::TypeStorage {
   /// Mutates the members and attributes of an identified struct/class.
   llvm::LogicalResult mutate(mlir::TypeStorageAllocator &allocator,
                              llvm::ArrayRef<mlir::Type> members, bool packed,
-                             bool padded) {
+                             bool padded, mlir::ArrayAttr annotations) {
     if (!name)
       return llvm::failure();
 
@@ -96,6 +103,7 @@ struct StructTypeStorage : public mlir::TypeStorage {
     this->members = allocator.copyInto(members);
     this->packed = packed;
     this->padded = padded;
+    this->annotations = annotations;
     incomplete = false;
     return llvm::success();
   }
@@ -126,6 +134,9 @@ struct UnionTypeStorage : public mlir::TypeStorage {
   bool packed;
   mlir::Type padding;
 
+  // See StructTypeStorage::annotations.
+  mlir::ArrayAttr annotations;
+
   UnionTypeStorage(llvm::ArrayRef<mlir::Type> members, mlir::StringAttr name,
                    bool incomplete, bool packed, mlir::Type padding)
       : members(members), name(name), incomplete(incomplete), packed(packed),
@@ -162,7 +173,7 @@ struct UnionTypeStorage : public mlir::TypeStorage {
   /// Mutates the members and attributes of an identified union.
   llvm::LogicalResult mutate(mlir::TypeStorageAllocator &allocator,
                              llvm::ArrayRef<mlir::Type> members, bool packed,
-                             mlir::Type padding) {
+                             mlir::Type padding, mlir::ArrayAttr annotations) {
     if (!name)
       return llvm::failure();
 
@@ -174,6 +185,7 @@ struct UnionTypeStorage : public mlir::TypeStorage {
     this->members = allocator.copyInto(members);
     this->packed = packed;
     this->padding = padding;
+    this->annotations = annotations;
     incomplete = false;
     return llvm::success();
   }
diff --git a/clang/lib/CIR/CodeGen/CIRGenModule.cpp b/clang/lib/CIR/CodeGen/CIRGenModule.cpp
index 7a05aed4c33e0..66c0f7b0c96bd 100644
--- a/clang/lib/CIR/CodeGen/CIRGenModule.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenModule.cpp
@@ -4172,6 +4172,15 @@ void CIRGenModule::addGlobalAnnotations(const ValueDecl *d,
     func.setAnnotationsAttr(builder.getArrayAttr(annotations));
 }
 
+mlir::ArrayAttr CIRGenModule::getRecordAnnotations(const RecordDecl *rd) {
+  if (!rd->hasAttr<AnnotateAttr>())
+    return {};
+  llvm::SmallVector<mlir::Attribute> annotations;
+  for (const auto *i : rd->specific_attrs<AnnotateAttr>())
+    annotations.push_back(emitAnnotateAttr(i));
+  return builder.getArrayAttr(annotations);
+}
+
 void CIRGenModule::emitGlobalAnnotations() {
   for (const auto &[mangledName, vd] : deferredAnnotations) {
     mlir::Operation *gv = getGlobalValue(mangledName);
diff --git a/clang/lib/CIR/CodeGen/CIRGenModule.h b/clang/lib/CIR/CodeGen/CIRGenModule.h
index 144f8c7b9f3e7..d557b6868a53c 100644
--- a/clang/lib/CIR/CodeGen/CIRGenModule.h
+++ b/clang/lib/CIR/CodeGen/CIRGenModule.h
@@ -933,6 +933,10 @@ class CIRGenModule : public CIRGenTypeCache {
   /// Add global annotations for a global value (GlobalOp or FuncOp).
   void addGlobalAnnotations(const clang::ValueDecl *d, mlir::Operation *gv);
 
+  /// Collect the annotate attributes on a record declaration into an array
+  /// attribute, or return null if there are none.
+  mlir::ArrayAttr getRecordAnnotations(const clang::RecordDecl *rd);
+
 private:
   /// Search \p currentClass and its non-virtual base subobjects for \p field,
   /// appending CIR field indices along the path from \p currentClass.
diff --git a/clang/lib/CIR/CodeGen/CIRGenRecordLayoutBuilder.cpp b/clang/lib/CIR/CodeGen/CIRGenRecordLayoutBuilder.cpp
index e33b2065ecb67..57b20249a5256 100644
--- a/clang/lib/CIR/CodeGen/CIRGenRecordLayoutBuilder.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenRecordLayoutBuilder.cpp
@@ -736,7 +736,7 @@ CIRGenTypes::computeRecordLayout(const RecordDecl *rd, cir::RecordType *ty) {
   // but we may need to recursively layout rd while laying D out as a base type.
   assert(!cir::MissingFeatures::astRecordDeclAttr());
   ty->complete(lowering.fieldTypes, lowering.packed, lowering.padded,
-               lowering.unionPadding);
+               lowering.unionPadding, cgm.getRecordAnnotations(rd));
 
   // Queue ABI metadata for the module-level cir.record_layouts attribute.
   if (ty->getName()) {
diff --git a/clang/lib/CIR/Dialect/IR/CIRTypes.cpp b/clang/lib/CIR/Dialect/IR/CIRTypes.cpp
index 9c2a40e3681aa..f62b8b78192ad 100644
--- a/clang/lib/CIR/Dialect/IR/CIRTypes.cpp
+++ b/clang/lib/CIR/Dialect/IR/CIRTypes.cpp
@@ -144,16 +144,25 @@ parseRecordBody(mlir::AsmParser &parser, bool &incomplete,
       });
 }
 
+/// Parse optional " annotations = [...]" clause.
+static mlir::ParseResult parseRecordAnnotations(mlir::AsmParser &parser,
+                                                mlir::ArrayAttr &annotations) {
+  if (parser.parseOptionalKeyword("annotations").failed())
+    return mlir::success();
+  return mlir::failure(parser.parseEqual().failed() ||
+                       parser.parseAttribute(annotations).failed());
+}
+
 /// Print a complete CIR record body:
-///   '<' ['class '] [name] ['packed '] ['padded '] body '>'
+///   '<' ['class '] [name] ['packed '] ['padded '] body [annotations] '>'
 /// where body is "incomplete" or "{members[, padding = {type}]}".
 /// RecordTy must be a mutable MLIR type (StructType or UnionType).
 template <typename RecordTy>
-static void printRecordBody(mlir::AsmPrinter &printer, RecordTy self,
-                            mlir::StringAttr name, bool hasClassPrefix,
-                            bool isPacked, bool isPadded, bool isIncomplete,
-                            llvm::ArrayRef<mlir::Type> members,
-                            mlir::Type padding = {}) {
+static void
+printRecordBody(mlir::AsmPrinter &printer, RecordTy self, mlir::StringAttr name,
+                bool hasClassPrefix, bool isPacked, bool isPadded,
+                bool isIncomplete, llvm::ArrayRef<mlir::Type> members,
+                mlir::Type padding = {}, mlir::ArrayAttr annotations = {}) {
   printer << '<';
   if (hasClassPrefix)
     printer << "class ";
@@ -185,6 +194,8 @@ static void printRecordBody(mlir::AsmPrinter &printer, RecordTy self,
       printer << '}';
     }
   }
+  if (annotations && !annotations.empty())
+    printer << " annotations = " << annotations;
   printer << '>';
 }
 
@@ -237,6 +248,10 @@ Type StructType::parse(mlir::AsmParser &parser) {
   if (parseRecordBody(parser, incomplete, members).failed())
     return {};
 
+  mlir::ArrayAttr annotations;
+  if (parseRecordAnnotations(parser, annotations).failed())
+    return {};
+
   if (parser.parseGreater())
     return {};
 
@@ -256,7 +271,7 @@ Type StructType::parse(mlir::AsmParser &parser) {
       return {};
     if (auto structTy = mlir::dyn_cast<StructType>(type))
       if (structTy.isIncomplete())
-        structTy.complete(membersRef, packed, padded);
+        structTy.complete(membersRef, packed, padded, annotations);
     assert(!cir::MissingFeatures::astRecordDeclAttr());
   } else {
     parser.emitError(loc, "anonymous records must be complete");
@@ -268,7 +283,8 @@ Type StructType::parse(mlir::AsmParser &parser) {
 
 void StructType::print(mlir::AsmPrinter &printer) const {
   printRecordBody(printer, *this, getName(), isClass(), getPacked(),
-                  getPadded(), isIncomplete(), getMembers());
+                  getPadded(), isIncomplete(), getMembers(), /*padding=*/{},
+                  getAnnotations());
 }
 
 mlir::LogicalResult
@@ -310,9 +326,14 @@ void StructType::removeABIConversionNamePrefix() {
         recordName.getType());
 }
 
-void StructType::complete(ArrayRef<Type> members, bool packed, bool padded) {
+mlir::ArrayAttr StructType::getAnnotations() const {
+  return getImpl()->annotations;
+}
+
+void StructType::complete(ArrayRef<Type> members, bool packed, bool padded,
+                          mlir::ArrayAttr annotations) {
   assert(!cir::MissingFeatures::astRecordDeclAttr());
-  if (mutate(members, packed, padded).failed())
+  if (mutate(members, packed, padded, annotations).failed())
     llvm_unreachable("failed to complete struct");
 }
 
@@ -384,6 +405,10 @@ Type UnionType::parse(mlir::AsmParser &parser) {
       return {};
   }
 
+  mlir::ArrayAttr annotations;
+  if (parseRecordAnnotations(parser, annotations).failed())
+    return {};
+
   if (parser.parseGreater())
     return {};
 
@@ -402,7 +427,7 @@ Type UnionType::parse(mlir::AsmParser &parser) {
       return {};
     if (auto unionTy = mlir::dyn_cast<UnionType>(type))
       if (unionTy.isIncomplete())
-        unionTy.complete(membersRef, packed, padding);
+        unionTy.complete(membersRef, packed, padding, annotations);
     assert(!cir::MissingFeatures::astRecordDeclAttr());
   } else {
     parser.emitError(loc, "anonymous records must be complete");
@@ -415,7 +440,7 @@ Type UnionType::parse(mlir::AsmParser &parser) {
 void UnionType::print(mlir::AsmPrinter &printer) const {
   printRecordBody(printer, *this, getName(), /*hasClassPrefix=*/false,
                   getPacked(), /*isPadded=*/false, isIncomplete(), getMembers(),
-                  getPadding());
+                  getPadding(), getAnnotations());
 }
 
 mlir::LogicalResult
@@ -456,10 +481,14 @@ void UnionType::removeABIConversionNamePrefix() {
         recordName.getType());
 }
 
+mlir::ArrayAttr UnionType::getAnnotations() const {
+  return getImpl()->annotations;
+}
+
 void UnionType::complete(ArrayRef<Type> members, bool packed,
-                         mlir::Type padding) {
+                         mlir::Type padding, mlir::ArrayAttr annotations) {
   assert(!cir::MissingFeatures::astRecordDeclAttr());
-  if (mutate(members, packed, padding).failed())
+  if (mutate(members, packed, padding, annotations).failed())
     llvm_unreachable("failed to complete union");
 }
 
@@ -532,14 +561,20 @@ std::string RecordType::getKindAsStr() const {
 std::string RecordType::getPrefixedName() const {
   return getKindAsStr() + "." + getName().getValue().str();
 }
+mlir::ArrayAttr RecordType::getAnnotations() const {
+  if (auto s = mlir::dyn_cast<StructType>(*this))
+    return s.getAnnotations();
+  return mlir::cast<UnionType>(*this).getAnnotations();
+}
 void RecordType::complete(ArrayRef<Type> members, bool packed, bool padded,
-                          mlir::Type padding) {
+                          mlir::Type padding, mlir::ArrayAttr annotations) {
   if (auto s = mlir::dyn_cast<StructType>(*this))
-    return s.complete(members, packed, padded);
+    return s.complete(members, packed, padded, annotations);
   // Unions derive padded from padding; assert the caller is consistent.
   assert((!padded || padding) &&
          "padded=true requires a non-null padding type");
-  return mlir::cast<UnionType>(*this).complete(members, packed, padding);
+  return mlir::cast<UnionType>(*this).complete(members, packed, padding,
+                                               annotations);
 }
 uint64_t RecordType::getElementOffset(const mlir::DataLayout &dataLayout,
                                       unsigned idx) const {
diff --git a/clang/lib/CIR/Dialect/Transforms/CXXABILowering.cpp b/clang/lib/CIR/Dialect/Transforms/CXXABILowering.cpp
index 704ebbeb1ecd9..a54da7d2829f0 100644
--- a/clang/lib/CIR/Dialect/Transforms/CXXABILowering.cpp
+++ b/clang/lib/CIR/Dialect/Transforms/CXXABILowering.cpp
@@ -901,7 +901,7 @@ class CIRABITypeConverter : public mlir::TypeConverter {
       if (mlir::Type pad = u.getPadding())
         loweredPadding = convertType(pad);
     convertedType.complete(convertedMembers, type.getPacked(), type.getPadded(),
-                           loweredPadding);
+                           loweredPadding, type.getAnnotations());
     addConvertedRecordType(convertedType);
     return convertedType;
   }
diff --git a/clang/test/CIR/CodeGen/annotate-attribute.cpp b/clang/test/CIR/CodeGen/annotate-attribute.cpp
index ec83abb417473..c672ecfa124f2 100644
--- a/clang/test/CIR/CodeGen/annotate-attribute.cpp
+++ b/clang/test/CIR/CodeGen/annotate-attribute.cpp
@@ -33,6 +33,10 @@ struct __attribute__((annotate("type_ann"))) Tagged {
   static int sget() { return 7; }
 };
 
+// CIR-DAG: !cir.struct<"Tagged" {!s32i} annotations = [#cir.annotation<"type_ann">]>
+// CIR-DAG: !cir.struct<"Box<int>" {!s32i} annotations = [#cir.annotation<"tpl_ann">]>
+// CIR-DAG: !cir.struct<"anon.{{[0-9]+}}" {!s32i} annotations = [#cir.annotation<"anon_ann">]>
+
 // CIR: cir.func {{.*}} @{{.*Tagged.*get.*}}({{.*}}) {{.*}}[#cir.annotation<"method_ann">]
 // CIR: cir.func {{.*}} @{{.*Tagged.*sget.*}}() {{.*}}[#cir.annotation<"static_method_ann">]
 
@@ -58,3 +62,18 @@ namespace ns {
 __attribute__((annotate("ns_global_ann")))
 int g = 5;
 }
+
+// Templates propagate the attribute to each instantiation.
+template <typename T> struct __attribute__((annotate("tpl_ann"))) Box {
+  T v;
+};
+Box<int> boxInstance;
+
+// An AST-anonymous struct (no tag name) is still given a synthesized CIR
+// type name (e.g. "anon.0") and goes through the same named-record
+// completion path as a tagged struct, so it is annotated the same way.
+struct __attribute__((annotate("anon_ann"))) {
+  int v;
+} anonInstance;
+
+int use_anon() { return anonInstance.v; }
diff --git a/clang/test/CIR/IR/record-annotations.cir b/clang/test/CIR/IR/record-annotations.cir
new file mode 100644
index 0000000000000..743e1d83907e8
--- /dev/null
+++ b/clang/test/CIR/IR/record-annotations.cir
@@ -0,0 +1,20 @@
+// RUN: cir-opt %s --verify-roundtrip | FileCheck %s
+
+!s32i = !cir.int<s, 32>
+!u8i = !cir.int<u, 8>
+
+// Struct, union, and multiple/argument-carrying annotations round-trip.
+!rec_S = !cir.struct<"S" {!s32i} annotations = [#cir.annotation<"ann">]>
+!rec_U = !cir.union<"U" {!s32i, !u8i} annotations = [#cir.annotation<"ann">]>
+!rec_M = !cir.struct<"M" {!s32i} annotations = [#cir.annotation<"a">, #cir.annotation<"b", ["x", 1 : i32]>]>
+
+// CHECK-DAG: !rec_S = !cir.struct<"S" {!s32i} annotations = [#cir.annotation<"ann">]>
+// CHECK-DAG: !rec_U = !cir.union<"U" {!s32i, !u8i} annotations = [#cir.annotation<"ann">]>
+// CHECK-DAG: !rec_M = !cir.struct<"M" {!s32i} annotations = [#cir.annotation<"a">, #cir.annotation<"b", ["x", 1 : i32]>]>
+
+module {
+  // Reference the types so they are printed.
+  cir.func @useTypes(%arg0: !rec_S, %arg1: !rec_U, %arg2: !rec_M) {
+    cir.return
+  }
+}



More information about the cfe-commits mailing list