[clang] [CIR][CodeGen] Support DesignatedInitUpdateExpr in constant emission (PR #194238)
via cfe-commits
cfe-commits at lists.llvm.org
Tue Apr 28 03:12:06 PDT 2026
https://github.com/AbdallahRashed updated https://github.com/llvm/llvm-project/pull/194238
>From 7aa125fdba002b6a52fd59bc09d48a9b3f267690 Mon Sep 17 00:00:00 2001
From: AbdallahRashed <abdallah.mrashed at gmail.com>
Date: Sun, 26 Apr 2026 17:04:55 +0200
Subject: [PATCH 1/3] [CIR][CodeGen] Support DesignatedInitUpdateExpr in
constant emission
---
clang/include/clang/CIR/MissingFeatures.h | 1 +
clang/lib/CIR/CodeGen/CIRGenExprConstant.cpp | 94 ++++++++++++++++++-
.../test/CIR/CodeGen/designated-init-update.c | 36 +++++++
3 files changed, 126 insertions(+), 5 deletions(-)
create mode 100644 clang/test/CIR/CodeGen/designated-init-update.c
diff --git a/clang/include/clang/CIR/MissingFeatures.h b/clang/include/clang/CIR/MissingFeatures.h
index 634d4dd83b3f8..fc87150b98eef 100644
--- a/clang/include/clang/CIR/MissingFeatures.h
+++ b/clang/include/clang/CIR/MissingFeatures.h
@@ -230,6 +230,7 @@ struct MissingFeatures {
static bool cleanupDeactivationScope() { return false; }
static bool cleanupWithPreservedValues() { return false; }
static bool cleanupsToDeactivate() { return false; }
+ static bool constEmitterAbstractForMemory() { return false; }
static bool constEmitterAggILE() { return false; }
static bool constEmitterArrayILE() { return false; }
static bool constEmitterVectorILE() { return false; }
diff --git a/clang/lib/CIR/CodeGen/CIRGenExprConstant.cpp b/clang/lib/CIR/CodeGen/CIRGenExprConstant.cpp
index a8b24fb686088..9616b970da2d4 100644
--- a/clang/lib/CIR/CodeGen/CIRGenExprConstant.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenExprConstant.cpp
@@ -626,6 +626,11 @@ bool ConstRecordBuilder::applyZeroInitPadding(const ASTRecordLayout &layout,
return true;
}
+static bool emitDesignatedInitUpdater(ConstantEmitter &emitter,
+ ConstantAggregateBuilder &constant,
+ CharUnits offset, QualType type,
+ const InitListExpr *updater);
+
bool ConstRecordBuilder::build(InitListExpr *ile, bool allowOverwrite) {
RecordDecl *rd = ile->getType()->castAsRecordDecl();
const ASTRecordLayout &layout = cgm.getASTContext().getASTRecordLayout(rd);
@@ -679,8 +684,19 @@ bool ConstRecordBuilder::build(InitListExpr *ile, bool allowOverwrite) {
// a new constant to emit independently.
if (allowOverwrite &&
(field->getType()->isArrayType() || field->getType()->isRecordType())) {
- cgm.errorNYI(field->getSourceRange(), "designated init lists");
- return false;
+ if (auto *subILE = dyn_cast<InitListExpr>(init)) {
+ CharUnits fieldOffset = cgm.getASTContext().toCharUnitsFromBits(
+ layout.getFieldOffset(index));
+ if (!emitDesignatedInitUpdater(emitter, builder,
+ startOffset + fieldOffset,
+ field->getType(), subILE))
+ return false;
+ // If we split apart the field's value, try to collapse it down to a
+ // single value now.
+ builder.condense(startOffset + fieldOffset,
+ cgm.getTypes().convertTypeForMem(field->getType()));
+ continue;
+ }
}
mlir::Attribute eltInitAttr =
@@ -868,6 +884,65 @@ bool ConstRecordBuilder::updateRecord(ConstantEmitter &emitter,
.build(updater, /*allowOverwrite*/ true);
}
+static bool emitDesignatedInitUpdater(ConstantEmitter &emitter,
+ ConstantAggregateBuilder &constant,
+ CharUnits offset, QualType type,
+ const InitListExpr *updater) {
+ if (type->isRecordType())
+ return ConstRecordBuilder::updateRecord(
+ emitter, constant, offset, const_cast<InitListExpr *>(updater));
+
+ auto *cat = emitter.cgm.getASTContext().getAsConstantArrayType(type);
+ if (!cat)
+ return false;
+ QualType elemType = cat->getElementType();
+ CharUnits elemSize = emitter.cgm.getASTContext().getTypeSizeInChars(elemType);
+ mlir::Type elemTy = emitter.cgm.getTypes().convertTypeForMem(elemType);
+
+ mlir::TypedAttr fillC;
+ if (const Expr *filler = updater->getArrayFiller()) {
+ if (!isa<NoInitExpr>(filler)) {
+ // Classic codegen uses tryEmitAbstractForMemory here to track abstract
+ // state for pointer authentication. We should use that when implemented.
+ assert(!cir::MissingFeatures::constEmitterAbstractForMemory());
+ mlir::Attribute result =
+ emitter.tryEmitPrivateForMemory(filler, elemType);
+ if (!result)
+ return false;
+ fillC = mlir::cast<mlir::TypedAttr>(result);
+ }
+ }
+
+ unsigned numElementsToUpdate =
+ fillC ? cat->getZExtSize() : updater->getNumInits();
+ for (unsigned i = 0; i != numElementsToUpdate; ++i, offset += elemSize) {
+ const Expr *init = nullptr;
+ if (i < updater->getNumInits())
+ init = updater->getInit(i);
+
+ if (!init && fillC) {
+ if (!constant.add(fillC, offset, true))
+ return false;
+ } else if (!init || isa<NoInitExpr>(init)) {
+ continue;
+ } else if (const auto *childILE = dyn_cast<InitListExpr>(init)) {
+ if (!emitDesignatedInitUpdater(emitter, constant, offset, elemType,
+ childILE))
+ return false;
+ // Attempt to reduce the array element to a single constant if necessary.
+ constant.condense(offset, elemTy);
+ } else {
+ mlir::Attribute val = emitter.tryEmitPrivateForMemory(init, elemType);
+ if (!val)
+ return false;
+ if (!constant.add(mlir::cast<mlir::TypedAttr>(val), offset, true))
+ return false;
+ }
+ }
+
+ return true;
+}
+
//===----------------------------------------------------------------------===//
// ConstExprEmitter
//===----------------------------------------------------------------------===//
@@ -1074,9 +1149,18 @@ class ConstExprEmitter
if (!c)
return {};
- cgm.errorNYI(e->getBeginLoc(),
- "ConstExprEmitter::VisitDesignatedInitUpdateExpr");
- return {};
+ ConstantAggregateBuilder constant(cgm);
+ constant.add(mlir::cast<mlir::TypedAttr>(c), CharUnits::Zero(), false);
+
+ if (!emitDesignatedInitUpdater(emitter, constant, CharUnits::Zero(),
+ destType, e->getUpdater()))
+ return {};
+
+ mlir::Type valTy = cgm.convertType(destType);
+ bool hasFlexibleArray = false;
+ if (const auto *rd = destType->getAsRecordDecl())
+ hasFlexibleArray = rd->hasFlexibleArrayMember();
+ return constant.build(valTy, hasFlexibleArray);
}
mlir::Attribute VisitCXXConstructExpr(CXXConstructExpr *e, QualType ty) {
diff --git a/clang/test/CIR/CodeGen/designated-init-update.c b/clang/test/CIR/CodeGen/designated-init-update.c
new file mode 100644
index 0000000000000..6c75d2abe1710
--- /dev/null
+++ b/clang/test/CIR/CodeGen/designated-init-update.c
@@ -0,0 +1,36 @@
+// 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: %clang_cc1 -triple x86_64-unknown-linux-gnu -emit-llvm %s -o %t.ll
+// RUN: FileCheck --check-prefix=OGCG --input-file=%t.ll %s
+
+struct S {
+ int a, b, c;
+};
+
+// Basic designated init update: start from {1, 2, 3}, override .b = 20
+struct S g1 = (struct S){1, 2, 3, .b = 20};
+
+// CIR: cir.global external @g1 = #cir.const_record<{#cir.int<1> : !s32i, #cir.int<20> : !s32i, #cir.int<3> : !s32i}> : !rec_S
+// LLVM: @g1 = global %struct.S { i32 1, i32 20, i32 3 }
+// OGCG: @g1 = global %struct.S { i32 1, i32 20, i32 3 }
+
+// Multiple field overrides
+struct S g2 = (struct S){10, 20, 30, .a = 100, .c = 300};
+
+// CIR: cir.global external @g2 = #cir.const_record<{#cir.int<100> : !s32i, #cir.int<20> : !s32i, #cir.int<300> : !s32i}> : !rec_S
+// LLVM: @g2 = global %struct.S { i32 100, i32 20, i32 300 }
+// OGCG: @g2 = global %struct.S { i32 100, i32 20, i32 300 }
+
+// Nested struct with designated init update
+struct Outer {
+ struct S inner;
+ int x;
+};
+
+struct Outer g3 = (struct Outer){{1, 2, 3}, 4, .inner.b = 50};
+
+// CIR: cir.global external @g3 = #cir.const_record<{#cir.const_record<{#cir.int<1> : !s32i, #cir.int<50> : !s32i, #cir.int<3> : !s32i}> : !rec_S, #cir.int<4> : !s32i}> : !rec_Outer
+// LLVM: @g3 = global %struct.Outer { %struct.S { i32 1, i32 50, i32 3 }, i32 4 }
+// OGCG: @g3 = global %struct.Outer { %struct.S { i32 1, i32 50, i32 3 }, i32 4 }
>From 08c868c493cfbf2cc6f3affefdaa54a942c9a2ba Mon Sep 17 00:00:00 2001
From: AbdallahRashed <abdallah.mrashed at gmail.com>
Date: Mon, 27 Apr 2026 23:08:53 +0200
Subject: [PATCH 2/3] [CIR][CodeGen] Implement
ConstantAggregateBuilder::split()
Implement split() for ConstRecordAttr, ConstArrayAttr, ZeroAttr, and
UndefAttr. This enables DesignatedInitUpdateExpr to work end-to-end
when the base is a compound literal (which produces an opaque expression
that Sema cannot fold in-place).
Also fix ConstRecordBuilder::build() to skip zero-init padding when in
overwrite mode, since gaps between updated fields contain valid base data
that should not be zeroed out.
Add a test case using a compound literal sub-object with a later field
override, which produces a real DesignatedInitUpdateExpr AST node and
exercises the split() path.
---
clang/lib/CIR/CodeGen/CIRGenExprConstant.cpp | 78 ++++++++++++++++++-
.../test/CIR/CodeGen/designated-init-update.c | 14 ++++
2 files changed, 89 insertions(+), 3 deletions(-)
diff --git a/clang/lib/CIR/CodeGen/CIRGenExprConstant.cpp b/clang/lib/CIR/CodeGen/CIRGenExprConstant.cpp
index 9616b970da2d4..fe970ac8f774c 100644
--- a/clang/lib/CIR/CodeGen/CIRGenExprConstant.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenExprConstant.cpp
@@ -347,7 +347,79 @@ std::optional<size_t> ConstantAggregateBuilder::splitAt(CharUnits pos) {
/// Hint indicates the location at which we'd like to split, but may be
/// ignored.
bool ConstantAggregateBuilder::split(size_t index, CharUnits hint) {
- cgm.errorNYI("split constant at index");
+ naturalLayout = false;
+ mlir::TypedAttr c = elements[index].element;
+ CharUnits offset = elements[index].offset;
+
+ if (auto constRecord = mlir::dyn_cast<cir::ConstRecordAttr>(c)) {
+ // Expand the record into its contained member elements.
+ auto recordTy = mlir::cast<cir::RecordType>(constRecord.getType());
+ mlir::ArrayAttr members = constRecord.getMembers();
+ unsigned numMembers = members.size();
+
+ SmallVector<Element> newElems;
+ newElems.reserve(numMembers);
+ for (unsigned i = 0; i < numMembers; ++i) {
+ auto memberAttr = mlir::cast<mlir::TypedAttr>(members[i]);
+ CharUnits memberOffset =
+ offset + CharUnits::fromQuantity(
+ recordTy.getElementOffset(dataLayout.layout, i));
+ newElems.emplace_back(memberAttr, memberOffset);
+ }
+ replace(elements, index, index + 1, newElems);
+ return true;
+ }
+
+ if (auto constArray = mlir::dyn_cast<cir::ConstArrayAttr>(c)) {
+ // Expand the array into its contained elements.
+ auto arrayTy = mlir::cast<cir::ArrayType>(constArray.getType());
+ CharUnits elemSize = getSize(arrayTy.getElementType());
+
+ if (auto arrayAttr = mlir::dyn_cast<mlir::ArrayAttr>(constArray.getElts())) {
+ // Array with explicit elements (plus possible trailing zeros).
+ SmallVector<Element> newElems;
+ unsigned numExplicit = arrayAttr.size();
+ unsigned trailingZeros = constArray.getTrailingZerosNum();
+ newElems.reserve(numExplicit + trailingZeros);
+
+ for (unsigned i = 0; i < numExplicit; ++i) {
+ auto eltAttr = mlir::cast<mlir::TypedAttr>(arrayAttr[i]);
+ newElems.emplace_back(eltAttr, offset + i * elemSize);
+ }
+ // Expand trailing zeros as individual zero elements.
+ for (unsigned i = 0; i < trailingZeros; ++i) {
+ auto zeroAttr = mlir::cast<mlir::TypedAttr>(
+ cir::ZeroAttr::get(arrayTy.getElementType()));
+ newElems.emplace_back(zeroAttr, offset + (numExplicit + i) * elemSize);
+ }
+ replace(elements, index, index + 1, newElems);
+ return true;
+ }
+ // TODO: Handle StringAttr elements (char arrays) if needed.
+ return false;
+ }
+
+ if (mlir::isa<cir::ZeroAttr>(c)) {
+ // Split into two zeros at the hinted offset.
+ CharUnits elemSize = getSize(c);
+ assert(hint > offset && hint < offset + elemSize && "nothing to split");
+ // Create two byte-array padding elements to represent the two halves.
+ mlir::TypedAttr firstZero = getPadding(hint - offset);
+ mlir::TypedAttr secondZero = getPadding(offset + elemSize - hint);
+ SmallVector<Element> newElems;
+ newElems.emplace_back(firstZero, offset);
+ newElems.emplace_back(secondZero, hint);
+ replace(elements, index, index + 1, newElems);
+ return true;
+ }
+
+ if (mlir::isa<cir::UndefAttr>(c)) {
+ // Drop undef; it doesn't contribute to the final layout.
+ replace(elements, index, index + 1, SmallVector<Element>{});
+ return true;
+ }
+
+ // FIXME: We could split a cir::IntAttr if the need ever arose.
return false;
}
@@ -674,7 +746,7 @@ bool ConstRecordBuilder::build(InitListExpr *ile, bool allowOverwrite) {
continue;
}
- if (zeroInitPadding &&
+ if (zeroInitPadding && !allowOverwrite &&
!applyZeroInitPadding(layout, index, *field, allowOverwrite, sizeSoFar,
zeroFieldSize))
return false;
@@ -730,7 +802,7 @@ bool ConstRecordBuilder::build(InitListExpr *ile, bool allowOverwrite) {
}
}
- return !zeroInitPadding ||
+ return !zeroInitPadding || allowOverwrite ||
applyZeroInitPadding(layout, allowOverwrite, sizeSoFar);
}
diff --git a/clang/test/CIR/CodeGen/designated-init-update.c b/clang/test/CIR/CodeGen/designated-init-update.c
index 6c75d2abe1710..c7a495c5d8d0b 100644
--- a/clang/test/CIR/CodeGen/designated-init-update.c
+++ b/clang/test/CIR/CodeGen/designated-init-update.c
@@ -34,3 +34,17 @@ struct Outer g3 = (struct Outer){{1, 2, 3}, 4, .inner.b = 50};
// CIR: cir.global external @g3 = #cir.const_record<{#cir.const_record<{#cir.int<1> : !s32i, #cir.int<50> : !s32i, #cir.int<3> : !s32i}> : !rec_S, #cir.int<4> : !s32i}> : !rec_Outer
// LLVM: @g3 = global %struct.Outer { %struct.S { i32 1, i32 50, i32 3 }, i32 4 }
// OGCG: @g3 = global %struct.Outer { %struct.S { i32 1, i32 50, i32 3 }, i32 4 }
+
+// Compound literal as sub-object with later field override.
+// This produces a DesignatedInitUpdateExpr AST node (unlike the cases above
+// which Sema folds in-place) and exercises ConstantAggregateBuilder::split().
+struct P {
+ struct S s;
+ int x;
+};
+
+struct P g4 = { (struct S){1, 2, 3}, 4, .s.b = 9 };
+
+// CIR: cir.global external @g4 = #cir.const_record<{#cir.const_record<{#cir.int<1> : !s32i, #cir.int<9> : !s32i, #cir.int<3> : !s32i}> : !rec_S, #cir.int<4> : !s32i}> : !rec_P
+// LLVM: @g4 = global %struct.P { %struct.S { i32 1, i32 9, i32 3 }, i32 4 }
+// OGCG: @g4 = global %struct.P { %struct.S { i32 1, i32 9, i32 3 }, i32 4 }
>From 3afef05d4ae1a44e7d084fc771883a85501d0a22 Mon Sep 17 00:00:00 2001
From: AbdallahRashed <abdallah.mrashed at gmail.com>
Date: Tue, 28 Apr 2026 12:09:26 +0200
Subject: [PATCH 3/3] [CIR][CodeGen] Implement buildFrom for ArrayType
- Implement ConstantAggregateBuilder::buildFrom for cir::ArrayType (was NYI)
- Add defensive dyn_cast<TypedAttr> in DesignatedInitUpdateExpr emission
- Add array-path test case (g5) exercising split + condense + buildFrom
---
clang/lib/CIR/CodeGen/CIRGenExprConstant.cpp | 58 +++++++++++++++++--
.../test/CIR/CodeGen/designated-init-update.c | 11 ++++
2 files changed, 63 insertions(+), 6 deletions(-)
diff --git a/clang/lib/CIR/CodeGen/CIRGenExprConstant.cpp b/clang/lib/CIR/CodeGen/CIRGenExprConstant.cpp
index fe970ac8f774c..d0f9551224495 100644
--- a/clang/lib/CIR/CodeGen/CIRGenExprConstant.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenExprConstant.cpp
@@ -471,9 +471,43 @@ ConstantAggregateBuilder::buildFrom(CIRGenModule &cgm, ArrayRef<Element> elems,
// If we want an array type, see if all the elements are the same type and
// appropriately spaced.
- if (mlir::isa<cir::ArrayType>(desiredTy)) {
- cgm.errorNYI("array aggregate constants");
- return {};
+ if (auto arrTy = mlir::dyn_cast<cir::ArrayType>(desiredTy)) {
+ mlir::Type eltTy = arrTy.getElementType();
+ uint64_t numElements = arrTy.getSize();
+ CharUnits eltSize = utils.getSize(eltTy);
+
+ // Check if all elements have the correct type and are at stride offsets.
+ SmallVector<mlir::Attribute> arrayElts(numElements);
+ bool canBuildArray = true;
+
+ for (const auto &[element, offset] : elems) {
+ CharUnits relOffset = offset - startOffset;
+ if (relOffset % eltSize != CharUnits::Zero() ||
+ element.getType() != eltTy) {
+ canBuildArray = false;
+ break;
+ }
+ uint64_t idx = relOffset / eltSize;
+ if (idx >= numElements) {
+ canBuildArray = false;
+ break;
+ }
+ arrayElts[idx] = element;
+ }
+
+ if (canBuildArray) {
+ // Fill gaps with zero.
+ for (uint64_t i = 0; i < numElements; ++i) {
+ if (!arrayElts[i])
+ arrayElts[i] = cir::ZeroAttr::get(eltTy);
+ }
+ CIRGenBuilderTy &builder = cgm.getBuilder();
+ return cir::ConstArrayAttr::get(
+ arrTy, mlir::ArrayAttr::get(builder.getContext(), arrayElts));
+ }
+
+ // Elements don't form a clean array layout; fall through to struct-based
+ // packing below.
}
// The size of the constant we plan to generate. This is usually just the size
@@ -769,6 +803,9 @@ bool ConstRecordBuilder::build(InitListExpr *ile, bool allowOverwrite) {
cgm.getTypes().convertTypeForMem(field->getType()));
continue;
}
+ // For non-InitListExpr inits (e.g. a compound literal or other
+ // expression that evaluates to the whole field value), fall through
+ // to normal emission below. This matches classic codegen behavior.
}
mlir::Attribute eltInitAttr =
@@ -981,7 +1018,9 @@ static bool emitDesignatedInitUpdater(ConstantEmitter &emitter,
emitter.tryEmitPrivateForMemory(filler, elemType);
if (!result)
return false;
- fillC = mlir::cast<mlir::TypedAttr>(result);
+ fillC = mlir::dyn_cast<mlir::TypedAttr>(result);
+ if (!fillC)
+ return false;
}
}
@@ -1007,7 +1046,10 @@ static bool emitDesignatedInitUpdater(ConstantEmitter &emitter,
mlir::Attribute val = emitter.tryEmitPrivateForMemory(init, elemType);
if (!val)
return false;
- if (!constant.add(mlir::cast<mlir::TypedAttr>(val), offset, true))
+ auto typedVal = mlir::dyn_cast<mlir::TypedAttr>(val);
+ if (!typedVal)
+ return false;
+ if (!constant.add(typedVal, offset, true))
return false;
}
}
@@ -1221,8 +1263,12 @@ class ConstExprEmitter
if (!c)
return {};
+ auto typedC = mlir::dyn_cast<mlir::TypedAttr>(c);
+ if (!typedC)
+ return {};
+
ConstantAggregateBuilder constant(cgm);
- constant.add(mlir::cast<mlir::TypedAttr>(c), CharUnits::Zero(), false);
+ constant.add(typedC, CharUnits::Zero(), false);
if (!emitDesignatedInitUpdater(emitter, constant, CharUnits::Zero(),
destType, e->getUpdater()))
diff --git a/clang/test/CIR/CodeGen/designated-init-update.c b/clang/test/CIR/CodeGen/designated-init-update.c
index c7a495c5d8d0b..30bba78834693 100644
--- a/clang/test/CIR/CodeGen/designated-init-update.c
+++ b/clang/test/CIR/CodeGen/designated-init-update.c
@@ -48,3 +48,14 @@ struct P g4 = { (struct S){1, 2, 3}, 4, .s.b = 9 };
// CIR: cir.global external @g4 = #cir.const_record<{#cir.const_record<{#cir.int<1> : !s32i, #cir.int<9> : !s32i, #cir.int<3> : !s32i}> : !rec_S, #cir.int<4> : !s32i}> : !rec_P
// LLVM: @g4 = global %struct.P { %struct.S { i32 1, i32 9, i32 3 }, i32 4 }
// OGCG: @g4 = global %struct.P { %struct.S { i32 1, i32 9, i32 3 }, i32 4 }
+
+// Array element override via compound literal — exercises the array path
+// of emitDesignatedInitUpdater and buildFrom for cir::ArrayType.
+struct Inner { int arr[4]; };
+struct ArrOuter { struct Inner in; int x; };
+
+struct ArrOuter g5 = { (struct Inner){{10, 20, 30, 40}}, 5, .in.arr[1] = 99 };
+
+// CIR: cir.global external @g5 = #cir.const_record<{#cir.const_record<{#cir.const_array<[#cir.int<10> : !s32i, #cir.int<99> : !s32i, #cir.int<30> : !s32i, #cir.int<40> : !s32i]> : !cir.array<!s32i x 4>}> : !rec_Inner, #cir.int<5> : !s32i}> : !rec_ArrOuter
+// LLVM: @g5 = global %struct.ArrOuter { %struct.Inner { [4 x i32] [i32 10, i32 99, i32 30, i32 40] }, i32 5 }
+// OGCG: @g5 = global %struct.ArrOuter { %struct.Inner { [4 x i32] [i32 10, i32 99, i32 30, i32 40] }, i32 5 }
More information about the cfe-commits
mailing list