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

Remove most of the metaprogramming in node.h in favor of listing all the members in the typed node structs. (#3310)

Split `node.h` into separate files for ID types (`id.h`) and for typed
nodes (`typed_nodes.h`). The per-node-kind data is now specified as part
of declaring the typed nodes, and is removed from the node kinds
x-macros, which now simply enumerate the node kinds.
Richard Smith 2 лет назад
Родитель
Сommit
a46e7dd967

+ 13 - 13
toolchain/check/context.cpp

@@ -168,7 +168,7 @@ static auto AddDominatedBlockAndBranchImpl(Context& context,
     return SemIR::NodeBlockId::Unreachable;
   }
   auto block_id = context.semantics_ir().AddNodeBlockId();
-  context.AddNode(BranchNode(parse_node, block_id, args...));
+  context.AddNode(BranchNode{parse_node, block_id, args...});
   return block_id;
 }
 
@@ -201,7 +201,7 @@ auto Context::AddConvergenceBlockAndPush(Parse::Node parse_node, int num_blocks)
       if (new_block_id == SemIR::NodeBlockId::Unreachable) {
         new_block_id = semantics_ir().AddNodeBlockId();
       }
-      AddNode(SemIR::Branch(parse_node, new_block_id));
+      AddNode(SemIR::Branch{parse_node, new_block_id});
     }
     node_block_stack().Pop();
   }
@@ -219,7 +219,7 @@ auto Context::AddConvergenceBlockWithArgAndPush(
       if (new_block_id == SemIR::NodeBlockId::Unreachable) {
         new_block_id = semantics_ir().AddNodeBlockId();
       }
-      AddNode(SemIR::BranchWithArg(parse_node, new_block_id, arg_id));
+      AddNode(SemIR::BranchWithArg{parse_node, new_block_id, arg_id});
     }
     node_block_stack().Pop();
   }
@@ -228,7 +228,7 @@ auto Context::AddConvergenceBlockWithArgAndPush(
   // Acquire the result value.
   SemIR::TypeId result_type_id =
       semantics_ir().GetNode(*block_args.begin()).type_id();
-  return AddNode(SemIR::BlockArg(parse_node, result_type_id, new_block_id));
+  return AddNode(SemIR::BlockArg{parse_node, result_type_id, new_block_id});
 }
 
 // Add the current code block to the enclosing function.
@@ -389,7 +389,7 @@ class TypeCompleter {
                  type_node.As<SemIR::StructType>().fields_id)) {
           Push(context_.semantics_ir()
                    .GetNodeAs<SemIR::StructTypeField>(field_id)
-                   .type_id);
+                   .field_type_id);
         }
         break;
 
@@ -511,10 +511,10 @@ class TypeCompleter {
     for (auto field_id : fields) {
       auto field =
           context_.semantics_ir().GetNodeAs<SemIR::StructTypeField>(field_id);
-      auto field_value_rep = GetNestedValueRepresentation(field.type_id);
-      if (field_value_rep.type_id != field.type_id) {
+      auto field_value_rep = GetNestedValueRepresentation(field.field_type_id);
+      if (field_value_rep.type_id != field.field_type_id) {
         same_as_object_rep = false;
-        field.type_id = field_value_rep.type_id;
+        field.field_type_id = field_value_rep.type_id;
         field_id = context_.AddNode(field);
       }
       value_rep_fields.push_back(field_id);
@@ -779,7 +779,7 @@ static auto ProfileType(Context& semantics_context, SemIR::Node node,
             semantics_context.semantics_ir().GetNodeAs<SemIR::StructTypeField>(
                 field_id);
         canonical_id.AddInteger(field.name_id.index);
-        canonical_id.AddInteger(field.type_id.index);
+        canonical_id.AddInteger(field.field_type_id.index);
       }
       break;
     }
@@ -822,7 +822,7 @@ auto Context::CanonicalizeStructType(Parse::Node parse_node,
                                      SemIR::NodeBlockId refs_id)
     -> SemIR::TypeId {
   return CanonicalizeTypeAndAddNodeIfNew(
-      SemIR::StructType(parse_node, SemIR::TypeId::TypeType, refs_id));
+      SemIR::StructType{parse_node, SemIR::TypeId::TypeType, refs_id});
 }
 
 auto Context::CanonicalizeTupleType(Parse::Node parse_node,
@@ -833,8 +833,8 @@ auto Context::CanonicalizeTupleType(Parse::Node parse_node,
     ProfileTupleType(type_ids, canonical_id);
   };
   auto make_tuple_node = [&] {
-    return AddNode(SemIR::TupleType(parse_node, SemIR::TypeId::TypeType,
-                                    semantics_ir_->AddTypeBlock(type_ids)));
+    return AddNode(SemIR::TupleType{parse_node, SemIR::TypeId::TypeType,
+                                    semantics_ir_->AddTypeBlock(type_ids)});
   };
   return CanonicalizeTypeImpl(SemIR::TupleType::Kind, profile_tuple,
                               make_tuple_node);
@@ -852,7 +852,7 @@ auto Context::GetBuiltinType(SemIR::BuiltinKind kind) -> SemIR::TypeId {
 auto Context::GetPointerType(Parse::Node parse_node,
                              SemIR::TypeId pointee_type_id) -> SemIR::TypeId {
   return CanonicalizeTypeAndAddNodeIfNew(
-      SemIR::PointerType(parse_node, SemIR::TypeId::TypeType, pointee_type_id));
+      SemIR::PointerType{parse_node, SemIR::TypeId::TypeType, pointee_type_id});
 }
 
 auto Context::GetUnqualifiedType(SemIR::TypeId type_id) -> SemIR::TypeId {

+ 32 - 35
toolchain/check/convert.cpp

@@ -90,8 +90,8 @@ static auto FinalizeTemporary(Context& context, SemIR::NodeId init_id,
         << "initialized multiple times? Have "
         << semantics_ir.GetNode(return_slot_id);
     auto init = semantics_ir.GetNode(init_id);
-    return context.AddNode(SemIR::Temporary(init.parse_node(), init.type_id(),
-                                            return_slot_id, init_id));
+    return context.AddNode(SemIR::Temporary{init.parse_node(), init.type_id(),
+                                            return_slot_id, init_id});
   }
 
   if (discarded) {
@@ -106,9 +106,9 @@ static auto FinalizeTemporary(Context& context, SemIR::NodeId init_id,
   // nodes.
   auto init = semantics_ir.GetNode(init_id);
   auto temporary_id = context.AddNode(
-      SemIR::TemporaryStorage(init.parse_node(), init.type_id()));
-  return context.AddNode(SemIR::Temporary(init.parse_node(), init.type_id(),
-                                          temporary_id, init_id));
+      SemIR::TemporaryStorage{init.parse_node(), init.type_id()});
+  return context.AddNode(SemIR::Temporary{init.parse_node(), init.type_id(),
+                                          temporary_id, init_id});
 }
 
 // Materialize a temporary to hold the result of the given expression if it is
@@ -132,14 +132,14 @@ static auto MakeElemAccessNode(Context& context, Parse::Node parse_node,
     // TODO: Add a new node kind for indexing an array at a constant index
     // so that we don't need an integer literal node here, and remove this
     // special case.
-    auto index_id = block.AddNode(SemIR::IntegerLiteral(
+    auto index_id = block.AddNode(SemIR::IntegerLiteral{
         parse_node, context.GetBuiltinType(SemIR::BuiltinKind::IntegerType),
-        context.semantics_ir().integers().Add(llvm::APInt(32, i))));
+        context.semantics_ir().integers().Add(llvm::APInt(32, i))});
     return block.AddNode(
-        AccessNodeT(parse_node, elem_type_id, aggregate_id, index_id));
+        AccessNodeT{parse_node, elem_type_id, aggregate_id, index_id});
   } else {
-    return block.AddNode(AccessNodeT(parse_node, elem_type_id, aggregate_id,
-                                     SemIR::MemberIndex(i)));
+    return block.AddNode(AccessNodeT{parse_node, elem_type_id, aggregate_id,
+                                     SemIR::MemberIndex(i)});
   }
 }
 
@@ -230,9 +230,8 @@ class CopyOnWriteBlock {
 
 // Performs a conversion from a tuple to an array type. Does not perform a
 // final conversion to the requested expression category.
-static auto ConvertTupleToArray(Context& context,
-                                SemIR::TupleType::Data tuple_type,
-                                SemIR::ArrayType::Data array_type,
+static auto ConvertTupleToArray(Context& context, SemIR::TupleType tuple_type,
+                                SemIR::ArrayType array_type,
                                 SemIR::NodeId value_id, ConversionTarget target)
     -> SemIR::NodeId {
   auto& semantics_ir = context.semantics_ir();
@@ -278,7 +277,7 @@ static auto ConvertTupleToArray(Context& context,
   SemIR::NodeId return_slot_id = target.init_id;
   if (!target.init_id.is_valid()) {
     return_slot_id = target_block->AddNode(
-        SemIR::TemporaryStorage(value.parse_node(), target.type_id));
+        SemIR::TemporaryStorage{value.parse_node(), target.type_id});
   }
 
   // Initialize each element of the array from the corresponding element of the
@@ -306,16 +305,15 @@ static auto ConvertTupleToArray(Context& context,
   target_block->InsertHere();
   inits.push_back(return_slot_id);
 
-  return context.AddNode(SemIR::ArrayInit(value.parse_node(), target.type_id,
+  return context.AddNode(SemIR::ArrayInit{value.parse_node(), target.type_id,
                                           value_id,
-                                          semantics_ir.AddNodeBlock(inits)));
+                                          semantics_ir.AddNodeBlock(inits)});
 }
 
 // Performs a conversion from a tuple to a tuple type. Does not perform a
 // final conversion to the requested expression category.
-static auto ConvertTupleToTuple(Context& context,
-                                SemIR::TupleType::Data src_type,
-                                SemIR::TupleType::Data dest_type,
+static auto ConvertTupleToTuple(Context& context, SemIR::TupleType src_type,
+                                SemIR::TupleType dest_type,
                                 SemIR::NodeId value_id, ConversionTarget target)
     -> SemIR::NodeId {
   auto& semantics_ir = context.semantics_ir();
@@ -378,19 +376,18 @@ static auto ConvertTupleToTuple(Context& context,
     new_block.Set(i, init_id);
   }
 
-  return is_init ? context.AddNode(SemIR::TupleInit(value.parse_node(),
+  return is_init ? context.AddNode(SemIR::TupleInit{value.parse_node(),
                                                     target.type_id, value_id,
-                                                    new_block.id()))
-                 : context.AddNode(SemIR::TupleValue(value.parse_node(),
+                                                    new_block.id()})
+                 : context.AddNode(SemIR::TupleValue{value.parse_node(),
                                                      target.type_id, value_id,
-                                                     new_block.id()));
+                                                     new_block.id()});
 }
 
 // Performs a conversion from a struct to a struct type. Does not perform a
 // final conversion to the requested expression category.
-static auto ConvertStructToStruct(Context& context,
-                                  SemIR::StructType::Data src_type,
-                                  SemIR::StructType::Data dest_type,
+static auto ConvertStructToStruct(Context& context, SemIR::StructType src_type,
+                                  SemIR::StructType dest_type,
                                   SemIR::NodeId value_id,
                                   ConversionTarget target) -> SemIR::NodeId {
   auto& semantics_ir = context.semantics_ir();
@@ -464,8 +461,8 @@ static auto ConvertStructToStruct(Context& context,
     // approach.
     auto init_id =
         ConvertAggregateElement<SemIR::StructAccess, SemIR::StructAccess>(
-            context, value.parse_node(), value_id, src_field.type_id,
-            literal_elems, inner_kind, target.init_id, dest_field.type_id,
+            context, value.parse_node(), value_id, src_field.field_type_id,
+            literal_elems, inner_kind, target.init_id, dest_field.field_type_id,
             target.init_block, i);
     if (init_id == SemIR::NodeId::BuiltinError) {
       return SemIR::NodeId::BuiltinError;
@@ -473,12 +470,12 @@ static auto ConvertStructToStruct(Context& context,
     new_block.Set(i, init_id);
   }
 
-  return is_init ? context.AddNode(SemIR::StructInit(value.parse_node(),
+  return is_init ? context.AddNode(SemIR::StructInit{value.parse_node(),
                                                      target.type_id, value_id,
-                                                     new_block.id()))
-                 : context.AddNode(SemIR::StructValue(value.parse_node(),
+                                                     new_block.id()})
+                 : context.AddNode(SemIR::StructValue{value.parse_node(),
                                                       target.type_id, value_id,
-                                                      new_block.id()));
+                                                      new_block.id()});
 }
 
 // Returns whether `category` is a valid expression category to produce as a
@@ -715,7 +712,7 @@ auto Convert(Context& context, Parse::Node parse_node, SemIR::NodeId expr_id,
           target.kind != ConversionTarget::Discarded) {
         // TODO: Support types with custom value representations.
         expr_id = context.AddNode(
-            SemIR::BindValue(expr.parse_node(), expr.type_id(), expr_id));
+            SemIR::BindValue{expr.parse_node(), expr.type_id(), expr_id});
       }
       break;
     }
@@ -730,8 +727,8 @@ auto Convert(Context& context, Parse::Node parse_node, SemIR::NodeId expr_id,
             SemIR::GetInitializingRepresentation(semantics_ir, target.type_id);
         init_rep.kind == SemIR::InitializingRepresentation::ByCopy) {
       target.init_block->InsertHere();
-      expr_id = context.AddNode(SemIR::InitializeFrom(
-          parse_node, target.type_id, expr_id, target.init_id));
+      expr_id = context.AddNode(SemIR::InitializeFrom{
+          parse_node, target.type_id, expr_id, target.init_id});
     }
   }
 

+ 2 - 2
toolchain/check/handle_array.cpp

@@ -42,9 +42,9 @@ auto HandleArrayExpression(Context& context, Parse::Node parse_node) -> bool {
     if (bound_value.getActiveBits() <= 64) {
       context.AddNodeAndPush(
           parse_node,
-          SemIR::ArrayType(
+          SemIR::ArrayType{
               parse_node, SemIR::TypeId::TypeType, bound_node_id,
-              ExpressionAsType(context, parse_node, element_type_node_id)));
+              ExpressionAsType(context, parse_node, element_type_node_id)});
       return true;
     }
   }

+ 2 - 2
toolchain/check/handle_call_expression.cpp

@@ -43,7 +43,7 @@ auto HandleCallExpression(Context& context, Parse::Node parse_node) -> bool {
     // Tentatively put storage for a temporary in the function's return slot.
     // This will be replaced if necessary when we perform initialization.
     auto temp_id = context.AddNode(
-        SemIR::TemporaryStorage(call_expr_parse_node, callable.return_type_id));
+        SemIR::TemporaryStorage{call_expr_parse_node, callable.return_type_id});
     context.ParamOrArgSave(temp_id);
   }
 
@@ -57,7 +57,7 @@ auto HandleCallExpression(Context& context, Parse::Node parse_node) -> bool {
   }
 
   auto call_node_id = context.AddNode(
-      SemIR::Call(call_expr_parse_node, type_id, callee_id, refs_id));
+      SemIR::Call{call_expr_parse_node, type_id, callee_id, refs_id});
 
   context.node_stack().Push(parse_node, call_node_id);
   return true;

+ 2 - 2
toolchain/check/handle_class.cpp

@@ -27,8 +27,8 @@ static auto BuildClassDeclaration(Context& context)
 
   // Add the class declaration.
   auto class_decl =
-      SemIR::ClassDeclaration(class_keyword, SemIR::TypeId::TypeType,
-                              SemIR::ClassId::Invalid, decl_block_id);
+      SemIR::ClassDeclaration{class_keyword, SemIR::TypeId::TypeType,
+                              SemIR::ClassId::Invalid, decl_block_id};
   auto class_decl_id = context.AddNode(class_decl);
 
   // Check whether this is a redeclaration.

+ 5 - 5
toolchain/check/handle_function.cpp

@@ -58,9 +58,9 @@ static auto BuildFunctionDeclaration(Context& context, bool is_definition)
           .PopForSoloParseNode<Parse::NodeKind::FunctionIntroducer>();
 
   // Add the function declaration.
-  auto function_decl = SemIR::FunctionDeclaration(
+  auto function_decl = SemIR::FunctionDeclaration{
       fn_node, context.GetBuiltinType(SemIR::BuiltinKind::FunctionType),
-      SemIR::FunctionId::Invalid);
+      SemIR::FunctionId::Invalid};
   auto function_decl_id = context.AddNode(function_decl);
 
   // Check whether this is a redeclaration.
@@ -145,7 +145,7 @@ auto HandleFunctionDefinition(Context& context, Parse::Node parse_node)
           "Missing `return` at end of function with declared return type.");
       context.emitter().Emit(parse_node, MissingReturnStatement);
     } else {
-      context.AddNode(SemIR::Return(parse_node));
+      context.AddNode(SemIR::Return{parse_node});
     }
   }
 
