llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT--> @llvm/pr-subscribers-clangir Author: TenHian <details> <summary>Changes</summary> Because ABI calling convention lowering is deferred in CIR, we do not use sret parameters for functions that return an aggregate. Instead, CIR emits a store through the `__retval` alloca and a reload from it. For types with a deleted copy constructor, this store+load constitutes an illegal bitwise copy. Approach -------- Detect whether the return type's copy constructor is deleted (recursing into members and bases). When it is, skip the `__retval` alloca entirely and forward the call result as an SSA value directly to `cir.return`. Key changes: - `emitAndUpdateRetAlloca`: skip `__retval` for non-copyable types - `emitReturnStmt`: for non-copyable types, emit the call with `ReturnValueSlot::forNoAggregateStore()` and return the SSA value - `emitReturnOfRValue`: when `returnValue` is absent, forward the RValue directly without going through a memory slot - `ReturnValueSlot`: add `noAggregateStore` flag to prevent `emitCall` from storing to memory when the result will be returned directly - `CIRGenVTables.cpp`: route aggregate returns through `emitReturnOfRValue` regardless of slot validity - NRVO guard: skip NRVO allocation when `__retval` is absent Impact ------ | Aspect | Description | | ------------------------- | ----------- | | Core fix | Non-copyable types no longer go through `__retval` store/load | | Fallthrough | Explicit trap (O0) / unreachable (O1+) when no `__retval` exists | | Virtual dispatch | Previously crashed, now correctly returns SSA value | | Copyable types | Unchanged — still use `__retval` store/load | | Ternary / branches | Still has redundant store/load through `agg.tmp` (not `__retval`; does not affect ABI lowering) | | Lambda invoker / thunk | Redundant store/load through a function-local temp when `returnValue` is absent (pre-existing from PR #<!-- -->197572; can be addressed in a follow-up) | Fixes https://github.com/llvm/llvm-project/issues/198602 --- Patch is 23.78 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/199205.diff 7 Files Affected: - (modified) clang/lib/CIR/CodeGen/CIRGenCall.cpp (+5) - (modified) clang/lib/CIR/CodeGen/CIRGenCall.h (+8) - (modified) clang/lib/CIR/CodeGen/CIRGenDecl.cpp (+1-1) - (modified) clang/lib/CIR/CodeGen/CIRGenFunction.cpp (+49-5) - (modified) clang/lib/CIR/CodeGen/CIRGenStmt.cpp (+70-33) - (modified) clang/lib/CIR/CodeGen/CIRGenVTables.cpp (+7-8) - (added) clang/test/CIR/CodeGenCXX/uncopyable-return.cpp (+327) ``````````diff diff --git a/clang/lib/CIR/CodeGen/CIRGenCall.cpp b/clang/lib/CIR/CodeGen/CIRGenCall.cpp index f648eff375a77..c4f0c6ae49e57 100644 --- a/clang/lib/CIR/CodeGen/CIRGenCall.cpp +++ b/clang/lib/CIR/CodeGen/CIRGenCall.cpp @@ -1346,6 +1346,11 @@ RValue CIRGenFunction::emitCall(const CIRGenFunctionInfo &funcInfo, return getUndefRValue(retTy); switch (getEvaluationKind(retTy)) { case cir::TEK_Aggregate: { + if (returnValue.isNoAggregateStore()) { + mlir::ResultRange results = theCall->getOpResults(); + assert(results.size() <= 1 && "multiple returns from a call"); + return RValue::get(results[0]); + } Address destPtr = returnValue.getValue(); if (!destPtr.isValid()) diff --git a/clang/lib/CIR/CodeGen/CIRGenCall.h b/clang/lib/CIR/CodeGen/CIRGenCall.h index b30b4969ca45e..904310b89dfb9 100644 --- a/clang/lib/CIR/CodeGen/CIRGenCall.h +++ b/clang/lib/CIR/CodeGen/CIRGenCall.h @@ -259,12 +259,20 @@ class CallArgList : public llvm::SmallVector<CallArg, 8> { /// whether the address is volatile or not. class ReturnValueSlot { Address addr = Address::invalid(); + bool noAggregateStore = false; public: ReturnValueSlot() = default; ReturnValueSlot(Address addr) : addr(addr) {} + static ReturnValueSlot forNoAggregateStore() { + ReturnValueSlot slot; + slot.noAggregateStore = true; + return slot; + } + bool isNull() const { return !addr.isValid(); } + bool isNoAggregateStore() const { return noAggregateStore; } Address getValue() const { return addr; } }; diff --git a/clang/lib/CIR/CodeGen/CIRGenDecl.cpp b/clang/lib/CIR/CodeGen/CIRGenDecl.cpp index 99f3aea03aae3..8f62df38f54cc 100644 --- a/clang/lib/CIR/CodeGen/CIRGenDecl.cpp +++ b/clang/lib/CIR/CodeGen/CIRGenDecl.cpp @@ -94,7 +94,7 @@ CIRGenFunction::emitAutoVarAlloca(const VarDecl &d, // unless: // - it's an NRVO variable. // - we are compiling OpenMP and it's an OpenMP local variable. - if (nrvo) { + if (nrvo && returnValue.isValid()) { // The named return value optimization: allocate this variable in the // return slot, so that we can elide the copy when returning this // variable (C++0x [class.copy]p34). diff --git a/clang/lib/CIR/CodeGen/CIRGenFunction.cpp b/clang/lib/CIR/CodeGen/CIRGenFunction.cpp index 52e7a9d3de412..fde1d5164ec82 100644 --- a/clang/lib/CIR/CodeGen/CIRGenFunction.cpp +++ b/clang/lib/CIR/CodeGen/CIRGenFunction.cpp @@ -22,6 +22,7 @@ #include "clang/CIR/Dialect/IR/CIRDialect.h" #include "clang/CIR/MissingFeatures.h" #include "llvm/ADT/ScopeExit.h" +#include "llvm/ADT/SmallPtrSet.h" #include "llvm/IR/FPEnv.h" #include <cassert> @@ -215,10 +216,43 @@ bool CIRGenFunction::constantFoldsToSimpleInteger(const Expr *cond, resultInt = intValue; return true; } +static bool hasDeletedCopyConstructor( + const clang::CXXRecordDecl *rd, + llvm::SmallPtrSet<const clang::CXXRecordDecl *, 8> &visited) { + if (!visited.insert(rd).second) + return false; + // Check user-declared copy ctors. + if (rd->hasUserDeclaredCopyConstructor()) { + for (const auto *ctor : rd->ctors()) + if (ctor->isCopyConstructor() && ctor->isDeleted()) + return true; + } else if (!rd->needsOverloadResolutionForCopyConstructor() && + rd->defaultedCopyConstructorIsDeleted()) { + return true; + } + // Check fields and bases recursively. + for (const auto *field : rd->fields()) + if (const auto *fieldRD = field->getType()->getAsCXXRecordDecl()) + if (hasDeletedCopyConstructor(fieldRD, visited)) + return true; + for (const auto &base : rd->bases()) + if (const auto *baseRD = base.getType()->getAsCXXRecordDecl()) + if (hasDeletedCopyConstructor(baseRD, visited)) + return true; + return false; +} void CIRGenFunction::emitAndUpdateRetAlloca(QualType type, mlir::Location loc, CharUnits alignment) { if (!type->isVoidType()) { + // Types with non-trivial or deleted copy constructors cannot be + // bitwise-copied into __retval. The return value must flow as an + // SSA value directly. + if (const auto *rd = type->getAsCXXRecordDecl()) { + llvm::SmallPtrSet<const clang::CXXRecordDecl *, 8> visited; + if (hasDeletedCopyConstructor(rd, visited)) + return; + } Address allocaAddr = Address::invalid(); returnValue = createMemTemp(type, alignment, loc, "__retval", &allocaAddr); fnRetAlloca = allocaAddr.getPointer(); @@ -336,11 +370,21 @@ cir::ReturnOp CIRGenFunction::LexicalScope::emitReturn(mlir::Location loc) { assert(fn && "emitReturn from non-function"); if (!fn.getFunctionType().hasVoidReturn()) { - // Load the value from `__retval` and return it via the `cir.return` op. - auto value = cir::LoadOp::create( - builder, loc, fn.getFunctionType().getReturnType(), *cgf.fnRetAlloca); - return cir::ReturnOp::create(builder, loc, - llvm::ArrayRef(value.getResult())); + if (cgf.fnRetAlloca) { + // Load the value from `__retval` and return it via the `cir.return` op. + auto value = cir::LoadOp::create( + builder, loc, fn.getFunctionType().getReturnType(), *cgf.fnRetAlloca); + return cir::ReturnOp::create(builder, loc, + llvm::ArrayRef(value.getResult())); + } + // Non-trivially-copyable return type with no __retval: + // falling off the end is UB; emit unreachable/trap. + if (cgf.cgm.getCodeGenOpts().OptimizationLevel == 0) + cir::TrapOp::create(builder, loc); + else + cir::UnreachableOp::create(builder, loc); + builder.clearInsertionPoint(); + return cir::ReturnOp(); } return cir::ReturnOp::create(builder, loc); } diff --git a/clang/lib/CIR/CodeGen/CIRGenStmt.cpp b/clang/lib/CIR/CodeGen/CIRGenStmt.cpp index 4777d8e429e34..e98ff2dfe2572 100644 --- a/clang/lib/CIR/CodeGen/CIRGenStmt.cpp +++ b/clang/lib/CIR/CodeGen/CIRGenStmt.cpp @@ -598,6 +598,8 @@ mlir::LogicalResult CIRGenFunction::emitReturnStmt(const ReturnStmt &s) { rv = ewc->getSubExpr(); createNewScope = true; } + mlir::Value directReturnValue = nullptr; + Address returnTemp = Address::invalid(); auto handleReturnVal = [&]() { if (getContext().getLangOpts().ElideConstructors && s.getNRVOCandidate() && @@ -642,11 +644,33 @@ mlir::LogicalResult CIRGenFunction::emitReturnStmt(const ReturnStmt &s) { /*isInit=*/true); break; case cir::TEK_Aggregate: - assert(!cir::MissingFeatures::aggValueSlotGC()); - emitAggExpr(rv, AggValueSlot::forAddr(returnValue, Qualifiers(), - AggValueSlot::IsDestructed, - AggValueSlot::IsNotAliased, - getOverlapForReturnValue())); + if (fnRetAlloca) { + assert(!cir::MissingFeatures::aggValueSlotGC()); + emitAggExpr(rv, AggValueSlot::forAddr(returnValue, Qualifiers(), + AggValueSlot::IsDestructed, + AggValueSlot::IsNotAliased, + getOverlapForReturnValue())); + } else { + // Non-trivially-copyable aggregate: emit result directly + // without bitwise-copying through __retval. + // CXXBindTemporaryExpr wraps the call when the type has a + // non-trivial destructor. Unwrap it to find the inner CallExpr. + const Expr *unwrapped = rv->IgnoreParens(); + if (auto *bte = dyn_cast<CXXBindTemporaryExpr>(unwrapped)) + unwrapped = bte->getSubExpr(); + if (const auto *ce = dyn_cast<CallExpr>(unwrapped)) { + RValue callRV = + emitCallExpr(const_cast<CallExpr *>(ce), + ReturnValueSlot::forNoAggregateStore()); + directReturnValue = callRV.getValue(); + } else { + returnTemp = createMemTemp(rv->getType(), loc, "agg.tmp"); + emitAggExpr(rv, AggValueSlot::forAddr(returnTemp, Qualifiers(), + AggValueSlot::IsDestructed, + AggValueSlot::IsNotAliased, + getOverlapForReturnValue())); + } + } break; } } @@ -666,15 +690,23 @@ mlir::LogicalResult CIRGenFunction::emitReturnStmt(const ReturnStmt &s) { // during the CFG flattening phase, we can just emit the return statement // directly. // TODO(cir): Eliminate this redundant load and the store above when we can. - if (fnRetAlloca) { + if (directReturnValue) { + cir::ReturnOp::create(builder, loc, {directReturnValue}); + } else if (returnTemp.isValid()) { + // Load from the temp alloca and return. The load happens after + // forceCleanup to avoid SSA dominance issues with scope blocks. + auto loaded = builder.createLoad(loc, returnTemp); + cir::ReturnOp::create(builder, loc, {loaded}); + } else if (fnRetAlloca) { + // Load the value from `__retval` and return it via the `cir.return` op. cir::AllocaOp retAlloca = mlir::cast<cir::AllocaOp>(fnRetAlloca->getDefiningOp()); auto value = cir::LoadOp::create(builder, loc, retAlloca.getAllocaType(), *fnRetAlloca); - cir::ReturnOp::create(builder, loc, {value}); } else { + cir::ReturnOp::create(builder, loc); } @@ -1251,33 +1283,38 @@ mlir::LogicalResult CIRGenFunction::emitSwitchStmt(const clang::SwitchStmt &s) { void CIRGenFunction::emitReturnOfRValue(mlir::Location loc, RValue rv, QualType ty) { - if (rv.isScalar()) { - builder.createStore(loc, rv.getValue(), returnValue); - } else if (rv.isAggregate()) { - Address rvAddr = rv.getAggregateAddress(); - // If the aggregate is already in the return slot (e.g. a callee was - // invoked through a ReturnValueSlot bound to returnValue), the copy is - // a no-op. Calling emitAggregateCopy here would also incorrectly - // require the type to have a trivial copy/move. - if (rvAddr.getPointer() != returnValue.getPointer()) { - LValue dest = makeAddrLValue(returnValue, ty); - LValue src = makeAddrLValue(rvAddr, ty); - emitAggregateCopy(dest, src, ty, getOverlapForReturnValue()); + if (returnValue.isValid()) { + if (rv.isScalar()) { + builder.createStore(loc, rv.getValue(), returnValue); + } else if (rv.isAggregate()) { + Address rvAddr = rv.getAggregateAddress(); + if (rvAddr.getPointer() != returnValue.getPointer()) { + LValue dest = makeAddrLValue(returnValue, ty); + LValue src = makeAddrLValue(rvAddr, ty); + emitAggregateCopy(dest, src, ty, getOverlapForReturnValue()); + } + } else { + cgm.errorNYI(loc, "emitReturnOfRValue: complex return type"); } - } else { - cgm.errorNYI(loc, "emitReturnOfRValue: complex return type"); } - // Classic codegen emits a branch through any cleanups before continuing to - // a shared return block. Because CIR handles branching through cleanups - // during the CFG flattening phase, we can just emit the return statement - // directly. - // TODO(cir): Eliminate this redundant load and the store above when we can. - // Load the value from `__retval` and return it via the `cir.return` op. - cir::AllocaOp retAlloca = - mlir::cast<cir::AllocaOp>(fnRetAlloca->getDefiningOp()); - auto value = cir::LoadOp::create(builder, loc, retAlloca.getAllocaType(), - *fnRetAlloca); - - cir::ReturnOp::create(builder, loc, {value}); + if (fnRetAlloca) { + // Load the value from `__retval` and return it via the `cir.return` op. + cir::AllocaOp retAlloca = + mlir::cast<cir::AllocaOp>(fnRetAlloca->getDefiningOp()); + auto value = cir::LoadOp::create(builder, loc, retAlloca.getAllocaType(), + *fnRetAlloca); + cir::ReturnOp::create(builder, loc, {value}); + } else { + // Non-trivially-copyable return type with no __retval: + // return the RValue directly. + if (rv.isScalar() && rv.getValue()) { + cir::ReturnOp::create(builder, loc, {rv.getValue()}); + } else if (rv.isAggregate() && rv.getAggregateAddress().isValid()) { + auto loaded = builder.createLoad(loc, rv.getAggregateAddress()); + cir::ReturnOp::create(builder, loc, {loaded}); + } else { + cir::ReturnOp::create(builder, loc); + } + } } diff --git a/clang/lib/CIR/CodeGen/CIRGenVTables.cpp b/clang/lib/CIR/CodeGen/CIRGenVTables.cpp index 03d777cf7363a..4b81c337f7af8 100644 --- a/clang/lib/CIR/CodeGen/CIRGenVTables.cpp +++ b/clang/lib/CIR/CodeGen/CIRGenVTables.cpp @@ -752,19 +752,18 @@ void CIRGenFunction::emitCallAndReturnForThunk(cir::FuncOp callee, else assert(!cir::MissingFeatures::opCallMustTail()); - // Emit return. For aggregate returns the call has already written the - // result through the slot bound to returnValue above; emit the - // corresponding load+return here rather than leaving the function to - // fall off the end and have LexicalScope::emitImplicitReturn drop a - // `cir.trap` / `cir.unreachable` in its place (which would silently - // discard the result we just stored). + // Route the return through emitReturnOfRValue rather than leaving the + // function to fall off the end, where LexicalScope::emitImplicitReturn + // would drop a `cir.trap` / `cir.unreachable` and silently discard the + // just-computed result. When the return type has a deleted copy ctor, + // returnValue is absent and emitCall materializes a temporary for the + // call result; emitReturnOfRValue handles both cases. if (!resultType->isVoidType()) { - if (slot.isNull()) + if (slot.isNull() && !hasAggregateEvaluationKind(resultType)) cgm.getCXXABI().emitReturnFromThunk(*this, rv, resultType); else emitReturnOfRValue(loc, rv, resultType); } - // Disable final ARC autorelease. assert(!cir::MissingFeatures::objCLifetime()); diff --git a/clang/test/CIR/CodeGenCXX/uncopyable-return.cpp b/clang/test/CIR/CodeGenCXX/uncopyable-return.cpp new file mode 100644 index 0000000000000..77ec6230e8da5 --- /dev/null +++ b/clang/test/CIR/CodeGenCXX/uncopyable-return.cpp @@ -0,0 +1,327 @@ +// RUN: %clang_cc1 -std=c++17 -triple x86_64-unknown-unknown -fclangir -emit-cir %s -o %t.cir +// RUN: FileCheck --check-prefix=CIR --input-file=%t.cir %s +// RUN: %clang_cc1 -std=c++17 -triple x86_64-unknown-unknown -fclangir -emit-llvm %s -o %t-cir.ll +// RUN: FileCheck --check-prefix=LLVM --input-file=%t-cir.ll %s +// RUN: %clang_cc1 -std=c++17 -triple x86_64-unknown-unknown -emit-llvm %s -o %t.ll +// RUN: FileCheck --check-prefix=OGCG --input-file=%t.ll %s +// +//===----------------------------------------------------------------------===// +// Tests that CIRGen does not emit illegal copies for return values of +// non-trivially-copyable struct types. +// +// Because ABI calling convention lowering is deferred in CIR, we do not use +// sret parameters for functions that return a non-copyable struct. This is +// acceptable as long as CIRGen does not introduce a store/load round-trip +// through __retval, which would constitute an illegal copy for types whose +// copy constructor is deleted. +// +// The expected CIR pattern for such functions is a direct forwarding of the +// call result to cir.return, without any alloca/store/load indirection. +// +// Uses -std=c++17 for guaranteed copy elision (P0135), so that returning a +// prvalue of a non-copyable type is well-formed without requiring an +// accessible copy or move constructor. +//===----------------------------------------------------------------------===// + +// --- Test 1: Forwarding return of a struct with deleted copy+move ctor --- +// This is the core bug from issue #198602. C++17 guaranteed copy elision +// ensures return foo() is well-formed. CIR must not introduce a store/load +// round-trip through __retval. +namespace deleted_copy_ctor { +struct S { + S(); + S(const S &) = delete; + S(S &&) = delete; + ~S(); +}; + +S foo(); +S bar() { return foo(); } + +// CIR-LABEL: cir.func {{.*}} @_ZN17deleted_copy_ctor3barEv +// CIR-NOT: __retval +// CIR: %{{[0-9]+}} = cir.call @_ZN17deleted_copy_ctor3fooEv() : () -> !rec{{.*}} +// CIR-NEXT: cir.return %{{[0-9]+}} : !rec{{.*}} + +// LLVM-LABEL: define {{.*}} @_ZN17deleted_copy_ctor3barEv( + +// OGCG-LABEL: define {{.*}} void @_ZN17deleted_copy_ctor3barEv( +// OGCG: call void @_ZN17deleted_copy_ctor3fooEv(ptr +// OGCG-NEXT: ret void +} + +// --- Test 2: Forwarding return with only copy deleted, move available --- +// When only the copy constructor is deleted but a move constructor exists, +// the return should still not go through __retval. +namespace copy_deleted { +struct S { + S(); + S(const S &) = delete; + S(S &&); + ~S(); +}; + +S foo(); +S bar() { return foo(); } + +// CIR-LABEL: cir.func {{.*}} @_ZN12copy_deleted3barEv +// CIR-NOT: __retval +// CIR: %{{[0-9]+}} = cir.call @_ZN12copy_deleted3fooEv() : () -> !rec{{.*}} +// CIR-NEXT: cir.return %{{[0-9]+}} : !rec{{.*}} + +// LLVM-LABEL: define {{.*}} @_ZN12copy_deleted3barEv( + +// OGCG-LABEL: define {{.*}} void @_ZN12copy_deleted3barEv( +// OGCG: call void @_ZN12copy_deleted3fooEv(ptr +// OGCG-NEXT: ret void +} + +// --- Test 3: Forwarding return with copy deleted by a member --- +namespace deleted_by_member { +struct B { + B(); + B(const B &) = delete; + B(B &&) = delete; + ~B(); +}; +struct A { + A(); + B b; + ~A(); +}; + +A foo(); +A bar() { return foo(); } + +// CIR-LABEL: cir.func {{.*}} @_ZN17deleted_by_member3barEv +// CIR-NOT: __retval +// CIR: %{{[0-9]+}} = cir.call @_ZN17deleted_by_member3fooEv() : () -> !rec{{.*}} +// CIR-NEXT: cir.return %{{[0-9]+}} : !rec{{.*}} + +// LLVM-LABEL: define {{.*}} @_ZN17deleted_by_member3barEv( + +// OGCG-LABEL: define {{.*}} void @_ZN17deleted_by_member3barEv( +// OGCG: call void @_ZN17deleted_by_member3fooEv(ptr +// OGCG-NEXT: ret void +} + +// --- Test 4: Forwarding return with copy deleted by a base class --- +namespace deleted_by_base { +struct B { + B(); + B(const B &) = delete; + B(B &&) = delete; + ~B(); +}; +struct A : B { + A(); + ~A(); +}; + +A foo(); +A bar() { return foo(); } + +// CIR-LABEL: cir.func {{.*}} @_ZN15deleted_by_base3barEv +// CIR-NOT: __retval +// CIR: %{{[0-9]+}} = cir.call @_ZN15deleted_by_base3fooEv() : () -> !rec{{.*}} +// CIR-NEXT: cir.return %{{[0-9]+}} : !rec{{.*}} + +// LLVM-LABEL: define {{.*}} @_ZN15deleted_by_base3barEv( + +// OGCG-LABEL: define {{.*}} void @_ZN15deleted_by_base3barEv( +// OGCG: call void @_ZN15deleted_by_base3fooEv(ptr +// OGCG-NEXT: ret void +} + +// --- Test 5: Struct with reference member (trivial copy ctor, not deleted) --- +// A class with a reference member has a valid trivial copy constructor +// (reference binding, not copying). The __retval store/load is acceptable. +namespace implicitly_deleted { +struct S { + S(); + int &ref; + ~S(); +}; +S foo(); +S bar() { return foo(); } +// CIR-LABEL: cir.func {{.*}} @_ZN18implicitly_deleted3barEv +// CIR: cir.alloca !rec{{.*}}, !cir.ptr<!rec{{.*}}, ["__retval"] +// CIR: cir.return +// LLVM-LABEL: define {{.*}} @_ZN18implicitly_deleted3barEv( +// OGCG-LABEL: define {{.*}} void @_ZN18implicitly_deleted3barEv( +// OGCG: call void @_ZN18implicitly_deleted3fooEv(ptr +// OGCG-NEXT: ret void +} + +// --- Test 6: Trivially-copyable struct (control group) --- +// For trivially-copyable types, the __retval store/load pattern is still +// acceptable since no copy constructor semantics are violated. This test +// verifies the fix does not regress existing behavior for trivial types. +namespace trivially_copyable { +struct S { + int x; + double y; +}; + +S make(); +S use() { return make(); } + +// CIR-LABEL: cir.func {{.*}} @_ZN18trivially_copyable3useEv +// CIR: cir.alloca !rec{{.*}}, !cir.ptr<!rec{{.*}}, ["__retval"] +// CIR: cir.store +// CIR: cir.load +// CIR: cir.return + +// LLVM-LABEL: define {{.*}} @_ZN18trivially_copyable3useEv( + +// OGCG-LABEL: define {{.*}} @_ZN18tr... [truncated] `````````` </details> https://github.com/llvm/llvm-project/pull/199205 _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
