[clang] 42972f5 - [CIR] Introduce data_member_offset attribute (#208777)
via cfe-commits
cfe-commits at lists.llvm.org
Tue Jul 21 05:56:52 PDT 2026
Author: Erich Keane
Date: 2026-07-21T05:56:47-07:00
New Revision: 42972f5e5d950b6644a926f402dc54ea3ff06ec3
URL: https://github.com/llvm/llvm-project/commit/42972f5e5d950b6644a926f402dc54ea3ff06ec3
DIFF: https://github.com/llvm/llvm-project/commit/42972f5e5d950b6644a926f402dc54ea3ff06ec3.diff
LOG: [CIR] Introduce data_member_offset attribute (#208777)
Some fields, like no_unique_address marked ones, aren't present in the
CIR struct type. This attribute represents an offset into a struct
instead, since get_member isn't possible without a member.
I also considered representing these types in the struct, but it ends up
causing a lot of awful looking structs, and forces the LLVMIR to contain
empty fields for each of these, but not in lexical order, which is
particularly awful looking.
I considered making this part of data-member, but it seems like not a
particularly good idea, since this isn't really getting a 'member' in
the traditional sense.
Added:
Modified:
clang/include/clang/CIR/Dialect/IR/CIRAttrs.td
clang/lib/CIR/CodeGen/CIRGenExprConstant.cpp
clang/lib/CIR/CodeGen/CIRGenModule.cpp
clang/lib/CIR/CodeGen/CIRGenModule.h
clang/lib/CIR/Dialect/IR/CIRDialect.cpp
clang/lib/CIR/Dialect/Transforms/CXXABILowering.cpp
clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRCXXABI.h
clang/lib/CIR/Dialect/Transforms/TargetLowering/LowerItaniumCXXABI.cpp
clang/test/CIR/CodeGen/pointer-to-empty-data-member.cpp
Removed:
################################################################################
diff --git a/clang/include/clang/CIR/Dialect/IR/CIRAttrs.td b/clang/include/clang/CIR/Dialect/IR/CIRAttrs.td
index bf91a1831eeff..529e6e32c865e 100644
--- a/clang/include/clang/CIR/Dialect/IR/CIRAttrs.td
+++ b/clang/include/clang/CIR/Dialect/IR/CIRAttrs.td
@@ -598,6 +598,45 @@ def CIR_DataMemberAttr : CIR_ValueLikeAttr<"DataMember", "data_member"> {
}];
}
+//===----------------------------------------------------------------------===//
+// DataMemberOffsetAttr
+//===----------------------------------------------------------------------===//
+
+def CIR_DataMemberOffsetAttr
+ : CIR_ValueLikeAttr<"DataMemberOffset", "data_member_offset"> {
+ let summary = "Constant pointer-to-data-member given by a byte offset";
+ let parameters = (ins AttributeSelfTypeParameter<
+ "", "cir::DataMemberType">:$type,
+ "uint64_t":$offset);
+ let description = [{
+ Like `#cir.data_member`, this is a literal attribute representing a constant
+ (non-null) pointer-to-data-member value. It is used for members that are
+ not laid out in the CIR record and therefore have no field-index path -- in
+ particular `[[no_unique_address]]` empty fields. The concrete byte offset
+ of the member within the class is stored directly.
+
+ Example:
+ ```
+ // Empty S::*p = &S::e (e is a no_unique_address empty field at byte 1)
+ #cir.data_member_offset<1> : !cir.data_member<!rec_Empty in !rec_S>
+ ```
+ }];
+
+ let builders = [
+ AttrBuilderWithInferredContext<(ins "cir::DataMemberType":$type,
+ "uint64_t":$offset), [{
+ return $_get(type.getContext(), type, offset);
+ }]>,
+ ];
+
+ // This attribute gets lowered during CXXABILowering.
+ let hasAttrToValueLowering = 0;
+
+ let assemblyFormat = [{
+ `<` $offset `>`
+ }];
+}
+
//===----------------------------------------------------------------------===//
// MethodAttr
//===----------------------------------------------------------------------===//
diff --git a/clang/lib/CIR/CodeGen/CIRGenExprConstant.cpp b/clang/lib/CIR/CodeGen/CIRGenExprConstant.cpp
index 5e8ed1e3b78b9..3fcf577776663 100644
--- a/clang/lib/CIR/CodeGen/CIRGenExprConstant.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenExprConstant.cpp
@@ -1469,11 +1469,17 @@ mlir::Attribute ConstantEmitter::tryEmitPrivate(const APValue &value,
const auto *fieldDecl = cast<FieldDecl>(memberDecl);
const auto *mpt = destType->castAs<MemberPointerType>();
const auto *destClass = mpt->getMostRecentCXXRecordDecl();
- if (fieldDecl->hasAttr<NoUniqueAddressAttr>()) {
- assert(!cir::MissingFeatures::noUniqueAddressLayout());
- cgm.errorNYI("ConstExprEmitter::tryEmitPrivate: no_unique_address field");
- return {};
+
+ // Empty [[no_unique_address]] fields have no CIR field index; represent the
+ // pointer-to-data-member by its concrete byte offset.
+ if (cgm.isEmptyFieldForMemberPointer(fieldDecl)) {
+ const ASTContext &astContext = cgm.getASTContext();
+ CharUnits offset =
+ astContext.getMemberPointerPathAdjustment(value) +
+ astContext.toCharUnitsFromBits(astContext.getFieldOffset(fieldDecl));
+ return cir::DataMemberOffsetAttr::get(cirTy, offset.getQuantity());
}
+
std::optional<llvm::SmallVector<int32_t>> path =
cgm.buildMemberPath(destClass, fieldDecl);
if (!path)
diff --git a/clang/lib/CIR/CodeGen/CIRGenModule.cpp b/clang/lib/CIR/CodeGen/CIRGenModule.cpp
index 0d5caa6c424df..7079bb54f44e9 100644
--- a/clang/lib/CIR/CodeGen/CIRGenModule.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenModule.cpp
@@ -2307,11 +2307,19 @@ mlir::Value CIRGenModule::emitMemberPointerConstant(const UnaryOperator *e) {
const auto *mpt = e->getType()->castAs<MemberPointerType>();
const auto *destClass = mpt->getMostRecentCXXRecordDecl();
- if (fieldDecl->hasAttr<NoUniqueAddressAttr>()) {
- assert(!cir::MissingFeatures::noUniqueAddressLayout());
- errorNYI(e->getExprLoc(),
- "emitMemberPointerConstant: no_unique_address field");
- return {};
+ // Empty [[no_unique_address]] fields have no CIR field index; represent the
+ // pointer-to-data-member by its concrete byte offset within the class.
+ if (isEmptyFieldForMemberPointer(fieldDecl)) {
+ // This function should ONLY be accessed in reference to itself, I don't see
+ // any cases/couldn't find any cases where anything else could get here, and
+ // classic-codegen does the same.
+ assert(fieldDecl->getParent() == destClass &&
+ "scalar member pointer should be relative to the declaring class");
+ uint64_t offset =
+ astContext.toCharUnitsFromBits(astContext.getFieldOffset(fieldDecl))
+ .getQuantity();
+ return cir::ConstantOp::create(builder, loc,
+ cir::DataMemberOffsetAttr::get(ty, offset));
}
std::optional<llvm::SmallVector<int32_t>> path =
@@ -2325,8 +2333,6 @@ mlir::Value CIRGenModule::emitMemberPointerConstant(const UnaryOperator *e) {
std::optional<llvm::SmallVector<int32_t>>
CIRGenModule::buildMemberPath(const CXXRecordDecl *destClass,
const FieldDecl *field) {
- assert(!field->hasAttr<NoUniqueAddressAttr>());
-
llvm::SmallVector<int32_t> path;
if (!findFieldMemberPath(destClass, field, path))
return std::nullopt;
@@ -2396,6 +2402,11 @@ bool CIRGenModule::findFieldMemberPath(const CXXRecordDecl *currentClass,
return false;
}
+bool CIRGenModule::isEmptyFieldForMemberPointer(const FieldDecl *field) {
+ return isEmptyFieldForLayout(astContext, field) &&
+ field->isPotentiallyOverlapping();
+}
+
void CIRGenModule::emitDeclContext(const DeclContext *dc) {
for (Decl *decl : dc->decls()) {
// Unlike other DeclContexts, the contents of an ObjCImplDecl at TU scope
diff --git a/clang/lib/CIR/CodeGen/CIRGenModule.h b/clang/lib/CIR/CodeGen/CIRGenModule.h
index e47498dbc1588..f5ac70a33aef5 100644
--- a/clang/lib/CIR/CodeGen/CIRGenModule.h
+++ b/clang/lib/CIR/CodeGen/CIRGenModule.h
@@ -726,6 +726,13 @@ class CIRGenModule : public CIRGenTypeCache {
std::optional<llvm::SmallVector<int32_t>>
buildMemberPath(const CXXRecordDecl *destClass, const FieldDecl *field);
+ /// Returns true if \p field is an empty field that isn't laid out in the CIR
+ /// record (e.g. a [[no_unique_address]] empty member). Such fields have no
+ /// CIR field index, so a pointer-to-data-member to them is represented by an
+ /// explicit byte offset (#cir.data_member_offset) rather than a field-index
+ /// path.
+ bool isEmptyFieldForMemberPointer(const FieldDecl *field);
+
llvm::StringRef getMangledName(clang::GlobalDecl gd);
// This function is to support the OpenACC 'bind' clause, which names an
// alternate name for the function to be called by. This function mangles
diff --git a/clang/lib/CIR/Dialect/IR/CIRDialect.cpp b/clang/lib/CIR/Dialect/IR/CIRDialect.cpp
index 281956a5e40a5..590b206f8258d 100644
--- a/clang/lib/CIR/Dialect/IR/CIRDialect.cpp
+++ b/clang/lib/CIR/Dialect/IR/CIRDialect.cpp
@@ -578,7 +578,8 @@ static LogicalResult checkConstantTypes(mlir::Operation *op, mlir::Type opType,
return success();
}
- if (isa<cir::DataMemberAttr, cir::MethodAttr>(attrType)) {
+ if (isa<cir::DataMemberAttr, cir::DataMemberOffsetAttr, cir::MethodAttr>(
+ attrType)) {
// More detailed type verifications are already done in
// DataMemberAttr::verify or MethodAttr::verify. Don't need to repeat here.
return success();
diff --git a/clang/lib/CIR/Dialect/Transforms/CXXABILowering.cpp b/clang/lib/CIR/Dialect/Transforms/CXXABILowering.cpp
index 704ebbeb1ecd9..50a51d9618118 100644
--- a/clang/lib/CIR/Dialect/Transforms/CXXABILowering.cpp
+++ b/clang/lib/CIR/Dialect/Transforms/CXXABILowering.cpp
@@ -67,7 +67,8 @@ bool isCXXABIAttributeLegal(const mlir::TypeConverter &tc,
return true;
// Data Member and method are ALWAYS illegal.
- if (isa<cir::DataMemberAttr, cir::MethodAttr>(attr))
+ if (isa<cir::DataMemberAttr, cir::DataMemberOffsetAttr, cir::MethodAttr>(
+ attr))
return false;
return llvm::TypeSwitch<mlir::Attribute, bool>(attr)
@@ -394,6 +395,12 @@ static mlir::TypedAttr lowerInitialValue(const LowerModule *lowerModule,
mlir::Type ty,
mlir::Attribute initVal) {
if (mlir::isa<cir::DataMemberType>(ty)) {
+ // Members without a CIR field index (e.g. no_unique_address empty fields)
+ // are represented by an explicit byte offset instead of a field path.
+ if (auto offsetVal =
+ mlir::dyn_cast_if_present<cir::DataMemberOffsetAttr>(initVal))
+ return lowerModule->getCXXABI().lowerDataMemberOffsetConstant(offsetVal,
+ layout, tc);
auto dataMemberVal = mlir::cast_if_present<cir::DataMemberAttr>(initVal);
return lowerModule->getCXXABI().lowerDataMemberConstant(dataMemberVal,
layout, tc);
diff --git a/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRCXXABI.h b/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRCXXABI.h
index 55607e24ce836..5c5d6421aea71 100644
--- a/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRCXXABI.h
+++ b/clang/lib/CIR/Dialect/Transforms/TargetLowering/CIRCXXABI.h
@@ -57,6 +57,13 @@ class CIRCXXABI {
const mlir::DataLayout &layout,
const mlir::TypeConverter &typeConverter) const = 0;
+ /// Lower the given by-offset data member pointer constant (used for members
+ /// with no CIR field index, e.g. no_unique_address empty fields) to a
+ /// constant of the ABI type.
+ virtual mlir::TypedAttr lowerDataMemberOffsetConstant(
+ cir::DataMemberOffsetAttr attr, const mlir::DataLayout &layout,
+ const mlir::TypeConverter &typeConverter) const = 0;
+
/// Lower the given member function pointer constant to a constant of the ABI
/// type. The returned constant is represented as an attribute as well.
virtual mlir::TypedAttr
diff --git a/clang/lib/CIR/Dialect/Transforms/TargetLowering/LowerItaniumCXXABI.cpp b/clang/lib/CIR/Dialect/Transforms/TargetLowering/LowerItaniumCXXABI.cpp
index 5218d4979f353..6c276a83f18cf 100644
--- a/clang/lib/CIR/Dialect/Transforms/TargetLowering/LowerItaniumCXXABI.cpp
+++ b/clang/lib/CIR/Dialect/Transforms/TargetLowering/LowerItaniumCXXABI.cpp
@@ -55,6 +55,10 @@ class LowerItaniumCXXABI : public CIRCXXABI {
cir::DataMemberAttr attr, const mlir::DataLayout &layout,
const mlir::TypeConverter &typeConverter) const override;
+ mlir::TypedAttr lowerDataMemberOffsetConstant(
+ cir::DataMemberOffsetAttr attr, const mlir::DataLayout &layout,
+ const mlir::TypeConverter &typeConverter) const override;
+
mlir::TypedAttr
lowerMethodConstant(cir::MethodAttr attr, const mlir::DataLayout &layout,
const mlir::TypeConverter &typeConverter) const override;
@@ -217,6 +221,17 @@ mlir::TypedAttr LowerItaniumCXXABI::lowerDataMemberConstant(
return cir::IntAttr::get(abiTy, memberOffset);
}
+mlir::TypedAttr LowerItaniumCXXABI::lowerDataMemberOffsetConstant(
+ cir::DataMemberOffsetAttr attr, const mlir::DataLayout &layout,
+ const mlir::TypeConverter &typeConverter) const {
+ // Itanium C++ ABI 2.3:
+ // A pointer to data member is an offset from the base address of the class
+ // object containing it, represented as a ptr
diff _t.
+ // The offset is already known (the member has no CIR field index).
+ mlir::Type abiTy = lowerDataMemberType(attr.getType(), typeConverter);
+ return cir::IntAttr::get(abiTy, static_cast<int64_t>(attr.getOffset()));
+}
+
mlir::TypedAttr LowerItaniumCXXABI::lowerMethodConstant(
cir::MethodAttr attr, const mlir::DataLayout &layout,
const mlir::TypeConverter &typeConverter) const {
diff --git a/clang/test/CIR/CodeGen/pointer-to-empty-data-member.cpp b/clang/test/CIR/CodeGen/pointer-to-empty-data-member.cpp
index 5d368f27033f5..0b493dc306594 100644
--- a/clang/test/CIR/CodeGen/pointer-to-empty-data-member.cpp
+++ b/clang/test/CIR/CodeGen/pointer-to-empty-data-member.cpp
@@ -87,7 +87,6 @@ struct hasNUA {
[[no_unique_address]] EmptyBase eb6;
int i;
};
-// FIXME(cir): We should represent eb1-6 somehow
// CIR-DAG: !rec_hasNUA = !cir.struct<"hasNUA" padded {!s32i, !cir.array<!u8i x 4>}>
// LLVM-DAG: %struct.hasNUA = type { i32, [4 x i8] }
@@ -96,19 +95,50 @@ const hasNUA nua = {{},{},{},{},{},{}, 1};
// LLVMCIR-DAG: @_ZL3nua = internal constant %struct.hasNUA { i32 1, [4 x i8] zeroinitializer }
// OGCG-DAG: @_ZL3nua = internal constant %struct.hasNUA { i32 1, [4 x i8] undef }
-// FIXME(cir): These are still an NYI, and we should enable tests here once we
-// figure out how to represent these in IR.
-//EmptyBase hasNUA::* eb1 = &hasNUA::eb1;
-//EmptyBase hasNUA::* eb2 = &hasNUA::eb2;
-//EmptyBase hasNUA::* eb3 = &hasNUA::eb3;
-//EmptyBase hasNUA::* eb4 = &hasNUA::eb4;
-//EmptyBase hasNUA::* eb5 = &hasNUA::eb5;
-//EmptyBase hasNUA::* eb6 = &hasNUA::eb6;
+// A pointer-to-data-member for a no_unique_address empty field has no CIR
+// field index (the field isn't laid out), so it is represented by its concrete
+// byte offset via #cir.data_member_offset.
+EmptyBase hasNUA::* eb1 = &hasNUA::eb1;
+// CIR-BEFORE-DAG: cir.global external @eb1 = #cir.data_member_offset<0> : !cir.data_member<!rec_EmptyBase in !rec_hasNUA>
+// CIR-AFTER-DAG: cir.global external @eb1 = #cir.int<0> : !s64i
+// LLVM-DAG: @eb1 = global i64 0
+EmptyBase hasNUA::* eb2 = &hasNUA::eb2;
+// CIR-BEFORE-DAG: cir.global external @eb2 = #cir.data_member_offset<1> : !cir.data_member<!rec_EmptyBase in !rec_hasNUA>
+// CIR-AFTER-DAG: cir.global external @eb2 = #cir.int<1> : !s64i
+// LLVM-DAG: @eb2 = global i64 1
+EmptyBase hasNUA::* eb3 = &hasNUA::eb3;
+// CIR-BEFORE-DAG: cir.global external @eb3 = #cir.data_member_offset<2> : !cir.data_member<!rec_EmptyBase in !rec_hasNUA>
+// CIR-AFTER-DAG: cir.global external @eb3 = #cir.int<2> : !s64i
+// LLVM-DAG: @eb3 = global i64 2
+EmptyBase hasNUA::* eb4 = &hasNUA::eb4;
+// CIR-BEFORE-DAG: cir.global external @eb4 = #cir.data_member_offset<3> : !cir.data_member<!rec_EmptyBase in !rec_hasNUA>
+// CIR-AFTER-DAG: cir.global external @eb4 = #cir.int<3> : !s64i
+// LLVM-DAG: @eb4 = global i64 3
+EmptyBase hasNUA::* eb5 = &hasNUA::eb5;
+// CIR-BEFORE-DAG: cir.global external @eb5 = #cir.data_member_offset<4> : !cir.data_member<!rec_EmptyBase in !rec_hasNUA>
+// CIR-AFTER-DAG: cir.global external @eb5 = #cir.int<4> : !s64i
+// LLVM-DAG: @eb5 = global i64 4
+EmptyBase hasNUA::* eb6 = &hasNUA::eb6;
+// CIR-BEFORE-DAG: cir.global external @eb6 = #cir.data_member_offset<5> : !cir.data_member<!rec_EmptyBase in !rec_hasNUA>
+// CIR-AFTER-DAG: cir.global external @eb6 = #cir.int<5> : !s64i
+// LLVM-DAG: @eb6 = global i64 5
int hasNUA::* nua_i = &hasNUA::i;
// CIR-BEFORE-DAG: cir.global external @nua_i = #cir.data_member<[0]> : !cir.data_member<!s32i in !rec_hasNUA>
// CIR-AFTER-DAG: cir.global external @nua_i = #cir.int<0> : !s64i
// LLVM-DAG: @nua_i = global i64 0
+struct FirstField { int a; };
+struct HoldsNUA { int b; [[no_unique_address]] EmptyBase e; [[no_unique_address]] EmptyBase e2; };
+struct DerivesNUA : FirstField, HoldsNUA {};
+EmptyBase DerivesNUA::* nua_in_base = &DerivesNUA::e;
+// CIR-BEFORE-DAG: cir.global external @nua_in_base = #cir.data_member_offset<4> : !cir.data_member<!rec_EmptyBase in !rec_DerivesNUA>
+// CIR-AFTER-DAG: cir.global external @nua_in_base = #cir.int<4> : !s64i
+// LLVM-DAG: @nua_in_base = global i64 4
+EmptyBase DerivesNUA::* nua_in_base2 = &DerivesNUA::e2;
+// CIR-BEFORE-DAG: cir.global external @nua_in_base2 = #cir.data_member_offset<8> : !cir.data_member<!rec_EmptyBase in !rec_DerivesNUA>
+// CIR-AFTER-DAG: cir.global external @nua_in_base2 = #cir.int<8> : !s64i
+// LLVM-DAG: @nua_in_base2 = global i64 8
+
union U1 {
EmptyBase eb;
};
@@ -205,9 +235,14 @@ int U6::* u6i = &U6::i;
// CIR-AFTER-DAG: cir.global external @u6i = #cir.int<0> : !s64i
// LLVM-DAG: @u6i = global i64 0
-// FIXME(cir): See above.
-//EmptyBase U6::* u6eb = &U6::eb;
-//EmptyBase U6::* u6eb2 = &U6::eb2;
+EmptyBase U6::* u6eb = &U6::eb;
+// CIR-BEFORE-DAG: cir.global external @u6eb = #cir.data_member_offset<0> : !cir.data_member<!rec_EmptyBase in !rec_U6>
+// CIR-AFTER-DAG: cir.global external @u6eb = #cir.int<0> : !s64i
+// LLVM-DAG: @u6eb = global i64 0
+EmptyBase U6::* u6eb2 = &U6::eb2;
+// CIR-BEFORE-DAG: cir.global external @u6eb2 = #cir.data_member_offset<0> : !cir.data_member<!rec_EmptyBase in !rec_U6>
+// CIR-AFTER-DAG: cir.global external @u6eb2 = #cir.int<0> : !s64i
+// LLVM-DAG: @u6eb2 = global i64 0
union U7 {
int i;
@@ -222,9 +257,14 @@ int U7::* u7i = &U7::i;
// CIR-AFTER-DAG: cir.global external @u7i = #cir.int<0> : !s64i
// LLVM-DAG: @u7i = global i64 0
-// FIXME(cir): See above.
-//EmptyBase U7::* u7eb = &U7::eb;
-//EmptyBase U7::* u7eb2 = &U7::eb2;
+EmptyBase U7::* u7eb = &U7::eb;
+// CIR-BEFORE-DAG: cir.global external @u7eb = #cir.data_member_offset<0> : !cir.data_member<!rec_EmptyBase in !rec_U7>
+// CIR-AFTER-DAG: cir.global external @u7eb = #cir.int<0> : !s64i
+// LLVM-DAG: @u7eb = global i64 0
+EmptyBase U7::* u7eb2 = &U7::eb2;
+// CIR-BEFORE-DAG: cir.global external @u7eb2 = #cir.data_member_offset<0> : !cir.data_member<!rec_EmptyBase in !rec_U7>
+// CIR-AFTER-DAG: cir.global external @u7eb2 = #cir.int<0> : !s64i
+// LLVM-DAG: @u7eb2 = global i64 0
void uses() {
auto x = &HasEmpty::s;
More information about the cfe-commits
mailing list