@@ -228,8 +228,8 @@ auto HandleReturnType(Context& context, Parse::Node parse_node) -> bool {
   // TODO: Use a dedicated node rather than VarStorage here.
   context.AddNodeAndPush(
       parse_node,
-      SemIR::VarStorage(parse_node, type_id,
-                        context.semantics_ir().strings().Add("return")));
+      SemIR::VarStorage{parse_node, type_id,
+                        context.semantics_ir().strings().Add("return")});
   return true;
 }
 

+ 1 - 1
toolchain/check/handle_if_statement.cpp

@@ -53,7 +53,7 @@ auto HandleIfStatement(Context& context, Parse::Node parse_node) -> bool {
       // block.
       auto else_block_id =
           context.node_stack().Pop<Parse::NodeKind::IfCondition>();
-      context.AddNode(SemIR::Branch(parse_node, else_block_id));
+      context.AddNode(SemIR::Branch{parse_node, else_block_id});
       context.node_block_stack().Pop();
       context.node_block_stack().Push(else_block_id);
       break;

+ 6 - 6
toolchain/check/handle_index.cpp

@@ -69,12 +69,12 @@ auto HandleIndexExpression(Context& context, Parse::Node parse_node) -> bool {
       if (array_cat == SemIR::ExpressionCategory::Value) {
         // If the operand is an array value, convert it to an ephemeral
         // reference to an array so we can perform a primitive indexing into it.
-        operand_node_id = context.AddNode(SemIR::ValueAsReference(
-            parse_node, operand_type_id, operand_node_id));
+        operand_node_id = context.AddNode(SemIR::ValueAsReference{
+            parse_node, operand_type_id, operand_node_id});
       }
       auto elem_id = context.AddNode(
-          SemIR::ArrayIndex(parse_node, array_type.element_type_id,
-                            operand_node_id, cast_index_id));
+          SemIR::ArrayIndex{parse_node, array_type.element_type_id,
+                            operand_node_id, cast_index_id});
       if (array_cat != SemIR::ExpressionCategory::DurableReference) {
         // Indexing a durable reference gives a durable reference expression.
         // Indexing anything else gives a value expression.
@@ -104,8 +104,8 @@ auto HandleIndexExpression(Context& context, Parse::Node parse_node) -> bool {
         index_node_id = SemIR::NodeId::BuiltinError;
       }
       context.AddNodeAndPush(parse_node,
-                             SemIR::TupleIndex(parse_node, element_type_id,
-                                               operand_node_id, index_node_id));
+                             SemIR::TupleIndex{parse_node, element_type_id,
+                                               operand_node_id, index_node_id});
       return true;
     }
     default: {

+ 8 - 8
toolchain/check/handle_literal.cpp

@@ -13,37 +13,37 @@ auto HandleLiteral(Context& context, Parse::Node parse_node) -> bool {
     case Lex::TokenKind::True: {
       context.AddNodeAndPush(
           parse_node,
-          SemIR::BoolLiteral(
+          SemIR::BoolLiteral{
               parse_node, context.GetBuiltinType(SemIR::BuiltinKind::BoolType),
               token_kind == Lex::TokenKind::True ? SemIR::BoolValue::True
-                                                 : SemIR::BoolValue::False));
+                                                 : SemIR::BoolValue::False});
       break;
     }
     case Lex::TokenKind::IntegerLiteral: {
       context.AddNodeAndPush(
           parse_node,
-          SemIR::IntegerLiteral(
+          SemIR::IntegerLiteral{
               parse_node,
               context.GetBuiltinType(SemIR::BuiltinKind::IntegerType),
-              context.tokens().GetIntegerLiteral(token)));
+              context.tokens().GetIntegerLiteral(token)});
       break;
     }
     case Lex::TokenKind::RealLiteral: {
       context.AddNodeAndPush(
           parse_node,
-          SemIR::RealLiteral(
+          SemIR::RealLiteral{
               parse_node,
               context.GetBuiltinType(SemIR::BuiltinKind::FloatingPointType),
-              context.tokens().GetRealLiteral(token)));
+              context.tokens().GetRealLiteral(token)});
       break;
     }
     case Lex::TokenKind::StringLiteral: {
       auto id = context.tokens().GetStringLiteral(token);
       context.AddNodeAndPush(
           parse_node,
-          SemIR::StringLiteral(
+          SemIR::StringLiteral{
               parse_node,
-              context.GetBuiltinType(SemIR::BuiltinKind::StringType), id));
+              context.GetBuiltinType(SemIR::BuiltinKind::StringType), id});
       break;
     }
     case Lex::TokenKind::Type: {

+ 3 - 3
toolchain/check/handle_loop_statement.cpp

@@ -20,7 +20,7 @@ auto HandleBreakStatementStart(Context& context, Parse::Node parse_node)
                       "`break` can only be used in a loop.");
     context.emitter().Emit(parse_node, BreakOutsideLoop);
   } else {
-    context.AddNode(SemIR::Branch(parse_node, stack.back().break_target));
+    context.AddNode(SemIR::Branch{parse_node, stack.back().break_target});
   }
 
   context.node_block_stack().Pop();
@@ -41,7 +41,7 @@ auto HandleContinueStatementStart(Context& context, Parse::Node parse_node)
                       "`continue` can only be used in a loop.");
     context.emitter().Emit(parse_node, ContinueOutsideLoop);
   } else {
-    context.AddNode(SemIR::Branch(parse_node, stack.back().continue_target));
+    context.AddNode(SemIR::Branch{parse_node, stack.back().continue_target});
   }
 
   context.node_block_stack().Pop();
@@ -111,7 +111,7 @@ auto HandleWhileStatement(Context& context, Parse::Node parse_node) -> bool {
   context.break_continue_stack().pop_back();
 
   // Add the loop backedge.
-  context.AddNode(SemIR::Branch(parse_node, loop_header_id));
+  context.AddNode(SemIR::Branch{parse_node, loop_header_id});
   context.node_block_stack().Pop();
 
   // Start emitting the loop exit block.

+ 4 - 4
toolchain/check/handle_name.cpp

@@ -52,7 +52,7 @@ auto HandleMemberAccessExpression(Context& context, Parse::Node parse_node)
     // TODO: Track that this node was named within `base_id`.
     context.AddNodeAndPush(
         parse_node,
-        SemIR::NameReference(parse_node, node.type_id(), name_id, node_id));
+        SemIR::NameReference{parse_node, node.type_id(), name_id, node_id});
     return true;
   }
 
@@ -73,8 +73,8 @@ auto HandleMemberAccessExpression(Context& context, Parse::Node parse_node)
             context.semantics_ir().GetNodeAs<SemIR::StructTypeField>(ref_id);
         if (name_id == field.name_id) {
           context.AddNodeAndPush(
-              parse_node, SemIR::StructAccess(parse_node, field.type_id,
-                                              base_id, SemIR::MemberIndex(i)));
+              parse_node, SemIR::StructAccess{parse_node, field.field_type_id,
+                                              base_id, SemIR::MemberIndex(i)});
           return true;
         }
       }
@@ -128,7 +128,7 @@ auto HandleNameExpression(Context& context, Parse::Node parse_node) -> bool {
   CARBON_CHECK(value.kind().value_kind() == SemIR::NodeValueKind::Typed);
   context.AddNodeAndPush(
       parse_node,
-      SemIR::NameReference(parse_node, value.type_id(), name_id, value_id));
+      SemIR::NameReference{parse_node, value.type_id(), name_id, value_id});
   return true;
 }
 

+ 2 - 2
toolchain/check/handle_namespace.cpp

@@ -15,9 +15,9 @@ auto HandleNamespaceStart(Context& context, Parse::Node /*parse_node*/)
 
 auto HandleNamespace(Context& context, Parse::Node parse_node) -> bool {
   auto name_context = context.declaration_name_stack().Pop();
-  auto namespace_id = context.AddNode(SemIR::Namespace(
+  auto namespace_id = context.AddNode(SemIR::Namespace{
       parse_node, context.GetBuiltinType(SemIR::BuiltinKind::NamespaceType),
-      context.semantics_ir().AddNameScope()));
+      context.semantics_ir().AddNameScope()});
   context.declaration_name_stack().AddNameToLookup(name_context, namespace_id);
   return true;
 }

+ 18 - 18
toolchain/check/handle_operator.cpp

@@ -24,9 +24,9 @@ auto HandleInfixOperator(Context& context, Parse::Node parse_node) -> bool {
 
       context.AddNodeAndPush(
           parse_node,
-          SemIR::BinaryOperatorAdd(
+          SemIR::BinaryOperatorAdd{
               parse_node, context.semantics_ir().GetNode(lhs_id).type_id(),
-              lhs_id, rhs_id));
+              lhs_id, rhs_id});
       return true;
 
     case Lex::TokenKind::And:
@@ -40,16 +40,16 @@ auto HandleInfixOperator(Context& context, Parse::Node parse_node) -> bool {
       // its value.
       auto resume_block_id = context.node_block_stack().PeekOrAdd(/*depth=*/1);
       context.AddNode(
-          SemIR::BranchWithArg(parse_node, resume_block_id, rhs_id));
+          SemIR::BranchWithArg{parse_node, resume_block_id, rhs_id});
       context.node_block_stack().Pop();
       context.AddCurrentCodeBlockToFunction();
 
       // Collect the result from either the first or second operand.
       context.AddNodeAndPush(
           parse_node,
-          SemIR::BlockArg(parse_node,
+          SemIR::BlockArg{parse_node,
                           context.semantics_ir().GetNode(rhs_id).type_id(),
-                          resume_block_id));
+                          resume_block_id});
       return true;
     }
     case Lex::TokenKind::Equal: {
@@ -65,7 +65,7 @@ auto HandleInfixOperator(Context& context, Parse::Node parse_node) -> bool {
       // TODO: Destroy the old value before reinitializing. This will require
       // building the destruction code before we build the RHS subexpression.
       rhs_id = Initialize(context, parse_node, lhs_id, rhs_id);
-      context.AddNode(SemIR::Assign(parse_node, lhs_id, rhs_id));
+      context.AddNode(SemIR::Assign{parse_node, lhs_id, rhs_id});
       // We model assignment as an expression, so we need to push a value for
       // it, even though it doesn't produce a value.
       // TODO: Consider changing our parse tree to model assignment as a
@@ -87,8 +87,8 @@ auto HandlePostfixOperator(Context& context, Parse::Node parse_node) -> bool {
     case Lex::TokenKind::Star: {
       auto inner_type_id = ExpressionAsType(context, parse_node, value_id);
       context.AddNodeAndPush(
-          parse_node, SemIR::PointerType(parse_node, SemIR::TypeId::TypeType,
-                                         inner_type_id));
+          parse_node, SemIR::PointerType{parse_node, SemIR::TypeId::TypeType,
+                                         inner_type_id});
       return true;
     }
 
@@ -123,12 +123,12 @@ auto HandlePrefixOperator(Context& context, Parse::Node parse_node) -> bool {
       }
       context.AddNodeAndPush(
           parse_node,
-          SemIR::AddressOf(
+          SemIR::AddressOf{
               parse_node,
               context.GetPointerType(
                   parse_node,
                   context.semantics_ir().GetNode(value_id).type_id()),
-              value_id));
+              value_id});
       return true;
     }
 
@@ -146,7 +146,7 @@ auto HandlePrefixOperator(Context& context, Parse::Node parse_node) -> bool {
       auto inner_type_id = ExpressionAsType(context, parse_node, value_id);
       context.AddNodeAndPush(
           parse_node,
-          SemIR::ConstType(parse_node, SemIR::TypeId::TypeType, inner_type_id));
+          SemIR::ConstType{parse_node, SemIR::TypeId::TypeType, inner_type_id});
       return true;
     }
 
@@ -154,9 +154,9 @@ auto HandlePrefixOperator(Context& context, Parse::Node parse_node) -> bool {
       value_id = ConvertToBoolValue(context, parse_node, value_id);
       context.AddNodeAndPush(
           parse_node,
-          SemIR::UnaryOperatorNot(
+          SemIR::UnaryOperatorNot{
               parse_node, context.semantics_ir().GetNode(value_id).type_id(),
-              value_id));
+              value_id});
       return true;
 
     case Lex::TokenKind::Star: {
@@ -186,7 +186,7 @@ auto HandlePrefixOperator(Context& context, Parse::Node parse_node) -> bool {
         builder.Emit();
       }
       context.AddNodeAndPush(
-          parse_node, SemIR::Dereference(parse_node, result_type_id, value_id));
+          parse_node, SemIR::Dereference{parse_node, result_type_id, value_id});
       return true;
     }
 
