https://github.com/erichkeane updated 
https://github.com/llvm/llvm-project/pull/208777

>From c08da135b07fd247ccad372cec9e38278fc27699 Mon Sep 17 00:00:00 2001
From: erichkeane <[email protected]>
Date: Thu, 9 Jul 2026 08:45:11 -0700
Subject: [PATCH 1/3] [CIR] Introduce data_member_offset attribute

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.
---
 .../include/clang/CIR/Dialect/IR/CIRAttrs.td  | 39 +++++++++++++
 clang/lib/CIR/CodeGen/CIRGenExprConstant.cpp  | 12 ++--
 clang/lib/CIR/CodeGen/CIRGenModule.cpp        | 48 ++++++++++++---
 clang/lib/CIR/CodeGen/CIRGenModule.h          |  9 +++
 clang/lib/CIR/Dialect/IR/CIRDialect.cpp       |  3 +-
 .../CIR/Dialect/Transforms/CXXABILowering.cpp |  9 ++-
 .../Transforms/TargetLowering/CIRCXXABI.h     |  7 +++
 .../TargetLowering/LowerItaniumCXXABI.cpp     | 15 +++++
 .../CodeGen/pointer-to-empty-data-member.cpp  | 58 ++++++++++++++-----
 9 files changed, 170 insertions(+), 30 deletions(-)

diff --git a/clang/include/clang/CIR/Dialect/IR/CIRAttrs.td 
b/clang/include/clang/CIR/Dialect/IR/CIRAttrs.td
index ec99899355b17..4316c639e0bbd 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 4e16351c9a951..2a90cdaac92f5 100644
--- a/clang/lib/CIR/CodeGen/CIRGenExprConstant.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenExprConstant.cpp
@@ -1415,11 +1415,13 @@ 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 within the class.
+    if (std::optional<uint64_t> offset =
+            cgm.getEmptyFieldMemberPointerOffset(destClass, fieldDecl))
+      return cir::DataMemberOffsetAttr::get(cirTy, *offset);
+
     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 6b7d44b09118b..94478052221fc 100644
--- a/clang/lib/CIR/CodeGen/CIRGenModule.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenModule.cpp
@@ -2306,12 +2306,12 @@ 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 (std::optional<uint64_t> offset =
+          getEmptyFieldMemberPointerOffset(destClass, fieldDecl))
+    return cir::ConstantOp::create(builder, loc,
+                                   cir::DataMemberOffsetAttr::get(ty, 
*offset));
 
   std::optional<llvm::SmallVector<int32_t>> path =
       buildMemberPath(destClass, fieldDecl);
@@ -2324,8 +2324,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;
@@ -2395,6 +2393,40 @@ bool CIRGenModule::findFieldMemberPath(const 
CXXRecordDecl *currentClass,
   return false;
 }
 
+/// Computes the byte offset of \p field within \p cls, descending through base
+/// subobjects when the field is inherited.
+static uint64_t getFieldByteOffsetInClass(const ASTContext &ctx,
+                                          const CXXRecordDecl *cls,
+                                          const FieldDecl *field) {
+  const ASTRecordLayout &layout = ctx.getASTRecordLayout(cls);
+  const auto *fieldParent = cast<CXXRecordDecl>(field->getParent());
+  if (fieldParent == cls)
+    return ctx
+        .toCharUnitsFromBits(layout.getFieldOffset(field->getFieldIndex()))
+        .getQuantity();
+
+  for (const CXXBaseSpecifier &base : cls->bases()) {
+    const auto *baseDecl = base.getType()->getAsCXXRecordDecl();
+    if (baseDecl == fieldParent || baseDecl->isDerivedFrom(fieldParent))
+      return layout.getBaseClassOffset(baseDecl).getQuantity() +
+             getFieldByteOffsetInClass(ctx, baseDecl, field);
+  }
+
+  llvm_unreachable("field is not a member of the given class");
+}
+
+std::optional<uint64_t>
+CIRGenModule::getEmptyFieldMemberPointerOffset(const CXXRecordDecl *destClass,
+                                               const FieldDecl *field) {
+  // Only empty, potentially-overlapping fields are omitted from the CIR record
+  // (see CIRRecordLowering::accumulateFields).
+  if (!isEmptyFieldForLayout(astContext, field) ||
+      !field->isPotentiallyOverlapping())
+    return std::nullopt;
+
+  return getFieldByteOffsetInClass(astContext, destClass, field);
+}
+
 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 144f8c7b9f3e7..66008d240b4e0 100644
