]> granicus.if.org Git - clang/commitdiff
[LibTooling] Add Stencil library for format-string style codegen.
authorYitzhak Mandelbaum <yitzhakm@google.com>
Thu, 18 Apr 2019 17:23:01 +0000 (17:23 +0000)
committerYitzhak Mandelbaum <yitzhakm@google.com>
Thu, 18 Apr 2019 17:23:01 +0000 (17:23 +0000)
Summary:
This file defines the *Stencil* abstraction: a code-generating object, parameterized by named references to (bound) AST nodes.  Given a match result, a stencil can be evaluated to a string of source code.

A stencil is similar in spirit to a format string: it is composed of a series of raw text strings, references to nodes (the parameters) and helper code-generation operations.

See thread on cfe-dev list with subject "[RFC] Easier source-to-source transformations with clang tooling" for background.

Reviewers: sbenza

Reviewed By: sbenza

Subscribers: ilya-biryukov, mgorny, jfb, jdoerfert, cfe-commits

Tags: #clang

Differential Revision: https://reviews.llvm.org/D59371

git-svn-id: https://llvm.org/svn/llvm-project/cfe/trunk@358691 91177308-0d34-0410-b5e6-96231b3b80d8

include/clang/Tooling/Refactoring/Stencil.h [new file with mode: 0644]
lib/Tooling/Refactoring/CMakeLists.txt
lib/Tooling/Refactoring/Stencil.cpp [new file with mode: 0644]
unittests/Tooling/CMakeLists.txt
unittests/Tooling/StencilTest.cpp [new file with mode: 0644]