@@ -209,15 +209,15 @@ auto HandleShortCircuitOperand(Context& context, Parse::Node parse_node)
   switch (auto token_kind = context.tokens().GetKind(token)) {
     case Lex::TokenKind::And:
       branch_value_id = cond_value_id;
-      short_circuit_result_id = context.AddNode(SemIR::BoolLiteral(
-          parse_node, bool_type_id, SemIR::BoolValue::False));
+      short_circuit_result_id = context.AddNode(SemIR::BoolLiteral{
+          parse_node, bool_type_id, SemIR::BoolValue::False});
       break;
 
     case Lex::TokenKind::Or:
       branch_value_id = context.AddNode(
-          SemIR::UnaryOperatorNot(parse_node, bool_type_id, cond_value_id));
+          SemIR::UnaryOperatorNot{parse_node, bool_type_id, cond_value_id});
       short_circuit_result_id = context.AddNode(
-          SemIR::BoolLiteral(parse_node, bool_type_id, SemIR::BoolValue::True));
+          SemIR::BoolLiteral{parse_node, bool_type_id, SemIR::BoolValue::True});
       break;
 
     default:

+ 1 - 1
toolchain/check/handle_paren.cpp

@@ -49,7 +49,7 @@ auto HandleTupleLiteral(Context& context, Parse::Node parse_node) -> bool {
   auto type_id = context.CanonicalizeTupleType(parse_node, std::move(type_ids));
 
   auto value_id =
-      context.AddNode(SemIR::TupleLiteral(parse_node, type_id, refs_id));
+      context.AddNode(SemIR::TupleLiteral{parse_node, type_id, refs_id});
   context.node_stack().Push(parse_node, value_id);
   return true;
 }

+ 4 - 4
toolchain/check/handle_pattern_binding.cpp

@@ -44,14 +44,14 @@ auto HandlePatternBinding(Context& context, Parse::Node parse_node) -> bool {
         cast_type_id = SemIR::TypeId::Error;
       }
       context.AddNodeAndPush(
-          parse_node, SemIR::VarStorage(name_node, cast_type_id, name_id));
+          parse_node, SemIR::VarStorage{name_node, cast_type_id, name_id});
       break;
 
     case Parse::NodeKind::ParameterListStart:
       // Parameters can have incomplete types in a function declaration, but not
       // in a function definition. We don't know which kind we have here.
       context.AddNodeAndPush(
-          parse_node, SemIR::Parameter(name_node, cast_type_id, name_id));
+          parse_node, SemIR::Parameter{name_node, cast_type_id, name_id});
       break;
 
     case Parse::NodeKind::LetIntroducer:
@@ -71,8 +71,8 @@ auto HandlePatternBinding(Context& context, Parse::Node parse_node) -> bool {
       // the `let` pattern before we see the initializer.
       context.node_stack().Push(
           parse_node,
-          context.semantics_ir().AddNodeInNoBlock(SemIR::BindName(
-              name_node, cast_type_id, name_id, SemIR::NodeId::Invalid)));
+          context.semantics_ir().AddNodeInNoBlock(SemIR::BindName{
+              name_node, cast_type_id, name_id, SemIR::NodeId::Invalid}));
       break;
 
     default:

+ 2 - 2
toolchain/check/handle_statement.cpp

@@ -49,7 +49,7 @@ auto HandleReturnStatement(Context& context, Parse::Node parse_node) -> bool {
           .Emit();
     }
 
-    context.AddNode(SemIR::Return(parse_node));
+    context.AddNode(SemIR::Return{parse_node});
   } else {
     auto arg = context.node_stack().PopExpression();
     context.node_stack()
@@ -72,7 +72,7 @@ auto HandleReturnStatement(Context& context, Parse::Node parse_node) -> bool {
                                  callable.return_type_id);
     }
 
-    context.AddNode(SemIR::ReturnExpression(parse_node, arg));
+    context.AddNode(SemIR::ReturnExpression{parse_node, arg});
   }
 
   // Switch to a new, unreachable, empty node block. This typically won't

+ 5 - 5
toolchain/check/handle_struct.cpp

@@ -29,7 +29,7 @@ auto HandleStructFieldType(Context& context, Parse::Node parse_node) -> bool {
       context.node_stack().PopWithParseNode<Parse::NodeKind::Name>();
 
   context.AddNodeAndPush(
-      parse_node, SemIR::StructTypeField(name_node, name_id, cast_type_id));
+      parse_node, SemIR::StructTypeField{name_node, name_id, cast_type_id});
   return true;
 }
 
@@ -44,9 +44,9 @@ auto HandleStructFieldValue(Context& context, Parse::Node parse_node) -> bool {
   StringId name_id = context.node_stack().Pop<Parse::NodeKind::Name>();
 
   // Store the name for the type.
-  context.args_type_info_stack().AddNode(SemIR::StructTypeField(
+  context.args_type_info_stack().AddNode(SemIR::StructTypeField{
       parse_node, name_id,
-      context.semantics_ir().GetNode(value_node_id).type_id()));
+      context.semantics_ir().GetNode(value_node_id).type_id()});
 
   // Push the value back on the stack as an argument.
   context.node_stack().Push(parse_node, value_node_id);
@@ -66,7 +66,7 @@ auto HandleStructLiteral(Context& context, Parse::Node parse_node) -> bool {
   auto type_id = context.CanonicalizeStructType(parse_node, type_block_id);
 
   auto value_id =
-      context.AddNode(SemIR::StructLiteral(parse_node, type_id, refs_id));
+      context.AddNode(SemIR::StructLiteral{parse_node, type_id, refs_id});
   context.node_stack().Push(parse_node, value_id);
   return true;
 }
@@ -100,7 +100,7 @@ auto HandleStructTypeLiteral(Context& context, Parse::Node parse_node) -> bool {
 
   context.AddNodeAndPush(
       parse_node,
-      SemIR::StructType(parse_node, SemIR::TypeId::TypeType, refs_id));
+      SemIR::StructType{parse_node, SemIR::TypeId::TypeType, refs_id});
   return true;
 }
 

+ 1 - 1
toolchain/check/handle_variable.cpp

@@ -41,7 +41,7 @@ auto HandleVariableDeclaration(Context& context, Parse::Node parse_node)
     init_id = Initialize(context, parse_node, var_id, init_id);
     // TODO: Consider using different node kinds for assignment versus
     // initialization.
-    context.AddNode(SemIR::Assign(parse_node, var_id, init_id));
+    context.AddNode(SemIR::Assign{parse_node, var_id, init_id});
   }
 
   context.node_stack()

+ 4 - 4
toolchain/check/pending_block.h

@@ -64,8 +64,8 @@ class PendingBlock {
       // 1) The block is empty. Replace `target_id` with an empty splice
       // pointing at `value_id`.
       context_.semantics_ir().ReplaceNode(
-          target_id, SemIR::SpliceBlock(value.parse_node(), value.type_id(),
-                                        SemIR::NodeBlockId::Empty, value_id));
+          target_id, SemIR::SpliceBlock{value.parse_node(), value.type_id(),
+                                        SemIR::NodeBlockId::Empty, value_id});
     } else if (nodes_.size() == 1 && nodes_[0] == value_id) {
       // 2) The block is {value_id}. Replace `target_id` with the node referred
       // to by `value_id`. This is intended to be the common case.
@@ -74,9 +74,9 @@ class PendingBlock {
       // 3) Anything else: splice it into the IR, replacing `target_id`.
       context_.semantics_ir().ReplaceNode(
           target_id,
-          SemIR::SpliceBlock(value.parse_node(), value.type_id(),
+          SemIR::SpliceBlock{value.parse_node(), value.type_id(),
                              context_.semantics_ir().AddNodeBlock(nodes_),
-                             value_id));
+                             value_id});
     }
 
     // Prepare to stash more pending instructions.

+ 3 - 3
toolchain/lower/file_context.cpp

@@ -238,9 +238,9 @@ auto FileContext::BuildType(SemIR::NodeId node_id) -> llvm::Type* {
         auto field = semantics_ir_->GetNodeAs<SemIR::StructTypeField>(field_id);
         // TODO: Handle recursive types. The restriction for builtins prevents
         // recursion while still letting them cache.
-        CARBON_CHECK(field.type_id.index < SemIR::BuiltinKind::ValidCount)
-            << field.type_id;
-        subtypes.push_back(GetType(field.type_id));
+        CARBON_CHECK(field.field_type_id.index < SemIR::BuiltinKind::ValidCount)
+            << field.field_type_id;
+        subtypes.push_back(GetType(field.field_type_id));
       }
       return llvm::StructType::get(*llvm_context_, subtypes);
     }

+ 29 - 1
toolchain/sem_ir/BUILD

@@ -14,13 +14,29 @@ cc_library(
     deps = ["//common:enum_base"],
 )
 
+cc_library(
+    name = "ids",
+    hdrs = ["ids.h"],
+    deps = [
+        "//common:ostream",
+        "//toolchain/base:index_base",
+        "//toolchain/sem_ir:builtin_kind",
+    ],
+)
+
 cc_library(
     name = "node_kind",
     srcs = ["node_kind.cpp"],
-    hdrs = ["node_kind.h"],
+    hdrs = [
+        "node_kind.h",
+        "typed_nodes.h",
+    ],
     textual_hdrs = ["node_kind.def"],
     deps = [
         "//common:enum_base",
+        "//toolchain/parse:tree",
+        "//toolchain/sem_ir:builtin_kind",
+        "//toolchain/sem_ir:ids",
         "@llvm-project//llvm:Support",
     ],
 )
@@ -78,6 +94,18 @@ cc_library(
     ],
 )
 
+cc_test(
+    name = "typed_nodes_test",
+    size = "small",
+    srcs = ["typed_nodes_test.cpp"],
+    deps = [
+        ":node",
+        ":node_kind",
+        "//testing/base:gtest_main",
+        "@com_google_googletest//:gtest",
+    ],
+)
+
 cc_test(
     name = "yaml_test",
     size = "small",

+ 5 - 4
toolchain/sem_ir/file.cpp

@@ -53,10 +53,10 @@ File::File(SharedValueStores& value_stores)
   // a normal type. Every other builtin is a type, including the
   // self-referential TypeType.
 #define CARBON_SEM_IR_BUILTIN_KIND(Name, ...)                      \
-  nodes_.push_back(Builtin(BuiltinKind::Name == BuiltinKind::Error \
+  nodes_.push_back(Builtin{BuiltinKind::Name == BuiltinKind::Error \
                                ? TypeId::Error                     \
                                : TypeId::TypeType,                 \
-                           BuiltinKind::Name));
+                           BuiltinKind::Name});
 #include "toolchain/sem_ir/builtin_kind.def"
 
   CARBON_CHECK(nodes_.size() == BuiltinKind::ValidCount)
@@ -82,7 +82,7 @@ File::File(SharedValueStores& value_stores, std::string filename,
   for (auto [i, node] : llvm::enumerate(builtins->nodes_)) {
     // We can reuse builtin type IDs because they're special-cased values.
     nodes_.push_back(
-        CrossReference(node.type_id(), BuiltinIR, SemIR::NodeId(i)));
+        CrossReference{node.type_id(), BuiltinIR, SemIR::NodeId(i)});
   }
 }
 
