Преглед изворни кода

Support parsing and testing unimplemented expressions (#957)

Co-authored-by: Jon Meow <46229924+jonmeow@users.noreply.github.com>
Geoff Romer пре 4 година
родитељ
комит
dc5e62fc7a

+ 6 - 1
executable_semantics/README.md

@@ -28,7 +28,12 @@ The parser is implemented using the flex and bison parser generator tools.
 -   [`syntax.ypp`](syntax/syntax.ypp) the grammar
 
 The parser translates program text into an abstract syntax tree (AST), defined
-in the [ast](ast/) subdirectory.
+in the [ast](ast/) subdirectory. The `UnimplementedExpression` node type can be
+used to define new expression syntaxes without defining their semantics, and the
+same techniques can be applied to other kinds of AST nodes as needed. See the
+handling of the `UNIMPL_EXAMPLE` token for an example of how this is done, and
+see [`unimplemented_example_test.cpp`](syntax/unimplemented_example_test.cpp)
+for an example of how to test it.
 
 The [type checker](interpreter/typecheck.h) defines what it means for an AST to
 be a valid program. The type checker prints an error and exits if the AST is

+ 1 - 0
executable_semantics/ast/BUILD

@@ -44,6 +44,7 @@ cc_library(
     ],
     hdrs = ["ast_test_matchers.h"],
     deps = [
+        ":ast",
         ":ast_node",
         ":declaration",
         ":expression",

+ 3 - 0
executable_semantics/ast/ast_node.h

@@ -45,6 +45,9 @@ class AstNode {
   auto operator=(AstNode&&) -> AstNode& = delete;
   virtual ~AstNode() = 0;
 
+  virtual void Print(llvm::raw_ostream& out) const = 0;
+  LLVM_DUMP_METHOD void Dump() const { Print(llvm::errs()); }
+
   // Returns an enumerator specifying the concrete type of this node.
   //
   // Abstract subclasses of AstNode will provide their own `kind()` method

+ 1 - 0
executable_semantics/ast/ast_rtti.txt

@@ -50,5 +50,6 @@ abstract class Expression : AstNode;
   class TypeTypeLiteral : Expression;
   class IdentifierExpression : Expression;
   class IntrinsicExpression : Expression;
+  class UnimplementedExpression : Expression;
 abstract class Member : AstNode;
   class FieldMember : Member, NamedEntity;

+ 30 - 21
executable_semantics/ast/ast_test_matchers.h

@@ -25,15 +25,13 @@ namespace Carbon {
 
 // Matches a Block node whose .statements() match `matcher`.
 inline auto BlockContentsAre(
-    ::testing::Matcher<llvm::ArrayRef<Nonnull<const Statement*>>> matcher)
-    -> TestingInternal::BlockContentsMatcher {
+    ::testing::Matcher<llvm::ArrayRef<Nonnull<const Statement*>>> matcher) {
   return TestingInternal::BlockContentsMatcher(std::move(matcher));
 }
 
 // Matches a literal with the given value.
 // TODO: add overload for string literals
-inline auto MatchesLiteral(int value)
-    -> TestingInternal::MatchesIntLiteralMatcher {
+inline auto MatchesLiteral(int value) {
   return TestingInternal::MatchesIntLiteralMatcher(value);
 }
 
@@ -41,55 +39,48 @@ inline auto MatchesLiteral(int value)
 // operands that match `lhs` and `rhs` (respectively). The name of the function
 // indicates what value of `.op()` they match.
 inline auto MatchesMul(::testing::Matcher<AstNode> lhs,
-                       ::testing::Matcher<AstNode> rhs)
-    -> TestingInternal::BinaryOperatorExpressionMatcher {
+                       ::testing::Matcher<AstNode> rhs) {
   return TestingInternal::BinaryOperatorExpressionMatcher(
       Operator::Mul, std::move(lhs), std::move(rhs));
 }
 
 inline auto MatchesAdd(::testing::Matcher<AstNode> lhs,
-                       ::testing::Matcher<AstNode> rhs)
-    -> TestingInternal::BinaryOperatorExpressionMatcher {
+                       ::testing::Matcher<AstNode> rhs) {
   return TestingInternal::BinaryOperatorExpressionMatcher(
       Operator::Add, std::move(lhs), std::move(rhs));
 }
 
 inline auto MatchesAnd(::testing::Matcher<AstNode> lhs,
-                       ::testing::Matcher<AstNode> rhs)
-    -> TestingInternal::BinaryOperatorExpressionMatcher {
+                       ::testing::Matcher<AstNode> rhs) {
   return TestingInternal::BinaryOperatorExpressionMatcher(
       Operator::And, std::move(lhs), std::move(rhs));
 }
 
 inline auto MatchesEq(::testing::Matcher<AstNode> lhs,
-                      ::testing::Matcher<AstNode> rhs)
-    -> TestingInternal::BinaryOperatorExpressionMatcher {
+                      ::testing::Matcher<AstNode> rhs) {
   return TestingInternal::BinaryOperatorExpressionMatcher(
       Operator::Eq, std::move(lhs), std::move(rhs));
 }
 
 inline auto MatchesOr(::testing::Matcher<AstNode> lhs,
-                      ::testing::Matcher<AstNode> rhs)
-    -> TestingInternal::BinaryOperatorExpressionMatcher {
+                      ::testing::Matcher<AstNode> rhs) {
   return TestingInternal::BinaryOperatorExpressionMatcher(
       Operator::Or, std::move(lhs), std::move(rhs));
 }
 
 inline auto MatchesSub(::testing::Matcher<AstNode> lhs,
-                       ::testing::Matcher<AstNode> rhs)
-    -> TestingInternal::BinaryOperatorExpressionMatcher {
+                       ::testing::Matcher<AstNode> rhs) {
   return TestingInternal::BinaryOperatorExpressionMatcher(
       Operator::Sub, std::move(lhs), std::move(rhs));
 }
 
 // Matches a return statement with no operand.
-inline auto MatchesEmptyReturn() -> TestingInternal::MatchesReturnMatcher {
+inline auto MatchesEmptyReturn() {
   return TestingInternal::MatchesReturnMatcher();
 }
 
 // Matches a return statement with an explicit operand that matches `matcher`.
-inline auto MatchesReturn(::testing::Matcher<AstNode> matcher)
-    -> TestingInternal::MatchesReturnMatcher {
+inline auto MatchesReturn(::testing::Matcher<AstNode> matcher) {
   return TestingInternal::MatchesReturnMatcher(matcher);
 }
 
@@ -113,11 +104,29 @@ inline auto MatchesReturn(::testing::Matcher<AstNode> matcher)
 // TODO: Add method for matching only if the declaration has no body.
 // TODO: Add methods for matching parameters, deduced parameters,
 //   and return term.
-inline auto MatchesFunctionDeclaration()
-    -> TestingInternal::MatchesFunctionDeclarationMatcher {
+inline auto MatchesFunctionDeclaration() {
   return TestingInternal::MatchesFunctionDeclarationMatcher();
 }
 
+// Matches an UnimplementedExpression with the given label, whose children
+// match `children_matcher`.
+inline auto MatchesUnimplementedExpression(
+    std::string label,
+    ::testing::Matcher<llvm::ArrayRef<Nonnull<const AstNode*>>>
+        children_matcher) {
+  return TestingInternal::MatchesUnimplementedExpressionMatcher(
+      std::move(label), std::move(children_matcher));
+}
+
+// Matches an `AST` whose declarations match the given matcher. Unlike other
+// matchers in this file, this matcher does not match pointers.
+inline auto ASTDeclarations(
+    ::testing::Matcher<std::vector<Nonnull<Declaration*>>>
+        declarations_matcher) {
+  return TestingInternal::ASTDeclarationsMatcher(
+      std::move(declarations_matcher));
+}
+
 }  // namespace Carbon
 
 #endif  // EXECUTABLE_SEMANTICS_AST_AST_TEST_MATCHERS_H_

+ 30 - 5
executable_semantics/ast/ast_test_matchers_internal.cpp

@@ -10,7 +10,9 @@
 namespace Carbon {
 namespace TestingInternal {
 
-auto BlockContentsMatcher::MatchAndExplain(
+AstNodeMatcherBase::~AstNodeMatcherBase() = default;
+
+auto BlockContentsMatcher::MatchAndExplainImpl(
     Nonnull<const AstNode*> node, ::testing::MatchResultListener* out) const
     -> bool {
   const auto* block = llvm::dyn_cast<Block>(node);
@@ -22,7 +24,7 @@ auto BlockContentsMatcher::MatchAndExplain(
   return matcher_.MatchAndExplain(block->statements(), out);
 }
 
-auto MatchesIntLiteralMatcher::MatchAndExplain(
+auto MatchesIntLiteralMatcher::MatchAndExplainImpl(
     const AstNode* node, ::testing::MatchResultListener* listener) const
     -> bool {
   const auto* literal = llvm::dyn_cast<IntLiteral>(node);
@@ -35,7 +37,7 @@ auto MatchesIntLiteralMatcher::MatchAndExplain(
   return matched;
 }
 
-auto BinaryOperatorExpressionMatcher::MatchAndExplain(
+auto BinaryOperatorExpressionMatcher::MatchAndExplainImpl(
     Nonnull<const AstNode*> node, ::testing::MatchResultListener* out) const
     -> bool {
   const auto* op = llvm::dyn_cast<PrimitiveOperatorExpression>(node);
@@ -70,7 +72,7 @@ void BinaryOperatorExpressionMatcher::DescribeToImpl(std::ostream* out,
   rhs_.DescribeTo(out);
 }
 
-auto MatchesReturnMatcher::MatchAndExplain(
+auto MatchesReturnMatcher::MatchAndExplainImpl(
     const AstNode* node, ::testing::MatchResultListener* listener) const
     -> bool {
   const auto* ret = llvm::dyn_cast<Return>(node);
@@ -135,7 +137,7 @@ class RawListenerOstream : public llvm::raw_ostream {
 };
 }  // namespace
 
-auto MatchesFunctionDeclarationMatcher::MatchAndExplain(
+auto MatchesFunctionDeclarationMatcher::MatchAndExplainImpl(
     const AstNode* node, ::testing::MatchResultListener* listener) const
     -> bool {
   RawListenerOstream out(listener);
@@ -185,5 +187,28 @@ void MatchesFunctionDeclarationMatcher::DescribeToImpl(std::ostream* out,
   }
 }
 
+auto MatchesUnimplementedExpressionMatcher::MatchAndExplainImpl(
+    const AstNode* node, ::testing::MatchResultListener* listener) const
+    -> bool {
+  const auto* unimplemented = llvm::dyn_cast<UnimplementedExpression>(node);
+  if (unimplemented == nullptr) {
+    *listener << "is not an UnimplementedExpression";
+    return false;
+  }
+  if (unimplemented->label() != label_) {
+    *listener << "is not labeled " << label_;
+    return false;
+  }
+  *listener << "is an unimplemented " << label_ << " node whose children ";
+  return children_matcher_.MatchAndExplain(unimplemented->children(), listener);
+}
+
+void MatchesUnimplementedExpressionMatcher::DescribeToImpl(std::ostream* out,
+                                                           bool negated) const {
+  *out << "is " << (negated ? "not " : "") << "an unimplemented " << label_
+       << " node whose children ";
+  children_matcher_.DescribeTo(out);
+}
+
 }  // namespace TestingInternal
 }  // namespace Carbon

+ 116 - 86
executable_semantics/ast/ast_test_matchers_internal.h

@@ -12,6 +12,7 @@
 
 #include <ostream>
 
+#include "executable_semantics/ast/ast.h"
 #include "executable_semantics/ast/ast_node.h"
 #include "executable_semantics/ast/declaration.h"
 #include "executable_semantics/ast/expression.h"
@@ -21,76 +22,91 @@
 namespace Carbon {
 namespace TestingInternal {
 
-// Matches a Block based on its contents.
-class BlockContentsMatcher {
+// Abstract GoogleMock matcher which matches AstNodes, and is agnostic to
+// whether they are passed by pointer or reference. Derived classes specify what
+// kinds of AstNodes they match by overriding DescribeToImpl and
+// MatchAndExplainImpl.
+class AstNodeMatcherBase {
  public:
   using is_gtest_matcher = void;
 
-  // Constructs a matcher which matches a Block node whose .statements() matches
-  // `matcher`.
-  explicit BlockContentsMatcher(
-      ::testing::Matcher<llvm::ArrayRef<Nonnull<const Statement*>>> matcher)
-      : matcher_(std::move(matcher)) {}
+  virtual ~AstNodeMatcherBase();
 
   void DescribeTo(std::ostream* out) const {
-    *out << "is a Block whose statements collection ";
-    matcher_.DescribeTo(out);
+    DescribeToImpl(out, /*negated=*/false);
   }
 
   void DescribeNegationTo(std::ostream* out) const {
-    *out << "is not a Block whose statements collection ";
-    matcher_.DescribeTo(out);
+    DescribeToImpl(out, /*negated=*/true);
   }
 
   auto MatchAndExplain(const AstNode& node,
                        ::testing::MatchResultListener* out) const -> bool {
-    return MatchAndExplain(&node, out);
+    return MatchAndExplainImpl(&node, out);
   }
 
   auto MatchAndExplain(Nonnull<const AstNode*> node,
-                       ::testing::MatchResultListener* out) const -> bool;
+                       ::testing::MatchResultListener* out) const -> bool {
+    return MatchAndExplainImpl(node, out);
+  }
 
  private:
-  testing::Matcher<llvm::ArrayRef<Nonnull<const Statement*>>> matcher_;
+  // The implementation of this method must satisfy the contract of
+  // `DescribeTo(out)` (as specified by GoogleMock) if `negated` is false,
+  // or the contract of `DescribeNegationTo(out)` if `negated` is true.
+  virtual void DescribeToImpl(std::ostream* out, bool negated) const = 0;
+
+  // The implementation of this method must satisfy the contract of
+  // `MatchAndExplain(node, out)`, as specified by GoogleMock.
+  virtual auto MatchAndExplainImpl(Nonnull<const AstNode*> node,
+                                   ::testing::MatchResultListener* out) const
+      -> bool = 0;
 };
 
-// Matches an IntLiteral.
-class MatchesIntLiteralMatcher {
+// Matches a Block based on its contents.
+class BlockContentsMatcher : public AstNodeMatcherBase {
  public:
-  using is_gtest_matcher = void;
-
-  // Constructs a matcher which matches an IntLiteral whose value() is `value`.
-  explicit MatchesIntLiteralMatcher(int value) : value_(value) {}
+  // Constructs a matcher which matches a Block node whose .statements() matches
+  // `matcher`.
+  explicit BlockContentsMatcher(
+      ::testing::Matcher<llvm::ArrayRef<Nonnull<const Statement*>>> matcher)
+      : matcher_(std::move(matcher)) {}
 
-  void DescribeTo(std::ostream* out) const {
-    DescribeToImpl(out, /*negated=*/false);
+ private:
+  void DescribeToImpl(std::ostream* out, bool negated) const override {
+    *out << "is " << (negated ? "not " : "")
+         << "a Block whose statements collection ";
+    matcher_.DescribeTo(out);
   }
 
-  void DescribeNegationTo(std::ostream* out) const {
-    DescribeToImpl(out, /*negated=*/true);
-  }
+  auto MatchAndExplainImpl(Nonnull<const AstNode*> node,
+                           ::testing::MatchResultListener* out) const
+      -> bool override;
 
-  auto MatchAndExplain(const AstNode& node,
-                       ::testing::MatchResultListener* listener) const -> bool {
-    return MatchAndExplain(&node, listener);
-  }
+  testing::Matcher<llvm::ArrayRef<Nonnull<const Statement*>>> matcher_;
+};
 
-  auto MatchAndExplain(const AstNode* node,
-                       ::testing::MatchResultListener* listener) const -> bool;
+// Matches an IntLiteral.
+class MatchesIntLiteralMatcher : public AstNodeMatcherBase {
+ public:
+  // Constructs a matcher which matches an IntLiteral whose value() is `value`.
+  explicit MatchesIntLiteralMatcher(int value) : value_(value) {}
 
  private:
-  void DescribeToImpl(std::ostream* out, bool negated) const {
+  void DescribeToImpl(std::ostream* out, bool negated) const override {
     *out << "is " << (negated ? "not " : "") << "a literal " << value_;
   }
 
+  auto MatchAndExplainImpl(const AstNode* node,
+                           ::testing::MatchResultListener* listener) const
+      -> bool override;
+
   int value_;
 };
 
 // Matches a PrimitiveOperatorExpression that has two operands.
-class BinaryOperatorExpressionMatcher {
+class BinaryOperatorExpressionMatcher : public AstNodeMatcherBase {
  public:
-  using is_gtest_matcher = void;
-
   // Constructs a matcher which matches a PrimitiveOperatorExpression whose
   // operator is `op`, and which has two operands that match `lhs` and `rhs`
   // respectively.
@@ -99,24 +115,12 @@ class BinaryOperatorExpressionMatcher {
                                            ::testing::Matcher<AstNode> rhs)
       : op_(op), lhs_(std::move(lhs)), rhs_(std::move(rhs)) {}
 
-  void DescribeTo(std::ostream* out) const {
-    DescribeToImpl(out, /*negated=*/false);
-  }
-
-  void DescribeNegationTo(std::ostream* out) const {
-    DescribeToImpl(out, /*negated=*/true);
-  }
-
-  auto MatchAndExplain(const AstNode& node,
-                       ::testing::MatchResultListener* out) const -> bool {
-    return MatchAndExplain(&node, out);
-  }
-
-  auto MatchAndExplain(Nonnull<const AstNode*> node,
-                       ::testing::MatchResultListener* out) const -> bool;
-
  private:
-  void DescribeToImpl(std::ostream* out, bool negated) const;
+  void DescribeToImpl(std::ostream* out, bool negated) const override;
+
+  auto MatchAndExplainImpl(Nonnull<const AstNode*> node,
+                           ::testing::MatchResultListener* out) const
+      -> bool override;
 
   Operator op_;
   ::testing::Matcher<AstNode> lhs_;
@@ -124,10 +128,8 @@ class BinaryOperatorExpressionMatcher {
 };
 
 // Matches a Return node.
-class MatchesReturnMatcher {
+class MatchesReturnMatcher : public AstNodeMatcherBase {
  public:
-  using is_gtest_matcher = void;
-
   // Constructs a matcher which matches a Return statement that has no operand.
   explicit MatchesReturnMatcher() = default;
 
@@ -136,34 +138,20 @@ class MatchesReturnMatcher {
   explicit MatchesReturnMatcher(::testing::Matcher<AstNode> matcher)
       : matcher_(std::move(matcher)) {}
 
-  void DescribeTo(std::ostream* out) const {
-    DescribeToImpl(out, /*negated=*/false);
-  }
-
-  void DescribeNegationTo(std::ostream* out) const {
-    DescribeToImpl(out, /*negated=*/true);
-  }
-
-  auto MatchAndExplain(const AstNode& node,
-                       ::testing::MatchResultListener* listener) const -> bool {
-    return MatchAndExplain(&node, listener);
-  }
-
-  auto MatchAndExplain(const AstNode* node,
-                       ::testing::MatchResultListener* listener) const -> bool;
-
  private:
-  void DescribeToImpl(std::ostream* out, bool negated) const;
+  void DescribeToImpl(std::ostream* out, bool negated) const override;
+
+  auto MatchAndExplainImpl(const AstNode* node,
+                           ::testing::MatchResultListener* listener) const
+      -> bool override;
 
   std::optional<::testing::Matcher<AstNode>> matcher_;
 };
 
 // Matches a FunctionDeclaration. See documentation for
 // MatchesFunctionDeclaration in ast_test_matchers.h.
-class MatchesFunctionDeclarationMatcher {
+class MatchesFunctionDeclarationMatcher : public AstNodeMatcherBase {
  public:
-  using is_gtest_matcher = void;
-
   MatchesFunctionDeclarationMatcher() = default;
 
   auto WithName(::testing::Matcher<std::string> name_matcher)
@@ -178,27 +166,69 @@ class MatchesFunctionDeclarationMatcher {
     return *this;
   }
 
+ private:
+  void DescribeToImpl(std::ostream* out, bool negated) const override;
+  auto MatchAndExplainImpl(const AstNode* node,
+                           ::testing::MatchResultListener* listener) const
+      -> bool override;
+
+  std::optional<::testing::Matcher<std::string>> name_matcher_;
+  std::optional<::testing::Matcher<AstNode>> body_matcher_;
+};
+
+// Matches an UnimplementedExpression.
+class MatchesUnimplementedExpressionMatcher : public AstNodeMatcherBase {
+ public:
+  // Constructs a matcher which matches an UnimplementedExpression that has the
+  // given label, and whose children match children_matcher.
+  MatchesUnimplementedExpressionMatcher(
+      std::string label,
+      ::testing::Matcher<llvm::ArrayRef<Nonnull<const AstNode*>>>
+          children_matcher)
+      : label_(std::move(label)),
+        children_matcher_(std::move(children_matcher)) {}
+
+ private:
+  void DescribeToImpl(std::ostream* out, bool negated) const override;
+
+  auto MatchAndExplainImpl(Nonnull<const AstNode*> node,
+                           ::testing::MatchResultListener* listener) const
+      -> bool override;
+
+  std::string label_;
+  ::testing::Matcher<llvm::ArrayRef<Nonnull<const AstNode*>>> children_matcher_;
+};
+
+// Matches an `AST`.
+class ASTDeclarationsMatcher {
+ public:
+  using is_gtest_matcher = void;
+
+  // Constructs a matcher which matches an `AST` whose `declarations` member
+  // matches `declarations_matcher`
+  explicit ASTDeclarationsMatcher(
+      ::testing::Matcher<std::vector<Nonnull<Declaration*>>>
+          declarations_matcher)
+      : declarations_matcher_(std::move(declarations_matcher)) {}
+
   void DescribeTo(std::ostream* out) const {
-    DescribeToImpl(out, /*negated=*/false);
+    *out << "AST declarations ";
+    declarations_matcher_.DescribeTo(out);
   }
 
   void DescribeNegationTo(std::ostream* out) const {
-    DescribeToImpl(out, /*negated=*/true);
+    *out << "AST declarations ";
+    declarations_matcher_.DescribeNegationTo(out);
   }
 
-  auto MatchAndExplain(const AstNode& node,
+  auto MatchAndExplain(const AST& ast,
                        ::testing::MatchResultListener* listener) const -> bool {
-    return MatchAndExplain(&node, listener);
+    *listener << "whose declarations ";
+    return declarations_matcher_.MatchAndExplain(ast.declarations, listener);
   }
 
-  auto MatchAndExplain(const AstNode* node,
-                       ::testing::MatchResultListener* listener) const -> bool;
-
  private:
-  void DescribeToImpl(std::ostream* out, bool negated) const;
-
-  std::optional<::testing::Matcher<std::string>> name_matcher_;
-  std::optional<::testing::Matcher<AstNode>> body_matcher_;
+  ::testing::Matcher<std::vector<Nonnull<Declaration*>>> declarations_matcher_;
 };
 
 }  // namespace TestingInternal

+ 34 - 0
executable_semantics/ast/ast_test_matchers_test.cpp

@@ -17,6 +17,7 @@ namespace Carbon {
 namespace {
 
 using ::testing::_;
+using ::testing::ElementsAre;
 using ::testing::IsEmpty;
 using ::testing::Not;
 
@@ -124,5 +125,38 @@ TEST(MatchesFunctionDeclarationTest, BasicUsage) {
   EXPECT_THAT(body, Not(MatchesFunctionDeclaration()));
 }
 
+TEST(MatchesUnimplementedExpressionTest, BasicUsage) {
+  IntLiteral two(DummyLoc, 2);
+  IntLiteral three(DummyLoc, 3);
+  UnimplementedExpression unimplemented(DummyLoc, "DummyLabel", &two, &three);
+
+  EXPECT_THAT(unimplemented, MatchesUnimplementedExpression(
+                                 "DummyLabel", ElementsAre(MatchesLiteral(2),
+                                                           MatchesLiteral(3))));
+  EXPECT_THAT(
+      &unimplemented,
+      MatchesUnimplementedExpression(
+          "DummyLabel", ElementsAre(MatchesLiteral(2), MatchesLiteral(3))));
+  EXPECT_THAT(
+      unimplemented,
+      Not(MatchesUnimplementedExpression(
+          "WrongLabel", ElementsAre(MatchesLiteral(2), MatchesLiteral(3)))));
+  EXPECT_THAT(unimplemented,
+              Not(MatchesUnimplementedExpression("DummyLabel", IsEmpty())));
+  EXPECT_THAT(two,
+              Not(MatchesUnimplementedExpression("DummyLabel", IsEmpty())));
+}
+
+TEST(ASTDeclarationsTest, BasicUsage) {
+  TuplePattern params(DummyLoc, {});
+  Block body(DummyLoc, {});
+  FunctionDeclaration decl(DummyLoc, "Foo", {}, &params,
+                           ReturnTerm::Omitted(DummyLoc), &body);
+  AST ast = {.declarations = {&decl}};
+
+  EXPECT_THAT(ast, ASTDeclarations(ElementsAre(MatchesFunctionDeclaration())));
+  EXPECT_THAT(ast, Not(ASTDeclarations(IsEmpty())));
+}
+
 }  // namespace
 }  // namespace Carbon

+ 12 - 8
executable_semantics/ast/declaration.cpp

@@ -4,6 +4,7 @@
 
 #include "executable_semantics/ast/declaration.h"
 
+#include "llvm/ADT/StringExtras.h"
 #include "llvm/Support/Casting.h"
 
 namespace Carbon {
@@ -32,7 +33,7 @@ void Declaration::Print(llvm::raw_ostream& out) const {
       const auto& choice = cast<ChoiceDeclaration>(*this);
       out << "choice " << choice.name() << " {\n";
       for (Nonnull<const AlternativeSignature*> alt : choice.alternatives()) {
-        out << "alt " << alt->name() << " " << alt->signature() << ";\n";
+        out << *alt << ";\n";
       }
       out << "}\n";
       break;
@@ -46,6 +47,10 @@ void Declaration::Print(llvm::raw_ostream& out) const {
   }
 }
 
+void GenericBinding::Print(llvm::raw_ostream& out) const {
+  out << name() << ":! " << type();
+}
+
 void ReturnTerm::Print(llvm::raw_ostream& out) const {
   switch (kind_) {
     case ReturnKind::Omitted:
@@ -63,14 +68,9 @@ void FunctionDeclaration::PrintDepth(int depth, llvm::raw_ostream& out) const {
   out << "fn " << name_ << " ";
   if (!deduced_parameters_.empty()) {
     out << "[";
-    unsigned int i = 0;
+    llvm::ListSeparator sep;
     for (Nonnull<const GenericBinding*> deduced : deduced_parameters_) {
-      if (i != 0) {
-        out << ", ";
-      }
-      out << deduced->name() << ":! ";
-      deduced->type().Print(out);
-      ++i;
+      out << sep << *deduced;
     }
     out << "]";
   }
@@ -84,4 +84,8 @@ void FunctionDeclaration::PrintDepth(int depth, llvm::raw_ostream& out) const {
   }
 }
 
+void AlternativeSignature::Print(llvm::raw_ostream& out) const {
+  out << "alt " << name() << " " << signature();
+}
+
 }  // namespace Carbon

+ 5 - 2
executable_semantics/ast/declaration.h

@@ -38,8 +38,7 @@ class Declaration : public virtual AstNode, public NamedEntity {
   Declaration(const Member&) = delete;
   auto operator=(const Member&) -> Declaration& = delete;
 
-  void Print(llvm::raw_ostream& out) const;
-  LLVM_DUMP_METHOD void Dump() const { Print(llvm::errs()); }
+  void Print(llvm::raw_ostream& out) const override;
 
   static auto classof(const AstNode* node) -> bool {
     return InheritsFromDeclaration(node->kind());
@@ -84,6 +83,8 @@ struct GenericBinding : public virtual AstNode, public NamedEntity {
         name_(std::move(name)),
         type_(type) {}
 
+  void Print(llvm::raw_ostream& out) const override;
+
   static auto classof(const AstNode* node) -> bool {
     return InheritsFromGenericBinding(node->kind());
   }
@@ -253,6 +254,8 @@ class AlternativeSignature : public virtual AstNode, public NamedEntity {
         name_(std::move(name)),
         signature_(signature) {}
 
+  void Print(llvm::raw_ostream& out) const override;
+
   static auto classof(const AstNode* node) -> bool {
     return InheritsFromAlternativeSignature(node->kind());
   }

+ 11 - 0
executable_semantics/ast/expression.cpp

@@ -184,6 +184,17 @@ void Expression::Print(llvm::raw_ostream& out) const {
           out << "print";
       }
       out << ")";
+      break;
+    case ExpressionKind::UnimplementedExpression: {
+      const auto& unimplemented = cast<UnimplementedExpression>(*this);
+      out << "UnimplementedExpression<" << unimplemented.label() << ">(";
+      llvm::ListSeparator sep;
+      for (Nonnull<const AstNode*> child : unimplemented.children()) {
+        out << sep << *child;
+      }
+      out << ")";
+      break;
+    }
   }
 }
 

+ 40 - 2
executable_semantics/ast/expression.h

@@ -36,8 +36,7 @@ class Expression : public virtual AstNode {
 
   ~Expression() override = 0;
 
-  void Print(llvm::raw_ostream& out) const;
-  LLVM_DUMP_METHOD void Dump() const { Print(llvm::errs()); }
+  void Print(llvm::raw_ostream& out) const override;
 
   static auto classof(const AstNode* node) {
     return InheritsFromExpression(node->kind());
@@ -451,6 +450,45 @@ class IntrinsicExpression : public Expression {
   Nonnull<TupleLiteral*> args_;
 };
 
+// An expression whose semantics have not been implemented. This can be used
+// as a placeholder during development, in order to implement and test parsing
+// of a new expression syntax without having to implement its semantics.
+class UnimplementedExpression : public Expression {
+ public:
+  // Constructs an UnimplementedExpression with the given label and the given
+  // children, which must all be convertible to Nonnull<AstNode*>. The label
+  // should correspond roughly to the name of the class that will eventually
+  // replace this usage of UnimplementedExpression.
+  template <typename... Children>
+  UnimplementedExpression(SourceLocation source_loc, std::string label,
+                          Children... children)
+      : AstNode(AstNodeKind::UnimplementedExpression, source_loc),
+        label_(std::move(label)) {
+    AddChildren(children...);
+  }
+
+  static auto classof(const AstNode* node) -> bool {
+    return InheritsFromUnimplementedExpression(node->kind());
+  }
+
+  auto label() const -> std::string_view { return label_; }
+  auto children() const -> llvm::ArrayRef<Nonnull<const AstNode*>> {
+    return children_;
+  }
+
+ private:
+  void AddChildren() {}
+
+  template <typename... Children>
+  void AddChildren(Nonnull<AstNode*> child, Children... children) {
+    children_.push_back(child);
+    AddChildren(children...);
+  }
+
+  std::string label_;
+  std::vector<Nonnull<AstNode*>> children_;
+};
+
 // Converts paren_contents to an Expression, interpreting the parentheses as
 // grouping if their contents permit that interpretation, or as forming a
 // tuple otherwise.

+ 1 - 2
executable_semantics/ast/member.h

@@ -30,8 +30,7 @@ class Member : public virtual AstNode, public NamedEntity {
   Member(const Member&) = delete;
   auto operator=(const Member&) -> Member& = delete;
 
-  void Print(llvm::raw_ostream& out) const;
-  LLVM_DUMP_METHOD void Dump() const { Print(llvm::errs()); }
+  void Print(llvm::raw_ostream& out) const override;
 
   static auto classof(const AstNode* node) -> bool {
     return InheritsFromMember(node->kind());

+ 1 - 2
executable_semantics/ast/pattern.h

@@ -36,8 +36,7 @@ class Pattern : public virtual AstNode {
 
   ~Pattern() override = 0;
 
-  void Print(llvm::raw_ostream& out) const;
-  LLVM_DUMP_METHOD void Dump() const { Print(llvm::errs()); }
+  void Print(llvm::raw_ostream& out) const override;
 
   static auto classof(const AstNode* node) -> bool {
     return InheritsFromPattern(node->kind());

+ 1 - 2
executable_semantics/ast/statement.h

@@ -25,9 +25,8 @@ class Statement : public virtual AstNode {
  public:
   ~Statement() override = 0;
 
-  void Print(llvm::raw_ostream& out) const { PrintDepth(-1, out); }
+  void Print(llvm::raw_ostream& out) const override { PrintDepth(-1, out); }
   void PrintDepth(int depth, llvm::raw_ostream& out) const;
-  LLVM_DUMP_METHOD void Dump() const { Print(llvm::errs()); }
 
   static auto classof(const AstNode* node) {
     return InheritsFromStatement(node->kind());

+ 4 - 0
executable_semantics/interpreter/interpreter.cpp

@@ -358,6 +358,8 @@ void Interpreter::StepLvalue() {
     case ExpressionKind::StringTypeLiteral:
     case ExpressionKind::IntrinsicExpression:
       FATAL() << "Can't treat expression as lvalue: " << exp;
+    case ExpressionKind::UnimplementedExpression:
+      FATAL() << "Unimplemented: " << exp;
   }
 }
 
@@ -651,6 +653,8 @@ void Interpreter::StepExp() {
       CHECK(act.pos() == 0);
       return todo_.FinishAction(arena_->New<StringType>());
     }
+    case ExpressionKind::UnimplementedExpression:
+      FATAL() << "Unimplemented: " << exp;
   }  // switch (exp->kind)
 }
 

+ 2 - 0
executable_semantics/interpreter/type_checker.cpp

@@ -750,6 +750,8 @@ auto TypeChecker::TypeCheckExp(Nonnull<Expression*> e, TypeEnv types,
       e->set_value_category(Expression::ValueCategory::Let);
       SetStaticType(e, arena_->New<TypeType>());
       return TCResult(types);
+    case ExpressionKind::UnimplementedExpression:
+      FATAL() << "Unimplemented: " << *e;
   }
 }
 

+ 22 - 0
executable_semantics/syntax/BUILD

@@ -22,6 +22,17 @@ cc_test(
     ],
 )
 
+cc_library(
+    name = "parse_test_matchers",
+    testonly = 1,
+    srcs = ["parse_test_matchers_internal.h"],
+    hdrs = ["parse_test_matchers.h"],
+    deps = [
+        ":syntax",
+        "@com_google_googletest//:gtest",
+    ],
+)
+
 cc_library(
     name = "syntax",
     srcs = [
@@ -108,3 +119,14 @@ mypy_test(
     include_imports = True,
     deps = [":format_grammar_lib"],
 )
+
+cc_test(
+    name = "unimplemented_example_test",
+    srcs = ["unimplemented_example_test.cpp"],
+    deps = [
+        ":parse_test_matchers",
+        ":syntax",
+        "//executable_semantics/ast:ast_test_matchers",
+        "@com_google_googletest//:gtest_main",
+    ],
+)

+ 2 - 0
executable_semantics/syntax/lexer.lpp

@@ -77,6 +77,7 @@ STRING               "String"
 TRUE                 "true"
 TYPE                 "Type"
 UNDERSCORE           "_"
+UNIMPL_EXAMPLE       "__unimplemented_example_infix"
 VAR                  "var"
 WHILE                "while"
 /* table-end */
@@ -169,6 +170,7 @@ string_literal        \"([^\\\"\n\v\f\r]|\\.)*\"
 {TRUE}                { return SIMPLE_TOKEN(TRUE);                }
 {TYPE}                { return SIMPLE_TOKEN(TYPE);                }
 {UNDERSCORE}          { return SIMPLE_TOKEN(UNDERSCORE);          }
+{UNIMPL_EXAMPLE}      { return SIMPLE_TOKEN(UNIMPL_EXAMPLE);      }
 {VAR}                 { return SIMPLE_TOKEN(VAR);                 }
 {WHILE}               { return SIMPLE_TOKEN(WHILE);               }
  /* table-end */

+ 23 - 0
executable_semantics/syntax/parse_test_matchers.h

@@ -0,0 +1,23 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#ifndef EXECUTABLE_SEMANTICS_SYNTAX_PARSE_TEST_MATCHERS_H_
+#define EXECUTABLE_SEMANTICS_SYNTAX_PARSE_TEST_MATCHERS_H_
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include "executable_semantics/syntax/parse_test_matchers_internal.h"
+
+namespace Carbon {
+
+// Matches the return value of `Parse()` if it represents a successful parse
+// whose output matches the given matcher.
+inline auto ParsedAs(::testing::Matcher<AST> ast_matcher) {
+  return TestingInternal::ParsedAsMatcher(std::move(ast_matcher));
+}
+
+}  // namespace Carbon
+
+#endif  // EXECUTABLE_SEMANTICS_SYNTAX_PARSE_TEST_MATCHERS_H_

+ 59 - 0
executable_semantics/syntax/parse_test_matchers_internal.h

@@ -0,0 +1,59 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#ifndef EXECUTABLE_SEMANTICS_SYNTAX_PARSE_TEST_MATCHERS_INTERNAL_H_
+#define EXECUTABLE_SEMANTICS_SYNTAX_PARSE_TEST_MATCHERS_INTERNAL_H_
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include <ostream>
+#include <variant>
+
+#include "executable_semantics/syntax/parse.h"
+
+namespace Carbon {
+namespace TestingInternal {
+
+// Implementation of ParsedAs(). See there for detailed documentation.
+class ParsedAsMatcher {
+ public:
+  using is_gtest_matcher = void;
+
+  explicit ParsedAsMatcher(::testing::Matcher<AST> ast_matcher)
+      : ast_matcher_(std::move(ast_matcher)) {}
+
+  void DescribeTo(std::ostream* out) const {
+    DescribeToImpl(out, /*negated=*/false);
+  }
+
+  void DescribeNegationTo(std::ostream* out) const {
+    DescribeToImpl(out, /*negated=*/true);
+  }
+
+  auto MatchAndExplain(const std::variant<AST, SyntaxErrorCode>& result,
+                       ::testing::MatchResultListener* listener) const -> bool {
+    if (std::holds_alternative<SyntaxErrorCode>(result)) {
+      *listener << "holds error code " << std::get<SyntaxErrorCode>(result);
+      return false;
+    } else {
+      *listener << "is a successful parse whose ";
+      return ast_matcher_.MatchAndExplain(std::get<AST>(result), listener);
+    }
+  }
+
+ private:
+  void DescribeToImpl(std::ostream* out, bool negated) const {
+    *out << "is " << (negated ? "not " : "")
+         << "a successful parse result whose ";
+    ast_matcher_.DescribeTo(out);
+  }
+
+  ::testing::Matcher<AST> ast_matcher_;
+};
+
+}  // namespace TestingInternal
+}  // namespace Carbon
+
+#endif  // EXECUTABLE_SEMANTICS_SYNTAX_PARSE_TEST_MATCHERS_INTERNAL_H_

+ 7 - 0
executable_semantics/syntax/parser.ypp

@@ -190,6 +190,7 @@
   TRUE
   TYPE
   UNDERSCORE
+  UNIMPL_EXAMPLE
   VAR
   WHILE
   // table-end
@@ -223,6 +224,7 @@
 // same precedence as POSTFIX_STAR.
 %precedence POSTFIX_STAR UNARY_STAR
 %left PERIOD ARROW
+%nonassoc UNIMPL_EXAMPLE
 %precedence
   LEFT_PARENTHESIS
   RIGHT_PARENTHESIS
@@ -381,6 +383,11 @@ expression:
     }
 | FN_TYPE tuple ARROW expression
     { $$ = arena->New<FunctionTypeLiteral>(context.source_loc(), $2, $4); }
+| expression UNIMPL_EXAMPLE expression
+    {
+      $$ = arena->New<UnimplementedExpression>(context.source_loc(),
+                                               "ExampleInfix", $1, $3);
+    }
 ;
 designator: PERIOD identifier { $$ = $2; }
 ;

+ 36 - 0
executable_semantics/syntax/unimplemented_example_test.cpp

@@ -0,0 +1,36 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include "executable_semantics/ast/ast_test_matchers.h"
+#include "executable_semantics/syntax/parse.h"
+#include "executable_semantics/syntax/parse_test_matchers.h"
+
+namespace Carbon {
+namespace {
+
+using ::testing::ElementsAre;
+
+TEST(UnimplementedExampleTest, VerifyPrecedence) {
+  static constexpr std::string_view Program = R"(
+    package ExecutableSemanticsTest api;
+    fn Main() -> i32 {
+      return 1 __unimplemented_example_infix 2 + 3;
+    }
+  )";
+  Arena arena;
+  EXPECT_THAT(ParseFromString(&arena, "dummy.carbon", Program, false),
+              ParsedAs(ASTDeclarations(
+                  ElementsAre(MatchesFunctionDeclaration().WithBody(
+                      BlockContentsAre(ElementsAre(MatchesReturn(MatchesAdd(
+                          MatchesUnimplementedExpression(
+                              "ExampleInfix", ElementsAre(MatchesLiteral(1),
+                                                          MatchesLiteral(2))),
+                          MatchesLiteral(3))))))))));
+}
+
+}  // namespace
+}  // namespace Carbon