#include "clang/Basic/OperatorKinds.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/Support/Casting.h"
+#include "llvm/Support/ErrorHandling.h"
#include <cassert>
#include <memory>
#include <tuple>
return Env.makeAtomicBoolValue();
}
+// Functionally updates `V` such that any instances of `TopBool` are replaced
+// with fresh atomic bools. Note: This implementation assumes that `B` is a
+// tree; if `B` is a DAG, it will lose any sharing between subvalues that was
+// present in the original .
+static BoolValue &unpackValue(BoolValue &V, Environment &Env);
+
+template <typename Derived, typename M>
+BoolValue &unpackBinaryBoolValue(Environment &Env, BoolValue &B, M build) {
+ auto &V = *cast<Derived>(&B);
+ BoolValue &Left = V.getLeftSubValue();
+ BoolValue &Right = V.getRightSubValue();
+ BoolValue &ULeft = unpackValue(Left, Env);
+ BoolValue &URight = unpackValue(Right, Env);
+
+ if (&ULeft == &Left && &URight == &Right)
+ return V;
+
+ return (Env.*build)(ULeft, URight);
+}
+
+static BoolValue &unpackValue(BoolValue &V, Environment &Env) {
+ switch (V.getKind()) {
+ case Value::Kind::Integer:
+ case Value::Kind::Reference:
+ case Value::Kind::Pointer:
+ case Value::Kind::Struct:
+ llvm_unreachable("BoolValue cannot have any of these kinds.");
+
+ case Value::Kind::AtomicBool:
+ return V;
+
+ case Value::Kind::TopBool:
+ // Unpack `TopBool` into a fresh atomic bool.
+ return Env.makeAtomicBoolValue();
+
+ case Value::Kind::Negation: {
+ auto &N = *cast<NegationValue>(&V);
+ BoolValue &Sub = N.getSubVal();
+ BoolValue &USub = unpackValue(Sub, Env);
+
+ if (&USub == &Sub)
+ return V;
+ return Env.makeNot(USub);
+ }
+ case Value::Kind::Conjunction:
+ return unpackBinaryBoolValue<ConjunctionValue>(Env, V,
+ &Environment::makeAnd);
+ case Value::Kind::Disjunction:
+ return unpackBinaryBoolValue<DisjunctionValue>(Env, V,
+ &Environment::makeOr);
+ case Value::Kind::Implication:
+ return unpackBinaryBoolValue<ImplicationValue>(
+ Env, V, &Environment::makeImplication);
+ case Value::Kind::Biconditional:
+ return unpackBinaryBoolValue<BiconditionalValue>(Env, V,
+ &Environment::makeIff);
+ }
+}
+
+// Unpacks the value (if any) associated with `E` and updates `E` to the new
+// value, if any unpacking occured.
+static Value *maybeUnpackLValueExpr(const Expr &E, Environment &Env) {
+ auto *Loc = Env.getStorageLocation(E, SkipPast::Reference);
+ if (Loc == nullptr)
+ return nullptr;
+ auto *Val = Env.getValue(*Loc);
+
+ auto *B = dyn_cast_or_null<BoolValue>(Val);
+ if (B == nullptr)
+ return Val;
+
+ auto &UnpackedVal = unpackValue(*B, Env);
+ if (&UnpackedVal == Val)
+ return Val;
+ Env.setValue(*Loc, UnpackedVal);
+ return &UnpackedVal;
+}
+
class TransferVisitor : public ConstStmtVisitor<TransferVisitor> {
public:
TransferVisitor(const StmtToEnvMap &StmtToEnv, Environment &Env,
}
case CK_LValueToRValue: {
- auto *SubExprVal = Env.getValue(*SubExpr, SkipPast::Reference);
+ // When an L-value is used as an R-value, it may result in sharing, so we
+ // need to unpack any nested `Top`s.
+ auto *SubExprVal = maybeUnpackLValueExpr(*SubExpr, Env);
if (SubExprVal == nullptr)
break;
#include "clang/Analysis/FlowSensitive/DataflowAnalysisContext.h"
#include "clang/Analysis/FlowSensitive/DataflowEnvironment.h"
#include "clang/Analysis/FlowSensitive/DataflowLattice.h"
+#include "clang/Analysis/FlowSensitive/DebugSupport.h"
#include "clang/Analysis/FlowSensitive/NoopAnalysis.h"
#include "clang/Analysis/FlowSensitive/Value.h"
#include "clang/Analysis/FlowSensitive/WatchedLiteralsSolver.h"
auto CS = Elt->getAs<CFGStmt>();
if (!CS)
return;
- auto S = CS->getStmt();
+ const auto *S = CS->getStmt();
if (auto *C = dyn_cast<CallExpr>(S)) {
if (auto *F = dyn_cast<FunctionDecl>(C->getCalleeDecl())) {
E.CalledFunctions.insert(F->getNameInfo().getAsString());
}
// Models an analysis that uses flow conditions.
+//
+// FIXME: Here and below: change class to final and final methods to override,
+// since we're marking them all as final.
class SpecialBoolAnalysis
: public DataflowAnalysis<SpecialBoolAnalysis, NoopLattice> {
public:
auto CS = Elt->getAs<CFGStmt>();
if (!CS)
return;
- auto S = CS->getStmt();
+ const auto *S = CS->getStmt();
auto SpecialBoolRecordDecl = recordDecl(hasName("SpecialBool"));
auto HasSpecialBoolType = hasType(SpecialBoolRecordDecl);
class OptionalIntAnalysis
: public DataflowAnalysis<OptionalIntAnalysis, NoopLattice> {
public:
- explicit OptionalIntAnalysis(ASTContext &Context, BoolValue &HasValueTop)
- : DataflowAnalysis<OptionalIntAnalysis, NoopLattice>(Context),
- HasValueTop(HasValueTop) {}
+ explicit OptionalIntAnalysis(ASTContext &Context)
+ : DataflowAnalysis<OptionalIntAnalysis, NoopLattice>(Context) {}
static NoopLattice initialElement() { return {}; }
auto CS = Elt->getAs<CFGStmt>();
if (!CS)
return;
- auto S = CS->getStmt();
+ const Stmt *S = CS->getStmt();
auto OptionalIntRecordDecl = recordDecl(hasName("OptionalInt"));
auto HasOptionalIntType = hasType(OptionalIntRecordDecl);
+ SmallVector<BoundNodes, 1> Matches = match(
+ stmt(anyOf(cxxConstructExpr(HasOptionalIntType).bind("construct"),
+ cxxOperatorCallExpr(
+ callee(cxxMethodDecl(ofClass(OptionalIntRecordDecl))))
+ .bind("operator"))),
+ *S, getASTContext());
if (const auto *E = selectFirst<CXXConstructExpr>(
- "call", match(cxxConstructExpr(HasOptionalIntType).bind("call"), *S,
- getASTContext()))) {
+ "construct", Matches)) {
auto &ConstructorVal = *Env.createValue(E->getType());
ConstructorVal.setProperty("has_value", Env.getBoolLiteralValue(false));
Env.setValue(*Env.getStorageLocation(*E, SkipPast::None), ConstructorVal);
- } else if (const auto *E = selectFirst<CXXOperatorCallExpr>(
- "call",
- match(cxxOperatorCallExpr(callee(cxxMethodDecl(ofClass(
- OptionalIntRecordDecl))))
- .bind("call"),
- *S, getASTContext()))) {
+ } else if (const auto *E =
+ selectFirst<CXXOperatorCallExpr>("operator", Matches)) {
assert(E->getNumArgs() > 0);
auto *Object = E->getArg(0);
assert(Object != nullptr);
Type->getAsCXXRecordDecl()->getQualifiedNameAsString() != "OptionalInt")
return false;
- return Val1.getProperty("has_value") == Val2.getProperty("has_value");
+ auto *Prop1 = Val1.getProperty("has_value");
+ auto *Prop2 = Val2.getProperty("has_value");
+ return Prop1 == Prop2 ||
+ (Prop1 != nullptr && Prop2 != nullptr && isa<TopBoolValue>(Prop1) &&
+ isa<TopBoolValue>(Prop2));
}
bool merge(QualType Type, const Value &Val1, const Environment &Env1,
if (HasValue1 == HasValue2)
MergedVal.setProperty("has_value", *HasValue1);
else
- MergedVal.setProperty("has_value", HasValueTop);
+ MergedVal.setProperty("has_value", MergedEnv.makeTopBoolValue());
return true;
}
-
- BoolValue &HasValueTop;
};
class WideningTest : public Test {
checkDataflow<OptionalIntAnalysis>(
AnalysisInputs<OptionalIntAnalysis>(
Code, ast_matchers::hasName("target"),
- [this](ASTContext &Context, Environment &Env) {
- assert(HasValueTop == nullptr);
- HasValueTop =
- &Env.takeOwnership(std::make_unique<AtomicBoolValue>());
- return OptionalIntAnalysis(Context, *HasValueTop);
+ [](ASTContext &Context, Environment &Env) {
+ return OptionalIntAnalysis(Context);
})
.withASTBuildArgs({"-fsyntax-only", "-std=c++17"})
.withASTBuildVirtualMappedFiles(std::move(FilesContents)),
&AO) { Match(Results, AO.ASTCtx); }),
llvm::Succeeded());
}
-
- BoolValue *HasValueTop = nullptr;
};
TEST_F(WideningTest, JoinDistinctValuesWithDistinctProperties) {
)";
runDataflow(
Code,
- [this](const llvm::StringMap<DataflowAnalysisState<NoopLattice>> &Results,
- ASTContext &ASTCtx) {
+ [](const llvm::StringMap<DataflowAnalysisState<NoopLattice>> &Results,
+ ASTContext &ASTCtx) {
ASSERT_THAT(Results.keys(), UnorderedElementsAre("p1", "p2", "p3"));
const Environment &Env1 = getEnvironmentAtAnnotation(Results, "p1");
const Environment &Env2 = getEnvironmentAtAnnotation(Results, "p2");
&Env1.getBoolLiteralValue(false));
EXPECT_EQ(GetFooValue(Env2)->getProperty("has_value"),
&Env2.getBoolLiteralValue(true));
- EXPECT_EQ(GetFooValue(Env3)->getProperty("has_value"), HasValueTop);
+ EXPECT_TRUE(
+ isa<TopBoolValue>(GetFooValue(Env3)->getProperty("has_value")));
});
}
});
}
+class TopAnalysis final : public DataflowAnalysis<TopAnalysis, NoopLattice> {
+public:
+ explicit TopAnalysis(ASTContext &Context)
+ : DataflowAnalysis<TopAnalysis, NoopLattice>(Context) {}
+
+ static NoopLattice initialElement() { return {}; }
+
+ void transfer(const CFGElement *Elt, NoopLattice &, Environment &Env) {
+ auto CS = Elt->getAs<CFGStmt>();
+ if (!CS)
+ return;
+ const Stmt *S = CS->getStmt();
+ SmallVector<BoundNodes, 1> Matches =
+ match(callExpr(callee(functionDecl(hasName("makeTop")))).bind("top"),
+ *S, getASTContext());
+ if (const auto *E = selectFirst<CallExpr>("top", Matches)) {
+ auto &Loc = Env.createStorageLocation(*E);
+ Env.setValue(Loc, Env.makeTopBoolValue());
+ Env.setStorageLocation(*E, Loc);
+ }
+ }
+
+ bool compareEquivalent(QualType Type, const Value &Val1,
+ const Environment &Env1, const Value &Val2,
+ const Environment &Env2) override {
+ // Changes to a sound approximation, which allows us to test whether we can
+ // (soundly) converge for some loops.
+ return false;
+ }
+};
+
+class TopTest : public Test {
+protected:
+ template <typename Matcher>
+ void runDataflow(llvm::StringRef Code, Matcher VerifyResults) {
+ ASSERT_THAT_ERROR(
+ checkDataflow<TopAnalysis>(
+ AnalysisInputs<TopAnalysis>(
+ Code, ast_matchers::hasName("target"),
+ [](ASTContext &Context, Environment &Env) {
+ return TopAnalysis(Context);
+ })
+ .withASTBuildArgs({"-fsyntax-only", "-std=c++17"}),
+ VerifyResults),
+ llvm::Succeeded());
+ }
+};
+
+// Tests that when Top is unused it remains Top.
+TEST_F(TopTest, UnusedTopInitializer) {
+ std::string Code = R"(
+ bool makeTop();
+
+ void target() {
+ bool Foo = makeTop();
+ /*[[p1]]*/
+ (void)0;
+ /*[[p2]]*/
+ }
+ )";
+ runDataflow(
+ Code,
+ [](const llvm::StringMap<DataflowAnalysisState<NoopLattice>> &Results,
+ const AnalysisOutputs &AO) {
+ ASSERT_THAT(Results.keys(),
+ UnorderedElementsAre("p1", "p2"));
+ const Environment &Env1 = getEnvironmentAtAnnotation(Results, "p1");
+ const Environment &Env2 = getEnvironmentAtAnnotation(Results, "p2");
+
+ const ValueDecl *FooDecl = findValueDecl(AO.ASTCtx, "Foo");
+ ASSERT_THAT(FooDecl, NotNull());
+
+
+ auto GetFooValue = [FooDecl](const Environment &Env) {
+ return Env.getValue(*FooDecl, SkipPast::None);
+ };
+
+ Value *FooVal1 = GetFooValue(Env1);
+ ASSERT_THAT(FooVal1, NotNull());
+ EXPECT_TRUE(isa<TopBoolValue>(FooVal1))
+ << debugString(FooVal1->getKind());
+
+ Value *FooVal2 = GetFooValue(Env2);
+ ASSERT_THAT(FooVal2, NotNull());
+ EXPECT_TRUE(isa<TopBoolValue>(FooVal2))
+ << debugString(FooVal2->getKind());
+
+ EXPECT_EQ(FooVal1, FooVal2);
+ });
+}
+
+// Tests that when Top is unused it remains Top. Like above, but uses the
+// assignment form rather than initialization, which uses Top as an lvalue that
+// is *not* in an rvalue position.
+TEST_F(TopTest, UnusedTopAssignment) {
+ std::string Code = R"(
+ bool makeTop();
+
+ void target() {
+ bool Foo;
+ Foo = makeTop();
+ /*[[p1]]*/
+ (void)0;
+ /*[[p2]]*/
+ }
+ )";
+ runDataflow(
+ Code,
+ [](const llvm::StringMap<DataflowAnalysisState<NoopLattice>> &Results,
+ const AnalysisOutputs &AO) {
+ ASSERT_THAT(Results.keys(),
+ UnorderedElementsAre("p1", "p2"));
+ const Environment &Env1 = getEnvironmentAtAnnotation(Results, "p1");
+ const Environment &Env2 = getEnvironmentAtAnnotation(Results, "p2");
+
+ const ValueDecl *FooDecl = findValueDecl(AO.ASTCtx, "Foo");
+ ASSERT_THAT(FooDecl, NotNull());
+
+
+ auto GetFooValue = [FooDecl](const Environment &Env) {
+ return Env.getValue(*FooDecl, SkipPast::None);
+ };
+
+ Value *FooVal1 = GetFooValue(Env1);
+ ASSERT_THAT(FooVal1, NotNull());
+ EXPECT_TRUE(isa<TopBoolValue>(FooVal1))
+ << debugString(FooVal1->getKind());
+
+ Value *FooVal2 = GetFooValue(Env2);
+ ASSERT_THAT(FooVal2, NotNull());
+ EXPECT_TRUE(isa<TopBoolValue>(FooVal2))
+ << debugString(FooVal2->getKind());
+
+ EXPECT_EQ(FooVal1, FooVal2);
+ });
+}
+
+TEST_F(TopTest, UnusedTopJoinsToTop) {
+ std::string Code = R"(
+ bool makeTop();
+
+ void target(bool Cond, bool F) {
+ bool Foo = makeTop();
+ // Force a new CFG block.
+ if (F) return;
+ (void)0;
+ /*[[p1]]*/
+
+ bool Zab1;
+ bool Zab2;
+ if (Cond) {
+ Zab1 = true;
+ } else {
+ Zab2 = true;
+ }
+ (void)0;
+ /*[[p2]]*/
+ }
+ )";
+ runDataflow(
+ Code,
+ [](const llvm::StringMap<DataflowAnalysisState<NoopLattice>> &Results,
+ const AnalysisOutputs &AO) {
+ ASSERT_THAT(Results.keys(), UnorderedElementsAre("p1", "p2"));
+ const Environment &Env1 = getEnvironmentAtAnnotation(Results, "p1");
+ const Environment &Env2 = getEnvironmentAtAnnotation(Results, "p2");
+
+ const ValueDecl *FooDecl = findValueDecl(AO.ASTCtx, "Foo");
+ ASSERT_THAT(FooDecl, NotNull());
+
+ auto GetFooValue = [FooDecl](const Environment &Env) {
+ return Env.getValue(*FooDecl, SkipPast::None);
+ };
+
+ Value *FooVal1 = GetFooValue(Env1);
+ ASSERT_THAT(FooVal1, NotNull());
+ EXPECT_TRUE(isa<TopBoolValue>(FooVal1))
+ << debugString(FooVal1->getKind());
+
+ Value *FooVal2 = GetFooValue(Env2);
+ ASSERT_THAT(FooVal2, NotNull());
+ EXPECT_TRUE(isa<TopBoolValue>(FooVal2))
+ << debugString(FooVal2->getKind());
+ });
+}
+
+TEST_F(TopTest, TopUsedBeforeBranchJoinsToSameAtomicBool) {
+ std::string Code = R"(
+ bool makeTop();
+
+ void target(bool Cond, bool F) {
+ bool Foo = makeTop();
+ /*[[p0]]*/
+
+ // Use `Top`.
+ bool Bar = Foo;
+ // Force a new CFG block.
+ if (F) return;
+ (void)0;
+ /*[[p1]]*/
+
+ bool Zab1;
+ bool Zab2;
+ if (Cond) {
+ Zab1 = true;
+ } else {
+ Zab2 = true;
+ }
+ (void)0;
+ /*[[p2]]*/
+ }
+ )";
+ runDataflow(
+ Code,
+ [](const llvm::StringMap<DataflowAnalysisState<NoopLattice>> &Results,
+ const AnalysisOutputs &AO) {
+ ASSERT_THAT(Results.keys(),
+ UnorderedElementsAre("p0", "p1", "p2"));
+ const Environment &Env0 = getEnvironmentAtAnnotation(Results, "p0");
+ const Environment &Env1 = getEnvironmentAtAnnotation(Results, "p1");
+ const Environment &Env2 = getEnvironmentAtAnnotation(Results, "p2");
+
+ const ValueDecl *FooDecl = findValueDecl(AO.ASTCtx, "Foo");
+ ASSERT_THAT(FooDecl, NotNull());
+
+ auto GetFooValue = [FooDecl](const Environment &Env) {
+ return Env.getValue(*FooDecl, SkipPast::None);
+ };
+
+ Value *FooVal0 = GetFooValue(Env0);
+ ASSERT_THAT(FooVal0, NotNull());
+ EXPECT_TRUE(isa<TopBoolValue>(FooVal0))
+ << debugString(FooVal0->getKind());
+
+ Value *FooVal1 = GetFooValue(Env1);
+ ASSERT_THAT(FooVal1, NotNull());
+ EXPECT_TRUE(isa<AtomicBoolValue>(FooVal1))
+ << debugString(FooVal1->getKind());
+
+ Value *FooVal2 = GetFooValue(Env2);
+ ASSERT_THAT(FooVal2, NotNull());
+ EXPECT_TRUE(isa<AtomicBoolValue>(FooVal2))
+ << debugString(FooVal2->getKind());
+
+ EXPECT_EQ(FooVal2, FooVal1);
+ });
+}
+
+TEST_F(TopTest, TopUsedInBothBranchesJoinsToAtomic) {
+ std::string Code = R"(
+ bool makeTop();
+
+ void target(bool Cond, bool F) {
+ bool Foo = makeTop();
+ // Force a new CFG block.
+ if (F) return;
+ (void)0;
+ /*[[p1]]*/
+
+ bool Zab1;
+ bool Zab2;
+ if (Cond) {
+ Zab1 = Foo;
+ } else {
+ Zab2 = Foo;
+ }
+ (void)0;
+ /*[[p2]]*/
+ }
+ )";
+ runDataflow(
+ Code,
+ [](const llvm::StringMap<DataflowAnalysisState<NoopLattice>> &Results,
+ const AnalysisOutputs &AO) {
+ ASSERT_THAT(Results.keys(), UnorderedElementsAre("p1", "p2"));
+ const Environment &Env1 = getEnvironmentAtAnnotation(Results, "p1");
+ const Environment &Env2 = getEnvironmentAtAnnotation(Results, "p2");
+
+ const ValueDecl *FooDecl = findValueDecl(AO.ASTCtx, "Foo");
+ ASSERT_THAT(FooDecl, NotNull());
+
+ auto GetFooValue = [FooDecl](const Environment &Env) {
+ return Env.getValue(*FooDecl, SkipPast::None);
+ };
+
+ Value *FooVal1 = GetFooValue(Env1);
+ ASSERT_THAT(FooVal1, NotNull());
+ EXPECT_TRUE(isa<TopBoolValue>(FooVal1))
+ << debugString(FooVal1->getKind());
+
+ Value *FooVal2 = GetFooValue(Env2);
+ ASSERT_THAT(FooVal2, NotNull());
+ EXPECT_TRUE(isa<AtomicBoolValue>(FooVal2))
+ << debugString(FooVal2->getKind());
+ });
+}
+
+TEST_F(TopTest, TopUsedInBothBranchesWithoutPrecisionLoss) {
+ std::string Code = R"(
+ bool makeTop();
+
+ void target(bool Cond, bool F) {
+ bool Foo = makeTop();
+ // Force a new CFG block.
+ if (F) return;
+ (void)0;
+
+ bool Bar;
+ if (Cond) {
+ Bar = Foo;
+ } else {
+ Bar = Foo;
+ }
+ (void)0;
+ /*[[p]]*/
+ }
+ )";
+ runDataflow(
+ Code,
+ [](const llvm::StringMap<DataflowAnalysisState<NoopLattice>> &Results,
+ const AnalysisOutputs &AO) {
+ ASSERT_THAT(Results.keys(), UnorderedElementsAre("p"));
+ const Environment &Env = getEnvironmentAtAnnotation(Results, "p");
+
+ const ValueDecl *FooDecl = findValueDecl(AO.ASTCtx, "Foo");
+ ASSERT_THAT(FooDecl, NotNull());
+
+ const ValueDecl *BarDecl = findValueDecl(AO.ASTCtx, "Bar");
+ ASSERT_THAT(BarDecl, NotNull());
+
+ auto *FooVal =
+ dyn_cast_or_null<BoolValue>(Env.getValue(*FooDecl, SkipPast::None));
+ ASSERT_THAT(FooVal, NotNull());
+
+ auto *BarVal =
+ dyn_cast_or_null<BoolValue>(Env.getValue(*BarDecl, SkipPast::None));
+ ASSERT_THAT(BarVal, NotNull());
+
+ EXPECT_TRUE(Env.flowConditionImplies(Env.makeIff(*FooVal, *BarVal)));
+ });
+}
+
+TEST_F(TopTest, TopUnusedBeforeLoopHeadJoinsToTop) {
+ std::string Code = R"(
+ bool makeTop();
+
+ void target(bool Cond, bool F) {
+ bool Foo = makeTop();
+ // Force a new CFG block.
+ if (F) return;
+ (void)0;
+ /*[[p1]]*/
+
+ while (Cond) {
+ // Use `Foo`.
+ bool Zab = Foo;
+ Zab = false;
+ Foo = makeTop();
+ }
+ (void)0;
+ /*[[p2]]*/
+ }
+ )";
+ runDataflow(
+ Code,
+ [](const llvm::StringMap<DataflowAnalysisState<NoopLattice>> &Results,
+ const AnalysisOutputs &AO) {
+ ASSERT_THAT(Results.keys(), UnorderedElementsAre("p1", "p2"));
+ const Environment &Env1 = getEnvironmentAtAnnotation(Results, "p1");
+ const Environment &Env2 = getEnvironmentAtAnnotation(Results, "p2");
+
+ const ValueDecl *FooDecl = findValueDecl(AO.ASTCtx, "Foo");
+ ASSERT_THAT(FooDecl, NotNull());
+
+ auto GetFooValue = [FooDecl](const Environment &Env) {
+ return Env.getValue(*FooDecl, SkipPast::None);
+ };
+
+ Value *FooVal1 = GetFooValue(Env1);
+ ASSERT_THAT(FooVal1, NotNull());
+ EXPECT_TRUE(isa<TopBoolValue>(FooVal1))
+ << debugString(FooVal1->getKind());
+
+ Value *FooVal2 = GetFooValue(Env2);
+ ASSERT_THAT(FooVal2, NotNull());
+ EXPECT_TRUE(isa<TopBoolValue>(FooVal2))
+ << debugString(FooVal2->getKind());
+
+ });
+}
} // namespace