Просмотр исходного кода

Add operator precedence parser. (#464)

Richard Smith 5 лет назад
Родитель
Сommit
09763653b7

+ 1 - 1
lexer/token_registry.def

@@ -143,7 +143,7 @@ CARBON_KEYWORD_TOKEN(RefKeyword,        "ref")
 CARBON_KEYWORD_TOKEN(ReturnKeyword,     "return")
 CARBON_KEYWORD_TOKEN(StaticKeyword,     "static")
 CARBON_KEYWORD_TOKEN(StructKeyword,     "struct")
-CARBON_KEYWORD_TOKEN(ThroKeyword,       "throw")
+CARBON_KEYWORD_TOKEN(ThrowKeyword,      "throw")
 CARBON_KEYWORD_TOKEN(TryKeyword,        "try")
 CARBON_KEYWORD_TOKEN(UnderscoreKeyword, "_")
 CARBON_KEYWORD_TOKEN(VarKeyword,        "var")

+ 23 - 0
parser/BUILD

@@ -36,6 +36,7 @@ cc_library(
     hdrs = ["parse_tree.h"],
     deps = [
         ":parse_node_kind",
+        ":precedence",
         "//diagnostics:diagnostic_emitter",
         "//lexer:token_kind",
         "//lexer:tokenized_buffer",
@@ -85,3 +86,25 @@ cc_fuzz_test(
         "@llvm-project//llvm:Support",
     ],
 )
+
+cc_library(
+    name = "precedence",
+    srcs = ["precedence.cpp"],
+    hdrs = ["precedence.h"],
+    deps = [
+        "//lexer:token_kind",
+        "@llvm-project//llvm:Support",
+    ],
+)
+
+cc_test(
+    name = "precedence_test",
+    srcs = ["precedence_test.cpp"],
+    deps = [
+        ":precedence",
+        "//lexer:token_kind",
+        "@llvm-project//llvm:gmock",
+        "@llvm-project//llvm:gtest",
+        "@llvm-project//llvm:gtest_main",
+    ],
+)

+ 3 - 0
parser/parse_node_kind.def

@@ -37,5 +37,8 @@ CARBON_PARSE_NODE_KIND(DesignatedName)
 CARBON_PARSE_NODE_KIND(CallExpression)
 CARBON_PARSE_NODE_KIND(CallExpressionComma)
 CARBON_PARSE_NODE_KIND(CallExpressionEnd)
+CARBON_PARSE_NODE_KIND(PrefixOperator)
+CARBON_PARSE_NODE_KIND(InfixOperator)
+CARBON_PARSE_NODE_KIND(PostfixOperator)
 
 #undef CARBON_PARSE_NODE_KIND

+ 76 - 0
parser/parse_tree_test.cpp

@@ -408,6 +408,82 @@ TEST_F(ParseTreeTest, InvalidDesignators) {
            MatchFileEnd()}));
 }
 