--- a/clang/lib/CIR/CodeGen/CIRGenModule.h
+++ b/clang/lib/CIR/CodeGen/CIRGenModule.h
@@ -726,6 +726,15 @@ class CIRGenModule : public CIRGenTypeCache {
   std::optional<llvm::SmallVector<int32_t>>
   buildMemberPath(const CXXRecordDecl *destClass, const FieldDecl *field);
 
+  /// If \p field is an empty field that isn't laid out in the CIR record
+  /// (e.g. a [[no_unique_address]] empty member), returns its concrete byte
+  /// offset within \p destClass. Such fields have no CIR field index, so a
+  /// pointer-to-data-member to them is represented by an explicit offset
+  /// rather than a field-index path.  Returns std::nullopt otherwise.
+  std::optional<uint64_t>
+  getEmptyFieldMemberPointerOffset(const CXXRecordDecl *destClass,
+                                   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 67cc5e09f26d0..24883fc3a2adb 100644
--- a/clang/lib/CIR/Dialect/IR/CIRDialect.cpp
+++ b/clang/lib/CIR/Dialect/IR/CIRDialect.cpp
@@ -546,7 +546,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 ptrdiff_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..4251c659565bd 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,14 +95,33 @@ 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
@@ -205,9 +223,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 +245,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;

>From 46db8234ff7752c70b9f04cc05f5b12766cbefc7 Mon Sep 17 00:00:00 2001
From: erichkeane <[email protected]>
Date: Mon, 13 Jul 2026 15:11:34 -0700
Subject: [PATCH 2/3] simplify how I calculate the offset of empty fields.

Self-review revealed that I was probably doing this the 'hard' way, so I
went back to classic codegen and realized I was mis-understanding the
use in emitMemberPointerCast, and could do it just with getFieldOffset.
This reverts the calculation I was doing to look into each base, and
instead replaces it with the way that classic codegen does it.
---
 clang/lib/CIR/CodeGen/CIRGenExprConstant.cpp  | 12 +++--
 clang/lib/CIR/CodeGen/CIRGenModule.cpp        | 49 ++++++-------------
 clang/lib/CIR/CodeGen/CIRGenModule.h          | 14 +++---
 .../CodeGen/pointer-to-empty-data-member.cpp  |  8 +++
 4 files changed, 36 insertions(+), 47 deletions(-)

diff --git a/clang/lib/CIR/CodeGen/CIRGenExprConstant.cpp 
b/clang/lib/CIR/CodeGen/CIRGenExprConstant.cpp
index 2a90cdaac92f5..30b2e9a6d6d9f 100644
--- a/clang/lib/CIR/CodeGen/CIRGenExprConstant.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenExprConstant.cpp
@@ -1417,10 +1417,14 @@ mlir::Attribute ConstantEmitter::tryEmitPrivate(const 
APValue &value,
     const auto *destClass = mpt->getMostRecentCXXRecordDecl();
 
     // 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 (std::optional<uint64_t> offset =
-            cgm.getEmptyFieldMemberPointerOffset(destClass, fieldDecl))
-      return cir::DataMemberOffsetAttr::get(cirTy, *offset);
+    // 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);
diff --git a/clang/lib/CIR/CodeGen/CIRGenModule.cpp 
b/clang/lib/CIR/CodeGen/CIRGenModule.cpp
index 94478052221fc..a3764f0844738 100644
--- a/clang/lib/CIR/CodeGen/CIRGenModule.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenModule.cpp
@@ -2308,10 +2308,18 @@ mlir::Value 
CIRGenModule::emitMemberPointerConstant(const UnaryOperator *e) {
 
   // 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 (std::optional<uint64_t> offset =
-          getEmptyFieldMemberPointerOffset(destClass, fieldDecl))
+  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));
+                                   cir::DataMemberOffsetAttr::get(ty, offset));
+  }
 
   std::optional<llvm::SmallVector<int32_t>> path =
       buildMemberPath(destClass, fieldDecl);
@@ -2393,38 +2401,9 @@ bool CIRGenModule::findFieldMemberPath(const 
CXXRecordDecl *currentClass,
   return false;
 }
 
