Przeglądaj źródła

Add `match` parsing (#3664)

Adds `match` parsing to toolchain
czapiga 2 lat temu
rodzic
commit
99bc9734d9

+ 95 - 0
toolchain/check/handle_match.cpp

@@ -0,0 +1,95 @@
+// 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 "toolchain/check/context.h"
+#include "toolchain/check/convert.h"
+
+namespace Carbon::Check {
+
+auto HandleMatchConditionStart(Context& context,
+                               Parse::MatchConditionStartId parse_node)
+    -> bool {
+  return context.TODO(parse_node, "HandleMatchConditionStart");
+}
+
+auto HandleMatchCondition(Context& context, Parse::MatchConditionId parse_node)
+    -> bool {
+  return context.TODO(parse_node, "HandleMatchCondition");
+}
+
+auto HandleMatchIntroducer(Context& context,
+                           Parse::MatchIntroducerId parse_node) -> bool {
+  return context.TODO(parse_node, "HandleMatchIntroducer");
+}
+
+auto HandleMatchStatementStart(Context& context,
+                               Parse::MatchStatementStartId parse_node)
+    -> bool {
+  return context.TODO(parse_node, "HandleMatchStatementStart");
+}
+
+auto HandleMatchCaseIntroducer(Context& context,
+                               Parse::MatchCaseIntroducerId parse_node)
+    -> bool {
+  return context.TODO(parse_node, "HandleMatchCaseIntroducer");
+}
+
+auto HandleMatchCaseGuardIntroducer(
+    Context& context, Parse::MatchCaseGuardIntroducerId parse_node) -> bool {
+  return context.TODO(parse_node, "HandleMatchCaseGuardIntroducer");
+}
+
+auto HandleMatchCaseGuardStart(Context& context,
+                               Parse::MatchCaseGuardStartId parse_node)
+    -> bool {
+  return context.TODO(parse_node, "HandleMatchCaseGuardStart");
+}
+
+auto HandleMatchCaseGuard(Context& context, Parse::MatchCaseGuardId parse_node)
+    -> bool {
+  return context.TODO(parse_node, "HandleMatchCaseGuard");
+}
+
+auto HandleMatchCaseEqualGreater(Context& context,
+                                 Parse::MatchCaseEqualGreaterId parse_node)
+    -> bool {
+  return context.TODO(parse_node, "HandleMatchCaseEqualGreater");
+}
+
+auto HandleMatchCaseStart(Context& context, Parse::MatchCaseStartId parse_node)
+    -> bool {
+  return context.TODO(parse_node, "HandleMatchCaseStart");
+}
+
+auto HandleMatchCase(Context& context, Parse::MatchCaseId parse_node) -> bool {
+  return context.TODO(parse_node, "HandleMatchCase");
+}
+
+auto HandleMatchDefaultIntroducer(Context& context,
+                                  Parse::MatchDefaultIntroducerId parse_node)
+    -> bool {
+  return context.TODO(parse_node, "MatchDefaultIntroducer");
+}
+
+auto HandleMatchDefaultEqualGreater(
+    Context& context, Parse::MatchDefaultEqualGreaterId parse_node) -> bool {
+  return context.TODO(parse_node, "MatchDefaultEqualGreater");
+}
+
+auto HandleMatchDefaultStart(Context& context,
+                             Parse::MatchDefaultStartId parse_node) -> bool {
+  return context.TODO(parse_node, "HandleMatchDefaultStart");
+}
+
+auto HandleMatchDefault(Context& context, Parse::MatchDefaultId parse_node)
+    -> bool {
+  return context.TODO(parse_node, "HandleMatchDefault");
+}
+
+auto HandleMatchStatement(Context& context, Parse::MatchStatementId parse_node)
+    -> bool {
+  return context.TODO(parse_node, "HandleMatchStatement");
+}
+
+}  // namespace Carbon::Check

+ 6 - 0
toolchain/diagnostics/diagnostic_kind.def

@@ -73,6 +73,12 @@ CARBON_DIAGNOSTIC_KIND(ExpectedVarAfterReturned)
 CARBON_DIAGNOSTIC_KIND(ExpectedVariableDecl)
 CARBON_DIAGNOSTIC_KIND(ExpectedChoiceDefinition)
 CARBON_DIAGNOSTIC_KIND(ExpectedChoiceAlternativeName)
+CARBON_DIAGNOSTIC_KIND(ExpectedMatchCases)
+CARBON_DIAGNOSTIC_KIND(ExpectedMatchCasesBlock)
+CARBON_DIAGNOSTIC_KIND(ExpectedMatchCaseArrow)
+CARBON_DIAGNOSTIC_KIND(ExpectedMatchCaseBlock)
+CARBON_DIAGNOSTIC_KIND(UnexpectedTokenInMatchCasesBlock)
+CARBON_DIAGNOSTIC_KIND(UnreachableMatchCase)
 CARBON_DIAGNOSTIC_KIND(OperatorRequiresParentheses)
 CARBON_DIAGNOSTIC_KIND(StatementOperatorAsSubExpr)
 CARBON_DIAGNOSTIC_KIND(UnaryOperatorRequiresParentheses)

+ 232 - 0
toolchain/parse/handle_match.cpp

@@ -0,0 +1,232 @@
+// 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 "toolchain/parse/context.h"
+
+namespace Carbon::Parse {
+
+static auto HandleStatementsBlockStart(Context& context, State finish,
+                                       NodeKind equal_greater, NodeKind starter,
+                                       NodeKind complete) -> void {
+  auto state = context.PopState();
+
+  if (!context.PositionIs(Lex::TokenKind::EqualGreater)) {
+    if (!state.has_error) {
+      CARBON_DIAGNOSTIC(ExpectedMatchCaseArrow, Error,
+                        "Expected `=>` introducing statement block.");
+      context.emitter().Emit(*context.position(), ExpectedMatchCaseArrow);
+    }
+
+    context.AddLeafNode(equal_greater, *context.position(), true);
+    context.AddNode(starter, *context.position(), state.subtree_start, true);
+    context.AddNode(complete, *context.position(), state.subtree_start, true);
+    context.SkipPastLikelyEnd(*context.position());
+    return;
+  }
+
+  context.AddLeafNode(equal_greater, context.Consume());
+
+  if (!context.PositionIs(Lex::TokenKind::OpenCurlyBrace)) {
+    if (!state.has_error) {
+      CARBON_DIAGNOSTIC(ExpectedMatchCaseBlock, Error,
+                        "Expected `{{` after `=>`.");
+      context.emitter().Emit(*context.position(), ExpectedMatchCaseBlock);
+    }
+
+    context.AddNode(starter, *context.position(), state.subtree_start, true);
+    context.AddNode(complete, *context.position(), state.subtree_start, true);
+    context.SkipPastLikelyEnd(*context.position());
+    return;
+  }
+
+  context.AddNode(starter, context.Consume(), state.subtree_start,
+                  state.has_error);
+  context.PushState(state, finish);
+  context.PushState(State::StatementScopeLoop);
+}
+
+static auto EmitUnexpectedTokenAndRecover(Context& context) -> void {
+  CARBON_DIAGNOSTIC(UnexpectedTokenInMatchCasesBlock, Error,
+                    "Unexpected `{0}`; expected `case`, `default` or `}`.",
+                    Lex::TokenKind);
+  context.emitter().Emit(*context.position(), UnexpectedTokenInMatchCasesBlock,
+                         context.PositionKind());
+  context.ReturnErrorOnState();
+  context.SkipPastLikelyEnd(*context.position());
+}
+
+auto HandleMatchIntroducer(Context& context) -> void {
+  auto state = context.PopState();
+  context.AddLeafNode(NodeKind::Placeholder, *context.position());
+  context.PushState(state, State::MatchConditionFinish);
+  context.PushState(State::ParenConditionAsMatch);
+  context.ConsumeAndDiscard();
+}
+
+auto HandleMatchConditionFinish(Context& context) -> void {
+  auto state = context.PopState();
+  context.ReplacePlaceholderNode(state.subtree_start, NodeKind::MatchIntroducer,
+                                 state.token);
+
+  if (!context.PositionIs(Lex::TokenKind::OpenCurlyBrace)) {
+    if (!state.has_error) {
+      CARBON_DIAGNOSTIC(ExpectedMatchCasesBlock, Error,
+                        "Expected `{{` starting block with cases.");
+      context.emitter().Emit(*context.position(), ExpectedMatchCasesBlock);
+    }
+
+    context.AddNode(NodeKind::MatchStatementStart, *context.position(),
+                    state.subtree_start, true);
+    context.AddNode(NodeKind::MatchStatement, *context.position(),
+                    state.subtree_start, true);
+    context.SkipPastLikelyEnd(*context.position());
+    return;
+  }
+
+  context.AddNode(NodeKind::MatchStatementStart, context.Consume(),
+                  state.subtree_start, state.has_error);
+
+  state.has_error = false;
+  if (context.PositionIs(Lex::TokenKind::CloseCurlyBrace)) {
+    CARBON_DIAGNOSTIC(ExpectedMatchCases, Error, "Expected cases.");
+    context.emitter().Emit(*context.position(), ExpectedMatchCases);
+    state.has_error = true;
+  }
+
+  context.PushState(state, State::MatchStatementFinish);
+  context.PushState(State::MatchCaseLoop);
+}
+
+auto HandleMatchCaseLoop(Context& context) -> void {
+  context.PopAndDiscardState();
+
+  if (context.PositionIs(Lex::TokenKind::Case)) {
+    context.PushState(State::MatchCaseLoop);
+    context.PushState(State::MatchCaseIntroducer);
+  } else if (context.PositionIs(Lex::TokenKind::Default)) {
+    context.PushState(State::MatchCaseLoopAfterDefault);
+    context.PushState(State::MatchDefaultIntroducer);
+  } else if (!context.PositionIs(Lex::TokenKind::CloseCurlyBrace)) {
+    EmitUnexpectedTokenAndRecover(context);
+    context.PushState(State::MatchCaseLoop);
+  }
+}
+
+auto HandleMatchCaseLoopAfterDefault(Context& context) -> void {
+  context.PopAndDiscardState();
+
+  Lex::TokenKind kind = context.PositionKind();
+  if (kind == Lex::TokenKind::Case or kind == Lex::TokenKind::Default) {
+    CARBON_DIAGNOSTIC(UnreachableMatchCase, Error, "Unreachable case; `{0}{1}",
+                      Lex::TokenKind, std::string);
+    context.emitter().Emit(*context.position(), UnreachableMatchCase, kind,
+                           "` occurs after the `default`");
+
+    context.ReturnErrorOnState();
+    context.PushState(State::MatchCaseLoopAfterDefault);
+    context.SkipPastLikelyEnd(*context.position());
+    return;
+  } else if (kind != Lex::TokenKind::CloseCurlyBrace) {
+    EmitUnexpectedTokenAndRecover(context);
+    context.PushState(State::MatchCaseLoopAfterDefault);
+  }
+}
+
+auto HandleMatchCaseIntroducer(Context& context) -> void {
+  auto state = context.PopState();
+
+  context.AddLeafNode(NodeKind::MatchCaseIntroducer, context.Consume());
+  context.PushState(state, State::MatchCaseAfterPattern);
+  context.PushState(State::Pattern);
+}
+
+auto HandleMatchCaseAfterPattern(Context& context) -> void {
+  auto state = context.PopState();
+  if (state.has_error) {
+    context.AddNode(NodeKind::MatchCaseStart, *context.position(),
+                    state.subtree_start, true);
+    context.AddNode(NodeKind::MatchCase, *context.position(),
+                    state.subtree_start, true);
+    context.SkipPastLikelyEnd(*context.position());
+    return;
+  }
+
+  context.PushState(state, State::MatchCaseStart);
+  if (context.PositionIs(Lex::TokenKind::If)) {
+    context.PushState(State::MatchCaseGuardFinish);
+    context.AddLeafNode(NodeKind::MatchCaseGuardIntroducer, context.Consume());
+    auto open_paren = context.ConsumeIf(Lex::TokenKind::OpenParen);
+    if (open_paren) {
+      context.AddLeafNode(NodeKind::MatchCaseGuardStart, *open_paren);
+      context.PushState(State::Expr);
+    } else {
+      context.AddLeafNode(NodeKind::MatchCaseGuardStart, *context.position(),
+                          true);
+      context.AddLeafNode(NodeKind::InvalidParse, *context.position(), true);
+      state = context.PopState();
+      context.AddNode(NodeKind::MatchCaseGuard, *context.position(),
+                      state.subtree_start, true);
+      state = context.PopState();
+      context.AddNode(NodeKind::MatchCaseStart, *context.position(),
+                      state.subtree_start, true);
+      context.AddNode(NodeKind::MatchCase, *context.position(),
+                      state.subtree_start, true);
+      context.SkipPastLikelyEnd(*context.position());
+      return;
+    }
+  }
+}
+
+auto HandleMatchCaseGuardFinish(Context& context) -> void {
+  auto state = context.PopState();
+
+  auto close_paren = context.ConsumeIf(Lex::TokenKind::CloseParen);
+  if (close_paren) {
+    context.AddNode(NodeKind::MatchCaseGuard, *close_paren, state.subtree_start,
+                    state.has_error);
+  } else {
+    context.AddNode(NodeKind::MatchCaseGuard, *context.position(),
+                    state.subtree_start, true);
+    context.ReturnErrorOnState();
+    context.SkipPastLikelyEnd(*context.position());
+    return;
+  }
+}
+
+auto HandleMatchCaseStart(Context& context) -> void {
+  HandleStatementsBlockStart(context, State::MatchCaseFinish,
+                             NodeKind::MatchCaseEqualGreater,
+                             NodeKind::MatchCaseStart, NodeKind::MatchCase);
+}
+
+auto HandleMatchCaseFinish(Context& context) -> void {
+  auto state = context.PopState();
+  context.AddNode(NodeKind::MatchCase,
+                  context.ConsumeChecked(Lex::TokenKind::CloseCurlyBrace),
+                  state.subtree_start, state.has_error);
+}
+
+auto HandleMatchDefaultIntroducer(Context& context) -> void {
+  context.AddLeafNode(NodeKind::MatchDefaultIntroducer, context.Consume());
+
+  HandleStatementsBlockStart(
+      context, State::MatchDefaultFinish, NodeKind::MatchDefaultEqualGreater,
+      NodeKind::MatchDefaultStart, NodeKind::MatchDefault);
+}
+
+auto HandleMatchDefaultFinish(Context& context) -> void {
+  auto state = context.PopState();
+  context.AddNode(NodeKind::MatchDefault,
+                  context.ConsumeChecked(Lex::TokenKind::CloseCurlyBrace),
+                  state.subtree_start, state.has_error);
+}
+
+auto HandleMatchStatementFinish(Context& context) -> void {
+  auto state = context.PopState();
+  context.AddNode(NodeKind::MatchStatement,
+                  context.ConsumeChecked(Lex::TokenKind::CloseCurlyBrace),
+                  state.subtree_start, state.has_error);
+}
+
+}  // namespace Carbon::Parse

+ 13 - 1
toolchain/parse/handle_paren_condition.cpp

@@ -6,7 +6,7 @@
 
 namespace Carbon::Parse {
 
-// Handles ParenConditionAs(If|While).
+// Handles ParenConditionAs(If|While|Match).
 static auto HandleParenCondition(Context& context, NodeKind start_kind,
                                  State finish_state) -> void {
   auto state = context.PopState();
@@ -39,6 +39,11 @@ auto HandleParenConditionAsWhile(Context& context) -> void {
                        State::ParenConditionFinishAsWhile);
 }
 
+auto HandleParenConditionAsMatch(Context& context) -> void {
+  HandleParenCondition(context, NodeKind::MatchConditionStart,
+                       State::ParenConditionFinishAsMatch);
+}
+
 auto HandleParenConditionFinishAsIf(Context& context) -> void {
   auto state = context.PopState();
 
@@ -52,4 +57,11 @@ auto HandleParenConditionFinishAsWhile(Context& context) -> void {
                                    NodeKind::WhileCondition);
 }
 
+auto HandleParenConditionFinishAsMatch(Context& context) -> void {
+  auto state = context.PopState();
+
+  context.ConsumeAndAddCloseSymbol(state.token, state,
+                                   NodeKind::MatchCondition);
+}
+
 }  // namespace Carbon::Parse

+ 4 - 0
toolchain/parse/handle_statement.cpp

@@ -52,6 +52,10 @@ auto HandleStatement(Context& context) -> void {
       context.PushState(State::StatementWhile);
       break;
     }
+    case Lex::TokenKind::Match: {
+      context.PushState(State::MatchIntroducer);
+      break;
+    }
     default: {
       context.PushState(State::ExprStatementFinish);
       context.PushStateForExpr(PrecedenceGroup::ForExprStatement());

+ 58 - 0
toolchain/parse/node_kind.def

@@ -737,6 +737,64 @@ CARBON_PARSE_NODE_KIND_BRACKET(ChoiceDefinition, ChoiceDefinitionStart,
 // ChoiceAlternativeList
 CARBON_PARSE_NODE_KIND_CHILD_COUNT(ChoiceAlternativeListComma, 0, Comma)
 
+// `match`:
+//     MatchIntroducer
+//       MatchConditionStart
+//       _external_: expression
+//     MatchCondition
+//   MatchStatementStart
+//   _repeated_ _external_: MatchCase
+//   _optional_ _external_: MatchStatementDefault
+// MatchStatement
+CARBON_PARSE_NODE_KIND_CHILD_COUNT(MatchIntroducer, 0, Match)
+CARBON_PARSE_NODE_KIND_CHILD_COUNT(MatchConditionStart, 0,
+                                   CARBON_IF_VALID(OpenParen))
+CARBON_PARSE_NODE_KIND_BRACKET(MatchCondition, MatchConditionStart,
+                               CARBON_IF_VALID(CloseParen))
+CARBON_PARSE_NODE_KIND_BRACKET(MatchStatementStart, MatchIntroducer,
+                               CARBON_IF_VALID(OpenCurlyBrace))
+CARBON_PARSE_NODE_KIND_BRACKET(MatchStatement, MatchStatementStart,
+                               CARBON_IF_VALID(CloseCurlyBrace))
+
+// `case`:
+//     MatchCaseIntroducer
+//     _external_: Pattern
+//       MatchCaseGuardIntroducer
+//       MatchCaseGuardStart
+//       _external_: expression
+//     MatchCaseGuard
+//     MatchCaseEqualGreater
+//   MatchCaseStart
+//   _repeated_ _external_: statement
+// MatchCase
+CARBON_PARSE_NODE_KIND_CHILD_COUNT(MatchCaseIntroducer, 0, Case)
+CARBON_PARSE_NODE_KIND_CHILD_COUNT(MatchCaseGuardIntroducer, 0, If)
+CARBON_PARSE_NODE_KIND_CHILD_COUNT(MatchCaseGuardStart, 0,
+                                   CARBON_IF_VALID(OpenParen))
+CARBON_PARSE_NODE_KIND_BRACKET(MatchCaseGuard,
+                               MatchCaseGuardIntroducer,
+                               CARBON_IF_VALID(CloseParen))
+CARBON_PARSE_NODE_KIND_CHILD_COUNT(MatchCaseEqualGreater, 0,
+                                   CARBON_IF_VALID(EqualGreater))
+CARBON_PARSE_NODE_KIND_BRACKET(MatchCaseStart, MatchCaseIntroducer,
+                                   CARBON_IF_VALID(OpenCurlyBrace))
+CARBON_PARSE_NODE_KIND_BRACKET(MatchCase, MatchCaseStart,
+                                   CARBON_IF_VALID(CloseCurlyBrace))
+
+// `default`:
+//     MatchDefaultIntroducer
+//     MatchDefaultEqualGreater
+//   MatchDefaultStart
+//   _repeated_ _external_: statement
+// MatchDefault
+CARBON_PARSE_NODE_KIND_CHILD_COUNT(MatchDefaultIntroducer, 0, Default)
+CARBON_PARSE_NODE_KIND_CHILD_COUNT(MatchDefaultEqualGreater, 0,
+                                   CARBON_IF_VALID(EqualGreater))
+CARBON_PARSE_NODE_KIND_BRACKET(MatchDefaultStart, MatchDefaultIntroducer,
+                               CARBON_IF_VALID(OpenCurlyBrace))
+CARBON_PARSE_NODE_KIND_BRACKET(MatchDefault, MatchDefaultStart,
+                               CARBON_IF_VALID(CloseCurlyBrace))
+
 #undef CARBON_PARSE_NODE_KIND
 #undef CARBON_PARSE_NODE_KIND_BRACKET
 #undef CARBON_PARSE_NODE_KIND_CHILD_COUNT

+ 147 - 16
toolchain/parse/state.def

@@ -730,28 +730,28 @@ CARBON_PARSE_STATE_VARIANTS2(PatternListFinish, Implicit, Tuple)
 
 // Handles the processing of a `(condition)` up through the expression.
 //
-// if/while {    (invalid)
-//         ^
-//   1. ParenConditionAs(If|While)Finish
+// if/while/match {    (invalid)
+//               ^
+//   1. ParenConditionAs(If|While|Match)Finish
 //
-// if/while ( ... )
-//          ^
-// if/while ???
-//         ^
+// if/while/match ( ... )
+//                ^
+// if/while/match ???
+//               ^
 //   1. Expr
-//   2. ParenConditionAs(If|While)Finish
-CARBON_PARSE_STATE_VARIANTS2(ParenCondition, If, While)
+//   2. ParenConditionAs(If|While|Match)Finish
+CARBON_PARSE_STATE_VARIANTS3(ParenCondition, If, While, Match)
 
 // Finishes the processing of a `(condition)` after the expression.
 //
-// if/while ( expr )
-//                 ^
-// if/while {
-//         ^
-// if/while ??? {
-//             ^
+// if/while/match ( expr )
+//                       ^
+// if/while/match {
+//               ^
+// if/while/match ??? {
+//                   ^
 //   (state done)
-CARBON_PARSE_STATE_VARIANTS2(ParenConditionFinish, If, While)
+CARBON_PARSE_STATE_VARIANTS3(ParenConditionFinish, If, While, Match)
 
 // Handles the `(` of a parenthesized single expression
 //
@@ -922,6 +922,10 @@ CARBON_PARSE_STATE_VARIANTS2(BindingPatternFinish, Generic, Regular)
 // ^
 //   1. Expr
 //   2. ExprStatementFinish
+//
+//  match ...
+// ^
+//   1. MatchIntroducer
 CARBON_PARSE_STATE(Statement)
 
 // Handles `break` processing at the `;`.
@@ -1304,4 +1308,131 @@ CARBON_PARSE_STATE(ChoiceAlternativeFinish)
 //   (state done)
 CARBON_PARSE_STATE(ChoiceDefinitionFinish)
 
+// Handles `match` introducer.
+//
+// match ...
+// ^~~~~
+//   1. ParenConditionAsMatch
+//   2. MatchConditionFinish
+CARBON_PARSE_STATE(MatchIntroducer)
+
+// Handles `match` cases block start after the condition.
+//
+// match (...) { ... }
+//             ^
+//   1. MatchCaseLoop
+//   2. MatchStatementFinish
+//
+// match (...) ???
+//             ^
+//   (state done)
+CARBON_PARSE_STATE(MatchConditionFinish)
+
+// Handles `match` cases.
+//
+// match (...) { case ...}
+//              ^
+//   1. MatchCaseIntroducer
+//   2. MatchCaseLoop
+// match (...) { default ...}
+//              ^
+//   1. MatchDefaultIntroducer
+//   2. MatchCaseLoopAfterDefault
+CARBON_PARSE_STATE(MatchCaseLoop)
+
+// Handles `match` `case` introducer.
+//
+// match (...) { case ...}
+//               ^~~~
+//   1. Pattern
+//   2. MatchCaseAfterPattern
+CARBON_PARSE_STATE(MatchCaseIntroducer)
+
+// Handles `match` case after pattern.
+//
+// match (...) { case ... => ... }
+//                       ^
+//   1. MatchCaseStart
+// match (...) { case ... if (...) }
+//                        ^~~~
+//   1. Expr
+//   2. MatchCaseGuardFinish
+//   3. MatchCaseStart
+CARBON_PARSE_STATE(MatchCaseAfterPattern)
+
+// Handles `match` case guard closing parenthesis.
+//
+// match (...) { case ... if (...) => ... }
+//                               ^
+//   (state done)
+CARBON_PARSE_STATE(MatchCaseGuardFinish)
+
+// Handles `match` case `=>` and `{` opening statements block.
+//
+// match (...) { case ... => {...} }
+//                        ^~~~
+//   1. StatementScopeLoop
+//   2. MatchCaseFinish
+//
+// match (...) { case ... ??? }
+//                        ^
+//   (state done)
+//
+// match (...) { case ... => ??? }
+//                           ^
+//   (state done)
+CARBON_PARSE_STATE(MatchCaseStart)
+
+// Handles `match` case statements block closing `}`.
+//
+// match (...) { case ... => {...} }
+//                               ^
+//   (state done)
+CARBON_PARSE_STATE(MatchCaseFinish)
+
+// Handles `match` default introducer, `=>` and `{` opening statements block.
+//
+// match (...) { default => {...} }
+//               ^~~~~~~~~~~~
+//   1. StatementScopeLoop
+//   2. MatchDefaultFinish
+//
+// match (...) { default ??? }
+//                       ^
+//   (state done)
+//
+// match (...) { default => ??? }
+//                          ^
+//   (state done)
+CARBON_PARSE_STATE(MatchDefaultIntroducer)
+
+// Handles `match` default case statements block closing `}`.
+//
+// match (...) { default => {...} }
+//                              ^
+//   (state done)
+CARBON_PARSE_STATE(MatchDefaultFinish)
+
+// Handles `match` cases after the `default` case.
+//
+// match (...) { default => {...} case ... }
+//                                ^~~~
+//   1. MatchCaseLoopAfterDefault
+//
+// match (...) { default => {...} default ... }
+//                                ^~~~~~~
+//   1. MatchCaseLoopAfterDefault
+//
+// match (...) { default => {...} }
+//                                ^
+//   (state done)
+CARBON_PARSE_STATE(MatchCaseLoopAfterDefault)
+
+// Finishes `match` statement.
+//
+// match (...) {...}
+//                 ^
+//   (state done)
+CARBON_PARSE_STATE(MatchStatementFinish)
+
 #undef CARBON_PARSE_STATE

+ 62 - 0
toolchain/parse/testdata/match/fail_cases_after_default.carbon

@@ -0,0 +1,62 @@
+// 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
+//
+// AUTOUPDATE
+
+fn f() -> i32 {
+  var x: i32 = 3;
+  match (x) {
+    default => { return 1; }
+    // CHECK:STDERR: fail_cases_after_default.carbon:[[@LINE+3]]:5: ERROR: Unreachable case; `case` occurs after the `default`
+    // CHECK:STDERR:     case a: i32 => { return 2; }
+    // CHECK:STDERR:     ^~~~
+    case a: i32 => { return 2; }
+    // CHECK:STDERR: fail_cases_after_default.carbon:[[@LINE+3]]:5: ERROR: Unreachable case; `default` occurs after the `default`
+    // CHECK:STDERR:     default => { return 3; }
+    // CHECK:STDERR:     ^~~~~~~
+    default => { return 3; }
+    // CHECK:STDERR: fail_cases_after_default.carbon:[[@LINE+3]]:5: ERROR: Unreachable case; `case` occurs after the `default`
+    // CHECK:STDERR:     case (a: i32) if (a == 4) => { return 4; }
+    // CHECK:STDERR:     ^~~~
+    case (a: i32) if (a == 4) => { return 4; }
+  }
+  return 0;
+}
+
+// CHECK:STDOUT: - filename: fail_cases_after_default.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:         {kind: 'FunctionIntroducer', text: 'fn'},
+// CHECK:STDOUT:         {kind: 'IdentifierName', text: 'f'},
+// CHECK:STDOUT:           {kind: 'TuplePatternStart', text: '('},
+// CHECK:STDOUT:         {kind: 'TuplePattern', text: ')', subtree_size: 2},
+// CHECK:STDOUT:           {kind: 'IntTypeLiteral', text: 'i32'},
+// CHECK:STDOUT:         {kind: 'ReturnType', text: '->', subtree_size: 2},
+// CHECK:STDOUT:       {kind: 'FunctionDefinitionStart', text: '{', subtree_size: 7},
+// CHECK:STDOUT:         {kind: 'VariableIntroducer', text: 'var'},
+// CHECK:STDOUT:           {kind: 'IdentifierName', text: 'x'},
+// CHECK:STDOUT:           {kind: 'IntTypeLiteral', text: 'i32'},
+// CHECK:STDOUT:         {kind: 'BindingPattern', text: ':', subtree_size: 3},
+// CHECK:STDOUT:           {kind: 'IntLiteral', text: '3'},
+// CHECK:STDOUT:         {kind: 'VariableInitializer', text: '=', subtree_size: 2},
+// CHECK:STDOUT:       {kind: 'VariableDecl', text: ';', subtree_size: 7},
+// CHECK:STDOUT:           {kind: 'MatchIntroducer', text: 'match'},
+// CHECK:STDOUT:             {kind: 'MatchConditionStart', text: '('},
+// CHECK:STDOUT:             {kind: 'IdentifierNameExpr', text: 'x'},
+// CHECK:STDOUT:           {kind: 'MatchCondition', text: ')', subtree_size: 3},
+// CHECK:STDOUT:         {kind: 'MatchStatementStart', text: '{', subtree_size: 5},
+// CHECK:STDOUT:             {kind: 'MatchDefaultIntroducer', text: 'default'},
+// CHECK:STDOUT:             {kind: 'MatchDefaultEqualGreater', text: '=>'},
+// CHECK:STDOUT:           {kind: 'MatchDefaultStart', text: '{', subtree_size: 3},
+// CHECK:STDOUT:             {kind: 'ReturnStatementStart', text: 'return'},
+// CHECK:STDOUT:             {kind: 'IntLiteral', text: '1'},
+// CHECK:STDOUT:           {kind: 'ReturnStatement', text: ';', subtree_size: 3},
+// CHECK:STDOUT:         {kind: 'MatchDefault', text: '}', subtree_size: 7},
+// CHECK:STDOUT:       {kind: 'MatchStatement', text: '}', has_error: yes, subtree_size: 13},
+// CHECK:STDOUT:         {kind: 'ReturnStatementStart', text: 'return'},
+// CHECK:STDOUT:         {kind: 'IntLiteral', text: '0'},
+// CHECK:STDOUT:       {kind: 'ReturnStatement', text: ';', subtree_size: 3},
+// CHECK:STDOUT:     {kind: 'FunctionDefinition', text: '}', subtree_size: 31},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]

+ 62 - 0
toolchain/parse/testdata/match/fail_missing_case_arrow.carbon

@@ -0,0 +1,62 @@
+// 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
+//
+// AUTOUPDATE
+
+fn f() -> i32 {
+  match (3) {
+    // CHECK:STDERR: fail_missing_case_arrow.carbon:[[@LINE+3]]:17: ERROR: Expected `=>` introducing statement block.
+    // CHECK:STDERR:     case x: i32 { return 2; }
+    // CHECK:STDERR:                 ^
+    case x: i32 { return 2; }
+    // CHECK:STDERR: fail_missing_case_arrow.carbon:[[@LINE+3]]:29: ERROR: Expected `=>` introducing statement block.
+    // CHECK:STDERR:     case x: i32 if (x == 3) { return 3; }
+    // CHECK:STDERR:                             ^
+    case x: i32 if (x == 3) { return 3; }
+  }
+  return 0;
+}
+
+// CHECK:STDOUT: - filename: fail_missing_case_arrow.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:         {kind: 'FunctionIntroducer', text: 'fn'},
+// CHECK:STDOUT:         {kind: 'IdentifierName', text: 'f'},
+// CHECK:STDOUT:           {kind: 'TuplePatternStart', text: '('},
+// CHECK:STDOUT:         {kind: 'TuplePattern', text: ')', subtree_size: 2},
+// CHECK:STDOUT:           {kind: 'IntTypeLiteral', text: 'i32'},
+// CHECK:STDOUT:         {kind: 'ReturnType', text: '->', subtree_size: 2},
+// CHECK:STDOUT:       {kind: 'FunctionDefinitionStart', text: '{', subtree_size: 7},
+// CHECK:STDOUT:           {kind: 'MatchIntroducer', text: 'match'},
+// CHECK:STDOUT:             {kind: 'MatchConditionStart', text: '('},
+// CHECK:STDOUT:             {kind: 'IntLiteral', text: '3'},
+// CHECK:STDOUT:           {kind: 'MatchCondition', text: ')', subtree_size: 3},
+// CHECK:STDOUT:         {kind: 'MatchStatementStart', text: '{', subtree_size: 5},
+// CHECK:STDOUT:             {kind: 'MatchCaseIntroducer', text: 'case'},
+// CHECK:STDOUT:               {kind: 'IdentifierName', text: 'x'},
+// CHECK:STDOUT:               {kind: 'IntTypeLiteral', text: 'i32'},
+// CHECK:STDOUT:             {kind: 'BindingPattern', text: ':', subtree_size: 3},
+// CHECK:STDOUT:             {kind: 'MatchCaseEqualGreater', text: '{', has_error: yes},
+// CHECK:STDOUT:           {kind: 'MatchCaseStart', text: '{', has_error: yes, subtree_size: 6},
+// CHECK:STDOUT:         {kind: 'MatchCase', text: '{', has_error: yes, subtree_size: 7},
+// CHECK:STDOUT:             {kind: 'MatchCaseIntroducer', text: 'case'},
+// CHECK:STDOUT:               {kind: 'IdentifierName', text: 'x'},
+// CHECK:STDOUT:               {kind: 'IntTypeLiteral', text: 'i32'},
+// CHECK:STDOUT:             {kind: 'BindingPattern', text: ':', subtree_size: 3},
+// CHECK:STDOUT:               {kind: 'MatchCaseGuardIntroducer', text: 'if'},
+// CHECK:STDOUT:               {kind: 'MatchCaseGuardStart', text: '('},
+// CHECK:STDOUT:                 {kind: 'IdentifierNameExpr', text: 'x'},
+// CHECK:STDOUT:                 {kind: 'IntLiteral', text: '3'},
+// CHECK:STDOUT:               {kind: 'InfixOperatorEqualEqual', text: '==', subtree_size: 3},
+// CHECK:STDOUT:             {kind: 'MatchCaseGuard', text: ')', subtree_size: 6},
+// CHECK:STDOUT:             {kind: 'MatchCaseEqualGreater', text: '{', has_error: yes},
+// CHECK:STDOUT:           {kind: 'MatchCaseStart', text: '{', has_error: yes, subtree_size: 12},
+// CHECK:STDOUT:         {kind: 'MatchCase', text: '{', has_error: yes, subtree_size: 13},
+// CHECK:STDOUT:       {kind: 'MatchStatement', text: '}', subtree_size: 26},
+// CHECK:STDOUT:         {kind: 'ReturnStatementStart', text: 'return'},
+// CHECK:STDOUT:         {kind: 'IntLiteral', text: '0'},
+// CHECK:STDOUT:       {kind: 'ReturnStatement', text: ';', subtree_size: 3},
+// CHECK:STDOUT:     {kind: 'FunctionDefinition', text: '}', subtree_size: 37},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]

+ 44 - 0
toolchain/parse/testdata/match/fail_missing_case_pattern.carbon

@@ -0,0 +1,44 @@
+// 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
+//
+// AUTOUPDATE
+
+fn f() -> i32 {
+  match (3) {
+    // CHECK:STDERR: fail_missing_case_pattern.carbon:[[@LINE+3]]:10: ERROR: Expected binding pattern.
+    // CHECK:STDERR:     case => { return 2; }
+    // CHECK:STDERR:          ^~
+    case => { return 2; }
+  }
+  return 0;
+}
+
+// CHECK:STDOUT: - filename: fail_missing_case_pattern.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:         {kind: 'FunctionIntroducer', text: 'fn'},
+// CHECK:STDOUT:         {kind: 'IdentifierName', text: 'f'},
+// CHECK:STDOUT:           {kind: 'TuplePatternStart', text: '('},
+// CHECK:STDOUT:         {kind: 'TuplePattern', text: ')', subtree_size: 2},
+// CHECK:STDOUT:           {kind: 'IntTypeLiteral', text: 'i32'},
+// CHECK:STDOUT:         {kind: 'ReturnType', text: '->', subtree_size: 2},
+// CHECK:STDOUT:       {kind: 'FunctionDefinitionStart', text: '{', subtree_size: 7},
+// CHECK:STDOUT:           {kind: 'MatchIntroducer', text: 'match'},
+// CHECK:STDOUT:             {kind: 'MatchConditionStart', text: '('},
+// CHECK:STDOUT:             {kind: 'IntLiteral', text: '3'},
+// CHECK:STDOUT:           {kind: 'MatchCondition', text: ')', subtree_size: 3},
+// CHECK:STDOUT:         {kind: 'MatchStatementStart', text: '{', subtree_size: 5},
+// CHECK:STDOUT:             {kind: 'MatchCaseIntroducer', text: 'case'},
+// CHECK:STDOUT:               {kind: 'IdentifierName', text: '=>', has_error: yes},
+// CHECK:STDOUT:               {kind: 'InvalidParse', text: '=>', has_error: yes},
+// CHECK:STDOUT:             {kind: 'BindingPattern', text: '=>', has_error: yes, subtree_size: 3},
+// CHECK:STDOUT:           {kind: 'MatchCaseStart', text: '=>', has_error: yes, subtree_size: 5},
+// CHECK:STDOUT:         {kind: 'MatchCase', text: '=>', has_error: yes, subtree_size: 6},
+// CHECK:STDOUT:       {kind: 'MatchStatement', text: '}', subtree_size: 12},
+// CHECK:STDOUT:         {kind: 'ReturnStatementStart', text: 'return'},
+// CHECK:STDOUT:         {kind: 'IntLiteral', text: '0'},
+// CHECK:STDOUT:       {kind: 'ReturnStatement', text: ';', subtree_size: 3},
+// CHECK:STDOUT:     {kind: 'FunctionDefinition', text: '}', subtree_size: 23},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]

+ 46 - 0
toolchain/parse/testdata/match/fail_missing_case_statements_block.carbon

@@ -0,0 +1,46 @@
+// 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
+//
+// AUTOUPDATE
+
+fn f() -> i32 {
+  match (3) {
+    case x: i32 =>
+    // CHECK:STDERR: fail_missing_case_statements_block.carbon:[[@LINE+3]]:5: ERROR: Expected `{` after `=>`.
+    // CHECK:STDERR:     default => { return 3; }
+    // CHECK:STDERR:     ^~~~~~~
+    default => { return 3; }
+  }
+  return 0;
+}
+
+// CHECK:STDOUT: - filename: fail_missing_case_statements_block.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:         {kind: 'FunctionIntroducer', text: 'fn'},
+// CHECK:STDOUT:         {kind: 'IdentifierName', text: 'f'},
+// CHECK:STDOUT:           {kind: 'TuplePatternStart', text: '('},
+// CHECK:STDOUT:         {kind: 'TuplePattern', text: ')', subtree_size: 2},
+// CHECK:STDOUT:           {kind: 'IntTypeLiteral', text: 'i32'},
+// CHECK:STDOUT:         {kind: 'ReturnType', text: '->', subtree_size: 2},
+// CHECK:STDOUT:       {kind: 'FunctionDefinitionStart', text: '{', subtree_size: 7},
+// CHECK:STDOUT:           {kind: 'MatchIntroducer', text: 'match'},
+// CHECK:STDOUT:             {kind: 'MatchConditionStart', text: '('},
+// CHECK:STDOUT:             {kind: 'IntLiteral', text: '3'},
+// CHECK:STDOUT:           {kind: 'MatchCondition', text: ')', subtree_size: 3},
+// CHECK:STDOUT:         {kind: 'MatchStatementStart', text: '{', subtree_size: 5},
+// CHECK:STDOUT:             {kind: 'MatchCaseIntroducer', text: 'case'},
+// CHECK:STDOUT:               {kind: 'IdentifierName', text: 'x'},
+// CHECK:STDOUT:               {kind: 'IntTypeLiteral', text: 'i32'},
+// CHECK:STDOUT:             {kind: 'BindingPattern', text: ':', subtree_size: 3},
+// CHECK:STDOUT:             {kind: 'MatchCaseEqualGreater', text: '=>'},
+// CHECK:STDOUT:           {kind: 'MatchCaseStart', text: 'default', has_error: yes, subtree_size: 6},
+// CHECK:STDOUT:         {kind: 'MatchCase', text: 'default', has_error: yes, subtree_size: 7},
+// CHECK:STDOUT:       {kind: 'MatchStatement', text: '}', subtree_size: 13},
+// CHECK:STDOUT:         {kind: 'ReturnStatementStart', text: 'return'},
+// CHECK:STDOUT:         {kind: 'IntLiteral', text: '0'},
+// CHECK:STDOUT:       {kind: 'ReturnStatement', text: ';', subtree_size: 3},
+// CHECK:STDOUT:     {kind: 'FunctionDefinition', text: '}', subtree_size: 24},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]

+ 37 - 0
toolchain/parse/testdata/match/fail_missing_cases.carbon

@@ -0,0 +1,37 @@
+// 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
+//
+// AUTOUPDATE
+
+
+fn f() -> i32 {
+  // CHECK:STDERR: fail_missing_cases.carbon:[[@LINE+3]]:14: ERROR: Expected cases.
+  // CHECK:STDERR:   match (3) {}
+  // CHECK:STDERR:              ^
+  match (3) {}
+  return 0;
+}
+
+// CHECK:STDOUT: - filename: fail_missing_cases.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:         {kind: 'FunctionIntroducer', text: 'fn'},
+// CHECK:STDOUT:         {kind: 'IdentifierName', text: 'f'},
+// CHECK:STDOUT:           {kind: 'TuplePatternStart', text: '('},
+// CHECK:STDOUT:         {kind: 'TuplePattern', text: ')', subtree_size: 2},
+// CHECK:STDOUT:           {kind: 'IntTypeLiteral', text: 'i32'},
+// CHECK:STDOUT:         {kind: 'ReturnType', text: '->', subtree_size: 2},
+// CHECK:STDOUT:       {kind: 'FunctionDefinitionStart', text: '{', subtree_size: 7},
+// CHECK:STDOUT:           {kind: 'MatchIntroducer', text: 'match'},
+// CHECK:STDOUT:             {kind: 'MatchConditionStart', text: '('},
+// CHECK:STDOUT:             {kind: 'IntLiteral', text: '3'},
+// CHECK:STDOUT:           {kind: 'MatchCondition', text: ')', subtree_size: 3},
+// CHECK:STDOUT:         {kind: 'MatchStatementStart', text: '{', subtree_size: 5},
+// CHECK:STDOUT:       {kind: 'MatchStatement', text: '}', has_error: yes, subtree_size: 6},
+// CHECK:STDOUT:         {kind: 'ReturnStatementStart', text: 'return'},
+// CHECK:STDOUT:         {kind: 'IntLiteral', text: '0'},
+// CHECK:STDOUT:       {kind: 'ReturnStatement', text: ';', subtree_size: 3},
+// CHECK:STDOUT:     {kind: 'FunctionDefinition', text: '}', subtree_size: 17},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]

+ 33 - 0
toolchain/parse/testdata/match/fail_missing_cases_block.carbon

@@ -0,0 +1,33 @@
+// 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
+//
+// AUTOUPDATE
+
+fn f() -> i32 {
+  match (1)
+  // CHECK:STDERR: fail_missing_cases_block.carbon:[[@LINE+3]]:3: ERROR: Expected `{` starting block with cases.
+  // CHECK:STDERR:   return 0;
+  // CHECK:STDERR:   ^~~~~~
+  return 0;
+}
+
+// CHECK:STDOUT: - filename: fail_missing_cases_block.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:         {kind: 'FunctionIntroducer', text: 'fn'},
+// CHECK:STDOUT:         {kind: 'IdentifierName', text: 'f'},
+// CHECK:STDOUT:           {kind: 'TuplePatternStart', text: '('},
+// CHECK:STDOUT:         {kind: 'TuplePattern', text: ')', subtree_size: 2},
+// CHECK:STDOUT:           {kind: 'IntTypeLiteral', text: 'i32'},
+// CHECK:STDOUT:         {kind: 'ReturnType', text: '->', subtree_size: 2},
+// CHECK:STDOUT:       {kind: 'FunctionDefinitionStart', text: '{', subtree_size: 7},
+// CHECK:STDOUT:           {kind: 'MatchIntroducer', text: 'match'},
+// CHECK:STDOUT:             {kind: 'MatchConditionStart', text: '('},
+// CHECK:STDOUT:             {kind: 'IntLiteral', text: '1'},
+// CHECK:STDOUT:           {kind: 'MatchCondition', text: ')', subtree_size: 3},
+// CHECK:STDOUT:         {kind: 'MatchStatementStart', text: 'return', has_error: yes, subtree_size: 5},
+// CHECK:STDOUT:       {kind: 'MatchStatement', text: 'return', has_error: yes, subtree_size: 6},
+// CHECK:STDOUT:     {kind: 'FunctionDefinition', text: '}', subtree_size: 14},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]

+ 42 - 0
toolchain/parse/testdata/match/fail_missing_default_arrow.carbon

@@ -0,0 +1,42 @@
+// 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
+//
+// AUTOUPDATE
+
+fn f() -> i32 {
+  match (3) {
+    // CHECK:STDERR: fail_missing_default_arrow.carbon:[[@LINE+3]]:13: ERROR: Expected `=>` introducing statement block.
+    // CHECK:STDERR:     default { return 1; }
+    // CHECK:STDERR:             ^
+    default { return 1; }
+  }
+  return 0;
+}
+
+// CHECK:STDOUT: - filename: fail_missing_default_arrow.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:         {kind: 'FunctionIntroducer', text: 'fn'},
+// CHECK:STDOUT:         {kind: 'IdentifierName', text: 'f'},
+// CHECK:STDOUT:           {kind: 'TuplePatternStart', text: '('},
+// CHECK:STDOUT:         {kind: 'TuplePattern', text: ')', subtree_size: 2},
+// CHECK:STDOUT:           {kind: 'IntTypeLiteral', text: 'i32'},
+// CHECK:STDOUT:         {kind: 'ReturnType', text: '->', subtree_size: 2},
+// CHECK:STDOUT:       {kind: 'FunctionDefinitionStart', text: '{', subtree_size: 7},
+// CHECK:STDOUT:           {kind: 'MatchIntroducer', text: 'match'},
+// CHECK:STDOUT:             {kind: 'MatchConditionStart', text: '('},
+// CHECK:STDOUT:             {kind: 'IntLiteral', text: '3'},
+// CHECK:STDOUT:           {kind: 'MatchCondition', text: ')', subtree_size: 3},
+// CHECK:STDOUT:         {kind: 'MatchStatementStart', text: '{', subtree_size: 5},
+// CHECK:STDOUT:             {kind: 'MatchDefaultIntroducer', text: 'default'},
+// CHECK:STDOUT:             {kind: 'MatchDefaultEqualGreater', text: '{', has_error: yes},
+// CHECK:STDOUT:           {kind: 'MatchDefaultStart', text: '{', has_error: yes, subtree_size: 3},
+// CHECK:STDOUT:         {kind: 'MatchDefault', text: '{', has_error: yes, subtree_size: 4},
+// CHECK:STDOUT:       {kind: 'MatchStatement', text: '}', subtree_size: 10},
+// CHECK:STDOUT:         {kind: 'ReturnStatementStart', text: 'return'},
+// CHECK:STDOUT:         {kind: 'IntLiteral', text: '0'},
+// CHECK:STDOUT:       {kind: 'ReturnStatement', text: ';', subtree_size: 3},
+// CHECK:STDOUT:     {kind: 'FunctionDefinition', text: '}', subtree_size: 21},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]

+ 42 - 0
toolchain/parse/testdata/match/fail_missing_default_statements_block.carbon

@@ -0,0 +1,42 @@
+// 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
+//
+// AUTOUPDATE
+
+fn f() -> i32 {
+  match (3) {
+    default =>
+  // CHECK:STDERR: fail_missing_default_statements_block.carbon:[[@LINE+3]]:3: ERROR: Expected `{` after `=>`.
+  // CHECK:STDERR:   }
+  // CHECK:STDERR:   ^
+  }
+  return 0;
+}
+
+// CHECK:STDOUT: - filename: fail_missing_default_statements_block.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:         {kind: 'FunctionIntroducer', text: 'fn'},
+// CHECK:STDOUT:         {kind: 'IdentifierName', text: 'f'},
+// CHECK:STDOUT:           {kind: 'TuplePatternStart', text: '('},
+// CHECK:STDOUT:         {kind: 'TuplePattern', text: ')', subtree_size: 2},
+// CHECK:STDOUT:           {kind: 'IntTypeLiteral', text: 'i32'},
+// CHECK:STDOUT:         {kind: 'ReturnType', text: '->', subtree_size: 2},
+// CHECK:STDOUT:       {kind: 'FunctionDefinitionStart', text: '{', subtree_size: 7},
+// CHECK:STDOUT:           {kind: 'MatchIntroducer', text: 'match'},
+// CHECK:STDOUT:             {kind: 'MatchConditionStart', text: '('},
+// CHECK:STDOUT:             {kind: 'IntLiteral', text: '3'},
+// CHECK:STDOUT:           {kind: 'MatchCondition', text: ')', subtree_size: 3},
+// CHECK:STDOUT:         {kind: 'MatchStatementStart', text: '{', subtree_size: 5},
+// CHECK:STDOUT:             {kind: 'MatchDefaultIntroducer', text: 'default'},
+// CHECK:STDOUT:             {kind: 'MatchDefaultEqualGreater', text: '=>'},
+// CHECK:STDOUT:           {kind: 'MatchDefaultStart', text: '}', has_error: yes, subtree_size: 3},
+// CHECK:STDOUT:         {kind: 'MatchDefault', text: '}', has_error: yes, subtree_size: 4},
+// CHECK:STDOUT:       {kind: 'MatchStatement', text: '}', subtree_size: 10},
+// CHECK:STDOUT:         {kind: 'ReturnStatementStart', text: 'return'},
+// CHECK:STDOUT:         {kind: 'IntLiteral', text: '0'},
+// CHECK:STDOUT:       {kind: 'ReturnStatement', text: ';', subtree_size: 3},
+// CHECK:STDOUT:     {kind: 'FunctionDefinition', text: '}', subtree_size: 21},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]

+ 49 - 0
toolchain/parse/testdata/match/fail_missing_guard_close_paren.carbon

@@ -0,0 +1,49 @@
+// 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
+//
+// AUTOUPDATE
+
+fn f() -> i32 {
+  match (true) {
+    // CHECK:STDERR: fail_missing_guard_close_paren.carbon:[[@LINE+3]]:21: ERROR: Opening symbol without a corresponding closing symbol.
+    // CHECK:STDERR:     case x: bool if (false => { return 1; }
+    // CHECK:STDERR:                     ^
+    case x: bool if (false => { return 1; }
+  }
+  return 0;
+}
+
+// CHECK:STDOUT: - filename: fail_missing_guard_close_paren.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:         {kind: 'FunctionIntroducer', text: 'fn'},
+// CHECK:STDOUT:         {kind: 'IdentifierName', text: 'f'},
+// CHECK:STDOUT:           {kind: 'TuplePatternStart', text: '('},
+// CHECK:STDOUT:         {kind: 'TuplePattern', text: ')', subtree_size: 2},
+// CHECK:STDOUT:           {kind: 'IntTypeLiteral', text: 'i32'},
+// CHECK:STDOUT:         {kind: 'ReturnType', text: '->', subtree_size: 2},
+// CHECK:STDOUT:       {kind: 'FunctionDefinitionStart', text: '{', subtree_size: 7},
+// CHECK:STDOUT:           {kind: 'MatchIntroducer', text: 'match'},
+// CHECK:STDOUT:             {kind: 'MatchConditionStart', text: '('},
+// CHECK:STDOUT:             {kind: 'BoolLiteralTrue', text: 'true'},
+// CHECK:STDOUT:           {kind: 'MatchCondition', text: ')', subtree_size: 3},
+// CHECK:STDOUT:         {kind: 'MatchStatementStart', text: '{', subtree_size: 5},
+// CHECK:STDOUT:             {kind: 'MatchCaseIntroducer', text: 'case'},
+// CHECK:STDOUT:               {kind: 'IdentifierName', text: 'x'},
+// CHECK:STDOUT:               {kind: 'BoolTypeLiteral', text: 'bool'},
+// CHECK:STDOUT:             {kind: 'BindingPattern', text: ':', subtree_size: 3},
+// CHECK:STDOUT:               {kind: 'MatchCaseGuardIntroducer', text: 'if'},
+// CHECK:STDOUT:               {kind: 'MatchCaseGuardStart', text: '('},
+// CHECK:STDOUT:               {kind: 'BoolLiteralFalse', text: 'false'},
+// CHECK:STDOUT:             {kind: 'MatchCaseGuard', text: '=>', has_error: yes, subtree_size: 4},
+// CHECK:STDOUT:             {kind: 'MatchCaseEqualGreater', text: '}', has_error: yes},
+// CHECK:STDOUT:           {kind: 'MatchCaseStart', text: '}', has_error: yes, subtree_size: 10},
+// CHECK:STDOUT:         {kind: 'MatchCase', text: '}', has_error: yes, subtree_size: 11},
+// CHECK:STDOUT:       {kind: 'MatchStatement', text: '}', subtree_size: 17},
+// CHECK:STDOUT:         {kind: 'ReturnStatementStart', text: 'return'},
+// CHECK:STDOUT:         {kind: 'IntLiteral', text: '0'},
+// CHECK:STDOUT:       {kind: 'ReturnStatement', text: ';', subtree_size: 3},
+// CHECK:STDOUT:     {kind: 'FunctionDefinition', text: '}', subtree_size: 28},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]

+ 48 - 0
toolchain/parse/testdata/match/fail_missing_guard_open_paren.carbon

@@ -0,0 +1,48 @@
+// 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
+//
+// AUTOUPDATE
+
+fn f() -> i32 {
+  match (true) {
+    // CHECK:STDERR: fail_missing_guard_open_paren.carbon:[[@LINE+3]]:26: ERROR: Closing symbol without a corresponding opening symbol.
+    // CHECK:STDERR:     case x: bool if false) => { return 1; }
+    // CHECK:STDERR:                          ^
+    case x: bool if false) => { return 1; }
+  }
+  return 0;
+}
+
+// CHECK:STDOUT: - filename: fail_missing_guard_open_paren.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:         {kind: 'FunctionIntroducer', text: 'fn'},
+// CHECK:STDOUT:         {kind: 'IdentifierName', text: 'f'},
+// CHECK:STDOUT:           {kind: 'TuplePatternStart', text: '('},
+// CHECK:STDOUT:         {kind: 'TuplePattern', text: ')', subtree_size: 2},
+// CHECK:STDOUT:           {kind: 'IntTypeLiteral', text: 'i32'},
+// CHECK:STDOUT:         {kind: 'ReturnType', text: '->', subtree_size: 2},
+// CHECK:STDOUT:       {kind: 'FunctionDefinitionStart', text: '{', subtree_size: 7},
+// CHECK:STDOUT:           {kind: 'MatchIntroducer', text: 'match'},
+// CHECK:STDOUT:             {kind: 'MatchConditionStart', text: '('},
+// CHECK:STDOUT:             {kind: 'BoolLiteralTrue', text: 'true'},
+// CHECK:STDOUT:           {kind: 'MatchCondition', text: ')', subtree_size: 3},
+// CHECK:STDOUT:         {kind: 'MatchStatementStart', text: '{', subtree_size: 5},
+// CHECK:STDOUT:             {kind: 'MatchCaseIntroducer', text: 'case'},
+// CHECK:STDOUT:               {kind: 'IdentifierName', text: 'x'},
+// CHECK:STDOUT:               {kind: 'BoolTypeLiteral', text: 'bool'},
+// CHECK:STDOUT:             {kind: 'BindingPattern', text: ':', subtree_size: 3},
+// CHECK:STDOUT:               {kind: 'MatchCaseGuardIntroducer', text: 'if'},
+// CHECK:STDOUT:               {kind: 'MatchCaseGuardStart', text: 'false', has_error: yes},
+// CHECK:STDOUT:               {kind: 'InvalidParse', text: 'false', has_error: yes},
+// CHECK:STDOUT:             {kind: 'MatchCaseGuard', text: 'false', has_error: yes, subtree_size: 4},
+// CHECK:STDOUT:           {kind: 'MatchCaseStart', text: 'false', has_error: yes, subtree_size: 9},
+// CHECK:STDOUT:         {kind: 'MatchCase', text: 'false', has_error: yes, subtree_size: 10},
+// CHECK:STDOUT:       {kind: 'MatchStatement', text: '}', subtree_size: 16},
+// CHECK:STDOUT:         {kind: 'ReturnStatementStart', text: 'return'},
+// CHECK:STDOUT:         {kind: 'IntLiteral', text: '0'},
+// CHECK:STDOUT:       {kind: 'ReturnStatement', text: ';', subtree_size: 3},
+// CHECK:STDOUT:     {kind: 'FunctionDefinition', text: '}', subtree_size: 27},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]

+ 45 - 0
toolchain/parse/testdata/match/fail_missing_matched_expr.carbon

@@ -0,0 +1,45 @@
+// 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
+//
+// AUTOUPDATE
+
+fn f() -> i32 {
+  // CHECK:STDERR: fail_missing_matched_expr.carbon:[[@LINE+3]]:9: ERROR: Expected `(` after `match`.
+  // CHECK:STDERR:   match {
+  // CHECK:STDERR:         ^
+  match {
+    default => { return 1; }
+  }
+  return 0;
+}
+
+// CHECK:STDOUT: - filename: fail_missing_matched_expr.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:         {kind: 'FunctionIntroducer', text: 'fn'},
+// CHECK:STDOUT:         {kind: 'IdentifierName', text: 'f'},
+// CHECK:STDOUT:           {kind: 'TuplePatternStart', text: '('},
+// CHECK:STDOUT:         {kind: 'TuplePattern', text: ')', subtree_size: 2},
+// CHECK:STDOUT:           {kind: 'IntTypeLiteral', text: 'i32'},
+// CHECK:STDOUT:         {kind: 'ReturnType', text: '->', subtree_size: 2},
+// CHECK:STDOUT:       {kind: 'FunctionDefinitionStart', text: '{', subtree_size: 7},
+// CHECK:STDOUT:           {kind: 'MatchIntroducer', text: 'match'},
+// CHECK:STDOUT:             {kind: 'MatchConditionStart', text: 'match', has_error: yes},
+// CHECK:STDOUT:             {kind: 'InvalidParse', text: '{', has_error: yes},
+// CHECK:STDOUT:           {kind: 'MatchCondition', text: 'match', has_error: yes, subtree_size: 3},
+// CHECK:STDOUT:         {kind: 'MatchStatementStart', text: '{', subtree_size: 5},
+// CHECK:STDOUT:             {kind: 'MatchDefaultIntroducer', text: 'default'},
+// CHECK:STDOUT:             {kind: 'MatchDefaultEqualGreater', text: '=>'},
+// CHECK:STDOUT:           {kind: 'MatchDefaultStart', text: '{', subtree_size: 3},
+// CHECK:STDOUT:             {kind: 'ReturnStatementStart', text: 'return'},
+// CHECK:STDOUT:             {kind: 'IntLiteral', text: '1'},
+// CHECK:STDOUT:           {kind: 'ReturnStatement', text: ';', subtree_size: 3},
+// CHECK:STDOUT:         {kind: 'MatchDefault', text: '}', subtree_size: 7},
+// CHECK:STDOUT:       {kind: 'MatchStatement', text: '}', subtree_size: 13},
+// CHECK:STDOUT:         {kind: 'ReturnStatementStart', text: 'return'},
+// CHECK:STDOUT:         {kind: 'IntLiteral', text: '0'},
+// CHECK:STDOUT:       {kind: 'ReturnStatement', text: ';', subtree_size: 3},
+// CHECK:STDOUT:     {kind: 'FunctionDefinition', text: '}', subtree_size: 24},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]

+ 112 - 0
toolchain/parse/testdata/match/fail_unexpected_tokens_in_cases_block.carbon

@@ -0,0 +1,112 @@
+// 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
+//
+// AUTOUPDATE
+
+fn f() -> i32 {
+  var x: i32 = 3;
+  match (x) {
+    // CHECK:STDERR: fail_unexpected_tokens_in_cases_block.carbon:[[@LINE+3]]:5: ERROR: Unexpected `let`; expected `case`, `default` or `}`.
+    // CHECK:STDERR:     let u: i32 =
+    // CHECK:STDERR:     ^~~
+    let u: i32 =
+    case y: i32 if (y > 2) => { return 1; }
+    // CHECK:STDERR: fail_unexpected_tokens_in_cases_block.carbon:[[@LINE+3]]:5: ERROR: Unexpected `if`; expected `case`, `default` or `}`.
+    // CHECK:STDERR:     if (true) {
+    // CHECK:STDERR:     ^~
+    if (true) {
+      return 2;
+    }
+    // CHECK:STDERR: fail_unexpected_tokens_in_cases_block.carbon:[[@LINE+3]]:5: ERROR: Unexpected `var`; expected `case`, `default` or `}`.
+    // CHECK:STDERR:     var w
+    // CHECK:STDERR:     ^~~
+    var w
+    case z: i32 if (z == 3) => { return 3; }
+    // CHECK:STDERR: fail_unexpected_tokens_in_cases_block.carbon:[[@LINE+3]]:5: ERROR: Unexpected `{`; expected `case`, `default` or `}`.
+    // CHECK:STDERR:     {
+    // CHECK:STDERR:     ^
+    {
+      break;
+    }
+    default => { return 3; }
+    // CHECK:STDERR: fail_unexpected_tokens_in_cases_block.carbon:[[@LINE+3]]:5: ERROR: Unexpected `return`; expected `case`, `default` or `}`.
+    // CHECK:STDERR:     return 1;
+    // CHECK:STDERR:     ^~~~~~
+    return 1;
+    // CHECK:STDERR: fail_unexpected_tokens_in_cases_block.carbon:[[@LINE+3]]:5: ERROR: Unexpected `(`; expected `case`, `default` or `}`.
+    // CHECK:STDERR:     ((), (), ())
+    // CHECK:STDERR:     ^
+    ((), (), ())
+  }
+  return 0;
+}
+
+// CHECK:STDOUT: - filename: fail_unexpected_tokens_in_cases_block.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:         {kind: 'FunctionIntroducer', text: 'fn'},
+// CHECK:STDOUT:         {kind: 'IdentifierName', text: 'f'},
+// CHECK:STDOUT:           {kind: 'TuplePatternStart', text: '('},
+// CHECK:STDOUT:         {kind: 'TuplePattern', text: ')', subtree_size: 2},
+// CHECK:STDOUT:           {kind: 'IntTypeLiteral', text: 'i32'},
+// CHECK:STDOUT:         {kind: 'ReturnType', text: '->', subtree_size: 2},
+// CHECK:STDOUT:       {kind: 'FunctionDefinitionStart', text: '{', subtree_size: 7},
+// CHECK:STDOUT:         {kind: 'VariableIntroducer', text: 'var'},
+// CHECK:STDOUT:           {kind: 'IdentifierName', text: 'x'},
+// CHECK:STDOUT:           {kind: 'IntTypeLiteral', text: 'i32'},
+// CHECK:STDOUT:         {kind: 'BindingPattern', text: ':', subtree_size: 3},
+// CHECK:STDOUT:           {kind: 'IntLiteral', text: '3'},
+// CHECK:STDOUT:         {kind: 'VariableInitializer', text: '=', subtree_size: 2},
+// CHECK:STDOUT:       {kind: 'VariableDecl', text: ';', subtree_size: 7},
+// CHECK:STDOUT:           {kind: 'MatchIntroducer', text: 'match'},
+// CHECK:STDOUT:             {kind: 'MatchConditionStart', text: '('},
+// CHECK:STDOUT:             {kind: 'IdentifierNameExpr', text: 'x'},
+// CHECK:STDOUT:           {kind: 'MatchCondition', text: ')', subtree_size: 3},
+// CHECK:STDOUT:         {kind: 'MatchStatementStart', text: '{', subtree_size: 5},
+// CHECK:STDOUT:             {kind: 'MatchCaseIntroducer', text: 'case'},
+// CHECK:STDOUT:               {kind: 'IdentifierName', text: 'y'},
+// CHECK:STDOUT:               {kind: 'IntTypeLiteral', text: 'i32'},
+// CHECK:STDOUT:             {kind: 'BindingPattern', text: ':', subtree_size: 3},
+// CHECK:STDOUT:               {kind: 'MatchCaseGuardIntroducer', text: 'if'},
+// CHECK:STDOUT:               {kind: 'MatchCaseGuardStart', text: '('},
+// CHECK:STDOUT:                 {kind: 'IdentifierNameExpr', text: 'y'},
+// CHECK:STDOUT:                 {kind: 'IntLiteral', text: '2'},
+// CHECK:STDOUT:               {kind: 'InfixOperatorGreater', text: '>', subtree_size: 3},
+// CHECK:STDOUT:             {kind: 'MatchCaseGuard', text: ')', subtree_size: 6},
+// CHECK:STDOUT:             {kind: 'MatchCaseEqualGreater', text: '=>'},
+// CHECK:STDOUT:           {kind: 'MatchCaseStart', text: '{', subtree_size: 12},
+// CHECK:STDOUT:             {kind: 'ReturnStatementStart', text: 'return'},
+// CHECK:STDOUT:             {kind: 'IntLiteral', text: '1'},
+// CHECK:STDOUT:           {kind: 'ReturnStatement', text: ';', subtree_size: 3},
+// CHECK:STDOUT:         {kind: 'MatchCase', text: '}', subtree_size: 16},
+// CHECK:STDOUT:             {kind: 'MatchCaseIntroducer', text: 'case'},
+// CHECK:STDOUT:               {kind: 'IdentifierName', text: 'z'},
+// CHECK:STDOUT:               {kind: 'IntTypeLiteral', text: 'i32'},
+// CHECK:STDOUT:             {kind: 'BindingPattern', text: ':', subtree_size: 3},
+// CHECK:STDOUT:               {kind: 'MatchCaseGuardIntroducer', text: 'if'},
+// CHECK:STDOUT:               {kind: 'MatchCaseGuardStart', text: '('},
+// CHECK:STDOUT:                 {kind: 'IdentifierNameExpr', text: 'z'},
+// CHECK:STDOUT:                 {kind: 'IntLiteral', text: '3'},
+// CHECK:STDOUT:               {kind: 'InfixOperatorEqualEqual', text: '==', subtree_size: 3},
+// CHECK:STDOUT:             {kind: 'MatchCaseGuard', text: ')', subtree_size: 6},
+// CHECK:STDOUT:             {kind: 'MatchCaseEqualGreater', text: '=>'},
+// CHECK:STDOUT:           {kind: 'MatchCaseStart', text: '{', subtree_size: 12},
+// CHECK:STDOUT:             {kind: 'ReturnStatementStart', text: 'return'},
+// CHECK:STDOUT:             {kind: 'IntLiteral', text: '3'},
+// CHECK:STDOUT:           {kind: 'ReturnStatement', text: ';', subtree_size: 3},
+// CHECK:STDOUT:         {kind: 'MatchCase', text: '}', subtree_size: 16},
+// CHECK:STDOUT:             {kind: 'MatchDefaultIntroducer', text: 'default'},
+// CHECK:STDOUT:             {kind: 'MatchDefaultEqualGreater', text: '=>'},
+// CHECK:STDOUT:           {kind: 'MatchDefaultStart', text: '{', subtree_size: 3},
+// CHECK:STDOUT:             {kind: 'ReturnStatementStart', text: 'return'},
+// CHECK:STDOUT:             {kind: 'IntLiteral', text: '3'},
+// CHECK:STDOUT:           {kind: 'ReturnStatement', text: ';', subtree_size: 3},
+// CHECK:STDOUT:         {kind: 'MatchDefault', text: '}', subtree_size: 7},
+// CHECK:STDOUT:       {kind: 'MatchStatement', text: '}', has_error: yes, subtree_size: 45},
+// CHECK:STDOUT:         {kind: 'ReturnStatementStart', text: 'return'},
+// CHECK:STDOUT:         {kind: 'IntLiteral', text: '0'},
+// CHECK:STDOUT:       {kind: 'ReturnStatement', text: ';', subtree_size: 3},
+// CHECK:STDOUT:     {kind: 'FunctionDefinition', text: '}', subtree_size: 63},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]

+ 106 - 0
toolchain/parse/testdata/match/match.carbon

@@ -0,0 +1,106 @@
+// 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
+//
+// AUTOUPDATE
+
+fn f() -> i32 {
+  var x: i32 = 3;
+  match (f(x)) {
+    case (a: i32, b: i32) => { return 0; }
+    case (a: i32) if (a < 0) => { return 2; }
+    case a: i32 if (a != x) => { return 3; }
+    default => { return 4; }
+  }
+  return 0;
+}
+
+// CHECK:STDOUT: - filename: match.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:         {kind: 'FunctionIntroducer', text: 'fn'},
+// CHECK:STDOUT:         {kind: 'IdentifierName', text: 'f'},
+// CHECK:STDOUT:           {kind: 'TuplePatternStart', text: '('},
+// CHECK:STDOUT:         {kind: 'TuplePattern', text: ')', subtree_size: 2},
+// CHECK:STDOUT:           {kind: 'IntTypeLiteral', text: 'i32'},
+// CHECK:STDOUT:         {kind: 'ReturnType', text: '->', subtree_size: 2},
+// CHECK:STDOUT:       {kind: 'FunctionDefinitionStart', text: '{', subtree_size: 7},
+// CHECK:STDOUT:         {kind: 'VariableIntroducer', text: 'var'},
+// CHECK:STDOUT:           {kind: 'IdentifierName', text: 'x'},
+// CHECK:STDOUT:           {kind: 'IntTypeLiteral', text: 'i32'},
+// CHECK:STDOUT:         {kind: 'BindingPattern', text: ':', subtree_size: 3},
+// CHECK:STDOUT:           {kind: 'IntLiteral', text: '3'},
+// CHECK:STDOUT:         {kind: 'VariableInitializer', text: '=', subtree_size: 2},
+// CHECK:STDOUT:       {kind: 'VariableDecl', text: ';', subtree_size: 7},
+// CHECK:STDOUT:           {kind: 'MatchIntroducer', text: 'match'},
+// CHECK:STDOUT:             {kind: 'MatchConditionStart', text: '('},
+// CHECK:STDOUT:                 {kind: 'IdentifierNameExpr', text: 'f'},
+// CHECK:STDOUT:               {kind: 'CallExprStart', text: '(', subtree_size: 2},
+// CHECK:STDOUT:               {kind: 'IdentifierNameExpr', text: 'x'},
+// CHECK:STDOUT:             {kind: 'CallExpr', text: ')', subtree_size: 4},
+// CHECK:STDOUT:           {kind: 'MatchCondition', text: ')', subtree_size: 6},
+// CHECK:STDOUT:         {kind: 'MatchStatementStart', text: '{', subtree_size: 8},
+// CHECK:STDOUT:             {kind: 'MatchCaseIntroducer', text: 'case'},
+// CHECK:STDOUT:               {kind: 'TuplePatternStart', text: '('},
+// CHECK:STDOUT:                 {kind: 'IdentifierName', text: 'a'},
+// CHECK:STDOUT:                 {kind: 'IntTypeLiteral', text: 'i32'},
+// CHECK:STDOUT:               {kind: 'BindingPattern', text: ':', subtree_size: 3},
+// CHECK:STDOUT:               {kind: 'PatternListComma', text: ','},
+// CHECK:STDOUT:                 {kind: 'IdentifierName', text: 'b'},
+// CHECK:STDOUT:                 {kind: 'IntTypeLiteral', text: 'i32'},
+// CHECK:STDOUT:               {kind: 'BindingPattern', text: ':', subtree_size: 3},
+// CHECK:STDOUT:             {kind: 'TuplePattern', text: ')', subtree_size: 9},
+// CHECK:STDOUT:             {kind: 'MatchCaseEqualGreater', text: '=>'},
+// CHECK:STDOUT:           {kind: 'MatchCaseStart', text: '{', subtree_size: 12},
+// CHECK:STDOUT:             {kind: 'ReturnStatementStart', text: 'return'},
+// CHECK:STDOUT:             {kind: 'IntLiteral', text: '0'},
+// CHECK:STDOUT:           {kind: 'ReturnStatement', text: ';', subtree_size: 3},
+// CHECK:STDOUT:         {kind: 'MatchCase', text: '}', subtree_size: 16},
+// CHECK:STDOUT:             {kind: 'MatchCaseIntroducer', text: 'case'},
+// CHECK:STDOUT:               {kind: 'TuplePatternStart', text: '('},
+// CHECK:STDOUT:                 {kind: 'IdentifierName', text: 'a'},
+// CHECK:STDOUT:                 {kind: 'IntTypeLiteral', text: 'i32'},
+// CHECK:STDOUT:               {kind: 'BindingPattern', text: ':', subtree_size: 3},
+// CHECK:STDOUT:             {kind: 'TuplePattern', text: ')', subtree_size: 5},
+// CHECK:STDOUT:               {kind: 'MatchCaseGuardIntroducer', text: 'if'},
+// CHECK:STDOUT:               {kind: 'MatchCaseGuardStart', text: '('},
+// CHECK:STDOUT:                 {kind: 'IdentifierNameExpr', text: 'a'},
+// CHECK:STDOUT:                 {kind: 'IntLiteral', text: '0'},
+// CHECK:STDOUT:               {kind: 'InfixOperatorLess', text: '<', subtree_size: 3},
+// CHECK:STDOUT:             {kind: 'MatchCaseGuard', text: ')', subtree_size: 6},
+// CHECK:STDOUT:             {kind: 'MatchCaseEqualGreater', text: '=>'},
+// CHECK:STDOUT:           {kind: 'MatchCaseStart', text: '{', subtree_size: 14},
+// CHECK:STDOUT:             {kind: 'ReturnStatementStart', text: 'return'},
+// CHECK:STDOUT:             {kind: 'IntLiteral', text: '2'},
+// CHECK:STDOUT:           {kind: 'ReturnStatement', text: ';', subtree_size: 3},
+// CHECK:STDOUT:         {kind: 'MatchCase', text: '}', subtree_size: 18},
+// CHECK:STDOUT:             {kind: 'MatchCaseIntroducer', text: 'case'},
+// CHECK:STDOUT:               {kind: 'IdentifierName', text: 'a'},
+// CHECK:STDOUT:               {kind: 'IntTypeLiteral', text: 'i32'},
+// CHECK:STDOUT:             {kind: 'BindingPattern', text: ':', subtree_size: 3},
+// CHECK:STDOUT:               {kind: 'MatchCaseGuardIntroducer', text: 'if'},
+// CHECK:STDOUT:               {kind: 'MatchCaseGuardStart', text: '('},
+// CHECK:STDOUT:                 {kind: 'IdentifierNameExpr', text: 'a'},
+// CHECK:STDOUT:                 {kind: 'IdentifierNameExpr', text: 'x'},
+// CHECK:STDOUT:               {kind: 'InfixOperatorExclaimEqual', text: '!=', subtree_size: 3},
+// CHECK:STDOUT:             {kind: 'MatchCaseGuard', text: ')', subtree_size: 6},
+// CHECK:STDOUT:             {kind: 'MatchCaseEqualGreater', text: '=>'},
+// CHECK:STDOUT:           {kind: 'MatchCaseStart', text: '{', subtree_size: 12},
+// CHECK:STDOUT:             {kind: 'ReturnStatementStart', text: 'return'},
+// CHECK:STDOUT:             {kind: 'IntLiteral', text: '3'},
+// CHECK:STDOUT:           {kind: 'ReturnStatement', text: ';', subtree_size: 3},
+// CHECK:STDOUT:         {kind: 'MatchCase', text: '}', subtree_size: 16},
+// CHECK:STDOUT:             {kind: 'MatchDefaultIntroducer', text: 'default'},
+// CHECK:STDOUT:             {kind: 'MatchDefaultEqualGreater', text: '=>'},
+// CHECK:STDOUT:           {kind: 'MatchDefaultStart', text: '{', subtree_size: 3},
+// CHECK:STDOUT:             {kind: 'ReturnStatementStart', text: 'return'},
+// CHECK:STDOUT:             {kind: 'IntLiteral', text: '4'},
+// CHECK:STDOUT:           {kind: 'ReturnStatement', text: ';', subtree_size: 3},
+// CHECK:STDOUT:         {kind: 'MatchDefault', text: '}', subtree_size: 7},
+// CHECK:STDOUT:       {kind: 'MatchStatement', text: '}', subtree_size: 66},
+// CHECK:STDOUT:         {kind: 'ReturnStatementStart', text: 'return'},
+// CHECK:STDOUT:         {kind: 'IntLiteral', text: '0'},
+// CHECK:STDOUT:       {kind: 'ReturnStatement', text: ';', subtree_size: 3},
+// CHECK:STDOUT:     {kind: 'FunctionDefinition', text: '}', subtree_size: 84},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]

+ 73 - 0
toolchain/parse/typed_nodes.h

@@ -5,6 +5,8 @@
 #ifndef CARBON_TOOLCHAIN_PARSE_TYPED_NODES_H_
 #define CARBON_TOOLCHAIN_PARSE_TYPED_NODES_H_
 
+#include <optional>
+
 #include "toolchain/parse/node_ids.h"
 #include "toolchain/parse/node_kind.h"
 
@@ -482,6 +484,77 @@ struct WhileStatement {
   CodeBlockId body;
 };
 
+using MatchConditionStart = LeafNode<NodeKind::MatchConditionStart>;
+
+struct MatchCondition {
+  static constexpr auto Kind = NodeKind::MatchCondition.Define();
+
+  MatchConditionStartId left_paren;
+  AnyExprId condition;
+};
+
+using MatchIntroducer = LeafNode<NodeKind::MatchIntroducer>;
+struct MatchStatementStart {
+  static constexpr auto Kind = NodeKind::MatchStatementStart.Define();
+
+  MatchIntroducerId introducer;
+  MatchConditionId left_brace;
+};
+
+using MatchCaseIntroducer = LeafNode<NodeKind::MatchCaseIntroducer>;
+using MatchCaseGuardIntroducer = LeafNode<NodeKind::MatchCaseGuardIntroducer>;
+using MatchCaseGuardStart = LeafNode<NodeKind::MatchCaseGuardStart>;
+
+struct MatchCaseGuard {
+  static constexpr auto Kind = NodeKind::MatchCaseGuard.Define();
+  MatchCaseGuardIntroducerId introducer;
+  MatchCaseGuardStartId left_paren;
+  AnyExprId condition;
+};
+
+using MatchCaseEqualGreater = LeafNode<NodeKind::MatchCaseEqualGreater>;
+
+struct MatchCaseStart {
+  static constexpr auto Kind = NodeKind::MatchCaseStart.Define();
+  MatchCaseIntroducerId introducer;
+  AnyPatternId pattern;
+  std::optional<MatchCaseGuardId> guard;
+  MatchCaseEqualGreaterId equal_greater_token;
+};
+
+struct MatchCase {
+  static constexpr auto Kind = NodeKind::MatchCase.Define();
+  MatchCaseStartId head;
+  llvm::SmallVector<AnyStatementId> statements;
+};
+
+using MatchDefaultIntroducer = LeafNode<NodeKind::MatchDefaultIntroducer>;
+using MatchDefaultEqualGreater = LeafNode<NodeKind::MatchDefaultEqualGreater>;
+
+struct MatchDefaultStart {
+  static constexpr auto Kind = NodeKind::MatchDefaultStart.Define();
+  MatchDefaultIntroducerId introducer;
+  MatchDefaultEqualGreaterId equal_greater_token;
+};
+
+struct MatchDefault {
+  static constexpr auto Kind = NodeKind::MatchDefault.Define();
+
+  MatchDefaultStartId introducer;
+  llvm::SmallVector<AnyStatementId> statements;
+};
+
+// A `match` statement: `match (expr) { case (...) => {...} default => {...}}`.
+struct MatchStatement {
+  static constexpr auto Kind =
+      NodeKind::MatchStatement.Define(NodeCategory::Statement);
+
+  MatchStatementStartId head;
+
+  llvm::SmallVector<MatchCaseId> cases;
+  std::optional<MatchDefaultId> default_case;
+};
+
 // Expression nodes
 // ----------------