Sfoglia il codice sorgente

Support for parsing expression patterns (#6977)

When parsing a pattern, if we encounter something that isn't pattern
syntax, try parsing as an expression instead. We only need one-token
lookahead to distinguish pattern syntax from expression syntax.

Track a precedence group through pattern parsing so that we can allow
different kinds of expressions in a top-level pattern (such as the
operand of `let`) and in a nested pattern (such as a subpattern of a
tuple pattern or within grouping parens). For example, we do not allow
`case if ...`, and for now I've chosen to also not allow logical or
relational operators at the top level of a pattern, so `case 1 + 1` is
OK, but `case 1 == 1` and `case true and false` require parentheses.
This decision should be ratified or revisited by a design proposal.

Very basic check support is also provided, only sufficient to form an
`ExprPattern` instruction and nothing beyond that. For now, all pattern
matching against an `ExprPattern` fails with a TODO error. To support
that, I've switched from calling `BeginSubpattern` in the parent handler
of a pattern and `EndSubpatternAs*` in the pattern handler itself to
calling both functions in parent handlers, with `EndSubpattern`
converting an expression into an expression pattern where needed.

Depends on #6976.

Assisted-by: Gemini via Google Antigravity
Richard Smith 1 mese fa
parent
commit
181a592b8c
49 ha cambiato i file con 679 aggiunte e 210 eliminazioni
  1. 6 3
      toolchain/check/cpp/import.cpp
  2. 4 2
      toolchain/check/function.cpp
  3. 4 2
      toolchain/check/handle_binding_pattern.cpp
  4. 7 3
      toolchain/check/handle_let_and_var.cpp
  5. 1 0
      toolchain/check/handle_loop_statement.cpp
  6. 8 4
      toolchain/check/handle_pattern_list.cpp
  7. 39 6
      toolchain/check/pattern.cpp
  8. 21 13
      toolchain/check/pattern.h
  9. 23 0
      toolchain/check/pattern_match.cpp
  10. 3 0
      toolchain/check/region_stack.h
  11. 7 3
      toolchain/check/testdata/impl/error_recovery.carbon
  12. 134 0
      toolchain/check/testdata/patterns/expression.carbon
  13. 2 0
      toolchain/diagnostics/kind.def
  14. 2 2
      toolchain/docs/check/pattern_matching.md
  15. 3 1
      toolchain/parse/context.h
  16. 8 1
      toolchain/parse/handle_expr.cpp
  17. 3 2
      toolchain/parse/handle_let.cpp
  18. 3 1
      toolchain/parse/handle_match.cpp
  19. 59 9
      toolchain/parse/handle_pattern.cpp
  20. 9 5
      toolchain/parse/handle_pattern_list.cpp
  21. 3 1
      toolchain/parse/handle_statement.cpp
  22. 2 1
      toolchain/parse/handle_unused.cpp
  23. 4 2
      toolchain/parse/handle_var.cpp
  24. 2 1
      toolchain/parse/node_ids.h
  25. 8 0
      toolchain/parse/precedence.h
  26. 18 2
      toolchain/parse/state.def
  27. 5 7
      toolchain/parse/testdata/basics/fail_paren_match_regression.carbon
  28. 16 14
      toolchain/parse/testdata/for/fail_missing_cond.carbon
  29. 6 8
      toolchain/parse/testdata/for/fail_returned_var.carbon
  30. 6 8
      toolchain/parse/testdata/for/fail_square_brackets.carbon
  31. 5 11
      toolchain/parse/testdata/function/declaration.carbon
  32. 4 10
      toolchain/parse/testdata/generics/impl/fail_impl.carbon
  33. 8 16
      toolchain/parse/testdata/generics/interface/fail_self_param_syntax.carbon
  34. 141 0
      toolchain/parse/testdata/let/expression_pattern_precedence.carbon
  35. 3 5
      toolchain/parse/testdata/let/fail_bad_name.carbon
  36. 3 5
      toolchain/parse/testdata/let/fail_empty.carbon
  37. 6 11
      toolchain/parse/testdata/let/fail_missing_name.carbon
  38. 4 6
      toolchain/parse/testdata/let/fail_no_semi.carbon
  39. 5 11
      toolchain/parse/testdata/let/missing_type.carbon
  40. 7 9
      toolchain/parse/testdata/match/fail_missing_case_pattern.carbon
  41. 22 5
      toolchain/parse/testdata/package_expr/fail_in_name.carbon
  42. 6 7
      toolchain/parse/testdata/var/fail_bad_name.carbon
  43. 4 6
      toolchain/parse/testdata/var/fail_empty.carbon
  44. 5 7
      toolchain/parse/testdata/var/fail_no_semi.carbon
  45. 18 0
      toolchain/sem_ir/formatter.cpp
  46. 1 0
      toolchain/sem_ir/formatter.h
  47. 1 0
      toolchain/sem_ir/inst_kind.def
  48. 8 0
      toolchain/sem_ir/inst_namer.cpp
  49. 12 0
      toolchain/sem_ir/typed_insts.h

+ 6 - 3
toolchain/check/cpp/import.cpp

@@ -1113,7 +1113,9 @@ static auto MakeImplicitParamPatternsBlockId(
   auto param_info = MapParameterType(context, loc_id, param_type);
   auto [type_inst_id, type_id] = param_info.type;
   SemIR::ExprRegionId type_expr_region_id =
-      EndSubpatternAsExpr(context, type_inst_id);
+      ConsumeSubpatternExpr(context, type_inst_id);
+
+  EndEmptySubpattern(context);
 
   if (!type_id.has_value()) {
     context.TODO(loc_id,
@@ -1163,7 +1165,7 @@ static auto MakeParamPatternsBlockId(Context& context, SemIR::LocId loc_id,
         ClangGetUnqualifiedTypePreserveNonNull(context, orig_param_type);
 
     // Mark the start of a region of insts, needed for the type expression
-    // created later with the call of `EndSubpatternAsExpr()`.
+    // created later with the call of `ConsumeSubpatternExpr()`.
     BeginSubpattern(context);
     auto param_info = MapParameterType(context, loc_id, param_type);
     auto [type_inst_id, type_id] = param_info.type;
@@ -1171,7 +1173,8 @@ static auto MakeParamPatternsBlockId(Context& context, SemIR::LocId loc_id,
     // region that allows control flow in the type expression e.g. fn F(x: if C
     // then i32 else i64).
     SemIR::ExprRegionId type_expr_region_id =
-        EndSubpatternAsExpr(context, type_inst_id);
+        ConsumeSubpatternExpr(context, type_inst_id);
+    EndEmptySubpattern(context);
 
     if (!type_id.has_value()) {
       context.TODO(loc_id, llvm::formatv("Unsupported: parameter type: {0}",

+ 4 - 2
toolchain/check/function.cpp

@@ -126,8 +126,9 @@ static auto MakeFunctionSignature(Context& context, SemIR::LocId loc_id,
     context.full_pattern_stack().StartImplicitParamList();
 
     BeginSubpattern(context);
-    auto self_type_region_id = EndSubpatternAsExpr(
+    auto self_type_region_id = ConsumeSubpatternExpr(
         context, context.types().GetTypeInstId(args.self_type_id));
+    EndEmptySubpattern(context);
 
     insts.self_param_id = AddParamPattern(
         context, loc_id, SemIR::NameId::SelfValue, self_type_region_id,
@@ -147,8 +148,9 @@ static auto MakeFunctionSignature(Context& context, SemIR::LocId loc_id,
     context.inst_block_stack().Push();
     for (auto param_type_id : args.param_type_ids) {
       BeginSubpattern(context);
-      auto param_type_region_id = EndSubpatternAsExpr(
+      auto param_type_region_id = ConsumeSubpatternExpr(
           context, context.types().GetTypeInstId(param_type_id));
+      EndEmptySubpattern(context);
 
       context.inst_block_stack().AddInstId(AddParamPattern(
           context, loc_id, SemIR::NameId::Underscore, param_type_region_id,

+ 4 - 2
toolchain/check/handle_binding_pattern.cpp

@@ -155,7 +155,7 @@ static auto HandleAnyBindingPattern(Context& context, Parse::NodeId node_id,
   }
 
   SemIR::ExprRegionId type_expr_region_id =
-      EndSubpatternAsExpr(context, type_expr.inst_id);
+      ConsumeSubpatternExpr(context, type_expr.inst_id);
 
   // The name in a generic binding may be wrapped in `template`.
   bool is_generic = node_kind == Parse::NodeKind::CompileTimeBindingPattern;
@@ -453,7 +453,9 @@ auto HandleParseNode(Context& context,
   auto [cast_type_inst_id, cast_type_id] =
       ExprAsType(context, type_node, parsed_type_id);
 
-  EndSubpatternAsExpr(context, cast_type_inst_id);
+  auto region_id = ConsumeSubpatternExpr(context, cast_type_inst_id);
+  // TODO: Should we be tracking this somewhere?
+  (void)region_id;
 
   auto [name_node, name_id] = context.node_stack().PopNameWithNodeId();
 

+ 7 - 3
toolchain/check/handle_let_and_var.cpp

@@ -131,6 +131,7 @@ auto HandleParseNode(Context& context, Parse::VariablePatternId node_id)
 // Handle the end of the full-pattern of a let/var declaration (before the
 // start of the initializer, if any).
 static auto EndFullPattern(Context& context) -> void {
+  EndSubpattern(context, context.node_stack());
   auto scope_id = context.scope_stack().PeekNameScopeId();
   if (scope_id.has_value() &&
       context.name_scopes().Get(scope_id).is_interface_definition()) {
@@ -179,10 +180,13 @@ auto HandleParseNode(Context& context, Parse::LetInitializerId node_id)
 
 auto HandleParseNode(Context& context,
                      Parse::AssociatedConstantInitializerId node_id) -> bool {
+  EndFullPattern(context);
   auto interface_decl =
       context.scope_stack().GetCurrentScopeAs<SemIR::InterfaceWithSelfDecl>();
   EndAssociatedConstantDeclRegion(context, interface_decl.interface_id);
-  return HandleInitializer(context, node_id);
+  context.node_stack().Push(node_id);
+  StartPatternInitializer(context);
+  return true;
 }
 
 auto HandleParseNode(Context& context, Parse::VariableInitializerId node_id)
@@ -252,6 +256,8 @@ static auto HandleDecl(Context& context, Parse::NodeId node_id) -> DeclInfo {
     context.node_stack().PopAndDiscardSoloNodeId<InitializerNodeKind>();
     EndPatternInitializer(context);
   } else {
+    EndFullPattern(context);
+
     // For an associated constant declaration, handle the completed declaration
     // now. We will have done this at the `=` if there was an initializer.
     if constexpr (IntroducerNodeKind ==
@@ -262,8 +268,6 @@ static auto HandleDecl(Context& context, Parse::NodeId node_id) -> DeclInfo {
       EndAssociatedConstantDeclRegion(context, interface_decl.interface_id);
     }
 
-    EndFullPattern(context);
-
     // A variable declaration without an explicit initializer is initialized by
     // calling `(T as Core.DefaultOrUnformed).Op()`.
     if constexpr (IntroducerNodeKind == Parse::NodeKind::VariableIntroducer) {

+ 1 - 0
toolchain/check/handle_loop_statement.cpp

@@ -123,6 +123,7 @@ auto HandleParseNode(Context& context, Parse::ForHeaderStartId node_id)
 }
 
 auto HandleParseNode(Context& context, Parse::ForInId node_id) -> bool {
+  EndSubpattern(context, context.node_stack());
   auto pattern_block_id = context.pattern_block_stack().Pop();
   AddInst<SemIR::NameBindingDecl>(context, node_id,
                                   {.pattern_block_id = pattern_block_id});

+ 8 - 4
toolchain/check/handle_pattern_list.cpp

@@ -43,7 +43,9 @@ static auto HandleParamListEnd(Context& context, Parse::NodeId node_id,
   if (context.node_stack().PeekIs(start_kind)) {
     // End the subpattern started by a trailing comma, or the opening delimiter
     // of an empty list.
-    EndSubpatternAsNonExpr(context);
+    EndEmptySubpattern(context);
+  } else {
+    EndSubpattern(context, context.node_stack());
   }
   // Note the Start node remains on the stack, where the param list handler can
   // make use of it.
@@ -67,7 +69,7 @@ auto HandleParseNode(Context& context, Parse::ExplicitParamListId node_id)
 }
 
 auto HandleParseNode(Context& context, Parse::ParenPatternId node_id) -> bool {
-  EndSubpatternAsNonExpr(context);
+  EndSubpattern(context, context.node_stack());
   auto pattern_id = context.node_stack().PopPattern();
   context.param_and_arg_refs_stack().PopAndDiscard();
   context.node_stack()
@@ -80,7 +82,9 @@ auto HandleParseNode(Context& context, Parse::TuplePatternId node_id) -> bool {
   if (context.node_stack().PeekIs(Parse::NodeKind::TuplePatternStart)) {
     // End the subpattern started by a trailing comma, or the opening delimiter
     // of an empty list.
-    EndSubpatternAsNonExpr(context);
+    EndEmptySubpattern(context);
+  } else {
+    EndSubpattern(context, context.node_stack());
   }
   auto refs_id = context.param_and_arg_refs_stack().EndAndPop(
       Parse::NodeKind::TuplePatternStart);
@@ -100,12 +104,12 @@ auto HandleParseNode(Context& context, Parse::TuplePatternId node_id) -> bool {
       node_id,
       AddPatternInst<SemIR::TuplePattern>(
           context, node_id, {.type_id = type_id, .elements_id = refs_id}));
-  EndSubpatternAsNonExpr(context);
   return true;
 }
 
 auto HandleParseNode(Context& context, Parse::PatternListCommaId /*node_id*/)
     -> bool {
+  EndSubpattern(context, context.node_stack());
   context.param_and_arg_refs_stack().ApplyComma();
   BeginSubpattern(context);
   return true;

+ 39 - 6
toolchain/check/pattern.cpp

@@ -14,10 +14,12 @@ namespace Carbon::Check {
 
 auto BeginSubpattern(Context& context) -> void {
   context.inst_block_stack().Push();
+  // TODO: This allocates an InstBlockId even in the case where the pattern has
+  // no associated expression. Find a way to avoid this.
   context.region_stack().PushRegion(context.inst_block_stack().PeekOrAdd());
 }
 
-auto EndSubpatternAsExpr(Context& context, SemIR::InstId result_id)
+static auto PopSubpatternExpr(Context& context, SemIR::InstId result_id)
     -> SemIR::ExprRegionId {
   if (context.region_stack().PeekRegion().size() > 1) {
     // End the exit block with a branch to a successor block, whose contents
@@ -39,14 +41,45 @@ auto EndSubpatternAsExpr(Context& context, SemIR::InstId result_id)
        .result_id = result_id});
 }
 
-auto EndSubpatternAsNonExpr(Context& context) -> void {
-  auto block_id = context.inst_block_stack().Pop();
-  CARBON_CHECK(block_id == context.region_stack().PeekRegion().back());
-  CARBON_CHECK(context.region_stack().PeekRegion().size() == 1);
-  CARBON_CHECK(context.inst_blocks().Get(block_id).empty());
+auto ConsumeSubpatternExpr(Context& context, SemIR::InstId result_id)
+    -> SemIR::ExprRegionId {
+  auto region_id = PopSubpatternExpr(context, result_id);
+  // Push an empty, unreachable region so that we can later detect the region
+  // has been consumed.
+  context.region_stack().PushUnreachableRegion();
+  return region_id;
+}
+
+auto EndEmptySubpattern(Context& context) -> void {
+  if (!context.region_stack().PeekRegion().empty()) {
+    CARBON_CHECK(context.inst_block_stack().PeekCurrentBlockContents().empty());
+    auto block_id = context.inst_block_stack().Pop();
+    CARBON_CHECK(block_id == context.region_stack().PeekRegion().back());
+    CARBON_CHECK(context.region_stack().PeekRegion().size() == 1);
+  }
   context.region_stack().PopAndDiscardRegion();
 }
 
+auto EndSubpattern(Context& context, NodeStack& node_stack) -> void {
+  auto [node_id, maybe_expr_id] =
+      node_stack.PopWithNodeIdIf<Parse::NodeCategory::Expr>();
+  if (maybe_expr_id) {
+    // We formed an expression, not a pattern, so convert it to an expression
+    // pattern now.
+    auto expr_region_id = PopSubpatternExpr(context, *maybe_expr_id);
+    auto pattern_type_id =
+        GetPatternType(context, context.insts().Get(*maybe_expr_id).type_id());
+    node_stack.Push(node_id, AddPatternInst<SemIR::ExprPattern>(
+                                 context, node_id,
+                                 {.type_id = pattern_type_id,
+                                  .expr_region_id = expr_region_id}));
+  } else {
+    // The expression region should have been consumed when forming the pattern
+    // instruction, so should now effectively be empty.
+    EndEmptySubpattern(context);
+  }
+}
+
 auto AddBindingEntityName(Context& context, SemIR::NameId name_id,
                           SemIR::ConstantId form_id, bool is_unused,
                           BindingPhase phase) -> SemIR::EntityNameId {

+ 21 - 13
toolchain/check/pattern.h

@@ -10,22 +10,32 @@
 
 namespace Carbon::Check {
 
-// Marks the start of a region of insts in a pattern context that might
-// represent an expression or a pattern. Typically this is called when
-// handling a parse node that can immediately precede a subpattern (such
-// as `let` or a `,` in a pattern list), and the handler for the subpattern
-// node makes the matching `EndSubpatternAs*` call.
+// Marks the start of a region of insts in a pattern context that might contain
+// an expression. Typically this is called when handling a parse node that can
+// immediately precede a subpattern (such as `let` or a `,` in a pattern list).
+// `End[Empty]Subpattern` should be called later by the consumer of the
+// subpattern.
 auto BeginSubpattern(Context& context) -> void;
 
-// Ends a region started by BeginSubpattern (in stack order), treating it as
-// an expression with the given result, and returns the ID of the region. The
-// region will not yet have any control-flow edges into or out of it.
-auto EndSubpatternAsExpr(Context& context, SemIR::InstId result_id)
+// Consumes the expression in a region started by the most recent
+// BeginSubpattern, and returns the ID of the region. The region will not yet
+// have any control-flow edges into or out of it.
+auto ConsumeSubpatternExpr(Context& context, SemIR::InstId result_id)
     -> SemIR::ExprRegionId;
 
 // Ends a region started by BeginSubpattern (in stack order), asserting that
-// it had no expression content.
-auto EndSubpatternAsNonExpr(Context& context) -> void;
+// it either had no expression content or the expression has been consumed.
+auto EndEmptySubpattern(Context& context) -> void;
+
+// Ends a region started by BeginSubpattern (in stack order). If the top of the
+// node stack is an expression, the subpattern region is consumed and converted
+// to an expression pattern, which replaces the expression on the node stack.
+// Otherwise, the top of the node stack should be a pattern, in which case this
+// asserts that the subpattern region is either empty or has been consumed.
+//
+// The node stack is passed explicitly as a reminder that this function affects
+// the node stack, unlike the other *Subpattern functions.
+auto EndSubpattern(Context& context, NodeStack& node_stack) -> void;
 
 // Information about a created binding pattern.
 struct BindingPatternInfo {
@@ -33,8 +43,6 @@ struct BindingPatternInfo {
   SemIR::InstId bind_id;
 };
 
-// TODO: Add EndSubpatternAsPattern, when needed.
-
 // The phase of a binding pattern.
 enum class BindingPhase { Template, Symbolic, Runtime };
 

+ 23 - 0
toolchain/check/pattern_match.cpp

@@ -152,6 +152,8 @@ class MatchContext {
                  SemIR::InstId scrutinee_id, WorkItem entry) -> void;
   auto DoPreWork(Context& context, SemIR::AnyParamPattern param_pattern,
                  SemIR::InstId scrutinee_id, WorkItem entry) -> void;
+  auto DoPreWork(Context& context, SemIR::ExprPattern expr_pattern,
+                 SemIR::InstId scrutinee_id, WorkItem entry) -> void;
   auto DoPreWork(Context& context, SemIR::ReturnSlotPattern return_slot_pattern,
                  SemIR::InstId scrutinee_id, WorkItem entry) -> void;
   auto DoPreWork(Context& context, SemIR::VarPattern var_pattern,
@@ -167,6 +169,8 @@ class MatchContext {
                   WorkItem entry) -> void;
   auto DoPostWork(Context& context, SemIR::AnyParamPattern param_pattern,
                   WorkItem entry) -> void;
+  auto DoPostWork(Context& context, SemIR::ExprPattern expr_pattern,
+                  WorkItem entry) -> void;
   auto DoPostWork(Context& context,
                   SemIR::ReturnSlotPattern return_slot_pattern, WorkItem entry)
       -> void;
@@ -574,6 +578,17 @@ auto MatchContext::DoPostWork(Context& /*context*/,
   // would have to be done here.
 }
 
+auto MatchContext::DoPreWork(Context& context,
+                             SemIR::ExprPattern /*expr_pattern*/,
+                             SemIR::InstId /*scrutinee_id*/, WorkItem entry)
+    -> void {
+  context.TODO(entry.pattern_id, "expression pattern");
+}
+
+auto MatchContext::DoPostWork(Context& /*context*/,
+                              SemIR::ExprPattern /*expr_pattern*/,
+                              WorkItem /*entry*/) -> void {}
+
 auto MatchContext::DoPreWork(Context& /*context*/,
                              SemIR::ReturnSlotPattern return_slot_pattern,
                              SemIR::InstId scrutinee_id, WorkItem entry)
@@ -803,6 +818,10 @@ auto MatchContext::Dispatch(Context& context, WorkItem entry) -> void {
           DoPreWork(context, any_param_pattern, work.scrutinee_id, entry);
           break;
         }
+        case CARBON_KIND(SemIR::ExprPattern expr_pattern): {
+          DoPreWork(context, expr_pattern, work.scrutinee_id, entry);
+          break;
+        }
         case CARBON_KIND(SemIR::ReturnSlotPattern return_slot_pattern): {
           DoPreWork(context, return_slot_pattern, work.scrutinee_id, entry);
           break;
@@ -831,6 +850,10 @@ auto MatchContext::Dispatch(Context& context, WorkItem entry) -> void {
           DoPostWork(context, any_param_pattern, entry);
           break;
         }
+        case CARBON_KIND(SemIR::ExprPattern expr_pattern): {
+          DoPostWork(context, expr_pattern, entry);
+          break;
+        }
         case CARBON_KIND(SemIR::ReturnSlotPattern return_slot_pattern): {
           DoPostWork(context, return_slot_pattern, entry);
           break;

+ 3 - 0
toolchain/check/region_stack.h

@@ -27,6 +27,9 @@ class RegionStack {
     stack_.AppendToTop(entry_block_id);
   }
 
+  // Mark the start of a new empty, unreachable region.
+  auto PushUnreachableRegion() -> void { stack_.PushArray(); }
+
   // Add `block_id` to the most recently pushed single-entry region. To preserve
   // the single-entry property, `block_id` must not be directly reachable from
   // any block outside the region. To ensure the region's blocks are in lexical

+ 7 - 3
toolchain/check/testdata/impl/error_recovery.carbon

@@ -118,14 +118,18 @@ class C {};
 //@dump-sem-ir-begin
 impl C as I {
   // This leaves the impl with a placeholder instruction in the witness table.
-  // CHECK:STDERR: fail_invalid_fn_syntax_in_impl.carbon:[[@LINE+8]]:11: error: expected `:`, `:!`, or `:?` in binding pattern [ExpectedBindingPattern]
+  // CHECK:STDERR: fail_invalid_fn_syntax_in_impl.carbon:[[@LINE+12]]:9: error: name `x` not found [NameNotFound]
   // CHECK:STDERR:   fn Op[x self: C]() {}
-  // CHECK:STDERR:           ^~~~
+  // CHECK:STDERR:         ^
   // CHECK:STDERR:
-  // CHECK:STDERR: fail_invalid_fn_syntax_in_impl.carbon:[[@LINE+4]]:11: error: semantics TODO: `handle invalid parse trees in `check`` [SemanticsTodo]
+  // CHECK:STDERR: fail_invalid_fn_syntax_in_impl.carbon:[[@LINE+8]]:11: error: expected `,` or `]` [UnexpectedTokenAfterListElement]
   // CHECK:STDERR:   fn Op[x self: C]() {}
   // CHECK:STDERR:           ^~~~
   // CHECK:STDERR:
+  // CHECK:STDERR: fail_invalid_fn_syntax_in_impl.carbon:[[@LINE+4]]:8: error: semantics TODO: `handle invalid parse trees in `check`` [SemanticsTodo]
+  // CHECK:STDERR:   fn Op[x self: C]() {}
+  // CHECK:STDERR:        ^~~~~~~~~~~
+  // CHECK:STDERR:
   fn Op[x self: C]() {}
 }
 //@dump-sem-ir-end

+ 134 - 0
toolchain/check/testdata/patterns/expression.carbon

@@ -0,0 +1,134 @@
+// 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-FILE: toolchain/testing/testdata/min_prelude/none.carbon
+//
+// AUTOUPDATE
+// TIP: To test this file alone, run:
+// TIP:   bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/check/testdata/patterns/expression.carbon
+// TIP: To dump output, run:
+// TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/check/testdata/patterns/expression.carbon
+
+// --- fail_todo_let.carbon
+
+library "[[@TEST_NAME]]";
+
+// TODO: We should reject this for using a refutable pattern in an irrefutable pattern context.
+let a: () = ();
+// CHECK:STDERR: fail_todo_let.carbon:[[@LINE+4]]:5: error: semantics TODO: `expression pattern` [SemanticsTodo]
+// CHECK:STDERR: let a = ();
+// CHECK:STDERR:     ^
+// CHECK:STDERR:
+let a = ();
+
+// --- fail_todo_in_tuple.carbon
+
+library "[[@TEST_NAME]]";
+
+// TODO: We should reject this for using a refutable pattern in an irrefutable pattern context.
+let a: () = ();
+// CHECK:STDERR: fail_todo_in_tuple.carbon:[[@LINE+4]]:13: error: semantics TODO: `expression pattern` [SemanticsTodo]
+// CHECK:STDERR: let (b: (), a, c: ()) = ((), (), ());
+// CHECK:STDERR:             ^
+// CHECK:STDERR:
+let (b: (), a, c: ()) = ((), (), ());
+
+// --- fail_todo_in_function_param.carbon
+
+library "[[@TEST_NAME]]";
+
+// TODO: We should reject this for using a refutable pattern in an irrefutable pattern context.
+let a: () = ();
+// CHECK:STDERR: fail_todo_in_function_param.carbon:[[@LINE+4]]:6: error: semantics TODO: `expression pattern` [SemanticsTodo]
+// CHECK:STDERR: fn F(a) {}
+// CHECK:STDERR:      ^
+// CHECK:STDERR:
+fn F(a) {}
+
+// --- fail_todo_parens.carbon
+
+library "[[@TEST_NAME]]";
+
+let a: () = ();
+// TODO: Replace this with a valid expression pattern once we support refutable
+// pattern contexts.
+// Note that this is a parenthesized zero-tuple expression pattern, not a
+// one-tuple pattern containing a zero-tuple.
+// CHECK:STDERR: fail_todo_parens.carbon:[[@LINE+4]]:6: error: semantics TODO: `expression pattern` [SemanticsTodo]
+// CHECK:STDERR: let (a) = ();
+// CHECK:STDERR:      ^
+// CHECK:STDERR:
+let (a) = ();
+
+// --- fail_todo_control_flow.carbon
+
+library "[[@TEST_NAME]]";
+
+fn F() {
+  // TODO: Replace this with a valid expression pattern once we support refutable
+  // pattern contexts.
+  //@dump-sem-ir-begin
+  // CHECK:STDERR: fail_todo_control_flow.carbon:[[@LINE+8]]:8: error: semantics TODO: `expression pattern` [SemanticsTodo]
+  // CHECK:STDERR:   let (if true then true else false, true and true) = (true, true);
+  // CHECK:STDERR:        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
+  // CHECK:STDERR:
+  // CHECK:STDERR: fail_todo_control_flow.carbon:[[@LINE+4]]:38: error: semantics TODO: `expression pattern` [SemanticsTodo]
+  // CHECK:STDERR:   let (if true then true else false, true and true) = (true, true);
+  // CHECK:STDERR:                                      ^~~~~~~~~~~~~
+  // CHECK:STDERR:
+  let (if true then true else false, true and true) = (true, true);
+  //@dump-sem-ir-end
+}
+
+// CHECK:STDOUT: --- fail_todo_control_flow.carbon
+// CHECK:STDOUT:
+// CHECK:STDOUT: constants {
+// CHECK:STDOUT:   %true: bool = bool_literal true [concrete]
+// CHECK:STDOUT:   %false: bool = bool_literal false [concrete]
+// CHECK:STDOUT:   %pattern_type.831: type = pattern_type bool [concrete]
+// CHECK:STDOUT:   %tuple.type: type = tuple_type (bool, bool) [concrete]
+// CHECK:STDOUT:   %pattern_type.860: type = pattern_type %tuple.type [concrete]
+// CHECK:STDOUT:   %tuple: %tuple.type = tuple_value (%true, %true) [concrete]
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @F() {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   name_binding_decl {
+// CHECK:STDOUT:     %expr_patt.loc16_26: %pattern_type.831 = expr_pattern %.loc16_8 in [concrete] {
+// CHECK:STDOUT:       %true.loc16_11: bool = bool_literal true [concrete = constants.%true]
+// CHECK:STDOUT:       if %true.loc16_11 br !if.expr.then else br !if.expr.else
+// CHECK:STDOUT:
+// CHECK:STDOUT:     !if.expr.then:
+// CHECK:STDOUT:       %true.loc16_21: bool = bool_literal true [concrete = constants.%true]
+// CHECK:STDOUT:       br !if.expr.result(%true.loc16_21)
+// CHECK:STDOUT:
+// CHECK:STDOUT:     !if.expr.else:
+// CHECK:STDOUT:       %false.loc16_31: bool = bool_literal false [concrete = constants.%false]
+// CHECK:STDOUT:       br !if.expr.result(%false.loc16_31)
+// CHECK:STDOUT:
+// CHECK:STDOUT:     !if.expr.result:
+// CHECK:STDOUT:       %.loc16_8: bool = block_arg !if.expr.result [concrete = constants.%true]
+// CHECK:STDOUT:       <elided>
+// CHECK:STDOUT:     }
+// CHECK:STDOUT:     %expr_patt.loc16_43: %pattern_type.831 = expr_pattern %.loc16_43 in [concrete] {
+// CHECK:STDOUT:       %true.loc16_38: bool = bool_literal true [concrete = constants.%true]
+// CHECK:STDOUT:       %false.loc16_43: bool = bool_literal false [concrete = constants.%false]
+// CHECK:STDOUT:       if %true.loc16_38 br !and.rhs else br !.loc16(%false.loc16_43)
+// CHECK:STDOUT:
+// CHECK:STDOUT:     !and.rhs:
+// CHECK:STDOUT:       %true.loc16_47: bool = bool_literal true [concrete = constants.%true]
+// CHECK:STDOUT:       br !.loc16(%true.loc16_47)
+// CHECK:STDOUT:
+// CHECK:STDOUT:     !.loc16:
+// CHECK:STDOUT:       %.loc16_43: bool = block_arg !.loc16 [concrete = constants.%true]
+// CHECK:STDOUT:       <elided>
+// CHECK:STDOUT:     }
+// CHECK:STDOUT:     %.loc16_51: %pattern_type.860 = tuple_pattern (%expr_patt.loc16_26, %expr_patt.loc16_43) [concrete]
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %true.loc16_56: bool = bool_literal true [concrete = constants.%true]
+// CHECK:STDOUT:   %true.loc16_62: bool = bool_literal true [concrete = constants.%true]
+// CHECK:STDOUT:   %.loc16_66: %tuple.type = tuple_literal (%true.loc16_56, %true.loc16_62) [concrete = constants.%tuple]
+// CHECK:STDOUT:   <elided>
+// CHECK:STDOUT: }
+// CHECK:STDOUT:

+ 2 - 0
toolchain/diagnostics/kind.def

@@ -104,8 +104,10 @@ CARBON_DIAGNOSTIC_KIND(ExpectedCodeBlock)
 CARBON_DIAGNOSTIC_KIND(ExpectedLambdaBody)
 CARBON_DIAGNOSTIC_KIND(ExpectedLambdaBodyAfterReturnType)
 CARBON_DIAGNOSTIC_KIND(ExpectedExpr)
+CARBON_DIAGNOSTIC_KIND(ExpectedPattern)
 CARBON_DIAGNOSTIC_KIND(ExpectedIdentifierAfterPeriodOrArrow)
 CARBON_DIAGNOSTIC_KIND(ExpectedIdentifierOrSelfAfterPeriod)
+CARBON_DIAGNOSTIC_KIND(ExpectedBindingName)
 CARBON_DIAGNOSTIC_KIND(ExpectedBindingPattern)
 CARBON_DIAGNOSTIC_KIND(ExpectedGenericBindingPatternAfterTemplate)
 CARBON_DIAGNOSTIC_KIND(ExpectedParenAfter)

+ 2 - 2
toolchain/docs/check/pattern_matching.md

@@ -125,8 +125,8 @@ be one that emits non-pattern instructions. To handle these situations, we
 speculatively push an instruction block onto the (non-pattern) stack whenever we
 are about to begin handling a subpattern, and then pop it at the end of the
 subpattern, with different treatment depending on whether the subpattern turned
-out to be a subexpression. This is handled by `BeginSubpattern`,
-`EndSubpatternAsExpr`, and `EndSubpatternAsNonExpr`.
+out to involve a subexpression. This is handled by `BeginSubpattern`,
+`ConsumeSubpatternExpr`, `EndSubpattern`, and `EndEmptySubpattern`.
 
 One further complication here is that the type expression can contain control
 flow (such as an `if` expression). Consequently, we can't represent the type

+ 3 - 1
toolchain/parse/context.h

@@ -332,10 +332,12 @@ class Context {
   // `in_unused_pattern` indicate whether that pattern is nested inside a `var`
   // or `unused` pattern.
   auto PushStateForPattern(StateKind kind, bool in_var_pattern,
-                           bool in_unused_pattern) -> void {
+                           bool in_unused_pattern, PrecedenceGroup precedence)
+      -> void {
     PushState({.kind = kind,
                .in_var_pattern = in_var_pattern,
                .in_unused_pattern = in_unused_pattern,
+               .ambient_precedence = precedence,
                .token = *position_,
                .subtree_start = tree_->size()});
   }

+ 8 - 1
toolchain/parse/handle_expr.cpp

@@ -246,7 +246,14 @@ auto HandleExprInPostfix(Context& context) -> void {
       // If not already diagnosed in the lexer, diagnose it here.
       if (token_kind != Lex::TokenKind::Error) {
         CARBON_DIAGNOSTIC(ExpectedExpr, Error, "expected expression");
-        context.emitter().Emit(*context.position(), ExpectedExpr);
+        CARBON_DIAGNOSTIC(ExpectedPattern, Error, "expected pattern");
+        llvm::SmallVector<StateKind, 2> state_kinds(
+            llvm::map_range(llvm::ArrayRef(context.state_stack()).take_back(2),
+                            [&](const Context::State& s) { return s.kind; }));
+        bool in_pattern = state_kinds == llvm::ArrayRef{StateKind::ExprPattern,
+                                                        StateKind::ExprLoop};
+        context.emitter().Emit(*context.position(),
+                               in_pattern ? ExpectedPattern : ExpectedExpr);
       }
 
       // Add a node to keep the parse tree balanced.

+ 3 - 2
toolchain/parse/handle_let.cpp

@@ -17,8 +17,9 @@ auto HandleLet(Context& context) -> void {
   context.PushState(state, StateKind::LetFinishAsRegular);
   context.PushState(state, StateKind::LetAfterPatternAsRegular);
 
-  // This will start at the pattern.
-  context.PushState(StateKind::Pattern);
+  context.PushStateForPattern(StateKind::Pattern, /*in_var_pattern=*/false,
+                              /*in_unused_pattern=*/false,
+                              PrecedenceGroup::ForTopLevelPattern());
 }
 
 auto HandleAssociatedConstant(Context& context) -> void {

+ 3 - 1
toolchain/parse/handle_match.cpp

@@ -142,7 +142,9 @@ auto HandleMatchCaseIntroducer(Context& context) -> void {
 
   context.AddLeafNode(NodeKind::MatchCaseIntroducer, context.Consume());
   context.PushState(state, StateKind::MatchCaseAfterPattern);
-  context.PushState(StateKind::Pattern);
+  context.PushStateForPattern(StateKind::Pattern, /*in_var_pattern=*/false,
+                              /*in_unused_pattern=*/false,
+                              PrecedenceGroup::ForTopLevelPattern());
 }
 
 auto HandleMatchCaseAfterPattern(Context& context) -> void {

+ 59 - 9
toolchain/parse/handle_pattern.cpp

@@ -7,29 +7,79 @@
 
 namespace Carbon::Parse {
 
+static auto IsBindingPatternOperator(Lex::TokenKind kind) -> bool {
+  return kind == Lex::TokenKind::Colon ||
+         kind == Lex::TokenKind::ColonExclaim ||
+         kind == Lex::TokenKind::ColonQuestion;
+}
+
 auto HandlePattern(Context& context) -> void {
   auto state = context.PopState();
   switch (context.PositionKind()) {
     case Lex::TokenKind::OpenParen:
       context.PushStateForPattern(StateKind::PatternListAsTuple,
-                                  state.in_var_pattern,
-                                  state.in_unused_pattern);
+                                  state.in_var_pattern, state.in_unused_pattern,
+                                  state.ambient_precedence);
       break;
     case Lex::TokenKind::Var:
       context.PushStateForPattern(StateKind::VariablePattern,
-                                  state.in_var_pattern,
-                                  state.in_unused_pattern);
+                                  state.in_var_pattern, state.in_unused_pattern,
+                                  state.ambient_precedence);
       break;
     case Lex::TokenKind::Unused:
       context.PushStateForPattern(StateKind::UnusedPattern,
-                                  state.in_var_pattern,
-                                  state.in_unused_pattern);
+                                  state.in_var_pattern, state.in_unused_pattern,
+                                  state.ambient_precedence);
       break;
-    default:
+    case Lex::TokenKind::Template:
+    case Lex::TokenKind::Ref:
       context.PushStateForPattern(StateKind::BindingPattern,
-                                  state.in_var_pattern,
-                                  state.in_unused_pattern);
+                                  state.in_var_pattern, state.in_unused_pattern,
+                                  state.ambient_precedence);
       break;
+    case Lex::TokenKind::Identifier:
+    case Lex::TokenKind::SelfValueIdentifier:
+    case Lex::TokenKind::Underscore: {
+      if (IsBindingPatternOperator(
+              context.PositionKind(Lookahead::NextToken))) {
+        context.PushStateForPattern(
+            StateKind::BindingPattern, state.in_var_pattern,
+            state.in_unused_pattern, state.ambient_precedence);
+        break;
+      }
+      [[fallthrough]];
+    }
+    default:
+      context.PushState(StateKind::ExprPattern);
+      context.PushStateForExpr(state.ambient_precedence);
+      break;
+  }
+}
+
+auto HandleExprPattern(Context& context) -> void {
+  auto state = context.PopState();
+
+  // If we parsed an expression followed by a binding operator, we most likely
+  // have a malformed attempt to introduce a binding pattern that we interpreted
+  // as an expression pattern, so diagnose that here rather than diagnosing a
+  // missing `;` at an outer level.
+  if (IsBindingPatternOperator(context.PositionKind())) {
+    if (!state.has_error) {
+      CARBON_DIAGNOSTIC(ExpectedBindingName, Error,
+                        "unexpected expression before {0} in binding pattern",
+                        Lex::TokenKind);
+      // TODO: Underline the parsed expression.
+      context.emitter().Emit(*context.position(), ExpectedBindingName,
+                             context.PositionKind());
+      state.has_error = true;
+    }
+    context.Consume();
+    // It'd be nice to skip the type expression here too, but we can't determine
+    // the end of it.
+  }
+
+  if (state.has_error) {
+    context.ReturnErrorOnState();
   }
 }
 

+ 9 - 5
toolchain/parse/handle_pattern_list.cpp

@@ -13,9 +13,11 @@ static auto HandlePatternListElement(Context& context, StateKind pattern_state,
   auto state = context.PopState();
 
   context.PushStateForPattern(finish_state_kind, state.in_var_pattern,
-                              state.in_unused_pattern);
+                              state.in_unused_pattern,
+                              state.ambient_precedence);
   context.PushStateForPattern(pattern_state, state.in_var_pattern,
-                              state.in_unused_pattern);
+                              state.in_unused_pattern,
+                              state.ambient_precedence);
 }
 
 auto HandlePatternListElementAsTuple(Context& context) -> void {
@@ -58,7 +60,8 @@ static auto HandlePatternListElementFinish(Context& context,
 
   if (list_token_kind == Context::ListTokenKind::Comma) {
     context.PushStateForPattern(param_state_kind, state.in_var_pattern,
-                                state.in_unused_pattern);
+                                state.in_unused_pattern,
+                                state.ambient_precedence);
   }
 }
 
@@ -90,12 +93,13 @@ static auto HandlePatternList(Context& context, NodeKind node_kind,
 
   context.PushStateForPattern(
       empty ? finish_state_empty : finish_state_nonempty, state.in_var_pattern,
-      state.in_unused_pattern);
+      state.in_unused_pattern, state.ambient_precedence);
   context.AddLeafNode(node_kind, open_token);
 
   if (!empty) {
     context.PushStateForPattern(param_state, state.in_var_pattern,
-                                state.in_unused_pattern);
+                                state.in_unused_pattern,
+                                PrecedenceGroup::ForTopLevelExpr());
   }
 }
 

+ 3 - 1
toolchain/parse/handle_statement.cpp

@@ -117,7 +117,9 @@ auto HandleStatementForHeader(Context& context) -> void {
 
   state.kind = StateKind::StatementForHeaderIn;
   context.PushState(state);
-  context.PushState(StateKind::Pattern);
+  context.PushStateForPattern(StateKind::Pattern, /*in_var_pattern=*/false,
+                              /*in_unused_pattern=*/false,
+                              PrecedenceGroup::ForTopLevelPattern());
 }
 
 auto HandleStatementForHeaderIn(Context& context) -> void {

+ 2 - 1
toolchain/parse/handle_unused.cpp

@@ -19,7 +19,8 @@ auto HandleUnusedPattern(Context& context) -> void {
   context.ConsumeChecked(Lex::TokenKind::Unused);
 
   context.PushStateForPattern(StateKind::Pattern, state.in_var_pattern,
-                              /*in_unused_pattern=*/true);
+                              /*in_unused_pattern=*/true,
+                              state.ambient_precedence);
 }
 
 auto HandleFinishUnusedPattern(Context& context) -> void {

+ 4 - 2
toolchain/parse/handle_var.cpp

@@ -26,7 +26,8 @@ static auto HandleVar(Context& context, StateKind finish_state_kind,
   }
 
   context.PushStateForPattern(StateKind::Pattern, /*in_var_pattern=*/true,
-                              /*in_unused_pattern=*/false);
+                              /*in_unused_pattern=*/false,
+                              PrecedenceGroup::ForTopLevelPattern());
 }
 
 auto HandleVarAsRegular(Context& context) -> void {
@@ -144,7 +145,8 @@ auto HandleVariablePattern(Context& context) -> void {
   context.ConsumeChecked(Lex::TokenKind::Var);
 
   context.PushStateForPattern(StateKind::Pattern, /*in_var_pattern=*/true,
-                              state.in_unused_pattern);
+                              state.in_unused_pattern,
+                              state.ambient_precedence);
 }
 
 auto HandleFinishVariablePattern(Context& context) -> void {

+ 2 - 1
toolchain/parse/node_ids.h

@@ -106,7 +106,8 @@ using AnyMemberAccessId =
     NodeIdInCategory<NodeCategory::MemberName | NodeCategory::MemberExpr |
                      NodeCategory::IntConst>;
 using AnyModifierId = NodeIdInCategory<NodeCategory::Modifier>;
-using AnyPatternId = NodeIdInCategory<NodeCategory::Pattern>;
+using AnyPatternId =
+    NodeIdInCategory<NodeCategory::Pattern | NodeCategory::Expr>;
 using AnyStatementId =
     NodeIdInCategory<NodeCategory::Statement | NodeCategory::Decl>;
 using AnyRequireImplsId = NodeIdInCategory<NodeCategory::RequireImpls>;

+ 8 - 0
toolchain/parse/precedence.h

@@ -66,6 +66,10 @@ class PrecedenceGroup {
   // after `where`, `require`, or `observe`.
   static auto ForRequirements() -> PrecedenceGroup;
 
+  // Get the precedence level for a pattern that is not nested within another
+  // (eg, tuple or struct) pattern.
+  static auto ForTopLevelPattern() -> PrecedenceGroup;
+
   // Look up the operator information of the given prefix operator token, or
   // return std::nullopt if the given token is not a prefix operator.
   static auto ForLeading(Lex::TokenKind kind) -> std::optional<PrecedenceGroup>;
@@ -182,6 +186,10 @@ inline auto PrecedenceGroup::ForRequirements() -> PrecedenceGroup {
   return PrecedenceGroup(Where);
 }
 
+inline auto PrecedenceGroup::ForTopLevelPattern() -> PrecedenceGroup {
+  return PrecedenceGroup(Relational);
+}
+
 }  // namespace Carbon::Parse
 
 #endif  // CARBON_TOOLCHAIN_PARSE_PRECEDENCE_H_

+ 18 - 2
toolchain/parse/state.def

@@ -1145,15 +1145,31 @@ CARBON_PARSE_STATE(ParenExprFinish)
 //
 //   1. VariablePattern
 //
-//  ...
+//  unused ...
+// ^
+//   1. UnusedPattern
+//
+//  [identifier|self] [:|:!|:?]
+// ^
+//  template ...
+// ^
+//  ref ...
 // ^
 //   1. BindingPattern
 //
 //  ...
 // ^
-//   1. UnusedPattern
+//   1. Expr
+//   2. ExprPattern
 CARBON_PARSE_STATE(Pattern)
 
+// Tracks that we are parsing an expression as an expression pattern.
+//
+//  ...
+// ^
+//   (state done)
+CARBON_PARSE_STATE(ExprPattern)
+
 // Handles the initial part of a binding pattern, enqueuing type expression
 // processing.
 //

+ 5 - 7
toolchain/parse/testdata/basics/fail_paren_match_regression.carbon

@@ -8,7 +8,7 @@
 // TIP: To dump output, run:
 // TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/parse/testdata/basics/fail_paren_match_regression.carbon
 
-// CHECK:STDERR: fail_paren_match_regression.carbon:[[@LINE+8]]:5: error: expected name in binding pattern [ExpectedBindingPattern]
+// CHECK:STDERR: fail_paren_match_regression.carbon:[[@LINE+8]]:5: error: expected pattern [ExpectedPattern]
 // CHECK:STDERR: var = (foo {})
 // CHECK:STDERR:     ^
 // CHECK:STDERR:
@@ -18,7 +18,7 @@
 // CHECK:STDERR:
 var = (foo {})
 
-// CHECK:STDERR: fail_paren_match_regression.carbon:[[@LINE+18]]:21: error: `var` declarations must end with a `;` [ExpectedDeclSemi]
+// CHECK:STDERR: fail_paren_match_regression.carbon:[[@LINE+16]]:21: error: `var` declarations must end with a `;` [ExpectedDeclSemi]
 // CHECK:STDERR: // CHECK:STDOUT:   ]
 // CHECK:STDERR:                     ^
 // CHECK:STDERR:
@@ -26,14 +26,12 @@ var = (foo {})
 // CHECK:STDOUT:   parse_tree: [
 // CHECK:STDOUT:     {kind: 'FileStart', text: ''},
 // CHECK:STDOUT:       {kind: 'VariableIntroducer', text: 'var'},
-// CHECK:STDOUT:           {kind: 'IdentifierNameNotBeforeSignature', text: '=', has_error: yes},
-// CHECK:STDOUT:           {kind: 'InvalidParse', text: '=', has_error: yes},
-// CHECK:STDOUT:         {kind: 'VarBindingPattern', text: '=', has_error: yes, subtree_size: 3},
-// CHECK:STDOUT:       {kind: 'VariablePattern', text: 'var', has_error: yes, subtree_size: 4},
+// CHECK:STDOUT:         {kind: 'InvalidParse', text: '=', has_error: yes},
+// CHECK:STDOUT:       {kind: 'VariablePattern', text: 'var', has_error: yes, subtree_size: 2},
 // CHECK:STDOUT:       {kind: 'VariableInitializer', text: '='},
 // CHECK:STDOUT:         {kind: 'ParenExprStart', text: '('},
 // CHECK:STDOUT:         {kind: 'IdentifierNameExpr', text: 'foo'},
 // CHECK:STDOUT:       {kind: 'ParenExpr', text: ')', has_error: yes, subtree_size: 3},
-// CHECK:STDOUT:     {kind: 'VariableDecl', text: ')', has_error: yes, subtree_size: 10},
+// CHECK:STDOUT:     {kind: 'VariableDecl', text: ')', has_error: yes, subtree_size: 8},
 // CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
 // CHECK:STDOUT:   ]

+ 16 - 14
toolchain/parse/testdata/for/fail_missing_cond.carbon

@@ -9,16 +9,20 @@
 // TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/parse/testdata/for/fail_missing_cond.carbon
 
 fn F() {
-  // CHECK:STDERR: fail_missing_cond.carbon:[[@LINE+8]]:7: error: expected `(` after `for` [ExpectedParenAfter]
-  // CHECK:STDERR:   for {
-  // CHECK:STDERR:       ^
-  // CHECK:STDERR:
-  // CHECK:STDERR: fail_missing_cond.carbon:[[@LINE+4]]:7: error: expected name in binding pattern [ExpectedBindingPattern]
+  // CHECK:STDERR: fail_missing_cond.carbon:[[@LINE+4]]:7: error: expected `(` after `for` [ExpectedParenAfter]
   // CHECK:STDERR:   for {
   // CHECK:STDERR:       ^
   // CHECK:STDERR:
   for {
   }
+// CHECK:STDERR: fail_missing_cond.carbon:[[@LINE+16]]:1: error: expected `in` after loop pattern [ExpectedIn]
+// CHECK:STDERR: }
+// CHECK:STDERR: ^
+// CHECK:STDERR:
+// CHECK:STDERR: fail_missing_cond.carbon:[[@LINE+12]]:1: error: expected expression [ExpectedExpr]
+// CHECK:STDERR: }
+// CHECK:STDERR: ^
+// CHECK:STDERR:
 // CHECK:STDERR: fail_missing_cond.carbon:[[@LINE+8]]:1: error: expected braced code block [ExpectedCodeBlock]
 // CHECK:STDERR: }
 // CHECK:STDERR: ^
@@ -38,18 +42,16 @@ fn F() {
 // CHECK:STDOUT:         {kind: 'ExplicitParamList', text: ')', subtree_size: 2},
 // CHECK:STDOUT:       {kind: 'FunctionDefinitionStart', text: '{', subtree_size: 5},
 // CHECK:STDOUT:           {kind: 'ForHeaderStart', text: 'for', has_error: yes},
-// CHECK:STDOUT:               {kind: 'IdentifierNameNotBeforeSignature', text: '{', has_error: yes},
-// CHECK:STDOUT:               {kind: 'InvalidParse', text: '{', has_error: yes},
-// CHECK:STDOUT:             {kind: 'LetBindingPattern', text: '{', has_error: yes, subtree_size: 3},
-// CHECK:STDOUT:           {kind: 'ForIn', text: 'for', has_error: yes, subtree_size: 4},
-// CHECK:STDOUT:             {kind: 'StructLiteralStart', text: '{'},
-// CHECK:STDOUT:           {kind: 'StructLiteral', text: '}', subtree_size: 2},
-// CHECK:STDOUT:         {kind: 'ForHeader', text: 'for', has_error: yes, subtree_size: 8},
+// CHECK:STDOUT:               {kind: 'StructLiteralStart', text: '{'},
+// CHECK:STDOUT:             {kind: 'StructLiteral', text: '}', subtree_size: 2},
+// CHECK:STDOUT:           {kind: 'ForIn', text: 'for', has_error: yes, subtree_size: 3},
+// CHECK:STDOUT:           {kind: 'InvalidParse', text: '}', has_error: yes},
+// CHECK:STDOUT:         {kind: 'ForHeader', text: 'for', has_error: yes, subtree_size: 6},
 // CHECK:STDOUT:           {kind: 'CodeBlockStart', text: '}', has_error: yes},
 // CHECK:STDOUT:             {kind: 'InvalidParse', text: '}', has_error: yes},
 // CHECK:STDOUT:           {kind: 'ExprStatement', text: '}', has_error: yes, subtree_size: 2},
 // CHECK:STDOUT:         {kind: 'CodeBlock', text: '}', has_error: yes, subtree_size: 4},
-// CHECK:STDOUT:       {kind: 'ForStatement', text: 'for', subtree_size: 13},
-// CHECK:STDOUT:     {kind: 'FunctionDefinition', text: '}', subtree_size: 19},
+// CHECK:STDOUT:       {kind: 'ForStatement', text: 'for', subtree_size: 11},
+// CHECK:STDOUT:     {kind: 'FunctionDefinition', text: '}', subtree_size: 17},
 // CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
 // CHECK:STDOUT:   ]

+ 6 - 8
toolchain/parse/testdata/for/fail_returned_var.carbon

@@ -9,7 +9,7 @@
 // TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/parse/testdata/for/fail_returned_var.carbon
 
 fn foo() -> i32 {
-  // CHECK:STDERR: fail_returned_var.carbon:[[@LINE+4]]:8: error: expected name in binding pattern [ExpectedBindingPattern]
+  // CHECK:STDERR: fail_returned_var.carbon:[[@LINE+4]]:8: error: expected pattern [ExpectedPattern]
   // CHECK:STDERR:   for (returned var x: i32 in y) {
   // CHECK:STDERR:        ^~~~~~~~
   // CHECK:STDERR:
@@ -29,17 +29,15 @@ fn foo() -> i32 {
 // CHECK:STDOUT:         {kind: 'ReturnType', text: '->', subtree_size: 2},
 // CHECK:STDOUT:       {kind: 'FunctionDefinitionStart', text: '{', subtree_size: 7},
 // CHECK:STDOUT:           {kind: 'ForHeaderStart', text: '('},
-// CHECK:STDOUT:               {kind: 'IdentifierNameNotBeforeSignature', text: 'returned', has_error: yes},
-// CHECK:STDOUT:               {kind: 'InvalidParse', text: 'returned', has_error: yes},
-// CHECK:STDOUT:             {kind: 'LetBindingPattern', text: 'returned', has_error: yes, subtree_size: 3},
-// CHECK:STDOUT:           {kind: 'ForIn', text: '(', has_error: yes, subtree_size: 4},
-// CHECK:STDOUT:         {kind: 'ForHeader', text: ')', has_error: yes, subtree_size: 6},
+// CHECK:STDOUT:             {kind: 'InvalidParse', text: 'returned', has_error: yes},
+// CHECK:STDOUT:           {kind: 'ForIn', text: '(', has_error: yes, subtree_size: 2},
+// CHECK:STDOUT:         {kind: 'ForHeader', text: ')', has_error: yes, subtree_size: 4},
 // CHECK:STDOUT:           {kind: 'CodeBlockStart', text: '{'},
 // CHECK:STDOUT:             {kind: 'ReturnStatementStart', text: 'return'},
 // CHECK:STDOUT:             {kind: 'ReturnVarModifier', text: 'var'},
 // CHECK:STDOUT:           {kind: 'ReturnStatement', text: ';', subtree_size: 3},
 // CHECK:STDOUT:         {kind: 'CodeBlock', text: '}', subtree_size: 5},
-// CHECK:STDOUT:       {kind: 'ForStatement', text: 'for', subtree_size: 12},
-// CHECK:STDOUT:     {kind: 'FunctionDefinition', text: '}', subtree_size: 20},
+// CHECK:STDOUT:       {kind: 'ForStatement', text: 'for', subtree_size: 10},
+// CHECK:STDOUT:     {kind: 'FunctionDefinition', text: '}', subtree_size: 18},
 // CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
 // CHECK:STDOUT:   ]

+ 6 - 8
toolchain/parse/testdata/for/fail_square_brackets.carbon

@@ -13,7 +13,7 @@ fn F() {
   // CHECK:STDERR:   for [] {
   // CHECK:STDERR:       ^
   // CHECK:STDERR:
-  // CHECK:STDERR: fail_square_brackets.carbon:[[@LINE+16]]:7: error: expected name in binding pattern [ExpectedBindingPattern]
+  // CHECK:STDERR: fail_square_brackets.carbon:[[@LINE+16]]:7: error: expected pattern [ExpectedPattern]
   // CHECK:STDERR:   for [] {
   // CHECK:STDERR:       ^
   // CHECK:STDERR:
@@ -42,17 +42,15 @@ fn F() {
 // CHECK:STDOUT:         {kind: 'ExplicitParamList', text: ')', subtree_size: 2},
 // CHECK:STDOUT:       {kind: 'FunctionDefinitionStart', text: '{', subtree_size: 5},
 // CHECK:STDOUT:           {kind: 'ForHeaderStart', text: 'for', has_error: yes},
-// CHECK:STDOUT:               {kind: 'IdentifierNameNotBeforeSignature', text: '[', has_error: yes},
-// CHECK:STDOUT:               {kind: 'InvalidParse', text: '[', has_error: yes},
-// CHECK:STDOUT:             {kind: 'LetBindingPattern', text: '[', has_error: yes, subtree_size: 3},
-// CHECK:STDOUT:           {kind: 'ForIn', text: 'for', has_error: yes, subtree_size: 4},
+// CHECK:STDOUT:             {kind: 'InvalidParse', text: '[', has_error: yes},
+// CHECK:STDOUT:           {kind: 'ForIn', text: 'for', has_error: yes, subtree_size: 2},
 // CHECK:STDOUT:           {kind: 'InvalidParse', text: '[', has_error: yes},
-// CHECK:STDOUT:         {kind: 'ForHeader', text: 'for', has_error: yes, subtree_size: 7},
+// CHECK:STDOUT:         {kind: 'ForHeader', text: 'for', has_error: yes, subtree_size: 5},
 // CHECK:STDOUT:           {kind: 'CodeBlockStart', text: '[', has_error: yes},
 // CHECK:STDOUT:             {kind: 'InvalidParse', text: '[', has_error: yes},
 // CHECK:STDOUT:           {kind: 'ExprStatement', text: '}', has_error: yes, subtree_size: 2},
 // CHECK:STDOUT:         {kind: 'CodeBlock', text: '[', has_error: yes, subtree_size: 4},
-// CHECK:STDOUT:       {kind: 'ForStatement', text: 'for', subtree_size: 12},
-// CHECK:STDOUT:     {kind: 'FunctionDefinition', text: '}', subtree_size: 18},
+// CHECK:STDOUT:       {kind: 'ForStatement', text: 'for', subtree_size: 10},
+// CHECK:STDOUT:     {kind: 'FunctionDefinition', text: '}', subtree_size: 16},
 // CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
 // CHECK:STDOUT:   ]

+ 5 - 11
toolchain/parse/testdata/function/declaration.carbon

@@ -146,12 +146,8 @@ fn F();
 struct X { fn () }
 fn F();
 
-// --- fail_with_identifier_as_param.carbon
+// --- with_identifier_as_param.carbon
 
-// CHECK:STDERR: fail_with_identifier_as_param.carbon:[[@LINE+4]]:11: error: expected `:`, `:!`, or `:?` in binding pattern [ExpectedBindingPattern]
-// CHECK:STDERR: fn foo(bar);
-// CHECK:STDERR:           ^
-// CHECK:STDERR:
 fn foo(bar);
 
 // --- fail_without_name_and_many_tokens_in_params.carbon
@@ -426,17 +422,15 @@ fn ComplexReturnForm() ->? X.Y(Z);
 // CHECK:STDOUT:     {kind: 'FunctionDecl', text: ';', subtree_size: 5},
 // CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
 // CHECK:STDOUT:   ]
-// CHECK:STDOUT: - filename: fail_with_identifier_as_param.carbon
+// CHECK:STDOUT: - filename: with_identifier_as_param.carbon
 // CHECK:STDOUT:   parse_tree: [
 // CHECK:STDOUT:     {kind: 'FileStart', text: ''},
 // CHECK:STDOUT:       {kind: 'FunctionIntroducer', text: 'fn'},
 // CHECK:STDOUT:       {kind: 'IdentifierNameMaybeBeforeSignature', text: 'foo'},
 // CHECK:STDOUT:         {kind: 'ExplicitParamListStart', text: '('},
-// CHECK:STDOUT:           {kind: 'IdentifierNameNotBeforeSignature', text: 'bar'},
-// CHECK:STDOUT:           {kind: 'InvalidParse', text: ')', has_error: yes},
-// CHECK:STDOUT:         {kind: 'LetBindingPattern', text: 'bar', has_error: yes, subtree_size: 3},
-// CHECK:STDOUT:       {kind: 'ExplicitParamList', text: ')', has_error: yes, subtree_size: 5},
-// CHECK:STDOUT:     {kind: 'FunctionDecl', text: ';', subtree_size: 8},
+// CHECK:STDOUT:         {kind: 'IdentifierNameExpr', text: 'bar'},
+// CHECK:STDOUT:       {kind: 'ExplicitParamList', text: ')', subtree_size: 3},
+// CHECK:STDOUT:     {kind: 'FunctionDecl', text: ';', subtree_size: 6},
 // CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
 // CHECK:STDOUT:   ]
 // CHECK:STDOUT: - filename: fail_without_name_and_many_tokens_in_params.carbon

+ 4 - 10
toolchain/parse/testdata/generics/impl/fail_impl.carbon

@@ -54,10 +54,6 @@ impl forall f32;
 // CHECK:STDERR:
 impl forall [] u32;
 
-// CHECK:STDERR: fail_impl.carbon:[[@LINE+8]]:21: error: expected `:`, `:!`, or `:?` in binding pattern [ExpectedBindingPattern]
-// CHECK:STDERR: impl forall [invalid] i8;
-// CHECK:STDERR:                     ^
-// CHECK:STDERR:
 // CHECK:STDERR: fail_impl.carbon:[[@LINE+4]]:25: error: expected `as` in `impl` declaration [ImplExpectedAs]
 // CHECK:STDERR: impl forall [invalid] i8;
 // CHECK:STDERR:                         ^
@@ -96,7 +92,7 @@ impl;
 
 impl
 
-// CHECK:STDERR: fail_impl.carbon:[[@LINE+92]]:21: error: expected expression [ExpectedExpr]
+// CHECK:STDERR: fail_impl.carbon:[[@LINE+90]]:21: error: expected expression [ExpectedExpr]
 // CHECK:STDERR: // CHECK:STDOUT:   ]
 // CHECK:STDERR:                     ^
 // CHECK:STDERR:
@@ -136,12 +132,10 @@ impl
 // CHECK:STDOUT:       {kind: 'ImplIntroducer', text: 'impl'},
 // CHECK:STDOUT:       {kind: 'Forall', text: 'forall'},
 // CHECK:STDOUT:         {kind: 'ImplicitParamListStart', text: '['},
-// CHECK:STDOUT:           {kind: 'IdentifierNameNotBeforeSignature', text: 'invalid'},
-// CHECK:STDOUT:           {kind: 'InvalidParse', text: ']', has_error: yes},
-// CHECK:STDOUT:         {kind: 'LetBindingPattern', text: 'invalid', has_error: yes, subtree_size: 3},
-// CHECK:STDOUT:       {kind: 'ImplicitParamList', text: ']', has_error: yes, subtree_size: 5},
+// CHECK:STDOUT:         {kind: 'IdentifierNameExpr', text: 'invalid'},
+// CHECK:STDOUT:       {kind: 'ImplicitParamList', text: ']', subtree_size: 3},
 // CHECK:STDOUT:       {kind: 'IntTypeLiteral', text: 'i8'},
-// CHECK:STDOUT:     {kind: 'ImplDecl', text: ';', has_error: yes, subtree_size: 9},
+// CHECK:STDOUT:     {kind: 'ImplDecl', text: ';', has_error: yes, subtree_size: 7},
 // CHECK:STDOUT:       {kind: 'ImplIntroducer', text: 'impl'},
 // CHECK:STDOUT:       {kind: 'Forall', text: 'forall'},
 // CHECK:STDOUT:       {kind: 'InvalidParse', text: 'f16', has_error: yes},

+ 8 - 16
toolchain/parse/testdata/generics/interface/fail_self_param_syntax.carbon

@@ -9,16 +9,12 @@
 // TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/parse/testdata/generics/interface/fail_self_param_syntax.carbon
 
 interface Foo {
-  // CHECK:STDERR: fail_self_param_syntax.carbon:[[@LINE+4]]:13: error: expected `:`, `:!`, or `:?` in binding pattern [ExpectedBindingPattern]
+  // CHECK:STDERR: fail_self_param_syntax.carbon:[[@LINE+4]]:13: error: expected `,` or `]` [UnexpectedTokenAfterListElement]
   // CHECK:STDERR:   fn Sub[me Self](b: Self) -> Self;
   // CHECK:STDERR:             ^~~~
   // CHECK:STDERR:
   fn Sub[me Self](b: Self) -> Self;
 
-  // CHECK:STDERR: fail_self_param_syntax.carbon:[[@LINE+4]]:10: error: expected name in binding pattern [ExpectedBindingPattern]
-  // CHECK:STDERR:   fn Mul[Self](b: Self) -> Self;
-  // CHECK:STDERR:          ^~~~
-  // CHECK:STDERR:
   fn Mul[Self](b: Self) -> Self;
 }
 
@@ -31,10 +27,8 @@ interface Foo {
 // CHECK:STDOUT:         {kind: 'FunctionIntroducer', text: 'fn'},
 // CHECK:STDOUT:         {kind: 'IdentifierNameMaybeBeforeSignature', text: 'Sub'},
 // CHECK:STDOUT:           {kind: 'ImplicitParamListStart', text: '['},
-// CHECK:STDOUT:             {kind: 'IdentifierNameNotBeforeSignature', text: 'me'},
-// CHECK:STDOUT:             {kind: 'InvalidParse', text: 'Self', has_error: yes},
-// CHECK:STDOUT:           {kind: 'LetBindingPattern', text: 'me', has_error: yes, subtree_size: 3},
-// CHECK:STDOUT:         {kind: 'ImplicitParamList', text: ']', has_error: yes, subtree_size: 5},
+// CHECK:STDOUT:           {kind: 'IdentifierNameExpr', text: 'me'},
+// CHECK:STDOUT:         {kind: 'ImplicitParamList', text: ']', has_error: yes, subtree_size: 3},
 // CHECK:STDOUT:           {kind: 'ExplicitParamListStart', text: '('},
 // CHECK:STDOUT:             {kind: 'IdentifierNameNotBeforeSignature', text: 'b'},
 // CHECK:STDOUT:             {kind: 'SelfTypeNameExpr', text: 'Self'},
@@ -42,14 +36,12 @@ interface Foo {
 // CHECK:STDOUT:         {kind: 'ExplicitParamList', text: ')', subtree_size: 5},
 // CHECK:STDOUT:           {kind: 'SelfTypeNameExpr', text: 'Self'},
 // CHECK:STDOUT:         {kind: 'ReturnType', text: '->', subtree_size: 2},
-// CHECK:STDOUT:       {kind: 'FunctionDecl', text: ';', subtree_size: 15},
+// CHECK:STDOUT:       {kind: 'FunctionDecl', text: ';', subtree_size: 13},
 // CHECK:STDOUT:         {kind: 'FunctionIntroducer', text: 'fn'},
 // CHECK:STDOUT:         {kind: 'IdentifierNameMaybeBeforeSignature', text: 'Mul'},
 // CHECK:STDOUT:           {kind: 'ImplicitParamListStart', text: '['},
-// CHECK:STDOUT:             {kind: 'IdentifierNameNotBeforeSignature', text: 'Self', has_error: yes},
-// CHECK:STDOUT:             {kind: 'InvalidParse', text: 'Self', has_error: yes},
-// CHECK:STDOUT:           {kind: 'LetBindingPattern', text: 'Self', has_error: yes, subtree_size: 3},
-// CHECK:STDOUT:         {kind: 'ImplicitParamList', text: ']', has_error: yes, subtree_size: 5},
+// CHECK:STDOUT:           {kind: 'SelfTypeNameExpr', text: 'Self'},
+// CHECK:STDOUT:         {kind: 'ImplicitParamList', text: ']', subtree_size: 3},
 // CHECK:STDOUT:           {kind: 'ExplicitParamListStart', text: '('},
 // CHECK:STDOUT:             {kind: 'IdentifierNameNotBeforeSignature', text: 'b'},
 // CHECK:STDOUT:             {kind: 'SelfTypeNameExpr', text: 'Self'},
@@ -57,7 +49,7 @@ interface Foo {
 // CHECK:STDOUT:         {kind: 'ExplicitParamList', text: ')', subtree_size: 5},
 // CHECK:STDOUT:           {kind: 'SelfTypeNameExpr', text: 'Self'},
 // CHECK:STDOUT:         {kind: 'ReturnType', text: '->', subtree_size: 2},
-// CHECK:STDOUT:       {kind: 'FunctionDecl', text: ';', subtree_size: 15},
-// CHECK:STDOUT:     {kind: 'InterfaceDefinition', text: '}', subtree_size: 34},
+// CHECK:STDOUT:       {kind: 'FunctionDecl', text: ';', subtree_size: 13},
+// CHECK:STDOUT:     {kind: 'InterfaceDefinition', text: '}', subtree_size: 30},
 // CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
 // CHECK:STDOUT:   ]

+ 141 - 0
toolchain/parse/testdata/let/expression_pattern_precedence.carbon

@@ -0,0 +1,141 @@
+// 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
+// TIP: To test this file alone, run:
+// TIP:   bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/parse/testdata/let/expression_pattern_precedence.carbon
+// TIP: To dump output, run:
+// TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/parse/testdata/let/expression_pattern_precedence.carbon
+
+// --- parens.carbon
+
+fn F() {
+  let (a == b) = c;
+
+  let (a and b) = c;
+
+  let (if true then 1 else 2) = 3;
+}
+
+// --- fail_relational.carbon
+
+fn G() {
+  // CHECK:STDERR: fail_relational.carbon:[[@LINE+4]]:9: error: `let` declarations must end with a `;` [ExpectedDeclSemi]
+  // CHECK:STDERR:   let a == b = c;
+  // CHECK:STDERR:         ^~
+  // CHECK:STDERR:
+  let a == b = c;
+}
+
+// --- fail_logical.carbon
+
+fn H() {
+  // CHECK:STDERR: fail_logical.carbon:[[@LINE+4]]:9: error: `let` declarations must end with a `;` [ExpectedDeclSemi]
+  // CHECK:STDERR:   let a and b = c;
+  // CHECK:STDERR:         ^~~
+  // CHECK:STDERR:
+  let a and b = c;
+}
+
+// --- fail_if.carbon
+
+fn I() {
+  // CHECK:STDERR: fail_if.carbon:[[@LINE+4]]:7: error: parentheses are required around this unary `if` operator [UnaryOperatorRequiresParentheses]
+  // CHECK:STDERR:   let if true then 1 else 2 = 3;
+  // CHECK:STDERR:       ^~
+  // CHECK:STDERR:
+  let if true then 1 else 2 = 3;
+}
+
+// CHECK:STDOUT: - filename: parens.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:         {kind: 'FunctionIntroducer', text: 'fn'},
+// CHECK:STDOUT:         {kind: 'IdentifierNameMaybeBeforeSignature', text: 'F'},
+// CHECK:STDOUT:           {kind: 'ExplicitParamListStart', text: '('},
+// CHECK:STDOUT:         {kind: 'ExplicitParamList', text: ')', subtree_size: 2},
+// CHECK:STDOUT:       {kind: 'FunctionDefinitionStart', text: '{', subtree_size: 5},
+// CHECK:STDOUT:         {kind: 'LetIntroducer', text: 'let'},
+// CHECK:STDOUT:           {kind: 'TuplePatternStart', text: '('},
+// CHECK:STDOUT:             {kind: 'IdentifierNameExpr', text: 'a'},
+// CHECK:STDOUT:             {kind: 'IdentifierNameExpr', text: 'b'},
+// CHECK:STDOUT:           {kind: 'InfixOperatorEqualEqual', text: '==', subtree_size: 3},
+// CHECK:STDOUT:         {kind: 'ParenPattern', text: ')', subtree_size: 5},
+// CHECK:STDOUT:         {kind: 'LetInitializer', text: '='},
+// CHECK:STDOUT:         {kind: 'IdentifierNameExpr', text: 'c'},
+// CHECK:STDOUT:       {kind: 'LetDecl', text: ';', subtree_size: 9},
+// CHECK:STDOUT:         {kind: 'LetIntroducer', text: 'let'},
+// CHECK:STDOUT:           {kind: 'TuplePatternStart', text: '('},
+// CHECK:STDOUT:               {kind: 'IdentifierNameExpr', text: 'a'},
+// CHECK:STDOUT:             {kind: 'ShortCircuitOperandAnd', text: 'and', subtree_size: 2},
+// CHECK:STDOUT:             {kind: 'IdentifierNameExpr', text: 'b'},
+// CHECK:STDOUT:           {kind: 'ShortCircuitOperatorAnd', text: 'and', subtree_size: 4},
+// CHECK:STDOUT:         {kind: 'ParenPattern', text: ')', subtree_size: 6},
+// CHECK:STDOUT:         {kind: 'LetInitializer', text: '='},
+// CHECK:STDOUT:         {kind: 'IdentifierNameExpr', text: 'c'},
+// CHECK:STDOUT:       {kind: 'LetDecl', text: ';', subtree_size: 10},
+// CHECK:STDOUT:         {kind: 'LetIntroducer', text: 'let'},
+// CHECK:STDOUT:           {kind: 'TuplePatternStart', text: '('},
+// CHECK:STDOUT:               {kind: 'BoolLiteralTrue', text: 'true'},
+// CHECK:STDOUT:             {kind: 'IfExprIf', text: 'if', subtree_size: 2},
+// CHECK:STDOUT:               {kind: 'IntLiteral', text: '1'},
+// CHECK:STDOUT:             {kind: 'IfExprThen', text: 'then', subtree_size: 2},
+// CHECK:STDOUT:             {kind: 'IntLiteral', text: '2'},
+// CHECK:STDOUT:           {kind: 'IfExprElse', text: 'else', subtree_size: 6},
+// CHECK:STDOUT:         {kind: 'ParenPattern', text: ')', subtree_size: 8},
+// CHECK:STDOUT:         {kind: 'LetInitializer', text: '='},
+// CHECK:STDOUT:         {kind: 'IntLiteral', text: '3'},
+// CHECK:STDOUT:       {kind: 'LetDecl', text: ';', subtree_size: 12},
+// CHECK:STDOUT:     {kind: 'FunctionDefinition', text: '}', subtree_size: 37},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]
+// CHECK:STDOUT: - filename: fail_relational.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:         {kind: 'FunctionIntroducer', text: 'fn'},
+// CHECK:STDOUT:         {kind: 'IdentifierNameMaybeBeforeSignature', text: 'G'},
+// CHECK:STDOUT:           {kind: 'ExplicitParamListStart', text: '('},
+// CHECK:STDOUT:         {kind: 'ExplicitParamList', text: ')', subtree_size: 2},
+// CHECK:STDOUT:       {kind: 'FunctionDefinitionStart', text: '{', subtree_size: 5},
+// CHECK:STDOUT:         {kind: 'LetIntroducer', text: 'let'},
+// CHECK:STDOUT:         {kind: 'IdentifierNameExpr', text: 'a'},
+// CHECK:STDOUT:       {kind: 'LetDecl', text: ';', has_error: yes, subtree_size: 3},
+// CHECK:STDOUT:     {kind: 'FunctionDefinition', text: '}', subtree_size: 9},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]
+// CHECK:STDOUT: - filename: fail_logical.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:         {kind: 'FunctionIntroducer', text: 'fn'},
+// CHECK:STDOUT:         {kind: 'IdentifierNameMaybeBeforeSignature', text: 'H'},
+// CHECK:STDOUT:           {kind: 'ExplicitParamListStart', text: '('},
+// CHECK:STDOUT:         {kind: 'ExplicitParamList', text: ')', subtree_size: 2},
+// CHECK:STDOUT:       {kind: 'FunctionDefinitionStart', text: '{', subtree_size: 5},
+// CHECK:STDOUT:         {kind: 'LetIntroducer', text: 'let'},
+// CHECK:STDOUT:         {kind: 'IdentifierNameExpr', text: 'a'},
+// CHECK:STDOUT:       {kind: 'LetDecl', text: ';', has_error: yes, subtree_size: 3},
+// CHECK:STDOUT:     {kind: 'FunctionDefinition', text: '}', subtree_size: 9},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]
+// CHECK:STDOUT: - filename: fail_if.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:         {kind: 'FunctionIntroducer', text: 'fn'},
+// CHECK:STDOUT:         {kind: 'IdentifierNameMaybeBeforeSignature', text: 'I'},
+// CHECK:STDOUT:           {kind: 'ExplicitParamListStart', text: '('},
+// CHECK:STDOUT:         {kind: 'ExplicitParamList', text: ')', subtree_size: 2},
+// CHECK:STDOUT:       {kind: 'FunctionDefinitionStart', text: '{', subtree_size: 5},
+// CHECK:STDOUT:         {kind: 'LetIntroducer', text: 'let'},
+// CHECK:STDOUT:             {kind: 'BoolLiteralTrue', text: 'true'},
+// CHECK:STDOUT:           {kind: 'IfExprIf', text: 'if', subtree_size: 2},
+// CHECK:STDOUT:             {kind: 'IntLiteral', text: '1'},
+// CHECK:STDOUT:           {kind: 'IfExprThen', text: 'then', subtree_size: 2},
+// CHECK:STDOUT:           {kind: 'IntLiteral', text: '2'},
+// CHECK:STDOUT:         {kind: 'IfExprElse', text: 'else', subtree_size: 6},
+// CHECK:STDOUT:         {kind: 'LetInitializer', text: '='},
+// CHECK:STDOUT:         {kind: 'IntLiteral', text: '3'},
+// CHECK:STDOUT:       {kind: 'LetDecl', text: ';', subtree_size: 10},
+// CHECK:STDOUT:     {kind: 'FunctionDefinition', text: '}', subtree_size: 16},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]

+ 3 - 5
toolchain/parse/testdata/let/fail_bad_name.carbon

@@ -8,7 +8,7 @@
 // TIP: To dump output, run:
 // TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/parse/testdata/let/fail_bad_name.carbon
 
-// CHECK:STDERR: fail_bad_name.carbon:[[@LINE+4]]:5: error: expected name in binding pattern [ExpectedBindingPattern]
+// CHECK:STDERR: fail_bad_name.carbon:[[@LINE+4]]:5: error: expected pattern [ExpectedPattern]
 // CHECK:STDERR: let ? = 4;
 // CHECK:STDERR:     ^
 // CHECK:STDERR:
@@ -18,11 +18,9 @@ let ? = 4;
 // CHECK:STDOUT:   parse_tree: [
 // CHECK:STDOUT:     {kind: 'FileStart', text: ''},
 // CHECK:STDOUT:       {kind: 'LetIntroducer', text: 'let'},
-// CHECK:STDOUT:         {kind: 'IdentifierNameNotBeforeSignature', text: '?', has_error: yes},
-// CHECK:STDOUT:         {kind: 'InvalidParse', text: '?', has_error: yes},
-// CHECK:STDOUT:       {kind: 'LetBindingPattern', text: '?', has_error: yes, subtree_size: 3},
+// CHECK:STDOUT:       {kind: 'InvalidParse', text: '?', has_error: yes},
 // CHECK:STDOUT:       {kind: 'LetInitializer', text: '='},
 // CHECK:STDOUT:       {kind: 'IntLiteral', text: '4'},
-// CHECK:STDOUT:     {kind: 'LetDecl', text: ';', subtree_size: 7},
+// CHECK:STDOUT:     {kind: 'LetDecl', text: ';', subtree_size: 5},
 // CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
 // CHECK:STDOUT:   ]

+ 3 - 5
toolchain/parse/testdata/let/fail_empty.carbon

@@ -8,7 +8,7 @@
 // TIP: To dump output, run:
 // TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/parse/testdata/let/fail_empty.carbon
 
-// CHECK:STDERR: fail_empty.carbon:[[@LINE+4]]:4: error: expected name in binding pattern [ExpectedBindingPattern]
+// CHECK:STDERR: fail_empty.carbon:[[@LINE+4]]:4: error: expected pattern [ExpectedPattern]
 // CHECK:STDERR: let;
 // CHECK:STDERR:    ^
 // CHECK:STDERR:
@@ -18,9 +18,7 @@ let;
 // CHECK:STDOUT:   parse_tree: [
 // CHECK:STDOUT:     {kind: 'FileStart', text: ''},
 // CHECK:STDOUT:       {kind: 'LetIntroducer', text: 'let'},
-// CHECK:STDOUT:         {kind: 'IdentifierNameNotBeforeSignature', text: ';', has_error: yes},
-// CHECK:STDOUT:         {kind: 'InvalidParse', text: ';', has_error: yes},
-// CHECK:STDOUT:       {kind: 'LetBindingPattern', text: ';', has_error: yes, subtree_size: 3},
-// CHECK:STDOUT:     {kind: 'LetDecl', text: ';', subtree_size: 5},
+// CHECK:STDOUT:       {kind: 'InvalidParse', text: ';', has_error: yes},
+// CHECK:STDOUT:     {kind: 'LetDecl', text: ';', subtree_size: 3},
 // CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
 // CHECK:STDOUT:   ]

+ 6 - 11
toolchain/parse/testdata/let/fail_missing_name.carbon

@@ -10,7 +10,7 @@
 
 // --- fail_runtime_binding.carbon
 
-// CHECK:STDERR: fail_runtime_binding.carbon:[[@LINE+4]]:5: error: expected name in binding pattern [ExpectedBindingPattern]
+// CHECK:STDERR: fail_runtime_binding.carbon:[[@LINE+4]]:5: error: expected pattern [ExpectedPattern]
 // CHECK:STDERR: let : i32 = 4;
 // CHECK:STDERR:     ^
 // CHECK:STDERR:
@@ -18,7 +18,7 @@ let : i32 = 4;
 
 // --- fail_compiletime_binding.carbon
 
-// CHECK:STDERR: fail_compiletime_binding.carbon:[[@LINE+4]]:5: error: expected name in binding pattern [ExpectedBindingPattern]
+// CHECK:STDERR: fail_compiletime_binding.carbon:[[@LINE+4]]:5: error: expected pattern [ExpectedPattern]
 // CHECK:STDERR: let :! bool = true;
 // CHECK:STDERR:     ^~
 // CHECK:STDERR:
@@ -28,24 +28,19 @@ let :! bool = true;
 // CHECK:STDOUT:   parse_tree: [
 // CHECK:STDOUT:     {kind: 'FileStart', text: ''},
 // CHECK:STDOUT:       {kind: 'LetIntroducer', text: 'let'},
-// CHECK:STDOUT:         {kind: 'IdentifierNameNotBeforeSignature', text: ':', has_error: yes},
-// CHECK:STDOUT:         {kind: 'IntTypeLiteral', text: 'i32'},
-// CHECK:STDOUT:       {kind: 'LetBindingPattern', text: ':', has_error: yes, subtree_size: 3},
+// CHECK:STDOUT:       {kind: 'InvalidParse', text: ':', has_error: yes},
 // CHECK:STDOUT:       {kind: 'LetInitializer', text: '='},
 // CHECK:STDOUT:       {kind: 'IntLiteral', text: '4'},
-// CHECK:STDOUT:     {kind: 'LetDecl', text: ';', subtree_size: 7},
+// CHECK:STDOUT:     {kind: 'LetDecl', text: ';', subtree_size: 5},
 // CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
 // CHECK:STDOUT:   ]
 // CHECK:STDOUT: - filename: fail_compiletime_binding.carbon
 // CHECK:STDOUT:   parse_tree: [
 // CHECK:STDOUT:     {kind: 'FileStart', text: ''},
 // CHECK:STDOUT:       {kind: 'LetIntroducer', text: 'let'},
-// CHECK:STDOUT:           {kind: 'IdentifierNameNotBeforeSignature', text: ':!', has_error: yes},
-// CHECK:STDOUT:         {kind: 'CompileTimeBindingPatternStart', text: ':!', has_error: yes, subtree_size: 2},
-// CHECK:STDOUT:         {kind: 'BoolTypeLiteral', text: 'bool'},
-// CHECK:STDOUT:       {kind: 'CompileTimeBindingPattern', text: ':!', has_error: yes, subtree_size: 4},
+// CHECK:STDOUT:       {kind: 'InvalidParse', text: ':!', has_error: yes},
 // CHECK:STDOUT:       {kind: 'LetInitializer', text: '='},
 // CHECK:STDOUT:       {kind: 'BoolLiteralTrue', text: 'true'},
-// CHECK:STDOUT:     {kind: 'LetDecl', text: ';', subtree_size: 8},
+// CHECK:STDOUT:     {kind: 'LetDecl', text: ';', subtree_size: 5},
 // CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
 // CHECK:STDOUT:   ]

+ 4 - 6
toolchain/parse/testdata/let/fail_no_semi.carbon

@@ -10,11 +10,11 @@
 
 let
 
-// CHECK:STDERR: fail_no_semi.carbon:[[@LINE+17]]:21: error: expected name in binding pattern [ExpectedBindingPattern]
+// CHECK:STDERR: fail_no_semi.carbon:[[@LINE+15]]:21: error: expected pattern [ExpectedPattern]
 // CHECK:STDERR: // CHECK:STDOUT:   ]
 // CHECK:STDERR:                     ^
 // CHECK:STDERR:
-// CHECK:STDERR: fail_no_semi.carbon:[[@LINE+13]]:21: error: `let` declarations must end with a `;` [ExpectedDeclSemi]
+// CHECK:STDERR: fail_no_semi.carbon:[[@LINE+11]]:21: error: `let` declarations must end with a `;` [ExpectedDeclSemi]
 // CHECK:STDERR: // CHECK:STDOUT:   ]
 // CHECK:STDERR:                     ^
 // CHECK:STDERR:
@@ -22,9 +22,7 @@ let
 // CHECK:STDOUT:   parse_tree: [
 // CHECK:STDOUT:     {kind: 'FileStart', text: ''},
 // CHECK:STDOUT:       {kind: 'LetIntroducer', text: 'let'},
-// CHECK:STDOUT:         {kind: 'IdentifierNameNotBeforeSignature', text: '', has_error: yes},
-// CHECK:STDOUT:         {kind: 'InvalidParse', text: '', has_error: yes},
-// CHECK:STDOUT:       {kind: 'LetBindingPattern', text: '', has_error: yes, subtree_size: 3},
-// CHECK:STDOUT:     {kind: 'LetDecl', text: 'let', has_error: yes, subtree_size: 5},
+// CHECK:STDOUT:       {kind: 'InvalidParse', text: '', has_error: yes},
+// CHECK:STDOUT:     {kind: 'LetDecl', text: 'let', has_error: yes, subtree_size: 3},
 // CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
 // CHECK:STDOUT:   ]

+ 5 - 11
toolchain/parse/testdata/let/fail_missing_type.carbon → toolchain/parse/testdata/let/missing_type.carbon

@@ -4,25 +4,19 @@
 //
 // AUTOUPDATE
 // TIP: To test this file alone, run:
-// TIP:   bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/parse/testdata/let/fail_missing_type.carbon
+// TIP:   bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/parse/testdata/let/missing_type.carbon
 // TIP: To dump output, run:
-// TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/parse/testdata/let/fail_missing_type.carbon
+// TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/parse/testdata/let/missing_type.carbon
 
-// CHECK:STDERR: fail_missing_type.carbon:[[@LINE+4]]:7: error: expected `:`, `:!`, or `:?` in binding pattern [ExpectedBindingPattern]
-// CHECK:STDERR: let a = 4;
-// CHECK:STDERR:       ^
-// CHECK:STDERR:
 let a = 4;
 
-// CHECK:STDOUT: - filename: fail_missing_type.carbon
+// CHECK:STDOUT: - filename: missing_type.carbon
 // CHECK:STDOUT:   parse_tree: [
 // CHECK:STDOUT:     {kind: 'FileStart', text: ''},
 // CHECK:STDOUT:       {kind: 'LetIntroducer', text: 'let'},
-// CHECK:STDOUT:         {kind: 'IdentifierNameNotBeforeSignature', text: 'a'},
-// CHECK:STDOUT:         {kind: 'InvalidParse', text: '=', has_error: yes},
-// CHECK:STDOUT:       {kind: 'LetBindingPattern', text: 'a', has_error: yes, subtree_size: 3},
+// CHECK:STDOUT:       {kind: 'IdentifierNameExpr', text: 'a'},
 // CHECK:STDOUT:       {kind: 'LetInitializer', text: '='},
 // CHECK:STDOUT:       {kind: 'IntLiteral', text: '4'},
-// CHECK:STDOUT:     {kind: 'LetDecl', text: ';', subtree_size: 7},
+// CHECK:STDOUT:     {kind: 'LetDecl', text: ';', subtree_size: 5},
 // CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
 // CHECK:STDOUT:   ]

+ 7 - 9
toolchain/parse/testdata/match/fail_missing_case_pattern.carbon

@@ -10,7 +10,7 @@
 
 fn f() -> i32 {
   match (3) {
-    // CHECK:STDERR: fail_missing_case_pattern.carbon:[[@LINE+4]]:10: error: expected name in binding pattern [ExpectedBindingPattern]
+    // CHECK:STDERR: fail_missing_case_pattern.carbon:[[@LINE+4]]:10: error: expected pattern [ExpectedPattern]
     // CHECK:STDERR:     case => { return 2; }
     // CHECK:STDERR:          ^~
     // CHECK:STDERR:
@@ -35,16 +35,14 @@ fn f() -> i32 {
 // 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: 'IdentifierNameNotBeforeSignature', text: '=>', has_error: yes},
-// CHECK:STDOUT:                 {kind: 'InvalidParse', text: '=>', has_error: yes},
-// CHECK:STDOUT:               {kind: 'LetBindingPattern', text: '=>', has_error: yes, subtree_size: 3},
-// CHECK:STDOUT:             {kind: 'MatchCase', text: '=>', has_error: yes, subtree_size: 5},
-// CHECK:STDOUT:           {kind: 'MatchHandlerStart', text: '=>', has_error: yes, subtree_size: 6},
-// CHECK:STDOUT:         {kind: 'MatchHandler', text: '=>', has_error: yes, subtree_size: 7},
-// CHECK:STDOUT:       {kind: 'MatchStatement', text: '}', subtree_size: 13},
+// CHECK:STDOUT:               {kind: 'InvalidParse', text: '=>', has_error: yes},
+// CHECK:STDOUT:             {kind: 'MatchCase', text: '=>', has_error: yes, subtree_size: 3},
+// CHECK:STDOUT:           {kind: 'MatchHandlerStart', text: '=>', has_error: yes, subtree_size: 4},
+// CHECK:STDOUT:         {kind: 'MatchHandler', text: '=>', has_error: yes, subtree_size: 5},
+// CHECK:STDOUT:       {kind: 'MatchStatement', text: '}', subtree_size: 11},
 // 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: 'FunctionDefinition', text: '}', subtree_size: 22},
 // CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
 // CHECK:STDOUT:   ]

+ 22 - 5
toolchain/parse/testdata/package_expr/fail_in_name.carbon

@@ -8,9 +8,20 @@
 // TIP: To dump output, run:
 // TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/parse/testdata/package_expr/fail_in_name.carbon
 
-// CHECK:STDERR: fail_in_name.carbon:[[@LINE+4]]:5: error: expected name in binding pattern [ExpectedBindingPattern]
+// CHECK:STDERR: fail_in_name.carbon:[[@LINE+4]]:14: error: unexpected expression before : in binding pattern [ExpectedBindingName]
+// CHECK:STDERR: var package.n: i32;
+// CHECK:STDERR:              ^
+// CHECK:STDERR:
+var package.n: i32;
+
+// `val` is a keyword, but during error recovery we treat it as an identifier.
+// CHECK:STDERR: fail_in_name.carbon:[[@LINE+8]]:13: error: expected identifier after `.` [ExpectedIdentifierAfterPeriodOrArrow]
+// CHECK:STDERR: var package.val: i32;
+// CHECK:STDERR:             ^~~
+// CHECK:STDERR:
+// CHECK:STDERR: fail_in_name.carbon:[[@LINE+4]]:16: error: unexpected expression before : in binding pattern [ExpectedBindingName]
 // CHECK:STDERR: var package.val: i32;
-// CHECK:STDERR:     ^~~~~~~
+// CHECK:STDERR:                ^
 // CHECK:STDERR:
 var package.val: i32;
 
@@ -31,9 +42,15 @@ class package.C {
 // CHECK:STDOUT:   parse_tree: [
 // CHECK:STDOUT:     {kind: 'FileStart', text: ''},
 // CHECK:STDOUT:       {kind: 'VariableIntroducer', text: 'var'},
-// CHECK:STDOUT:           {kind: 'IdentifierNameNotBeforeSignature', text: 'package', has_error: yes},
-// CHECK:STDOUT:           {kind: 'InvalidParse', text: 'package', has_error: yes},
-// CHECK:STDOUT:         {kind: 'VarBindingPattern', text: 'package', has_error: yes, subtree_size: 3},
+// CHECK:STDOUT:           {kind: 'PackageExpr', text: 'package'},
+// CHECK:STDOUT:           {kind: 'IdentifierNameNotBeforeSignature', text: 'n'},
+// CHECK:STDOUT:         {kind: 'MemberAccessExpr', text: '.', subtree_size: 3},
+// CHECK:STDOUT:       {kind: 'VariablePattern', text: 'var', has_error: yes, subtree_size: 4},
+// CHECK:STDOUT:     {kind: 'VariableDecl', text: ';', subtree_size: 6},
+// CHECK:STDOUT:       {kind: 'VariableIntroducer', text: 'var'},
+// CHECK:STDOUT:           {kind: 'PackageExpr', text: 'package'},
+// CHECK:STDOUT:           {kind: 'IdentifierNameNotBeforeSignature', text: 'val', has_error: yes},
+// CHECK:STDOUT:         {kind: 'MemberAccessExpr', text: '.', subtree_size: 3},
 // CHECK:STDOUT:       {kind: 'VariablePattern', text: 'var', has_error: yes, subtree_size: 4},
 // CHECK:STDOUT:     {kind: 'VariableDecl', text: ';', subtree_size: 6},
 // CHECK:STDOUT:       {kind: 'NamespaceStart', text: 'namespace'},

+ 6 - 7
toolchain/parse/testdata/var/fail_bad_name.carbon

@@ -8,9 +8,9 @@
 // TIP: To dump output, run:
 // TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/parse/testdata/var/fail_bad_name.carbon
 
-// CHECK:STDERR: fail_bad_name.carbon:[[@LINE+4]]:5: error: expected name in binding pattern [ExpectedBindingPattern]
+// CHECK:STDERR: fail_bad_name.carbon:[[@LINE+4]]:6: error: expected expression [ExpectedExpr]
 // CHECK:STDERR: var *;
-// CHECK:STDERR:     ^
+// CHECK:STDERR:      ^
 // CHECK:STDERR:
 var *;
 
@@ -18,10 +18,9 @@ var *;
 // CHECK:STDOUT:   parse_tree: [
 // CHECK:STDOUT:     {kind: 'FileStart', text: ''},
 // CHECK:STDOUT:       {kind: 'VariableIntroducer', text: 'var'},
-// CHECK:STDOUT:           {kind: 'IdentifierNameNotBeforeSignature', text: '*', has_error: yes},
-// CHECK:STDOUT:           {kind: 'InvalidParse', text: '*', has_error: yes},
-// CHECK:STDOUT:         {kind: 'VarBindingPattern', text: '*', has_error: yes, subtree_size: 3},
-// CHECK:STDOUT:       {kind: 'VariablePattern', text: 'var', has_error: yes, subtree_size: 4},
-// CHECK:STDOUT:     {kind: 'VariableDecl', text: ';', subtree_size: 6},
+// CHECK:STDOUT:           {kind: 'InvalidParse', text: ';', has_error: yes},
+// CHECK:STDOUT:         {kind: 'PrefixOperatorStar', text: '*', has_error: yes, subtree_size: 2},
+// CHECK:STDOUT:       {kind: 'VariablePattern', text: 'var', subtree_size: 3},
+// CHECK:STDOUT:     {kind: 'VariableDecl', text: ';', subtree_size: 5},
 // CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
 // CHECK:STDOUT:   ]

+ 4 - 6
toolchain/parse/testdata/var/fail_empty.carbon

@@ -8,7 +8,7 @@
 // TIP: To dump output, run:
 // TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/parse/testdata/var/fail_empty.carbon
 
-// CHECK:STDERR: fail_empty.carbon:[[@LINE+4]]:4: error: expected name in binding pattern [ExpectedBindingPattern]
+// CHECK:STDERR: fail_empty.carbon:[[@LINE+4]]:4: error: expected pattern [ExpectedPattern]
 // CHECK:STDERR: var;
 // CHECK:STDERR:    ^
 // CHECK:STDERR:
@@ -18,10 +18,8 @@ var;
 // CHECK:STDOUT:   parse_tree: [
 // CHECK:STDOUT:     {kind: 'FileStart', text: ''},
 // CHECK:STDOUT:       {kind: 'VariableIntroducer', text: 'var'},
-// CHECK:STDOUT:           {kind: 'IdentifierNameNotBeforeSignature', text: ';', has_error: yes},
-// CHECK:STDOUT:           {kind: 'InvalidParse', text: ';', has_error: yes},
-// CHECK:STDOUT:         {kind: 'VarBindingPattern', text: ';', has_error: yes, subtree_size: 3},
-// CHECK:STDOUT:       {kind: 'VariablePattern', text: 'var', has_error: yes, subtree_size: 4},
-// CHECK:STDOUT:     {kind: 'VariableDecl', text: ';', subtree_size: 6},
+// CHECK:STDOUT:         {kind: 'InvalidParse', text: ';', has_error: yes},
+// CHECK:STDOUT:       {kind: 'VariablePattern', text: 'var', has_error: yes, subtree_size: 2},
+// CHECK:STDOUT:     {kind: 'VariableDecl', text: ';', subtree_size: 4},
 // CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
 // CHECK:STDOUT:   ]

+ 5 - 7
toolchain/parse/testdata/var/fail_no_semi.carbon

@@ -10,11 +10,11 @@
 
 var
 
-// CHECK:STDERR: fail_no_semi.carbon:[[@LINE+18]]:21: error: expected name in binding pattern [ExpectedBindingPattern]
+// CHECK:STDERR: fail_no_semi.carbon:[[@LINE+16]]:21: error: expected pattern [ExpectedPattern]
 // CHECK:STDERR: // CHECK:STDOUT:   ]
 // CHECK:STDERR:                     ^
 // CHECK:STDERR:
-// CHECK:STDERR: fail_no_semi.carbon:[[@LINE+14]]:21: error: `var` declarations must end with a `;` [ExpectedDeclSemi]
+// CHECK:STDERR: fail_no_semi.carbon:[[@LINE+12]]:21: error: `var` declarations must end with a `;` [ExpectedDeclSemi]
 // CHECK:STDERR: // CHECK:STDOUT:   ]
 // CHECK:STDERR:                     ^
 // CHECK:STDERR:
@@ -22,10 +22,8 @@ var
 // CHECK:STDOUT:   parse_tree: [
 // CHECK:STDOUT:     {kind: 'FileStart', text: ''},
 // CHECK:STDOUT:       {kind: 'VariableIntroducer', text: 'var'},
-// CHECK:STDOUT:           {kind: 'IdentifierNameNotBeforeSignature', text: '', has_error: yes},
-// CHECK:STDOUT:           {kind: 'InvalidParse', text: '', has_error: yes},
-// CHECK:STDOUT:         {kind: 'VarBindingPattern', text: '', has_error: yes, subtree_size: 3},
-// CHECK:STDOUT:       {kind: 'VariablePattern', text: 'var', has_error: yes, subtree_size: 4},
-// CHECK:STDOUT:     {kind: 'VariableDecl', text: 'var', has_error: yes, subtree_size: 6},
+// CHECK:STDOUT:         {kind: 'InvalidParse', text: '', has_error: yes},
+// CHECK:STDOUT:       {kind: 'VariablePattern', text: 'var', has_error: yes, subtree_size: 2},
+// CHECK:STDOUT:     {kind: 'VariableDecl', text: 'var', has_error: yes, subtree_size: 4},
 // CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
 // CHECK:STDOUT:   ]

+ 18 - 0
toolchain/sem_ir/formatter.cpp

@@ -1551,6 +1551,24 @@ auto Formatter::FormatArg(AbsoluteInstBlockId id) -> void {
   FormatArg(static_cast<InstBlockId>(id));
 }
 
+auto Formatter::FormatArg(ExprRegionId id) -> void {
+  const auto& region = sem_ir_->expr_regions().Get(id);
+
+  FormatArg(region.result_id);
+  out() << " in ";
+  OpenBrace();
+  for (auto [i, block_id] : llvm::enumerate(region.block_ids)) {
+    if (i != 0) {
+      IndentLabel();
+      FormatLabel(block_id);
+      out() << ":\n";
+    }
+
+    FormatCodeBlock(block_id);
+  }
+  CloseBrace();
+}
+
 auto Formatter::FormatArg(RealId id) -> void {
   // TODO: Format with a `.` when the exponent is near zero.
   const auto& real = sem_ir_->reals().Get(id);

+ 1 - 0
toolchain/sem_ir/formatter.h

@@ -274,6 +274,7 @@ class Formatter {
   auto FormatArg(NameScopeId id) -> void;
   auto FormatArg(InstBlockId id) -> void;
   auto FormatArg(AbsoluteInstBlockId id) -> void;
+  auto FormatArg(ExprRegionId id) -> void;
   auto FormatArg(RealId id) -> void;
   auto FormatArg(StringLiteralValueId id) -> void;
   auto FormatArg(ConstantId id) -> void { FormatConstant(id); }

+ 1 - 0
toolchain/sem_ir/inst_kind.def

@@ -61,6 +61,7 @@ CARBON_SEM_IR_INST_KIND(CustomWitness)
 CARBON_SEM_IR_INST_KIND(Deref)
 CARBON_SEM_IR_INST_KIND(ErrorInst)
 CARBON_SEM_IR_INST_KIND(ExportDecl)
+CARBON_SEM_IR_INST_KIND(ExprPattern)
 CARBON_SEM_IR_INST_KIND(FacetAccessType)
 CARBON_SEM_IR_INST_KIND(FacetType)
 CARBON_SEM_IR_INST_KIND(FacetValue)

+ 8 - 0
toolchain/sem_ir/inst_namer.cpp

@@ -1009,6 +1009,14 @@ auto InstNamer::NamingContext::NameInst() -> void {
       AddInstName("custom_witness");
       return;
     }
+    case CARBON_KIND(ExprPattern inst): {
+      for (auto block_id :
+           sem_ir().expr_regions().Get(inst.expr_region_id).block_ids) {
+        PushBlockId(scope_id_, block_id);
+      }
+      AddInstName("expr_patt");
+      return;
+    }
     case CARBON_KIND(FacetAccessType inst): {
       auto name_id = SemIR::NameId::None;
       if (auto name =

+ 12 - 0
toolchain/sem_ir/typed_insts.h

@@ -630,6 +630,18 @@ struct ExportDecl {
   InstId value_id;
 };
 
+// Represents an expression pattern.
+struct ExprPattern {
+  static constexpr auto Kind = InstKind::ExprPattern.Define<Parse::NodeId>(
+      {.ir_name = "expr_pattern",
+       .expr_category = ExprCategory::Pattern,
+       .constant_kind = InstConstantKind::AlwaysUnique,
+       .is_lowered = false});
+
+  TypeId type_id;
+  ExprRegionId expr_region_id;
+};
+
 // Represents accessing the `type` field in a facet value, which is notionally a
 // pair of a type and a witness.
 struct FacetAccessType {