-/// Computes the byte offset of \p field within \p cls, descending through base
-/// subobjects when the field is inherited.
-static uint64_t getFieldByteOffsetInClass(const ASTContext &ctx,
-                                          const CXXRecordDecl *cls,
-                                          const FieldDecl *field) {
-  const ASTRecordLayout &layout = ctx.getASTRecordLayout(cls);
-  const auto *fieldParent = cast<CXXRecordDecl>(field->getParent());
-  if (fieldParent == cls)
-    return ctx
-        .toCharUnitsFromBits(layout.getFieldOffset(field->getFieldIndex()))
-        .getQuantity();
-
-  for (const CXXBaseSpecifier &base : cls->bases()) {
-    const auto *baseDecl = base.getType()->getAsCXXRecordDecl();
-    if (baseDecl == fieldParent || baseDecl->isDerivedFrom(fieldParent))
-      return layout.getBaseClassOffset(baseDecl).getQuantity() +
-             getFieldByteOffsetInClass(ctx, baseDecl, field);
-  }
-
-  llvm_unreachable("field is not a member of the given class");
-}
-
-std::optional<uint64_t>
-CIRGenModule::getEmptyFieldMemberPointerOffset(const CXXRecordDecl *destClass,
-                                               const FieldDecl *field) {
-  // Only empty, potentially-overlapping fields are omitted from the CIR record
-  // (see CIRRecordLowering::accumulateFields).
-  if (!isEmptyFieldForLayout(astContext, field) ||
-      !field->isPotentiallyOverlapping())
-    return std::nullopt;
-
-  return getFieldByteOffsetInClass(astContext, destClass, field);
+bool CIRGenModule::isEmptyFieldForMemberPointer(const FieldDecl *field) {
+  return isEmptyFieldForLayout(astContext, field) &&
+         field->isPotentiallyOverlapping();
 }
 
 void CIRGenModule::emitDeclContext(const DeclContext *dc) {
diff --git a/clang/lib/CIR/CodeGen/CIRGenModule.h 
b/clang/lib/CIR/CodeGen/CIRGenModule.h
index 66008d240b4e0..82fd0d4405ef3 100644
--- a/clang/lib/CIR/CodeGen/CIRGenModule.h
+++ b/clang/lib/CIR/CodeGen/CIRGenModule.h
@@ -726,14 +726,12 @@ class CIRGenModule : public CIRGenTypeCache {
   std::optional<llvm::SmallVector<int32_t>>
   buildMemberPath(const CXXRecordDecl *destClass, const FieldDecl *field);
 
-  /// If \p field is an empty field that isn't laid out in the CIR record
-  /// (e.g. a [[no_unique_address]] empty member), returns its concrete byte
-  /// offset within \p destClass. Such fields have no CIR field index, so a
-  /// pointer-to-data-member to them is represented by an explicit offset
-  /// rather than a field-index path.  Returns std::nullopt otherwise.
-  std::optional<uint64_t>
-  getEmptyFieldMemberPointerOffset(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
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 4251c659565bd..4ce8620a496f1 100644
--- a/clang/test/CIR/CodeGen/pointer-to-empty-data-member.cpp
+++ b/clang/test/CIR/CodeGen/pointer-to-empty-data-member.cpp
@@ -127,6 +127,14 @@ int hasNUA::* nua_i = &hasNUA::i;
 // 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; };
+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
+
 union U1 {
   EmptyBase eb;
 };

>From 43cbc6b2bd4cb707654f4ba9b90f9a1a5a04181e Mon Sep 17 00:00:00 2001
From: erichkeane <[email protected]>
Date: Wed, 15 Jul 2026 07:19:44 -0700
Subject: [PATCH 3/3] Add an additional field offset test

---
 clang/test/CIR/CodeGen/pointer-to-empty-data-member.cpp | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

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 4ce8620a496f1..0b493dc306594 100644
--- a/clang/test/CIR/CodeGen/pointer-to-empty-data-member.cpp
+++ b/clang/test/CIR/CodeGen/pointer-to-empty-data-member.cpp
@@ -128,12 +128,16 @@ int hasNUA::* nua_i = &hasNUA::i;
 // LLVM-DAG: @nua_i = global i64 0
 
 struct FirstField { int a; };
-struct HoldsNUA { int b; [[no_unique_address]] EmptyBase e; };
+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;

_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to