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

Move child count and bracketing information for parse nodes into the node kind definition. (#4000)

Instead of tracking the bracketing and child count information in the
kind macro in `node_kind.def`, provide it to `NodeKind::Define` in
`typed_nodes.h`. If a node is both bracketed and has a fixed child
count, track both facts and check them both in tree verification, since
it's easy to do so now.

The overall goal here is to reduce `node_kind.def` down to a simple list
of names. I have a slightly different approach in mind for the token
kinds.

---------

Co-authored-by: Jon Ross-Perkins <jperkins@google.com>
Co-authored-by: Carbon Infra Bot <carbon-external-infra@google.com>
Richard Smith 1 год назад
Родитель
Сommit
419d2e39d8
5 измененных файлов с 476 добавлено и 386 удалено
  1. 16 40
      toolchain/parse/node_kind.cpp
  2. 182 227
      toolchain/parse/node_kind.def
  3. 73 11
      toolchain/parse/node_kind.h
  4. 11 0
      toolchain/parse/tree.cpp
  5. 194 108
      toolchain/parse/typed_nodes.h

+ 16 - 40
toolchain/parse/node_kind.cpp

@@ -39,55 +39,17 @@ CARBON_DEFINE_ENUM_CLASS_NAMES(NodeKind) = {
 #include "toolchain/parse/node_kind.def"
 };
 
-auto NodeKind::has_bracket() const -> bool {
-  static constexpr bool HasBracket[] = {
-#define CARBON_PARSE_NODE_KIND_BRACKET(...) true,
-#define CARBON_PARSE_NODE_KIND_CHILD_COUNT(...) false,
-#include "toolchain/parse/node_kind.def"
-  };
-  return HasBracket[AsInt()];
-}
-
-auto NodeKind::bracket() const -> NodeKind {
-  // Nodes are never self-bracketed, so we use that for nodes that instead set
-  // child_count.
-  static constexpr NodeKind Bracket[] = {
-#define CARBON_PARSE_NODE_KIND_BRACKET(Name, BracketName, ...) \
-  NodeKind::BracketName,
-#define CARBON_PARSE_NODE_KIND_CHILD_COUNT(Name, ...) NodeKind::Name,
-#include "toolchain/parse/node_kind.def"
-  };
-  auto bracket = Bracket[AsInt()];
-  CARBON_CHECK(bracket != *this) << *this;
-  return bracket;
-}
-
-auto NodeKind::child_count() const -> int32_t {
-  static constexpr int32_t ChildCount[] = {
-#define CARBON_PARSE_NODE_KIND_BRACKET(...) -1,
-#define CARBON_PARSE_NODE_KIND_CHILD_COUNT(Name, Size, ...) Size,
-#include "toolchain/parse/node_kind.def"
-  };
-  auto child_count = ChildCount[AsInt()];
-  CARBON_CHECK(child_count >= 0) << *this;
-  return child_count;
-}
-
 auto NodeKind::CheckMatchesTokenKind(Lex::TokenKind token_kind, bool has_error)
     -> void {
   static constexpr Lex::TokenKind TokenIfValid[] = {
 #define CARBON_IF_VALID(LexTokenKind) LexTokenKind
-#define CARBON_PARSE_NODE_KIND_BRACKET(Name, BracketName, LexTokenKind) \
-  Lex::TokenKind::LexTokenKind,
-#define CARBON_PARSE_NODE_KIND_CHILD_COUNT(Name, Size, LexTokenKind) \
+#define CARBON_PARSE_NODE_KIND_WITH_TOKEN(Name, LexTokenKind) \
   Lex::TokenKind::LexTokenKind,
 #include "toolchain/parse/node_kind.def"
   };
   static constexpr Lex::TokenKind TokenIfError[] = {
 #define CARBON_IF_VALID(LexTokenKind) Error
-#define CARBON_PARSE_NODE_KIND_BRACKET(Name, BracketName, LexTokenKind) \
-  Lex::TokenKind::LexTokenKind,
-#define CARBON_PARSE_NODE_KIND_CHILD_COUNT(Name, Size, LexTokenKind) \
+#define CARBON_PARSE_NODE_KIND_WITH_TOKEN(Name, LexTokenKind) \
   Lex::TokenKind::LexTokenKind,
 #include "toolchain/parse/node_kind.def"
   };
@@ -102,6 +64,20 @@ auto NodeKind::CheckMatchesTokenKind(Lex::TokenKind token_kind, bool has_error)
       << ", but expected token kind " << expected_token_kind;
 }
 
+auto NodeKind::has_bracket() const -> bool {
+  return definition().has_bracket();
+}
+
+auto NodeKind::bracket() const -> NodeKind { return definition().bracket(); }
+
+auto NodeKind::has_child_count() const -> bool {
+  return definition().has_child_count();
+}
+
+auto NodeKind::child_count() const -> int32_t {
+  return definition().child_count();
+}
+
 auto NodeKind::category() const -> NodeCategory {
   return definition().category();
 }

+ 182 - 227
toolchain/parse/node_kind.def

@@ -10,12 +10,8 @@
 // Supported x-macros are:
 // - CARBON_PARSE_NODE_KIND(Name)
 //   Used as a fallback if other macros are missing.
-//   - CARBON_PARSE_NODE_KIND_BRACKET(Name, BracketName, LexTokenKind)
-//     Defines a bracketed node kind. BracketName should refer to the node
-//     kind that is the _start_ of the bracketed range.
-//   - CARBON_PARSE_NODE_KIND_CHILD_COUNT(Name, ChildCount, LexTokenKind)
-//     Defines a parse node with a set number of children, often 0. This count
-//     must be correct even when the node contains errors.
+//   - CARBON_PARSE_NODE_KIND_WITH_TOKEN(Name, LexTokenKind)
+//     Defines a parse node kind and specifies its corresponding token kind.
 //     - CARBON_PARSE_NODE_KIND_PREFIX_OPERATOR(Name)
 //       Defines a parse node for a prefix operator, with the Name as token.
 //     - CARBON_PARSE_NODE_KIND_INFIX_OPERATOR(Name)
@@ -39,22 +35,18 @@
 // This tree represents the subset relationship between these macros, where if a
 // specific x-macro isn't defined, it'll fall back to the parent macro.
 //
-// Parse nodes are clustered based on language feature. Comments will show their
-// relationship in postorder, using indentation for child node relationships.
+// Parse nodes are clustered based on language feature. See typed_nodes.h for
+// the expected tree structure under each node kind.
 
-#if !(defined(CARBON_PARSE_NODE_KIND) ||          \
-      (defined(CARBON_PARSE_NODE_KIND_BRACKET) && \
-       defined(CARBON_PARSE_NODE_KIND_CHILD_COUNT)))
+#if !(defined(CARBON_PARSE_NODE_KIND) || \
+      defined(CARBON_PARSE_NODE_KIND_WITH_TOKEN))
 #error "Must define CARBON_PARSE_NODE_KIND family x-macros to use this file."
 #endif
 
-// The BRACKET and CHILD_COUNT macros will use CARBON_PARSE_NODE_KIND by default
-// when undefined.
-#ifndef CARBON_PARSE_NODE_KIND_BRACKET
-#define CARBON_PARSE_NODE_KIND_BRACKET(Name, ...) CARBON_PARSE_NODE_KIND(Name)
-#endif
-#ifndef CARBON_PARSE_NODE_KIND_CHILD_COUNT
-#define CARBON_PARSE_NODE_KIND_CHILD_COUNT(Name, ...) \
+// The WITH_TOKEN macro will use CARBON_PARSE_NODE_KIND by default when
+// undefined.
+#ifndef CARBON_PARSE_NODE_KIND_WITH_TOKEN
+#define CARBON_PARSE_NODE_KIND_WITH_TOKEN(Name, LexTokenKind) \
   CARBON_PARSE_NODE_KIND(Name)
 #endif
 
@@ -65,7 +57,7 @@
 //   #define CARBON_PARSE_NODE_KIND_PREFIX_OPERATOR(Name, ...) <code>
 #ifndef CARBON_PARSE_NODE_KIND_PREFIX_OPERATOR
 #define CARBON_PARSE_NODE_KIND_PREFIX_OPERATOR(Name) \
