Browse Source

Implement parsing observe declarations (#6674)

This implements parsing of the
[`observe`](https://docs.carbon-lang.dev/docs/design/generics/details.html#observing-a-type-implements-an-interface)
declarations.

- Added states and node kinds.
- Added node categories.
- Added a diagnostic for invalid keywords/operators.
- Implemented parser state handlers.
- Added structs to `typed_nodes.h`.
- Added parser tests.
Özgür 2 tháng trước cách đây
mục cha
commit
d11ee4b2b1

+ 29 - 0
toolchain/check/handle_observe.cpp

@@ -0,0 +1,29 @@
+// 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/handle.h"
+#include "toolchain/parse/node_ids.h"
+
+namespace Carbon::Check {
+
+auto HandleParseNode(Context& context, Parse::ObserveIntroducerId node_id)
+    -> bool {
+  return context.TODO(node_id, "ObserveIntroducerId");
+}
+
+auto HandleParseNode(Context& context, Parse::ObserveEqualEqualId node_id)
+    -> bool {
+  return context.TODO(node_id, "ObserveEqualEqualId");
+}
+
+auto HandleParseNode(Context& context, Parse::ObserveImplsId node_id) -> bool {
+  return context.TODO(node_id, "ObserveImplsId");
+}
+
+auto HandleParseNode(Context& context, Parse::ObserveDeclId node_id) -> bool {
+  return context.TODO(node_id, "ObserveDeclId");
+}
+
+}  // namespace Carbon::Check

+ 3 - 0
toolchain/check/node_stack.h

@@ -467,6 +467,7 @@ class NodeStack {
       case Parse::NodeKind::LetInitializer:
       case Parse::NodeKind::LetIntroducer:
       case Parse::NodeKind::NamedConstraintIntroducer:
+      case Parse::NodeKind::ObserveIntroducer:
       case Parse::NodeKind::RefBindingName:
       case Parse::NodeKind::ReturnStatementStart:
       case Parse::NodeKind::StructLiteralStart:
@@ -527,6 +528,8 @@ class NodeStack {
       case Parse::NodeKind::MatchIntroducer:
       case Parse::NodeKind::MatchStatementStart:
       case Parse::NodeKind::NamespaceStart:
+      case Parse::NodeKind::ObserveEqualEqual:
+      case Parse::NodeKind::ObserveImpls:
       case Parse::NodeKind::PackageIntroducer:
       case Parse::NodeKind::ParenExprStart:
       case Parse::NodeKind::PatternListComma:

+ 1 - 0
toolchain/diagnostics/kind.def

@@ -115,6 +115,7 @@ CARBON_DIAGNOSTIC_KIND(ExpectedStructLiteralField)
 CARBON_DIAGNOSTIC_KIND(ExpectedVarAfterReturned)
 CARBON_DIAGNOSTIC_KIND(ExpectedChoiceDefinition)
 CARBON_DIAGNOSTIC_KIND(ExpectedChoiceAlternativeName)
+CARBON_DIAGNOSTIC_KIND(ExpectedObserveOperator)
 CARBON_DIAGNOSTIC_KIND(NestedVar)
 CARBON_DIAGNOSTIC_KIND(NestedUnused)
 CARBON_DIAGNOSTIC_KIND(OperatorRequiresParentheses)

+ 1 - 0
toolchain/parse/BUILD

@@ -103,6 +103,7 @@ cc_library(
         ":context",
         ":dump",
         ":node_kind",
+        ":precedence",
         ":state",
         ":tree",
         "//common:check",

+ 3 - 0
toolchain/parse/handle_decl_scope_loop.cpp

@@ -8,6 +8,7 @@
 #include "toolchain/parse/context.h"
 #include "toolchain/parse/handle.h"
 #include "toolchain/parse/node_kind.h"
+#include "toolchain/parse/state.h"
 
 namespace Carbon::Parse {
 
@@ -130,6 +131,8 @@ static constexpr auto DeclIntroducers = [] {
       StateKind::TypeAfterIntroducerAsInterface);
   set(Lex::TokenKind::Namespace, NodeKind::NamespaceStart,
       StateKind::Namespace);
+  set(Lex::TokenKind::Observe, NodeKind::ObserveIntroducer,
+      StateKind::ObserveAfterIntroducer);
   set(Lex::TokenKind::Require, NodeKind::RequireIntroducer,
       StateKind::RequireAfterIntroducer);
   set_contextual(Lex::TokenKind::Let, RegularContext, NodeKind::LetIntroducer,

+ 72 - 0
toolchain/parse/handle_observe.cpp

@@ -0,0 +1,72 @@
+// 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/lex/token_kind.h"
+#include "toolchain/parse/context.h"
+#include "toolchain/parse/handle.h"
+#include "toolchain/parse/node_kind.h"
+#include "toolchain/parse/precedence.h"
+#include "toolchain/parse/state.h"
+#include "toolchain/parse/typed_nodes.h"
+
+namespace Carbon::Parse {
+auto HandleObserveAfterIntroducer(Context& context) -> void {
+  auto state = context.PopState();
+
+  context.PushState(state, StateKind::ObserveOperator);
+  context.PushStateForExpr(PrecedenceGroup::ForRequirements());
+}
+
+auto HandleObserveOperator(Context& context) -> void {
+  auto state = context.PopState();
+
+  switch (context.PositionKind()) {
+    case Lex::TokenKind::EqualEqual: {
+      state.token = context.Consume();
+      context.PushState(state, StateKind::ObserveFinishOperator);
+      context.PushStateForExpr(PrecedenceGroup::ForRequirements());
+      return;
+    }
+    case Lex::TokenKind::Impls: {
+      state.token = context.Consume();
+      context.PushState(state, StateKind::ObserveFinishOperator);
+      context.PushState(StateKind::Expr);
+      return;
+    }
+    default: {
+      if (!state.has_error) {
+        CARBON_DIAGNOSTIC(ExpectedObserveOperator, Error,
+                          "observe should use `==` or `impls` operator");
+        context.emitter().Emit(*context.position(), ExpectedObserveOperator);
+      }
+      context.RecoverFromDeclError(state, NodeKind::ObserveDecl,
+                                   /*skip_past_likely_end=*/true);
+      return;
+    }
+  }
+}
+
+auto HandleObserveFinishOperator(Context& context) -> void {
+  auto state = context.PopState();
+  auto token_kind = context.tokens().GetKind(state.token);
+
+  if (token_kind == Lex::TokenKind::EqualEqual) {
+    context.AddNode(NodeKind::ObserveEqualEqual, state.token, state.has_error);
+  } else {
+    context.AddNode(NodeKind::ObserveImpls, state.token, state.has_error);
+  }
+  if (context.PositionIs(Lex::TokenKind::Semi)) {
+    context.PushState(state, StateKind::ObserveDecl);
+  } else {
+    context.PushState(state, StateKind::ObserveOperator);
+  }
+}
+
+auto HandleObserveDecl(Context& context) -> void {
+  auto state = context.PopState();
+  context.AddNodeExpectingDeclSemi(state, NodeKind::ObserveDecl,
+                                   Lex::TokenKind::Observe,
+                                   /*is_def_allowed=*/false);
+}
+}  // namespace Carbon::Parse

+ 2 - 0
toolchain/parse/handle_statement.cpp

@@ -4,6 +4,7 @@
 
 #include <optional>
 
+#include "toolchain/lex/token_kind.h"
 #include "toolchain/parse/context.h"
 #include "toolchain/parse/handle.h"
 
@@ -64,6 +65,7 @@ auto HandleStatement(Context& context) -> void {
     case Lex::TokenKind::Let:
     case Lex::TokenKind::Library:
     case Lex::TokenKind::Namespace:
+    case Lex::TokenKind::Observe:
     // We intentionally don't handle Package here, because `package.` can be
     // used at the start of an expression, and it's not worth disambiguating it.
     case Lex::TokenKind::Var: {

+ 17 - 15
toolchain/parse/node_category.h

@@ -15,21 +15,23 @@ namespace Carbon::Parse {
 //   #define CARBON_NODE_CATEGORY_FOR_XYZ(Name) ...
 //   CARBON_NODE_CATEGORY(CARBON_NODE_CATEGORY_FOR_XYZ)
 //   #undef CARBON_NODE_CATEGORY_FOR_XYZ
-#define CARBON_NODE_CATEGORY(X)                        \
-  X(Decl)                                              \
-  X(Expr)                                              \
-  /* `impl <type> as` or just `impl as` */             \
-  X(ImplAs)                                            \
-  X(IntConst)                                          \
-  X(MemberExpr)                                        \
-  X(MemberName)                                        \
-  X(Modifier)                                          \
-  X(NonExprName)                                       \
-  X(PackageName)                                       \
-  X(Pattern)                                           \
-  /* `require <type> impls` or just `require impls` */ \
-  X(RequireImpls)                                      \
-  X(Requirement)                                       \
+#define CARBON_NODE_CATEGORY(X)                              \
+  X(Decl)                                                    \
+  X(Expr)                                                    \
+  /* `impl <type> as` or just `impl as` */                   \
+  X(ImplAs)                                                  \
+  X(IntConst)                                                \
+  X(MemberExpr)                                              \
+  X(MemberName)                                              \
+  X(Modifier)                                                \
+  X(NonExprName)                                             \
+  /* `observe <type> == <type>` or `observe <type> impls` */ \
+  X(ObserveOperator)                                         \
+  X(PackageName)                                             \
+  X(Pattern)                                                 \
+  /* `require <type> impls` or just `require impls` */       \
+  X(RequireImpls)                                            \
+  X(Requirement)                                             \
   X(Statement)
 
 // We expect this to grow, so are using a bigger size than needed.

+ 4 - 0
toolchain/parse/node_ids.h

@@ -7,6 +7,7 @@
 
 #include "toolchain/base/index_base.h"
 #include "toolchain/lex/token_index.h"
+#include "toolchain/parse/node_category.h"
 #include "toolchain/parse/node_kind.h"
 
 namespace Carbon::Parse {
@@ -110,6 +111,9 @@ using AnyStatementId =
     NodeIdInCategory<NodeCategory::Statement | NodeCategory::Decl>;
 using AnyRequireImplsId = NodeIdInCategory<NodeCategory::RequireImpls>;
 using AnyRequirementId = NodeIdInCategory<NodeCategory::Requirement>;
+using AnyObserveOperatorId = NodeIdInCategory<NodeCategory::ObserveOperator>;
+using AnyObserveOperandId =
+    NodeIdInCategory<NodeCategory::Expr | NodeCategory::ObserveOperator>;
 using AnyNonExprNameId = NodeIdInCategory<NodeCategory::NonExprName>;
 using AnyPackageNameId = NodeIdInCategory<NodeCategory::PackageName>;
 

+ 5 - 0
toolchain/parse/node_kind.def

@@ -393,6 +393,11 @@ CARBON_PARSE_NODE_KIND(RequireTypeImpls)
 CARBON_PARSE_NODE_KIND(RequireDefaultSelfImpls)
 CARBON_PARSE_NODE_KIND(RequireDecl)
 
+CARBON_PARSE_NODE_KIND(ObserveIntroducer)
+CARBON_PARSE_NODE_KIND(ObserveEqualEqual)
+CARBON_PARSE_NODE_KIND(ObserveImpls)
+CARBON_PARSE_NODE_KIND(ObserveDecl)
+
 CARBON_PARSE_NODE_KIND(NamedConstraintIntroducer)
 CARBON_PARSE_NODE_KIND(NamedConstraintDefinitionStart)
 CARBON_PARSE_NODE_KIND(NamedConstraintDefinition)

+ 1 - 1
toolchain/parse/precedence.h

@@ -63,7 +63,7 @@ class PrecedenceGroup {
   static auto ForRequireImpls() -> PrecedenceGroup;
 
   // Get the precedence level at which to parse expressions in requirements
-  // after `where` or `require`.
+  // after `where`, `require`, or `observe`.
   static auto ForRequirements() -> PrecedenceGroup;
 
   // Look up the operator information of the given prefix operator token, or

+ 41 - 0
toolchain/parse/state.def

@@ -396,6 +396,10 @@ CARBON_PARSE_STATE(DeclNameAndParamsAfterParams)
 // ^~~~~~~
 //   1. RequireAfterIntroducer
 //
+// observe ...
+// ^~~~~~~
+//   1. ObserveAfterIntroducer
+//
 // var ...        (variant is Regular)
 // ^~~
 //   1. VarAsRegular
@@ -1549,6 +1553,43 @@ CARBON_PARSE_STATE(RequireBeforeImpls)
 //   (state done)
 CARBON_PARSE_STATE(RequireDecl)
 
+// Handles processing of an `observe` declaration after the introducer.
+//
+// observe TypeExpression ...
+//        ^
+//   1. Expr
+//   2. ObserveOperator
+CARBON_PARSE_STATE(ObserveAfterIntroducer)
+
+// Handles processing of an operator in an `observe` declaration.
+//
+// observe TypeExpression == TypeExpression ...
+//                        ^~
+//   1. Expr
+//   2. ObserveFinishOperator
+// observe TypeExpression impls ...
+//                        ^~~~~
+//   1. Expr
+//   2. ObserveFinishOperator
+CARBON_PARSE_STATE(ObserveOperator)
+
+// Finishes an operator in an `observe` declaration.
+//
+// observe TypeExpression == TypeExpression ...
+//                                         ^
+//   1. ObserveOperator
+// observe TypeExpression == TypeExpression ;
+//                                         ^
+//   1. ObserveDecl
+CARBON_PARSE_STATE(ObserveFinishOperator)
+
+// Handles processing of a completed `observe` declaration.
+//
+// observe ... ;
+//             ^
+//   (state done)
+CARBON_PARSE_STATE(ObserveDecl)
+
 // Handles the start of a `var` or `returned var` in a non-class context.
 //
 // var ...             (variant is Regular)

+ 24 - 0
toolchain/parse/testdata/observe/fail_invalid_operator.carbon

@@ -0,0 +1,24 @@
+// 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/observe/fail_invalid_operator.carbon
+// TIP: To dump output, run:
+// TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/parse/testdata/observe/fail_invalid_operator.carbon
+
+// CHECK:STDERR: fail_invalid_operator.carbon:[[@LINE+4]]:11: error: observe should use `==` or `impls` operator [ExpectedObserveOperator]
+// CHECK:STDERR: observe T = U impls I;
+// CHECK:STDERR:           ^
+// CHECK:STDERR:
+observe T = U impls I;
+
+// CHECK:STDOUT: - filename: fail_invalid_operator.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:       {kind: 'ObserveIntroducer', text: 'observe'},
+// CHECK:STDOUT:       {kind: 'IdentifierNameExpr', text: 'T'},
+// CHECK:STDOUT:     {kind: 'ObserveDecl', text: ';', has_error: yes, subtree_size: 3},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]

+ 62 - 0
toolchain/parse/testdata/observe/fail_missing_expr.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
+// TIP: To test this file alone, run:
+// TIP:   bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/parse/testdata/observe/fail_missing_expr.carbon
+// TIP: To dump output, run:
+// TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/parse/testdata/observe/fail_missing_expr.carbon
+
+// --- fail_missing_expr_after_observe.carbon
+
+// CHECK:STDERR: fail_missing_expr_after_observe.carbon:[[@LINE+4]]:9: error: expected expression [ExpectedExpr]
+// CHECK:STDERR: observe impls I;
+// CHECK:STDERR:         ^~~~~
+// CHECK:STDERR:
+observe impls I;
+
+// --- fail_missing_expr_after_impls.carbon
+
+// CHECK:STDERR: fail_missing_expr_after_impls.carbon:[[@LINE+4]]:17: error: expected expression [ExpectedExpr]
+// CHECK:STDERR: observe T impls ;
+// CHECK:STDERR:                 ^
+// CHECK:STDERR:
+observe T impls ;
+
+// --- fail_missing_impls.carbon
+
+// CHECK:STDERR: fail_missing_impls.carbon:[[@LINE+4]]:11: error: observe should use `==` or `impls` operator [ExpectedObserveOperator]
+// CHECK:STDERR: observe T ;
+// CHECK:STDERR:           ^
+// CHECK:STDERR:
+observe T ;
+
+// CHECK:STDOUT: - filename: fail_missing_expr_after_observe.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:       {kind: 'ObserveIntroducer', text: 'observe'},
+// CHECK:STDOUT:         {kind: 'InvalidParse', text: 'impls', has_error: yes},
+// CHECK:STDOUT:         {kind: 'IdentifierNameExpr', text: 'I'},
+// CHECK:STDOUT:       {kind: 'ObserveImpls', text: 'impls', has_error: yes, subtree_size: 3},
+// CHECK:STDOUT:     {kind: 'ObserveDecl', text: ';', has_error: yes, subtree_size: 5},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]
+// CHECK:STDOUT: - filename: fail_missing_expr_after_impls.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:       {kind: 'ObserveIntroducer', text: 'observe'},
+// CHECK:STDOUT:         {kind: 'IdentifierNameExpr', text: 'T'},
+// CHECK:STDOUT:         {kind: 'InvalidParse', text: ';', has_error: yes},
+// CHECK:STDOUT:       {kind: 'ObserveImpls', text: 'impls', has_error: yes, subtree_size: 3},
+// CHECK:STDOUT:     {kind: 'ObserveDecl', text: ';', has_error: yes, subtree_size: 5},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]
+// CHECK:STDOUT: - filename: fail_missing_impls.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:       {kind: 'ObserveIntroducer', text: 'observe'},
+// CHECK:STDOUT:       {kind: 'IdentifierNameExpr', text: 'T'},
+// CHECK:STDOUT:     {kind: 'ObserveDecl', text: ';', has_error: yes, subtree_size: 3},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]

+ 103 - 0
toolchain/parse/testdata/observe/observe.carbon

@@ -0,0 +1,103 @@
+// 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/observe/observe.carbon
+// TIP: To dump output, run:
+// TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/parse/testdata/observe/observe.carbon
+
+// --- base.carbon
+
+observe A == B == C;
+
+// --- impls.carbon
+
+observe T impls I;
+
+// --- equal_equal_impls.carbon
+
+observe T == U impls I;
+observe A == B == C == D impls I;
+
+// --- statement.carbon
+
+fn F() {
+    observe A == B;
+}
+
+interface I {
+    observe A == B;
+}
+
+// CHECK:STDOUT: - filename: base.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:       {kind: 'ObserveIntroducer', text: 'observe'},
+// CHECK:STDOUT:           {kind: 'IdentifierNameExpr', text: 'A'},
+// CHECK:STDOUT:           {kind: 'IdentifierNameExpr', text: 'B'},
+// CHECK:STDOUT:         {kind: 'ObserveEqualEqual', text: '==', subtree_size: 3},
+// CHECK:STDOUT:         {kind: 'IdentifierNameExpr', text: 'C'},
+// CHECK:STDOUT:       {kind: 'ObserveEqualEqual', text: '==', subtree_size: 5},
+// CHECK:STDOUT:     {kind: 'ObserveDecl', text: ';', subtree_size: 7},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]
+// CHECK:STDOUT: - filename: impls.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:       {kind: 'ObserveIntroducer', text: 'observe'},
+// CHECK:STDOUT:         {kind: 'IdentifierNameExpr', text: 'T'},
+// CHECK:STDOUT:         {kind: 'IdentifierNameExpr', text: 'I'},
+// CHECK:STDOUT:       {kind: 'ObserveImpls', text: 'impls', subtree_size: 3},
+// CHECK:STDOUT:     {kind: 'ObserveDecl', text: ';', subtree_size: 5},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]
+// CHECK:STDOUT: - filename: equal_equal_impls.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:       {kind: 'ObserveIntroducer', text: 'observe'},
+// CHECK:STDOUT:           {kind: 'IdentifierNameExpr', text: 'T'},
+// CHECK:STDOUT:           {kind: 'IdentifierNameExpr', text: 'U'},
+// CHECK:STDOUT:         {kind: 'ObserveEqualEqual', text: '==', subtree_size: 3},
+// CHECK:STDOUT:         {kind: 'IdentifierNameExpr', text: 'I'},
+// CHECK:STDOUT:       {kind: 'ObserveImpls', text: 'impls', subtree_size: 5},
+// CHECK:STDOUT:     {kind: 'ObserveDecl', text: ';', subtree_size: 7},
+// CHECK:STDOUT:       {kind: 'ObserveIntroducer', text: 'observe'},
+// CHECK:STDOUT:               {kind: 'IdentifierNameExpr', text: 'A'},
+// CHECK:STDOUT:               {kind: 'IdentifierNameExpr', text: 'B'},
+// CHECK:STDOUT:             {kind: 'ObserveEqualEqual', text: '==', subtree_size: 3},
+// CHECK:STDOUT:             {kind: 'IdentifierNameExpr', text: 'C'},
+// CHECK:STDOUT:           {kind: 'ObserveEqualEqual', text: '==', subtree_size: 5},
+// CHECK:STDOUT:           {kind: 'IdentifierNameExpr', text: 'D'},
+// CHECK:STDOUT:         {kind: 'ObserveEqualEqual', text: '==', subtree_size: 7},
+// CHECK:STDOUT:         {kind: 'IdentifierNameExpr', text: 'I'},
+// CHECK:STDOUT:       {kind: 'ObserveImpls', text: 'impls', subtree_size: 9},
+// CHECK:STDOUT:     {kind: 'ObserveDecl', text: ';', subtree_size: 11},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]
+// CHECK:STDOUT: - filename: statement.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: 'ObserveIntroducer', text: 'observe'},
+// CHECK:STDOUT:           {kind: 'IdentifierNameExpr', text: 'A'},
+// CHECK:STDOUT:           {kind: 'IdentifierNameExpr', text: 'B'},
+// CHECK:STDOUT:         {kind: 'ObserveEqualEqual', text: '==', subtree_size: 3},
+// CHECK:STDOUT:       {kind: 'ObserveDecl', text: ';', subtree_size: 5},
+// CHECK:STDOUT:     {kind: 'FunctionDefinition', text: '}', subtree_size: 11},
+// CHECK:STDOUT:         {kind: 'InterfaceIntroducer', text: 'interface'},
+// CHECK:STDOUT:         {kind: 'IdentifierNameNotBeforeSignature', text: 'I'},
+// CHECK:STDOUT:       {kind: 'InterfaceDefinitionStart', text: '{', subtree_size: 3},
+// CHECK:STDOUT:         {kind: 'ObserveIntroducer', text: 'observe'},
+// CHECK:STDOUT:           {kind: 'IdentifierNameExpr', text: 'A'},
+// CHECK:STDOUT:           {kind: 'IdentifierNameExpr', text: 'B'},
+// CHECK:STDOUT:         {kind: 'ObserveEqualEqual', text: '==', subtree_size: 3},
+// CHECK:STDOUT:       {kind: 'ObserveDecl', text: ';', subtree_size: 5},
+// CHECK:STDOUT:     {kind: 'InterfaceDefinition', text: '}', subtree_size: 9},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]

+ 39 - 0
toolchain/parse/typed_nodes.h

@@ -1589,6 +1589,45 @@ struct RequireDecl {
   Lex::SemiTokenIndex token;
 };
 
+// `observe` declarations
+// --------------------------------
+
+// `observe`
+using ObserveIntroducer =
+    LeafNode<NodeKind::ObserveIntroducer, Lex::ObserveTokenIndex>;
+
+// `==`
+struct ObserveEqualEqual {
+  static constexpr auto Kind = NodeKind::ObserveEqualEqual.Define(
+      {.category = NodeCategory::ObserveOperator, .child_count = 2});
+
+  AnyObserveOperandId lhs;
+  Lex::EqualEqualTokenIndex token;
+  AnyExprId rhs;
+};
+
+// `impls`
+struct ObserveImpls {
+  static constexpr auto Kind = NodeKind::ObserveImpls.Define(
+      {.category = NodeCategory::ObserveOperator, .child_count = 2});
+
+  AnyObserveOperandId lhs;
+  Lex::ImplsTokenIndex token;
+  AnyExprId rhs;
+};
+
+// `observe T == U impls I`
+struct ObserveDecl {
+  static constexpr auto Kind =
+      NodeKind::ObserveDecl.Define({.category = NodeCategory::Decl,
+                                    .bracketed_by = ObserveIntroducer::Kind});
+
+  ObserveIntroducerId introducer;
+  llvm::SmallVector<AnyModifierId> modifiers;
+  AnyObserveOperatorId op;
+  Lex::SemiTokenIndex token;
+};
+
 // `impl`...`as` declarations and definitions
 // ------------------------------------------