[flang-commits] [clang] [flang] [RFC][flang][runtime] Add FortranFloat128Math wrapper library. (PR #81971)
via flang-commits
flang-commits at lists.llvm.org
Thu Feb 15 23:14:09 PST 2024
llvmbot wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-flang-fir-hlfir
Author: Slava Zakharin (vzakhari)
<details>
<summary>Changes</summary>
Implemented few entry points for REAL(16) math in FortranF128Math
static library. It is a thin wrapper around GNU libquadmath.
Flang driver can always link it, and the dependencies will
be brought in as needed.
The final Fortran program/library that uses any of the entry points
will depend on the underlying third-party library - this dependency
has to be resolved somehow. I added FLANG_RUNTIME_F128_MATH_LIB
CMake control so that the compiler driver and the runtime library
can be built using the same third-party library: this way the linker
knows which dependency to link in (under --as-needed).
The compiler distribution should specify which third-party library
is required for linking/running the apps that use REAL(16).
The compiler package may provide a version of the third-party library
or at least a stub library that can be used for linking, but
the final program execution will still require the actual library.
---
Patch is 27.69 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/81971.diff
13 Files Affected:
- (modified) clang/include/clang/Driver/Driver.h (+10)
- (modified) clang/lib/Driver/ToolChains/CommonArgs.cpp (+8)
- (modified) flang/CMakeLists.txt (+17)
- (modified) flang/include/flang/Optimizer/Builder/IntrinsicCall.h (+10-9)
- (modified) flang/lib/Optimizer/Builder/IntrinsicCall.cpp (+71-30)
- (modified) flang/runtime/CMakeLists.txt (+20)
- (added) flang/runtime/Float128Math/CMakeLists.txt (+56)
- (added) flang/runtime/Float128Math/cabs.cpp (+24)
- (added) flang/runtime/Float128Math/math-entries.h (+77)
- (added) flang/runtime/Float128Math/sin.cpp (+22)
- (added) flang/runtime/Float128Math/sqrt.cpp (+22)
- (modified) flang/test/Lower/Intrinsics/missing-math-runtime.f90 (+5-1)
- (modified) flang/tools/flang-driver/driver.cpp (+3)
``````````diff
diff --git a/clang/include/clang/Driver/Driver.h b/clang/include/clang/Driver/Driver.h
index 908bc87c14b1ca..a5ca637853a6ae 100644
--- a/clang/include/clang/Driver/Driver.h
+++ b/clang/include/clang/Driver/Driver.h
@@ -251,6 +251,11 @@ class Driver {
/// from non-system headers are emitted.
HeaderIncludeFilteringKind CCPrintHeadersFiltering = HIFIL_None;
+ /// Name of the library that provides implementations of
+ /// IEEE-754 128-bit float math functions used by Fortran F128
+ /// runtime library. It should be linked as needed by the linker job.
+ std::string FlangF128MathLibrary;
+
/// Set CC_LOG_DIAGNOSTICS mode, which causes the frontend to log diagnostics
/// to CCLogDiagnosticsFilename or to stderr, in a stable machine readable
/// format.
@@ -440,6 +445,11 @@ class Driver {
bool offloadHostOnly() const { return Offload == OffloadHost; }
bool offloadDeviceOnly() const { return Offload == OffloadDevice; }
+ void setFlangF128MathLibrary(std::string name) {
+ FlangF128MathLibrary = std::move(name);
+ }
+ StringRef getFlangF128MathLibrary() const { return FlangF128MathLibrary; }
+
/// Compute the desired OpenMP runtime from the flags provided.
OpenMPRuntimeKind getOpenMPRuntime(const llvm::opt::ArgList &Args) const;
diff --git a/clang/lib/Driver/ToolChains/CommonArgs.cpp b/clang/lib/Driver/ToolChains/CommonArgs.cpp
index 0fd7b8424eb4ba..63d8e2f68f389f 100644
--- a/clang/lib/Driver/ToolChains/CommonArgs.cpp
+++ b/clang/lib/Driver/ToolChains/CommonArgs.cpp
@@ -1285,6 +1285,14 @@ void tools::addFortranRuntimeLibs(const ToolChain &TC, const ArgList &Args,
// add the correct libraries to link against as dependents in the object
// file.
if (!TC.getTriple().isKnownWindowsMSVCEnvironment()) {
+ StringRef f128LibName = TC.getDriver().getFlangF128MathLibrary();
+ f128LibName.consume_front_insensitive("lib");
+ if (!f128LibName.empty()) {
+ CmdArgs.push_back("-lFortranFloat128");
+ addAsNeededOption(TC, Args, CmdArgs, /*as_needed=*/true);
+ CmdArgs.push_back(Args.MakeArgString("-l" + f128LibName));
+ addAsNeededOption(TC, Args, CmdArgs, /*as_needed=*/false);
+ }
CmdArgs.push_back("-lFortranRuntime");
CmdArgs.push_back("-lFortranDecimal");
}
diff --git a/flang/CMakeLists.txt b/flang/CMakeLists.txt
index f8ad39ba712f8c..21617aeea0215e 100644
--- a/flang/CMakeLists.txt
+++ b/flang/CMakeLists.txt
@@ -33,6 +33,17 @@ endif()
option(FLANG_ENABLE_WERROR "Fail and stop building flang if a warning is triggered." OFF)
+# The out of tree builds of the compiler and the Fortran runtime
+# must use the same setting of FLANG_RUNTIME_F128_MATH_LIB
+# to be composable. Failure to synchronize this setting may result
+# in linking errors or fatal failures in F128 runtime functions.
+set(FLANG_RUNTIME_F128_MATH_LIB "" CACHE STRING
+ "Specifies the target library used for implementing IEEE-754 128-bit float \
+ math in F18 runtime, e.g. it might be libquadmath for targets where \
+ REAL(16) is mapped to __float128, or libm for targets where REAL(16) \
+ is mapped to long double, etc."
+ )
+
# Check for a standalone build and configure as appropriate from
# there.
if (CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
@@ -321,6 +332,12 @@ if (FLANG_REPOSITORY_STRING)
add_definitions(-DFLANG_REPOSITORY_STRING="${FLANG_REPOSITORY_STRING}")
endif()
+if (FLANG_RUNTIME_F128_MATH_LIB)
+ add_compile_definitions(
+ -DFLANG_RUNTIME_F128_MATH_LIB="${FLANG_RUNTIME_F128_MATH_LIB}"
+ )
+endif()
+
include(TestBigEndian)
test_big_endian(IS_BIGENDIAN)
if (IS_BIGENDIAN)
diff --git a/flang/include/flang/Optimizer/Builder/IntrinsicCall.h b/flang/include/flang/Optimizer/Builder/IntrinsicCall.h
index 3f1e22ecca4ccc..7cb99d61a686ed 100644
--- a/flang/include/flang/Optimizer/Builder/IntrinsicCall.h
+++ b/flang/include/flang/Optimizer/Builder/IntrinsicCall.h
@@ -494,12 +494,13 @@ struct RuntimeFunction {
fir::runtime::FuncTypeBuilderFunc typeGenerator;
};
-/// Callback type for generating lowering for a math operation.
-using MathGeneratorTy = mlir::Value (*)(fir::FirOpBuilder &, mlir::Location,
- llvm::StringRef, mlir::FunctionType,
- llvm::ArrayRef<mlir::Value>);
-
struct MathOperation {
+ // Callback type for generating lowering for a math operation.
+ using MathGeneratorTy = mlir::Value (*)(fir::FirOpBuilder &, mlir::Location,
+ const MathOperation &,
+ mlir::FunctionType,
+ llvm::ArrayRef<mlir::Value>);
+
// Overrides fir::runtime::FuncTypeBuilderFunc to add FirOpBuilder argument.
using FuncTypeBuilderFunc = mlir::FunctionType (*)(mlir::MLIRContext *,
fir::FirOpBuilder &);
@@ -681,25 +682,25 @@ getTypesForArgs(llvm::ArrayRef<mlir::Value> args) {
}
mlir::Value genLibCall(fir::FirOpBuilder &builder, mlir::Location loc,
- llvm::StringRef libFuncName,
+ const MathOperation &mathOp,
mlir::FunctionType libFuncType,
llvm::ArrayRef<mlir::Value> args);
template <typename T>
mlir::Value genMathOp(fir::FirOpBuilder &builder, mlir::Location loc,
- llvm::StringRef mathLibFuncName,
+ const MathOperation &mathOp,
mlir::FunctionType mathLibFuncType,
llvm::ArrayRef<mlir::Value> args);
template <typename T>
mlir::Value genComplexMathOp(fir::FirOpBuilder &builder, mlir::Location loc,
- llvm::StringRef mathLibFuncName,
+ const MathOperation &mathOp,
mlir::FunctionType mathLibFuncType,
llvm::ArrayRef<mlir::Value> args);
mlir::Value genLibSplitComplexArgsCall(fir::FirOpBuilder &builder,
mlir::Location loc,
- llvm::StringRef libFuncName,
+ const MathOperation &mathOp,
mlir::FunctionType libFuncType,
llvm::ArrayRef<mlir::Value> args);
diff --git a/flang/lib/Optimizer/Builder/IntrinsicCall.cpp b/flang/lib/Optimizer/Builder/IntrinsicCall.cpp
index a3536895ca3b7c..bba53bb57bee51 100644
--- a/flang/lib/Optimizer/Builder/IntrinsicCall.cpp
+++ b/flang/lib/Optimizer/Builder/IntrinsicCall.cpp
@@ -657,10 +657,61 @@ static llvm::cl::opt<bool>
"instead of libm complex operations"),
llvm::cl::init(false));
+/// Return a string containing the given Fortran intrinsic name
+/// with the type of its arguments specified in funcType
+/// surrounded by the given prefix/suffix.
+static std::string
+prettyPrintIntrinsicName(fir::FirOpBuilder &builder, mlir::Location loc,
+ llvm::StringRef prefix, llvm::StringRef name,
+ llvm::StringRef suffix, mlir::FunctionType funcType) {
+ std::string output = prefix.str();
+ llvm::raw_string_ostream sstream(output);
+ if (name == "pow") {
+ assert(funcType.getNumInputs() == 2 && "power operator has two arguments");
+ std::string displayName{" ** "};
+ sstream << numericMlirTypeToFortran(builder, funcType.getInput(0), loc,
+ displayName)
+ << displayName
+ << numericMlirTypeToFortran(builder, funcType.getInput(1), loc,
+ displayName);
+ } else {
+ sstream << name.upper() << "(";
+ if (funcType.getNumInputs() > 0)
+ sstream << numericMlirTypeToFortran(builder, funcType.getInput(0), loc,
+ name);
+ for (mlir::Type argType : funcType.getInputs().drop_front()) {
+ sstream << ", " << numericMlirTypeToFortran(builder, argType, loc, name);
+ }
+ sstream << ")";
+ }
+ sstream << suffix;
+ return output;
+}
+
+// Generate a call to the Fortran runtime library providing
+// support for 128-bit float math via a third-party library.
+// If the compiler is built without FLANG_RUNTIME_F128_MATH_LIB,
+// this function will report an error.
+static mlir::Value genLibF128Call(fir::FirOpBuilder &builder,
+ mlir::Location loc,
+ const MathOperation &mathOp,
+ mlir::FunctionType libFuncType,
+ llvm::ArrayRef<mlir::Value> args) {
+#ifndef FLANG_RUNTIME_F128_MATH_LIB
+ std::string message = prettyPrintIntrinsicName(
+ builder, loc, "compiler is built without support for '", mathOp.key, "'",
+ libFuncType);
+ fir::emitFatalError(loc, message, /*genCrashDiag=*/false);
+#else // FLANG_RUNTIME_F128_MATH_LIB
+ return genLibCall(builder, loc, libFuncName, libFuncType, args);
+#endif // FLANG_RUNTIME_F128_MATH_LIB
+}
+
mlir::Value genLibCall(fir::FirOpBuilder &builder, mlir::Location loc,
- llvm::StringRef libFuncName,
+ const MathOperation &mathOp,
mlir::FunctionType libFuncType,
llvm::ArrayRef<mlir::Value> args) {
+ llvm::StringRef libFuncName = mathOp.runtimeFunc;
LLVM_DEBUG(llvm::dbgs() << "Generating '" << libFuncName
<< "' call with type ";
libFuncType.dump(); llvm::dbgs() << "\n");
@@ -718,7 +769,7 @@ mlir::Value genLibCall(fir::FirOpBuilder &builder, mlir::Location loc,
mlir::Value genLibSplitComplexArgsCall(fir::FirOpBuilder &builder,
mlir::Location loc,
- llvm::StringRef libFuncName,
+ const MathOperation &mathOp,
mlir::FunctionType libFuncType,
llvm::ArrayRef<mlir::Value> args) {
assert(args.size() == 2 && "Incorrect #args to genLibSplitComplexArgsCall");
@@ -762,13 +813,12 @@ mlir::Value genLibSplitComplexArgsCall(fir::FirOpBuilder &builder,
cplx2, /*isImagPart=*/true);
splitArgs.push_back(imag2);
- return genLibCall(builder, loc, libFuncName, getSplitComplexArgsType(),
- splitArgs);
+ return genLibCall(builder, loc, mathOp, getSplitComplexArgsType(), splitArgs);
}
template <typename T>
mlir::Value genMathOp(fir::FirOpBuilder &builder, mlir::Location loc,
- llvm::StringRef mathLibFuncName,
+ const MathOperation &mathOp,
mlir::FunctionType mathLibFuncType,
llvm::ArrayRef<mlir::Value> args) {
// TODO: we have to annotate the math operations with flags
@@ -791,13 +841,14 @@ mlir::Value genMathOp(fir::FirOpBuilder &builder, mlir::Location loc,
// can be also lowered to libm calls for "fast" and "relaxed"
// modes.
mlir::Value result;
+ llvm::StringRef mathLibFuncName = mathOp.runtimeFunc;
if (mathRuntimeVersion == preciseVersion &&
// Some operations do not have to be lowered as conservative
// calls, since they do not affect strict FP behavior.
// For example, purely integer operations like exponentiation
// with integer operands fall into this class.
!mathLibFuncName.empty()) {
- result = genLibCall(builder, loc, mathLibFuncName, mathLibFuncType, args);
+ result = genLibCall(builder, loc, mathOp, mathLibFuncType, args);
} else {
LLVM_DEBUG(llvm::dbgs() << "Generating '" << mathLibFuncName
<< "' operation with type ";
@@ -810,7 +861,7 @@ mlir::Value genMathOp(fir::FirOpBuilder &builder, mlir::Location loc,
template <typename T>
mlir::Value genComplexMathOp(fir::FirOpBuilder &builder, mlir::Location loc,
- llvm::StringRef mathLibFuncName,
+ const MathOperation &mathOp,
mlir::FunctionType mathLibFuncType,
llvm::ArrayRef<mlir::Value> args) {
mlir::Value result;
@@ -819,11 +870,12 @@ mlir::Value genComplexMathOp(fir::FirOpBuilder &builder, mlir::Location loc,
// If we have libm functions, we can attempt to generate the more precise
// version of the complex math operation.
+ llvm::StringRef mathLibFuncName = mathOp.runtimeFunc;
if (!mathLibFuncName.empty()) {
// If we enabled MLIR complex or can use approximate operations, we should
// NOT use libm.
if (!forceMlirComplex && !canUseApprox) {
- result = genLibCall(builder, loc, mathLibFuncName, mathLibFuncType, args);
+ result = genLibCall(builder, loc, mathOp, mathLibFuncType, args);
LLVM_DEBUG(result.dump(); llvm::dbgs() << "\n");
return result;
}
@@ -863,6 +915,10 @@ mlir::Value genComplexMathOp(fir::FirOpBuilder &builder, mlir::Location loc,
/// TODO: support remaining Fortran math intrinsics.
/// See https://gcc.gnu.org/onlinedocs/gcc-12.1.0/gfortran/\
/// Intrinsic-Procedures.html for a reference.
+constexpr auto FuncTypeReal16Real16 = genFuncType<Ty::Real<16>, Ty::Real<16>>;
+constexpr auto FuncTypeReal16Complex16 =
+ genFuncType<Ty::Real<16>, Ty::Complex<16>>;
+
static constexpr MathOperation mathOperations[] = {
{"abs", "fabsf", genFuncType<Ty::Real<4>, Ty::Real<4>>,
genMathOp<mlir::math::AbsFOp>},
@@ -874,6 +930,7 @@ static constexpr MathOperation mathOperations[] = {
genComplexMathOp<mlir::complex::AbsOp>},
{"abs", "cabs", genFuncType<Ty::Real<8>, Ty::Complex<8>>,
genComplexMathOp<mlir::complex::AbsOp>},
+ {"abs", RTNAME_STRING(CAbsF128), FuncTypeReal16Complex16, genLibF128Call},
{"acos", "acosf", genFuncType<Ty::Real<4>, Ty::Real<4>>, genLibCall},
{"acos", "acos", genFuncType<Ty::Real<8>, Ty::Real<8>>, genLibCall},
{"acos", "cacosf", genFuncType<Ty::Complex<4>, Ty::Complex<4>>, genLibCall},
@@ -1110,6 +1167,7 @@ static constexpr MathOperation mathOperations[] = {
genMathOp<mlir::math::SinOp>},
{"sin", "sin", genFuncType<Ty::Real<8>, Ty::Real<8>>,
genMathOp<mlir::math::SinOp>},
+ {"sin", RTNAME_STRING(SinF128), FuncTypeReal16Real16, genLibF128Call},
{"sin", "csinf", genFuncType<Ty::Complex<4>, Ty::Complex<4>>,
genComplexMathOp<mlir::complex::SinOp>},
{"sin", "csin", genFuncType<Ty::Complex<8>, Ty::Complex<8>>,
@@ -1122,6 +1180,7 @@ static constexpr MathOperation mathOperations[] = {
genMathOp<mlir::math::SqrtOp>},
{"sqrt", "sqrt", genFuncType<Ty::Real<8>, Ty::Real<8>>,
genMathOp<mlir::math::SqrtOp>},
+ {"sqrt", RTNAME_STRING(SqrtF128), FuncTypeReal16Real16, genLibF128Call},
{"sqrt", "csqrtf", genFuncType<Ty::Complex<4>, Ty::Complex<4>>,
genComplexMathOp<mlir::complex::SqrtOp>},
{"sqrt", "csqrt", genFuncType<Ty::Complex<8>, Ty::Complex<8>>,
@@ -1345,27 +1404,9 @@ static void checkPrecisionLoss(llvm::StringRef name,
// lowering and could be used here. Emit an error and continue
// generating the code with the narrowing cast so that the user
// can get a complete list of the problematic intrinsic calls.
- std::string message("not yet implemented: no math runtime available for '");
- llvm::raw_string_ostream sstream(message);
- if (name == "pow") {
- assert(funcType.getNumInputs() == 2 && "power operator has two arguments");
- std::string displayName{" ** "};
- sstream << numericMlirTypeToFortran(builder, funcType.getInput(0), loc,
- displayName)
- << displayName
- << numericMlirTypeToFortran(builder, funcType.getInput(1), loc,
- displayName);
- } else {
- sstream << name.upper() << "(";
- if (funcType.getNumInputs() > 0)
- sstream << numericMlirTypeToFortran(builder, funcType.getInput(0), loc,
- name);
- for (mlir::Type argType : funcType.getInputs().drop_front()) {
- sstream << ", " << numericMlirTypeToFortran(builder, argType, loc, name);
- }
- sstream << ")";
- }
- sstream << "'";
+ std::string message = prettyPrintIntrinsicName(
+ builder, loc, "not yet implemented: no math runtime available for '",
+ name, "'", funcType);
mlir::emitError(loc, message);
}
@@ -1887,7 +1928,7 @@ IntrinsicLibrary::getRuntimeCallGenerator(llvm::StringRef name,
for (auto [fst, snd] : llvm::zip(actualFuncType.getInputs(), args))
convertedArguments.push_back(builder.createConvert(loc, fst, snd));
mlir::Value result = mathOp->funcGenerator(
- builder, loc, mathOp->runtimeFunc, actualFuncType, convertedArguments);
+ builder, loc, *mathOp, actualFuncType, convertedArguments);
mlir::Type soughtType = soughtFuncType.getResult(0);
return builder.createConvert(loc, soughtType, result);
};
diff --git a/flang/runtime/CMakeLists.txt b/flang/runtime/CMakeLists.txt
index dfa9da502db0a8..ac89184a7cbffc 100644
--- a/flang/runtime/CMakeLists.txt
+++ b/flang/runtime/CMakeLists.txt
@@ -46,6 +46,23 @@ if (CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
endif ()
include_directories(BEFORE
${FLANG_SOURCE_DIR}/include)
+
+ # The out of tree builds of the compiler and the Fortran runtime
+ # must use the same setting of FLANG_RUNTIME_F128_MATH_LIB
+ # to be composable. Failure to synchronize this setting may result
+ # in linking errors or fatal failures in F128 runtime functions.
+ set(FLANG_RUNTIME_F128_MATH_LIB "" CACHE STRING
+ "Specifies the target library used for implementing IEEE-754 128-bit float \
+ math in F18 runtime, e.g. it might be libquadmath for targets where \
+ REAL(16) is mapped to __float128, or libm for targets where REAL(16) \
+ is mapped to long double, etc."
+ )
+
+ if (NOT FLANG_RUNTIME_F128_MATH_LIB STREQUAL "")
+ add_compile_definitions(
+ -DFLANG_RUNTIME_F128_MATH_LIB="${FLANG_RUNTIME_F128_MATH_LIB}"
+ )
+ endif()
endif()
include(CheckCXXSymbolExists)
@@ -83,6 +100,9 @@ add_definitions(-U_GLIBCXX_ASSERTIONS)
add_definitions(-U_LIBCPP_ENABLE_ASSERTIONS)
add_subdirectory(FortranMain)
+if (NOT ${FLANG_RUNTIME_F128_MATH_LIB} STREQUAL "")
+ add_subdirectory(Float128Math)
+endif()
set(sources
ISO_Fortran_binding.cpp
diff --git a/flang/runtime/Float128Math/CMakeLists.txt b/flang/runtime/Float128Math/CMakeLists.txt
new file mode 100644
index 00000000000000..f8da4d7ca1a9fe
--- /dev/null
+++ b/flang/runtime/Float128Math/CMakeLists.txt
@@ -0,0 +1,56 @@
+#===-- runtime/Float128Math/CMakeLists.txt ---------------------------------===#
+#
+# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+# See https://llvm.org/LICENSE.txt for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+#
+#===------------------------------------------------------------------------===#
+
+# FortranFloat128 implements IEEE-754 128-bit float math functions.
+# It is a thin wapper and it currently relies on third-party
+# libraries available for the target.
+# It is distributed as a static library only.
+# Fortran programs/libraries that end up linking any of the provided
+# will have a dependency on the third-party library that is being
+# used for building this FortranFloat128Math library.
+
+if (${FLANG_RUNTIME_F128_MATH_LIB} STREQUAL "libquadmath" OR
+ ${FLANG_RUNTIME_F128_MATH_LIB} STREQUAL "quadmath")
+ check_include_file(quadmath.h FOUND_QUADMATH_HEADER)
+ if(FOUND_QUADMATH_HEADER)
+ add_compile_definitions(HAS_QUADMATHLIB)
+ else()
+ message(FATAL_ERROR
+ "FLANG_RUNTIME_F128_MATH_LIB setting requires quadmath.h "
+ "to be available: ${FLANG_RUNTIME_F128_MATH_LIB}"
+ )
+ endif()
+else()
+ message(FATAL_ERROR
+ "Unsupported third-party library for Fortran F128 math runtime: "
+ "${FLANG_RUNTIME_F128_MATH_LIB}"
+ )
+endif()
+
+set(sources
+ cabs.cpp
+ sin.cpp
+ sqrt.cpp
+ )
+
+include_directories(AFTER "${CMAKE_CURRENT_SOURCE_DIR}/..")
+add_flang_library(FortranFloat128Math STATIC INSTALL_WITH_TOOLCHAIN ${sources})
+
+if (DEFINED MSVC)
+ set(CMAKE_MSVC_RUNTIME_LIBRARY MultiThreaded)
+ add_flang_library(FortranFloat128Math.static STATIC INSTALL_WITH_TO...
[truncated]
``````````
</details>
https://github.com/llvm/llvm-project/pull/81971
More information about the flang-commits
mailing list