+TEST_F(ParseTreeTest, Operators) {
+  TokenizedBuffer tokens = GetTokenizedBuffer(
+      "fn F() {\n"
+      "  n = a * b + c * d = d * d << e & f - not g;\n"
+      "  ++++n;\n"
+      "  n++++;\n"
+      "  a and b and c;\n"
+      "  a and b or c;\n"
+      "  a or b and c;\n"
+      "  not a and not b and not c;\n"
+      "}");
+  ParseTree tree = ParseTree::Parse(tokens, consumer);
+  EXPECT_TRUE(tree.HasErrors());
+
+  EXPECT_THAT(
+      tree,
+      MatchParseTreeNodes(
+          {MatchFunctionDeclaration(
+               MatchDeclaredName("F"),
+               MatchParameterList(MatchParameterListEnd()),
+               MatchCodeBlock(
+                   MatchExpressionStatement(MatchInfixOperator(
+                       MatchNameReference("n"), "=",
+                       MatchInfixOperator(
+                           MatchInfixOperator(
+                               MatchInfixOperator(MatchNameReference("a"), "*",
+                                                  MatchNameReference("b")),
+                               "+",
+                               MatchInfixOperator(MatchNameReference("c"), "*",
+                                                  MatchNameReference("d"))),
+                           "=",
+                           MatchInfixOperator(
+                               HasError,
+                               MatchInfixOperator(
+                                   HasError,
+                                   MatchInfixOperator(
+                                       HasError,
+                                       MatchInfixOperator(
+                                           MatchNameReference("d"), "*",
+                                           MatchNameReference("d")),
+                                       "<<", MatchNameReference("e")),
+                                   "&", MatchNameReference("f")),
+                               "-",
+                               MatchPrefixOperator("not",
+                                                   MatchNameReference("g")))))),
+                   MatchExpressionStatement(MatchPrefixOperator(
+                       "++",
+                       MatchPrefixOperator("++", MatchNameReference("n")))),
+                   MatchExpressionStatement(MatchPostfixOperator(
+                       MatchPostfixOperator(MatchNameReference("n"), "++"),
+                       "++")),
+                   MatchExpressionStatement(MatchInfixOperator(
+                       MatchInfixOperator(MatchNameReference("a"), "and",
+                                          MatchNameReference("b")),
+                       "and", MatchNameReference("c"))),
+                   MatchExpressionStatement(MatchInfixOperator(
+                       HasError,
+                       MatchInfixOperator(MatchNameReference("a"), "and",
+                                          MatchNameReference("b")),
+                       "or", MatchNameReference("c"))),
+                   MatchExpressionStatement(MatchInfixOperator(
+                       HasError,
+                       MatchInfixOperator(MatchNameReference("a"), "or",
+                                          MatchNameReference("b")),
+                       "and", MatchNameReference("c"))),
+                   MatchExpressionStatement(MatchInfixOperator(
+                       MatchInfixOperator(
+                           MatchPrefixOperator("not", MatchNameReference("a")),
+                           "and",
+                           MatchPrefixOperator("not", MatchNameReference("b"))),
+                       "and",
+                       MatchPrefixOperator("not", MatchNameReference("c")))),
+                   MatchCodeBlockEnd())),
+           MatchFileEnd()}));
+}
+
 auto GetAndDropLine(llvm::StringRef& s) -> std::string {
   auto newline_offset = s.find_first_of('\n');
   llvm::StringRef line = s.slice(0, newline_offset);

+ 81 - 5
parser/parser_impl.cpp

@@ -86,6 +86,13 @@ struct UnexpectedTokenInFunctionArgs
       "Unexpected token in function argument list.";
 };
 
+struct OperatorRequiresParentheses
+    : SimpleDiagnostic<OperatorRequiresParentheses> {
+  static constexpr llvm::StringLiteral ShortName = "syntax-error";
+  static constexpr llvm::StringLiteral Message =
+      "Parentheses are required to disambiguate operator precedence.";
+};
+
 ParseTree::Parser::Parser(ParseTree& tree_arg, TokenizedBuffer& tokens_arg,
                           TokenDiagnosticEmitter& emitter)
     : tree(tree_arg),
@@ -111,7 +118,11 @@ auto ParseTree::Parser::Parse(TokenizedBuffer& tokens,
 
   Parser parser(tree, tokens, emitter);
   while (!parser.AtEndOfFile()) {
-    parser.ParseDeclaration();
+    if (!parser.ParseDeclaration()) {
+      // We don't have an enclosing parse tree node to mark as erroneous, so
+      // just mark the tree as a whole.
+      tree.has_errors = true;
+    }
   }
 
   parser.AddLeafNode(ParseNodeKind::FileEnd(), *parser.position);
@@ -441,9 +452,7 @@ auto ParseTree::Parser::ParseDeclaration() -> llvm::Optional<Node> {
     return *found_semi_n;
   }
 
-  // Nothing, not even a semicolon found. We still need to mark that an error
-  // occurred though.
-  tree.has_errors = true;
+  // Nothing, not even a semicolon found.
   return llvm::None;
 }
 