diff --git a/include/clang/Tooling/Refactoring/Stencil.h b/include/clang/Tooling/Refactoring/Stencil.h
new file mode 100644 (file)
index 0000000..2620f74
--- /dev/null
@@ -0,0 +1,161 @@
+//===--- Stencil.h - Stencil class ------------------------------*- C++ -*-===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+///
+/// /file
+/// This file defines the *Stencil* abstraction: a code-generating object,
+/// parameterized by named references to (bound) AST nodes.  Given a match
+/// result, a stencil can be evaluated to a string of source code.
+///
+/// A stencil is similar in spirit to a format string: it is composed of a
+/// series of raw text strings, references to nodes (the parameters) and helper
+/// code-generation operations.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_CLANG_TOOLING_REFACTOR_STENCIL_H_
+#define LLVM_CLANG_TOOLING_REFACTOR_STENCIL_H_
+
+#include "clang/AST/ASTContext.h"
+#include "clang/AST/ASTTypeTraits.h"
+#include "clang/ASTMatchers/ASTMatchFinder.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/Support/Error.h"
+#include <string>
+#include <vector>
+
+namespace clang {
+namespace tooling {
+
+/// A stencil is represented as a sequence of "parts" that can each individually
+/// generate a code string based on a match result.  The different kinds of
+/// parts include (raw) text, references to bound nodes and assorted operations
+/// on bound nodes.
+///
+/// Users can create custom Stencil operations by implementing this interface.
+class StencilPartInterface {
+public:
+  virtual ~StencilPartInterface() = default;
+
+  /// Evaluates this part to a string and appends it to \c Result.  \c Result is
+  /// undefined in the case of an error.
+  virtual llvm::Error eval(const ast_matchers::MatchFinder::MatchResult &Match,
+                           std::string *Result) const = 0;
+
+  virtual bool isEqual(const StencilPartInterface &other) const = 0;
+
+  const void *typeId() const { return TypeId; }
+
+protected:
+  StencilPartInterface(const void *DerivedId) : TypeId(DerivedId) {}
+
+  // Since this is an abstract class, copying/assigning only make sense for
+  // derived classes implementing `clone()`.
+  StencilPartInterface(const StencilPartInterface &) = default;
+  StencilPartInterface &operator=(const StencilPartInterface &) = default;
+
+  /// Unique identifier of the concrete type of this instance.  Supports safe
+  /// downcasting.
+  const void *TypeId;
+};
+
+/// A copyable facade for a std::unique_ptr<StencilPartInterface>. Copies result
+/// in a copy of the underlying pointee object.
+class StencilPart {
+public:
+  explicit StencilPart(std::shared_ptr<StencilPartInterface> Impl)
+      : Impl(std::move(Impl)) {}
+
+  /// See `StencilPartInterface::eval()`.
+  llvm::Error eval(const ast_matchers::MatchFinder::MatchResult &Match,
+                   std::string *Result) const {
+    return Impl->eval(Match, Result);
+  }
+
+  bool operator==(const StencilPart &Other) const {
+    if (Impl == Other.Impl)
+      return true;
+    if (Impl == nullptr || Other.Impl == nullptr)
+      return false;
+    return Impl->isEqual(*Other.Impl);
+  }
+
+private:
+  std::shared_ptr<StencilPartInterface> Impl;
+};
+
+/// A sequence of code fragments, references to parameters and code-generation
+/// operations that together can be evaluated to (a fragment of) source code,
+/// given a match result.
+class Stencil {
+public:
+  Stencil() = default;
+
+  /// Composes a stencil from a series of parts.
+  template <typename... Ts> static Stencil cat(Ts &&... Parts) {
+    Stencil S;
+    S.Parts = {wrap(std::forward<Ts>(Parts))...};
+    return S;
+  }
+
+  /// Appends data from a \p OtherStencil to this stencil.
+  void append(Stencil OtherStencil);
+
+  // Evaluates the stencil given a match result. Requires that the nodes in the
+  // result includes any ids referenced in the stencil. References to missing
+  // nodes will result in an invalid_argument error.
+  llvm::Expected<std::string>
+  eval(const ast_matchers::MatchFinder::MatchResult &Match) const;
+
+  // Allow Stencils to operate as std::function, for compatibility with
+  // Transformer's TextGenerator.
+  llvm::Expected<std::string>
+  operator()(const ast_matchers::MatchFinder::MatchResult &Result) const {
+    return eval(Result);
+  }
+
+private:
+  friend bool operator==(const Stencil &A, const Stencil &B);
+  static StencilPart wrap(llvm::StringRef Text);
+  static StencilPart wrap(StencilPart Part) { return Part; }
+
+  std::vector<StencilPart> Parts;
+};
+
+inline bool operator==(const Stencil &A, const Stencil &B) {
+  return A.Parts == B.Parts;
+}
+
+inline bool operator!=(const Stencil &A, const Stencil &B) { return !(A == B); }
+
+// Functions for conveniently building stencils.
+namespace stencil {
+/// Convenience wrapper for Stencil::cat that can be imported with a using decl.
+template <typename... Ts> Stencil cat(Ts &&... Parts) {
+  return Stencil::cat(std::forward<Ts>(Parts)...);
+}
+
+/// \returns exactly the text provided.
+StencilPart text(llvm::StringRef Text);
+
+/// \returns the source corresponding to the identified node.
+StencilPart node(llvm::StringRef Id);
+/// Variant of \c node() that identifies the node as a statement, for purposes
+/// of deciding whether to include any trailing semicolon.  Only relevant for
+/// Expr nodes, which, by default, are *not* considered as statements.
+/// \returns the source corresponding to the identified node, considered as a
+/// statement.
+StencilPart sNode(llvm::StringRef Id);
+
+/// For debug use only; semantics are not guaranteed.
+///
+/// \returns the string resulting from calling the node's print() method.
+StencilPart dPrint(llvm::StringRef Id);
+} // namespace stencil
+} // namespace tooling
+} // namespace clang
+#endif // LLVM_CLANG_TOOLING_REFACTOR_STENCIL_H_
index e98cb0fab5eb67a15fec41af58ad14bfdd044948..37e40e634e5c8c75c54f614c65af464f9c068521 100644 (file)
@@ -13,6 +13,7 @@ add_clang_library(clangToolingRefactor
   Rename/USRFindingAction.cpp
   Rename/USRLocFinder.cpp
   SourceCode.cpp
+  Stencil.cpp
   Transformer.cpp
 
   LINK_LIBS
diff --git a/lib/Tooling/Refactoring/Stencil.cpp b/lib/Tooling/Refactoring/Stencil.cpp
new file mode 100644 (file)
index 0000000..adc26ca
--- /dev/null
@@ -0,0 +1,199 @@
+//===--- Stencil.cpp - Stencil implementation -------------------*- C++ -*-===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+#include "clang/Tooling/Refactoring/Stencil.h"
+#include "clang/AST/ASTContext.h"
+#include "clang/AST/ASTTypeTraits.h"
+#include "clang/AST/Expr.h"
+#include "clang/ASTMatchers/ASTMatchFinder.h"
+#include "clang/ASTMatchers/ASTMatchers.h"
+#include "clang/Lex/Lexer.h"
+#include "clang/Tooling/Refactoring/SourceCode.h"
+#include "llvm/Support/Errc.h"
+#include <atomic>
+#include <string>
+
+using namespace clang;
+using namespace tooling;
+
+using ast_matchers::MatchFinder;
+using llvm::Error;
+
+// A down_cast function to safely down cast a StencilPartInterface to a subclass
+// D. Returns nullptr if P is not an instance of D.
+template <typename D> const D *down_cast(const StencilPartInterface *P) {
+  if (P == nullptr || D::typeId() != P->typeId())
+    return nullptr;
+  return static_cast<const D *>(P);
+}
+
+static llvm::Expected<ast_type_traits::DynTypedNode>
+getNode(const ast_matchers::BoundNodes &Nodes, StringRef Id) {
+  auto &NodesMap = Nodes.getMap();
+  auto It = NodesMap.find(Id);
+  if (It == NodesMap.end())
+    return llvm::make_error<llvm::StringError>(llvm::errc::invalid_argument,
+                                               "Id not bound: " + Id);
+  return It->second;
+}
+
+namespace {
+// An arbitrary fragment of code within a stencil.
+struct RawTextData {
+  explicit RawTextData(std::string T) : Text(std::move(T)) {}
+  std::string Text;
+};
+
+// A debugging operation to dump the AST for a particular (bound) AST node.
+struct DebugPrintNodeOpData {
+  explicit DebugPrintNodeOpData(std::string S) : Id(std::move(S)) {}
+  std::string Id;
+};
+// Whether to associate a trailing semicolon with a node when identifying it's
+// text.  This flag is needed for expressions (clang::Expr), because their role
+// is ambiguous when they are also complete statements.  When this flag is
+// `Always`, an expression node will be treated like a statement, and will
+// therefore be associated with any trailing semicolon.
+enum class SemiAssociation : bool {
+  Always,
+  Inferred,
+};
+
+// A reference to a particular (bound) AST node.
+struct NodeRefData {
+  explicit NodeRefData(std::string S, SemiAssociation SA)
+      : Id(std::move(S)), SemiAssoc(SA) {}
+  std::string Id;
+  SemiAssociation SemiAssoc;
+};
+} // namespace
+
+bool isEqualData(const RawTextData &A, const RawTextData &B) {
+  return A.Text == B.Text;
+}
+
+bool isEqualData(const DebugPrintNodeOpData &A, const DebugPrintNodeOpData &B) {
+  return A.Id == B.Id;
+}
+
+bool isEqualData(const NodeRefData &A, const NodeRefData &B) {
+  return A.Id == B.Id && A.SemiAssoc == B.SemiAssoc;
+}
+
+// The `evalData()` overloads evaluate the given stencil data to a string, given
+// the match result, and append it to `Result`. We define an overload for each
+// type of stencil data.
+
+Error evalData(const RawTextData &Data, const MatchFinder::MatchResult &,
+               std::string *Result) {
+  Result->append(Data.Text);
+  return Error::success();
+}
+
+Error evalData(const DebugPrintNodeOpData &Data,
+               const MatchFinder::MatchResult &Match, std::string *Result) {
+  std::string Output;
+  llvm::raw_string_ostream Os(Output);
+  auto NodeOrErr = getNode(Match.Nodes, Data.Id);
+  if (auto Err = NodeOrErr.takeError())
+    return Err;
+  NodeOrErr->print(Os, PrintingPolicy(Match.Context->getLangOpts()));
+  *Result += Os.str();
+  return Error::success();
+}
+
+Error evalData(const NodeRefData &Data, const MatchFinder::MatchResult &Match,
+               std::string *Result) {
+  auto NodeOrErr = getNode(Match.Nodes, Data.Id);
+  if (auto Err = NodeOrErr.takeError())
+    return Err;
+  auto &Node = *NodeOrErr;
+  switch (Data.SemiAssoc) {
+  case SemiAssociation::Inferred:
+    // Include the semicolon for non-expression statements:
+    *Result += Node.get<Stmt>() != nullptr && Node.get<Expr>() == nullptr
+                   ? getExtendedText(NodeOrErr.get(), tok::TokenKind::semi,
+                                     *Match.Context)
+                   : getText(NodeOrErr.get(), *Match.Context);
+    break;
+  case SemiAssociation::Always:
+    *Result +=
+        getExtendedText(NodeOrErr.get(), tok::TokenKind::semi, *Match.Context);
+    break;
+  }
+  return Error::success();
+}
+
+template <typename T>
+class StencilPartImpl : public StencilPartInterface {
+  T Data;
+
+public:
+  template <typename... Ps>
+  explicit StencilPartImpl(Ps &&... Args)
+      : StencilPartInterface(StencilPartImpl::typeId()),
+        Data(std::forward<Ps>(Args)...) {}
+
+  // Generates a unique identifier for this class (specifically, one per
+  // instantiation of the template).
+  static const void* typeId() {
+    static bool b;
+    return &b;
+  }
+
+  Error eval(const MatchFinder::MatchResult &Match,
+             std::string *Result) const override {
+    return evalData(Data, Match, Result);
+  }
+
+  bool isEqual(const StencilPartInterface &Other) const override {
+    if (const auto *OtherPtr = down_cast<StencilPartImpl>(&Other))
+      return isEqualData(Data, OtherPtr->Data);
+    return false;
+  }
+};
+
+namespace {
+using RawText = StencilPartImpl<RawTextData>;
+using DebugPrintNodeOp = StencilPartImpl<DebugPrintNodeOpData>;
+using NodeRef = StencilPartImpl<NodeRefData>;
+} // namespace
+
+StencilPart Stencil::wrap(StringRef Text) {
+  return stencil::text(Text);
+}
+
+void Stencil::append(Stencil OtherStencil) {
+  for (auto &Part : OtherStencil.Parts)
+    Parts.push_back(std::move(Part));
+}
+
+llvm::Expected<std::string>
+Stencil::eval(const MatchFinder::MatchResult &Match) const {
+  std::string Result;
+  for (const auto &Part : Parts)
+    if (auto Err = Part.eval(Match, &Result))
+      return std::move(Err);
+  return Result;
+}
+
+StencilPart stencil::text(StringRef Text) {
+  return StencilPart(llvm::make_unique<RawText>(Text));
+}
+
+StencilPart stencil::node(StringRef Id) {
+  return StencilPart(llvm::make_unique<NodeRef>(Id, SemiAssociation::Inferred));
+}
+
+StencilPart stencil::sNode(StringRef Id) {
+  return StencilPart(llvm::make_unique<NodeRef>(Id, SemiAssociation::Always));
+}
+
+StencilPart stencil::dPrint(StringRef Id) {
+  return StencilPart(llvm::make_unique<DebugPrintNodeOp>(Id));
+}
index afa0bcb5ebc5af1f0baad311b9bfa44b884b8ee8..994b88b95b4e4c5564d6b98cf2f25f43d4800b2c 100644 (file)
@@ -50,6 +50,7 @@ add_clang_unittest(ToolingTests
   ReplacementsYamlTest.cpp
   RewriterTest.cpp
   SourceCodeTest.cpp
+  StencilTest.cpp
   ToolingTest.cpp
   TransformerTest.cpp
   )
diff --git a/unittests/Tooling/StencilTest.cpp b/unittests/Tooling/StencilTest.cpp
new file mode 100644 (file)
index 0000000..ffdca05
--- /dev/null
@@ -0,0 +1,223 @@
+//===- unittest/Tooling/StencilTest.cpp -----------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+#include "clang/Tooling/Refactoring/Stencil.h"
+#include "clang/ASTMatchers/ASTMatchers.h"
+#include "clang/Tooling/FixIt.h"
+#include "clang/Tooling/Tooling.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+using namespace clang;
+using namespace tooling;
+using namespace ast_matchers;
+
+namespace {
+using ::testing::AllOf;
+using ::testing::Eq;
+using ::testing::HasSubstr;
+using MatchResult = MatchFinder::MatchResult;
+using tooling::stencil::node;
+using tooling::stencil::sNode;
+using tooling::stencil::text;
+
+// In tests, we can't directly match on llvm::Expected since its accessors
+// mutate the object. So, we collapse it to an Optional.
+static llvm::Optional<std::string> toOptional(llvm::Expected<std::string> V) {
+  if (V)
+    return *V;
+  ADD_FAILURE() << "Losing error in conversion to IsSomething: "
+                << llvm::toString(V.takeError());
+  return llvm::None;
+}
+
+// A very simple matcher for llvm::Optional values.
+MATCHER_P(IsSomething, ValueMatcher, "") {
+  if (!arg)
+    return false;
+  return ::testing::ExplainMatchResult(ValueMatcher, *arg, result_listener);
+}
+
+// Create a valid translation-unit from a statement.
+static std::string wrapSnippet(llvm::Twine StatementCode) {
+  return ("auto stencil_test_snippet = []{" + StatementCode + "};").str();
+}
+
+static DeclarationMatcher wrapMatcher(const StatementMatcher &Matcher) {
+  return varDecl(hasName("stencil_test_snippet"),
+                 hasDescendant(compoundStmt(hasAnySubstatement(Matcher))));
+}
+
+struct TestMatch {
+  // The AST unit from which `result` is built. We bundle it because it backs
+  // the result. Users are not expected to access it.
+  std::unique_ptr<ASTUnit> AstUnit;
+  // The result to use in the test. References `ast_unit`.
+  MatchResult Result;
+};
+
+// Matches `Matcher` against the statement `StatementCode` and returns the
+// result. Handles putting the statement inside a function and modifying the
+// matcher correspondingly. `Matcher` should match `StatementCode` exactly --
+// that is, produce exactly one match.
+static llvm::Optional<TestMatch> matchStmt(llvm::Twine StatementCode,
+                                           StatementMatcher Matcher) {
+  auto AstUnit = buildASTFromCode(wrapSnippet(StatementCode));
+  if (AstUnit == nullptr) {
+    ADD_FAILURE() << "AST construction failed";
+    return llvm::None;
+  }
+  ASTContext &Context = AstUnit->getASTContext();
+  auto Matches = ast_matchers::match(wrapMatcher(Matcher), Context);
+  // We expect a single, exact match for the statement.
+  if (Matches.size() != 1) {
+    ADD_FAILURE() << "Wrong number of matches: " << Matches.size();
+    return llvm::None;
+  }
+  return TestMatch{std::move(AstUnit), MatchResult(Matches[0], &Context)};
+}
+
+class StencilTest : public ::testing::Test {
+protected:
+  // Verifies that the given stencil fails when evaluated on a valid match
+  // result. Binds a statement to "stmt", a (non-member) ctor-initializer to
+  // "init", an expression to "expr" and a (nameless) declaration to "decl".
+  void testError(const Stencil &Stencil,
+                 ::testing::Matcher<std::string> Matcher) {
+    const std::string Snippet = R"cc(
+      struct A {};
+      class F : public A {
+       public:
+        F(int) {}
+      };
+      F(1);
+    )cc";
+    auto StmtMatch = matchStmt(
+        Snippet,
+        stmt(hasDescendant(
+                 cxxConstructExpr(
+                     hasDeclaration(decl(hasDescendant(cxxCtorInitializer(
+                                                           isBaseInitializer())
+                                                           .bind("init")))
+                                        .bind("decl")))
+                     .bind("expr")))
+            .bind("stmt"));
+    ASSERT_TRUE(StmtMatch);
+    if (auto ResultOrErr = Stencil.eval(StmtMatch->Result)) {
+      ADD_FAILURE() << "Expected failure but succeeded: " << *ResultOrErr;
+    } else {
+      auto Err = llvm::handleErrors(ResultOrErr.takeError(),
+                                    [&Matcher](const llvm::StringError &Err) {
+                                      EXPECT_THAT(Err.getMessage(), Matcher);
+                                    });
+      if (Err) {
+        ADD_FAILURE() << "Unhandled error: " << llvm::toString(std::move(Err));
+      }
+    }
+  }
+
+  // Tests failures caused by references to unbound nodes. `unbound_id` is the
+  // id that will cause the failure.
+  void testUnboundNodeError(const Stencil &Stencil, llvm::StringRef UnboundId) {
+    testError(Stencil, AllOf(HasSubstr(UnboundId), HasSubstr("not bound")));
+  }
+};
+
+TEST_F(StencilTest, SingleStatement) {
+  StringRef Condition("C"), Then("T"), Else("E");
+  const std::string Snippet = R"cc(
+    if (true)
+      return 1;
+    else
+      return 0;
+  )cc";
+  auto StmtMatch = matchStmt(
+      Snippet, ifStmt(hasCondition(expr().bind(Condition)),
+                      hasThen(stmt().bind(Then)), hasElse(stmt().bind(Else))));
+  ASSERT_TRUE(StmtMatch);
+  // Invert the if-then-else.
+  auto Stencil = Stencil::cat("if (!", node(Condition), ") ", sNode(Else),
+                              " else ", sNode(Then));
+  EXPECT_THAT(toOptional(Stencil.eval(StmtMatch->Result)),
+              IsSomething(Eq("if (!true) return 0; else return 1;")));
+}
+
+TEST_F(StencilTest, SingleStatementCallOperator) {
+  StringRef Condition("C"), Then("T"), Else("E");
+  const std::string Snippet = R"cc(
+    if (true)
+      return 1;
+    else
+      return 0;
+  )cc";
+  auto StmtMatch = matchStmt(
+      Snippet, ifStmt(hasCondition(expr().bind(Condition)),
+                      hasThen(stmt().bind(Then)), hasElse(stmt().bind(Else))));
+  ASSERT_TRUE(StmtMatch);
+  // Invert the if-then-else.
+  Stencil S = Stencil::cat("if (!", node(Condition), ") ", sNode(Else),
+                              " else ", sNode(Then));
+  EXPECT_THAT(toOptional(S(StmtMatch->Result)),
+              IsSomething(Eq("if (!true) return 0; else return 1;")));
+}
+
+TEST_F(StencilTest, UnboundNode) {
+  const std::string Snippet = R"cc(
+    if (true)
+      return 1;
+    else
+      return 0;
+  )cc";
+  auto StmtMatch = matchStmt(Snippet, ifStmt(hasCondition(stmt().bind("a1")),
+                                             hasThen(stmt().bind("a2"))));
+  ASSERT_TRUE(StmtMatch);
+  auto Stencil = Stencil::cat("if(!", sNode("a1"), ") ", node("UNBOUND"), ";");
+  auto ResultOrErr = Stencil.eval(StmtMatch->Result);
+  EXPECT_TRUE(llvm::errorToBool(ResultOrErr.takeError()))
+      << "Expected unbound node, got " << *ResultOrErr;
+}
+
+// Tests that a stencil with a single parameter (`Id`) evaluates to the expected
+// string, when `Id` is bound to the expression-statement in `Snippet`.
+void testExpr(StringRef Id, StringRef Snippet, const Stencil &Stencil,
+              StringRef Expected) {
+  auto StmtMatch = matchStmt(Snippet, expr().bind(Id));
+  ASSERT_TRUE(StmtMatch);
+  EXPECT_THAT(toOptional(Stencil.eval(StmtMatch->Result)),
+              IsSomething(Expected));
+}
+
+TEST_F(StencilTest, NodeOp) {
+  StringRef Id = "id";
+  testExpr(Id, "3;", Stencil::cat(node(Id)), "3");
+}
+
+TEST_F(StencilTest, SNodeOp) {
+  StringRef Id = "id";
+  testExpr(Id, "3;", Stencil::cat(sNode(Id)), "3;");
+}
+
+TEST(StencilEqualityTest, Equality) {
+  using stencil::dPrint;
+  auto Lhs = Stencil::cat("foo", node("node"), dPrint("dprint_id"));
+  auto Rhs = Lhs;
+  EXPECT_EQ(Lhs, Rhs);
+}
+
+TEST(StencilEqualityTest, InEqualityDifferentOrdering) {
+  auto Lhs = Stencil::cat("foo", node("node"));
+  auto Rhs = Stencil::cat(node("node"), "foo");
+  EXPECT_NE(Lhs, Rhs);
+}
+
+TEST(StencilEqualityTest, InEqualityDifferentSizes) {
+  auto Lhs = Stencil::cat("foo", node("node"), "bar", "baz");
+  auto Rhs = Stencil::cat("foo", node("node"), "bar");
+  EXPECT_NE(Lhs, Rhs);
+}
+} // namespace