-  CARBON_PARSE_NODE_KIND_CHILD_COUNT(PrefixOperator##Name, 1, Name)
+  CARBON_PARSE_NODE_KIND_WITH_TOKEN(PrefixOperator##Name, Name)
 #endif
 
 // This is expected to be used with something like:
@@ -75,7 +67,7 @@
 //   #define CARBON_PARSE_NODE_KIND_INFIX_OPERATOR(Name, ...) <code>
 #ifndef CARBON_PARSE_NODE_KIND_INFIX_OPERATOR
 #define CARBON_PARSE_NODE_KIND_INFIX_OPERATOR(Name) \
-  CARBON_PARSE_NODE_KIND_CHILD_COUNT(InfixOperator##Name, 2, Name)
+  CARBON_PARSE_NODE_KIND_WITH_TOKEN(InfixOperator##Name, Name)
 #endif
 
 // This is expected to be used with something like:
@@ -85,7 +77,7 @@
 //   #define CARBON_PARSE_NODE_KIND_POSTFIX_OPERATOR(Name, ...) <code>
 #ifndef CARBON_PARSE_NODE_KIND_POSTFIX_OPERATOR
 #define CARBON_PARSE_NODE_KIND_POSTFIX_OPERATOR(Name) \
-  CARBON_PARSE_NODE_KIND_CHILD_COUNT(PostfixOperator##Name, 1, Name)
+  CARBON_PARSE_NODE_KIND_WITH_TOKEN(PostfixOperator##Name, Name)
 #endif
 
 // This is expected to be used with something like:
@@ -95,7 +87,7 @@
 //   #define CARBON_PARSE_NODE_KIND_TOKEN_LITERAL(Name, ...) <code>
 #ifndef CARBON_PARSE_NODE_KIND_TOKEN_LITERAL
 #define CARBON_PARSE_NODE_KIND_TOKEN_LITERAL(Name, LexTokenKinds) \
-  CARBON_PARSE_NODE_KIND_CHILD_COUNT(Name, 0, LexTokenKinds)
+  CARBON_PARSE_NODE_KIND_WITH_TOKEN(Name, LexTokenKinds)
 #endif
 
 // This is expected to be used with something like:
@@ -105,49 +97,48 @@
 //   #define CARBON_PARSE_NODE_KIND_TOKEN_MODIFIER(Name, ...) <code>
 #ifndef CARBON_PARSE_NODE_KIND_TOKEN_MODIFIER
 #define CARBON_PARSE_NODE_KIND_TOKEN_MODIFIER(Name) \
-  CARBON_PARSE_NODE_KIND_CHILD_COUNT(Name##Modifier, 0, Name)
+  CARBON_PARSE_NODE_KIND_WITH_TOKEN(Name##Modifier, Name)
 #endif
 
 // The start of the file.
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(FileStart, 0, FileStart)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(FileStart, FileStart)
 
 // The end of the file.
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(FileEnd, 0, FileEnd)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(FileEnd, FileEnd)
 
 // An invalid parse. Used to balance the parse tree. Always has an error.
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(InvalidParse, 0, Error)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(InvalidParse, Error)
 
 // An invalid subtree. Always has an error.
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(InvalidParseStart, 0, Error)
-CARBON_PARSE_NODE_KIND_BRACKET(InvalidParseSubtree, InvalidParseStart, Error)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(InvalidParseStart, Error)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(InvalidParseSubtree, Error)
 
 // A placeholder node to be replaced; it will never exist in a valid parse tree.
 // Its token kind is not enforced even when valid.
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(Placeholder, 0, Error)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(Placeholder, Error)
 
 // An empty declaration, such as `;`.
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(EmptyDecl, 0, CARBON_IF_VALID(Semi))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(EmptyDecl, CARBON_IF_VALID(Semi))
 
 // An identifier name in a non-expression context, such as a declaration.
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(IdentifierName, 0,
-                                   CARBON_IF_VALID(Identifier))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(IdentifierName, CARBON_IF_VALID(Identifier))
 
 // An identifier name in an expression context.
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(IdentifierNameExpr, 0, Identifier)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(IdentifierNameExpr, Identifier)
 
 // The `self` value and `Self` type identifier keywords. Typically of the form
 // `self: Self`.
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(SelfValueName, 0, SelfValueIdentifier)
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(SelfValueNameExpr, 0, SelfValueIdentifier)
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(SelfTypeNameExpr, 0, SelfTypeIdentifier)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(SelfValueName, SelfValueIdentifier)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(SelfValueNameExpr, SelfValueIdentifier)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(SelfTypeNameExpr, SelfTypeIdentifier)
 
 // The `base` value keyword, introduced by `base: B`. Typically referenced in
 // an expression, as in `x.base` or `{.base = ...}`, but can also be used as a
 // declared name, as in `{.base: partial B}`.
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(BaseName, 0, Base)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(BaseName, Base)
 
 // The `package` keyword in an expression.
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(PackageExpr, 0, Package)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(PackageExpr, Package)
 
 // ----------------------------------------------------------------------------
 
@@ -177,8 +168,8 @@ CARBON_PARSE_NODE_KIND_CHILD_COUNT(PackageExpr, 0, Package)
 // ----------------------------------------------------------------------------
 
 // The name of a package or library for `package`, `import`, and `library`.
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(PackageName, 0, Identifier)
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(LibraryName, 0, StringLiteral)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(PackageName, Identifier)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(LibraryName, StringLiteral)
 
 // `package`:
 //   PackageIntroducer
@@ -186,9 +177,8 @@ CARBON_PARSE_NODE_KIND_CHILD_COUNT(LibraryName, 0, StringLiteral)
 //   _optional_ _external_: PackageName
 //   _optional_ _external_: LibrarySpecifier
 // PackageDecl
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(PackageIntroducer, 0, Package)
-CARBON_PARSE_NODE_KIND_BRACKET(PackageDecl, PackageIntroducer,
-                               CARBON_IF_VALID(Semi))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(PackageIntroducer, Package)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(PackageDecl, CARBON_IF_VALID(Semi))
 
 // `import`:
 //   ImportIntroducer
@@ -196,24 +186,22 @@ CARBON_PARSE_NODE_KIND_BRACKET(PackageDecl, PackageIntroducer,
 //   _optional_ _external_: PackageName
 //   _optional_ _external_: LibrarySpecifier
 // ImportDecl
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(ImportIntroducer, 0, Import)
-CARBON_PARSE_NODE_KIND_BRACKET(ImportDecl, ImportIntroducer,
-                               CARBON_IF_VALID(Semi))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(ImportIntroducer, Import)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(ImportDecl, CARBON_IF_VALID(Semi))
 
 // `library` as declaration:
 //   LibraryIntroducer
 //   _repeated_ _external_: modifier
 //   DefaultLibrary or _external_: LibraryName
 // LibraryDecl
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(DefaultLibrary, 0, Default)
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(LibraryIntroducer, 0, Library)
-CARBON_PARSE_NODE_KIND_BRACKET(LibraryDecl, LibraryIntroducer,
-                               CARBON_IF_VALID(Semi))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(DefaultLibrary, Default)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(LibraryIntroducer, Library)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(LibraryDecl, CARBON_IF_VALID(Semi))
 
 // `library` in `package` or `import`:
 //   _external_: LibraryName or DefaultLibrary
 // LibrarySpecifier
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(LibrarySpecifier, 1, Library)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(LibrarySpecifier, Library)
 
 // Declaration names.
 //
@@ -227,33 +215,30 @@ CARBON_PARSE_NODE_KIND_CHILD_COUNT(LibrarySpecifier, 1, Library)
 //   _optional_ _external_: ImplicitParamList
 //   _optional_ _external_: TuplePattern
 // NameQualifier
-CARBON_PARSE_NODE_KIND_BRACKET(NameQualifier, IdentifierName,
-                               CARBON_IF_VALID(Period))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(NameQualifier, CARBON_IF_VALID(Period))
 
 // `export`:
 //   ExportIntroducer
 //   _external_: _declaration name_
 // ExportDecl
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(ExportIntroducer, 0, Export)
-CARBON_PARSE_NODE_KIND_BRACKET(ExportDecl, ExportIntroducer,
-                               CARBON_IF_VALID(Semi))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(ExportIntroducer, Export)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(ExportDecl, CARBON_IF_VALID(Semi))
 
 // `namespace`:
 //   NamespaceStart
 //   _repeated_ _external_: modifier
 //   _external_: _declaration name_
 // Namespace
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(NamespaceStart, 0, Namespace)
-CARBON_PARSE_NODE_KIND_BRACKET(Namespace, NamespaceStart, CARBON_IF_VALID(Semi))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(NamespaceStart, Namespace)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(Namespace, CARBON_IF_VALID(Semi))
 
 // A code block:
 //   CodeBlockStart
 //   _repeated_ _external_: statement
 // CodeBlock
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(CodeBlockStart, 0,
-                                   CARBON_IF_VALID(OpenCurlyBrace))
-CARBON_PARSE_NODE_KIND_BRACKET(CodeBlock, CodeBlockStart,
-                               CARBON_IF_VALID(CloseCurlyBrace))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(CodeBlockStart,
+                                  CARBON_IF_VALID(OpenCurlyBrace))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(CodeBlock, CARBON_IF_VALID(CloseCurlyBrace))
 
 // `fn` declarations start with a function signature:
 //
@@ -277,20 +262,15 @@ CARBON_PARSE_NODE_KIND_BRACKET(CodeBlock, CodeBlockStart,
 //   BuiltinFunctionDefinitionStart
 //   BuiltinName
 // BuiltinFunctionDefinition
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(FunctionIntroducer, 0, Fn)
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(ReturnType, 1, MinusGreater)
-CARBON_PARSE_NODE_KIND_BRACKET(FunctionDefinitionStart, FunctionIntroducer,
-                               OpenCurlyBrace)
-CARBON_PARSE_NODE_KIND_BRACKET(FunctionDefinition, FunctionDefinitionStart,
-                               CloseCurlyBrace)
-CARBON_PARSE_NODE_KIND_BRACKET(FunctionDecl, FunctionIntroducer,
-                               CARBON_IF_VALID(Semi))
-CARBON_PARSE_NODE_KIND_BRACKET(BuiltinFunctionDefinitionStart,
-                               FunctionIntroducer, Equal)
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(BuiltinName, 0, StringLiteral)
-CARBON_PARSE_NODE_KIND_BRACKET(BuiltinFunctionDefinition,
-                               BuiltinFunctionDefinitionStart,
-                               CARBON_IF_VALID(Semi))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(FunctionIntroducer, Fn)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(ReturnType, MinusGreater)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(FunctionDefinitionStart, OpenCurlyBrace)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(FunctionDefinition, CloseCurlyBrace)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(FunctionDecl, CARBON_IF_VALID(Semi))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(BuiltinFunctionDefinitionStart, Equal)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(BuiltinName, StringLiteral)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(BuiltinFunctionDefinition,
+                                  CARBON_IF_VALID(Semi))
 
 // `alias`:
 //   AliasIntroducer
@@ -299,9 +279,9 @@ CARBON_PARSE_NODE_KIND_BRACKET(BuiltinFunctionDefinition,
 //   AliasInitializer
 //   _external_: expression
 // Alias
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(AliasIntroducer, 0, Alias)
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(AliasInitializer, 0, Equal)
-CARBON_PARSE_NODE_KIND_BRACKET(Alias, AliasIntroducer, CARBON_IF_VALID(Semi))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(AliasIntroducer, Alias)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(AliasInitializer, Equal)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(Alias, CARBON_IF_VALID(Semi))
 
 // A tuple pattern:
 //   TuplePatternStart
@@ -312,9 +292,9 @@ CARBON_PARSE_NODE_KIND_BRACKET(Alias, AliasIntroducer, CARBON_IF_VALID(Semi))
 //
 // Patterns and PatternListComma may repeat with PatternListComma as a
 // separator.
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(TuplePatternStart, 0, OpenParen)
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(PatternListComma, 0, Comma)
-CARBON_PARSE_NODE_KIND_BRACKET(TuplePattern, TuplePatternStart, CloseParen)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(TuplePatternStart, OpenParen)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(PatternListComma, Comma)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(TuplePattern, CloseParen)
 
 // An implicit parameter list:
 //   ImplicitParamListStart
@@ -325,9 +305,8 @@ CARBON_PARSE_NODE_KIND_BRACKET(TuplePattern, TuplePatternStart, CloseParen)
 //
 // Patterns and PatternListComma may repeat with PatternListComma as a
 // separator.
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(ImplicitParamListStart, 0, OpenSquareBracket)
-CARBON_PARSE_NODE_KIND_BRACKET(ImplicitParamList, ImplicitParamListStart,
-                               CloseSquareBracket)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(ImplicitParamListStart, OpenSquareBracket)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(ImplicitParamList, CloseSquareBracket)
 
 // An array type, such as  `[i32; 3]` or `[i32;]`:
 //     ArrayExprStart
@@ -335,9 +314,9 @@ CARBON_PARSE_NODE_KIND_BRACKET(ImplicitParamList, ImplicitParamListStart,
 //   ArrayExprSemi
 //   _optional_ _external_: expression
 // ArrayExpr
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(ArrayExprStart, 0, OpenSquareBracket)
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(ArrayExprSemi, 2, CARBON_IF_VALID(Semi))
-CARBON_PARSE_NODE_KIND_BRACKET(ArrayExpr, ArrayExprSemi, CloseSquareBracket)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(ArrayExprStart, OpenSquareBracket)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(ArrayExprSemi, CARBON_IF_VALID(Semi))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(ArrayExpr, CloseSquareBracket)
 
 // A binding pattern, such as `name: Type`:
 //       IdentifierName or SelfValueName
@@ -345,10 +324,10 @@ CARBON_PARSE_NODE_KIND_BRACKET(ArrayExpr, ArrayExprSemi, CloseSquareBracket)
 //     [Generic]BindingPattern
 //   _optional_ Addr
 // _optional_ Template
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(BindingPattern, 2, CARBON_IF_VALID(Colon))
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(CompileTimeBindingPattern, 2, ColonExclaim)
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(Addr, 1, Addr)
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(Template, 1, Template)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(BindingPattern, CARBON_IF_VALID(Colon))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(CompileTimeBindingPattern, ColonExclaim)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(Addr, Addr)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(Template, Template)
 
 // `let` declarations, including associated constant declarations:
 //   LetIntroducer
@@ -363,9 +342,9 @@ CARBON_PARSE_NODE_KIND_CHILD_COUNT(Template, 1, Template)
 //
 // The LetInitializer and following expression are paired: either both will be
 // present, or neither will.
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(LetIntroducer, 0, Let)
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(LetInitializer, 0, Equal)
-CARBON_PARSE_NODE_KIND_BRACKET(LetDecl, LetIntroducer, CARBON_IF_VALID(Semi))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(LetIntroducer, Let)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(LetInitializer, Equal)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(LetDecl, CARBON_IF_VALID(Semi))
 
 // `var` and `returned var`:
 //   VariableIntroducer
@@ -382,37 +361,35 @@ CARBON_PARSE_NODE_KIND_BRACKET(LetDecl, LetIntroducer, CARBON_IF_VALID(Semi))
 //
 // The VariableInitializer and following expression are paired: either both will
 // be present, or neither will.
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(VariableIntroducer, 0, CARBON_IF_VALID(Var))
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(ReturnedModifier, 0, Returned)
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(VariableInitializer, 0, Equal)
-CARBON_PARSE_NODE_KIND_BRACKET(VariableDecl, VariableIntroducer,
-                               CARBON_IF_VALID(Semi))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(VariableIntroducer, CARBON_IF_VALID(Var))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(ReturnedModifier, Returned)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(VariableInitializer, Equal)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(VariableDecl, CARBON_IF_VALID(Semi))
 
 // An expression statement:
 //   _external_: expression
 // ExprStatement
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(ExprStatement, 1, CARBON_IF_VALID(Semi))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(ExprStatement, CARBON_IF_VALID(Semi))
 
 // `break`:
 //   BreakStatementStart
 // BreakStatement
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(BreakStatementStart, 0, Break)
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(BreakStatement, 1, CARBON_IF_VALID(Semi))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(BreakStatementStart, Break)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(BreakStatement, CARBON_IF_VALID(Semi))
 
 // `continue`:
 //   ContinueStatementStart
 // ContinueStatement
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(ContinueStatementStart, 0, Continue)
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(ContinueStatement, 1, CARBON_IF_VALID(Semi))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(ContinueStatementStart, Continue)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(ContinueStatement, CARBON_IF_VALID(Semi))
 
 // `return`:
 //   ReturnStatementStart
 //   _optional_ ReturnVarModifier or _external_: expression
 // ReturnStatement
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(ReturnStatementStart, 0, Return)
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(ReturnVarModifier, 0, Var)
-CARBON_PARSE_NODE_KIND_BRACKET(ReturnStatement, ReturnStatementStart,
-                               CARBON_IF_VALID(Semi))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(ReturnStatementStart, Return)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(ReturnVarModifier, Var)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(ReturnStatement, CARBON_IF_VALID(Semi))
 
 // `for`:
 //     ForHeaderStart
@@ -425,12 +402,10 @@ CARBON_PARSE_NODE_KIND_BRACKET(ReturnStatement, ReturnStatementStart,
 // ForStatement
 //
 // Versus a normal `var`, ForIn replaces VariableDecl.
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(ForHeaderStart, 0,
-                                   CARBON_IF_VALID(OpenParen))
-CARBON_PARSE_NODE_KIND_BRACKET(ForIn, VariableIntroducer, CARBON_IF_VALID(In))
-CARBON_PARSE_NODE_KIND_BRACKET(ForHeader, ForHeaderStart,
-                               CARBON_IF_VALID(CloseParen))
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(ForStatement, 2, For)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(ForHeaderStart, CARBON_IF_VALID(OpenParen))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(ForIn, CARBON_IF_VALID(In))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(ForHeader, CARBON_IF_VALID(CloseParen))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(ForStatement, For)
 
 // `if` statement + `else`:
 //     IfConditionStart
@@ -442,12 +417,10 @@ CARBON_PARSE_NODE_KIND_CHILD_COUNT(ForStatement, 2, For)
 // IfStatement
 //
 // IfStatementElse and the following node are optional based on `else` presence.
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(IfConditionStart, 0,
-                                   CARBON_IF_VALID(OpenParen))
-CARBON_PARSE_NODE_KIND_BRACKET(IfCondition, IfConditionStart,
-                               CARBON_IF_VALID(CloseParen))
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(IfStatementElse, 0, Else)
-CARBON_PARSE_NODE_KIND_BRACKET(IfStatement, IfCondition, If)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(IfConditionStart, CARBON_IF_VALID(OpenParen))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(IfCondition, CARBON_IF_VALID(CloseParen))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(IfStatementElse, Else)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(IfStatement, If)
 
 // `while`:
 //     WhileConditionStart
@@ -455,26 +428,25 @@ CARBON_PARSE_NODE_KIND_BRACKET(IfStatement, IfCondition, If)
 //   WhileCondition
 //   _external_: CodeBlock
 // WhileStatement
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(WhileConditionStart, 0,
-                                   CARBON_IF_VALID(OpenParen))
-CARBON_PARSE_NODE_KIND_BRACKET(WhileCondition, WhileConditionStart,
-                               CARBON_IF_VALID(CloseParen))
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(WhileStatement, 2, While)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(WhileConditionStart,
+                                  CARBON_IF_VALID(OpenParen))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(WhileCondition, CARBON_IF_VALID(CloseParen))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(WhileStatement, While)
 
 // Index expressions, such as `a[1]`:
 //     _external_: expression
 //   IndexExprStart
 //   _external_: expression
 // IndexExpr
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(IndexExprStart, 1, OpenSquareBracket)
-CARBON_PARSE_NODE_KIND_BRACKET(IndexExpr, IndexExprStart, CloseSquareBracket)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(IndexExprStart, OpenSquareBracket)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(IndexExpr, CloseSquareBracket)
 
 // Parenthesized single expressions, such as `(2)`:
 //   ParenExprStart
 //   _external_: expression
 // ParenExpr
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(ParenExprStart, 0, OpenParen)
-CARBON_PARSE_NODE_KIND_BRACKET(ParenExpr, ParenExprStart, CloseParen)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(ParenExprStart, OpenParen)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(ParenExpr, CloseParen)
 
 // Tuples, such as `(1, 2)`:
 //   TupleLiteralStart
@@ -485,9 +457,9 @@ CARBON_PARSE_NODE_KIND_BRACKET(ParenExpr, ParenExprStart, CloseParen)
 //
 // Expressions and TupleLiteralComma may repeat with TupleLiteralComma as a
 // separator.
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(TupleLiteralStart, 0, OpenParen)
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(TupleLiteralComma, 0, Comma)
-CARBON_PARSE_NODE_KIND_BRACKET(TupleLiteral, TupleLiteralStart, CloseParen)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(TupleLiteralStart, OpenParen)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(TupleLiteralComma, Comma)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(TupleLiteral, CloseParen)
 
 // Call expressions, such as `a()`:
 //     _external_: expression
@@ -498,23 +470,23 @@ CARBON_PARSE_NODE_KIND_BRACKET(TupleLiteral, TupleLiteralStart, CloseParen)
 // CallExpr
 //
 // Exprs and CallExprComma may repeat with CallExprComma as a separator.
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(CallExprStart, 1, OpenParen)
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(CallExprComma, 0, Comma)
-CARBON_PARSE_NODE_KIND_BRACKET(CallExpr, CallExprStart, CloseParen)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(CallExprStart, OpenParen)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(CallExprComma, Comma)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(CallExpr, CloseParen)
 
 // A member access expression, such as `a.b` or
 // `GetObject().(Interface.member)`:
 //   _external_: lhs expression
 //   _external_: rhs expression
 // MemberAccessExpr
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(MemberAccessExpr, 2, Period)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(MemberAccessExpr, Period)
 
 // A pointer member access expression, such as `a->b` or
 // `GetObject()->(Interface.member)`:
 //   _external_: lhs expression
 //   _external_: rhs expression
 // PointerMemberAccessExpr
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(PointerMemberAccessExpr, 2, MinusGreater)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(PointerMemberAccessExpr, MinusGreater)
 
 // A value literal.
 CARBON_PARSE_NODE_KIND_TOKEN_LITERAL(BoolLiteralFalse, False)
@@ -589,10 +561,10 @@ CARBON_PARSE_NODE_KIND_POSTFIX_OPERATOR(Star)
 //   ShortCircuitOperand(And|Or)
 //   _external_: expression
 // ShortCircuitOperand(And|Or)
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(ShortCircuitOperandAnd, 1, And)
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(ShortCircuitOperandOr, 1, Or)
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(ShortCircuitOperatorAnd, 2, And)
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(ShortCircuitOperatorOr, 2, Or)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(ShortCircuitOperandAnd, And)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(ShortCircuitOperandOr, Or)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(ShortCircuitOperatorAnd, And)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(ShortCircuitOperatorOr, Or)
 
 // `if` expression + `then` + `else`:
 //     _external_: expression
@@ -601,9 +573,9 @@ CARBON_PARSE_NODE_KIND_CHILD_COUNT(ShortCircuitOperatorOr, 2, Or)
 //   IfExprThen
 //   _external_: expression
 // IfExprElse
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(IfExprIf, 1, If)
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(IfExprThen, 1, Then)
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(IfExprElse, 3, CARBON_IF_VALID(Else))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(IfExprIf, If)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(IfExprThen, Then)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(IfExprElse, CARBON_IF_VALID(Else))
 
 // Struct literals, such as `{.a = 0}`:
 //   StructLiteralStart
@@ -631,16 +603,14 @@ CARBON_PARSE_NODE_KIND_CHILD_COUNT(IfExprElse, 3, CARBON_IF_VALID(Else))
 // When a valid StructTypeField or StructField cannot be formed, elements
 // may be replaced by InvalidParse, which may have a preceding sibling
 // StructFieldDesignator if one was successfully parsed.
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(StructLiteralStart, 0, OpenCurlyBrace)
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(StructTypeLiteralStart, 0, OpenCurlyBrace)
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(StructFieldDesignator, 1, Period)
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(StructField, 2, Equal)
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(StructTypeField, 2, Colon)
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(StructComma, 0, Comma)
-CARBON_PARSE_NODE_KIND_BRACKET(StructLiteral, StructLiteralStart,
-                               CloseCurlyBrace)
-CARBON_PARSE_NODE_KIND_BRACKET(StructTypeLiteral, StructTypeLiteralStart,
-                               CloseCurlyBrace)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(StructLiteralStart, OpenCurlyBrace)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(StructTypeLiteralStart, OpenCurlyBrace)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(StructFieldDesignator, Period)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(StructField, Equal)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(StructTypeField, Colon)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(StructComma, Comma)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(StructLiteral, CloseCurlyBrace)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(StructTypeLiteral, CloseCurlyBrace)
 
 // Various modifiers. These are all a single token.
 CARBON_PARSE_NODE_KIND_TOKEN_MODIFIER(Abstract)
@@ -668,22 +638,18 @@ CARBON_PARSE_NODE_KIND_TOKEN_MODIFIER(Virtual)
 // The above is the structure for a definition; for a declaration,
 // ClassDefinitionStart and later nodes are removed and replaced by
 // ClassDecl.
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(ClassIntroducer, 0, Class)
-CARBON_PARSE_NODE_KIND_BRACKET(ClassDefinitionStart, ClassIntroducer,
-                               OpenCurlyBrace)
-CARBON_PARSE_NODE_KIND_BRACKET(ClassDefinition, ClassDefinitionStart,
-                               CloseCurlyBrace)
-CARBON_PARSE_NODE_KIND_BRACKET(ClassDecl, ClassIntroducer,
-                               CARBON_IF_VALID(Semi))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(ClassIntroducer, Class)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(ClassDefinitionStart, OpenCurlyBrace)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(ClassDefinition, CloseCurlyBrace)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(ClassDecl, CARBON_IF_VALID(Semi))
 
 // `adapt`:
 //   AdaptIntroducer
 //   _repeated_ _external_: modifier
 //   _external_: expression
 // AdaptDecl
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(AdaptIntroducer, 0, Adapt)
-CARBON_PARSE_NODE_KIND_BRACKET(AdaptDecl, AdaptIntroducer,
-                               CARBON_IF_VALID(Semi))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(AdaptIntroducer, Adapt)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(AdaptDecl, CARBON_IF_VALID(Semi))
 
 // `base`:
 //   BaseIntroducer
@@ -691,9 +657,9 @@ CARBON_PARSE_NODE_KIND_BRACKET(AdaptDecl, AdaptIntroducer,
 //   BaseColon
 //   _external_: expression
 // BaseDecl
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(BaseIntroducer, 0, Base)
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(BaseColon, 0, Colon)
-CARBON_PARSE_NODE_KIND_BRACKET(BaseDecl, BaseIntroducer, CARBON_IF_VALID(Semi))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(BaseIntroducer, Base)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(BaseColon, Colon)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(BaseDecl, CARBON_IF_VALID(Semi))
 
 // `interface`:
 //     InterfaceIntroducer
@@ -708,13 +674,10 @@ CARBON_PARSE_NODE_KIND_BRACKET(BaseDecl, BaseIntroducer, CARBON_IF_VALID(Semi))
 // The above is the structure for a definition; for a declaration,
 // InterfaceDefinitionStart and later nodes are removed and replaced by
 // InterfaceDecl.
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(InterfaceIntroducer, 0, Interface)
-CARBON_PARSE_NODE_KIND_BRACKET(InterfaceDefinitionStart, InterfaceIntroducer,
-                               OpenCurlyBrace)
-CARBON_PARSE_NODE_KIND_BRACKET(InterfaceDefinition, InterfaceDefinitionStart,
-                               CloseCurlyBrace)
-CARBON_PARSE_NODE_KIND_BRACKET(InterfaceDecl, InterfaceIntroducer,
-                               CARBON_IF_VALID(Semi))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(InterfaceIntroducer, Interface)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(InterfaceDefinitionStart, OpenCurlyBrace)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(InterfaceDefinition, CloseCurlyBrace)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(InterfaceDecl, CARBON_IF_VALID(Semi))
 
 // `impl ... as`:
 //     ImplIntroducer
@@ -730,24 +693,22 @@ CARBON_PARSE_NODE_KIND_BRACKET(InterfaceDecl, InterfaceIntroducer,
 // The above is the structure for a definition; for a declaration,
 // ImplDefinitionStart and later nodes are removed and replaced by
 // ImplDecl.
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(ImplIntroducer, 0, Impl)
-CARBON_PARSE_NODE_KIND_BRACKET(ImplDefinitionStart, ImplIntroducer,
-                               OpenCurlyBrace)
-CARBON_PARSE_NODE_KIND_BRACKET(ImplDefinition, ImplDefinitionStart,
-                               CloseCurlyBrace)
-CARBON_PARSE_NODE_KIND_BRACKET(ImplDecl, ImplIntroducer, CARBON_IF_VALID(Semi))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(ImplIntroducer, Impl)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(ImplDefinitionStart, OpenCurlyBrace)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(ImplDefinition, CloseCurlyBrace)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(ImplDecl, CARBON_IF_VALID(Semi))
 
 // `forall ...`:
 //   _external_: ImplicitParamList
 // ImplForall
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(ImplForall, 1, Forall)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(ImplForall, Forall)
 
 // `... as`:
 //   _external_: expression
 // TypeImplAs
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(TypeImplAs, 1, As)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(TypeImplAs, As)
 // `as` without a type before it
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(DefaultSelfImplAs, 0, As)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(DefaultSelfImplAs, As)
 
 // `constraint`:
 //     NamedConstraintIntroducer
@@ -762,13 +723,11 @@ CARBON_PARSE_NODE_KIND_CHILD_COUNT(DefaultSelfImplAs, 0, As)
 // The above is the structure for a definition; for a declaration,
 // NamedConstraintDefinitionStart and later nodes are removed and replaced by
 // NamedConstraintDecl.
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(NamedConstraintIntroducer, 0, Constraint)
-CARBON_PARSE_NODE_KIND_BRACKET(NamedConstraintDefinitionStart,
-                               NamedConstraintIntroducer, OpenCurlyBrace)
-CARBON_PARSE_NODE_KIND_BRACKET(NamedConstraintDefinition,
-                               NamedConstraintDefinitionStart, CloseCurlyBrace)
-CARBON_PARSE_NODE_KIND_BRACKET(NamedConstraintDecl, NamedConstraintIntroducer,
-                               CARBON_IF_VALID(Semi))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(NamedConstraintIntroducer, Constraint)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(NamedConstraintDefinitionStart,
+                                  OpenCurlyBrace)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(NamedConstraintDefinition, CloseCurlyBrace)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(NamedConstraintDecl, CARBON_IF_VALID(Semi))
 
 // `choice`:
 //     ChoiceIntroducer
@@ -776,11 +735,11 @@ CARBON_PARSE_NODE_KIND_BRACKET(NamedConstraintDecl, NamedConstraintIntroducer,
 //   ChoiceDefinitionStart
 //   _optional_ _external_: ChoiceAlternativeList
 // ChoiceDefinition
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(ChoiceIntroducer, 0, Choice)
-CARBON_PARSE_NODE_KIND_BRACKET(ChoiceDefinitionStart, ChoiceIntroducer,
-                               CARBON_IF_VALID(OpenCurlyBrace))
-CARBON_PARSE_NODE_KIND_BRACKET(ChoiceDefinition, ChoiceDefinitionStart,
-                               CARBON_IF_VALID(CloseCurlyBrace))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(ChoiceIntroducer, Choice)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(ChoiceDefinitionStart,
+                                  CARBON_IF_VALID(OpenCurlyBrace))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(ChoiceDefinition,
+                                  CARBON_IF_VALID(CloseCurlyBrace))
 
 // Choice alternative list:
 //     _external_: IdentifierName
@@ -788,7 +747,7 @@ CARBON_PARSE_NODE_KIND_BRACKET(ChoiceDefinition, ChoiceDefinitionStart,
 //     _optional_: ChoiceAlternativeListComma
 //   _repeated_
 // ChoiceAlternativeList
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(ChoiceAlternativeListComma, 0, Comma)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(ChoiceAlternativeListComma, Comma)
 
 // `match`:
 //     MatchIntroducer
@@ -799,15 +758,14 @@ CARBON_PARSE_NODE_KIND_CHILD_COUNT(ChoiceAlternativeListComma, 0, Comma)
 //   _repeated_ _external_: MatchCase
 //   _optional_ _external_: MatchStatementDefault
 // MatchStatement
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(MatchIntroducer, 0, Match)
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(MatchConditionStart, 0,
-                                   CARBON_IF_VALID(OpenParen))
-CARBON_PARSE_NODE_KIND_BRACKET(MatchCondition, MatchConditionStart,
-                               CARBON_IF_VALID(CloseParen))
-CARBON_PARSE_NODE_KIND_BRACKET(MatchStatementStart, MatchIntroducer,
-                               CARBON_IF_VALID(OpenCurlyBrace))
-CARBON_PARSE_NODE_KIND_BRACKET(MatchStatement, MatchStatementStart,
-                               CARBON_IF_VALID(CloseCurlyBrace))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(MatchIntroducer, Match)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(MatchConditionStart,
+                                  CARBON_IF_VALID(OpenParen))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(MatchCondition, CARBON_IF_VALID(CloseParen))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(MatchStatementStart,
+                                  CARBON_IF_VALID(OpenCurlyBrace))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(MatchStatement,
+                                  CARBON_IF_VALID(CloseCurlyBrace))
 
 // `case`:
 //     MatchCaseIntroducer
@@ -820,18 +778,16 @@ CARBON_PARSE_NODE_KIND_BRACKET(MatchStatement, MatchStatementStart,
 //   MatchCaseStart
 //   _repeated_ _external_: statement
 // MatchCase
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(MatchCaseIntroducer, 0, Case)
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(MatchCaseGuardIntroducer, 0, If)
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(MatchCaseGuardStart, 0,
-                                   CARBON_IF_VALID(OpenParen))
-CARBON_PARSE_NODE_KIND_BRACKET(MatchCaseGuard, MatchCaseGuardIntroducer,
-                               CARBON_IF_VALID(CloseParen))
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(MatchCaseEqualGreater, 0,
-                                   CARBON_IF_VALID(EqualGreater))
-CARBON_PARSE_NODE_KIND_BRACKET(MatchCaseStart, MatchCaseIntroducer,
-                               CARBON_IF_VALID(OpenCurlyBrace))
-CARBON_PARSE_NODE_KIND_BRACKET(MatchCase, MatchCaseStart,
-                               CARBON_IF_VALID(CloseCurlyBrace))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(MatchCaseIntroducer, Case)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(MatchCaseGuardIntroducer, If)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(MatchCaseGuardStart,
+                                  CARBON_IF_VALID(OpenParen))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(MatchCaseGuard, CARBON_IF_VALID(CloseParen))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(MatchCaseEqualGreater,
+                                  CARBON_IF_VALID(EqualGreater))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(MatchCaseStart,
+                                  CARBON_IF_VALID(OpenCurlyBrace))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(MatchCase, CARBON_IF_VALID(CloseCurlyBrace))
 
 // `default`:
 //     MatchDefaultIntroducer
@@ -839,17 +795,16 @@ CARBON_PARSE_NODE_KIND_BRACKET(MatchCase, MatchCaseStart,
 //   MatchDefaultStart
 //   _repeated_ _external_: statement
 // MatchDefault
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(MatchDefaultIntroducer, 0, Default)
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(MatchDefaultEqualGreater, 0,
-                                   CARBON_IF_VALID(EqualGreater))
-CARBON_PARSE_NODE_KIND_BRACKET(MatchDefaultStart, MatchDefaultIntroducer,
-                               CARBON_IF_VALID(OpenCurlyBrace))
-CARBON_PARSE_NODE_KIND_BRACKET(MatchDefault, MatchDefaultStart,
-                               CARBON_IF_VALID(CloseCurlyBrace))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(MatchDefaultIntroducer, Default)
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(MatchDefaultEqualGreater,
+                                  CARBON_IF_VALID(EqualGreater))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(MatchDefaultStart,
+                                  CARBON_IF_VALID(OpenCurlyBrace))
+CARBON_PARSE_NODE_KIND_WITH_TOKEN(MatchDefault,
+                                  CARBON_IF_VALID(CloseCurlyBrace))
 
 #undef CARBON_PARSE_NODE_KIND
-#undef CARBON_PARSE_NODE_KIND_BRACKET
-#undef CARBON_PARSE_NODE_KIND_CHILD_COUNT
+#undef CARBON_PARSE_NODE_KIND_WITH_TOKEN
 #undef CARBON_PARSE_NODE_KIND_INFIX_OPERATOR
 #undef CARBON_PARSE_NODE_KIND_POSTFIX_OPERATOR
 #undef CARBON_PARSE_NODE_KIND_PREFIX_OPERATOR

+ 73 - 11
toolchain/parse/node_kind.h

@@ -33,7 +33,7 @@ enum class NodeCategory : uint32_t {
   LLVM_MARK_AS_BITMASK_ENUM(/*LargestValue=*/Statement)
 };
 
-inline constexpr auto operator!(NodeCategory k) -> bool {
+constexpr auto operator!(NodeCategory k) -> bool {
   return !static_cast<uint32_t>(k);
 }
 
@@ -47,6 +47,16 @@ CARBON_DEFINE_RAW_ENUM_CLASS(NodeKind, uint8_t) {
 
 // A class wrapping an enumeration of the different kinds of nodes in the parse
 // tree.
+//
+// In order to allow the children of a node to be determined without relying on
+// the subtree size field in the parse node, each node kind must have one of:
+//
+// - a bracketing node kind, which is always the kind for the first child, and
+//   is never the kind of any other child, or
+// - a fixed child count,
+//
+// or both. This is required even for nodes for which `Tree::node_has_errors`
+// returns `true`.
 class NodeKind : public CARBON_ENUM_BASE(NodeKind) {
  public:
 #define CARBON_PARSE_NODE_KIND(Name) CARBON_ENUM_CONSTANT_DECL(Name)
@@ -57,15 +67,18 @@ class NodeKind : public CARBON_ENUM_BASE(NodeKind) {
   auto CheckMatchesTokenKind(Lex::TokenKind lex_token_kind, bool has_error)
       -> void;
 
-  // Returns true if the node is bracketed; otherwise, child_count is used.
+  // Returns true if the node is bracketed.
   auto has_bracket() const -> bool;
 
   // Returns the bracketing node kind for the current node kind. Requires that
   // has_bracket is true.
   auto bracket() const -> NodeKind;
 
+  // Returns true if the node is has a fixed child count.
+  auto has_child_count() const -> bool;
+
   // Returns the number of children that the node must have, often 0. Requires
-  // that has_bracket is false.
+  // that has_child_count is true.
   auto child_count() const -> int32_t;
 
   // Returns which categories this node kind is in.
@@ -78,11 +91,11 @@ class NodeKind : public CARBON_ENUM_BASE(NodeKind) {
   using EnumBase::Make;
 
   class Definition;
+  struct DefinitionArgs;
 
   // Provides a definition for this parse node kind. Should only be called
   // once, to construct the kind as part of defining it in `typed_nodes.h`.
-  constexpr auto Define(NodeCategory category = NodeCategory::None) const
-      -> Definition;
+  constexpr auto Define(DefinitionArgs args) const -> Definition;
 
  private:
   // Looks up the definition for this instruction kind.
@@ -107,6 +120,17 @@ static_assert(
 // We expect the parse node kind to fit compactly into 8 bits.
 static_assert(sizeof(NodeKind) == 1, "Kind objects include padding!");
 
+// Optional arguments that can be supplied when defining a node kind. At least
+// one of `bracketed_by` and `child_count` is required.
+struct NodeKind::DefinitionArgs {
+  // The category for the node.
+  NodeCategory category = NodeCategory::None;
+  // The kind of the bracketing node, which is the first child.
+  std::optional<NodeKind> bracketed_by = std::nullopt;
+  // The fixed child count.
+  int32_t child_count = -1;
+};
+
 // A definition of a parse node kind. This is a NodeKind value, plus
 // ancillary data such as the name to use for the node kind in LLVM IR. These
 // are not copyable, and only one instance of this type is expected to exist per
@@ -118,20 +142,58 @@ class NodeKind::Definition : public NodeKind {
   Definition(const Definition&) = delete;
   auto operator=(const Definition&) -> Definition& = delete;
 
+  // Returns true if the node is bracketed.
+  constexpr auto has_bracket() const -> bool { return bracket_ != *this; }
+
+  // Returns the bracketing node kind for the current node kind. Requires that
+  // has_bracket is true.
+  constexpr auto bracket() const -> NodeKind {
+    CARBON_CHECK(has_bracket()) << *this;
+    return bracket_;
+  }
+
+  // Returns true if the node is has a fixed child count.
+  constexpr auto has_child_count() const -> bool { return child_count_ >= 0; }
+
+  // Returns the number of children that the node must have, often 0. Requires
+  // that has_child_count is true.
+  constexpr auto child_count() const -> int32_t {
+    CARBON_CHECK(has_child_count()) << *this;
+    return child_count_;
+  }
+
   // Returns which categories this node kind is in.
   constexpr auto category() const -> NodeCategory { return category_; }
 
  private:
   friend class NodeKind;
 
-  constexpr Definition(NodeKind kind, NodeCategory category)
-      : NodeKind(kind), category_(category) {}
-
-  NodeCategory category_;
+  // This is factored out and non-constexpr to improve the compile-time error
+  // message if the check below fails.
+  auto MustSpecifyEitherBracketingNodeOrChildCount() {
+    CARBON_FATAL()
+        << "Must specify either bracketing node or fixed child count.";
+  }
+
+  constexpr explicit Definition(NodeKind kind, DefinitionArgs args)
+      : NodeKind(kind),
+        category_(args.category),
+        bracket_(args.bracketed_by.value_or(kind)),
+        child_count_(args.child_count) {
+    if (!has_bracket() && !has_child_count()) {
+      MustSpecifyEitherBracketingNodeOrChildCount();
+    }
+  }
+
+  NodeCategory category_ = NodeCategory::None;
+  // Nodes are never self-bracketed, so we use *this to indicate that the node
+  // is not bracketed.
+  NodeKind bracket_ = *this;
+  int32_t child_count_ = -1;
 };
 
-constexpr auto NodeKind::Define(NodeCategory category) const -> Definition {
-  return Definition(*this, category);
+constexpr auto NodeKind::Define(DefinitionArgs args) const -> Definition {
+  return Definition(*this, args);
 }
 
 // HasKindMember<T> is true if T has a `static const NodeKind::Definition Kind`

+ 11 - 0
toolchain/parse/tree.cpp

@@ -211,6 +211,7 @@ auto Tree::Verify() const -> ErrorOr<Success> {
 
     int subtree_size = 1;
     if (n_impl.kind.has_bracket()) {
+      int child_count = 0;
       while (true) {
         if (nodes.empty()) {
           return Error(
@@ -220,7 +221,17 @@ auto Tree::Verify() const -> ErrorOr<Success> {
         }
         auto child_impl = node_impls_[nodes.pop_back_val().index];
         subtree_size += child_impl.subtree_size;
+        ++child_count;
         if (n_impl.kind.bracket() == child_impl.kind) {
+          // If there's a bracketing node and a child count, verify the child
+          // count too.
+          if (n_impl.kind.has_child_count() &&
+              child_count != n_impl.kind.child_count()) {
+            return Error(llvm::formatv(
+                "NodeId #{0} is a {1} with child_count {2}, but encountered "
+                "{3} nodes before we reached the bracketing node.",
+                n, n_impl.kind, n_impl.kind.child_count(), child_count));
+          }
           break;
         }
       }

+ 194 - 108
toolchain/parse/typed_nodes.h

@@ -29,7 +29,8 @@ using CommaSeparatedList = llvm::SmallVector<ListItem<Element, Comma>>;
 // This class provides a shorthand for defining parse node kinds for leaf nodes.
 template <const NodeKind& KindT, NodeCategory Category = NodeCategory::None>
 struct LeafNode {
-  static constexpr auto Kind = KindT.Define(Category);
+  static constexpr auto Kind =
+      KindT.Define({.category = Category, .child_count = 0});
 };
 
 // ----------------------------------------------------------------------------
@@ -81,8 +82,9 @@ using InvalidParse =
 // An invalid subtree. Always has an error so can never be extracted.
 using InvalidParseStart = LeafNode<NodeKind::InvalidParseStart>;
 struct InvalidParseSubtree {
-  static constexpr auto Kind =
-      NodeKind::InvalidParseSubtree.Define(NodeCategory::Decl);
+  static constexpr auto Kind = NodeKind::InvalidParseSubtree.Define(
+      {.category = NodeCategory::Decl,
+       .bracketed_by = InvalidParseStart::Kind});
 
   InvalidParseStartId start;
   llvm::SmallVector<NodeIdNot<InvalidParseStart>> extra;
@@ -139,7 +141,8 @@ struct NameAndParams {
 
 // A name qualifier: `A.`, `A(T:! type).`, or `A[T:! type](N:! T).`.
 struct NameQualifier {
-  static constexpr auto Kind = NodeKind::NameQualifier.Define();
+  static constexpr auto Kind =
+      NodeKind::NameQualifier.Define({.bracketed_by = IdentifierName::Kind});
 
   NameAndParams name_and_params;
 };
@@ -166,7 +169,8 @@ using PackageIntroducer = LeafNode<NodeKind::PackageIntroducer>;
 
 // `library` in `package` or `import`.
 struct LibrarySpecifier {
-  static constexpr auto Kind = NodeKind::LibrarySpecifier.Define();
+  static constexpr auto Kind =
+      NodeKind::LibrarySpecifier.Define({.child_count = 1});
 
   NodeIdOneOf<LibraryName, DefaultLibrary> name;
 };
@@ -174,7 +178,9 @@ struct LibrarySpecifier {
 // First line of the file, such as:
 //   `impl package MyPackage library "MyLibrary";`
 struct PackageDecl {
-  static constexpr auto Kind = NodeKind::PackageDecl.Define(NodeCategory::Decl);
+  static constexpr auto Kind =
+      NodeKind::PackageDecl.Define({.category = NodeCategory::Decl,
+                                    .bracketed_by = PackageIntroducer::Kind});
 
   PackageIntroducerId introducer;
   llvm::SmallVector<AnyModifierId> modifiers;
@@ -185,7 +191,8 @@ struct PackageDecl {
 // `import TheirPackage library "TheirLibrary";`
 using ImportIntroducer = LeafNode<NodeKind::ImportIntroducer>;
 struct ImportDecl {
-  static constexpr auto Kind = NodeKind::ImportDecl.Define(NodeCategory::Decl);
+  static constexpr auto Kind = NodeKind::ImportDecl.Define(
+      {.category = NodeCategory::Decl, .bracketed_by = ImportIntroducer::Kind});
 
   ImportIntroducerId introducer;
   llvm::SmallVector<AnyModifierId> modifiers;
@@ -196,7 +203,9 @@ struct ImportDecl {
 // `library` as declaration.
 using LibraryIntroducer = LeafNode<NodeKind::LibraryIntroducer>;
 struct LibraryDecl {
-  static constexpr auto Kind = NodeKind::LibraryDecl.Define(NodeCategory::Decl);
+  static constexpr auto Kind =
+      NodeKind::LibraryDecl.Define({.category = NodeCategory::Decl,
+                                    .bracketed_by = LibraryIntroducer::Kind});
 
   LibraryIntroducerId introducer;
   llvm::SmallVector<AnyModifierId> modifiers;
@@ -206,7 +215,8 @@ struct LibraryDecl {
 // `export` as a declaration.
 using ExportIntroducer = LeafNode<NodeKind::ExportIntroducer>;
 struct ExportDecl {
-  static constexpr auto Kind = NodeKind::ExportDecl.Define(NodeCategory::Decl);
+  static constexpr auto Kind = NodeKind::ExportDecl.Define(
+      {.category = NodeCategory::Decl, .bracketed_by = ExportIntroducer::Kind});
 
   ExportIntroducerId introducer;
   llvm::SmallVector<AnyModifierId> modifiers;
@@ -220,7 +230,8 @@ using NamespaceStart = LeafNode<NodeKind::NamespaceStart>;
 
 // A namespace: `namespace N;`.
 struct Namespace {
-  static constexpr auto Kind = NodeKind::Namespace.Define(NodeCategory::Decl);
+  static constexpr auto Kind = NodeKind::Namespace.Define(
+      {.category = NodeCategory::Decl, .bracketed_by = NamespaceStart::Kind});
 
   NamespaceStartId introducer;
   llvm::SmallVector<AnyModifierId> modifiers;
@@ -232,8 +243,8 @@ struct Namespace {
 
 // A pattern binding, such as `name: Type`.
 struct BindingPattern {
-  static constexpr auto Kind =
-      NodeKind::BindingPattern.Define(NodeCategory::Pattern);
+  static constexpr auto Kind = NodeKind::BindingPattern.Define(
+      {.category = NodeCategory::Pattern, .child_count = 2});
 
   NodeIdOneOf<IdentifierName, SelfValueName> name;
   AnyExprId type;
@@ -241,8 +252,8 @@ struct BindingPattern {
 
 // `name:! Type`
 struct CompileTimeBindingPattern {
-  static constexpr auto Kind =
-      NodeKind::CompileTimeBindingPattern.Define(NodeCategory::Pattern);
+  static constexpr auto Kind = NodeKind::CompileTimeBindingPattern.Define(
+      {.category = NodeCategory::Pattern, .child_count = 2});
 
   NodeIdOneOf<IdentifierName, SelfValueName> name;
   AnyExprId type;
@@ -250,14 +261,16 @@ struct CompileTimeBindingPattern {
 
 // An address-of binding: `addr self: Self*`.
 struct Addr {
-  static constexpr auto Kind = NodeKind::Addr.Define(NodeCategory::Pattern);
+  static constexpr auto Kind = NodeKind::Addr.Define(
+      {.category = NodeCategory::Pattern, .child_count = 1});
 
   AnyPatternId inner;
 };
 
 // A template binding: `template T:! type`.
 struct Template {
-  static constexpr auto Kind = NodeKind::Template.Define(NodeCategory::Pattern);
+  static constexpr auto Kind = NodeKind::Template.Define(
+      {.category = NodeCategory::Pattern, .child_count = 1});
 
   // This is a CompileTimeBindingPatternId in any valid program.
   // TODO: Should the parser enforce that?
@@ -270,7 +283,8 @@ using PatternListComma = LeafNode<NodeKind::PatternListComma>;
 // A parameter list or tuple pattern: `(a: i32, b: i32)`.
 struct TuplePattern {
   static constexpr auto Kind =
-      NodeKind::TuplePattern.Define(NodeCategory::Pattern);
+      NodeKind::TuplePattern.Define({.category = NodeCategory::Pattern,
+                                     .bracketed_by = TuplePatternStart::Kind});
 
   TuplePatternStartId left_paren;
   CommaSeparatedList<AnyPatternId, PatternListCommaId> params;
@@ -280,7 +294,8 @@ using ImplicitParamListStart = LeafNode<NodeKind::ImplicitParamListStart>;
 
 // An implicit parameter list: `[T:! type, self: Self]`.
 struct ImplicitParamList {
-  static constexpr auto Kind = NodeKind::ImplicitParamList.Define();
+  static constexpr auto Kind = NodeKind::ImplicitParamList.Define(
+      {.bracketed_by = ImplicitParamListStart::Kind});
 
   ImplicitParamListStartId left_square;
   CommaSeparatedList<AnyPatternId, PatternListCommaId> params;
@@ -293,7 +308,7 @@ using FunctionIntroducer = LeafNode<NodeKind::FunctionIntroducer>;
 
 // A return type: `-> i32`.
 struct ReturnType {
-  static constexpr auto Kind = NodeKind::ReturnType.Define();
+  static constexpr auto Kind = NodeKind::ReturnType.Define({.child_count = 1});
 
   AnyExprId type;
 };
@@ -301,7 +316,8 @@ struct ReturnType {
 // A function signature: `fn F() -> i32`.
 template <const NodeKind& KindT, NodeCategory Category>
 struct FunctionSignature {
-  static constexpr auto Kind = KindT.Define(Category);
+  static constexpr auto Kind = KindT.Define(
+      {.category = Category, .bracketed_by = FunctionIntroducer::Kind});
 
   FunctionIntroducerId introducer;
   llvm::SmallVector<AnyModifierId> modifiers;
@@ -316,8 +332,9 @@ using FunctionDefinitionStart =
 
 // A function definition: `fn F() -> i32 { ... }`.
 struct FunctionDefinition {
-  static constexpr auto Kind =
-      NodeKind::FunctionDefinition.Define(NodeCategory::Decl);
+  static constexpr auto Kind = NodeKind::FunctionDefinition.Define(
+      {.category = NodeCategory::Decl,
+       .bracketed_by = FunctionDefinitionStart::Kind});
 
   FunctionDefinitionStartId signature;
   llvm::SmallVector<AnyStatementId> body;
@@ -330,8 +347,9 @@ using BuiltinName = LeafNode<NodeKind::BuiltinName>;
 
 // A builtin function definition: `fn F() -> i32 = "builtin name";`
 struct BuiltinFunctionDefinition {
-  static constexpr auto Kind =
-      NodeKind::BuiltinFunctionDefinition.Define(NodeCategory::Decl);
+  static constexpr auto Kind = NodeKind::BuiltinFunctionDefinition.Define(
+      {.category = NodeCategory::Decl,
+       .bracketed_by = BuiltinFunctionDefinitionStart::Kind});
 
   BuiltinFunctionDefinitionStartId signature;
   BuiltinNameId builtin_name;
@@ -345,8 +363,9 @@ using AliasInitializer = LeafNode<NodeKind::AliasInitializer>;
 
 // An `alias` declaration: `alias a = b;`.
 struct Alias {
-  static constexpr auto Kind =
-      NodeKind::Alias.Define(NodeCategory::Decl | NodeCategory::Statement);
+  static constexpr auto Kind = NodeKind::Alias.Define(
+      {.category = NodeCategory::Decl | NodeCategory::Statement,
+       .bracketed_by = AliasIntroducer::Kind});
 
   AliasIntroducerId introducer;
   llvm::SmallVector<AnyModifierId> modifiers;
@@ -363,8 +382,9 @@ using LetInitializer = LeafNode<NodeKind::LetInitializer>;
 
 // A `let` declaration: `let a: i32 = 5;`.
 struct LetDecl {
-  static constexpr auto Kind =
-      NodeKind::LetDecl.Define(NodeCategory::Decl | NodeCategory::Statement);
+  static constexpr auto Kind = NodeKind::LetDecl.Define(
+      {.category = NodeCategory::Decl | NodeCategory::Statement,
+       .bracketed_by = LetIntroducer::Kind});
 
   LetIntroducerId introducer;
   llvm::SmallVector<AnyModifierId> modifiers;
@@ -387,7 +407,8 @@ using VariableInitializer = LeafNode<NodeKind::VariableInitializer>;
 // A `var` declaration: `var a: i32;` or `var a: i32 = 5;`.
 struct VariableDecl {
   static constexpr auto Kind = NodeKind::VariableDecl.Define(
-      NodeCategory::Decl | NodeCategory::Statement);
+      {.category = NodeCategory::Decl | NodeCategory::Statement,
+       .bracketed_by = VariableIntroducer::Kind});
 
   VariableIntroducerId introducer;
   llvm::SmallVector<AnyModifierId> modifiers;
@@ -408,7 +429,8 @@ using CodeBlockStart = LeafNode<NodeKind::CodeBlockStart>;
 
 // A code block: `{ statement; statement; ... }`.
 struct CodeBlock {
-  static constexpr auto Kind = NodeKind::CodeBlock.Define();
+  static constexpr auto Kind =
+      NodeKind::CodeBlock.Define({.bracketed_by = CodeBlockStart::Kind});
 
   CodeBlockStartId left_brace;
   llvm::SmallVector<AnyStatementId> statements;
@@ -416,8 +438,8 @@ struct CodeBlock {
 
 // An expression statement: `F(x);`.
 struct ExprStatement {
-  static constexpr auto Kind =
-      NodeKind::ExprStatement.Define(NodeCategory::Statement);
+  static constexpr auto Kind = NodeKind::ExprStatement.Define(
+      {.category = NodeCategory::Statement, .child_count = 1});
 
   AnyExprId expr;
 };
@@ -426,8 +448,10 @@ using BreakStatementStart = LeafNode<NodeKind::BreakStatementStart>;
 
 // A break statement: `break;`.
 struct BreakStatement {
-  static constexpr auto Kind =
-      NodeKind::BreakStatement.Define(NodeCategory::Statement);
+  static constexpr auto Kind = NodeKind::BreakStatement.Define(
+      {.category = NodeCategory::Statement,
+       .bracketed_by = BreakStatementStart::Kind,
+       .child_count = 1});
 
   BreakStatementStartId introducer;
 };
@@ -436,8 +460,10 @@ using ContinueStatementStart = LeafNode<NodeKind::ContinueStatementStart>;
 
 // A continue statement: `continue;`.
 struct ContinueStatement {
-  static constexpr auto Kind =
-      NodeKind::ContinueStatement.Define(NodeCategory::Statement);
+  static constexpr auto Kind = NodeKind::ContinueStatement.Define(
+      {.category = NodeCategory::Statement,
+       .bracketed_by = ContinueStatementStart::Kind,
+       .child_count = 1});
 
   ContinueStatementStartId introducer;
 };
@@ -447,8 +473,9 @@ using ReturnVarModifier = LeafNode<NodeKind::ReturnVarModifier>;
 
 // A return statement: `return;` or `return expr;` or `return var;`.
 struct ReturnStatement {
-  static constexpr auto Kind =
-      NodeKind::ReturnStatement.Define(NodeCategory::Statement);
+  static constexpr auto Kind = NodeKind::ReturnStatement.Define(
+      {.category = NodeCategory::Statement,
+       .bracketed_by = ReturnStatementStart::Kind});
 
   ReturnStatementStartId introducer;
   std::optional<AnyExprId> expr;
@@ -459,7 +486,8 @@ using ForHeaderStart = LeafNode<NodeKind::ForHeaderStart>;
 
 // The `var ... in` portion of a `for` statement.
 struct ForIn {
-  static constexpr auto Kind = NodeKind::ForIn.Define();
+  static constexpr auto Kind = NodeKind::ForIn.Define(
+      {.bracketed_by = VariableIntroducer::Kind, .child_count = 2});
 
   VariableIntroducerId introducer;
   AnyPatternId pattern;
@@ -467,7 +495,8 @@ struct ForIn {
 
 // The `for (var ... in ...)` portion of a `for` statement.
 struct ForHeader {
-  static constexpr auto Kind = NodeKind::ForHeader.Define();
+  static constexpr auto Kind =
+      NodeKind::ForHeader.Define({.bracketed_by = ForHeaderStart::Kind});
 
   ForHeaderStartId introducer;
   ForInId var;
@@ -477,7 +506,9 @@ struct ForHeader {
 // A complete `for (...) { ... }` statement.
 struct ForStatement {
   static constexpr auto Kind =
-      NodeKind::ForStatement.Define(NodeCategory::Statement);
+      NodeKind::ForStatement.Define({.category = NodeCategory::Statement,
+                                     .bracketed_by = ForHeader::Kind,
+                                     .child_count = 2});
 
   ForHeaderId header;
   CodeBlockId body;
@@ -487,7 +518,8 @@ using IfConditionStart = LeafNode<NodeKind::IfConditionStart>;
 
 // The condition portion of an `if` statement: `(expr)`.
 struct IfCondition {
-  static constexpr auto Kind = NodeKind::IfCondition.Define();
+  static constexpr auto Kind = NodeKind::IfCondition.Define(
+      {.bracketed_by = IfConditionStart::Kind, .child_count = 2});
 
   IfConditionStartId left_paren;
   AnyExprId condition;
@@ -497,8 +529,8 @@ using IfStatementElse = LeafNode<NodeKind::IfStatementElse>;
 
 // An `if` statement: `if (expr) { ... } else { ... }`.
 struct IfStatement {
-  static constexpr auto Kind =
-      NodeKind::IfStatement.Define(NodeCategory::Statement);
+  static constexpr auto Kind = NodeKind::IfStatement.Define(
+      {.category = NodeCategory::Statement, .bracketed_by = IfCondition::Kind});
 
   IfConditionId head;
   CodeBlockId then;
@@ -514,7 +546,8 @@ using WhileConditionStart = LeafNode<NodeKind::WhileConditionStart>;
 
 // The condition portion of a `while` statement: `(expr)`.
 struct WhileCondition {
-  static constexpr auto Kind = NodeKind::WhileCondition.Define();
+  static constexpr auto Kind = NodeKind::WhileCondition.Define(
+      {.bracketed_by = WhileConditionStart::Kind, .child_count = 2});
 
   WhileConditionStartId left_paren;
   AnyExprId condition;
@@ -523,7 +556,9 @@ struct WhileCondition {
 // A `while` statement: `while (expr) { ... }`.
 struct WhileStatement {
   static constexpr auto Kind =
-      NodeKind::WhileStatement.Define(NodeCategory::Statement);
+      NodeKind::WhileStatement.Define({.category = NodeCategory::Statement,
+                                       .bracketed_by = WhileCondition::Kind,
+                                       .child_count = 2});
 
   WhileConditionId head;
   CodeBlockId body;
@@ -532,7 +567,8 @@ struct WhileStatement {
 using MatchConditionStart = LeafNode<NodeKind::MatchConditionStart>;
 
 struct MatchCondition {
-  static constexpr auto Kind = NodeKind::MatchCondition.Define();
+  static constexpr auto Kind = NodeKind::MatchCondition.Define(
+      {.bracketed_by = MatchConditionStart::Kind, .child_count = 2});
 
   MatchConditionStartId left_paren;
   AnyExprId condition;
@@ -540,7 +576,8 @@ struct MatchCondition {
 
 using MatchIntroducer = LeafNode<NodeKind::MatchIntroducer>;
 struct MatchStatementStart {
-  static constexpr auto Kind = NodeKind::MatchStatementStart.Define();
+  static constexpr auto Kind = NodeKind::MatchStatementStart.Define(
+      {.bracketed_by = MatchIntroducer::Kind, .child_count = 2});
 
   MatchIntroducerId introducer;
   MatchConditionId left_brace;
@@ -551,7 +588,9 @@ using MatchCaseGuardIntroducer = LeafNode<NodeKind::MatchCaseGuardIntroducer>;
 using MatchCaseGuardStart = LeafNode<NodeKind::MatchCaseGuardStart>;
 
 struct MatchCaseGuard {
-  static constexpr auto Kind = NodeKind::MatchCaseGuard.Define();
+  static constexpr auto Kind = NodeKind::MatchCaseGuard.Define(
+      {.bracketed_by = MatchCaseGuardIntroducer::Kind, .child_count = 3});
+
   MatchCaseGuardIntroducerId introducer;
   MatchCaseGuardStartId left_paren;
   AnyExprId condition;
@@ -560,7 +599,9 @@ struct MatchCaseGuard {
 using MatchCaseEqualGreater = LeafNode<NodeKind::MatchCaseEqualGreater>;
 
 struct MatchCaseStart {
-  static constexpr auto Kind = NodeKind::MatchCaseStart.Define();
+  static constexpr auto Kind = NodeKind::MatchCaseStart.Define(
+      {.bracketed_by = MatchCaseIntroducer::Kind});
+
   MatchCaseIntroducerId introducer;
   AnyPatternId pattern;
   std::optional<MatchCaseGuardId> guard;
@@ -568,7 +609,9 @@ struct MatchCaseStart {
 };
 
 struct MatchCase {
-  static constexpr auto Kind = NodeKind::MatchCase.Define();
+  static constexpr auto Kind =
+      NodeKind::MatchCase.Define({.bracketed_by = MatchCaseStart::Kind});
+
   MatchCaseStartId head;
   llvm::SmallVector<AnyStatementId> statements;
 };
@@ -577,13 +620,16 @@ using MatchDefaultIntroducer = LeafNode<NodeKind::MatchDefaultIntroducer>;
 using MatchDefaultEqualGreater = LeafNode<NodeKind::MatchDefaultEqualGreater>;
 
 struct MatchDefaultStart {
-  static constexpr auto Kind = NodeKind::MatchDefaultStart.Define();
+  static constexpr auto Kind = NodeKind::MatchDefaultStart.Define(
+      {.bracketed_by = MatchDefaultIntroducer::Kind, .child_count = 2});
+
   MatchDefaultIntroducerId introducer;
   MatchDefaultEqualGreaterId equal_greater_token;
 };
 
 struct MatchDefault {
-  static constexpr auto Kind = NodeKind::MatchDefault.Define();
+  static constexpr auto Kind =
+      NodeKind::MatchDefault.Define({.bracketed_by = MatchDefaultStart::Kind});
 
   MatchDefaultStartId introducer;
   llvm::SmallVector<AnyStatementId> statements;
@@ -591,8 +637,9 @@ struct MatchDefault {
 
 // A `match` statement: `match (expr) { case (...) => {...} default => {...}}`.
 struct MatchStatement {
-  static constexpr auto Kind =
-      NodeKind::MatchStatement.Define(NodeCategory::Statement);
+  static constexpr auto Kind = NodeKind::MatchStatement.Define(
+      {.category = NodeCategory::Statement,
+       .bracketed_by = MatchStatementStart::Kind});
 
   MatchStatementStartId head;
 
@@ -609,7 +656,8 @@ using ArrayExprStart = LeafNode<NodeKind::ArrayExprStart>;
 //
 // TODO: Consider flattening this into `ArrayExpr`.
 struct ArrayExprSemi {
-  static constexpr auto Kind = NodeKind::ArrayExprSemi.Define();
+  static constexpr auto Kind = NodeKind::ArrayExprSemi.Define(
+      {.bracketed_by = ArrayExprStart::Kind, .child_count = 2});
 
   ArrayExprStartId left_square;
   AnyExprId type;
@@ -617,7 +665,8 @@ struct ArrayExprSemi {
 
 // An array type, such as  `[i32; 3]` or `[i32;]`.
 struct ArrayExpr {
-  static constexpr auto Kind = NodeKind::ArrayExpr.Define(NodeCategory::Expr);
+  static constexpr auto Kind = NodeKind::ArrayExpr.Define(
+      {.category = NodeCategory::Expr, .bracketed_by = ArrayExprSemi::Kind});
 
   ArrayExprSemiId start;
   std::optional<AnyExprId> bound;
@@ -627,14 +676,18 @@ struct ArrayExpr {
 //
 // TODO: Consider flattening this into `IndexExpr`.
 struct IndexExprStart {
-  static constexpr auto Kind = NodeKind::IndexExprStart.Define();
+  static constexpr auto Kind =
+      NodeKind::IndexExprStart.Define({.child_count = 1});
 
   AnyExprId sequence;
 };
 
 // An indexing expression, such as `a[1]`.
 struct IndexExpr {
-  static constexpr auto Kind = NodeKind::IndexExpr.Define(NodeCategory::Expr);
+  static constexpr auto Kind =
+      NodeKind::IndexExpr.Define({.category = NodeCategory::Expr,
+                                  .bracketed_by = IndexExprStart::Kind,
+                                  .child_count = 2});
 
   IndexExprStartId start;
   AnyExprId index;
@@ -644,8 +697,10 @@ using ParenExprStart = LeafNode<NodeKind::ParenExprStart>;
 
 // A parenthesized expression: `(a)`.
 struct ParenExpr {
-  static constexpr auto Kind =
-      NodeKind::ParenExpr.Define(NodeCategory::Expr | NodeCategory::MemberExpr);
+  static constexpr auto Kind = NodeKind::ParenExpr.Define(
+      {.category = NodeCategory::Expr | NodeCategory::MemberExpr,
+       .bracketed_by = ParenExprStart::Kind,
+       .child_count = 2});
 
   ParenExprStartId start;
   AnyExprId expr;
@@ -657,7 +712,8 @@ using TupleLiteralComma = LeafNode<NodeKind::TupleLiteralComma>;
 // A tuple literal: `()`, `(a, b, c)`, or `(a,)`.
 struct TupleLiteral {
   static constexpr auto Kind =
-      NodeKind::TupleLiteral.Define(NodeCategory::Expr);
+      NodeKind::TupleLiteral.Define({.category = NodeCategory::Expr,
+                                     .bracketed_by = TupleLiteralStart::Kind});
 
   TupleLiteralStartId start;
   CommaSeparatedList<AnyExprId, TupleLiteralCommaId> elements;
@@ -667,7 +723,8 @@ struct TupleLiteral {
 //
 // TODO: Consider flattening this into `CallExpr`.
 struct CallExprStart {
-  static constexpr auto Kind = NodeKind::CallExprStart.Define();
+  static constexpr auto Kind =
+      NodeKind::CallExprStart.Define({.child_count = 1});
 
   AnyExprId callee;
 };
@@ -676,7 +733,8 @@ using CallExprComma = LeafNode<NodeKind::CallExprComma>;
 
 // A call expression: `F(a, b, c)`.
 struct CallExpr {
-  static constexpr auto Kind = NodeKind::CallExpr.Define(NodeCategory::Expr);
+  static constexpr auto Kind = NodeKind::CallExpr.Define(
+      {.category = NodeCategory::Expr, .bracketed_by = CallExprStart::Kind});
 
   CallExprStartId start;
   CommaSeparatedList<AnyExprId, CallExprCommaId> arguments;
@@ -684,8 +742,8 @@ struct CallExpr {
 
 // A member access expression: `a.b` or `a.(b)`.
 struct MemberAccessExpr {
-  static constexpr auto Kind =
-      NodeKind::MemberAccessExpr.Define(NodeCategory::Expr);
+  static constexpr auto Kind = NodeKind::MemberAccessExpr.Define(
+      {.category = NodeCategory::Expr, .child_count = 2});
 
   AnyExprId lhs;
   AnyMemberNameOrMemberExprId rhs;
@@ -693,8 +751,8 @@ struct MemberAccessExpr {
 
 // An indirect member access expression: `a->b` or `a->(b)`.
 struct PointerMemberAccessExpr {
-  static constexpr auto Kind =
-      NodeKind::PointerMemberAccessExpr.Define(NodeCategory::Expr);
+  static constexpr auto Kind = NodeKind::PointerMemberAccessExpr.Define(
+      {.category = NodeCategory::Expr, .child_count = 2});
 
   AnyExprId lhs;
   AnyMemberNameOrMemberExprId rhs;
@@ -703,7 +761,8 @@ struct PointerMemberAccessExpr {
 // A prefix operator expression.
 template <const NodeKind& KindT>
 struct PrefixOperator {
-  static constexpr auto Kind = KindT.Define(NodeCategory::Expr);
+  static constexpr auto Kind =
+      KindT.Define({.category = NodeCategory::Expr, .child_count = 1});
 
   AnyExprId operand;
 };
@@ -711,7 +770,8 @@ struct PrefixOperator {
 // An infix operator expression.
 template <const NodeKind& KindT>
 struct InfixOperator {
-  static constexpr auto Kind = KindT.Define(NodeCategory::Expr);
+  static constexpr auto Kind =
+      KindT.Define({.category = NodeCategory::Expr, .child_count = 2});
 
   AnyExprId lhs;
   AnyExprId rhs;
@@ -720,7 +780,8 @@ struct InfixOperator {
 // A postfix operator expression.
 template <const NodeKind& KindT>
 struct PostfixOperator {
-  static constexpr auto Kind = KindT.Define(NodeCategory::Expr);
+  static constexpr auto Kind =
+      KindT.Define({.category = NodeCategory::Expr, .child_count = 1});
 
   AnyExprId operand;
 };
@@ -748,28 +809,34 @@ struct PostfixOperator {
 // TODO: Make this be a template if we ever need to write generic code to cover
 // both cases at once, say in check.
 struct ShortCircuitOperandAnd {
-  static constexpr auto Kind = NodeKind::ShortCircuitOperandAnd.Define();
+  static constexpr auto Kind =
+      NodeKind::ShortCircuitOperandAnd.Define({.child_count = 1});
 
   AnyExprId operand;
 };
 
 struct ShortCircuitOperandOr {
-  static constexpr auto Kind = NodeKind::ShortCircuitOperandOr.Define();
+  static constexpr auto Kind =
+      NodeKind::ShortCircuitOperandOr.Define({.child_count = 1});
 
   AnyExprId operand;
 };
 
 struct ShortCircuitOperatorAnd {
-  static constexpr auto Kind =
-      NodeKind::ShortCircuitOperatorAnd.Define(NodeCategory::Expr);
+  static constexpr auto Kind = NodeKind::ShortCircuitOperatorAnd.Define(
+      {.category = NodeCategory::Expr,
+       .bracketed_by = ShortCircuitOperandAnd::Kind,
+       .child_count = 2});
 
   ShortCircuitOperandAndId lhs;
   AnyExprId rhs;
 };
 
 struct ShortCircuitOperatorOr {
-  static constexpr auto Kind =
-      NodeKind::ShortCircuitOperatorOr.Define(NodeCategory::Expr);
+  static constexpr auto Kind = NodeKind::ShortCircuitOperatorOr.Define(
+      {.category = NodeCategory::Expr,
+       .bracketed_by = ShortCircuitOperandOr::Kind,
+       .child_count = 2});
 
   ShortCircuitOperandOrId lhs;
   AnyExprId rhs;
@@ -777,21 +844,24 @@ struct ShortCircuitOperatorOr {
 
 // The `if` portion of an `if` expression: `if expr`.
 struct IfExprIf {
-  static constexpr auto Kind = NodeKind::IfExprIf.Define();
+  static constexpr auto Kind = NodeKind::IfExprIf.Define({.child_count = 1});
 
   AnyExprId condition;
 };
 
 // The `then` portion of an `if` expression: `then expr`.
 struct IfExprThen {
-  static constexpr auto Kind = NodeKind::IfExprThen.Define();
+  static constexpr auto Kind = NodeKind::IfExprThen.Define({.child_count = 1});
 
   AnyExprId result;
 };
 
 // A full `if` expression: `if expr then expr else expr`.
 struct IfExprElse {
-  static constexpr auto Kind = NodeKind::IfExprElse.Define(NodeCategory::Expr);
+  static constexpr auto Kind =
+      NodeKind::IfExprElse.Define({.category = NodeCategory::Expr,
+                                   .bracketed_by = IfExprIf::Kind,
+                                   .child_count = 3});
 
   IfExprIfId start;
   IfExprThenId then;
@@ -804,8 +874,8 @@ struct IfExprElse {
 using ChoiceIntroducer = LeafNode<NodeKind::ChoiceIntroducer>;
 
 struct ChoiceSignature {
-  static constexpr auto Kind =
-      NodeKind::ChoiceDefinitionStart.Define(NodeCategory::None);
+  static constexpr auto Kind = NodeKind::ChoiceDefinitionStart.Define(
+      {.category = NodeCategory::None, .bracketed_by = ChoiceIntroducer::Kind});
 
   ChoiceIntroducerId introducer;
   llvm::SmallVector<AnyModifierId> modifiers;
@@ -818,8 +888,9 @@ using ChoiceAlternativeListComma =
     LeafNode<NodeKind::ChoiceAlternativeListComma>;
 
 struct ChoiceDefinition {
-  static constexpr auto Kind =
-      NodeKind::ChoiceDefinition.Define(NodeCategory::Decl);
+  static constexpr auto Kind = NodeKind::ChoiceDefinition.Define(
+      {.category = NodeCategory::Decl,
+       .bracketed_by = ChoiceDefinitionStart::Kind});
 
   ChoiceDefinitionStartId signature;
   struct Alternative {
@@ -840,14 +911,16 @@ using StructComma = LeafNode<NodeKind::StructComma>;
 
 // `.a`
 struct StructFieldDesignator {
-  static constexpr auto Kind = NodeKind::StructFieldDesignator.Define();
+  static constexpr auto Kind =
+      NodeKind::StructFieldDesignator.Define({.child_count = 1});
 
   NodeIdOneOf<IdentifierName, BaseName> name;
 };
 
 // `.a = 0`
 struct StructField {
-  static constexpr auto Kind = NodeKind::StructField.Define();
+  static constexpr auto Kind = NodeKind::StructField.Define(
+      {.bracketed_by = StructFieldDesignator::Kind, .child_count = 2});
 
   StructFieldDesignatorId designator;
   AnyExprId expr;
@@ -855,7 +928,8 @@ struct StructField {
 
 // `.a: i32`
 struct StructTypeField {
-  static constexpr auto Kind = NodeKind::StructTypeField.Define();
+  static constexpr auto Kind = NodeKind::StructTypeField.Define(
+      {.bracketed_by = StructFieldDesignator::Kind, .child_count = 2});
 
   StructFieldDesignatorId designator;
   AnyExprId type_expr;
@@ -863,8 +937,9 @@ struct StructTypeField {
 
 // Struct literals, such as `{.a = 0}`.
 struct StructLiteral {
-  static constexpr auto Kind =
-      NodeKind::StructLiteral.Define(NodeCategory::Expr);
+  static constexpr auto Kind = NodeKind::StructLiteral.Define(
+      {.category = NodeCategory::Expr,
+       .bracketed_by = StructLiteralStart::Kind});
 
   StructLiteralStartId start;
   CommaSeparatedList<StructFieldId, StructCommaId> fields;
@@ -872,8 +947,9 @@ struct StructLiteral {
 
 // Struct type literals, such as `{.a: i32}`.
 struct StructTypeLiteral {
-  static constexpr auto Kind =
-      NodeKind::StructTypeLiteral.Define(NodeCategory::Expr);
+  static constexpr auto Kind = NodeKind::StructTypeLiteral.Define(
+      {.category = NodeCategory::Expr,
+       .bracketed_by = StructTypeLiteralStart::Kind});
 
   StructTypeLiteralStartId start;
   CommaSeparatedList<StructTypeFieldId, StructCommaId> fields;
@@ -888,7 +964,8 @@ using ClassIntroducer = LeafNode<NodeKind::ClassIntroducer>;
 // A class signature `class C`
 template <const NodeKind& KindT, NodeCategory Category>
 struct ClassSignature {
-  static constexpr auto Kind = KindT.Define(Category);
+  static constexpr auto Kind = KindT.Define(
+      {.category = Category, .bracketed_by = ClassIntroducer::Kind});
 
   ClassIntroducerId introducer;
   llvm::SmallVector<AnyModifierId> modifiers;
@@ -903,8 +980,9 @@ using ClassDefinitionStart =
 
 // `class C { ... }`
 struct ClassDefinition {
-  static constexpr auto Kind =
-      NodeKind::ClassDefinition.Define(NodeCategory::Decl);
+  static constexpr auto Kind = NodeKind::ClassDefinition.Define(
+      {.category = NodeCategory::Decl,
+       .bracketed_by = ClassDefinitionStart::Kind});
 
   ClassDefinitionStartId signature;
   llvm::SmallVector<AnyDeclId> members;
@@ -917,7 +995,8 @@ struct ClassDefinition {
 using AdaptIntroducer = LeafNode<NodeKind::AdaptIntroducer>;
 // `adapt SomeType;`
 struct AdaptDecl {
-  static constexpr auto Kind = NodeKind::AdaptDecl.Define(NodeCategory::Decl);
+  static constexpr auto Kind = NodeKind::AdaptDecl.Define(
+      {.category = NodeCategory::Decl, .bracketed_by = AdaptIntroducer::Kind});
 
   AdaptIntroducerId introducer;
   llvm::SmallVector<AnyModifierId> modifiers;
@@ -932,7 +1011,8 @@ using BaseIntroducer = LeafNode<NodeKind::BaseIntroducer>;
 using BaseColon = LeafNode<NodeKind::BaseColon>;
 // `extend base: BaseClass;`
 struct BaseDecl {
-  static constexpr auto Kind = NodeKind::BaseDecl.Define(NodeCategory::Decl);
+  static constexpr auto Kind = NodeKind::BaseDecl.Define(
+      {.category = NodeCategory::Decl, .bracketed_by = BaseIntroducer::Kind});
 
   BaseIntroducerId introducer;
   llvm::SmallVector<AnyModifierId> modifiers;
@@ -949,7 +1029,8 @@ using InterfaceIntroducer = LeafNode<NodeKind::InterfaceIntroducer>;
 // `interface I`
 template <const NodeKind& KindT, NodeCategory Category>
 struct InterfaceSignature {
-  static constexpr auto Kind = KindT.Define(Category);
+  static constexpr auto Kind = KindT.Define(
+      {.category = Category, .bracketed_by = InterfaceIntroducer::Kind});
 
   InterfaceIntroducerId introducer;
   llvm::SmallVector<AnyModifierId> modifiers;
@@ -965,8 +1046,9 @@ using InterfaceDefinitionStart =
 
 // `interface I { ... }`
 struct InterfaceDefinition {
-  static constexpr auto Kind =
-      NodeKind::InterfaceDefinition.Define(NodeCategory::Decl);
+  static constexpr auto Kind = NodeKind::InterfaceDefinition.Define(
+      {.category = NodeCategory::Decl,
+       .bracketed_by = InterfaceDefinitionStart::Kind});
 
   InterfaceDefinitionStartId signature;
   llvm::SmallVector<AnyDeclId> members;
@@ -980,7 +1062,7 @@ using ImplIntroducer = LeafNode<NodeKind::ImplIntroducer>;
 
 // `forall [...]`
 struct ImplForall {
-  static constexpr auto Kind = NodeKind::ImplForall.Define();
+  static constexpr auto Kind = NodeKind::ImplForall.Define({.child_count = 1});
 
   ImplicitParamListId params;
 };
@@ -991,8 +1073,8 @@ using DefaultSelfImplAs =
 
 // `<type> as`
 struct TypeImplAs {
-  static constexpr auto Kind =
-      NodeKind::TypeImplAs.Define(NodeCategory::ImplAs);
+  static constexpr auto Kind = NodeKind::TypeImplAs.Define(
+      {.category = NodeCategory::ImplAs, .child_count = 1});
 
   AnyExprId type_expr;
 };
@@ -1000,7 +1082,8 @@ struct TypeImplAs {
 // `impl T as I`
 template <const NodeKind& KindT, NodeCategory Category>
 struct ImplSignature {
-  static constexpr auto Kind = KindT.Define(Category);
+  static constexpr auto Kind = KindT.Define(
+      {.category = Category, .bracketed_by = ImplIntroducer::Kind});
 
   ImplIntroducerId introducer;
   llvm::SmallVector<AnyModifierId> modifiers;
@@ -1017,8 +1100,9 @@ using ImplDefinitionStart =
 
 // `impl T as I { ... }`
 struct ImplDefinition {
-  static constexpr auto Kind =
-      NodeKind::ImplDefinition.Define(NodeCategory::Decl);
+  static constexpr auto Kind = NodeKind::ImplDefinition.Define(
+      {.category = NodeCategory::Decl,
+       .bracketed_by = ImplDefinitionStart::Kind});
 
   ImplDefinitionStartId signature;
   llvm::SmallVector<AnyDeclId> members;
@@ -1033,7 +1117,8 @@ using NamedConstraintIntroducer = LeafNode<NodeKind::NamedConstraintIntroducer>;
 // `constraint NC`
 template <const NodeKind& KindT, NodeCategory Category>
 struct NamedConstraintSignature {
-  static constexpr auto Kind = KindT.Define(Category);
+  static constexpr auto Kind = KindT.Define(
+      {.category = Category, .bracketed_by = NamedConstraintIntroducer::Kind});
 
   NamedConstraintIntroducerId introducer;
   llvm::SmallVector<AnyModifierId> modifiers;
@@ -1050,8 +1135,9 @@ using NamedConstraintDefinitionStart =
 
 // `constraint NC { ... }`
 struct NamedConstraintDefinition {
-  static constexpr auto Kind =
-      NodeKind::NamedConstraintDefinition.Define(NodeCategory::Decl);
+  static constexpr auto Kind = NodeKind::NamedConstraintDefinition.Define(
+      {.category = NodeCategory::Decl,
+       .bracketed_by = NamedConstraintDefinitionStart::Kind});
 
   NamedConstraintDefinitionStartId signature;
   llvm::SmallVector<AnyDeclId> members;