@@ -583,8 +592,75 @@ auto ParseTree::Parser::ParsePostfixExpression() -> llvm::Optional<Node> {
   }
 }
 
+auto ParseTree::Parser::ParseOperatorExpression(
+    llvm::Optional<PrecedenceGroup> ambient_precedence)
+    -> llvm::Optional<Node> {
+  auto start = StartSubtree();
+
+  llvm::Optional<Node> lhs;
+  llvm::Optional<PrecedenceGroup> lhs_precedence;
+
+  // Check for a prefix operator.
+  if (auto operator_precedence =
+          PrecedenceGroup::ForLeading(tokens.GetKind(*position));
+      !operator_precedence) {
+    lhs = ParsePostfixExpression();
+  } else {
+    if (ambient_precedence && PrecedenceGroup::GetPriority(
+                                  *ambient_precedence, *operator_precedence) !=
+                                  OperatorPriority::RightFirst) {
+      // The precedence rules don't permit this prefix operator in this
+      // context. Diagnose this, but carry on and parse it anyway.
+      emitter.EmitError<OperatorRequiresParentheses>(*position);
+    }
+
+    auto operator_token = Consume(tokens.GetKind(*position));
+    bool has_errors = !ParseOperatorExpression(*operator_precedence);
+    lhs = AddNode(ParseNodeKind::PrefixOperator(), operator_token, start,
+                  has_errors);
+    lhs_precedence = *operator_precedence;
+  }
+
+  // Consume a sequence of infix and postfix operators.
+  while (auto trailing_operator =
+             PrecedenceGroup::ForTrailing(tokens.GetKind(*position))) {
+    auto [operator_precedence, is_binary] = *trailing_operator;
+    if (ambient_precedence && PrecedenceGroup::GetPriority(
+                                  *ambient_precedence, operator_precedence) !=
+                                  OperatorPriority::RightFirst) {
+      // The precedence rules don't permit this operator in this context. Try
+      // again in the enclosing expression context.
+      return lhs;
+    }
+
+    if (lhs_precedence &&
+        PrecedenceGroup::GetPriority(*lhs_precedence, operator_precedence) !=
+            OperatorPriority::LeftFirst) {
+      // Either the LHS operator and this operator are ambiguous, or the
+      // LHS operaor is a unary operator that can't be nested within
+      // this operator. Either way, parentheses are required.
+      emitter.EmitError<OperatorRequiresParentheses>(*position);
+      lhs = llvm::None;
+    }
+
+    auto operator_token = Consume(tokens.GetKind(*position));
+
+    if (is_binary) {
+      auto rhs = ParseOperatorExpression(operator_precedence);
+      lhs = AddNode(ParseNodeKind::InfixOperator(), operator_token, start,
+                    /*has_error=*/!lhs || !rhs);
+    } else {
+      lhs = AddNode(ParseNodeKind::PostfixOperator(), operator_token, start,
+                    /*has_error=*/!lhs);
+    }
+    lhs_precedence = operator_precedence;
+  }
+
+  return lhs;
+}
+
 auto ParseTree::Parser::ParseExpression() -> llvm::Optional<Node> {
-  return ParsePostfixExpression();
+  return ParseOperatorExpression(llvm::None);
 }
 
 auto ParseTree::Parser::ParseExpressionStatement() -> llvm::Optional<Node> {

+ 6 - 0
parser/parser_impl.h

@@ -11,6 +11,7 @@
 #include "llvm/ADT/Optional.h"
 #include "parser/parse_node_kind.h"
 #include "parser/parse_tree.h"
+#include "parser/precedence.h"
 
 namespace Carbon {
 
@@ -154,6 +155,11 @@ class ParseTree::Parser {
   // -   designators
   auto ParsePostfixExpression() -> llvm::Optional<Node>;
 
+  // Parses an expression involving operators, in a context with the given
+  // precedence.
+  auto ParseOperatorExpression(llvm::Optional<PrecedenceGroup> precedence)
+      -> llvm::Optional<Node>;
+
   // Parses an expression.
   auto ParseExpression() -> llvm::Optional<Node>;
 

+ 267 - 0
parser/precedence.cpp

@@ -0,0 +1,267 @@
+// 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 "parser/precedence.h"
+
+#include <utility>
+
+namespace Carbon {
+
+namespace {
+enum PrecedenceLevel : int8_t {
+  // Numeric.
+  NumericPrefix,
+  NumericPostfix,
+  Modulo,
+  Multiplicative,
+  Additive,
+  // Bitwise.
+  BitwisePrefix,
+  BitwiseAnd,
+  BitwiseOr,
+  BitwiseXor,
+  BitShift,
+  // Logical.
+  LogicalPrefix,
+  Relational,
+  LogicalAnd,
+  LogicalOr,
+  // Assignment.
+  SimpleAssignment,
+  CompoundAssignment,
+};
+constexpr int8_t NumPrecedenceLevels = CompoundAssignment + 1;
+
+// A precomputed lookup table determining the relative precedence of two
+// precedence groups.
+struct OperatorPriorityTable {
+  constexpr OperatorPriorityTable() : table{} {
+    // Start with a list of <higher precedence>, <lower precedence>
+    // relationships.
+    MarkHigherThan({NumericPrefix, NumericPostfix},
+                   {Modulo, Multiplicative, BitShift});
+    MarkHigherThan({Multiplicative}, {Additive});
+    MarkHigherThan({BitwisePrefix},
+                   {BitwiseAnd, BitwiseOr, BitwiseXor, BitShift});
+    MarkHigherThan(
+        {Modulo, Additive, BitwiseAnd, BitwiseOr, BitwiseXor, BitShift},
+        {SimpleAssignment, CompoundAssignment, Relational});
+    MarkHigherThan({Relational, LogicalPrefix}, {LogicalAnd, LogicalOr});
+
+    // Compute the transitive closure of the above relationships: if we parse
+    // `a $ b @ c` as `(a $ b) @ c` and parse `b @ c % d` as `(b @ c) % d`,
+    // then we will parse `a $ b @ c % d` as `((a $ b) @ c) % d` and should
+    // also parse `a $ bc % d` as `(a $ bc) % d`.
+    MakeTransitivelyClosed();
+
+    // Make the relation symmetric. If we parse `a $ b @ c` as `(a $ b) @ c`
+    // then we want to parse `a @ b $ c` as `a @ (b $ c)`.
+    MakeSymmetric();
+
+    // Fill in the diagonal, which represents operator associativity.
+    AddAssociativityRules();
+  }
+
+  constexpr void MarkHigherThan(
+      std::initializer_list<PrecedenceLevel> higher_group,
+      std::initializer_list<PrecedenceLevel> lower_group) {
+    for (auto higher : higher_group) {
+      for (auto lower : lower_group) {
+        table[higher][lower] = OperatorPriority::LeftFirst;
+      }
+    }
+  }
+
+  constexpr void MakeTransitivelyClosed() {
+    // A naive algorithm compiles acceptably fast for now (~0.5s). This should
+    // be revisited if we see compile time problems after adding precedence
+    // groups; it's easy to do this faster.
+    bool changed = false;
+    do {
+      changed = false;
+      for (int8_t a = 0; a != NumPrecedenceLevels; ++a) {
+        for (int8_t b = 0; b != NumPrecedenceLevels; ++b) {
+          if (table[a][b] == OperatorPriority::LeftFirst) {
+            for (int8_t c = 0; c != NumPrecedenceLevels; ++c) {
+              if (table[b][c] == OperatorPriority::LeftFirst &&
+                  table[a][c] != OperatorPriority::LeftFirst) {
+                table[a][c] = OperatorPriority::LeftFirst;
+                changed = true;
+              }
+            }
+          }
+        }
+      }
+    } while (changed);
+  }
+
+  constexpr void MakeSymmetric() {
+    for (int8_t a = 0; a != NumPrecedenceLevels; ++a) {
+      for (int8_t b = 0; b != NumPrecedenceLevels; ++b) {
+        if (table[a][b] == OperatorPriority::LeftFirst) {
+          if (table[b][a] == OperatorPriority::LeftFirst) {
+            throw "inconsistent lookup table entries";
+          }
+          table[b][a] = OperatorPriority::RightFirst;
+        }
+      }
+    }
+  }
+
+  constexpr void AddAssociativityRules() {
+    // Associativity rules occupy the diagonal
+
+    // For prefix operators, RightFirst would mean `@@x` is `@(@x)` and
+    // Ambiguous would mean it's an error. LeftFirst is meaningless. For now we
+    // allow all prefix operators to be repeated.
+    for (PrecedenceLevel prefix :
+         {NumericPrefix, BitwisePrefix, LogicalPrefix}) {
+      table[prefix][prefix] = OperatorPriority::RightFirst;
+    }
+
+    // Postfix operators are symmetric with prefix operators.
+    for (PrecedenceLevel postfix : {NumericPostfix}) {
+      table[postfix][postfix] = OperatorPriority::LeftFirst;
+    }
+
+    // Traditionally-associative operators are given left-to-right
+    // associativity.
+    for (PrecedenceLevel assoc :
+         {Multiplicative, Additive, BitwiseAnd, BitwiseOr, BitwiseXor,
+          LogicalAnd, LogicalOr}) {
+      table[assoc][assoc] = OperatorPriority::LeftFirst;
+    }
+
+    // Assignment is given right-to-left associativity in order to support
+    // chained assignment.
+    table[SimpleAssignment][SimpleAssignment] = OperatorPriority::RightFirst;
+
+    // For other operators, there isn't an obvious answer and we require
+    // explicit parentheses.
+  }
+
+  OperatorPriority table[NumPrecedenceLevels][NumPrecedenceLevels];
+};
+}  // namespace
+
+auto PrecedenceGroup::ForLeading(TokenKind kind)
+    -> llvm::Optional<PrecedenceGroup> {
+  switch (kind) {
+    case TokenKind::NotKeyword():
+      return PrecedenceGroup(LogicalPrefix);
+
+    case TokenKind::Minus():
+    case TokenKind::MinusMinus():
+    case TokenKind::PlusPlus():
+      return PrecedenceGroup(NumericPrefix);
+
+    case TokenKind::Tilde():
+      return PrecedenceGroup(BitwisePrefix);
+
+    default:
+      return llvm::None;
+  }
+}
+
+auto PrecedenceGroup::ForTrailing(TokenKind kind) -> llvm::Optional<Trailing> {
+  switch (kind) {
+    // Assignment operators.
+    case TokenKind::Equal():
+      return Trailing{.level = SimpleAssignment, .is_binary = true};
+    case TokenKind::PlusEqual():
+    case TokenKind::MinusEqual():
+    case TokenKind::StarEqual():
+    case TokenKind::SlashEqual():
+    case TokenKind::PercentEqual():
+    case TokenKind::AmpEqual():
+    case TokenKind::PipeEqual():
+    case TokenKind::GreaterGreaterEqual():
+    case TokenKind::LessLessEqual():
+      return Trailing{.level = CompoundAssignment, .is_binary = true};
+
+    // Logical operators.
+    case TokenKind::AndKeyword():
+      return Trailing{.level = LogicalAnd, .is_binary = true};
+    case TokenKind::OrKeyword():
+      return Trailing{.level = LogicalOr, .is_binary = true};
+
+    // Bitwise operators.
+    case TokenKind::Amp():
+      return Trailing{.level = BitwiseAnd, .is_binary = true};
+    case TokenKind::Pipe():
+      return Trailing{.level = BitwiseOr, .is_binary = true};
+    case TokenKind::XorKeyword():
+      return Trailing{.level = BitwiseXor, .is_binary = true};
+    case TokenKind::GreaterGreater():
+    case TokenKind::LessLess():
+      return Trailing{.level = BitShift, .is_binary = true};
+
+    // Relational operators.
+    case TokenKind::EqualEqual():
+    case TokenKind::ExclaimEqual():
+    case TokenKind::Less():
+    case TokenKind::LessEqual():
+    case TokenKind::Greater():
+    case TokenKind::GreaterEqual():
+    case TokenKind::LessEqualGreater():
+      return Trailing{.level = Relational, .is_binary = true};
+
+    // Addative operators.
+    case TokenKind::Plus():
+    case TokenKind::Minus():
+      return Trailing{.level = Additive, .is_binary = true};
+
+    // Multiplicative operators.
+    case TokenKind::Star():
+    case TokenKind::Slash():
+      return Trailing{.level = Multiplicative, .is_binary = true};
+    case TokenKind::Percent():
+      return Trailing{.level = Modulo, .is_binary = true};
+
+    // Postfix operators.
+    case TokenKind::MinusMinus():
+    case TokenKind::PlusPlus():
+      return Trailing{.level = NumericPostfix, .is_binary = false};
+
+    // Prefix-only operators.
+    case TokenKind::Tilde():
+    case TokenKind::NotKeyword():
+      break;
+
+    // Symbolic tokens that might be operators eventually.
+    case TokenKind::Backslash():
+    case TokenKind::Caret():
+    case TokenKind::CaretEqual():
+    case TokenKind::Comma():
+    case TokenKind::TildeEqual():
+    case TokenKind::Exclaim():
+    case TokenKind::LessGreater():
+    case TokenKind::Question():
+    case TokenKind::Colon():
+      break;
+
+    // Symbolic tokens that are intentionally not operators.
+    case TokenKind::At():
+    case TokenKind::LessMinus():
+    case TokenKind::MinusGreater():
+    case TokenKind::EqualGreater():
+    case TokenKind::ColonEqual():
+    case TokenKind::Period():
+    case TokenKind::Semi():
+      break;
+
+    default:
+      break;
+  }
+
+  return llvm::None;
+}
+
+auto PrecedenceGroup::GetPriority(PrecedenceGroup left, PrecedenceGroup right)
+    -> OperatorPriority {
+  static constexpr OperatorPriorityTable lookup;
+  return lookup.table[left.level][right.level];
+}
+
+}  // namespace Carbon

+ 83 - 0
parser/precedence.h

@@ -0,0 +1,83 @@
+// 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 PARSER_PRECEDENCE_H_
+#define PARSER_PRECEDENCE_H_
+
+#include "lexer/token_kind.h"
+#include "llvm/ADT/Optional.h"
+
+namespace Carbon {
+
+// Given two operators `$` and `@`, and an expression `a $ b @ c`, how should
+// the expression be parsed?
+enum class OperatorPriority : int8_t {
+  // The left operator has higher precedence: `(a $ b) @ c`.
+  LeftFirst = -1,
+  // The expression is ambiguous.
+  Ambiguous = 0,
+  // The right operator has higher precedence: `a $ (b @ c)`.
+  RightFirst = 1,
+};
+
+enum class Associativity : int8_t {
+  LeftToRight = -1,
+  None = 0,
+  RightToLeft = 1
+};
+
+// A precedence group associated with an operator or expression.
+class PrecedenceGroup {
+ private:
+  PrecedenceGroup(int8_t level) : level(level) {}
+
+ public:
+  // Objects of this type should only be constructed using the static factory
+  // functions below.
+  PrecedenceGroup() = delete;
+
+  // Look up the operator information of the given prefix operator token, or
+  // return llvm::None if the given token is not a prefix operator.
+  static auto ForLeading(TokenKind kind) -> llvm::Optional<PrecedenceGroup>;
+
+  struct Trailing;
+
+  // Look up the operator information of the given infix or postfix operator
+  // token, or return llvm::None if the given token is not an infix or postfix
+  // operator.
+  static auto ForTrailing(TokenKind kind) -> llvm::Optional<Trailing>;
+
+  friend auto operator==(PrecedenceGroup lhs, PrecedenceGroup rhs) -> bool {
+    return lhs.level == rhs.level;
+  }
+  friend auto operator!=(PrecedenceGroup lhs, PrecedenceGroup rhs) -> bool {
+    return lhs.level != rhs.level;
+  }
+
+  // Compare the precedence levels for two adjacent operators.
+  static auto GetPriority(PrecedenceGroup left, PrecedenceGroup right)
+      -> OperatorPriority;
+
+  // Get the associativity of this precedence group.
+  Associativity GetAssociativity() const {
+    return static_cast<Associativity>(GetPriority(*this, *this));
+  }
+
+ private:
+  // The precedence level.
+  int8_t level;
+};
+
+// Precedence information for a trailing operator.
+struct PrecedenceGroup::Trailing {
+  // The precedence level.
+  PrecedenceGroup level;
+  // `true` if this is an infix binary operator, `false` if this is a postfix
+  // unary operator.
+  bool is_binary;
+};
+
+}  // namespace Carbon
+
+#endif  // PARSER_PRECEDENCE_H_

+ 116 - 0
parser/precedence_test.cpp

@@ -0,0 +1,116 @@
+// 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 "parser/precedence.h"
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "lexer/token_kind.h"
+
+namespace Carbon {
+namespace {
+
+using ::testing::Eq;
+using ::testing::Ne;
+
+TEST(PrecedenceTest, OperatorsAreRecognized) {
+  EXPECT_TRUE(PrecedenceGroup::ForLeading(TokenKind::Minus()).hasValue());
+  EXPECT_TRUE(PrecedenceGroup::ForLeading(TokenKind::Tilde()).hasValue());
+  EXPECT_FALSE(PrecedenceGroup::ForLeading(TokenKind::Slash()).hasValue());
+  EXPECT_FALSE(PrecedenceGroup::ForLeading(TokenKind::Identifier()).hasValue());
+
+  EXPECT_TRUE(PrecedenceGroup::ForTrailing(TokenKind::Minus()).hasValue());
+  EXPECT_FALSE(PrecedenceGroup::ForTrailing(TokenKind::Tilde()).hasValue());
+  EXPECT_TRUE(PrecedenceGroup::ForTrailing(TokenKind::Slash()).hasValue());
+  EXPECT_FALSE(
+      PrecedenceGroup::ForTrailing(TokenKind::Identifier()).hasValue());
+
+  EXPECT_TRUE(PrecedenceGroup::ForTrailing(TokenKind::Minus())->is_binary);
+  EXPECT_FALSE(
+      PrecedenceGroup::ForTrailing(TokenKind::MinusMinus())->is_binary);
+}
+
+TEST(PrecedenceTest, Associativity) {
+  EXPECT_THAT(
+      PrecedenceGroup::ForLeading(TokenKind::Minus())->GetAssociativity(),
+      Eq(Associativity::RightToLeft));
+  EXPECT_THAT(PrecedenceGroup::ForTrailing(TokenKind::PlusPlus())
+                  ->level.GetAssociativity(),
+              Eq(Associativity::LeftToRight));
+  EXPECT_THAT(
+      PrecedenceGroup::ForTrailing(TokenKind::Plus())->level.GetAssociativity(),
+      Eq(Associativity::LeftToRight));
+  EXPECT_THAT(PrecedenceGroup::ForTrailing(TokenKind::Equal())
+                  ->level.GetAssociativity(),
+              Eq(Associativity::RightToLeft));
+  EXPECT_THAT(PrecedenceGroup::ForTrailing(TokenKind::PlusEqual())
+                  ->level.GetAssociativity(),
+              Eq(Associativity::None));
+}
+
+TEST(PrecedenceTest, DirectRelations) {
+  EXPECT_THAT(PrecedenceGroup::GetPriority(
+                  PrecedenceGroup::ForTrailing(TokenKind::Star())->level,
+                  PrecedenceGroup::ForTrailing(TokenKind::Plus())->level),
+              Eq(OperatorPriority::LeftFirst));
+  EXPECT_THAT(PrecedenceGroup::GetPriority(
+                  PrecedenceGroup::ForTrailing(TokenKind::Plus())->level,
+                  PrecedenceGroup::ForTrailing(TokenKind::Star())->level),
+              Eq(OperatorPriority::RightFirst));
+
+  EXPECT_THAT(PrecedenceGroup::GetPriority(
+                  PrecedenceGroup::ForTrailing(TokenKind::Amp())->level,
+                  PrecedenceGroup::ForTrailing(TokenKind::Less())->level),
+              Eq(OperatorPriority::LeftFirst));
+  EXPECT_THAT(PrecedenceGroup::GetPriority(
+                  PrecedenceGroup::ForTrailing(TokenKind::Less())->level,
+                  PrecedenceGroup::ForTrailing(TokenKind::Amp())->level),
+              Eq(OperatorPriority::RightFirst));
+}
+
+TEST(PrecedenceTest, IndirectRelations) {
+  EXPECT_THAT(PrecedenceGroup::GetPriority(
+                  PrecedenceGroup::ForTrailing(TokenKind::Star())->level,
+                  PrecedenceGroup::ForTrailing(TokenKind::OrKeyword())->level),
+              Eq(OperatorPriority::LeftFirst));
+  EXPECT_THAT(PrecedenceGroup::GetPriority(
+                  PrecedenceGroup::ForTrailing(TokenKind::OrKeyword())->level,
+                  PrecedenceGroup::ForTrailing(TokenKind::Star())->level),
+              Eq(OperatorPriority::RightFirst));
+
+  EXPECT_THAT(PrecedenceGroup::GetPriority(
+                  *PrecedenceGroup::ForLeading(TokenKind::Tilde()),
+                  PrecedenceGroup::ForTrailing(TokenKind::Equal())->level),
+              Eq(OperatorPriority::LeftFirst));
+  EXPECT_THAT(PrecedenceGroup::GetPriority(
+                  PrecedenceGroup::ForTrailing(TokenKind::Equal())->level,
+                  *PrecedenceGroup::ForLeading(TokenKind::Tilde())),
+              Eq(OperatorPriority::RightFirst));
+}
+
+TEST(PrecedenceTest, IncomparableOperators) {
+  EXPECT_THAT(PrecedenceGroup::GetPriority(
+                  *PrecedenceGroup::ForLeading(TokenKind::Tilde()),
+                  *PrecedenceGroup::ForLeading(TokenKind::NotKeyword())),
+              Eq(OperatorPriority::Ambiguous));
+  EXPECT_THAT(PrecedenceGroup::GetPriority(
+                  *PrecedenceGroup::ForLeading(TokenKind::Tilde()),
+                  *PrecedenceGroup::ForLeading(TokenKind::Minus())),
+              Eq(OperatorPriority::Ambiguous));
+  EXPECT_THAT(PrecedenceGroup::GetPriority(
+                  *PrecedenceGroup::ForLeading(TokenKind::Minus()),
+                  PrecedenceGroup::ForTrailing(TokenKind::Amp())->level),
+              Eq(OperatorPriority::Ambiguous));
+  EXPECT_THAT(PrecedenceGroup::GetPriority(
+                  PrecedenceGroup::ForTrailing(TokenKind::Equal())->level,
+                  PrecedenceGroup::ForTrailing(TokenKind::PipeEqual())->level),
+              Eq(OperatorPriority::Ambiguous));
+  EXPECT_THAT(PrecedenceGroup::GetPriority(
+                  PrecedenceGroup::ForTrailing(TokenKind::Plus())->level,
+                  PrecedenceGroup::ForTrailing(TokenKind::Amp())->level),
+              Eq(OperatorPriority::Ambiguous));
+}
+
+}  // namespace
+}  // namespace Carbon