[clang] [Clang] Coroutines: Properly Check if `await_suspend` return type convertible to `std::coroutine_handle<>` (PR #85684)
Yuxuan Chen via cfe-commits
cfe-commits at lists.llvm.org
Mon Mar 18 12:40:16 PDT 2024
https://github.com/yuxuanchen1997 created https://github.com/llvm/llvm-project/pull/85684
This patch aims to fix https://github.com/llvm/llvm-project/issues/77111
The original issue is crash-on-invalid. However, Clang currently accepts programs where `await_suspend` returns a type that's convertible to `std::coroutine_handle<>`. This is more relaxed than the standard. Under this assumption, we mishandled the case where the return type has a user defined destructor. The crash happens before we can check the return value of `await_suspend`.
This path handles the case where the expression is wrapped in a `CXXBindTemporaryExpr` and refactored a little bit how the diagnosis logic to make it more explicit as suggested by a previous `FIXME` in `mayTailCall`.
I am also ok with changing the patch to reject programs whose `await_suspend` doesn't strictly return `std::coroutine_handle` if upstream prefers.
>From 08de54f02038795924a6e5fdbcf51a496fcedf56 Mon Sep 17 00:00:00 2001
From: Yuxuan Chen <ych at meta.com>
Date: Mon, 18 Mar 2024 10:45:20 -0700
Subject: [PATCH] Check if Coroutine await_suspend type returns the right type
---
.../clang/Basic/DiagnosticSemaKinds.td | 2 +-
clang/include/clang/Sema/Sema.h | 2 +
clang/lib/Sema/SemaCoroutine.cpp | 75 +++++++++++------
clang/lib/Sema/SemaExprCXX.cpp | 84 +++++++++----------
clang/test/SemaCXX/coroutines.cpp | 28 +++++--
5 files changed, 116 insertions(+), 75 deletions(-)
diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td
index 8e97902564af08..f99170445c76b6 100644
--- a/clang/include/clang/Basic/DiagnosticSemaKinds.td
+++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td
@@ -11701,7 +11701,7 @@ def err_coroutine_promise_new_requires_nothrow : Error<
def note_coroutine_promise_call_implicitly_required : Note<
"call to %0 implicitly required by coroutine function here">;
def err_await_suspend_invalid_return_type : Error<
- "return type of 'await_suspend' is required to be 'void' or 'bool' (have %0)"
+ "return type of 'await_suspend' is required to be 'void' or 'bool' or convertible to 'std::coroutine_handle<>' (have %0)"
>;
def note_await_ready_no_bool_conversion : Note<
"return type of 'await_ready' is required to be contextually convertible to 'bool'"
diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h
index 95ea5ebc7f1ac1..4976ff96b03d5b 100644
--- a/clang/include/clang/Sema/Sema.h
+++ b/clang/include/clang/Sema/Sema.h
@@ -7011,6 +7011,8 @@ class Sema final {
ExprResult BuildTypeTrait(TypeTrait Kind, SourceLocation KWLoc,
ArrayRef<TypeSourceInfo *> Args,
SourceLocation RParenLoc);
+ bool EvaluateBinaryTypeTrait(TypeTrait BTT, QualType LhsT, QualType RhsT,
+ SourceLocation KeyLoc);
/// ActOnArrayTypeTrait - Parsed one of the binary type trait support
/// pseudo-functions.
diff --git a/clang/lib/Sema/SemaCoroutine.cpp b/clang/lib/Sema/SemaCoroutine.cpp
index 736632857efc36..fbe230737404fa 100644
--- a/clang/lib/Sema/SemaCoroutine.cpp
+++ b/clang/lib/Sema/SemaCoroutine.cpp
@@ -331,16 +331,12 @@ static ExprResult buildMemberCall(Sema &S, Expr *Base, SourceLocation Loc,
// coroutine.
static Expr *maybeTailCall(Sema &S, QualType RetType, Expr *E,
SourceLocation Loc) {
- if (RetType->isReferenceType())
- return nullptr;
+ assert(!RetType->isReferenceType() &&
+ "Should have diagnosed reference types.");
Type const *T = RetType.getTypePtr();
if (!T->isClassType() && !T->isStructureType())
return nullptr;
- // FIXME: Add convertability check to coroutine_handle<>. Possibly via
- // EvaluateBinaryTypeTrait(BTT_IsConvertible, ...) which is at the moment
- // a private function in SemaExprCXX.cpp
-
ExprResult AddressExpr = buildMemberCall(S, E, Loc, "address", std::nullopt);
if (AddressExpr.isInvalid())
return nullptr;
@@ -358,6 +354,14 @@ static Expr *maybeTailCall(Sema &S, QualType RetType, Expr *E,
return S.MaybeCreateExprWithCleanups(JustAddress);
}
+static bool isConvertibleToCoroutineHandle(Sema &S, QualType Ty,
+ SourceLocation Loc) {
+ QualType ErasedHandleType =
+ lookupCoroutineHandleType(S, S.Context.VoidTy, Loc);
+ return S.EvaluateBinaryTypeTrait(BTT_IsConvertible, Ty, ErasedHandleType,
+ Loc);
+}
+
/// Build calls to await_ready, await_suspend, and await_resume for a co_await
/// expression.
/// The generated AST tries to clean up temporary objects as early as
@@ -418,39 +422,60 @@ static ReadySuspendResumeResult buildCoawaitCalls(Sema &S, VarDecl *CoroPromise,
return Calls;
}
Expr *CoroHandle = CoroHandleRes.get();
- CallExpr *AwaitSuspend = cast_or_null<CallExpr>(
- BuildSubExpr(ACT::ACT_Suspend, "await_suspend", CoroHandle));
+ auto *AwaitSuspend = [&]() -> CallExpr * {
+ auto *SubExpr = BuildSubExpr(ACT::ACT_Suspend, "await_suspend", CoroHandle);
+ if (!SubExpr)
+ return nullptr;
+ if (auto *E = dyn_cast<CXXBindTemporaryExpr>(SubExpr)) {
+ // This happens when await_suspend return type is not trivially
+ // destructible. This doesn't happen for the permitted return types of
+ // such function. Diagnose it later.
+ return cast_or_null<CallExpr>(E->getSubExpr());
+ } else {
+ return cast_or_null<CallExpr>(SubExpr);
+ }
+ }();
+
if (!AwaitSuspend)
return Calls;
+
if (!AwaitSuspend->getType()->isDependentType()) {
+ auto InvalidAwaitSuspendReturnType = [&](QualType RetType) {
+ // non-class prvalues always have cv-unqualified types
+ S.Diag(AwaitSuspend->getCalleeDecl()->getLocation(),
+ diag::err_await_suspend_invalid_return_type)
+ << RetType;
+ S.Diag(Loc, diag::note_coroutine_promise_call_implicitly_required)
+ << AwaitSuspend->getDirectCallee();
+ Calls.IsInvalid = true;
+ };
+
// [expr.await]p3 [...]
// - await-suspend is the expression e.await_suspend(h), which shall be
// a prvalue of type void, bool, or std::coroutine_handle<Z> for some
// type Z.
QualType RetType = AwaitSuspend->getCallReturnType(S.Context);
- // Support for coroutine_handle returning await_suspend.
- if (Expr *TailCallSuspend =
- maybeTailCall(S, RetType, AwaitSuspend, Loc))
+ if (RetType->isReferenceType()) {
+ InvalidAwaitSuspendReturnType(RetType);
+ } else if (RetType->isBooleanType() || RetType->isVoidType()) {
+ Calls.Results[ACT::ACT_Suspend] =
+ S.MaybeCreateExprWithCleanups(AwaitSuspend);
+ } else if (isConvertibleToCoroutineHandle(S, RetType, Loc)) {
+ // Support for coroutine_handle returning await_suspend.
+ //
// Note that we don't wrap the expression with ExprWithCleanups here
// because that might interfere with tailcall contract (e.g. inserting
// clean up instructions in-between tailcall and return). Instead
// ExprWithCleanups is wrapped within maybeTailCall() prior to the resume
// call.
- Calls.Results[ACT::ACT_Suspend] = TailCallSuspend;
- else {
- // non-class prvalues always have cv-unqualified types
- if (RetType->isReferenceType() ||
- (!RetType->isBooleanType() && !RetType->isVoidType())) {
- S.Diag(AwaitSuspend->getCalleeDecl()->getLocation(),
- diag::err_await_suspend_invalid_return_type)
- << RetType;
- S.Diag(Loc, diag::note_coroutine_promise_call_implicitly_required)
- << AwaitSuspend->getDirectCallee();
- Calls.IsInvalid = true;
- } else
- Calls.Results[ACT::ACT_Suspend] =
- S.MaybeCreateExprWithCleanups(AwaitSuspend);
+ Expr *TailCallSuspend = maybeTailCall(S, RetType, AwaitSuspend, Loc);
+ if (TailCallSuspend)
+ Calls.Results[ACT::ACT_Suspend] = TailCallSuspend;
+ else
+ InvalidAwaitSuspendReturnType(RetType);
+ } else {
+ InvalidAwaitSuspendReturnType(RetType);
}
}
diff --git a/clang/lib/Sema/SemaExprCXX.cpp b/clang/lib/Sema/SemaExprCXX.cpp
index c34a40fa7c81ac..db04e59a91332d 100644
--- a/clang/lib/Sema/SemaExprCXX.cpp
+++ b/clang/lib/Sema/SemaExprCXX.cpp
@@ -5559,9 +5559,6 @@ static bool EvaluateUnaryTypeTrait(Sema &Self, TypeTrait UTT,
}
}
-static bool EvaluateBinaryTypeTrait(Sema &Self, TypeTrait BTT, QualType LhsT,
- QualType RhsT, SourceLocation KeyLoc);
-
static bool EvaluateBooleanTypeTrait(Sema &S, TypeTrait Kind,
SourceLocation KWLoc,
ArrayRef<TypeSourceInfo *> Args,
@@ -5576,8 +5573,8 @@ static bool EvaluateBooleanTypeTrait(Sema &S, TypeTrait Kind,
// Evaluate ReferenceBindsToTemporary and ReferenceConstructsFromTemporary
// alongside the IsConstructible traits to avoid duplication.
if (Kind <= BTT_Last && Kind != BTT_ReferenceBindsToTemporary && Kind != BTT_ReferenceConstructsFromTemporary)
- return EvaluateBinaryTypeTrait(S, Kind, Args[0]->getType(),
- Args[1]->getType(), RParenLoc);
+ return S.EvaluateBinaryTypeTrait(Kind, Args[0]->getType(),
+ Args[1]->getType(), RParenLoc);
switch (Kind) {
case clang::BTT_ReferenceBindsToTemporary:
@@ -5674,7 +5671,8 @@ static bool EvaluateBooleanTypeTrait(Sema &S, TypeTrait Kind,
QualType TPtr = S.Context.getPointerType(S.BuiltinRemoveReference(T, UnaryTransformType::RemoveCVRef, {}));
QualType UPtr = S.Context.getPointerType(S.BuiltinRemoveReference(U, UnaryTransformType::RemoveCVRef, {}));
- return EvaluateBinaryTypeTrait(S, TypeTrait::BTT_IsConvertibleTo, UPtr, TPtr, RParenLoc);
+ return S.EvaluateBinaryTypeTrait(TypeTrait::BTT_IsConvertibleTo, UPtr,
+ TPtr, RParenLoc);
}
if (Kind == clang::TT_IsNothrowConstructible)
@@ -5807,8 +5805,8 @@ ExprResult Sema::ActOnTypeTrait(TypeTrait Kind, SourceLocation KWLoc,
return BuildTypeTrait(Kind, KWLoc, ConvertedArgs, RParenLoc);
}
-static bool EvaluateBinaryTypeTrait(Sema &Self, TypeTrait BTT, QualType LhsT,
- QualType RhsT, SourceLocation KeyLoc) {
+bool Sema::EvaluateBinaryTypeTrait(TypeTrait BTT, QualType LhsT, QualType RhsT,
+ SourceLocation KeyLoc) {
assert(!LhsT->isDependentType() && !RhsT->isDependentType() &&
"Cannot evaluate traits of dependent types");
@@ -5832,15 +5830,15 @@ static bool EvaluateBinaryTypeTrait(Sema &Self, TypeTrait BTT, QualType LhsT,
if (!BaseInterface || !DerivedInterface)
return false;
- if (Self.RequireCompleteType(
+ if (RequireCompleteType(
KeyLoc, RhsT, diag::err_incomplete_type_used_in_type_trait_expr))
return false;
return BaseInterface->isSuperClassOf(DerivedInterface);
}
- assert(Self.Context.hasSameUnqualifiedType(LhsT, RhsT)
- == (lhsRecord == rhsRecord));
+ assert(Context.hasSameUnqualifiedType(LhsT, RhsT) ==
+ (lhsRecord == rhsRecord));
// Unions are never base classes, and never have base classes.
// It doesn't matter if they are complete or not. See PR#41843
@@ -5856,21 +5854,21 @@ static bool EvaluateBinaryTypeTrait(Sema &Self, TypeTrait BTT, QualType LhsT,
// If Base and Derived are class types and are different types
// (ignoring possible cv-qualifiers) then Derived shall be a
// complete type.
- if (Self.RequireCompleteType(KeyLoc, RhsT,
- diag::err_incomplete_type_used_in_type_trait_expr))
+ if (RequireCompleteType(KeyLoc, RhsT,
+ diag::err_incomplete_type_used_in_type_trait_expr))
return false;
return cast<CXXRecordDecl>(rhsRecord->getDecl())
->isDerivedFrom(cast<CXXRecordDecl>(lhsRecord->getDecl()));
}
case BTT_IsSame:
- return Self.Context.hasSameType(LhsT, RhsT);
+ return Context.hasSameType(LhsT, RhsT);
case BTT_TypeCompatible: {
// GCC ignores cv-qualifiers on arrays for this builtin.
Qualifiers LhsQuals, RhsQuals;
- QualType Lhs = Self.getASTContext().getUnqualifiedArrayType(LhsT, LhsQuals);
- QualType Rhs = Self.getASTContext().getUnqualifiedArrayType(RhsT, RhsQuals);
- return Self.Context.typesAreCompatible(Lhs, Rhs);
+ QualType Lhs = getASTContext().getUnqualifiedArrayType(LhsT, LhsQuals);
+ QualType Rhs = getASTContext().getUnqualifiedArrayType(RhsT, RhsQuals);
+ return Context.typesAreCompatible(Lhs, Rhs);
}
case BTT_IsConvertible:
case BTT_IsConvertibleTo:
@@ -5909,16 +5907,16 @@ static bool EvaluateBinaryTypeTrait(Sema &Self, TypeTrait BTT, QualType LhsT,
return LhsT->isVoidType();
// A function definition requires a complete, non-abstract return type.
- if (!Self.isCompleteType(KeyLoc, RhsT) || Self.isAbstractType(KeyLoc, RhsT))
+ if (!isCompleteType(KeyLoc, RhsT) || isAbstractType(KeyLoc, RhsT))
return false;
// Compute the result of add_rvalue_reference.
if (LhsT->isObjectType() || LhsT->isFunctionType())
- LhsT = Self.Context.getRValueReferenceType(LhsT);
+ LhsT = Context.getRValueReferenceType(LhsT);
// Build a fake source and destination for initialization.
InitializedEntity To(InitializedEntity::InitializeTemporary(RhsT));
- OpaqueValueExpr From(KeyLoc, LhsT.getNonLValueExprType(Self.Context),
+ OpaqueValueExpr From(KeyLoc, LhsT.getNonLValueExprType(Context),
Expr::getValueKindForType(LhsT));
Expr *FromPtr = &From;
InitializationKind Kind(InitializationKind::CreateCopy(KeyLoc,
@@ -5927,21 +5925,21 @@ static bool EvaluateBinaryTypeTrait(Sema &Self, TypeTrait BTT, QualType LhsT,
// Perform the initialization in an unevaluated context within a SFINAE
// trap at translation unit scope.
EnterExpressionEvaluationContext Unevaluated(
- Self, Sema::ExpressionEvaluationContext::Unevaluated);
- Sema::SFINAETrap SFINAE(Self, /*AccessCheckingSFINAE=*/true);
- Sema::ContextRAII TUContext(Self, Self.Context.getTranslationUnitDecl());
- InitializationSequence Init(Self, To, Kind, FromPtr);
+ *this, Sema::ExpressionEvaluationContext::Unevaluated);
+ Sema::SFINAETrap SFINAE(*this, /*AccessCheckingSFINAE=*/true);
+ Sema::ContextRAII TUContext(*this, Context.getTranslationUnitDecl());
+ InitializationSequence Init(*this, To, Kind, FromPtr);
if (Init.Failed())
return false;
- ExprResult Result = Init.Perform(Self, To, Kind, FromPtr);
+ ExprResult Result = Init.Perform(*this, To, Kind, FromPtr);
if (Result.isInvalid() || SFINAE.hasErrorOccurred())
return false;
if (BTT != BTT_IsNothrowConvertible)
return true;
- return Self.canThrow(Result.get()) == CT_Cannot;
+ return canThrow(Result.get()) == CT_Cannot;
}
case BTT_IsAssignable:
@@ -5959,12 +5957,12 @@ static bool EvaluateBinaryTypeTrait(Sema &Self, TypeTrait BTT, QualType LhsT,
// For both, T and U shall be complete types, (possibly cv-qualified)
// void, or arrays of unknown bound.
if (!LhsT->isVoidType() && !LhsT->isIncompleteArrayType() &&
- Self.RequireCompleteType(KeyLoc, LhsT,
- diag::err_incomplete_type_used_in_type_trait_expr))
+ RequireCompleteType(KeyLoc, LhsT,
+ diag::err_incomplete_type_used_in_type_trait_expr))
return false;
if (!RhsT->isVoidType() && !RhsT->isIncompleteArrayType() &&
- Self.RequireCompleteType(KeyLoc, RhsT,
- diag::err_incomplete_type_used_in_type_trait_expr))
+ RequireCompleteType(KeyLoc, RhsT,
+ diag::err_incomplete_type_used_in_type_trait_expr))
return false;
// cv void is never assignable.
@@ -5974,27 +5972,27 @@ static bool EvaluateBinaryTypeTrait(Sema &Self, TypeTrait BTT, QualType LhsT,
// Build expressions that emulate the effect of declval<T>() and
// declval<U>().
if (LhsT->isObjectType() || LhsT->isFunctionType())
- LhsT = Self.Context.getRValueReferenceType(LhsT);
+ LhsT = Context.getRValueReferenceType(LhsT);
if (RhsT->isObjectType() || RhsT->isFunctionType())
- RhsT = Self.Context.getRValueReferenceType(RhsT);
- OpaqueValueExpr Lhs(KeyLoc, LhsT.getNonLValueExprType(Self.Context),
+ RhsT = Context.getRValueReferenceType(RhsT);
+ OpaqueValueExpr Lhs(KeyLoc, LhsT.getNonLValueExprType(Context),
Expr::getValueKindForType(LhsT));
- OpaqueValueExpr Rhs(KeyLoc, RhsT.getNonLValueExprType(Self.Context),
+ OpaqueValueExpr Rhs(KeyLoc, RhsT.getNonLValueExprType(Context),
Expr::getValueKindForType(RhsT));
// Attempt the assignment in an unevaluated context within a SFINAE
// trap at translation unit scope.
EnterExpressionEvaluationContext Unevaluated(
- Self, Sema::ExpressionEvaluationContext::Unevaluated);
- Sema::SFINAETrap SFINAE(Self, /*AccessCheckingSFINAE=*/true);
- Sema::ContextRAII TUContext(Self, Self.Context.getTranslationUnitDecl());
- ExprResult Result = Self.BuildBinOp(/*S=*/nullptr, KeyLoc, BO_Assign, &Lhs,
- &Rhs);
+ *this, Sema::ExpressionEvaluationContext::Unevaluated);
+ Sema::SFINAETrap SFINAE(*this, /*AccessCheckingSFINAE=*/true);
+ Sema::ContextRAII TUContext(*this, Context.getTranslationUnitDecl());
+ ExprResult Result =
+ BuildBinOp(/*S=*/nullptr, KeyLoc, BO_Assign, &Lhs, &Rhs);
if (Result.isInvalid())
return false;
// Treat the assignment as unused for the purpose of -Wdeprecated-volatile.
- Self.CheckUnusedVolatileAssignment(Result.get());
+ CheckUnusedVolatileAssignment(Result.get());
if (SFINAE.hasErrorOccurred())
return false;
@@ -6003,7 +6001,7 @@ static bool EvaluateBinaryTypeTrait(Sema &Self, TypeTrait BTT, QualType LhsT,
return true;
if (BTT == BTT_IsNothrowAssignable)
- return Self.canThrow(Result.get()) == CT_Cannot;
+ return canThrow(Result.get()) == CT_Cannot;
if (BTT == BTT_IsTriviallyAssignable) {
// Under Objective-C ARC and Weak, if the destination has non-trivial
@@ -6011,14 +6009,14 @@ static bool EvaluateBinaryTypeTrait(Sema &Self, TypeTrait BTT, QualType LhsT,
if (LhsT.getNonReferenceType().hasNonTrivialObjCLifetime())
return false;
- return !Result.get()->hasNonTrivialCall(Self.Context);
+ return !Result.get()->hasNonTrivialCall(Context);
}
llvm_unreachable("unhandled type trait");
return false;
}
case BTT_IsLayoutCompatible: {
- return Self.IsLayoutCompatible(LhsT, RhsT);
+ return IsLayoutCompatible(LhsT, RhsT);
}
default: llvm_unreachable("not a BTT");
}
diff --git a/clang/test/SemaCXX/coroutines.cpp b/clang/test/SemaCXX/coroutines.cpp
index 2292932583fff6..cdd9be4c201d3f 100644
--- a/clang/test/SemaCXX/coroutines.cpp
+++ b/clang/test/SemaCXX/coroutines.cpp
@@ -1005,12 +1005,24 @@ coro<promise_no_return_func> no_return_value_or_return_void_3() {
co_return 43; // expected-error {{no member named 'return_value'}}
}
-struct bad_await_suspend_return {
+struct non_trivial_destruction_type {
+ ~non_trivial_destruction_type();
+};
+
+struct bad_await_suspend_return_1 {
bool await_ready();
- // expected-error at +1 {{return type of 'await_suspend' is required to be 'void' or 'bool' (have 'char')}}
+ // expected-error at +1 {{return type of 'await_suspend' is required to be 'void' or 'bool' or convertible to 'std::coroutine_handle<>' (have 'char')}}
char await_suspend(std::coroutine_handle<>);
void await_resume();
};
+
+struct bad_await_suspend_return_2 {
+ bool await_ready();
+ // expected-error at +1 {{return type of 'await_suspend' is required to be 'void' or 'bool' or convertible to 'std::coroutine_handle<>' (have 'non_trivial_destruction_type')}}
+ non_trivial_destruction_type await_suspend(std::coroutine_handle<>);
+ void await_resume();
+};
+
struct bad_await_ready_return {
// expected-note at +1 {{return type of 'await_ready' is required to be contextually convertible to 'bool'}}
void await_ready();
@@ -1028,8 +1040,8 @@ struct await_ready_explicit_bool {
template <class SuspendTy>
struct await_suspend_type_test {
bool await_ready();
- // expected-error at +2 {{return type of 'await_suspend' is required to be 'void' or 'bool' (have 'bool &')}}
- // expected-error at +1 {{return type of 'await_suspend' is required to be 'void' or 'bool' (have 'bool &&')}}
+ // expected-error at +2 {{return type of 'await_suspend' is required to be 'void' or 'bool' or convertible to 'std::coroutine_handle<>' (have 'bool &')}}
+ // expected-error at +1 {{return type of 'await_suspend' is required to be 'void' or 'bool' or convertible to 'std::coroutine_handle<>' (have 'bool &&')}}
SuspendTy await_suspend(std::coroutine_handle<>);
// cxx20_23-warning at -1 {{volatile-qualified return type 'const volatile bool' is deprecated}}
void await_resume();
@@ -1042,8 +1054,12 @@ void test_bad_suspend() {
co_await a; // expected-note {{call to 'await_ready' implicitly required by coroutine function here}}
}
{
- bad_await_suspend_return b;
- co_await b; // expected-note {{call to 'await_suspend' implicitly required by coroutine function here}}
+ bad_await_suspend_return_1 b1;
+ co_await b1; // expected-note {{call to 'await_suspend' implicitly required by coroutine function here}}
+ }
+ {
+ bad_await_suspend_return_2 b2;
+ co_await b2; // expected-note {{call to 'await_suspend' implicitly required by coroutine function here}}
}
{
await_ready_explicit_bool c;
More information about the cfe-commits
mailing list