@@ -374,7 +374,8 @@ auto File::StringifyTypeExpression(NodeId outer_node_id,
       case StructTypeField::Kind: {
         auto field = node.As<StructTypeField>();
         out << "." << strings().Get(field.name_id) << ": ";
-        steps.push_back({.node_id = GetTypeAllowBuiltinTypes(field.type_id)});
+        steps.push_back(
+            {.node_id = GetTypeAllowBuiltinTypes(field.field_type_id)});
         break;
       }
       case TupleType::Kind: {

+ 9 - 2
toolchain/sem_ir/formatter.cpp

@@ -667,7 +667,14 @@ class Formatter {
   template <typename NodeT>
   auto FormatInstructionRHS(NodeT node) -> void {
     // By default, an instruction has a comma-separated argument list.
-    std::apply([&](auto... args) { FormatArgs(args...); }, node.args_tuple());
+    using Info = TypedNodeArgsInfo<NodeT>;
+    if constexpr (Info::NumArgs == 2) {
+      FormatArgs(Info::template Get<0>(node), Info::template Get<1>(node));
+    } else if constexpr (Info::NumArgs == 1) {
+      FormatArgs(Info::template Get<0>(node));
+    } else {
+      FormatArgs();
+    }
   }
 
   auto FormatInstructionRHS(BlockArg node) -> void {
@@ -792,7 +799,7 @@ class Formatter {
       auto field = semantics_ir_.GetNodeAs<StructTypeField>(field_id);
       FormatString(field.name_id);
       out_ << ": ";
-      FormatType(field.type_id);
+      FormatType(field.field_type_id);
     }
     out_ << "}";
   }

+ 213 - 0
toolchain/sem_ir/ids.h

@@ -0,0 +1,213 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#ifndef CARBON_TOOLCHAIN_SEM_IR_IDS_H_
+#define CARBON_TOOLCHAIN_SEM_IR_IDS_H_
+
+#include <cstdint>
+
+#include "common/ostream.h"
+#include "toolchain/base/index_base.h"
+#include "toolchain/sem_ir/builtin_kind.h"
+
+namespace Carbon::SemIR {
+
+// The ID of a node.
+struct NodeId : public IndexBase, public Printable<NodeId> {
+  // An explicitly invalid node ID.
+  static const NodeId Invalid;
+
+// Builtin node IDs.
+#define CARBON_SEM_IR_BUILTIN_KIND_NAME(Name) static const NodeId Builtin##Name;
+#include "toolchain/sem_ir/builtin_kind.def"
+
+  // Returns the cross-reference node ID for a builtin. This relies on File
+  // guarantees for builtin cross-reference placement.
+  static constexpr auto ForBuiltin(BuiltinKind kind) -> NodeId {
+    return NodeId(kind.AsInt());
+  }
+
+  using IndexBase::IndexBase;
+  auto Print(llvm::raw_ostream& out) const -> void {
+    out << "node";
+    if (!is_valid()) {
+      IndexBase::Print(out);
+    } else if (index < BuiltinKind::ValidCount) {
+      out << BuiltinKind::FromInt(index);
+    } else {
+      // Use the `+` as a small reminder that this is a delta, rather than an
+      // absolute index.
+      out << "+" << index - BuiltinKind::ValidCount;
+    }
+  }
+};
+
+constexpr NodeId NodeId::Invalid = NodeId(NodeId::InvalidIndex);
+
+#define CARBON_SEM_IR_BUILTIN_KIND_NAME(Name) \
+  constexpr NodeId NodeId::Builtin##Name =    \
+      NodeId::ForBuiltin(BuiltinKind::Name);
+#include "toolchain/sem_ir/builtin_kind.def"
+
+// The ID of a function.
+struct FunctionId : public IndexBase, public Printable<FunctionId> {
+  // An explicitly invalid function ID.
+  static const FunctionId Invalid;
+
+  using IndexBase::IndexBase;
+  auto Print(llvm::raw_ostream& out) const -> void {
+    out << "function";
+    IndexBase::Print(out);
+  }
+};
+
+constexpr FunctionId FunctionId::Invalid = FunctionId(FunctionId::InvalidIndex);
+
+// The ID of a class.
+struct ClassId : public IndexBase, public Printable<ClassId> {
+  // An explicitly invalid class ID.
+  static const ClassId Invalid;
+
+  using IndexBase::IndexBase;
+  auto Print(llvm::raw_ostream& out) const -> void {
+    out << "class";
+    IndexBase::Print(out);
+  }
+};
+
+constexpr ClassId ClassId::Invalid = ClassId(ClassId::InvalidIndex);
+
+// The ID of a cross-referenced IR.
+struct CrossReferenceIRId : public IndexBase,
+                            public Printable<CrossReferenceIRId> {
+  using IndexBase::IndexBase;
+  auto Print(llvm::raw_ostream& out) const -> void {
+    out << "ir";
+    IndexBase::Print(out);
+  }
+};
+
+// A boolean value.
+struct BoolValue : public IndexBase, public Printable<BoolValue> {
+  static const BoolValue False;
+  static const BoolValue True;
+
+  using IndexBase::IndexBase;
+  auto Print(llvm::raw_ostream& out) const -> void {
+    switch (index) {
+      case 0:
+        out << "false";
+        break;
+      case 1:
+        out << "true";
+        break;
+      default:
+        CARBON_FATAL() << "Invalid bool value " << index;
+    }
+  }
+};
+
+constexpr BoolValue BoolValue::False = BoolValue(0);
+constexpr BoolValue BoolValue::True = BoolValue(1);
+
+// The ID of a name scope.
+struct NameScopeId : public IndexBase, public Printable<NameScopeId> {
+  // An explicitly invalid ID.
+  static const NameScopeId Invalid;
+
+  using IndexBase::IndexBase;
+  auto Print(llvm::raw_ostream& out) const -> void {
+    out << "name_scope";
+    IndexBase::Print(out);
+  }
+};
+
+constexpr NameScopeId NameScopeId::Invalid =
+    NameScopeId(NameScopeId::InvalidIndex);
+
+// The ID of a node block.
+struct NodeBlockId : public IndexBase, public Printable<NodeBlockId> {
+  // All File instances must provide the 0th node block as empty.
+  static const NodeBlockId Empty;
+
+  // An explicitly invalid ID.
+  static const NodeBlockId Invalid;
+
+  // An ID for unreachable code.
+  static const NodeBlockId Unreachable;
+
+  using IndexBase::IndexBase;
+  auto Print(llvm::raw_ostream& out) const -> void {
+    if (index == Unreachable.index) {
+      out << "unreachable";
+    } else {
+      out << "block";
+      IndexBase::Print(out);
+    }
+  }
+};
+
+constexpr NodeBlockId NodeBlockId::Empty = NodeBlockId(0);
+constexpr NodeBlockId NodeBlockId::Invalid =
+    NodeBlockId(NodeBlockId::InvalidIndex);
+constexpr NodeBlockId NodeBlockId::Unreachable =
+    NodeBlockId(NodeBlockId::InvalidIndex - 1);
+
+// The ID of a node block.
+struct TypeId : public IndexBase, public Printable<TypeId> {
+  // The builtin TypeType.
+  static const TypeId TypeType;
+
+  // The builtin Error.
+  static const TypeId Error;
+
+  // An explicitly invalid ID.
+  static const TypeId Invalid;
+
+  using IndexBase::IndexBase;
+  auto Print(llvm::raw_ostream& out) const -> void {
+    out << "type";
+    if (index == TypeType.index) {
+      out << "TypeType";
+    } else if (index == Error.index) {
+      out << "Error";
+    } else {
+      IndexBase::Print(out);
+    }
+  }
+};
+
+constexpr TypeId TypeId::TypeType = TypeId(TypeId::InvalidIndex - 2);
+constexpr TypeId TypeId::Error = TypeId(TypeId::InvalidIndex - 1);
+constexpr TypeId TypeId::Invalid = TypeId(TypeId::InvalidIndex);
+
+// The ID of a type block.
+struct TypeBlockId : public IndexBase, public Printable<TypeBlockId> {
+  using IndexBase::IndexBase;
+  auto Print(llvm::raw_ostream& out) const -> void {
+    out << "typeBlock";
+    IndexBase::Print(out);
+  }
+};
+
+// An index for member access, for structs and tuples.
+struct MemberIndex : public IndexBase, public Printable<MemberIndex> {
+  using IndexBase::IndexBase;
+  auto Print(llvm::raw_ostream& out) const -> void {
+    out << "member";
+    IndexBase::Print(out);
+  }
+};
+
+}  // namespace Carbon::SemIR
+
+// Support use of Id types as DenseMap/DenseSet keys.
+template <>
+struct llvm::DenseMapInfo<Carbon::SemIR::NodeBlockId>
+    : public Carbon::IndexMapInfo<Carbon::SemIR::NodeBlockId> {};
+template <>
+struct llvm::DenseMapInfo<Carbon::SemIR::NodeId>
+    : public Carbon::IndexMapInfo<Carbon::SemIR::NodeId> {};
+
+#endif  // CARBON_TOOLCHAIN_SEM_IR_IDS_H_

+ 11 - 6
toolchain/sem_ir/node.cpp

@@ -9,17 +9,22 @@ namespace Carbon::SemIR {
 auto Node::Print(llvm::raw_ostream& out) const -> void {
   out << "{kind: " << kind_;
 
-  auto print_args = [&](auto... args) {
-    int n = 0;
-    ((out << ", arg" << n++ << ": " << args), ...);
+  auto print_args = [&](auto info) {
+    using Info = decltype(info);
+    if constexpr (Info::NumArgs > 0) {
+      out << ", arg0: " << FromRaw<typename Info::template ArgType<0>>(arg0_);
+    }
+    if constexpr (Info::NumArgs > 1) {
+      out << ", arg1: " << FromRaw<typename Info::template ArgType<1>>(arg1_);
+    }
   };
 
   // clang warns on unhandled enum values; clang-tidy is incorrect here.
   // NOLINTNEXTLINE(bugprone-switch-missing-default-case)
   switch (kind_) {
-#define CARBON_SEM_IR_NODE_KIND(Name)                       \
-  case Name::Kind:                                          \
-    std::apply(print_args, As<SemIR::Name>().args_tuple()); \
+#define CARBON_SEM_IR_NODE_KIND(Name)      \
+  case Name::Kind:                         \
+    print_args(TypedNodeArgsInfo<Name>()); \
     break;
 #include "toolchain/sem_ir/node_kind.def"
   }

+ 112 - 719
toolchain/sem_ir/node.h

@@ -14,515 +14,122 @@
 #include "toolchain/parse/tree.h"
 #include "toolchain/sem_ir/builtin_kind.h"
 #include "toolchain/sem_ir/node_kind.h"
+#include "toolchain/sem_ir/typed_nodes.h"
 
 namespace Carbon::SemIR {
 
-// The ID of a node.
-struct NodeId : public IndexBase, public Printable<NodeId> {
-  // An explicitly invalid node ID.
-  static const NodeId Invalid;
+// Data about the arguments of a typed node, to aid in type erasure. The `KindT`
+// parameter is used to check that `TypedNode` is a typed node.
+template <typename TypedNode,
+          const NodeKind::Definition& KindT = TypedNode::Kind>
+struct TypedNodeArgsInfo {
+  // A corresponding std::tuple<...> type.
+  using Tuple = decltype(StructReflection::AsTuple(std::declval<TypedNode>()));
 
-// Builtin node IDs.
-#define CARBON_SEM_IR_BUILTIN_KIND_NAME(Name) static const NodeId Builtin##Name;
-#include "toolchain/sem_ir/builtin_kind.def"
+  static constexpr int FirstArgField =
+      HasParseNode<TypedNode> + HasTypeId<TypedNode>;
 
-  // Returns the cross-reference node ID for a builtin. This relies on File
-  // guarantees for builtin cross-reference placement.
-  static constexpr auto ForBuiltin(BuiltinKind kind) -> NodeId {
-    return NodeId(kind.AsInt());
-  }
-
-  using IndexBase::IndexBase;
-  auto Print(llvm::raw_ostream& out) const -> void {
-    out << "node";
-    if (!is_valid()) {
-      IndexBase::Print(out);
-    } else if (index < BuiltinKind::ValidCount) {
-      out << BuiltinKind::FromInt(index);
-    } else {
-      // Use the `+` as a small reminder that this is a delta, rather than an
-      // absolute index.
-      out << "+" << index - BuiltinKind::ValidCount;
-    }
-  }
-};
-
-constexpr NodeId NodeId::Invalid = NodeId(NodeId::InvalidIndex);
-
-#define CARBON_SEM_IR_BUILTIN_KIND_NAME(Name) \
-  constexpr NodeId NodeId::Builtin##Name =    \
-      NodeId::ForBuiltin(BuiltinKind::Name);
-#include "toolchain/sem_ir/builtin_kind.def"
+  static constexpr int NumArgs = std::tuple_size_v<Tuple> - FirstArgField;
+  static_assert(NumArgs <= 2,
+                "Unsupported: typed node has more than two data fields");
 
-// The ID of a function.
-struct FunctionId : public IndexBase, public Printable<FunctionId> {
-  // An explicitly invalid function ID.
-  static const FunctionId Invalid;
+  template <int N>
+  using ArgType = std::tuple_element_t<FirstArgField + N, Tuple>;
 
-  using IndexBase::IndexBase;
-  auto Print(llvm::raw_ostream& out) const -> void {
-    out << "function";
-    IndexBase::Print(out);
+  template <int N>
+  static auto Get(TypedNode node) -> ArgType<N> {
+    return std::get<FirstArgField + N>(StructReflection::AsTuple(node));
   }
 };
 
-constexpr FunctionId FunctionId::Invalid = FunctionId(FunctionId::InvalidIndex);
-
-// The ID of a class.
-struct ClassId : public IndexBase, public Printable<ClassId> {
-  // An explicitly invalid class ID.
-  static const ClassId Invalid;
-
-  using IndexBase::IndexBase;
-  auto Print(llvm::raw_ostream& out) const -> void {
-    out << "class";
-    IndexBase::Print(out);
-  }
-};
-
-constexpr ClassId ClassId::Invalid = ClassId(ClassId::InvalidIndex);
-
-// The ID of a cross-referenced IR.
-struct CrossReferenceIRId : public IndexBase,
-                            public Printable<CrossReferenceIRId> {
-  using IndexBase::IndexBase;
-  auto Print(llvm::raw_ostream& out) const -> void {
-    out << "ir";
-    IndexBase::Print(out);
-  }
-};
-
-// A boolean value.
-struct BoolValue : public IndexBase, public Printable<BoolValue> {
-  static const BoolValue False;
-  static const BoolValue True;
-
-  using IndexBase::IndexBase;
-  auto Print(llvm::raw_ostream& out) const -> void {
-    switch (index) {
-      case 0:
-        out << "false";
-        break;
-      case 1:
-        out << "true";
-        break;
-      default:
-        CARBON_FATAL() << "Invalid bool value " << index;
-    }
-  }
-};
-
-constexpr BoolValue BoolValue::False = BoolValue(0);
-constexpr BoolValue BoolValue::True = BoolValue(1);
-
-// The ID of a name scope.
-struct NameScopeId : public IndexBase, public Printable<NameScopeId> {
-  // An explicitly invalid ID.
-  static const NameScopeId Invalid;
-
-  using IndexBase::IndexBase;
-  auto Print(llvm::raw_ostream& out) const -> void {
-    out << "name_scope";
-    IndexBase::Print(out);
-  }
-};
-
-constexpr NameScopeId NameScopeId::Invalid =
-    NameScopeId(NameScopeId::InvalidIndex);
-
-// The ID of a node block.
-struct NodeBlockId : public IndexBase, public Printable<NodeBlockId> {
-  // All File instances must provide the 0th node block as empty.
-  static const NodeBlockId Empty;
-
-  // An explicitly invalid ID.
-  static const NodeBlockId Invalid;
-
-  // An ID for unreachable code.
-  static const NodeBlockId Unreachable;
-
-  using IndexBase::IndexBase;
-  auto Print(llvm::raw_ostream& out) const -> void {
-    if (index == Unreachable.index) {
-      out << "unreachable";
-    } else {
-      out << "block";
-      IndexBase::Print(out);
-    }
-  }
-};
-
-constexpr NodeBlockId NodeBlockId::Empty = NodeBlockId(0);
-constexpr NodeBlockId NodeBlockId::Invalid =
-    NodeBlockId(NodeBlockId::InvalidIndex);
-constexpr NodeBlockId NodeBlockId::Unreachable =
-    NodeBlockId(NodeBlockId::InvalidIndex - 1);
-
-// The ID of a node block.
-struct TypeId : public IndexBase, public Printable<TypeId> {
-  // The builtin TypeType.
-  static const TypeId TypeType;
-
-  // The builtin Error.
-  static const TypeId Error;
-
-  // An explicitly invalid ID.
-  static const TypeId Invalid;
-
-  using IndexBase::IndexBase;
-  auto Print(llvm::raw_ostream& out) const -> void {
-    out << "type";
-    if (index == TypeType.index) {
-      out << "TypeType";
-    } else if (index == Error.index) {
-      out << "Error";
-    } else {
-      IndexBase::Print(out);
-    }
-  }
-};
-
-constexpr TypeId TypeId::TypeType = TypeId(TypeId::InvalidIndex - 2);
-constexpr TypeId TypeId::Error = TypeId(TypeId::InvalidIndex - 1);
-constexpr TypeId TypeId::Invalid = TypeId(TypeId::InvalidIndex);
-
-// The ID of a type block.
-struct TypeBlockId : public IndexBase, public Printable<TypeBlockId> {
-  using IndexBase::IndexBase;
-  auto Print(llvm::raw_ostream& out) const -> void {
-    out << "typeBlock";
-    IndexBase::Print(out);
-  }
-};
-
-// An index for member access, for structs and tuples.
-struct MemberIndex : public IndexBase, public Printable<MemberIndex> {
-  using IndexBase::IndexBase;
-  auto Print(llvm::raw_ostream& out) const -> void {
-    out << "member";
-    IndexBase::Print(out);
-  }
-};
-
-// Data storage for the operands of each kind of node.
-//
-// For each node kind declared in `node_kinds.def`, a struct here with the same
-// name describes the kind-specific storage for that node. A node kind can
-// store up to two IDs.
-//
-// A typed node also has:
-//
-// -  An injected `Parse::Node parse_node;` field, unless it specifies
-//    `using HasParseNode = std::false_type;`, and
-// -  An injected `TypeId type_id;` field, unless it specifies
-//    `using HasTypeId = std::false_type;`.
-namespace NodeData {
-struct AddressOf {
-  NodeId lvalue_id;
-};
-
-struct ArrayIndex {
-  NodeId array_id;
-  NodeId index_id;
-};
-
-// Initializes an array from a tuple. `tuple_id` is the source tuple
-// expression. `inits_and_return_slot_id` contains one initializer per array
-// element, plus a final element that is the return slot for the
-// initialization.
-struct ArrayInit {
-  NodeId tuple_id;
-  NodeBlockId inits_and_return_slot_id;
-};
-
-struct ArrayType {
-  NodeId bound_id;
-  TypeId element_type_id;
-};
-
-// Performs a source-level initialization or assignment of `lhs_id` from
-// `rhs_id`. This finishes initialization of `lhs_id` in the same way as
-// `InitializeFrom`.
-struct Assign {
-  using HasType = std::false_type;
-
-  NodeId lhs_id;
-  NodeId rhs_id;
-};
-
-struct BinaryOperatorAdd {
-  NodeId lhs_id;
-  NodeId rhs_id;
-};
-
-struct BindName {
-  StringId name_id;
-  NodeId value_id;
-};
-
-struct BindValue {
-  NodeId value_id;
-};
-
-struct BlockArg {
-  NodeBlockId block_id;
-};
-
-struct BoolLiteral {
-  BoolValue value;
-};
-
-struct Branch {
-  using HasType = std::false_type;
-
-  NodeBlockId target_id;
-};
-
-struct BranchIf {
-  using HasType = std::false_type;
-
-  NodeBlockId target_id;
-  NodeId cond_id;
-};
-
-struct BranchWithArg {
-  using HasType = std::false_type;
-
-  NodeBlockId target_id;
-  NodeId arg_id;
-};
-
-struct Builtin {
-  // Builtins don't have a parse node associated with them.
-  using HasParseNode = std::false_type;
-
-  BuiltinKind builtin_kind;
-};
-
-struct Call {
-  NodeId callee_id;
-  NodeBlockId args_id;
-};
-
-struct ClassDeclaration {
-  ClassId class_id;
-  // The declaration block, containing the class name's qualifiers and the
-  // class's generic parameters.
-  NodeBlockId decl_block_id;
-};
-
-struct ConstType {
-  TypeId inner_id;
-};
-
-struct CrossReference {
-  // A node's parse tree node must refer to a node in the current parse tree.
-  // This cannot use the cross-referenced node's parse tree node because it
-  // will be in a different parse tree.
-  using HasParseNode = std::false_type;
-
-  CrossReferenceIRId ir_id;
-  NodeId node_id;
-};
-
-struct Dereference {
-  NodeId pointer_id;
-};
-
-struct FunctionDeclaration {
-  FunctionId function_id;
-};
-
-// Finalizes the initialization of `dest_id` from the initializer expression
-// `src_id`, by performing a final copy from source to destination, for types
-// whose initialization is not in-place.
-struct InitializeFrom {
-  NodeId src_id;
-  NodeId dest_id;
-};
-
-struct IntegerLiteral {
-  IntegerId integer_id;
-};
-
-struct NameReference {
-  StringId name_id;
-  NodeId value_id;
-};
-
-struct Namespace {
-  NameScopeId name_scope_id;
-};
-
-struct NoOp {
-  using HasType = std::false_type;
-};
-
-struct Parameter {
-  StringId name_id;
-};
-
-struct PointerType {
-  TypeId pointee_id;
-};
-
-struct RealLiteral {
-  RealId real_id;
-};
-
-struct Return {
-  using HasType = std::false_type;
-};
-
-struct ReturnExpression {
-  using HasType = std::false_type;
-
-  NodeId expr_id;
-};
-
-struct SpliceBlock {
-  NodeBlockId block_id;
-  NodeId result_id;
-};
-
-struct StringLiteral {
-  StringId string_id;
-};
-
-struct StructAccess {
-  NodeId struct_id;
-  MemberIndex index;
-};
-
-struct StructInit {
-  NodeId src_id;
-  NodeBlockId elements_id;
-};
-
-struct StructLiteral {
-  NodeBlockId elements_id;
-};
-
-struct StructType {
-  NodeBlockId fields_id;
-};
-
-struct StructTypeField {
-  using HasType = std::false_type;
-
-  StringId name_id;
-  TypeId type_id;
-};
-
-struct StructValue {
-  NodeId src_id;
-  NodeBlockId elements_id;
-};
-
-struct Temporary {
-  NodeId storage_id;
-  NodeId init_id;
-};
-
-struct TemporaryStorage {};
-
-struct TupleAccess {
-  NodeId tuple_id;
-  MemberIndex index;
-};
-
-struct TupleIndex {
-  NodeId tuple_id;
-  NodeId index_id;
-};
-
-struct TupleInit {
-  NodeId src_id;
-  NodeBlockId elements_id;
-};
-
-struct TupleLiteral {
-  NodeBlockId elements_id;
-};
-
-struct TupleType {
-  TypeBlockId elements_id;
-};
-
-struct TupleValue {
-  NodeId src_id;
-  NodeBlockId elements_id;
-};
-
-struct UnaryOperatorNot {
-  NodeId operand_id;
-};
-
-struct ValueAsReference {
-  NodeId value_id;
-};
-
-struct VarStorage {
-  StringId name_id;
-};
-}  // namespace NodeData
-
-template <NodeKind::RawEnumType KindT, typename DataT>
-struct TypedNode;
-
-// The standard structure for Node. This is trying to provide a minimal
-// amount of information for a node:
+// A type-erased representation of a SemIR node, that may be constructed from
+// the specific kinds of node defined in `typed_nodes.h`. This provides access
+// to common fields present on most or all kinds of nodes:
 //
 // - `parse_node` for error placement.
 // - `kind` for run-time logic when the input Kind is unknown.
 // - `type_id` for quick type checking.
-// - Up to two Kind-specific members.
 //
-// To create a specific kind of `Node`, use the appropriate `TypedNode`
-// constructor. A `TypedNode` implicitly converts to a `Node`.
+// In addition, kind-specific data can be accessed by casting to the specific
+// kind of node:
 //
-// Given a `Node`, you may:
-//
-// - Access non-Kind-specific members like `Print`.
-// - Use `node.kind()` or `Is<Kind>` to determine what kind of node it is.
-// - Access Kind-specific members using `node.As<Kind>()`, which produces a
-//   `TypedNode` with type-specific members, including `parse_node` and
-//   `type_id` for nodes that have associated parse nodes and types.
-//   - Using the wrong kind in `node.As<Kind>()` is a programming error, and
-//     will CHECK-fail in debug modes (opt may too, but it's not an API
+// - Use `node.kind()` or `Is<TypedNode>` to determine what kind of node it is.
+// - Cast to a specific type using `node.As<TypedNode>()`
+//   - Using the wrong kind in `node.As<TypedNode>()` is a programming error,
+//     and will CHECK-fail in debug modes (opt may too, but it's not an API
 //     guarantee).
-// - Use `node.TryAs<Kind>()` to safely access type-specific node data where
-//   the node's kind is not known.
+// - Use `node.TryAs<TypedNode>()` to safely access type-specific node data
+//   where the node's kind is not known.
 class Node : public Printable<Node> {
  public:
-  template <NodeKind::RawEnumType Kind, typename Data>
-  /*implicit*/
-  Node(TypedNode<Kind, Data> typed_node)
-      : Node(typed_node.parse_node_or_invalid(), NodeKind::Create(Kind),
-             typed_node.type_id_or_invalid(), typed_node.arg0_or_invalid(),
-             typed_node.arg1_or_invalid()) {}
+  template <typename TypedNode, typename Info = TypedNodeArgsInfo<TypedNode>>
+  // NOLINTNEXTLINE(google-explicit-constructor)
+  Node(TypedNode typed_node)
+      : parse_node_(Parse::Node::Invalid),
+        kind_(TypedNode::Kind),
+        type_id_(TypeId::Invalid),
+        arg0_(NodeId::InvalidIndex),
+        arg1_(NodeId::InvalidIndex) {
+    if constexpr (HasParseNode<TypedNode>) {
+      parse_node_ = typed_node.parse_node;
+    }
+    if constexpr (HasTypeId<TypedNode>) {
+      type_id_ = typed_node.type_id;
+    }
+    if constexpr (Info::NumArgs > 0) {
+      arg0_ = ToRaw(Info::template Get<0>(typed_node));
+    }
+    if constexpr (Info::NumArgs > 1) {
+      arg1_ = ToRaw(Info::template Get<1>(typed_node));
+    }
+  }
 
   // Returns whether this node has the specified type.
-  template <typename Typed>
+  template <typename TypedNode>
   auto Is() const -> bool {
-    return kind() == Typed::Kind;
+    return kind() == TypedNode::Kind;
   }
 
   // Casts this node to the given typed node, which must match the node's kind,
   // and returns the typed node.
-  template <typename Typed>
-  auto As() const -> Typed {
-    CARBON_CHECK(Is<Typed>()) << "Casting node of kind " << kind()
-                              << " to wrong kind " << Typed::Kind;
-    return Typed::FromRawData(parse_node_, type_id_, arg0_, arg1_);
+  template <typename TypedNode, typename Info = TypedNodeArgsInfo<TypedNode>>
+  auto As() const -> TypedNode {
+    CARBON_CHECK(Is<TypedNode>()) << "Casting node of kind " << kind()
+                                  << " to wrong kind " << TypedNode::Kind;
+    auto build_with_type_id_and_args = [&](auto... type_id_and_args) {
+      if constexpr (HasParseNode<TypedNode>) {
+        return TypedNode{parse_node(), type_id_and_args...};
+      } else {
+        return TypedNode{type_id_and_args...};
+      }
+    };
+
+    auto build_with_args = [&](auto... args) {
+      if constexpr (HasTypeId<TypedNode>) {
+        return build_with_type_id_and_args(type_id(), args...);
+      } else {
+        return build_with_type_id_and_args(args...);
+      }
+    };
+
+    if constexpr (Info::NumArgs == 0) {
+      return build_with_args();
+    } else if constexpr (Info::NumArgs == 1) {
+      return build_with_args(
+          FromRaw<typename Info::template ArgType<0>>(arg0_));
+    } else if constexpr (Info::NumArgs == 2) {
+      return build_with_args(
+          FromRaw<typename Info::template ArgType<0>>(arg0_),
+          FromRaw<typename Info::template ArgType<1>>(arg1_));
+    }
   }
 
   // If this node is the given kind, returns a typed node, otherwise returns
   // nullopt.
-  template <typename Typed>
-  auto TryAs() const -> std::optional<Typed> {
-    if (Is<Typed>()) {
-      return As<Typed>();
+  template <typename TypedNode>
+  auto TryAs() const -> std::optional<TypedNode> {
+    if (Is<TypedNode>()) {
+      return As<TypedNode>();
     } else {
       return std::nullopt;
     }
@@ -537,15 +144,33 @@ class Node : public Printable<Node> {
   auto Print(llvm::raw_ostream& out) const -> void;
 
  private:
-  explicit Node(Parse::Node parse_node, NodeKind kind, TypeId type_id,
-                int32_t arg0 = NodeId::InvalidIndex,
-                int32_t arg1 = NodeId::InvalidIndex)
+  friend class NodeTestHelper;
+
+  // Raw constructor, used for testing.
+  explicit Node(NodeKind kind, Parse::Node parse_node, TypeId type_id,
+                int32_t arg0, int32_t arg1)
       : parse_node_(parse_node),
         kind_(kind),
         type_id_(type_id),
         arg0_(arg0),
         arg1_(arg1) {}
 
+  // Convert a field to its raw representation, used as `arg0_` / `arg1_`.
+  static constexpr auto ToRaw(IndexBase base) -> int32_t { return base.index; }
+  static constexpr auto ToRaw(BuiltinKind kind) -> int32_t {
+    return kind.AsInt();
+  }
+
+  // Convert a field from its raw representation.
+  template <typename T>
+  static constexpr auto FromRaw(int32_t raw) -> T {
+    return T(raw);
+  }
+  template <>
+  constexpr auto FromRaw<BuiltinKind>(int32_t raw) -> BuiltinKind {
+    return BuiltinKind::FromInt(raw);
+  }
+
   Parse::Node parse_node_;
   NodeKind kind_;
   TypeId type_id_;
@@ -561,245 +186,13 @@ class Node : public Printable<Node> {
 // may be worth investigating further.
 static_assert(sizeof(Node) == 20, "Unexpected Node size");
 
-namespace NodeInternals {
-template <typename DataT>
-struct TypedNodeImpl;
-}
-
-// Representation of a specific kind of node. This has the following public
-// data members:
-//
-// - A `parse_node` member for nodes with an associated parse node.
-// - A `type_id` member for nodes with an associated type.
-// - Each member from the `NodeData` struct, above.
-//
-// A `TypedNode` can be constructed by passing its fields in order:
-//
-// - First, the `parse_node`, for nodes with a location,
-// - Then, the `type_id`, for nodes with a type,
-// - Then, each field of the `NodeData` struct above.
-template <NodeKind::RawEnumType KindT, typename DataT>
-struct TypedNode : NodeInternals::TypedNodeImpl<DataT>,
-                   Printable<TypedNode<KindT, DataT>> {
-  static constexpr NodeKind Kind = NodeKind::Create(KindT);
-  using Data = DataT;
-
-  // Members from base classes, repeated here to make the API of this class
-  // easier to understand.
-#if 0
-  // From HasParseNodeBase, unless `DataT::HasParseNode` is `false_type`.
-  Parse::Node parse_node;
-
-  // From HasTypeBase, unless `DataT::HasType` is `false_type`.
-  TypeId type_id;
-
-  // Up to two operand types and names, from `DataT`.
-  IdType1 id_1;
-  IdType2 id_2;
-
-  // Construct the node from its elements. For any omitted fields, the
-  // parameter is removed here. Constructor is inherited from TypedNodeBase.
-  TypedNode(Parse::Node parse_node, TypeId type_id, IdType1 id_1, IdType2 id_2);
-
-  // Returns the operands of the node.
-  auto args() const -> DataT;
-
-  // Returns the operands of the node as a tuple of up to two operands.
-  auto args_tuple() const -> std::tuple<IdType1, IdType2>;
-#endif
-
-  using NodeInternals::TypedNodeImpl<DataT>::TypedNodeImpl;
-
-  static auto FromRawData(Parse::Node parse_node, TypeId type_id, int32_t arg0,
-                          int32_t arg1) -> TypedNode {
-    return TypedNode(TypedNode::FromParseNode(parse_node),
-                     TypedNode::FromTypeId(type_id),
-                     TypedNode::FromRawArgs(arg0, arg1));
-  }
-
-  auto Print(llvm::raw_ostream& out) const -> void { Node(*this).Print(out); }
-};
-
-// Declare type names for each specific kind of node.
-#define CARBON_SEM_IR_NODE_KIND(Name) \
-  using Name = TypedNode<NodeKind::Name, NodeData::Name>;
-#include "toolchain/sem_ir/node_kind.def"
-
-// Implementation details for typed nodes.
-namespace NodeInternals {
-template <typename T>
-using GetHasParseNode = typename T::HasParseNode;
-template <typename T>
-using GetHasType = typename T::HasType;
-
-// Apply Getter<T>, or provide Default if it doesn't exist.
-template <typename T, template <typename> typename Getter, typename Default,
-          typename Void = void>
-struct GetWithDefaultImpl {
-  using Result = Default;
-};
-template <typename T, template <typename> typename Getter, typename Default>
-struct GetWithDefaultImpl<T, Getter, Default, std::void_t<Getter<T>>> {
-  using Result = Getter<T>;
-};
-template <typename T, template <typename> typename Getter, typename Default>
-using GetWithDefault = typename GetWithDefaultImpl<T, Getter, Default>::Result;
-
-// Base class for nodes that have a `parse_node` field.
-struct HasParseNodeBase {
-  Parse::Node parse_node;
-
-  static auto FromParseNode(Parse::Node parse_node) -> HasParseNodeBase {
-    return {.parse_node = parse_node};
-  }
-
-  auto parse_node_or_invalid() const -> Parse::Node { return parse_node; }
-};
-
-// Base class for nodes that have no `parse_node` field.
-struct HasNoParseNodeBase {
-  static auto FromParseNode(Parse::Node /*parse_node*/) -> HasNoParseNodeBase {
-    return {};
-  }
-
-  auto parse_node_or_invalid() const -> Parse::Node {
-    return Parse::Node::Invalid;
-  }
-};
-
-// ParseNodeBase<T> holds the `parse_node` field if the node has a parse tree
-// node, and is either HasParseNodeBase or HasNoParseNodeBase.
-template <typename T>
-using ParseNodeBase =
-    std::conditional_t<GetWithDefault<T, GetHasParseNode, std::true_type>{},
-                       HasParseNodeBase, HasNoParseNodeBase>;
-
-// Base class for nodes that have a `type_id` field.
-struct HasTypeBase {
-  TypeId type_id;
-
-  static auto FromTypeId(TypeId type_id) -> HasTypeBase {
-    return {.type_id = type_id};
-  }
-
-  auto type_id_or_invalid() const -> TypeId { return type_id; }
-};
-
-// Base class for nodes that have no `type_id` field.
-struct HasNoTypeBase {
-  static auto FromTypeId(TypeId /*type_id*/) -> HasNoTypeBase { return {}; }
-
-  auto type_id_or_invalid() const -> TypeId { return TypeId::Invalid; }
-};
-
-// TypeBase<T> holds the `type_id` field if the node has a type, and is either
-// HasTypeBase or HasNoTypeBase.
-template <typename T>
-using TypeBase =
-    std::conditional_t<GetWithDefault<T, GetHasType, std::true_type>{},
-                       HasTypeBase, HasNoTypeBase>;
-
-// Convert a field from its raw representation.
-template <typename T>
-constexpr auto FromRaw(int32_t raw) -> T {
-  return T(raw);
+// Typed nodes can be printed by converting them to nodes.
+template <typename TypedNode, typename = TypedNodeArgsInfo<TypedNode>>
+inline llvm::raw_ostream& operator<<(llvm::raw_ostream& out, TypedNode node) {
+  Node(node).Print(out);
+  return out;
 }
-template <>
-constexpr auto FromRaw<BuiltinKind>(int32_t raw) -> BuiltinKind {
-  return BuiltinKind::FromInt(raw);
-}
-
-// Convert a field to its raw representation.
-constexpr auto ToRaw(IndexBase base) -> int32_t { return base.index; }
-constexpr auto ToRaw(BuiltinKind kind) -> int32_t { return kind.AsInt(); }
-
-template <typename T>
-using FieldTypes = decltype(StructReflection::AsTuple(std::declval<T>()));
-
-// Base class for nodes that contains the node data.
-template <typename T, typename = FieldTypes<T>>
-struct DataBase;
-
-template <typename T, typename... Fields>
-struct DataBase<T, std::tuple<Fields...>> : T {
-  static_assert(sizeof...(Fields) <= 2, "Too many fields in node data");
-
-  static auto FromRawArgs(decltype(ToRaw(std::declval<Fields>()))... args, ...)
-      -> DataBase {
-    return {FromRaw<Fields>(args)...};
-  }
-
-  // Returns the operands of the node.
-  auto args() const -> T { return *this; }
-
-  // Returns the operands of the node as a tuple.
-  auto args_tuple() const -> auto {
-    return StructReflection::AsTuple(static_cast<const T&>(*this));
-  }
-
-  auto arg0_or_invalid() const -> auto {
-    if constexpr (sizeof...(Fields) >= 1) {
-      return ToRaw(std::get<0>(args_tuple()));
-    } else {
-      return NodeId::InvalidIndex;
-    }
-  }
-
-  auto arg1_or_invalid() const -> auto {
-    if constexpr (sizeof...(Fields) >= 2) {
-      return ToRaw(std::get<1>(args_tuple()));
-    } else {
-      return NodeId::InvalidIndex;
-    }
-  }
-};
-
-template <typename, typename, typename, typename>
-struct TypedNodeBase;
-
-// A helper base class that produces a constructor with one correctly-typed
-// parameter for each struct field.
-template <typename DataT, typename... ParseNodeFields, typename... TypeFields,
-          typename... DataFields>
-struct TypedNodeBase<DataT, std::tuple<ParseNodeFields...>,
-                     std::tuple<TypeFields...>, std::tuple<DataFields...>>
-    : ParseNodeBase<DataT>, TypeBase<DataT>, DataBase<DataT> {
-  // Braced initialization of base classes confuses clang-format.
-  // clang-format off
-  constexpr TypedNodeBase(ParseNodeFields... parse_node_fields,
-                          TypeFields... type_fields, DataFields... data_fields)
-      : ParseNodeBase<DataT>{parse_node_fields...},
-        TypeBase<DataT>{type_fields...},
-        DataBase<DataT>{data_fields...} {
-  }
-  // clang-format on
-
-  constexpr TypedNodeBase(ParseNodeBase<DataT> parse_node_base,
-                          TypeBase<DataT> type_base, DataBase<DataT> data_base)
-      : ParseNodeBase<DataT>(parse_node_base),
-        TypeBase<DataT>(type_base),
-        DataBase<DataT>(data_base) {}
-};
-
-template <typename DataT>
-using MakeTypedNodeBase =
-    TypedNodeBase<DataT, FieldTypes<ParseNodeBase<DataT>>,
-                  FieldTypes<TypeBase<DataT>>, FieldTypes<DataT>>;
-
-template <typename DataT>
-struct TypedNodeImpl : MakeTypedNodeBase<DataT> {
-  using MakeTypedNodeBase<DataT>::MakeTypedNodeBase;
-};
-}  // namespace NodeInternals
 
 }  // namespace Carbon::SemIR
 
-// Support use of Id types as DenseMap/DenseSet keys.
-template <>
-struct llvm::DenseMapInfo<Carbon::SemIR::NodeBlockId>
-    : public Carbon::IndexMapInfo<Carbon::SemIR::NodeBlockId> {};
-template <>
-struct llvm::DenseMapInfo<Carbon::SemIR::NodeId>
-    : public Carbon::IndexMapInfo<Carbon::SemIR::NodeId> {};
-
 #endif  // CARBON_TOOLCHAIN_SEM_IR_NODE_H_

+ 13 - 12
toolchain/sem_ir/node_kind.cpp

@@ -4,6 +4,8 @@
 
 #include "toolchain/sem_ir/node_kind.h"
 
+#include "toolchain/sem_ir/typed_nodes.h"
+
 namespace Carbon::SemIR {
 
 CARBON_DEFINE_ENUM_CLASS_NAMES(NodeKind) = {
@@ -11,30 +13,29 @@ CARBON_DEFINE_ENUM_CLASS_NAMES(NodeKind) = {
 #include "toolchain/sem_ir/node_kind.def"
 };
 
-// Returns the name to use for this node kind in Semantics IR.
-[[nodiscard]] auto NodeKind::ir_name() const -> llvm::StringRef {
-  static constexpr llvm::StringRef Table[] = {
-#define CARBON_SEM_IR_NODE_KIND_WITH_IR_NAME(Name, IR_Name) IR_Name,
-#include "toolchain/sem_ir/node_kind.def"
-  };
-  return Table[AsInt()];
+auto NodeKind::ir_name() const -> llvm::StringLiteral {
+  return definition().ir_name();
 }
 
 auto NodeKind::value_kind() const -> NodeValueKind {
   static constexpr NodeValueKind Table[] = {
-#define CARBON_SEM_IR_NODE_KIND_WITH_VALUE_KIND(Name, Kind) NodeValueKind::Kind,
+#define CARBON_SEM_IR_NODE_KIND(Name) \
+  HasTypeId<SemIR::Name> ? NodeValueKind::Typed : NodeValueKind::None,
 #include "toolchain/sem_ir/node_kind.def"
   };
   return Table[AsInt()];
 }
 
 auto NodeKind::terminator_kind() const -> TerminatorKind {
-  static constexpr TerminatorKind Table[] = {
-#define CARBON_SEM_IR_NODE_KIND_WITH_TERMINATOR_KIND(Name, Kind) \
-  TerminatorKind::Kind,
+  return definition().terminator_kind();
+}
+
+auto NodeKind::definition() const -> const Definition& {
+  static constexpr const Definition* Table[] = {
+#define CARBON_SEM_IR_NODE_KIND(Name) &SemIR::Name::Kind,
 #include "toolchain/sem_ir/node_kind.def"
   };
-  return Table[AsInt()];
+  return *Table[AsInt()];
 }
 
 }  // namespace Carbon::SemIR

+ 53 - 90
toolchain/sem_ir/node_kind.def

@@ -7,101 +7,64 @@
 // inclusion to expand to the desired output. Macro definitions are cleaned up
 // at the end of this file.
 //
-// Exactly one of these macros should be defined before including this header:
+// This macro should be defined before including this header:
 // - CARBON_SEM_IR_NODE_KIND(Name)
 //   Invoked for each kind of semantic node.
-// - CARBON_SEM_IR_NODE_KIND_WITH_VALUE_KIND(Name, TypeFieldKind)
-//   Invoked for each kind of semantic node, along with information about
-//   whether the node produces a value, and if so, what kind of value.
-// - CARBON_SEM_IR_NODE_KIND_WITH_TERMINATOR_KIND(Name, TerminatorKind)
-//   Invoked for each kind of semantic node, along with information about
-//   whether the node is a terminator node.
-// - CARBON_SEM_IR_NODE_KIND_WITH_IR_NAME(Name, IRName)
-//   Invoked for each kind of semantic node, along with the name that is used
-//   to denote this node in textual Semantics IR.
 
-#if defined(CARBON_SEM_IR_NODE_KIND)
-#define CARBON_SEM_IR_NODE_KIND_IMPL(Name, IRName, ValueKind, TerminatorKind) \
-  CARBON_SEM_IR_NODE_KIND(Name)
-#elif defined(CARBON_SEM_IR_NODE_KIND_WITH_VALUE_KIND)
-#define CARBON_SEM_IR_NODE_KIND_IMPL(Name, IRName, ValueKind, TerminatorKind) \
-  CARBON_SEM_IR_NODE_KIND_WITH_VALUE_KIND(Name, ValueKind)
-#elif defined(CARBON_SEM_IR_NODE_KIND_WITH_TERMINATOR_KIND)
-#define CARBON_SEM_IR_NODE_KIND_IMPL(Name, IRName, ValueKind, TerminatorKind) \
-  CARBON_SEM_IR_NODE_KIND_WITH_TERMINATOR_KIND(Name, TerminatorKind)
-#elif defined(CARBON_SEM_IR_NODE_KIND_WITH_IR_NAME)
-#define CARBON_SEM_IR_NODE_KIND_IMPL(Name, IRName, ValueKind, TerminatorKind) \
-  CARBON_SEM_IR_NODE_KIND_WITH_IR_NAME(Name, IRName)
-#else
+#ifndef CARBON_SEM_IR_NODE_KIND
 #error "Must define the x-macro to use this file."
 #endif
 
-// A cross-reference between IRs.
-CARBON_SEM_IR_NODE_KIND_IMPL(CrossReference, "xref", Typed, NotTerminator)
-
-CARBON_SEM_IR_NODE_KIND_IMPL(AddressOf, "address_of", Typed, NotTerminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(ArrayIndex, "array_index", Typed, NotTerminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(ArrayInit, "array_init", Typed, NotTerminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(ArrayType, "array_type", Typed, NotTerminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(Assign, "assign", None, NotTerminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(BinaryOperatorAdd, "add", Typed, NotTerminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(BindName, "bind_name", Typed, NotTerminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(BindValue, "bind_value", Typed, NotTerminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(BlockArg, "block_arg", Typed, NotTerminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(BoolLiteral, "bool_literal", Typed, NotTerminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(Branch, "br", None, Terminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(BranchIf, "br", None, TerminatorSequence)
-CARBON_SEM_IR_NODE_KIND_IMPL(BranchWithArg, "br", None, Terminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(Builtin, "builtin", Typed, NotTerminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(Call, "call", Typed, NotTerminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(ClassDeclaration, "class_declaration", Typed,
-                             NotTerminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(ConstType, "const_type", Typed, NotTerminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(Dereference, "dereference", Typed, NotTerminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(FunctionDeclaration, "fn_decl", Typed,
-                             NotTerminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(InitializeFrom, "initialize_from", Typed,
-                             NotTerminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(IntegerLiteral, "int_literal", Typed,
-                             NotTerminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(NameReference, "name_reference", Typed,
-                             NotTerminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(Namespace, "namespace", Typed, NotTerminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(NoOp, "no_op", None, NotTerminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(Parameter, "parameter", Typed, NotTerminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(PointerType, "ptr_type", Typed, NotTerminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(RealLiteral, "real_literal", Typed, NotTerminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(ReturnExpression, "return", None, Terminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(Return, "return", None, Terminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(SpliceBlock, "splice_block", Typed, NotTerminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(StringLiteral, "string_literal", Typed,
-                             NotTerminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(StructAccess, "struct_access", Typed,
-                             NotTerminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(StructInit, "struct_init", Typed, NotTerminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(StructLiteral, "struct_literal", Typed,
-                             NotTerminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(StructTypeField, "struct_type_field", None,
-                             NotTerminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(StructType, "struct_type", Typed, NotTerminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(StructValue, "struct_value", Typed, NotTerminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(TemporaryStorage, "temporary_storage", Typed,
-                             NotTerminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(Temporary, "temporary", Typed, NotTerminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(TupleAccess, "tuple_access", Typed, NotTerminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(TupleIndex, "tuple_index", Typed, NotTerminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(TupleInit, "tuple_init", Typed, NotTerminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(TupleLiteral, "tuple_literal", Typed,
-                             NotTerminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(TupleType, "tuple_type", Typed, NotTerminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(TupleValue, "tuple_value", Typed, NotTerminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(UnaryOperatorNot, "not", Typed, NotTerminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(ValueAsReference, "value_as_reference", Typed,
-                             NotTerminator)
-CARBON_SEM_IR_NODE_KIND_IMPL(VarStorage, "var", Typed, NotTerminator)
+// For each node kind declared here there is a matching definition in
+// `typed.nodes.h`.
+CARBON_SEM_IR_NODE_KIND(AddressOf)
+CARBON_SEM_IR_NODE_KIND(ArrayIndex)
+CARBON_SEM_IR_NODE_KIND(ArrayInit)
+CARBON_SEM_IR_NODE_KIND(ArrayType)
+CARBON_SEM_IR_NODE_KIND(Assign)
+CARBON_SEM_IR_NODE_KIND(BinaryOperatorAdd)
+CARBON_SEM_IR_NODE_KIND(BindName)
+CARBON_SEM_IR_NODE_KIND(BindValue)
+CARBON_SEM_IR_NODE_KIND(BlockArg)
+CARBON_SEM_IR_NODE_KIND(BoolLiteral)
+CARBON_SEM_IR_NODE_KIND(Branch)
+CARBON_SEM_IR_NODE_KIND(BranchIf)
+CARBON_SEM_IR_NODE_KIND(BranchWithArg)
+CARBON_SEM_IR_NODE_KIND(Builtin)
+CARBON_SEM_IR_NODE_KIND(Call)
+CARBON_SEM_IR_NODE_KIND(ClassDeclaration)
+CARBON_SEM_IR_NODE_KIND(ConstType)
+CARBON_SEM_IR_NODE_KIND(CrossReference)
+CARBON_SEM_IR_NODE_KIND(Dereference)
+CARBON_SEM_IR_NODE_KIND(FunctionDeclaration)
+CARBON_SEM_IR_NODE_KIND(InitializeFrom)
+CARBON_SEM_IR_NODE_KIND(IntegerLiteral)
+CARBON_SEM_IR_NODE_KIND(NameReference)
+CARBON_SEM_IR_NODE_KIND(Namespace)
+CARBON_SEM_IR_NODE_KIND(NoOp)
+CARBON_SEM_IR_NODE_KIND(Parameter)
+CARBON_SEM_IR_NODE_KIND(PointerType)
+CARBON_SEM_IR_NODE_KIND(RealLiteral)
+CARBON_SEM_IR_NODE_KIND(ReturnExpression)
+CARBON_SEM_IR_NODE_KIND(Return)
+CARBON_SEM_IR_NODE_KIND(SpliceBlock)
+CARBON_SEM_IR_NODE_KIND(StringLiteral)
+CARBON_SEM_IR_NODE_KIND(StructAccess)
+CARBON_SEM_IR_NODE_KIND(StructInit)
+CARBON_SEM_IR_NODE_KIND(StructLiteral)
+CARBON_SEM_IR_NODE_KIND(StructTypeField)
+CARBON_SEM_IR_NODE_KIND(StructType)
+CARBON_SEM_IR_NODE_KIND(StructValue)
+CARBON_SEM_IR_NODE_KIND(TemporaryStorage)
+CARBON_SEM_IR_NODE_KIND(Temporary)
+CARBON_SEM_IR_NODE_KIND(TupleAccess)
+CARBON_SEM_IR_NODE_KIND(TupleIndex)
+CARBON_SEM_IR_NODE_KIND(TupleInit)
+CARBON_SEM_IR_NODE_KIND(TupleLiteral)
+CARBON_SEM_IR_NODE_KIND(TupleType)
+CARBON_SEM_IR_NODE_KIND(TupleValue)
+CARBON_SEM_IR_NODE_KIND(UnaryOperatorNot)
+CARBON_SEM_IR_NODE_KIND(ValueAsReference)
+CARBON_SEM_IR_NODE_KIND(VarStorage)
 
 #undef CARBON_SEM_IR_NODE_KIND
-#undef CARBON_SEM_IR_NODE_KIND_WITH_VALUE_KIND
-#undef CARBON_SEM_IR_NODE_KIND_WITH_TERMINATOR_KIND
-#undef CARBON_SEM_IR_NODE_KIND_WITH_IR_NAME
-#undef CARBON_SEM_IR_NODE_KIND_IMPL

+ 52 - 1
toolchain/sem_ir/node_kind.h

@@ -48,7 +48,7 @@ class NodeKind : public CARBON_ENUM_BASE(NodeKind) {
   using EnumBase::Create;
 
   // Returns the name to use for this node kind in Semantics IR.
-  [[nodiscard]] auto ir_name() const -> llvm::StringRef;
+  [[nodiscard]] auto ir_name() const -> llvm::StringLiteral;
 
   // Returns whether this kind of node is expected to produce a value.
   [[nodiscard]] auto value_kind() const -> NodeValueKind;
@@ -63,6 +63,18 @@ class NodeKind : public CARBON_ENUM_BASE(NodeKind) {
   // Compute a fingerprint for this node kind, allowing its use as part of the
   // key in a `FoldingSet`.
   void Profile(llvm::FoldingSetNodeID& id) { id.AddInteger(AsInt()); }
+
+  class Definition;
+
+  // Provides a definition for this node kind. Should only be called once, to
+  // construct the kind as part of defining it in `typed_nodes.h`.
+  constexpr auto Define(llvm::StringLiteral ir_name,
+                        TerminatorKind terminator_kind =
+                            TerminatorKind::NotTerminator) const -> Definition;
+
+ private:
+  // Looks up the definition for this node kind.
+  [[nodiscard]] auto definition() const -> const Definition&;
 };
 
 #define CARBON_SEM_IR_NODE_KIND(Name) \
@@ -72,6 +84,45 @@ class NodeKind : public CARBON_ENUM_BASE(NodeKind) {
 // We expect the node kind to fit compactly into 8 bits.
 static_assert(sizeof(NodeKind) == 1, "Kind objects include padding!");
 
+// A definition of a 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 node
+// kind, specifically `TypedNode::Kind`. Use `NodeKind` instead as a thin
+// wrapper around a node kind index.
+class NodeKind::Definition : public NodeKind {
+ public:
+  // Returns the name to use for this node kind in Semantics IR.
+  [[nodiscard]] constexpr auto ir_name() const -> llvm::StringLiteral {
+    return ir_name_;
+  }
+
+  // Returns whether this node kind is a code block terminator. See
+  // NodeKind::terminator_kind().
+  [[nodiscard]] constexpr auto terminator_kind() const -> TerminatorKind {
+    return terminator_kind_;
+  }
+
+ private:
+  friend class NodeKind;
+
+  constexpr Definition(NodeKind kind, llvm::StringLiteral ir_name,
+                       TerminatorKind terminator_kind)
+      : NodeKind(kind), ir_name_(ir_name), terminator_kind_(terminator_kind) {}
+
+  // Not copyable.
+  Definition(const Definition&) = delete;
+  Definition& operator=(const Definition&) = delete;
+
+  llvm::StringLiteral ir_name_;
+  TerminatorKind terminator_kind_;
+};
+
+constexpr auto NodeKind::Define(llvm::StringLiteral ir_name,
+                                TerminatorKind terminator_kind) const
+    -> Definition {
+  return Definition(*this, ir_name, terminator_kind);
+}
+
 }  // namespace Carbon::SemIR
 
 #endif  // CARBON_TOOLCHAIN_SEM_IR_NODE_KIND_H_

+ 491 - 0
toolchain/sem_ir/typed_nodes.h

@@ -0,0 +1,491 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#ifndef CARBON_TOOLCHAIN_SEM_IR_TYPED_NODES_H_
+#define CARBON_TOOLCHAIN_SEM_IR_TYPED_NODES_H_
+
+#include "toolchain/parse/tree.h"
+#include "toolchain/sem_ir/builtin_kind.h"
+#include "toolchain/sem_ir/ids.h"
+#include "toolchain/sem_ir/node_kind.h"
+
+// Representations for specific kinds of nodes.
+//
+// Each type should be a struct with up to four members:
+//
+// - Optionally, a `Parse::Node parse_node;` member, for nodes with an
+//   associated location. Almost all nodes should have this, with exceptions
+//   being things that are generated internally, without any relation to source
+//   syntax, such as predeclared builtins.
+// - Optionally, a `TypeId type_id;` member, for nodes that produce a value.
+//   This includes nodes that produce an abstract value, such as a `Namespace`,
+//   for which a placeholder type should be used.
+// - Up to two `[...]Id` members describing the contents of the struct.
+//
+// The field names here matter -- the first two fields must have the names
+// specified above, when present. When converting to a `SemIR::Node`, they will
+// become the parse node and type associated with the type-erased node.
+//
+// In addition, each type provides a constant `Kind` that associates the type
+// with a particular member of the `NodeKind` enumeration. This `Kind`
+// declaration also defines the node kind by calling `NodeKind::Define` and
+// specifying additional information about the node kind. This information is
+// available through the member functions of the `NodeKind` value declared in
+// `node_kind.h`, and includes the name used in textual IR and whether the node
+// is a terminator instruction.
+namespace Carbon::SemIR {
+
+struct AddressOf {
+  static constexpr auto Kind = NodeKind::AddressOf.Define("address_of");
+
+  Parse::Node parse_node;
+  TypeId type_id;
+  NodeId lvalue_id;
+};
+
+struct ArrayIndex {
+  static constexpr auto Kind = NodeKind::ArrayIndex.Define("array_index");
+
+  Parse::Node parse_node;
+  TypeId type_id;
+  NodeId array_id;
+  NodeId index_id;
+};
+
+// Initializes an array from a tuple. `tuple_id` is the source tuple
+// expression. `inits_and_return_slot_id` contains one initializer per array
+// element, plus a final element that is the return slot for the
+// initialization.
+struct ArrayInit {
+  static constexpr auto Kind = NodeKind::ArrayInit.Define("array_init");
+
+  Parse::Node parse_node;
+  TypeId type_id;
+  NodeId tuple_id;
+  NodeBlockId inits_and_return_slot_id;
+};
+
+struct ArrayType {
+  static constexpr auto Kind = NodeKind::ArrayType.Define("array_type");
+
+  Parse::Node parse_node;
+  TypeId type_id;
+  NodeId bound_id;
+  TypeId element_type_id;
+};
+
+// Performs a source-level initialization or assignment of `lhs_id` from
+// `rhs_id`. This finishes initialization of `lhs_id` in the same way as
+// `InitializeFrom`.
+struct Assign {
+  static constexpr auto Kind = NodeKind::Assign.Define("assign");
+
+  Parse::Node parse_node;
+  // Assignments are statements, and so have no type.
+  NodeId lhs_id;
+  NodeId rhs_id;
+};
+
+struct BinaryOperatorAdd {
+  static constexpr auto Kind = NodeKind::BinaryOperatorAdd.Define("add");
+
+  Parse::Node parse_node;
+  TypeId type_id;
+  NodeId lhs_id;
+  NodeId rhs_id;
+};
+
+struct BindName {
+  static constexpr auto Kind = NodeKind::BindName.Define("bind_name");
+
+  Parse::Node parse_node;
+  TypeId type_id;
+  StringId name_id;
+  NodeId value_id;
+};
+
+struct BindValue {
+  static constexpr auto Kind = NodeKind::BindValue.Define("bind_value");
+
+  Parse::Node parse_node;
+  TypeId type_id;
+  NodeId value_id;
+};
+
+struct BlockArg {
+  static constexpr auto Kind = NodeKind::BlockArg.Define("block_arg");
+
+  Parse::Node parse_node;
+  TypeId type_id;
+  NodeBlockId block_id;
+};
+
+struct BoolLiteral {
+  static constexpr auto Kind = NodeKind::BoolLiteral.Define("bool_literal");
+
+  Parse::Node parse_node;
+  TypeId type_id;
+  BoolValue value;
+};
+
+struct Branch {
+  static constexpr auto Kind =
+      NodeKind::Branch.Define("br", TerminatorKind::Terminator);
+
+  Parse::Node parse_node;
+  // Branches don't produce a value, so have no type.
+  NodeBlockId target_id;
+};
+
+struct BranchIf {
+  static constexpr auto Kind =
+      NodeKind::BranchIf.Define("br", TerminatorKind::TerminatorSequence);
+
+  Parse::Node parse_node;
+  // Branches don't produce a value, so have no type.
+  NodeBlockId target_id;
+  NodeId cond_id;
+};
+
+struct BranchWithArg {
+  static constexpr auto Kind =
+      NodeKind::BranchWithArg.Define("br", TerminatorKind::Terminator);
+
+  Parse::Node parse_node;
+  // Branches don't produce a value, so have no type.
+  NodeBlockId target_id;
+  NodeId arg_id;
+};
+
+struct Builtin {
+  static constexpr auto Kind = NodeKind::Builtin.Define("builtin");
+
+  // Builtins don't have a parse node associated with them.
+  TypeId type_id;
+  BuiltinKind builtin_kind;
+};
+
+struct Call {
+  static constexpr auto Kind = NodeKind::Call.Define("call");
+
+  Parse::Node parse_node;
+  TypeId type_id;
+  NodeId callee_id;
+  NodeBlockId args_id;
+};
+
+struct ClassDeclaration {
+  static constexpr auto Kind =
+      NodeKind::ClassDeclaration.Define("class_declaration");
+
+  Parse::Node parse_node;
+  TypeId type_id;
+  ClassId class_id;
+  // The declaration block, containing the class name's qualifiers and the
+  // class's generic parameters.
+  NodeBlockId decl_block_id;
+};
+
+struct ConstType {
+  static constexpr auto Kind = NodeKind::ConstType.Define("const_type");
+
+  Parse::Node parse_node;
+  TypeId type_id;
+  TypeId inner_id;
+};
+
+// A cross-reference between IRs.
+struct CrossReference {
+  static constexpr auto Kind = NodeKind::CrossReference.Define("xref");
+
+  // No parse node: a node's parse tree node must refer to a node in the
+  // current parse tree. This cannot use the cross-referenced node's parse tree
+  // node because it will be in a different parse tree.
+  TypeId type_id;
+  CrossReferenceIRId ir_id;
+  NodeId node_id;
+};
+
+struct Dereference {
+  static constexpr auto Kind = NodeKind::Dereference.Define("dereference");
+
+  Parse::Node parse_node;
+  TypeId type_id;
+  NodeId pointer_id;
+};
+
+struct FunctionDeclaration {
+  static constexpr auto Kind = NodeKind::FunctionDeclaration.Define("fn_decl");
+
+  Parse::Node parse_node;
+  TypeId type_id;
+  FunctionId function_id;
+};
+
+// Finalizes the initialization of `dest_id` from the initializer expression
+// `src_id`, by performing a final copy from source to destination, for types
+// whose initialization is not in-place.
+struct InitializeFrom {
+  static constexpr auto Kind =
+      NodeKind::InitializeFrom.Define("initialize_from");
+
+  Parse::Node parse_node;
+  TypeId type_id;
+  NodeId src_id;
+  NodeId dest_id;
+};
+
+struct IntegerLiteral {
+  static constexpr auto Kind = NodeKind::IntegerLiteral.Define("int_literal");
+
+  Parse::Node parse_node;
+  TypeId type_id;
+  IntegerId integer_id;
+};
+
+struct NameReference {
+  static constexpr auto Kind = NodeKind::NameReference.Define("name_reference");
+
+  Parse::Node parse_node;
+  TypeId type_id;
+  StringId name_id;
+  NodeId value_id;
+};
+
+struct Namespace {
+  static constexpr auto Kind = NodeKind::Namespace.Define("namespace");
+
+  Parse::Node parse_node;
+  TypeId type_id;
+  NameScopeId name_scope_id;
+};
+
+struct NoOp {
+  static constexpr auto Kind = NodeKind::NoOp.Define("no_op");
+
+  Parse::Node parse_node;
+  // This node doesn't produce a value, so has no type.
+};
+
+struct Parameter {
+  static constexpr auto Kind = NodeKind::Parameter.Define("parameter");
+
+  Parse::Node parse_node;
+  TypeId type_id;
+  StringId name_id;
+};
+
+struct PointerType {
+  static constexpr auto Kind = NodeKind::PointerType.Define("ptr_type");
+
+  Parse::Node parse_node;
+  TypeId type_id;
+  TypeId pointee_id;
+};
+
+struct RealLiteral {
+  static constexpr auto Kind = NodeKind::RealLiteral.Define("real_literal");
+
+  Parse::Node parse_node;
+  TypeId type_id;
+  RealId real_id;
+};
+
+struct Return {
+  static constexpr auto Kind =
+      NodeKind::Return.Define("return", TerminatorKind::Terminator);
+
+  Parse::Node parse_node;
+  // This is a statement, so has no type.
+};
+
+struct ReturnExpression {
+  static constexpr auto Kind =
+      NodeKind::ReturnExpression.Define("return", TerminatorKind::Terminator);
+
+  Parse::Node parse_node;
+  // This is a statement, so has no type.
+  NodeId expr_id;
+};
+
+struct SpliceBlock {
+  static constexpr auto Kind = NodeKind::SpliceBlock.Define("splice_block");
+
+  Parse::Node parse_node;
+  TypeId type_id;
+  NodeBlockId block_id;
+  NodeId result_id;
+};
+
+struct StringLiteral {
+  static constexpr auto Kind = NodeKind::StringLiteral.Define("string_literal");
+
+  Parse::Node parse_node;
+  TypeId type_id;
+  StringId string_id;
+};
+
+struct StructAccess {
+  static constexpr auto Kind = NodeKind::StructAccess.Define("struct_access");
+
+  Parse::Node parse_node;
+  TypeId type_id;
+  NodeId struct_id;
+  MemberIndex index;
+};
+
+struct StructInit {
+  static constexpr auto Kind = NodeKind::StructInit.Define("struct_init");
+
+  Parse::Node parse_node;
+  TypeId type_id;
+  NodeId src_id;
+  NodeBlockId elements_id;
+};
+
+struct StructLiteral {
+  static constexpr auto Kind = NodeKind::StructLiteral.Define("struct_literal");
+
+  Parse::Node parse_node;
+  TypeId type_id;
+  NodeBlockId elements_id;
+};
+
+struct StructType {
+  static constexpr auto Kind = NodeKind::StructType.Define("struct_type");
+
+  Parse::Node parse_node;
+  TypeId type_id;
+  NodeBlockId fields_id;
+};
+
+struct StructTypeField {
+  static constexpr auto Kind =
+      NodeKind::StructTypeField.Define("struct_type_field");
+
+  Parse::Node parse_node;
+  // This node is an implementation detail of `StructType`, and doesn't produce
+  // a value, so has no type, even though it declares a field with a type.
+  StringId name_id;
+  TypeId field_type_id;
+};
+
+struct StructValue {
+  static constexpr auto Kind = NodeKind::StructValue.Define("struct_value");
+
+  Parse::Node parse_node;
+  TypeId type_id;
+  NodeId src_id;
+  NodeBlockId elements_id;
+};
+
+struct Temporary {
+  static constexpr auto Kind = NodeKind::Temporary.Define("temporary");
+
+  Parse::Node parse_node;
+  TypeId type_id;
+  NodeId storage_id;
+  NodeId init_id;
+};
+
+struct TemporaryStorage {
+  static constexpr auto Kind =
+      NodeKind::TemporaryStorage.Define("temporary_storage");
+
+  Parse::Node parse_node;
+  TypeId type_id;
+};
+
+struct TupleAccess {
+  static constexpr auto Kind = NodeKind::TupleAccess.Define("tuple_access");
+
+  Parse::Node parse_node;
+  TypeId type_id;
+  NodeId tuple_id;
+  MemberIndex index;
+};
+
+struct TupleIndex {
+  static constexpr auto Kind = NodeKind::TupleIndex.Define("tuple_index");
+
+  Parse::Node parse_node;
+  TypeId type_id;
+  NodeId tuple_id;
+  NodeId index_id;
+};
+
+struct TupleInit {
+  static constexpr auto Kind = NodeKind::TupleInit.Define("tuple_init");
+
+  Parse::Node parse_node;
+  TypeId type_id;
+  NodeId src_id;
+  NodeBlockId elements_id;
+};
+
+struct TupleLiteral {
+  static constexpr auto Kind = NodeKind::TupleLiteral.Define("tuple_literal");
+
+  Parse::Node parse_node;
+  TypeId type_id;
+  NodeBlockId elements_id;
+};
+
+struct TupleType {
+  static constexpr auto Kind = NodeKind::TupleType.Define("tuple_type");
+
+  Parse::Node parse_node;
+  TypeId type_id;
+  TypeBlockId elements_id;
+};
+
+struct TupleValue {
+  static constexpr auto Kind = NodeKind::TupleValue.Define("tuple_value");
+
+  Parse::Node parse_node;
+  TypeId type_id;
+  NodeId src_id;
+  NodeBlockId elements_id;
+};
+
+struct UnaryOperatorNot {
+  static constexpr auto Kind = NodeKind::UnaryOperatorNot.Define("not");
+
+  Parse::Node parse_node;
+  TypeId type_id;
+  NodeId operand_id;
+};
+
+struct ValueAsReference {
+  static constexpr auto Kind =
+      NodeKind::ValueAsReference.Define("value_as_reference");
+
+  Parse::Node parse_node;
+  TypeId type_id;
+  NodeId value_id;
+};
+
+struct VarStorage {
+  static constexpr auto Kind = NodeKind::VarStorage.Define("var");
+
+  Parse::Node parse_node;
+  TypeId type_id;
+  StringId name_id;
+};
+
+// HasParseNode<T> is true if T has a `Parse::Node parse_node` field.
+template <typename T, typename ParseNodeType = Parse::Node T::*>
+constexpr bool HasParseNode = false;
+template <typename T>
+constexpr bool HasParseNode<T, decltype(&T::parse_node)> = true;
+
+// HasTypeId<T> is true if T has a `TypeId type_id` field.
+template <typename T, typename TypeIdType = TypeId T::*>
+constexpr bool HasTypeId = false;
+template <typename T>
+constexpr bool HasTypeId<T, decltype(&T::type_id)> = true;
+
+}  // namespace Carbon::SemIR
+
+#endif  // CARBON_TOOLCHAIN_SEM_IR_TYPED_NODES_H_

+ 133 - 0
toolchain/sem_ir/typed_nodes_test.cpp

@@ -0,0 +1,133 @@
+// 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/sem_ir/typed_nodes.h"
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include "toolchain/sem_ir/node.h"
+
+namespace Carbon::SemIR {
+
+// A friend of `SemIR::Node` that is used to pierce the abstraction.
+class NodeTestHelper {
+ public:
+  static auto MakeNode(NodeKind node_kind, Parse::Node parse_node,
+                       TypeId type_id, int32_t arg0, int32_t arg1) -> Node {
+    return Node(node_kind, parse_node, type_id, arg0, arg1);
+  }
+};
+
+}  // namespace Carbon::SemIR
+
+namespace Carbon::SemIR {
+namespace {
+
+// Check that each node kind defines a Kind member using the correct NodeKind
+// enumerator.
+#define CARBON_SEM_IR_NODE_KIND(Name) \
+  static_assert(Name::Kind == NodeKind::Name);
+#include "toolchain/sem_ir/node_kind.def"
+
+template <typename Ignored, typename... Types>
+using TypesExceptFirst = ::testing::Types<Types...>;
+
+// Form a list of all typed node types. Use `TypesExceptFirst` and a leading
+// `void` to handle the problem that we only want N-1 commas in this list.
+using TypedNodeTypes = TypesExceptFirst<void
+#define CARBON_SEM_IR_NODE_KIND(Name) , Name
+#include "toolchain/sem_ir/node_kind.def"
+                                        >;
+
+// Set up the test fixture.
+template <typename TypedNode>
+class TypedNodeTest : public testing::Test {};
+
+TYPED_TEST_SUITE(TypedNodeTest, TypedNodeTypes);
+
+TYPED_TEST(TypedNodeTest, CommonFieldOrder) {
+  using TypedNode = TypeParam;
+
+  Node node = NodeTestHelper::MakeNode(TypeParam::Kind, Parse::Node(1),
+                                       TypeId(2), 3, 4);
+  EXPECT_EQ(node.kind(), TypeParam::Kind);
+  EXPECT_EQ(node.parse_node(), Parse::Node(1));
+  EXPECT_EQ(node.type_id(), TypeId(2));
+
+  TypedNode typed = node.As<TypedNode>();
+  if constexpr (HasParseNode<TypedNode>) {
+    EXPECT_EQ(typed.parse_node, Parse::Node(1));
+  }
+  if constexpr (HasTypeId<TypedNode>) {
+    EXPECT_EQ(typed.type_id, TypeId(2));
+  }
+}
+
+TYPED_TEST(TypedNodeTest, RoundTrip) {
+  using TypedNode = TypeParam;
+
+  Node node1 = NodeTestHelper::MakeNode(TypeParam::Kind, Parse::Node(1),
+                                        TypeId(2), 3, 4);
+  EXPECT_EQ(node1.kind(), TypeParam::Kind);
+  EXPECT_EQ(node1.parse_node(), Parse::Node(1));
+  EXPECT_EQ(node1.type_id(), TypeId(2));
+
+  TypedNode typed1 = node1.As<TypedNode>();
+  Node node2 = typed1;
+
+  EXPECT_EQ(node1.kind(), node2.kind());
+  if constexpr (HasParseNode<TypedNode>) {
+    EXPECT_EQ(node1.parse_node(), node2.parse_node());
+  }
+  if constexpr (HasTypeId<TypedNode>) {
+    EXPECT_EQ(node1.type_id(), node2.type_id());
+  }
+
+  // If the typed node has no padding, we should get exactly the same thing
+  // if we convert back from a node.
+  TypedNode typed2 = node2.As<TypedNode>();
+  if constexpr (std::has_unique_object_representations_v<TypedNode>) {
+    EXPECT_EQ(std::memcmp(&typed1, &typed2, sizeof(TypedNode)), 0);
+  }
+
+  // The original node might not be identical after one round trip, because the
+  // fields not carried by the typed node are lost. But they should be stable
+  // if we round-trip again.
+  Node node3 = typed2;
+  if constexpr (std::has_unique_object_representations_v<Node>) {
+    EXPECT_EQ(std::memcmp(&node2, &node3, sizeof(Node)), 0);
+  }
+}
+
+TYPED_TEST(TypedNodeTest, StructLayout) {
+  using TypedNode = TypeParam;
+
+  TypedNode typed =
+      NodeTestHelper::MakeNode(TypeParam::Kind, Parse::Node(1), TypeId(2), 3, 4)
+          .template As<TypedNode>();
+
+  // Check that the memory representation of the typed node is what we expect.
+  // TODO: Struct layout is not guaranteed, and this test could fail in some
+  // build environment. If so, we should disable it.
+  int32_t fields[4] = {};
+  int field = 0;
+  if constexpr (HasParseNode<TypedNode>) {
+    fields[field++] = 1;
+  }
+  if constexpr (HasTypeId<TypedNode>) {
+    fields[field++] = 2;
+  }
+  fields[field++] = 3;
+  fields[field++] = 4;
+
+  ASSERT_LE(sizeof(TypedNode), sizeof(fields));
+  // We can only do this check if the typed node has no padding.
+  if constexpr (std::has_unique_object_representations_v<TypedNode>) {
+    EXPECT_EQ(std::memcmp(&fields, &typed, sizeof(TypedNode)), 0);
+  }
+}
+
+}  // namespace
+}  // namespace Carbon::SemIR