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

Add typed nodes to SemIR. (#3280)

Replace `SemIR::Node::GetAsFoo` and `SemIR::Node::Foo::Make` with
`SemIR::Foo` class that represents a particular kind of node, with named
fields.

Rename `SemIR::IntegerLiteral` and `SemIR::RealLiteral` to
`IntegerValue` / `RealValue` to better reflect their purpose and avoid a
name collision with the corresponding `SemIR` node kinds.

Remove `NodeKind::Invalid` and the `SemIR::Node` default constructor
entirely, as they were not used for anything.
Richard Smith 2 лет назад
Родитель
Сommit
c7a9e29a89
41 измененных файлов с 1402 добавлено и 911 удалено
  1. 16 0
      common/BUILD
  2. 162 0
      common/struct_reflection.h
  3. 97 0
      common/struct_reflection_test.cpp
  4. 46 46
      toolchain/check/context.cpp
  5. 112 112
      toolchain/check/convert.cpp
  6. 2 1
      toolchain/check/declaration_name_stack.cpp
  7. 4 4
      toolchain/check/handle_array.cpp
  8. 7 6
      toolchain/check/handle_call_expression.cpp
  9. 8 9
      toolchain/check/handle_function.cpp
  10. 1 1
      toolchain/check/handle_if_statement.cpp
  11. 21 19
      toolchain/check/handle_index.cpp
  12. 6 7
      toolchain/check/handle_let.cpp
  13. 6 6
      toolchain/check/handle_literal.cpp
  14. 14 15
      toolchain/check/handle_name.cpp
  15. 2 2
      toolchain/check/handle_namespace.cpp
  16. 21 22
      toolchain/check/handle_operator.cpp
  17. 2 2
      toolchain/check/handle_paren.cpp
  18. 5 5
      toolchain/check/handle_pattern_binding.cpp
  19. 6 6
      toolchain/check/handle_statement.cpp
  20. 8 8
      toolchain/check/handle_struct.cpp
  21. 3 4
      toolchain/check/handle_variable.cpp
  22. 5 6
      toolchain/check/pending_block.h
  23. 2 2
      toolchain/check/testdata/basics/builtin_nodes.carbon
  24. 4 4
      toolchain/check/testdata/basics/multifile_raw_and_textual_ir.carbon
  25. 4 4
      toolchain/check/testdata/basics/multifile_raw_ir.carbon
  26. 2 2
      toolchain/check/testdata/basics/raw_and_textual_ir.carbon
  27. 2 2
      toolchain/check/testdata/basics/raw_ir.carbon
  28. 18 16
      toolchain/lower/file_context.cpp
  29. 3 3
      toolchain/lower/function_context.cpp
  30. 1 1
      toolchain/lower/function_context.h
  31. 132 154
      toolchain/lower/handle.cpp
  32. 19 20
      toolchain/lower/handle_expression_category.cpp
  33. 5 5
      toolchain/lower/handle_type.cpp
  34. 1 0
      toolchain/sem_ir/BUILD
  35. 45 57
      toolchain/sem_ir/file.cpp
  36. 28 20
      toolchain/sem_ir/file.h
  37. 2 2
      toolchain/sem_ir/file_test.cpp
  38. 72 100
      toolchain/sem_ir/formatter.cpp
  39. 9 17
      toolchain/sem_ir/node.cpp
  40. 499 219
      toolchain/sem_ir/node.h
  41. 0 2
      toolchain/sem_ir/node_kind.def

+ 16 - 0
common/BUILD

@@ -153,6 +153,22 @@ cc_test(
     ],
 )
 
+cc_library(
+    name = "struct_reflection",
+    hdrs = ["struct_reflection.h"],
+)
+
+cc_test(
+    name = "struct_reflection_test",
+    size = "small",
+    srcs = ["struct_reflection_test.cpp"],
+    deps = [
+        ":struct_reflection",
+        "//testing/base:gtest_main",
+        "@com_google_googletest//:gtest",
+    ],
+)
+
 cc_library(
     name = "vlog",
     srcs = ["vlog_internal.h"],

+ 162 - 0
common/struct_reflection.h

@@ -0,0 +1,162 @@
+// 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_COMMON_STRUCT_REFLECTION_H_
+#define CARBON_COMMON_STRUCT_REFLECTION_H_
+
+// Reflection support for simple struct types.
+//
+// Example usage:
+//
+// ```
+// struct A { int x; std::string y; };
+//
+// A a;
+// std::tuple<int, std::string> t = StructReflection::AsTuple(a);
+// ```
+//
+// Limitations:
+//
+// - Only simple aggregate structs are supported. Types with base classes,
+//   non-public data members, constructors, or virtual functions are not
+//   supported.
+// - Structs with more than 5 fields are not supported. This limit is easy to
+//   increase if needed, but removing it entirely is hard.
+// - Structs containing a reference to the same type are not supported.
+
+#include <tuple>
+#include <type_traits>
+
+namespace Carbon::StructReflection {
+
+namespace Internal {
+
+// A type that can be converted to any field type within type T.
+template <typename T>
+struct AnyField {
+  template <typename FieldT>
+  operator FieldT&() const;
+  template <typename FieldT>
+  operator FieldT&&() const;
+
+  // Don't allow conversion to T itself. This ensures we don't match against a
+  // copy or move constructor.
+  operator T&() const = delete;
+  operator T&&() const = delete;
+};
+
+// The detection mechanism below intentionally misses field initializers.
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wmissing-field-initializers"
+
+// Detector for whether we can list-initialize T from the given list of fields.
+template <typename T, typename... Fields>
+constexpr bool CanListInitialize(decltype(T{Fields()...})*) {
+  return true;
+}
+template <typename T, typename... Fields>
+constexpr bool CanListInitialize(...) {
+  return false;
+}
+
+#pragma clang diagnostic pop
+
+// Simple detector to find the number of data fields in a struct. This proceeds
+// in two passes:
+//
+// 1) Add AnyField<T>s until we can initialize T from our list of initializers.
+// 2) Add more AnyField<T>s until we can't initialize any more.
+template <typename T, bool AnyWorkedSoFar = false, typename... Fields>
+constexpr auto CountFields() -> int {
+  if constexpr (CanListInitialize<T, Fields...>(0)) {
+    return CountFields<T, true, Fields..., AnyField<T>>();
+  } else if constexpr (AnyWorkedSoFar) {
+    static_assert(sizeof...(Fields) <= 5,
+                  "Unsupported: too many fields in struct");
+    return sizeof...(Fields) - 1;
+  } else if constexpr (sizeof...(Fields) > 32) {
+    // If we go too far without finding a working initializer, something
+    // probably went wrong with our calculation. Bail out before we recurse too
+    // deeply.
+    static_assert(sizeof...(Fields) <= 32,
+                  "Internal error, could not count fields in struct");
+  } else {
+    return CountFields<T, false, Fields..., AnyField<T>>();
+  }
+}
+
+// Utility to access fields by index.
+template <int NumFields>
+struct FieldAccessor;
+
+template <>
+struct FieldAccessor<0> {
+  template <typename T>
+  static auto Get(T& /*value*/) -> auto {
+    return std::tuple<>();
+  }
+};
+
+template <>
+struct FieldAccessor<1> {
+  template <typename T>
+  static auto Get(T& value) -> auto {
+    auto& [field0] = value;
+    return std::tuple<decltype(field0)>(field0);
+  }
+};
+
+template <>
+struct FieldAccessor<2> {
+  template <typename T>
+  static auto Get(T& value) -> auto {
+    auto& [field0, field1] = value;
+    return std::tuple<decltype(field0), decltype(field1)>(field0, field1);
+  }
+};
+
+template <>
+struct FieldAccessor<3> {
+  template <typename T>
+  static auto Get(T& value) -> auto {
+    auto& [field0, field1, field2] = value;
+    return std::tuple<decltype(field0), decltype(field1), decltype(field2)>(
+        field0, field1, field2);
+  }
+};
+
+template <>
+struct FieldAccessor<4> {
+  template <typename T>
+  static auto Get(T& value) -> auto {
+    auto& [field0, field1, field2, field3] = value;
+    return std::tuple<decltype(field0), decltype(field1), decltype(field2),
+                      decltype(field3)>(field0, field1, field2, field3);
+  }
+};
+
+template <>
+struct FieldAccessor<5> {
+  template <typename T>
+  static auto Get(T& value) -> auto {
+    auto& [field0, field1, field2, field3, field4] = value;
+    return std::tuple<decltype(field0), decltype(field1), decltype(field2),
+                      decltype(field3), decltype(field4)>(
+        field0, field1, field2, field3, field4);
+  }
+};
+
+}  // namespace Internal
+
+// Get the fields of the struct `T` as a tuple.
+template <typename T>
+auto AsTuple(T value) -> auto {
+  // We use aggregate initialization to detect the number of fields.
+  static_assert(std::is_aggregate_v<T>, "Only aggregates are supported");
+  return Internal::FieldAccessor<Internal::CountFields<T>()>::Get(value);
+}
+
+}  // namespace Carbon::StructReflection
+
+#endif  // CARBON_COMMON_STRUCT_REFLECTION_H_

+ 97 - 0
common/struct_reflection_test.cpp

@@ -0,0 +1,97 @@
+// 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 "common/struct_reflection.h"
+
+#include <gtest/gtest.h>
+
+namespace Carbon::StructReflection {
+namespace {
+
+struct ZeroFields {};
+
+struct OneField {
+  int x;
+};
+
+struct TwoFields {
+  int x;
+  int y;
+};
+
+struct ReferenceField {
+  int& ref;
+};
+
+struct NoDefaultConstructor {
+  NoDefaultConstructor(int n) : v(n) {}
+  int v;
+};
+
+struct OneFieldNoDefaultConstructor {
+  NoDefaultConstructor x;
+};
+
+struct TwoFieldsNoDefaultConstructor {
+  NoDefaultConstructor x;
+  NoDefaultConstructor y;
+};
+
+TEST(StructReflectionTest, CanListInitialize) {
+  {
+    using Type = OneField;
+    using Field = Internal::AnyField<Type>;
+    static_assert(Internal::CanListInitialize<Type>(0));
+    static_assert(Internal::CanListInitialize<Type, Field>(0));
+    static_assert(!Internal::CanListInitialize<Type, Field, Field>(0));
+  }
+
+  {
+    using Type = OneFieldNoDefaultConstructor;
+    using Field = Internal::AnyField<Type>;
+    static_assert(!Internal::CanListInitialize<Type>(0));
+    static_assert(Internal::CanListInitialize<Type, Field>(0));
+    static_assert(!Internal::CanListInitialize<Type, Field, Field>(0));
+  }
+}
+
+TEST(StructReflectionTest, CountFields) {
+  static_assert(Internal::CountFields<ZeroFields>() == 0);
+  static_assert(Internal::CountFields<OneField>() == 1);
+  static_assert(Internal::CountFields<TwoFields>() == 2);
+  static_assert(Internal::CountFields<ReferenceField>() == 1);
+  static_assert(Internal::CountFields<OneFieldNoDefaultConstructor>() == 1);
+}
+
+TEST(StructReflectionTest, EmptyStruct) {
+  std::tuple<> fields = AsTuple(ZeroFields());
+  static_cast<void>(fields);
+}
+
+TEST(StructReflectionTest, OneField) {
+  std::tuple<int> fields = AsTuple(OneField{.x = 1});
+  EXPECT_EQ(std::get<0>(fields), 1);
+}
+
+TEST(StructReflectionTest, TwoField) {
+  std::tuple<int, int> fields = AsTuple(TwoFields{.x = 1, .y = 2});
+  EXPECT_EQ(std::get<0>(fields), 1);
+  EXPECT_EQ(std::get<1>(fields), 2);
+}
+
+TEST(StructReflectionTest, NoDefaultConstructor) {
+  std::tuple<NoDefaultConstructor, NoDefaultConstructor> fields =
+      AsTuple(TwoFieldsNoDefaultConstructor{.x = 1, .y = 2});
+  EXPECT_EQ(std::get<0>(fields).v, 1);
+  EXPECT_EQ(std::get<1>(fields).v, 2);
+}
+
+TEST(StructReflectionTest, ReferenceField) {
+  int n = 0;
+  std::tuple<int&> fields = AsTuple(ReferenceField{.ref = n});
+  EXPECT_EQ(&std::get<0>(fields), &n);
+}
+
+}  // namespace
+}  // namespace Carbon::StructReflection

+ 46 - 46
toolchain/check/context.cpp

@@ -149,13 +149,11 @@ auto Context::FollowNameReferences(SemIR::NodeId node_id) -> SemIR::NodeId {
     auto node = semantics_ir().GetNode(node_id);
     switch (node.kind()) {
       case SemIR::NodeKind::NameReference: {
-        auto [name_id, value_id] = node.GetAsNameReference();
-        node_id = value_id;
+        node_id = node.As<SemIR::NameReference>().value_id;
         break;
       }
       case SemIR::NodeKind::NameReferenceUntyped: {
-        auto [name_id, value_id] = node.GetAsNameReferenceUntyped();
-        node_id = value_id;
+        node_id = node.As<SemIR::NameReferenceUntyped>().value_id;
         break;
       }
       default:
@@ -172,27 +170,27 @@ static auto AddDominatedBlockAndBranchImpl(Context& context,
     return SemIR::NodeBlockId::Unreachable;
   }
   auto block_id = context.semantics_ir().AddNodeBlockId();
-  context.AddNode(BranchNode::Make(parse_node, block_id, args...));
+  context.AddNode(BranchNode(parse_node, block_id, args...));
   return block_id;
 }
 
 auto Context::AddDominatedBlockAndBranch(Parse::Node parse_node)
     -> SemIR::NodeBlockId {
-  return AddDominatedBlockAndBranchImpl<SemIR::Node::Branch>(*this, parse_node);
+  return AddDominatedBlockAndBranchImpl<SemIR::Branch>(*this, parse_node);
 }
 
 auto Context::AddDominatedBlockAndBranchWithArg(Parse::Node parse_node,
                                                 SemIR::NodeId arg_id)
     -> SemIR::NodeBlockId {
-  return AddDominatedBlockAndBranchImpl<SemIR::Node::BranchWithArg>(
-      *this, parse_node, arg_id);
+  return AddDominatedBlockAndBranchImpl<SemIR::BranchWithArg>(*this, parse_node,
+                                                              arg_id);
 }
 
 auto Context::AddDominatedBlockAndBranchIf(Parse::Node parse_node,
                                            SemIR::NodeId cond_id)
     -> SemIR::NodeBlockId {
-  return AddDominatedBlockAndBranchImpl<SemIR::Node::BranchIf>(
-      *this, parse_node, cond_id);
+  return AddDominatedBlockAndBranchImpl<SemIR::BranchIf>(*this, parse_node,
+                                                         cond_id);
 }
 
 auto Context::AddConvergenceBlockAndPush(Parse::Node parse_node, int num_blocks)
@@ -205,7 +203,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::Node::Branch::Make(parse_node, new_block_id));
+      AddNode(SemIR::Branch(parse_node, new_block_id));
     }
     node_block_stack().Pop();
   }
@@ -223,8 +221,7 @@ auto Context::AddConvergenceBlockWithArgAndPush(
       if (new_block_id == SemIR::NodeBlockId::Unreachable) {
         new_block_id = semantics_ir().AddNodeBlockId();
       }
-      AddNode(
-          SemIR::Node::BranchWithArg::Make(parse_node, new_block_id, arg_id));
+      AddNode(SemIR::BranchWithArg(parse_node, new_block_id, arg_id));
     }
     node_block_stack().Pop();
   }
@@ -233,8 +230,7 @@ auto Context::AddConvergenceBlockWithArgAndPush(
   // Acquire the result value.
   SemIR::TypeId result_type_id =
       semantics_ir().GetNode(*block_args.begin()).type_id();
-  return AddNode(
-      SemIR::Node::BlockArg::Make(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.
@@ -247,9 +243,10 @@ auto Context::AddCurrentCodeBlockToFunction() -> void {
     return;
   }
 
-  auto function_id = semantics_ir()
-                         .GetNode(return_scope_stack().back())
-                         .GetAsFunctionDeclaration();
+  auto function_id =
+      semantics_ir()
+          .GetNodeAs<SemIR::FunctionDeclaration>(return_scope_stack().back())
+          .function_id;
   semantics_ir()
       .GetFunction(function_id)
       .body_block_ids.push_back(node_block_stack().PeekOrAdd());
@@ -339,45 +336,49 @@ static auto ProfileType(Context& semantics_context, SemIR::Node node,
                         llvm::FoldingSetNodeID& canonical_id) -> void {
   switch (node.kind()) {
     case SemIR::NodeKind::ArrayType: {
-      auto [bound_id, element_type_id] = node.GetAsArrayType();
+      auto array_type = node.As<SemIR::ArrayType>();
       canonical_id.AddInteger(
-          semantics_context.semantics_ir().GetArrayBoundValue(bound_id));
-      canonical_id.AddInteger(element_type_id.index);
+          semantics_context.semantics_ir().GetArrayBoundValue(
+              array_type.bound_id));
+      canonical_id.AddInteger(array_type.element_type_id.index);
       break;
     }
     case SemIR::NodeKind::Builtin:
-      canonical_id.AddInteger(node.GetAsBuiltin().AsInt());
+      canonical_id.AddInteger(node.As<SemIR::Builtin>().builtin_kind.AsInt());
       break;
     case SemIR::NodeKind::CrossReference: {
       // TODO: Cross-references should be canonicalized by looking at their
       // target rather than treating them as new unique types.
-      auto [xref_id, node_id] = node.GetAsCrossReference();
-      canonical_id.AddInteger(xref_id.index);
-      canonical_id.AddInteger(node_id.index);
+      auto xref = node.As<SemIR::CrossReference>();
+      canonical_id.AddInteger(xref.ir_id.index);
+      canonical_id.AddInteger(xref.node_id.index);
       break;
     }
     case SemIR::NodeKind::ConstType:
       canonical_id.AddInteger(
-          semantics_context.GetUnqualifiedType(node.GetAsConstType()).index);
+          semantics_context
+              .GetUnqualifiedType(node.As<SemIR::ConstType>().inner_id)
+              .index);
       break;
     case SemIR::NodeKind::PointerType:
-      canonical_id.AddInteger(node.GetAsPointerType().index);
+      canonical_id.AddInteger(node.As<SemIR::PointerType>().pointee_id.index);
       break;
     case SemIR::NodeKind::StructType: {
-      auto refs =
-          semantics_context.semantics_ir().GetNodeBlock(node.GetAsStructType());
-      for (const auto& ref_id : refs) {
-        auto ref = semantics_context.semantics_ir().GetNode(ref_id);
-        auto [name_id, type_id] = ref.GetAsStructTypeField();
-        canonical_id.AddInteger(name_id.index);
-        canonical_id.AddInteger(type_id.index);
+      auto fields = semantics_context.semantics_ir().GetNodeBlock(
+          node.As<SemIR::StructType>().fields_id);
+      for (const auto& field_id : fields) {
+        auto field =
+            semantics_context.semantics_ir().GetNodeAs<SemIR::StructTypeField>(
+                field_id);
+        canonical_id.AddInteger(field.name_id.index);
+        canonical_id.AddInteger(field.type_id.index);
       }
       break;
     }
     case SemIR::NodeKind::TupleType:
-      ProfileTupleType(
-          semantics_context.semantics_ir().GetTypeBlock(node.GetAsTupleType()),
-          canonical_id);
+      ProfileTupleType(semantics_context.semantics_ir().GetTypeBlock(
+                           node.As<SemIR::TupleType>().elements_id),
+                       canonical_id);
       break;
     default:
       CARBON_FATAL() << "Unexpected type node " << node;
@@ -410,8 +411,8 @@ auto Context::CanonicalizeType(SemIR::NodeId node_id) -> SemIR::TypeId {
 auto Context::CanonicalizeStructType(Parse::Node parse_node,
                                      SemIR::NodeBlockId refs_id)
     -> SemIR::TypeId {
-  return CanonicalizeTypeAndAddNodeIfNew(SemIR::Node::StructType::Make(
-      parse_node, SemIR::TypeId::TypeType, refs_id));
+  return CanonicalizeTypeAndAddNodeIfNew(
+      SemIR::StructType(parse_node, SemIR::TypeId::TypeType, refs_id));
 }
 
 auto Context::CanonicalizeTupleType(Parse::Node parse_node,
@@ -422,9 +423,8 @@ auto Context::CanonicalizeTupleType(Parse::Node parse_node,
     ProfileTupleType(type_ids, canonical_id);
   };
   auto make_tuple_node = [&] {
-    return AddNode(
-        SemIR::Node::TupleType::Make(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::NodeKind::TupleType, profile_tuple,
                               make_tuple_node);
@@ -432,15 +432,15 @@ auto Context::CanonicalizeTupleType(Parse::Node parse_node,
 
 auto Context::GetPointerType(Parse::Node parse_node,
                              SemIR::TypeId pointee_type_id) -> SemIR::TypeId {
-  return CanonicalizeTypeAndAddNodeIfNew(SemIR::Node::PointerType::Make(
-      parse_node, SemIR::TypeId::TypeType, pointee_type_id));
+  return CanonicalizeTypeAndAddNodeIfNew(
+      SemIR::PointerType(parse_node, SemIR::TypeId::TypeType, pointee_type_id));
 }
 
 auto Context::GetUnqualifiedType(SemIR::TypeId type_id) -> SemIR::TypeId {
   SemIR::Node type_node =
       semantics_ir_->GetNode(semantics_ir_->GetTypeAllowBuiltinTypes(type_id));
-  if (type_node.kind() == SemIR::NodeKind::ConstType) {
-    return type_node.GetAsConstType();
+  if (auto const_type = type_node.TryAs<SemIR::ConstType>()) {
+    return const_type->inner_id;
   }
   return type_id;
 }

+ 112 - 112
toolchain/check/convert.cpp

@@ -37,21 +37,22 @@ static auto FindReturnSlotForInitializer(SemIR::File& semantics_ir,
                         "filled in properly";
 
     case SemIR::NodeKind::InitializeFrom: {
-      auto [src_id, dest_id] = init.GetAsInitializeFrom();
-      return dest_id;
+      return init.As<SemIR::InitializeFrom>().dest_id;
     }
 
     case SemIR::NodeKind::Call: {
-      auto [refs_id, callee_id] = init.GetAsCall();
-      if (!semantics_ir.GetFunction(callee_id).return_slot_id.is_valid()) {
+      auto call = init.As<SemIR::Call>();
+      if (!semantics_ir.GetFunction(call.function_id)
+               .return_slot_id.is_valid()) {
         return SemIR::NodeId::Invalid;
       }
-      return semantics_ir.GetNodeBlock(refs_id).back();
+      return semantics_ir.GetNodeBlock(call.args_id).back();
     }
 
     case SemIR::NodeKind::ArrayInit: {
-      auto [src_id, refs_id] = init.GetAsArrayInit();
-      return semantics_ir.GetNodeBlock(refs_id).back();
+      return semantics_ir
+          .GetNodeBlock(init.As<SemIR::ArrayInit>().inits_and_return_slot_id)
+          .back();
     }
   }
 }
@@ -89,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::Node::Temporary::Make(
-        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) {
@@ -105,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::Node::TemporaryStorage::Make(init.parse_node(), init.type_id()));
-  return context.AddNode(SemIR::Node::Temporary::Make(
-      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
@@ -127,18 +128,18 @@ static auto MakeElemAccessNode(Context& context, Parse::Node parse_node,
                                SemIR::NodeId aggregate_id,
                                SemIR::TypeId elem_type_id, NodeBlockT& block,
                                std::size_t i) {
-  if constexpr (std::is_same_v<AccessNodeT, SemIR::Node::ArrayIndex>) {
+  if constexpr (std::is_same_v<AccessNodeT, SemIR::ArrayIndex>) {
     // 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::Node::IntegerLiteral::Make(
+    auto index_id = block.AddNode(SemIR::IntegerLiteral(
         parse_node, context.CanonicalizeType(SemIR::NodeId::BuiltinIntegerType),
-        context.semantics_ir().AddIntegerLiteral(llvm::APInt(32, i))));
+        context.semantics_ir().AddInteger(llvm::APInt(32, i))));
     return block.AddNode(
-        AccessNodeT::Make(parse_node, elem_type_id, aggregate_id, index_id));
+        AccessNodeT(parse_node, elem_type_id, aggregate_id, index_id));
   } else {
-    return block.AddNode(AccessNodeT::Make(
-        parse_node, elem_type_id, aggregate_id, SemIR::MemberIndex(i)));
+    return block.AddNode(AccessNodeT(parse_node, elem_type_id, aggregate_id,
+                                     SemIR::MemberIndex(i)));
   }
 }
 
@@ -229,13 +230,13 @@ 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::Node tuple_type,
-                                SemIR::Node array_type, SemIR::NodeId value_id,
-                                ConversionTarget target) -> SemIR::NodeId {
+static auto ConvertTupleToArray(Context& context,
+                                SemIR::TupleType::Data tuple_type,
+                                SemIR::ArrayType::Data array_type,
+                                SemIR::NodeId value_id, ConversionTarget target)
+    -> SemIR::NodeId {
   auto& semantics_ir = context.semantics_ir();
-  auto [array_bound_id, element_type_id] = array_type.GetAsArrayType();
-  auto tuple_elem_types_id = tuple_type.GetAsTupleType();
-  const auto& tuple_elem_types = semantics_ir.GetTypeBlock(tuple_elem_types_id);
+  auto tuple_elem_types = semantics_ir.GetTypeBlock(tuple_type.elements_id);
 
   auto value = semantics_ir.GetNode(value_id);
 
@@ -243,14 +244,14 @@ static auto ConvertTupleToArray(Context& context, SemIR::Node tuple_type,
   // directly. Otherwise, materialize a temporary if needed and index into the
   // result.
   llvm::ArrayRef<SemIR::NodeId> literal_elems;
-  if (value.kind() == SemIR::NodeKind::TupleLiteral) {
-    literal_elems = semantics_ir.GetNodeBlock(value.GetAsTupleLiteral());
+  if (auto tuple_literal = value.TryAs<SemIR::TupleLiteral>()) {
+    literal_elems = semantics_ir.GetNodeBlock(tuple_literal->elements_id);
   } else {
     value_id = MaterializeIfInitializing(context, value_id);
   }
 
   // Check that the tuple is the right size.
-  uint64_t array_bound = semantics_ir.GetArrayBoundValue(array_bound_id);
+  uint64_t array_bound = semantics_ir.GetArrayBoundValue(array_type.bound_id);
   if (tuple_elem_types.size() != array_bound) {
     CARBON_DIAGNOSTIC(
         ArrayInitFromLiteralArgCountMismatch, Error,
@@ -276,8 +277,8 @@ static auto ConvertTupleToArray(Context& context, SemIR::Node tuple_type,
   // destination for the array initialization if we weren't given one.
   SemIR::NodeId return_slot_id = target.init_id;
   if (!target.init_id.is_valid()) {
-    return_slot_id = target_block->AddNode(SemIR::Node::TemporaryStorage::Make(
-        value.parse_node(), target.type_id));
+    return_slot_id = target_block->AddNode(
+        SemIR::TemporaryStorage(value.parse_node(), target.type_id));
   }
 
   // Initialize each element of the array from the corresponding element of the
@@ -289,11 +290,11 @@ static auto ConvertTupleToArray(Context& context, SemIR::Node tuple_type,
   for (auto [i, src_type_id] : llvm::enumerate(tuple_elem_types)) {
     // TODO: This call recurses back into conversion. Switch to an iterative
     // approach.
-    auto init_id = ConvertAggregateElement<SemIR::Node::TupleAccess,
-                                           SemIR::Node::ArrayIndex>(
-        context, value.parse_node(), value_id, src_type_id, literal_elems,
-        ConversionTarget::FullInitializer, return_slot_id, element_type_id,
-        target_block, i);
+    auto init_id =
+        ConvertAggregateElement<SemIR::TupleAccess, SemIR::ArrayIndex>(
+            context, value.parse_node(), value_id, src_type_id, literal_elems,
+            ConversionTarget::FullInitializer, return_slot_id,
+            array_type.element_type_id, target_block, i);
     if (init_id == SemIR::NodeId::BuiltinError) {
       return SemIR::NodeId::BuiltinError;
     }
@@ -305,19 +306,21 @@ static auto ConvertTupleToArray(Context& context, SemIR::Node tuple_type,
   target_block->InsertHere();
   inits.push_back(return_slot_id);
 
-  return context.AddNode(
-      SemIR::Node::ArrayInit::Make(value.parse_node(), target.type_id, value_id,
-                                   semantics_ir.AddNodeBlock(inits)));
+  return context.AddNode(SemIR::ArrayInit(value.parse_node(), target.type_id,
+                                          value_id,
+                                          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::Node src_type,
-                                SemIR::Node dest_type, SemIR::NodeId value_id,
-                                ConversionTarget target) -> SemIR::NodeId {
+static auto ConvertTupleToTuple(Context& context,
+                                SemIR::TupleType::Data src_type,
+                                SemIR::TupleType::Data dest_type,
+                                SemIR::NodeId value_id, ConversionTarget target)
+    -> SemIR::NodeId {
   auto& semantics_ir = context.semantics_ir();
-  auto src_elem_types = semantics_ir.GetTypeBlock(src_type.GetAsTupleType());
-  auto dest_elem_types = semantics_ir.GetTypeBlock(dest_type.GetAsTupleType());
+  auto src_elem_types = semantics_ir.GetTypeBlock(src_type.elements_id);
+  auto dest_elem_types = semantics_ir.GetTypeBlock(dest_type.elements_id);
 
   auto value = semantics_ir.GetNode(value_id);
 
@@ -325,8 +328,10 @@ static auto ConvertTupleToTuple(Context& context, SemIR::Node src_type,
   // directly. Otherwise, materialize a temporary if needed and index into the
   // result.
   llvm::ArrayRef<SemIR::NodeId> literal_elems;
-  if (value.kind() == SemIR::NodeKind::TupleLiteral) {
-    literal_elems = semantics_ir.GetNodeBlock(value.GetAsTupleLiteral());
+  auto literal_elems_id = SemIR::NodeBlockId::Invalid;
+  if (auto tuple_literal = value.TryAs<SemIR::TupleLiteral>()) {
+    literal_elems_id = tuple_literal->elements_id;
+    literal_elems = semantics_ir.GetNodeBlock(literal_elems_id);
   } else {
     value_id = MaterializeIfInitializing(context, value_id);
   }
@@ -357,42 +362,40 @@ static auto ConvertTupleToTuple(Context& context, SemIR::Node src_type,
   // Initialize each element of the destination from the corresponding element
   // of the source.
   // TODO: Annotate diagnostics coming from here with the element index.
-  CopyOnWriteBlock new_block(semantics_ir,
-                             value.kind() == SemIR::NodeKind::TupleLiteral
-                                 ? value.GetAsTupleLiteral()
-                                 : SemIR::NodeBlockId::Invalid,
+  CopyOnWriteBlock new_block(semantics_ir, literal_elems_id,
                              src_elem_types.size());
   for (auto [i, src_type_id, dest_type_id] :
        llvm::enumerate(src_elem_types, dest_elem_types)) {
     // TODO: This call recurses back into conversion. Switch to an iterative
     // approach.
-    auto init_id = ConvertAggregateElement<SemIR::Node::TupleAccess,
-                                           SemIR::Node::TupleAccess>(
-        context, value.parse_node(), value_id, src_type_id, literal_elems,
-        inner_kind, target.init_id, dest_type_id, target.init_block, i);
+    auto init_id =
+        ConvertAggregateElement<SemIR::TupleAccess, SemIR::TupleAccess>(
+            context, value.parse_node(), value_id, src_type_id, literal_elems,
+            inner_kind, target.init_id, dest_type_id, target.init_block, i);
     if (init_id == SemIR::NodeId::BuiltinError) {
       return SemIR::NodeId::BuiltinError;
     }
     new_block.Set(i, init_id);
   }
 
-  return context.AddNode(
-      is_init
-          ? SemIR::Node::TupleInit::Make(value.parse_node(), target.type_id,
-                                         value_id, new_block.id())
-          : SemIR::Node::TupleValue::Make(value.parse_node(), target.type_id,
-                                          value_id, new_block.id()));
+  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(),
+                                                     target.type_id, value_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::Node src_type,
-                                  SemIR::Node dest_type, SemIR::NodeId value_id,
+static auto ConvertStructToStruct(Context& context,
+                                  SemIR::StructType::Data src_type,
+                                  SemIR::StructType::Data dest_type,
+                                  SemIR::NodeId value_id,
                                   ConversionTarget target) -> SemIR::NodeId {
   auto& semantics_ir = context.semantics_ir();
-  auto src_elem_fields = semantics_ir.GetNodeBlock(src_type.GetAsStructType());
-  auto dest_elem_fields =
-      semantics_ir.GetNodeBlock(dest_type.GetAsStructType());
+  auto src_elem_fields = semantics_ir.GetNodeBlock(src_type.fields_id);
+  auto dest_elem_fields = semantics_ir.GetNodeBlock(dest_type.fields_id);
 
   auto value = semantics_ir.GetNode(value_id);
 
@@ -400,8 +403,10 @@ static auto ConvertStructToStruct(Context& context, SemIR::Node src_type,
   // directly. Otherwise, materialize a temporary if needed and index into the
   // result.
   llvm::ArrayRef<SemIR::NodeId> literal_elems;
-  if (value.kind() == SemIR::NodeKind::StructLiteral) {
-    literal_elems = semantics_ir.GetNodeBlock(value.GetAsStructLiteral());
+  auto literal_elems_id = SemIR::NodeBlockId::Invalid;
+  if (auto struct_literal = value.TryAs<SemIR::StructLiteral>()) {
+    literal_elems_id = struct_literal->elements_id;
+    literal_elems = semantics_ir.GetNodeBlock(literal_elems_id);
   } else {
     value_id = MaterializeIfInitializing(context, value_id);
   }
@@ -434,47 +439,45 @@ static auto ConvertStructToStruct(Context& context, SemIR::Node src_type,
   // Initialize each element of the destination from the corresponding element
   // of the source.
   // TODO: Annotate diagnostics coming from here with the element index.
-  CopyOnWriteBlock new_block(semantics_ir,
-                             value.kind() == SemIR::NodeKind::StructLiteral
-                                 ? value.GetAsStructLiteral()
-                                 : SemIR::NodeBlockId::Invalid,
+  CopyOnWriteBlock new_block(semantics_ir, literal_elems_id,
                              src_elem_fields.size());
   for (auto [i, src_field_id, dest_field_id] :
        llvm::enumerate(src_elem_fields, dest_elem_fields)) {
-    auto [src_name_id, src_type_id] =
-        semantics_ir.GetNode(src_field_id).GetAsStructTypeField();
-    auto [dest_name_id, dest_type_id] =
-        semantics_ir.GetNode(dest_field_id).GetAsStructTypeField();
-    if (src_name_id != dest_name_id) {
+    auto src_field =
+        semantics_ir.GetNodeAs<SemIR::StructTypeField>(src_field_id);
+    auto dest_field =
+        semantics_ir.GetNodeAs<SemIR::StructTypeField>(dest_field_id);
+    if (src_field.name_id != dest_field.name_id) {
       CARBON_DIAGNOSTIC(
           StructInitFieldNameMismatch, Error,
           "Mismatched names for field {0} in struct initialization: "
           "source has field name `{1}`, destination has field name `{2}`.",
           size_t, llvm::StringRef, llvm::StringRef);
       context.emitter().Emit(value.parse_node(), StructInitFieldNameMismatch,
-                             i + 1, semantics_ir.GetString(src_name_id),
-                             semantics_ir.GetString(dest_name_id));
+                             i + 1, semantics_ir.GetString(src_field.name_id),
+                             semantics_ir.GetString(dest_field.name_id));
       return SemIR::NodeId::BuiltinError;
     }
 
     // TODO: This call recurses back into conversion. Switch to an iterative
     // approach.
-    auto init_id = ConvertAggregateElement<SemIR::Node::StructAccess,
-                                           SemIR::Node::StructAccess>(
-        context, value.parse_node(), value_id, src_type_id, literal_elems,
-        inner_kind, target.init_id, dest_type_id, target.init_block, i);
+    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,
+            target.init_block, i);
     if (init_id == SemIR::NodeId::BuiltinError) {
       return SemIR::NodeId::BuiltinError;
     }
     new_block.Set(i, init_id);
   }
 
-  return context.AddNode(
-      is_init
-          ? SemIR::Node::StructInit::Make(value.parse_node(), target.type_id,
-                                          value_id, new_block.id())
-          : SemIR::Node::StructValue::Make(value.parse_node(), target.type_id,
-                                           value_id, new_block.id()));
+  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(),
+                                                      target.type_id, value_id,
+                                                      new_block.id()));
 }
 
 // Returns whether `category` is a valid expression category to produce as a
@@ -544,11 +547,11 @@ static auto PerformBuiltinConversion(Context& context, Parse::Node parse_node,
 
   // A tuple (T1, T2, ..., Tn) converts to (U1, U2, ..., Un) if each Ti
   // converts to Ui.
-  if (target_type_node.kind() == SemIR::NodeKind::TupleType) {
+  if (auto target_tuple_type = target_type_node.TryAs<SemIR::TupleType>()) {
     auto value_type_node = semantics_ir.GetNode(
         semantics_ir.GetTypeAllowBuiltinTypes(value_type_id));
-    if (value_type_node.kind() == SemIR::NodeKind::TupleType) {
-      return ConvertTupleToTuple(context, value_type_node, target_type_node,
+    if (auto src_tuple_type = value_type_node.TryAs<SemIR::TupleType>()) {
+      return ConvertTupleToTuple(context, *src_tuple_type, *target_tuple_type,
                                  value_id, target);
     }
   }
@@ -557,21 +560,21 @@ static auto PerformBuiltinConversion(Context& context, Parse::Node parse_node,
   // {.f_p(1): U_p(1), .f_p(2): U_p(2), ..., .f_p(n): U_p(n)} if
   // (p(1), ..., p(n)) is a permutation of (1, ..., n) and each Ti converts
   // to Ui.
-  if (target_type_node.kind() == SemIR::NodeKind::StructType) {
+  if (auto target_struct_type = target_type_node.TryAs<SemIR::StructType>()) {
     auto value_type_node = semantics_ir.GetNode(
         semantics_ir.GetTypeAllowBuiltinTypes(value_type_id));
-    if (value_type_node.kind() == SemIR::NodeKind::StructType) {
-      return ConvertStructToStruct(context, value_type_node, target_type_node,
-                                   value_id, target);
+    if (auto src_struct_type = value_type_node.TryAs<SemIR::StructType>()) {
+      return ConvertStructToStruct(context, *src_struct_type,
+                                   *target_struct_type, value_id, target);
     }
   }
 
   // A tuple (T1, T2, ..., Tn) converts to [T; n] if each Ti converts to T.
-  if (target_type_node.kind() == SemIR::NodeKind::ArrayType) {
+  if (auto target_array_type = target_type_node.TryAs<SemIR::ArrayType>()) {
     auto value_type_node = semantics_ir.GetNode(
         semantics_ir.GetTypeAllowBuiltinTypes(value_type_id));
-    if (value_type_node.kind() == SemIR::NodeKind::TupleType) {
-      return ConvertTupleToArray(context, value_type_node, target_type_node,
+    if (auto src_tuple_type = value_type_node.TryAs<SemIR::TupleType>()) {
+      return ConvertTupleToArray(context, *src_tuple_type, *target_array_type,
                                  value_id, target);
     }
   }
@@ -579,18 +582,14 @@ static auto PerformBuiltinConversion(Context& context, Parse::Node parse_node,
   if (target.type_id == SemIR::TypeId::TypeType) {
     // A tuple of types converts to type `type`.
     // TODO: This should apply even for non-literal tuples.
-    if (value.kind() == SemIR::NodeKind::TupleLiteral) {
-      auto tuple_block_id = value.GetAsTupleLiteral();
+    if (auto tuple_literal = value.TryAs<SemIR::TupleLiteral>()) {
       llvm::SmallVector<SemIR::TypeId> type_ids;
-      // If it is empty tuple type, we don't fetch anything.
-      if (tuple_block_id != SemIR::NodeBlockId::Empty) {
-        const auto& tuple_block = semantics_ir.GetNodeBlock(tuple_block_id);
-        for (auto tuple_node_id : tuple_block) {
-          // TODO: This call recurses back into conversion. Switch to an
-          // iterative approach.
-          type_ids.push_back(
-              ExpressionAsType(context, parse_node, tuple_node_id));
-        }
+      for (auto tuple_node_id :
+           semantics_ir.GetNodeBlock(tuple_literal->elements_id)) {
+        // TODO: This call recurses back into conversion. Switch to an
+        // iterative approach.
+        type_ids.push_back(
+            ExpressionAsType(context, parse_node, tuple_node_id));
       }
       auto tuple_type_id =
           context.CanonicalizeTupleType(parse_node, std::move(type_ids));
@@ -600,8 +599,9 @@ static auto PerformBuiltinConversion(Context& context, Parse::Node parse_node,
     // `{}` converts to `{} as type`.
     // TODO: This conversion should also be performed for a non-literal value
     // of type `{}`.
-    if (value.kind() == SemIR::NodeKind::StructLiteral &&
-        value.GetAsStructLiteral() == SemIR::NodeBlockId::Empty) {
+    if (auto struct_literal = value.TryAs<SemIR::StructLiteral>();
+        struct_literal &&
+        struct_literal->elements_id == SemIR::NodeBlockId::Empty) {
       value_id = semantics_ir.GetTypeAllowBuiltinTypes(value_type_id);
     }
   }
@@ -692,8 +692,8 @@ auto Convert(Context& context, Parse::Node parse_node, SemIR::NodeId expr_id,
       if (target.kind != ConversionTarget::ValueOrReference &&
           target.kind != ConversionTarget::Discarded) {
         // TODO: Support types with custom value representations.
-        expr_id = context.AddNode(SemIR::Node::BindValue::Make(
-            expr.parse_node(), expr.type_id(), expr_id));
+        expr_id = context.AddNode(
+            SemIR::BindValue(expr.parse_node(), expr.type_id(), expr_id));
       }
       break;
     }
@@ -708,7 +708,7 @@ 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::Node::InitializeFrom::Make(
+      expr_id = context.AddNode(SemIR::InitializeFrom(
           parse_node, target.type_id, expr_id, target.init_id));
     }
   }

+ 2 - 1
toolchain/check/declaration_name_stack.cpp

@@ -120,7 +120,8 @@ auto DeclarationNameStack::UpdateScopeIfNeeded(NameContext& name_context)
   switch (resolved_node.kind()) {
     case SemIR::NodeKind::Namespace:
       name_context.state = NameContext::State::Resolved;
-      name_context.target_scope_id = resolved_node.GetAsNamespace();
+      name_context.target_scope_id =
+          resolved_node.As<SemIR::Namespace>().name_scope_id;
       break;
     default:
       name_context.state = NameContext::State::ResolvedNonScope;

+ 4 - 4
toolchain/check/handle_array.cpp

@@ -35,14 +35,14 @@ auto HandleArrayExpression(Context& context, Parse::Node parse_node) -> bool {
       .PopAndDiscardSoloParseNode<Parse::NodeKind::ArrayExpressionSemi>();
   auto element_type_node_id = context.node_stack().PopExpression();
   auto bound_node = context.semantics_ir().GetNode(bound_node_id);
-  if (bound_node.kind() == SemIR::NodeKind::IntegerLiteral) {
-    auto bound_value = context.semantics_ir().GetIntegerLiteral(
-        bound_node.GetAsIntegerLiteral());
+  if (auto literal = bound_node.TryAs<SemIR::IntegerLiteral>()) {
+    const auto& bound_value =
+        context.semantics_ir().GetInteger(literal->integer_id);
     // TODO: Produce an error if the array type is too large.
     if (bound_value.getActiveBits() <= 64) {
       context.AddNodeAndPush(
           parse_node,
-          SemIR::Node::ArrayType::Make(
+          SemIR::ArrayType(
               parse_node, SemIR::TypeId::TypeType, bound_node_id,
               ExpressionAsType(context, parse_node, element_type_node_id)));
       return true;

+ 7 - 6
toolchain/check/handle_call_expression.cpp

@@ -19,7 +19,8 @@ auto HandleCallExpression(Context& context, Parse::Node parse_node) -> bool {
           .PopWithParseNode<Parse::NodeKind::CallExpressionStart>();
   auto name_node =
       context.semantics_ir().GetNode(context.FollowNameReferences(name_id));
-  if (name_node.kind() != SemIR::NodeKind::FunctionDeclaration) {
+  auto function_name = name_node.TryAs<SemIR::FunctionDeclaration>();
+  if (!function_name) {
     // TODO: Work on error.
     context.TODO(parse_node, "Not a callable name");
     context.node_stack().Push(parse_node, name_id);
@@ -27,7 +28,7 @@ auto HandleCallExpression(Context& context, Parse::Node parse_node) -> bool {
     return true;
   }
 
-  auto function_id = name_node.GetAsFunctionDeclaration();
+  auto function_id = function_name->function_id;
   const auto& callable = context.semantics_ir().GetFunction(function_id);
 
   // For functions with an implicit return type, the return type is the empty
@@ -41,8 +42,8 @@ auto HandleCallExpression(Context& context, Parse::Node parse_node) -> bool {
   if (callable.return_slot_id.is_valid()) {
     // 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::Node::TemporaryStorage::Make(
-        call_expr_parse_node, callable.return_type_id));
+    auto temp_id = context.AddNode(
+        SemIR::TemporaryStorage(call_expr_parse_node, callable.return_type_id));
     context.ParamOrArgSave(temp_id);
   }
 
@@ -55,8 +56,8 @@ auto HandleCallExpression(Context& context, Parse::Node parse_node) -> bool {
     return true;
   }
 
-  auto call_node_id = context.AddNode(SemIR::Node::Call::Make(
-      call_expr_parse_node, type_id, refs_id, function_id));
+  auto call_node_id = context.AddNode(
+      SemIR::Call(call_expr_parse_node, type_id, refs_id, function_id));
 
   context.node_stack().Push(parse_node, call_node_id);
   return true;

+ 8 - 9
toolchain/check/handle_function.cpp

@@ -57,8 +57,8 @@ static auto BuildFunctionDeclaration(Context& context)
        .return_type_id = return_type_id,
        .return_slot_id = return_slot_id,
        .body_block_ids = {}});
-  auto decl_id = context.AddNode(
-      SemIR::Node::FunctionDeclaration::Make(fn_node, function_id));
+  auto decl_id =
+      context.AddNode(SemIR::FunctionDeclaration(fn_node, function_id));
   context.declaration_name_stack().AddNameToLookup(name_context, decl_id);
 
   if (SemIR::IsEntryPoint(context.semantics_ir(), function_id)) {
@@ -100,7 +100,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::Node::Return::Make(parse_node));
+      context.AddNode(SemIR::Return(parse_node));
     }
   }
 
@@ -123,11 +123,10 @@ auto HandleFunctionDefinitionStart(Context& context, Parse::Node parse_node)
   context.AddCurrentCodeBlockToFunction();
 
   // Bring the parameters into scope.
-  for (auto ref_id :
+  for (auto param_id :
        context.semantics_ir().GetNodeBlock(function.param_refs_id)) {
-    auto ref = context.semantics_ir().GetNode(ref_id);
-    auto name_id = ref.GetAsParameter();
-    context.AddNameToLookup(ref.parse_node(), name_id, ref_id);
+    auto param = context.semantics_ir().GetNodeAs<SemIR::Parameter>(param_id);
+    context.AddNameToLookup(param.parse_node, param.name_id, param_id);
   }
 
   context.node_stack().Push(parse_node, function_id);
@@ -154,8 +153,8 @@ auto HandleReturnType(Context& context, Parse::Node parse_node) -> bool {
   // TODO: Use a dedicated node rather than VarStorage here.
   context.AddNodeAndPush(
       parse_node,
-      SemIR::Node::VarStorage::Make(
-          parse_node, type_id, context.semantics_ir().AddString("return")));
+      SemIR::VarStorage(parse_node, type_id,
+                        context.semantics_ir().AddString("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::Node::Branch::Make(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;

+ 21 - 19
toolchain/check/handle_index.cpp

@@ -21,10 +21,10 @@ auto HandleIndexExpressionStart(Context& /*context*/,
 static auto ValidateIntegerLiteralBound(Context& context,
                                         Parse::Node parse_node,
                                         SemIR::Node operand_node,
-                                        SemIR::Node index_node, int size)
-    -> const llvm::APInt* {
-  const auto& index_val = context.semantics_ir().GetIntegerLiteral(
-      index_node.GetAsIntegerLiteral());
+                                        SemIR::IntegerLiteral index_node,
+                                        int size) -> const llvm::APInt* {
+  const auto& index_val =
+      context.semantics_ir().GetInteger(index_node.integer_id);
   if (index_val.uge(size)) {
     CARBON_DIAGNOSTIC(IndexOutOfBounds, Error,
                       "Index `{0}` is past the end of `{1}`.", llvm::APSInt,
@@ -51,13 +51,14 @@ auto HandleIndexExpression(Context& context, Parse::Node parse_node) -> bool {
 
   switch (operand_type_node.kind()) {
     case SemIR::NodeKind::ArrayType: {
-      auto [bound_id, element_type_id] = operand_type_node.GetAsArrayType();
+      auto array_type = operand_type_node.As<SemIR::ArrayType>();
       // We can check whether integers are in-bounds, although it doesn't affect
       // the IR for an array.
-      if (index_node.kind() == SemIR::NodeKind::IntegerLiteral &&
+      if (auto index_literal = index_node.TryAs<SemIR::IntegerLiteral>();
+          index_literal &&
           !ValidateIntegerLiteralBound(
-              context, parse_node, operand_node, index_node,
-              context.semantics_ir().GetArrayBoundValue(bound_id))) {
+              context, parse_node, operand_node, *index_literal,
+              context.semantics_ir().GetArrayBoundValue(array_type.bound_id))) {
         index_node_id = SemIR::NodeId::BuiltinError;
       }
       auto cast_index_id = ConvertToValueOfType(
@@ -68,11 +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::Node::ValueAsReference::Make(
+        operand_node_id = context.AddNode(SemIR::ValueAsReference(
             parse_node, operand_type_id, operand_node_id));
       }
-      auto elem_id = context.AddNode(SemIR::Node::ArrayIndex::Make(
-          parse_node, element_type_id, operand_node_id, cast_index_id));
+      auto elem_id = context.AddNode(
+          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.
@@ -85,12 +87,12 @@ auto HandleIndexExpression(Context& context, Parse::Node parse_node) -> bool {
     }
     case SemIR::NodeKind::TupleType: {
       SemIR::TypeId element_type_id = SemIR::TypeId::Error;
-      if (index_node.kind() == SemIR::NodeKind::IntegerLiteral) {
+      if (auto index_literal = index_node.TryAs<SemIR::IntegerLiteral>()) {
         auto type_block = context.semantics_ir().GetTypeBlock(
-            operand_type_node.GetAsTupleType());
-        if (const auto* index_val =
-                ValidateIntegerLiteralBound(context, parse_node, operand_node,
-                                            index_node, type_block.size())) {
+            operand_type_node.As<SemIR::TupleType>().elements_id);
+        if (const auto* index_val = ValidateIntegerLiteralBound(
+                context, parse_node, operand_node, *index_literal,
+                type_block.size())) {
           element_type_id = type_block[index_val->getZExtValue()];
         } else {
           index_node_id = SemIR::NodeId::BuiltinError;
@@ -101,9 +103,9 @@ auto HandleIndexExpression(Context& context, Parse::Node parse_node) -> bool {
         context.emitter().Emit(parse_node, TupleIndexIntegerLiteral);
         index_node_id = SemIR::NodeId::BuiltinError;
       }
-      context.AddNodeAndPush(parse_node, SemIR::Node::TupleIndex::Make(
-                                             parse_node, element_type_id,
-                                             operand_node_id, index_node_id));
+      context.AddNodeAndPush(parse_node,
+                             SemIR::TupleIndex(parse_node, element_type_id,
+                                               operand_node_id, index_node_id));
       return true;
     }
     default: {

+ 6 - 7
toolchain/check/handle_let.cpp

@@ -22,17 +22,16 @@ auto HandleLetDeclaration(Context& context, Parse::Node parse_node) -> bool {
 
   // Update the binding with its value and add it to the current block, after
   // the computation of the value.
-  auto [name_id, absent_value_id] = pattern.GetAsBindName();
-  CARBON_CHECK(!absent_value_id.is_valid())
+  // TODO: Support other kinds of pattern here.
+  auto bind_name = pattern.As<SemIR::BindName>();
+  CARBON_CHECK(!bind_name.value_id.is_valid())
       << "Binding should not already have a value!";
-  context.semantics_ir().ReplaceNode(
-      pattern_id,
-      SemIR::Node::BindName::Make(pattern.parse_node(), pattern.type_id(),
-                                  name_id, value_id));
+  bind_name.value_id = value_id;
+  context.semantics_ir().ReplaceNode(pattern_id, bind_name);
   context.node_block_stack().AddNodeId(pattern_id);
 
   // Add the name of the binding to the current scope.
-  context.AddNameToLookup(pattern.parse_node(), name_id, pattern_id);
+  context.AddNameToLookup(pattern.parse_node(), bind_name.name_id, pattern_id);
   return true;
 }
 

+ 6 - 6
toolchain/check/handle_literal.cpp

@@ -13,7 +13,7 @@ auto HandleLiteral(Context& context, Parse::Node parse_node) -> bool {
     case Lex::TokenKind::True: {
       context.AddNodeAndPush(
           parse_node,
-          SemIR::Node::BoolLiteral::Make(
+          SemIR::BoolLiteral(
               parse_node,
               context.CanonicalizeType(SemIR::NodeId::BuiltinBoolType),
               token_kind == Lex::TokenKind::True ? SemIR::BoolValue::True
@@ -21,24 +21,24 @@ auto HandleLiteral(Context& context, Parse::Node parse_node) -> bool {
       break;
     }
     case Lex::TokenKind::IntegerLiteral: {
-      auto id = context.semantics_ir().AddIntegerLiteral(
+      auto id = context.semantics_ir().AddInteger(
           context.tokens().GetIntegerLiteral(token));
       context.AddNodeAndPush(
           parse_node,
-          SemIR::Node::IntegerLiteral::Make(
+          SemIR::IntegerLiteral(
               parse_node,
               context.CanonicalizeType(SemIR::NodeId::BuiltinIntegerType), id));
       break;
     }
     case Lex::TokenKind::RealLiteral: {
       auto token_value = context.tokens().GetRealLiteral(token);
-      auto id = context.semantics_ir().AddRealLiteral(
+      auto id = context.semantics_ir().AddReal(
           {.mantissa = token_value.mantissa,
            .exponent = token_value.exponent,
            .is_decimal = token_value.is_decimal});
       context.AddNodeAndPush(
           parse_node,
-          SemIR::Node::RealLiteral::Make(
+          SemIR::RealLiteral(
               parse_node,
               context.CanonicalizeType(SemIR::NodeId::BuiltinFloatingPointType),
               id));
@@ -49,7 +49,7 @@ auto HandleLiteral(Context& context, Parse::Node parse_node) -> bool {
           context.tokens().GetStringLiteral(token));
       context.AddNodeAndPush(
           parse_node,
-          SemIR::Node::StringLiteral::Make(
+          SemIR::StringLiteral(
               parse_node,
               context.CanonicalizeType(SemIR::NodeId::BuiltinStringType), id));
       break;

+ 14 - 15
toolchain/check/handle_name.cpp

@@ -17,10 +17,10 @@ auto HandleMemberAccessExpression(Context& context, Parse::Node parse_node)
 
   auto base =
       context.semantics_ir().GetNode(context.FollowNameReferences(base_id));
-  if (base.kind() == SemIR::NodeKind::Namespace) {
+  if (auto namespc = base.TryAs<SemIR::Namespace>()) {
     // For a namespace, just resolve the name.
     auto node_id =
-        context.LookupName(parse_node, name_id, base.GetAsNamespace(),
+        context.LookupName(parse_node, name_id, namespc->name_scope_id,
                            /*print_diagnostics=*/true);
     context.node_stack().Push(parse_node, node_id);
     return true;
@@ -34,16 +34,15 @@ auto HandleMemberAccessExpression(Context& context, Parse::Node parse_node)
 
   switch (base_type.kind()) {
     case SemIR::NodeKind::StructType: {
-      auto refs =
-          context.semantics_ir().GetNodeBlock(base_type.GetAsStructType());
+      auto refs = context.semantics_ir().GetNodeBlock(
+          base_type.As<SemIR::StructType>().fields_id);
       // TODO: Do we need to optimize this with a lookup table for O(1)?
       for (auto [i, ref_id] : llvm::enumerate(refs)) {
-        auto ref = context.semantics_ir().GetNode(ref_id);
-        if (auto [field_name_id, field_type_id] = ref.GetAsStructTypeField();
-            name_id == field_name_id) {
+        auto field =
+            context.semantics_ir().GetNodeAs<SemIR::StructTypeField>(ref_id);
+        if (name_id == field.name_id) {
           context.AddNodeAndPush(
-              parse_node,
-              SemIR::Node::StructAccess::Make(parse_node, field_type_id,
+              parse_node, SemIR::StructAccess(parse_node, field.type_id,
                                               base_id, SemIR::MemberIndex(i)));
           return true;
         }
@@ -97,15 +96,15 @@ auto HandleNameExpression(Context& context, Parse::Node parse_node) -> bool {
   auto value = context.semantics_ir().GetNode(value_id);
   if (value.kind().value_kind() == SemIR::NodeValueKind::Typed) {
     // This is a reference to a name binding that has a value and a type.
-    context.AddNodeAndPush(parse_node,
-                           SemIR::Node::NameReference::Make(
-                               parse_node, value.type_id(), name_id, value_id));
+    context.AddNodeAndPush(
+        parse_node,
+        SemIR::NameReference(parse_node, value.type_id(), name_id, value_id));
   } else {
     // This is something like a namespace name, that can be found by name lookup
     // but isn't a first-class value with a type.
-    context.AddNodeAndPush(parse_node,
-                           SemIR::Node::NameReferenceUntyped::Make(
-                               parse_node, value.type_id(), name_id, value_id));
+    context.AddNodeAndPush(
+        parse_node, SemIR::NameReferenceUntyped(parse_node, value.type_id(),
+                                                name_id, value_id));
   }
   return true;
 }

+ 2 - 2
toolchain/check/handle_namespace.cpp

@@ -15,8 +15,8 @@ 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::Node::Namespace::Make(
-      parse_node, context.semantics_ir().AddNameScope()));
+  auto namespace_id = context.AddNode(
+      SemIR::Namespace(parse_node, context.semantics_ir().AddNameScope()));
   context.declaration_name_stack().AddNameToLookup(name_context, namespace_id);
   return true;
 }

+ 21 - 22
toolchain/check/handle_operator.cpp

@@ -24,7 +24,7 @@ auto HandleInfixOperator(Context& context, Parse::Node parse_node) -> bool {
 
       context.AddNodeAndPush(
           parse_node,
-          SemIR::Node::BinaryOperatorAdd::Make(
+          SemIR::BinaryOperatorAdd(
               parse_node, context.semantics_ir().GetNode(lhs_id).type_id(),
               lhs_id, rhs_id));
       return true;
@@ -39,17 +39,17 @@ auto HandleInfixOperator(Context& context, Parse::Node parse_node) -> bool {
       // When the second operand is evaluated, the result of `and` and `or` is
       // its value.
       auto resume_block_id = context.node_block_stack().PeekOrAdd(/*depth=*/1);
-      context.AddNode(SemIR::Node::BranchWithArg::Make(
-          parse_node, resume_block_id, rhs_id));
+      context.AddNode(
+          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::Node::BlockArg::Make(
-              parse_node, context.semantics_ir().GetNode(rhs_id).type_id(),
-              resume_block_id));
+          SemIR::BlockArg(parse_node,
+                          context.semantics_ir().GetNode(rhs_id).type_id(),
+                          resume_block_id));
       return true;
     }
     case Lex::TokenKind::Equal: {
@@ -63,7 +63,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::Node::Assign::Make(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
@@ -85,8 +85,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::Node::PointerType::Make(
-                          parse_node, SemIR::TypeId::TypeType, inner_type_id));
+          parse_node, SemIR::PointerType(parse_node, SemIR::TypeId::TypeType,
+                                         inner_type_id));
       return true;
     }
 
@@ -120,7 +120,7 @@ auto HandlePrefixOperator(Context& context, Parse::Node parse_node) -> bool {
       }
       context.AddNodeAndPush(
           parse_node,
-          SemIR::Node::AddressOf::Make(
+          SemIR::AddressOf(
               parse_node,
               context.GetPointerType(
                   parse_node,
@@ -142,8 +142,8 @@ auto HandlePrefixOperator(Context& context, Parse::Node parse_node) -> bool {
       }
       auto inner_type_id = ExpressionAsType(context, parse_node, value_id);
       context.AddNodeAndPush(
-          parse_node, SemIR::Node::ConstType::Make(
-                          parse_node, SemIR::TypeId::TypeType, inner_type_id));
+          parse_node,
+          SemIR::ConstType(parse_node, SemIR::TypeId::TypeType, inner_type_id));
       return true;
     }
 
@@ -151,7 +151,7 @@ auto HandlePrefixOperator(Context& context, Parse::Node parse_node) -> bool {
       value_id = ConvertToBoolValue(context, parse_node, value_id);
       context.AddNodeAndPush(
           parse_node,
-          SemIR::Node::UnaryOperatorNot::Make(
+          SemIR::UnaryOperatorNot(
               parse_node, context.semantics_ir().GetNode(value_id).type_id(),
               value_id));
       return true;
@@ -162,8 +162,8 @@ auto HandlePrefixOperator(Context& context, Parse::Node parse_node) -> bool {
       auto type_node = context.semantics_ir().GetNode(
           context.semantics_ir().GetTypeAllowBuiltinTypes(type_id));
       auto result_type_id = SemIR::TypeId::Error;
-      if (type_node.kind() == SemIR::NodeKind::PointerType) {
-        result_type_id = type_node.GetAsPointerType();
+      if (auto pointer_type = type_node.TryAs<SemIR::PointerType>()) {
+        result_type_id = pointer_type->pointee_id;
       } else {
         CARBON_DIAGNOSTIC(
             DereferenceOfNonPointer, Error,
@@ -183,8 +183,7 @@ auto HandlePrefixOperator(Context& context, Parse::Node parse_node) -> bool {
       }
       value_id = ConvertToValueExpression(context, value_id);
       context.AddNodeAndPush(
-          parse_node,
-          SemIR::Node::Dereference::Make(parse_node, result_type_id, value_id));
+          parse_node, SemIR::Dereference(parse_node, result_type_id, value_id));
       return true;
     }
 
@@ -207,15 +206,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::Node::BoolLiteral::Make(
+      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::Node::UnaryOperatorNot::Make(
-          parse_node, bool_type_id, cond_value_id));
-      short_circuit_result_id = context.AddNode(SemIR::Node::BoolLiteral::Make(
-          parse_node, bool_type_id, SemIR::BoolValue::True));
+      branch_value_id = context.AddNode(
+          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));
       break;
 
     default:

+ 2 - 2
toolchain/check/handle_paren.cpp

@@ -48,8 +48,8 @@ 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::Node::TupleLiteral::Make(parse_node, type_id, refs_id));
+  auto value_id =
+      context.AddNode(SemIR::TupleLiteral(parse_node, type_id, refs_id));
   context.node_stack().Push(parse_node, value_id);
   return true;
 }

+ 5 - 5
toolchain/check/handle_pattern_binding.cpp

@@ -32,12 +32,12 @@ auto HandlePatternBinding(Context& context, Parse::Node parse_node) -> bool {
   switch (auto context_parse_node_kind = context.parse_tree().node_kind(
               context.node_stack().PeekParseNode())) {
     case Parse::NodeKind::VariableIntroducer:
-      context.AddNodeAndPush(parse_node, SemIR::Node::VarStorage::Make(
-                                             name_node, cast_type_id, name_id));
+      context.AddNodeAndPush(
+          parse_node, SemIR::VarStorage(name_node, cast_type_id, name_id));
       break;
     case Parse::NodeKind::ParameterListStart:
-      context.AddNodeAndPush(parse_node, SemIR::Node::Parameter::Make(
-                                             name_node, cast_type_id, name_id));
+      context.AddNodeAndPush(
+          parse_node, SemIR::Parameter(name_node, cast_type_id, name_id));
       break;
     case Parse::NodeKind::LetIntroducer:
       // Create the node, but don't add it to a block until after we've formed
@@ -46,7 +46,7 @@ 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::Node::BindName::Make(
+          context.semantics_ir().AddNodeInNoBlock(SemIR::BindName(
               name_node, cast_type_id, name_id, SemIR::NodeId::Invalid)));
       break;
     default:

+ 6 - 6
toolchain/check/handle_statement.cpp

@@ -29,10 +29,10 @@ auto HandleExpressionStatement(Context& context, Parse::Node /*parse_node*/)
 
 auto HandleReturnStatement(Context& context, Parse::Node parse_node) -> bool {
   CARBON_CHECK(!context.return_scope_stack().empty());
-  const auto& fn_node =
-      context.semantics_ir().GetNode(context.return_scope_stack().back());
+  auto fn_node = context.semantics_ir().GetNodeAs<SemIR::FunctionDeclaration>(
+      context.return_scope_stack().back());
   const auto& callable =
-      context.semantics_ir().GetFunction(fn_node.GetAsFunctionDeclaration());
+      context.semantics_ir().GetFunction(fn_node.function_id);
 
   if (context.parse_tree().node_kind(context.node_stack().PeekParseNode()) ==
       Parse::NodeKind::ReturnStatementStart) {
@@ -49,7 +49,7 @@ auto HandleReturnStatement(Context& context, Parse::Node parse_node) -> bool {
           .Emit();
     }
 
-    context.AddNode(SemIR::Node::Return::Make(parse_node));
+    context.AddNode(SemIR::Return(parse_node));
   } else {
     auto arg = context.node_stack().PopExpression();
     context.node_stack()
@@ -63,7 +63,7 @@ auto HandleReturnStatement(Context& context, Parse::Node parse_node) -> bool {
                         "There was no return type provided.");
       context.emitter()
           .Build(parse_node, ReturnStatementDisallowExpression)
-          .Note(fn_node.parse_node(), ReturnStatementImplicitNote)
+          .Note(fn_node.parse_node, ReturnStatementImplicitNote)
           .Emit();
     } else if (callable.return_slot_id.is_valid()) {
       arg = Initialize(context, parse_node, callable.return_slot_id, arg);
@@ -72,7 +72,7 @@ auto HandleReturnStatement(Context& context, Parse::Node parse_node) -> bool {
                                  callable.return_type_id);
     }
 
-    context.AddNode(SemIR::Node::ReturnExpression::Make(parse_node, arg));
+    context.AddNode(SemIR::ReturnExpression(parse_node, arg));
   }
 
   // Switch to a new, unreachable, empty node block. This typically won't

+ 8 - 8
toolchain/check/handle_struct.cpp

@@ -28,8 +28,8 @@ auto HandleStructFieldType(Context& context, Parse::Node parse_node) -> bool {
   auto [name_node, name_id] =
       context.node_stack().PopWithParseNode<Parse::NodeKind::Name>();
 
-  context.AddNodeAndPush(parse_node, SemIR::Node::StructTypeField::Make(
-                                         name_node, name_id, cast_type_id));
+  context.AddNodeAndPush(
+      parse_node, SemIR::StructTypeField(name_node, name_id, cast_type_id));
   return true;
 }
 
@@ -44,7 +44,7 @@ auto HandleStructFieldValue(Context& context, Parse::Node parse_node) -> bool {
   SemIR::StringId name_id = context.node_stack().Pop<Parse::NodeKind::Name>();
 
   // Store the name for the type.
-  context.args_type_info_stack().AddNode(SemIR::Node::StructTypeField::Make(
+  context.args_type_info_stack().AddNode(SemIR::StructTypeField(
       parse_node, name_id,
       context.semantics_ir().GetNode(value_node_id).type_id()));
 
@@ -65,8 +65,8 @@ 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::Node::StructLiteral::Make(parse_node, type_id, refs_id));
+  auto value_id =
+      context.AddNode(SemIR::StructLiteral(parse_node, type_id, refs_id));
   context.node_stack().Push(parse_node, value_id);
   return true;
 }
@@ -98,9 +98,9 @@ auto HandleStructTypeLiteral(Context& context, Parse::Node parse_node) -> bool {
   CARBON_CHECK(refs_id != SemIR::NodeBlockId::Empty)
       << "{} is handled by StructLiteral.";
 
-  context.AddNodeAndPush(parse_node,
-                         SemIR::Node::StructType::Make(
-                             parse_node, SemIR::TypeId::TypeType, refs_id));
+  context.AddNodeAndPush(
+      parse_node,
+      SemIR::StructType(parse_node, SemIR::TypeId::TypeType, refs_id));
   return true;
 }
 

+ 3 - 4
toolchain/check/handle_variable.cpp

@@ -24,15 +24,14 @@ auto HandleVariableDeclaration(Context& context, Parse::Node parse_node)
   // Get the storage and add it to name lookup.
   SemIR::NodeId var_id =
       context.node_stack().Pop<Parse::NodeKind::PatternBinding>();
-  auto var = context.semantics_ir().GetNode(var_id);
-  auto name_id = var.GetAsVarStorage();
-  context.AddNameToLookup(var.parse_node(), name_id, var_id);
+  auto var = context.semantics_ir().GetNodeAs<SemIR::VarStorage>(var_id);
+  context.AddNameToLookup(var.parse_node, var.name_id, var_id);
   // If there was an initializer, assign it to storage.
   if (has_init) {
     init_id = Initialize(context, parse_node, var_id, init_id);
     // TODO: Consider using different node kinds for assignment versus
     // initialization.
-    context.AddNode(SemIR::Node::Assign::Make(parse_node, var_id, init_id));
+    context.AddNode(SemIR::Assign(parse_node, var_id, init_id));
   }
 
   context.node_stack()

+ 5 - 6
toolchain/check/pending_block.h

@@ -64,9 +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::Node::SpliceBlock::Make(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.
@@ -75,9 +74,9 @@ class PendingBlock {
       // 3) Anything else: splice it into the IR, replacing `target_id`.
       context_.semantics_ir().ReplaceNode(
           target_id,
-          SemIR::Node::SpliceBlock::Make(
-              value.parse_node(), value.type_id(),
-              context_.semantics_ir().AddNodeBlock(nodes_), value_id));
+          SemIR::SpliceBlock(value.parse_node(), value.type_id(),
+                             context_.semantics_ir().AddNodeBlock(nodes_),
+                             value_id));
     }
 
     // Prepare to stash more pending instructions.

+ 2 - 2
toolchain/check/testdata/basics/builtin_nodes.carbon

@@ -11,9 +11,9 @@
 // CHECK:STDOUT:   - cross_reference_irs_size: 1
 // CHECK:STDOUT:     functions: [
 // CHECK:STDOUT:     ]
-// CHECK:STDOUT:     integer_literals: [
+// CHECK:STDOUT:     integers: [
 // CHECK:STDOUT:     ]
-// CHECK:STDOUT:     real_literals: [
+// CHECK:STDOUT:     reals: [
 // CHECK:STDOUT:     ]
 // CHECK:STDOUT:     strings: [
 // CHECK:STDOUT:     ]

+ 4 - 4
toolchain/check/testdata/basics/multifile_raw_and_textual_ir.carbon

@@ -20,9 +20,9 @@ fn B() {}
 // CHECK:STDOUT:     functions: [
 // CHECK:STDOUT:       {name: str0, param_refs: block0, body: [block1]},
 // CHECK:STDOUT:     ]
-// CHECK:STDOUT:     integer_literals: [
+// CHECK:STDOUT:     integers: [
 // CHECK:STDOUT:     ]
-// CHECK:STDOUT:     real_literals: [
+// CHECK:STDOUT:     reals: [
 // CHECK:STDOUT:     ]
 // CHECK:STDOUT:     strings: [
 // CHECK:STDOUT:       A,
@@ -60,9 +60,9 @@ fn B() {}
 // CHECK:STDOUT:     functions: [
 // CHECK:STDOUT:       {name: str0, param_refs: block0, body: [block1]},
 // CHECK:STDOUT:     ]
-// CHECK:STDOUT:     integer_literals: [
+// CHECK:STDOUT:     integers: [
 // CHECK:STDOUT:     ]
-// CHECK:STDOUT:     real_literals: [
+// CHECK:STDOUT:     reals: [
 // CHECK:STDOUT:     ]
 // CHECK:STDOUT:     strings: [
 // CHECK:STDOUT:       B,

+ 4 - 4
toolchain/check/testdata/basics/multifile_raw_ir.carbon

@@ -20,9 +20,9 @@ fn B() {}
 // CHECK:STDOUT:     functions: [
 // CHECK:STDOUT:       {name: str0, param_refs: block0, body: [block1]},
 // CHECK:STDOUT:     ]
-// CHECK:STDOUT:     integer_literals: [
+// CHECK:STDOUT:     integers: [
 // CHECK:STDOUT:     ]
-// CHECK:STDOUT:     real_literals: [
+// CHECK:STDOUT:     reals: [
 // CHECK:STDOUT:     ]
 // CHECK:STDOUT:     strings: [
 // CHECK:STDOUT:       A,
@@ -51,9 +51,9 @@ fn B() {}
 // CHECK:STDOUT:     functions: [
 // CHECK:STDOUT:       {name: str0, param_refs: block0, body: [block1]},
 // CHECK:STDOUT:     ]
-// CHECK:STDOUT:     integer_literals: [
+// CHECK:STDOUT:     integers: [
 // CHECK:STDOUT:     ]
-// CHECK:STDOUT:     real_literals: [
+// CHECK:STDOUT:     reals: [
 // CHECK:STDOUT:     ]
 // CHECK:STDOUT:     strings: [
 // CHECK:STDOUT:       B,

+ 2 - 2
toolchain/check/testdata/basics/raw_and_textual_ir.carbon

@@ -18,10 +18,10 @@ fn Foo(n: i32) -> (i32, f64) {
 // CHECK:STDOUT:     functions: [
 // CHECK:STDOUT:       {name: str0, param_refs: block1, return_type: type3, return_slot: node+4, body: [block4]},
 // CHECK:STDOUT:     ]
-// CHECK:STDOUT:     integer_literals: [
+// CHECK:STDOUT:     integers: [
 // CHECK:STDOUT:       2,
 // CHECK:STDOUT:     ]
-// CHECK:STDOUT:     real_literals: [
+// CHECK:STDOUT:     reals: [
 // CHECK:STDOUT:       {mantissa: 34, exponent: -1, is_decimal: 1},
 // CHECK:STDOUT:     ]
 // CHECK:STDOUT:     strings: [

+ 2 - 2
toolchain/check/testdata/basics/raw_ir.carbon

@@ -18,10 +18,10 @@ fn Foo(n: i32) -> (i32, f64) {
 // CHECK:STDOUT:     functions: [
 // CHECK:STDOUT:       {name: str0, param_refs: block1, return_type: type3, return_slot: node+4, body: [block4]},
 // CHECK:STDOUT:     ]
-// CHECK:STDOUT:     integer_literals: [
+// CHECK:STDOUT:     integers: [
 // CHECK:STDOUT:       2,
 // CHECK:STDOUT:     ]
-// CHECK:STDOUT:     real_literals: [
+// CHECK:STDOUT:     reals: [
 // CHECK:STDOUT:       {mantissa: 34, exponent: -1, is_decimal: 1},
 // CHECK:STDOUT:     ]
 // CHECK:STDOUT:     strings: [

+ 18 - 16
toolchain/lower/file_context.cpp

@@ -132,7 +132,7 @@ auto FileContext::BuildFunctionDeclaration(SemIR::FunctionId function_id)
           llvm_context(), GetType(function.return_type_id)));
     } else {
       arg.setName(semantics_ir().GetString(
-          semantics_ir().GetNode(node_id).GetAsParameter()));
+          semantics_ir().GetNodeAs<SemIR::Parameter>(node_id).name_id));
     }
   }
 
@@ -208,26 +208,27 @@ auto FileContext::BuildType(SemIR::NodeId node_id) -> llvm::Type* {
   auto node = semantics_ir_->GetNode(node_id);
   switch (node.kind()) {
     case SemIR::NodeKind::ArrayType: {
-      auto [bound_node_id, type_id] = node.GetAsArrayType();
+      auto array_type = node.As<SemIR::ArrayType>();
       return llvm::ArrayType::get(
-          GetType(type_id), semantics_ir_->GetArrayBoundValue(bound_node_id));
+          GetType(array_type.element_type_id),
+          semantics_ir_->GetArrayBoundValue(array_type.bound_id));
     }
     case SemIR::NodeKind::ConstType:
-      return GetType(node.GetAsConstType());
+      return GetType(node.As<SemIR::ConstType>().inner_id);
     case SemIR::NodeKind::PointerType:
       return llvm::PointerType::get(*llvm_context_, /*AddressSpace=*/0);
     case SemIR::NodeKind::StructType: {
-      auto refs = semantics_ir_->GetNodeBlock(node.GetAsStructType());
+      auto fields =
+          semantics_ir_->GetNodeBlock(node.As<SemIR::StructType>().fields_id);
       llvm::SmallVector<llvm::Type*> subtypes;
-      subtypes.reserve(refs.size());
-      for (auto ref_id : refs) {
-        auto [field_name_id, field_type_id] =
-            semantics_ir_->GetNode(ref_id).GetAsStructTypeField();
+      subtypes.reserve(fields.size());
+      for (auto field_id : fields) {
+        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.type_id.index < SemIR::BuiltinKind::ValidCount)
+            << field.type_id;
+        subtypes.push_back(GetType(field.type_id));
       }
       return llvm::StructType::get(*llvm_context_, subtypes);
     }
@@ -236,11 +237,12 @@ auto FileContext::BuildType(SemIR::NodeId node_id) -> llvm::Type* {
       // can be collectively replaced with LLVM's void, particularly around
       // function returns. LLVM doesn't allow declaring variables with a void
       // type, so that may require significant special casing.
-      auto refs = semantics_ir_->GetTypeBlock(node.GetAsTupleType());
+      auto elements =
+          semantics_ir_->GetTypeBlock(node.As<SemIR::TupleType>().elements_id);
       llvm::SmallVector<llvm::Type*> subtypes;
-      subtypes.reserve(refs.size());
-      for (auto ref_id : refs) {
-        subtypes.push_back(GetType(ref_id));
+      subtypes.reserve(elements.size());
+      for (auto element_id : elements) {
+        subtypes.push_back(GetType(element_id));
       }
       return llvm::StructType::get(*llvm_context_, subtypes);
     }

+ 3 - 3
toolchain/lower/function_context.cpp

@@ -44,9 +44,9 @@ auto FunctionContext::LowerBlock(SemIR::NodeBlockId block_id) -> void {
     // clang warns on unhandled enum values; clang-tidy is incorrect here.
     // NOLINTNEXTLINE(bugprone-switch-missing-default-case)
     switch (node.kind()) {
-#define CARBON_SEMANTICS_NODE_KIND(Name) \
-  case SemIR::NodeKind::Name:            \
-    Handle##Name(*this, node_id, node);  \
+#define CARBON_SEMANTICS_NODE_KIND(Name)                  \
+  case SemIR::NodeKind::Name:                             \
+    Handle##Name(*this, node_id, node.As<SemIR::Name>()); \
     break;
 #include "toolchain/sem_ir/node_kind.def"
     }

+ 1 - 1
toolchain/lower/function_context.h

@@ -133,7 +133,7 @@ class FunctionContext {
 // Declare handlers for each SemIR::File node.
 #define CARBON_SEMANTICS_NODE_KIND(Name)                             \
   auto Handle##Name(FunctionContext& context, SemIR::NodeId node_id, \
-                    SemIR::Node node)                                \
+                    SemIR::Name node)                                \
       ->void;
 #include "toolchain/sem_ir/node_kind.def"
 

+ 132 - 154
toolchain/lower/handle.cpp

@@ -9,106 +9,95 @@
 
 namespace Carbon::Lower {
 
-auto HandleInvalid(FunctionContext& /*context*/, SemIR::NodeId /*node_id*/,
-                   SemIR::Node /*node*/) -> void {
-  llvm_unreachable("never in actual IR");
-}
-
 auto HandleCrossReference(FunctionContext& /*context*/,
-                          SemIR::NodeId /*node_id*/, SemIR::Node node) -> void {
+                          SemIR::NodeId /*node_id*/, SemIR::CrossReference node)
+    -> void {
   CARBON_FATAL() << "TODO: Add support: " << node;
 }
 
 auto HandleAddressOf(FunctionContext& context, SemIR::NodeId node_id,
-                     SemIR::Node node) -> void {
-  context.SetLocal(node_id, context.GetLocal(node.GetAsAddressOf()));
+                     SemIR::AddressOf node) -> void {
+  context.SetLocal(node_id, context.GetLocal(node.lvalue_id));
 }
 
 auto HandleArrayIndex(FunctionContext& context, SemIR::NodeId node_id,
-                      SemIR::Node node) -> void {
-  auto [array_node_id, index_node_id] = node.GetAsArrayIndex();
-  auto* array_value = context.GetLocal(array_node_id);
+                      SemIR::ArrayIndex node) -> void {
+  auto* array_value = context.GetLocal(node.array_id);
   auto* llvm_type =
-      context.GetType(context.semantics_ir().GetNode(array_node_id).type_id());
+      context.GetType(context.semantics_ir().GetNode(node.array_id).type_id());
   llvm::Value* indexes[2] = {
       llvm::ConstantInt::get(llvm::Type::getInt32Ty(context.llvm_context()), 0),
-      context.GetLocal(index_node_id)};
+      context.GetLocal(node.index_id)};
   context.SetLocal(node_id,
                    context.builder().CreateInBoundsGEP(llvm_type, array_value,
                                                        indexes, "array.index"));
 }
 
 auto HandleArrayInit(FunctionContext& context, SemIR::NodeId node_id,
-                     SemIR::Node node) -> void {
-  auto [src_id, refs_id] = node.GetAsArrayInit();
+                     SemIR::ArrayInit node) -> void {
   // The result of initialization is the return slot of the initializer.
   context.SetLocal(
-      node_id,
-      context.GetLocal(context.semantics_ir().GetNodeBlock(refs_id).back()));
+      node_id, context.GetLocal(context.semantics_ir()
+                                    .GetNodeBlock(node.inits_and_return_slot_id)
+                                    .back()));
 }
 
 auto HandleAssign(FunctionContext& context, SemIR::NodeId /*node_id*/,
-                  SemIR::Node node) -> void {
-  auto [storage_id, value_id] = node.GetAsAssign();
-  auto storage_type_id = context.semantics_ir().GetNode(storage_id).type_id();
-  context.FinishInitialization(storage_type_id, storage_id, value_id);
+                  SemIR::Assign node) -> void {
+  auto storage_type_id = context.semantics_ir().GetNode(node.lhs_id).type_id();
+  context.FinishInitialization(storage_type_id, node.lhs_id, node.rhs_id);
 }
 
 auto HandleBinaryOperatorAdd(FunctionContext& /*context*/,
-                             SemIR::NodeId /*node_id*/, SemIR::Node node)
-    -> void {
+                             SemIR::NodeId /*node_id*/,
+                             SemIR::BinaryOperatorAdd node) -> void {
   CARBON_FATAL() << "TODO: Add support: " << node;
 }
 
 auto HandleBindName(FunctionContext& context, SemIR::NodeId node_id,
-                    SemIR::Node node) -> void {
-  auto [name_id, value_id] = node.GetAsBindName();
-  context.SetLocal(node_id, context.GetLocal(value_id));
+                    SemIR::BindName node) -> void {
+  context.SetLocal(node_id, context.GetLocal(node.value_id));
 }
 
 auto HandleBlockArg(FunctionContext& context, SemIR::NodeId node_id,
-                    SemIR::Node node) -> void {
-  SemIR::NodeBlockId block_id = node.GetAsBlockArg();
-  context.SetLocal(node_id, context.GetBlockArg(block_id, node.type_id()));
+                    SemIR::BlockArg node) -> void {
+  context.SetLocal(node_id, context.GetBlockArg(node.block_id, node.type_id));
 }
 
 auto HandleBoolLiteral(FunctionContext& context, SemIR::NodeId node_id,
-                       SemIR::Node node) -> void {
-  llvm::Value* v = llvm::ConstantInt::get(context.builder().getInt1Ty(),
-                                          node.GetAsBoolLiteral().index);
+                       SemIR::BoolLiteral node) -> void {
+  llvm::Value* v =
+      llvm::ConstantInt::get(context.builder().getInt1Ty(), node.value.index);
   context.SetLocal(node_id, v);
 }
 
 auto HandleBranch(FunctionContext& context, SemIR::NodeId /*node_id*/,
-                  SemIR::Node node) -> void {
-  SemIR::NodeBlockId target_block_id = node.GetAsBranch();
-
+                  SemIR::Branch node) -> void {
   // Opportunistically avoid creating a BasicBlock that contains just a branch.
   llvm::BasicBlock* block = context.builder().GetInsertBlock();
-  if (block->empty() && context.TryToReuseBlock(target_block_id, block)) {
+  if (block->empty() && context.TryToReuseBlock(node.target_id, block)) {
     // Reuse this block as the branch target.
   } else {
-    context.builder().CreateBr(context.GetBlock(target_block_id));
+    context.builder().CreateBr(context.GetBlock(node.target_id));
   }
 
   context.builder().ClearInsertionPoint();
 }
 
 auto HandleBranchIf(FunctionContext& context, SemIR::NodeId /*node_id*/,
-                    SemIR::Node node) -> void {
-  auto [target_block_id, cond_id] = node.GetAsBranchIf();
-  llvm::Value* cond = context.GetLocal(cond_id);
-  llvm::BasicBlock* then_block = context.GetBlock(target_block_id);
+                    SemIR::BranchIf node) -> void {
+  llvm::Value* cond = context.GetLocal(node.cond_id);
+  llvm::BasicBlock* then_block = context.GetBlock(node.target_id);
   llvm::BasicBlock* else_block = context.CreateSyntheticBlock();
   context.builder().CreateCondBr(cond, then_block, else_block);
   context.builder().SetInsertPoint(else_block);
 }
 
 auto HandleBranchWithArg(FunctionContext& context, SemIR::NodeId /*node_id*/,
-                         SemIR::Node node) -> void {
-  auto [target_block_id, arg_id] = node.GetAsBranchWithArg();
-  llvm::Value* arg = context.GetLocal(arg_id);
-  SemIR::TypeId arg_type_id = context.semantics_ir().GetNode(arg_id).type_id();
+                         SemIR::BranchWithArg node) -> void {
+  llvm::Value* arg = context.GetLocal(node.arg_id);
+  SemIR::TypeId arg_type_id =
+      context.semantics_ir().GetNode(node.arg_id).type_id();
 
   // Opportunistically avoid creating a BasicBlock that contains just a branch.
   // We only do this for a block that we know will only have a single
@@ -117,45 +106,44 @@ auto HandleBranchWithArg(FunctionContext& context, SemIR::NodeId /*node_id*/,
   llvm::BasicBlock* block = context.builder().GetInsertBlock();
   llvm::BasicBlock* phi_predecessor = block;
   if (block->empty() && context.IsCurrentSyntheticBlock(block) &&
-      context.TryToReuseBlock(target_block_id, block)) {
+      context.TryToReuseBlock(node.target_id, block)) {
     // Reuse this block as the branch target.
     phi_predecessor = block->getSinglePredecessor();
     CARBON_CHECK(phi_predecessor)
         << "Synthetic block did not have a single predecessor";
   } else {
-    context.builder().CreateBr(context.GetBlock(target_block_id));
+    context.builder().CreateBr(context.GetBlock(node.target_id));
   }
 
-  context.GetBlockArg(target_block_id, arg_type_id)
+  context.GetBlockArg(node.target_id, arg_type_id)
       ->addIncoming(arg, phi_predecessor);
   context.builder().ClearInsertionPoint();
 }
 
 auto HandleBuiltin(FunctionContext& /*context*/, SemIR::NodeId /*node_id*/,
-                   SemIR::Node node) -> void {
+                   SemIR::Builtin node) -> void {
   CARBON_FATAL() << "TODO: Add support: " << node;
 }
 
 auto HandleCall(FunctionContext& context, SemIR::NodeId node_id,
-                SemIR::Node node) -> void {
-  auto [refs_id, function_id] = node.GetAsCall();
-  auto* llvm_function = context.GetFunction(function_id);
-  const auto& function = context.semantics_ir().GetFunction(function_id);
+                SemIR::Call node) -> void {
+  auto* llvm_function = context.GetFunction(node.function_id);
+  const auto& function = context.semantics_ir().GetFunction(node.function_id);
 
   std::vector<llvm::Value*> args;
   llvm::ArrayRef<SemIR::NodeId> arg_ids =
-      context.semantics_ir().GetNodeBlock(refs_id);
+      context.semantics_ir().GetNodeBlock(node.args_id);
 
   if (function.return_slot_id.is_valid()) {
     args.push_back(context.GetLocal(arg_ids.back()));
     arg_ids = arg_ids.drop_back();
   }
 
-  for (auto ref_id : arg_ids) {
-    auto arg_type_id = context.semantics_ir().GetNode(ref_id).type_id();
+  for (auto arg_id : arg_ids) {
+    auto arg_type_id = context.semantics_ir().GetNode(arg_id).type_id();
     if (SemIR::GetValueRepresentation(context.semantics_ir(), arg_type_id)
             .kind != SemIR::ValueRepresentation::None) {
-      args.push_back(context.GetLocal(ref_id));
+      args.push_back(context.GetLocal(arg_id));
     }
   }
 
@@ -165,7 +153,7 @@ auto HandleCall(FunctionContext& context, SemIR::NodeId node_id,
     // StubReference needs a value to propagate.
     // TODO: Remove this now the StubReferences are gone.
     context.SetLocal(node_id,
-                     llvm::PoisonValue::get(context.GetType(node.type_id())));
+                     llvm::PoisonValue::get(context.GetType(node.type_id)));
   } else {
     context.SetLocal(node_id,
                      context.builder().CreateCall(llvm_function, args,
@@ -174,13 +162,13 @@ auto HandleCall(FunctionContext& context, SemIR::NodeId node_id,
 }
 
 auto HandleDereference(FunctionContext& context, SemIR::NodeId node_id,
-                       SemIR::Node node) -> void {
-  context.SetLocal(node_id, context.GetLocal(node.GetAsDereference()));
+                       SemIR::Dereference node) -> void {
+  context.SetLocal(node_id, context.GetLocal(node.pointer_id));
 }
 
 auto HandleFunctionDeclaration(FunctionContext& /*context*/,
-                               SemIR::NodeId /*node_id*/, SemIR::Node node)
-    -> void {
+                               SemIR::NodeId /*node_id*/,
+                               SemIR::FunctionDeclaration node) -> void {
   CARBON_FATAL()
       << "Should not be encountered. If that changes, we may want to change "
          "higher-level logic to skip them rather than calling this. "
@@ -188,16 +176,14 @@ auto HandleFunctionDeclaration(FunctionContext& /*context*/,
 }
 
 auto HandleInitializeFrom(FunctionContext& context, SemIR::NodeId /*node_id*/,
-                          SemIR::Node node) -> void {
-  auto [init_id, storage_id] = node.GetAsInitializeFrom();
-  auto storage_type_id = context.semantics_ir().GetNode(storage_id).type_id();
-  context.FinishInitialization(storage_type_id, storage_id, init_id);
+                          SemIR::InitializeFrom node) -> void {
+  auto storage_type_id = context.semantics_ir().GetNode(node.dest_id).type_id();
+  context.FinishInitialization(storage_type_id, node.dest_id, node.src_id);
 }
 
 auto HandleIntegerLiteral(FunctionContext& context, SemIR::NodeId node_id,
-                          SemIR::Node node) -> void {
-  llvm::APInt i =
-      context.semantics_ir().GetIntegerLiteral(node.GetAsIntegerLiteral());
+                          SemIR::IntegerLiteral node) -> void {
+  const llvm::APInt& i = context.semantics_ir().GetInteger(node.integer_id);
   // TODO: This won't offer correct semantics, but seems close enough for now.
   llvm::Value* v =
       llvm::ConstantInt::get(context.builder().getInt32Ty(), i.getZExtValue());
@@ -205,36 +191,34 @@ auto HandleIntegerLiteral(FunctionContext& context, SemIR::NodeId node_id,
 }
 
 auto HandleNameReference(FunctionContext& context, SemIR::NodeId node_id,
-                         SemIR::Node node) -> void {
-  auto [name_id, value_id] = node.GetAsNameReference();
-  context.SetLocal(node_id, context.GetLocal(value_id));
+                         SemIR::NameReference node) -> void {
+  context.SetLocal(node_id, context.GetLocal(node.value_id));
 }
 
 auto HandleNameReferenceUntyped(FunctionContext& /*context*/,
-                                SemIR::NodeId /*node_id*/, SemIR::Node /*node*/)
-    -> void {
+                                SemIR::NodeId /*node_id*/,
+                                SemIR::NameReferenceUntyped /*node*/) -> void {
   // No action to take: untyped name references don't hold a value.
 }
 
 auto HandleNamespace(FunctionContext& /*context*/, SemIR::NodeId /*node_id*/,
-                     SemIR::Node /*node*/) -> void {
+                     SemIR::Namespace /*node*/) -> void {
   // No action to take.
 }
 
 auto HandleNoOp(FunctionContext& /*context*/, SemIR::NodeId /*node_id*/,
-                SemIR::Node /*node*/) -> void {
+                SemIR::NoOp /*node*/) -> void {
   // No action to take.
 }
 
 auto HandleParameter(FunctionContext& /*context*/, SemIR::NodeId /*node_id*/,
-                     SemIR::Node /*node*/) -> void {
+                     SemIR::Parameter /*node*/) -> void {
   CARBON_FATAL() << "Parameters should be lowered by `BuildFunctionDefinition`";
 }
 
 auto HandleRealLiteral(FunctionContext& context, SemIR::NodeId node_id,
-                       SemIR::Node node) -> void {
-  SemIR::RealLiteral real =
-      context.semantics_ir().GetRealLiteral(node.GetAsRealLiteral());
+                       SemIR::RealLiteral node) -> void {
+  const SemIR::Real& real = context.semantics_ir().GetReal(node.real_id);
   // TODO: This will probably have overflow issues, and should be fixed.
   double val =
       real.mantissa.getZExtValue() *
@@ -245,16 +229,15 @@ auto HandleRealLiteral(FunctionContext& context, SemIR::NodeId node_id,
 }
 
 auto HandleReturn(FunctionContext& context, SemIR::NodeId /*node_id*/,
-                  SemIR::Node /*node*/) -> void {
+                  SemIR::Return /*node*/) -> void {
   context.builder().CreateRetVoid();
 }
 
 auto HandleReturnExpression(FunctionContext& context, SemIR::NodeId /*node_id*/,
-                            SemIR::Node node) -> void {
-  SemIR::NodeId expr_id = node.GetAsReturnExpression();
+                            SemIR::ReturnExpression node) -> void {
   switch (SemIR::GetInitializingRepresentation(
               context.semantics_ir(),
-              context.semantics_ir().GetNode(expr_id).type_id())
+              context.semantics_ir().GetNode(node.expr_id).type_id())
               .kind) {
     case SemIR::InitializingRepresentation::None:
     case SemIR::InitializingRepresentation::InPlace:
@@ -263,20 +246,20 @@ auto HandleReturnExpression(FunctionContext& context, SemIR::NodeId /*node_id*/,
       return;
     case SemIR::InitializingRepresentation::ByCopy:
       // The expression produces the value representation for the type.
-      context.builder().CreateRet(context.GetLocal(expr_id));
+      context.builder().CreateRet(context.GetLocal(node.expr_id));
       return;
   }
 }
 
 auto HandleSpliceBlock(FunctionContext& context, SemIR::NodeId node_id,
-                       SemIR::Node node) -> void {
-  auto [block_id, result_id] = node.GetAsSpliceBlock();
-  context.LowerBlock(block_id);
-  context.SetLocal(node_id, context.GetLocal(result_id));
+                       SemIR::SpliceBlock node) -> void {
+  context.LowerBlock(node.block_id);
+  context.SetLocal(node_id, context.GetLocal(node.result_id));
 }
 
 auto HandleStringLiteral(FunctionContext& /*context*/,
-                         SemIR::NodeId /*node_id*/, SemIR::Node node) -> void {
+                         SemIR::NodeId /*node_id*/, SemIR::StringLiteral node)
+    -> void {
   CARBON_FATAL() << "TODO: Add support: " << node;
 }
 
@@ -326,32 +309,31 @@ static auto GetStructOrTupleElement(FunctionContext& context,
 }
 
 auto HandleStructAccess(FunctionContext& context, SemIR::NodeId node_id,
-                        SemIR::Node node) -> void {
-  auto [struct_id, member_index] = node.GetAsStructAccess();
-  auto struct_type_id = context.semantics_ir().GetNode(struct_id).type_id();
+                        SemIR::StructAccess node) -> void {
+  auto struct_type_id =
+      context.semantics_ir().GetNode(node.struct_id).type_id();
 
   // Get type information for member names.
-  auto type_refs = context.semantics_ir().GetNodeBlock(
-      context.semantics_ir()
-          .GetNode(context.semantics_ir().GetType(struct_type_id))
-          .GetAsStructType());
-  auto [field_name_id, field_type_id] =
+  auto fields = context.semantics_ir().GetNodeBlock(
       context.semantics_ir()
-          .GetNode(type_refs[member_index.index])
-          .GetAsStructTypeField();
-  auto member_name = context.semantics_ir().GetString(field_name_id);
+          .GetNodeAs<SemIR::StructType>(
+              context.semantics_ir().GetType(struct_type_id))
+          .fields_id);
+  auto field = context.semantics_ir().GetNodeAs<SemIR::StructTypeField>(
+      fields[node.index.index]);
+  auto member_name = context.semantics_ir().GetString(field.name_id);
 
-  context.SetLocal(
-      node_id, GetStructOrTupleElement(context, struct_id, member_index.index,
-                                       node.type_id(), member_name));
+  context.SetLocal(node_id, GetStructOrTupleElement(context, node.struct_id,
+                                                    node.index.index,
+                                                    node.type_id, member_name));
 }
 
 auto HandleStructLiteral(FunctionContext& context, SemIR::NodeId node_id,
-                         SemIR::Node node) -> void {
+                         SemIR::StructLiteral node) -> void {
   // A StructLiteral should always be converted to a StructInit or StructValue
   // if its value is needed.
   context.SetLocal(node_id,
-                   llvm::PoisonValue::get(context.GetType(node.type_id())));
+                   llvm::PoisonValue::get(context.GetType(node.type_id)));
 }
 
 // Emits the value representation for a struct or tuple whose elements are the
@@ -399,12 +381,12 @@ auto EmitStructOrTupleValueRepresentation(FunctionContext& context,
 }
 
 auto HandleStructInit(FunctionContext& context, SemIR::NodeId node_id,
-                      SemIR::Node node) -> void {
-  auto* llvm_type = context.GetType(node.type_id());
+                      SemIR::StructInit node) -> void {
+  auto* llvm_type = context.GetType(node.type_id);
 
-  switch (SemIR::GetInitializingRepresentation(context.semantics_ir(),
-                                               node.type_id())
-              .kind) {
+  switch (
+      SemIR::GetInitializingRepresentation(context.semantics_ir(), node.type_id)
+          .kind) {
     case SemIR::InitializingRepresentation::None:
     case SemIR::InitializingRepresentation::InPlace:
       // TODO: Add a helper to poison a value slot.
@@ -412,63 +394,60 @@ auto HandleStructInit(FunctionContext& context, SemIR::NodeId node_id,
       break;
 
     case SemIR::InitializingRepresentation::ByCopy: {
-      auto [struct_literal_id, refs_id] = node.GetAsStructInit();
-      context.SetLocal(node_id,
-                       EmitStructOrTupleValueRepresentation(
-                           context, node.type_id(), refs_id, "struct.init"));
+      context.SetLocal(
+          node_id, EmitStructOrTupleValueRepresentation(
+                       context, node.type_id, node.elements_id, "struct.init"));
       break;
     }
   }
 }
 
 auto HandleStructValue(FunctionContext& context, SemIR::NodeId node_id,
-                       SemIR::Node node) -> void {
-  auto [struct_literal_id, refs_id] = node.GetAsStructValue();
-  context.SetLocal(node_id, EmitStructOrTupleValueRepresentation(
-                                context, node.type_id(), refs_id, "struct"));
+                       SemIR::StructValue node) -> void {
+  context.SetLocal(node_id,
+                   EmitStructOrTupleValueRepresentation(
+                       context, node.type_id, node.elements_id, "struct"));
 }
 
 auto HandleStructTypeField(FunctionContext& /*context*/,
-                           SemIR::NodeId /*node_id*/, SemIR::Node /*node*/)
-    -> void {
+                           SemIR::NodeId /*node_id*/,
+                           SemIR::StructTypeField /*node*/) -> void {
   // No action to take.
 }
 
 auto HandleTupleAccess(FunctionContext& context, SemIR::NodeId node_id,
-                       SemIR::Node node) -> void {
-  auto [tuple_node_id, index] = node.GetAsTupleAccess();
-  context.SetLocal(node_id,
-                   GetStructOrTupleElement(context, tuple_node_id, index.index,
-                                           node.type_id(), "tuple.elem"));
+                       SemIR::TupleAccess node) -> void {
+  context.SetLocal(
+      node_id, GetStructOrTupleElement(context, node.tuple_id, node.index.index,
+                                       node.type_id, "tuple.elem"));
 }
 
 auto HandleTupleIndex(FunctionContext& context, SemIR::NodeId node_id,
-                      SemIR::Node node) -> void {
-  auto [tuple_node_id, index_node_id] = node.GetAsTupleIndex();
-  auto index_node = context.semantics_ir().GetNode(index_node_id);
-  const auto index = context.semantics_ir()
-                         .GetIntegerLiteral(index_node.GetAsIntegerLiteral())
-                         .getZExtValue();
+                      SemIR::TupleIndex node) -> void {
+  auto index_node =
+      context.semantics_ir().GetNodeAs<SemIR::IntegerLiteral>(node.index_id);
+  auto index =
+      context.semantics_ir().GetInteger(index_node.integer_id).getZExtValue();
   context.SetLocal(node_id,
-                   GetStructOrTupleElement(context, tuple_node_id, index,
-                                           node.type_id(), "tuple.index"));
+                   GetStructOrTupleElement(context, node.tuple_id, index,
+                                           node.type_id, "tuple.index"));
 }
 
 auto HandleTupleLiteral(FunctionContext& context, SemIR::NodeId node_id,
-                        SemIR::Node node) -> void {
+                        SemIR::TupleLiteral node) -> void {
   // A TupleLiteral should always be converted to a TupleInit or TupleValue if
   // its value is needed.
   context.SetLocal(node_id,
-                   llvm::PoisonValue::get(context.GetType(node.type_id())));
+                   llvm::PoisonValue::get(context.GetType(node.type_id)));
 }
 
 auto HandleTupleInit(FunctionContext& context, SemIR::NodeId node_id,
-                     SemIR::Node node) -> void {
-  auto* llvm_type = context.GetType(node.type_id());
+                     SemIR::TupleInit node) -> void {
+  auto* llvm_type = context.GetType(node.type_id);
 
-  switch (SemIR::GetInitializingRepresentation(context.semantics_ir(),
-                                               node.type_id())
-              .kind) {
+  switch (
+      SemIR::GetInitializingRepresentation(context.semantics_ir(), node.type_id)
+          .kind) {
     case SemIR::InitializingRepresentation::None:
     case SemIR::InitializingRepresentation::InPlace:
       // TODO: Add a helper to poison a value slot.
@@ -476,35 +455,34 @@ auto HandleTupleInit(FunctionContext& context, SemIR::NodeId node_id,
       break;
 
     case SemIR::InitializingRepresentation::ByCopy: {
-      auto [struct_literal_id, refs_id] = node.GetAsTupleInit();
       context.SetLocal(
-          node_id, EmitStructOrTupleValueRepresentation(context, node.type_id(),
-                                                        refs_id, "tuple.init"));
+          node_id, EmitStructOrTupleValueRepresentation(
+                       context, node.type_id, node.elements_id, "tuple.init"));
       break;
     }
   }
 }
 
 auto HandleTupleValue(FunctionContext& context, SemIR::NodeId node_id,
-                      SemIR::Node node) -> void {
-  auto [struct_literal_id, refs_id] = node.GetAsTupleValue();
-  context.SetLocal(node_id, EmitStructOrTupleValueRepresentation(
-                                context, node.type_id(), refs_id, "tuple"));
+                      SemIR::TupleValue node) -> void {
+  context.SetLocal(
+      node_id, EmitStructOrTupleValueRepresentation(context, node.type_id,
+                                                    node.elements_id, "tuple"));
 }
 
 auto HandleUnaryOperatorNot(FunctionContext& context, SemIR::NodeId node_id,
-                            SemIR::Node node) -> void {
-  context.SetLocal(node_id, context.builder().CreateNot(context.GetLocal(
-                                node.GetAsUnaryOperatorNot())));
+                            SemIR::UnaryOperatorNot node) -> void {
+  context.SetLocal(
+      node_id, context.builder().CreateNot(context.GetLocal(node.operand_id)));
 }
 
 auto HandleVarStorage(FunctionContext& context, SemIR::NodeId node_id,
-                      SemIR::Node node) -> void {
+                      SemIR::VarStorage node) -> void {
   // TODO: Eventually this name will be optional, and we'll want to provide
   // something like `var` as a default. However, that's not possible right now
   // so cannot be tested.
-  auto name = context.semantics_ir().GetString(node.GetAsVarStorage());
-  auto* alloca = context.builder().CreateAlloca(context.GetType(node.type_id()),
+  auto name = context.semantics_ir().GetString(node.name_id);
+  auto* alloca = context.builder().CreateAlloca(context.GetType(node.type_id),
                                                 /*ArraySize=*/nullptr, name);
   context.SetLocal(node_id, alloca);
 }

+ 19 - 20
toolchain/lower/handle_expression_category.cpp

@@ -8,24 +8,24 @@
 namespace Carbon::Lower {
 
 auto HandleBindValue(FunctionContext& context, SemIR::NodeId node_id,
-                     SemIR::Node node) -> void {
+                     SemIR::BindValue node) -> void {
   switch (auto rep = SemIR::GetValueRepresentation(context.semantics_ir(),
-                                                   node.type_id());
+                                                   node.type_id);
           rep.kind) {
     case SemIR::ValueRepresentation::None:
       // Nothing should use this value, but StubReference needs a value to
       // propagate.
       // TODO: Remove this now the StubReferences are gone.
       context.SetLocal(node_id,
-                       llvm::PoisonValue::get(context.GetType(node.type_id())));
+                       llvm::PoisonValue::get(context.GetType(node.type_id)));
       break;
     case SemIR::ValueRepresentation::Copy:
       context.SetLocal(node_id, context.builder().CreateLoad(
-                                    context.GetType(node.type_id()),
-                                    context.GetLocal(node.GetAsBindValue())));
+                                    context.GetType(node.type_id),
+                                    context.GetLocal(node.value_id)));
       break;
     case SemIR::ValueRepresentation::Pointer:
-      context.SetLocal(node_id, context.GetLocal(node.GetAsBindValue()));
+      context.SetLocal(node_id, context.GetLocal(node.value_id));
       break;
     case SemIR::ValueRepresentation::Custom:
       CARBON_FATAL() << "TODO: Add support for BindValue with custom value rep";
@@ -33,28 +33,27 @@ auto HandleBindValue(FunctionContext& context, SemIR::NodeId node_id,
 }
 
 auto HandleTemporary(FunctionContext& context, SemIR::NodeId node_id,
-                     SemIR::Node node) -> void {
-  auto [temporary_id, init_id] = node.GetAsTemporary();
-  context.FinishInitialization(node.type_id(), temporary_id, init_id);
-  context.SetLocal(node_id, context.GetLocal(temporary_id));
+                     SemIR::Temporary node) -> void {
+  context.FinishInitialization(node.type_id, node.storage_id, node.init_id);
+  context.SetLocal(node_id, context.GetLocal(node.storage_id));
 }
 
 auto HandleTemporaryStorage(FunctionContext& context, SemIR::NodeId node_id,
-                            SemIR::Node node) -> void {
-  context.SetLocal(
-      node_id, context.builder().CreateAlloca(context.GetType(node.type_id()),
-                                              nullptr, "temp"));
+                            SemIR::TemporaryStorage node) -> void {
+  context.SetLocal(node_id,
+                   context.builder().CreateAlloca(context.GetType(node.type_id),
+                                                  nullptr, "temp"));
 }
 
 auto HandleValueAsReference(FunctionContext& context, SemIR::NodeId node_id,
-                            SemIR::Node node) -> void {
-  CARBON_CHECK(SemIR::GetExpressionCategory(context.semantics_ir(),
-                                            node.GetAsValueAsReference()) ==
-               SemIR::ExpressionCategory::Value);
+                            SemIR::ValueAsReference node) -> void {
   CARBON_CHECK(
-      SemIR::GetValueRepresentation(context.semantics_ir(), node.type_id())
+      SemIR::GetExpressionCategory(context.semantics_ir(), node.value_id) ==
+      SemIR::ExpressionCategory::Value);
+  CARBON_CHECK(
+      SemIR::GetValueRepresentation(context.semantics_ir(), node.type_id)
           .kind == SemIR::ValueRepresentation::Pointer);
-  context.SetLocal(node_id, context.GetLocal(node.GetAsValueAsReference()));
+  context.SetLocal(node_id, context.GetLocal(node.value_id));
 }
 
 }  // namespace Carbon::Lower

+ 5 - 5
toolchain/lower/handle_type.cpp

@@ -7,27 +7,27 @@
 namespace Carbon::Lower {
 
 auto HandleArrayType(FunctionContext& context, SemIR::NodeId node_id,
-                     SemIR::Node /*node*/) -> void {
+                     SemIR::ArrayType /*node*/) -> void {
   context.SetLocal(node_id, context.GetTypeAsValue());
 }
 
 auto HandleConstType(FunctionContext& context, SemIR::NodeId node_id,
-                     SemIR::Node /*node*/) -> void {
+                     SemIR::ConstType /*node*/) -> void {
   context.SetLocal(node_id, context.GetTypeAsValue());
 }
 
 auto HandlePointerType(FunctionContext& context, SemIR::NodeId node_id,
-                       SemIR::Node /*node*/) -> void {
+                       SemIR::PointerType /*node*/) -> void {
   context.SetLocal(node_id, context.GetTypeAsValue());
 }
 
 auto HandleStructType(FunctionContext& context, SemIR::NodeId node_id,
-                      SemIR::Node /*node*/) -> void {
+                      SemIR::StructType /*node*/) -> void {
   context.SetLocal(node_id, context.GetTypeAsValue());
 }
 
 auto HandleTupleType(FunctionContext& context, SemIR::NodeId node_id,
-                     SemIR::Node /*node*/) -> void {
+                     SemIR::TupleType /*node*/) -> void {
   context.SetLocal(node_id, context.GetTypeAsValue());
 }
 

+ 1 - 0
toolchain/sem_ir/BUILD

@@ -32,6 +32,7 @@ cc_library(
     deps = [
         "//common:check",
         "//common:ostream",
+        "//common:struct_reflection",
         "//toolchain/base:index_base",
         "//toolchain/parse:tree",
         "//toolchain/sem_ir:builtin_kind",

+ 45 - 57
toolchain/sem_ir/file.cpp

@@ -24,11 +24,11 @@ File::File()
   // Error uses a self-referential type so that it's not accidentally treated as
   // a normal type. Every other builtin is a type, including the
   // self-referential TypeType.
-#define CARBON_SEMANTICS_BUILTIN_KIND(Name, ...)                               \
-  nodes_.push_back(Node::Builtin::Make(BuiltinKind::Name,                      \
-                                       BuiltinKind::Name == BuiltinKind::Error \
-                                           ? TypeId::Error                     \
-                                           : TypeId::TypeType));
+#define CARBON_SEMANTICS_BUILTIN_KIND(Name, ...)                   \
+  nodes_.push_back(Builtin(BuiltinKind::Name == BuiltinKind::Error \
+                               ? TypeId::Error                     \
+                               : TypeId::TypeType,                 \
+                           BuiltinKind::Name));
 #include "toolchain/sem_ir/builtin_kind.def"
 
   CARBON_CHECK(nodes_.size() == BuiltinKind::ValidCount)
@@ -51,8 +51,8 @@ File::File(std::string filename, const File* builtins)
   static constexpr auto BuiltinIR = CrossReferenceIRId(0);
   for (auto [i, node] : llvm::enumerate(builtins->nodes_)) {
     // We can reuse builtin type IDs because they're special-cased values.
-    nodes_.push_back(Node::CrossReference::Make(node.type_id(), BuiltinIR,
-                                                SemIR::NodeId(i)));
+    nodes_.push_back(
+        CrossReference(node.type_id(), BuiltinIR, SemIR::NodeId(i)));
   }
 }
 
@@ -150,13 +150,13 @@ auto File::Print(llvm::raw_ostream& out, bool include_builtins) const -> void {
       << "\n";
 
   PrintList(out, "functions", functions_);
-  // Integer literals are an APInt, and default to a signed print, but the
-  // ZExtValue print is correct.
-  PrintList(out, "integer_literals", integer_literals_,
+  // Integer values are APInts, and default to a signed print, but we currently
+  // treat them as unsigned.
+  PrintList(out, "integers", integers_,
             [](llvm::raw_ostream& out, const llvm::APInt& val) {
               val.print(out, /*isSigned=*/false);
             });
-  PrintList(out, "real_literals", real_literals_);
+  PrintList(out, "reals", reals_);
   PrintList(out, "strings", strings_);
   PrintList(out, "types", types_);
   PrintBlock(out, "type_blocks", type_blocks_);
@@ -210,7 +210,6 @@ static auto GetTypePrecedence(NodeKind kind) -> int {
     case NodeKind::FunctionDeclaration:
     case NodeKind::InitializeFrom:
     case NodeKind::IntegerLiteral:
-    case NodeKind::Invalid:
     case NodeKind::NameReference:
     case NodeKind::NameReferenceUntyped:
     case NodeKind::Namespace:
@@ -276,13 +275,14 @@ auto File::StringifyType(TypeId type_id, bool in_type_context) const
     // NOLINTNEXTLINE(bugprone-switch-missing-default-case)
     switch (node.kind()) {
       case NodeKind::ArrayType: {
-        auto [bound_id, type_id] = node.GetAsArrayType();
+        auto array = node.As<ArrayType>();
         if (step.index == 0) {
           out << "[";
           steps.push_back(step.Next());
-          steps.push_back({.node_id = GetTypeAllowBuiltinTypes(type_id)});
+          steps.push_back(
+              {.node_id = GetTypeAllowBuiltinTypes(array.element_type_id)});
         } else if (step.index == 1) {
-          out << "; " << GetArrayBoundValue(bound_id) << "]";
+          out << "; " << GetArrayBoundValue(array.bound_id) << "]";
         }
         break;
       }
@@ -292,7 +292,7 @@ auto File::StringifyType(TypeId type_id, bool in_type_context) const
 
           // Add parentheses if required.
           auto inner_type_node_id =
-              GetTypeAllowBuiltinTypes(node.GetAsConstType());
+              GetTypeAllowBuiltinTypes(node.As<ConstType>().inner_id);
           if (GetTypePrecedence(GetNode(inner_type_node_id).kind()) <
               GetTypePrecedence(node.kind())) {
             out << "(";
@@ -308,15 +308,15 @@ auto File::StringifyType(TypeId type_id, bool in_type_context) const
       case NodeKind::PointerType: {
         if (step.index == 0) {
           steps.push_back(step.Next());
-          steps.push_back(
-              {.node_id = GetTypeAllowBuiltinTypes(node.GetAsPointerType())});
+          steps.push_back({.node_id = GetTypeAllowBuiltinTypes(
+                               node.As<PointerType>().pointee_id)});
         } else if (step.index == 1) {
           out << "*";
         }
         break;
       }
       case NodeKind::StructType: {
-        auto refs = GetNodeBlock(node.GetAsStructType());
+        auto refs = GetNodeBlock(node.As<StructType>().fields_id);
         if (refs.empty()) {
           out << "{}";
           break;
@@ -334,13 +334,13 @@ auto File::StringifyType(TypeId type_id, bool in_type_context) const
         break;
       }
       case NodeKind::StructTypeField: {
-        auto [name_id, type_id] = node.GetAsStructTypeField();
-        out << "." << GetString(name_id) << ": ";
-        steps.push_back({.node_id = GetTypeAllowBuiltinTypes(type_id)});
+        auto field = node.As<StructTypeField>();
+        out << "." << GetString(field.name_id) << ": ";
+        steps.push_back({.node_id = GetTypeAllowBuiltinTypes(field.type_id)});
         break;
       }
       case NodeKind::TupleType: {
-        auto refs = GetTypeBlock(node.GetAsTupleType());
+        auto refs = GetTypeBlock(node.As<TupleType>().elements_id);
         if (refs.empty()) {
           out << "()";
           break;
@@ -410,8 +410,6 @@ auto File::StringifyType(TypeId type_id, bool in_type_context) const
         // when stringification is needed.
         out << "<cannot stringify " << step.node_id << ">";
         break;
-      case NodeKind::Invalid:
-        llvm_unreachable("NodeKind::Invalid is never used.");
     }
   }
 
@@ -421,7 +419,7 @@ auto File::StringifyType(TypeId type_id, bool in_type_context) const
     auto outer_node = GetNode(outer_node_id);
     if (outer_node.kind() == NodeKind::TupleType ||
         (outer_node.kind() == NodeKind::StructType &&
-         GetNodeBlock(outer_node.GetAsStructType()).empty())) {
+         GetNodeBlock(outer_node.As<StructType>().fields_id).empty())) {
       out << " as type";
     }
   }
@@ -437,7 +435,6 @@ auto GetExpressionCategory(const File& file, NodeId node_id)
     // clang warns on unhandled enum values; clang-tidy is incorrect here.
     // NOLINTNEXTLINE(bugprone-switch-missing-default-case)
     switch (node.kind()) {
-      case NodeKind::Invalid:
       case NodeKind::Assign:
       case NodeKind::Branch:
       case NodeKind::BranchIf:
@@ -452,15 +449,14 @@ auto GetExpressionCategory(const File& file, NodeId node_id)
         return ExpressionCategory::NotExpression;
 
       case NodeKind::CrossReference: {
-        auto [xref_id, xref_node_id] = node.GetAsCrossReference();
-        ir = &ir->GetCrossReferenceIR(xref_id);
-        node_id = xref_node_id;
+        auto xref = node.As<CrossReference>();
+        ir = &ir->GetCrossReferenceIR(xref.ir_id);
+        node_id = xref.node_id;
         continue;
       }
 
       case NodeKind::NameReference: {
-        auto [name_id, value_id] = node.GetAsNameReference();
-        node_id = value_id;
+        node_id = node.As<NameReference>().value_id;
         continue;
       }
 
@@ -485,38 +481,32 @@ auto GetExpressionCategory(const File& file, NodeId node_id)
         return ExpressionCategory::Value;
 
       case NodeKind::BindName: {
-        auto [name_id, value_id] = node.GetAsBindName();
-        node_id = value_id;
+        node_id = node.As<BindName>().value_id;
         continue;
       }
 
       case NodeKind::ArrayIndex: {
-        auto [base_id, index_id] = node.GetAsArrayIndex();
-        node_id = base_id;
+        node_id = node.As<ArrayIndex>().array_id;
         continue;
       }
 
       case NodeKind::StructAccess: {
-        auto [base_id, member_index] = node.GetAsStructAccess();
-        node_id = base_id;
+        node_id = node.As<StructAccess>().struct_id;
         continue;
       }
 
       case NodeKind::TupleAccess: {
-        auto [base_id, index_id] = node.GetAsTupleAccess();
-        node_id = base_id;
+        node_id = node.As<TupleAccess>().tuple_id;
         continue;
       }
 
       case NodeKind::TupleIndex: {
-        auto [base_id, index_id] = node.GetAsTupleIndex();
-        node_id = base_id;
+        node_id = node.As<TupleIndex>().tuple_id;
         continue;
       }
 
       case NodeKind::SpliceBlock: {
-        auto [block_id, result_id] = node.GetAsSpliceBlock();
-        node_id = result_id;
+        node_id = node.As<SpliceBlock>().result_id;
         continue;
       }
 
@@ -569,7 +559,6 @@ auto GetValueRepresentation(const File& file, TypeId type_id)
       case NodeKind::FunctionDeclaration:
       case NodeKind::InitializeFrom:
       case NodeKind::IntegerLiteral:
-      case NodeKind::Invalid:
       case NodeKind::NameReference:
       case NodeKind::NameReferenceUntyped:
       case NodeKind::Namespace:
@@ -597,15 +586,14 @@ auto GetValueRepresentation(const File& file, TypeId type_id)
         CARBON_FATAL() << "Type refers to non-type node " << node;
 
       case NodeKind::CrossReference: {
-        auto [xref_id, xref_node_id] = node.GetAsCrossReference();
-        ir = &ir->GetCrossReferenceIR(xref_id);
-        node_id = xref_node_id;
+        auto xref = node.As<CrossReference>();
+        ir = &ir->GetCrossReferenceIR(xref.ir_id);
+        node_id = xref.node_id;
         continue;
       }
 
       case NodeKind::SpliceBlock: {
-        auto [block_id, result_id] = node.GetAsSpliceBlock();
-        node_id = result_id;
+        node_id = node.As<SpliceBlock>().result_id;
         continue;
       }
 
@@ -616,16 +604,15 @@ auto GetValueRepresentation(const File& file, TypeId type_id)
         return {.kind = ValueRepresentation::Pointer, .type = type_id};
 
       case NodeKind::StructType: {
-        const auto& fields = ir->GetNodeBlock(node.GetAsStructType());
+        const auto& fields = ir->GetNodeBlock(node.As<StructType>().fields_id);
         if (fields.empty()) {
           // An empty struct has an empty representation.
           return {.kind = ValueRepresentation::None, .type = TypeId::Invalid};
         }
         if (fields.size() == 1) {
           // A struct with one field has the same representation as its field.
-          auto [field_name_id, field_type_id] =
-              ir->GetNode(fields.front()).GetAsStructTypeField();
-          node_id = ir->GetTypeAllowBuiltinTypes(field_type_id);
+          node_id = ir->GetTypeAllowBuiltinTypes(
+              ir->GetNode(fields.front()).As<StructTypeField>().type_id);
           continue;
         }
         // For any other struct, use a pointer representation.
@@ -633,7 +620,8 @@ auto GetValueRepresentation(const File& file, TypeId type_id)
       }
 
       case NodeKind::TupleType: {
-        const auto& elements = ir->GetTypeBlock(node.GetAsTupleType());
+        const auto& elements =
+            ir->GetTypeBlock(node.As<TupleType>().elements_id);
         if (elements.empty()) {
           // An empty tuple has an empty representation.
           return {.kind = ValueRepresentation::None, .type = TypeId::Invalid};
@@ -650,7 +638,7 @@ auto GetValueRepresentation(const File& file, TypeId type_id)
       case NodeKind::Builtin:
         // clang warns on unhandled enum values; clang-tidy is incorrect here.
         // NOLINTNEXTLINE(bugprone-switch-missing-default-case)
-        switch (node.GetAsBuiltin()) {
+        switch (node.As<Builtin>().builtin_kind) {
           case BuiltinKind::TypeType:
           case BuiltinKind::Error:
           case BuiltinKind::Invalid:
@@ -670,7 +658,7 @@ auto GetValueRepresentation(const File& file, TypeId type_id)
         return {.kind = ValueRepresentation::Copy, .type = type_id};
 
       case NodeKind::ConstType:
-        node_id = ir->GetTypeAllowBuiltinTypes(node.GetAsConstType());
+        node_id = ir->GetTypeAllowBuiltinTypes(node.As<ConstType>().inner_id);
         continue;
     }
   }

+ 28 - 20
toolchain/sem_ir/file.h

@@ -51,7 +51,9 @@ struct Function : public Printable<Function> {
   llvm::SmallVector<NodeBlockId> body_block_ids;
 };
 
-struct RealLiteral : public Printable<RealLiteral> {
+// TODO: Replace this with a Rational type, per the design:
+// docs/design/expressions/literals.md
+struct Real : public Printable<Real> {
   auto Print(llvm::raw_ostream& out) const -> void {
     out << "{mantissa: ";
     mantissa.print(out, /*isSigned=*/false);
@@ -91,7 +93,7 @@ class File : public Printable<File> {
 
   // Returns array bound value from the bound node.
   auto GetArrayBoundValue(NodeId bound_id) const -> uint64_t {
-    return GetIntegerLiteral(GetNode(bound_id).GetAsIntegerLiteral())
+    return GetInteger(GetNodeAs<IntegerLiteral>(bound_id).integer_id)
         .getZExtValue();
   }
 
@@ -119,18 +121,18 @@ class File : public Printable<File> {
     return functions_[function_id.index];
   }
 
-  // Adds an integer literal, returning an ID to reference it.
-  auto AddIntegerLiteral(llvm::APInt integer_literal) -> IntegerLiteralId {
-    IntegerLiteralId id(integer_literals_.size());
+  // Adds an integer value, returning an ID to reference it.
+  auto AddInteger(llvm::APInt integer) -> IntegerId {
+    IntegerId id(integers_.size());
     // TODO: Return failure on overflow instead of crashing.
     CARBON_CHECK(id.index >= 0);
-    integer_literals_.push_back(integer_literal);
+    integers_.push_back(integer);
     return id;
   }
 
-  // Returns the requested integer literal.
-  auto GetIntegerLiteral(IntegerLiteralId int_id) const -> const llvm::APInt& {
-    return integer_literals_[int_id.index];
+  // Returns the requested integer value.
+  auto GetInteger(IntegerId int_id) const -> const llvm::APInt& {
+    return integers_[int_id.index];
   }
 
   // Adds a name scope, returning an ID to reference it.
@@ -175,6 +177,12 @@ class File : public Printable<File> {
   // Returns the requested node.
   auto GetNode(NodeId node_id) const -> Node { return nodes_[node_id.index]; }
 
+  // Returns the requested node, which is known to have the specified type.
+  template <typename NodeT>
+  auto GetNodeAs(NodeId node_id) const -> NodeT {
+    return GetNode(node_id).As<NodeT>();
+  }
+
   // Reserves and returns a node block ID. The contents of the node block
   // should be specified by calling SetNodeBlock, or by pushing the ID onto the
   // NodeBlockStack.
@@ -225,18 +233,18 @@ class File : public Printable<File> {
     return node_blocks_[block_id.index];
   }
 
-  // Adds a real literal, returning an ID to reference it.
-  auto AddRealLiteral(RealLiteral real_literal) -> RealLiteralId {
-    RealLiteralId id(real_literals_.size());
+  // Adds a real value, returning an ID to reference it.
+  auto AddReal(Real real) -> RealId {
+    RealId id(reals_.size());
     // TODO: Return failure on overflow instead of crashing.
     CARBON_CHECK(id.index >= 0);
-    real_literals_.push_back(real_literal);
+    reals_.push_back(real);
     return id;
   }
 
-  // Returns the requested real literal.
-  auto GetRealLiteral(RealLiteralId int_id) const -> const RealLiteral& {
-    return real_literals_[int_id.index];
+  // Returns the requested real value.
+  auto GetReal(RealId real_id) const -> const Real& {
+    return reals_[real_id.index];
   }
 
   // Adds an string, returning an ID to reference it.
@@ -372,14 +380,14 @@ class File : public Printable<File> {
   // crossing node blocks).
   llvm::SmallVector<const File*> cross_reference_irs_;
 
-  // Storage for integer literals.
-  llvm::SmallVector<llvm::APInt> integer_literals_;
+  // Storage for integer values.
+  llvm::SmallVector<llvm::APInt> integers_;
 
   // Storage for name scopes.
   llvm::SmallVector<llvm::DenseMap<StringId, NodeId>> name_scopes_;
 
-  // Storage for real literals.
-  llvm::SmallVector<RealLiteral> real_literals_;
+  // Storage for real values.
+  llvm::SmallVector<Real> reals_;
 
   // Storage for strings. strings_ provides a list of allocated strings, while
   // string_to_id_ provides a mapping to identify strings.

+ 2 - 2
toolchain/sem_ir/file_test.cpp

@@ -47,8 +47,8 @@ TEST(SemIRTest, YAML) {
   auto file = Yaml::Sequence(ElementsAre(Yaml::Mapping(ElementsAre(
       Pair("cross_reference_irs_size", "1"),
       Pair("functions", Yaml::Sequence(SizeIs(1))),
-      Pair("integer_literals", Yaml::Sequence(ElementsAre("0"))),
-      Pair("real_literals", Yaml::Sequence(IsEmpty())),
+      Pair("integers", Yaml::Sequence(ElementsAre("0"))),
+      Pair("reals", Yaml::Sequence(IsEmpty())),
       Pair("strings", Yaml::Sequence(ElementsAre("F", "x"))),
       Pair("types", Yaml::Sequence(ElementsAre(node_builtin))),
       Pair("type_blocks", Yaml::Sequence(IsEmpty())),

+ 72 - 100
toolchain/sem_ir/formatter.cpp

@@ -350,55 +350,54 @@ class NodeNamer {
 
       switch (node.kind()) {
         case NodeKind::Branch: {
-          auto dest_id = node.GetAsBranch();
-          AddBlockLabel(scope_idx, dest_id, node);
+          AddBlockLabel(scope_idx, node.As<Branch>().target_id, node);
           break;
         }
         case NodeKind::BranchIf: {
-          auto [dest_id, cond_id] = node.GetAsBranchIf();
-          AddBlockLabel(scope_idx, dest_id, node);
+          AddBlockLabel(scope_idx, node.As<BranchIf>().target_id, node);
           break;
         }
         case NodeKind::BranchWithArg: {
-          auto [dest_id, arg_id] = node.GetAsBranchWithArg();
-          AddBlockLabel(scope_idx, dest_id, node);
+          AddBlockLabel(scope_idx, node.As<BranchWithArg>().target_id, node);
           break;
         }
         case NodeKind::SpliceBlock: {
-          auto [block_id, result_id] = node.GetAsSpliceBlock();
-          CollectNamesInBlock(scope_idx, block_id);
+          CollectNamesInBlock(scope_idx, node.As<SpliceBlock>().block_id);
           break;
         }
         case NodeKind::BindName: {
-          auto [name_id, value_id] = node.GetAsBindName();
-          add_node_name_id(name_id);
+          add_node_name_id(node.As<BindName>().name_id);
           continue;
         }
         case NodeKind::FunctionDeclaration: {
           add_node_name_id(
-              semantics_ir_.GetFunction(node.GetAsFunctionDeclaration())
+              semantics_ir_
+                  .GetFunction(node.As<FunctionDeclaration>().function_id)
                   .name_id);
           continue;
         }
         case NodeKind::NameReference: {
-          auto [name_id, value_id] = node.GetAsNameReference();
-          add_node_name(semantics_ir_.GetString(name_id).str() + ".ref");
+          add_node_name(
+              semantics_ir_.GetString(node.As<NameReference>().name_id).str() +
+              ".ref");
           continue;
         }
         case NodeKind::NameReferenceUntyped: {
-          auto [name_id, value_id] = node.GetAsNameReferenceUntyped();
-          add_node_name(semantics_ir_.GetString(name_id).str() + ".ref");
+          add_node_name(
+              semantics_ir_.GetString(node.As<NameReferenceUntyped>().name_id)
+                  .str() +
+              ".ref");
           continue;
         }
         case NodeKind::Parameter: {
-          add_node_name_id(node.GetAsParameter());
+          add_node_name_id(node.As<Parameter>().name_id);
           continue;
         }
         case NodeKind::VarStorage: {
           // TODO: Eventually this name will be optional, and we'll want to
           // provide something like `var` as a default. However, that's not
           // possible right now so cannot be tested.
-          add_node_name_id(node.GetAsVarStorage());
+          add_node_name_id(node.As<VarStorage>().name_id);
           continue;
         }
         default: {
@@ -515,7 +514,7 @@ class Formatter {
   auto FormatInstruction(NodeId node_id) -> void {
     if (!node_id.is_valid()) {
       Indent();
-      out_ << NodeKind::Invalid.ir_name() << "\n";
+      out_ << "invalid\n";
       return;
     }
 
@@ -526,9 +525,9 @@ class Formatter {
     // clang warns on unhandled enum values; clang-tidy is incorrect here.
     // NOLINTNEXTLINE(bugprone-switch-missing-default-case)
     switch (node.kind()) {
-#define CARBON_SEMANTICS_NODE_KIND(Name)          \
-  case NodeKind::Name:                            \
-    FormatInstruction<Node::Name>(node_id, node); \
+#define CARBON_SEMANTICS_NODE_KIND(Kind)         \
+  case NodeKind::Kind:                           \
+    FormatInstruction(node_id, node.As<Kind>()); \
     break;
 #include "toolchain/sem_ir/node_kind.def"
     }
@@ -536,12 +535,12 @@ class Formatter {
 
   auto Indent() -> void { out_.indent(indent_); }
 
-  template <typename Kind>
-  auto FormatInstruction(NodeId node_id, Node node) -> void {
+  template <typename NodeT>
+  auto FormatInstruction(NodeId node_id, NodeT node) -> void {
     Indent();
     FormatInstructionLHS(node_id, node);
-    out_ << node.kind().ir_name();
-    FormatInstructionRHS<Kind>(node);
+    out_ << NodeT::Kind.ir_name();
+    FormatInstructionRHS(node);
     out_ << "\n";
   }
 
@@ -575,68 +574,59 @@ class Formatter {
     }
   }
 
-  template <typename Kind>
-  auto FormatInstructionRHS(Node node) -> void {
+  template <typename NodeT>
+  auto FormatInstructionRHS(NodeT node) -> void {
     // By default, an instruction has a comma-separated argument list.
-    FormatArgs(Kind::Get(node));
+    std::apply([&](auto... args) { FormatArgs(args...); }, node.args_tuple());
   }
 
-  template <>
-  auto FormatInstructionRHS<Node::BlockArg>(Node node) -> void {
+  auto FormatInstructionRHS(BlockArg node) -> void {
     out_ << " ";
-    FormatLabel(node.GetAsBlockArg());
+    FormatLabel(node.block_id);
   }
 
-  template <>
-  auto FormatInstruction<Node::BranchIf>(NodeId /*node_id*/, Node node)
-      -> void {
+  auto FormatInstruction(NodeId /*node_id*/, BranchIf node) -> void {
     if (!in_terminator_sequence_) {
       Indent();
     }
-    auto [label_id, cond_id] = node.GetAsBranchIf();
     out_ << "if ";
-    FormatNodeName(cond_id);
+    FormatNodeName(node.cond_id);
     out_ << " " << NodeKind::Branch.ir_name() << " ";
-    FormatLabel(label_id);
+    FormatLabel(node.target_id);
     out_ << " else ";
     in_terminator_sequence_ = true;
   }
 
-  template <>
-  auto FormatInstruction<Node::BranchWithArg>(NodeId /*node_id*/, Node node)
-      -> void {
+  auto FormatInstruction(NodeId /*node_id*/, BranchWithArg node) -> void {
     if (!in_terminator_sequence_) {
       Indent();
     }
-    auto [label_id, arg_id] = node.GetAsBranchWithArg();
     out_ << NodeKind::BranchWithArg.ir_name() << " ";
-    FormatLabel(label_id);
+    FormatLabel(node.target_id);
     out_ << "(";
-    FormatNodeName(arg_id);
+    FormatNodeName(node.arg_id);
     out_ << ")\n";
     in_terminator_sequence_ = false;
   }
 
-  template <>
-  auto FormatInstruction<Node::Branch>(NodeId /*node_id*/, Node node) -> void {
+  auto FormatInstruction(NodeId /*node_id*/, Branch node) -> void {
     if (!in_terminator_sequence_) {
       Indent();
     }
     out_ << NodeKind::Branch.ir_name() << " ";
-    FormatLabel(node.GetAsBranch());
+    FormatLabel(node.target_id);
     out_ << "\n";
     in_terminator_sequence_ = false;
   }
 
-  template <>
-  auto FormatInstructionRHS<Node::ArrayInit>(Node node) -> void {
+  auto FormatInstructionRHS(ArrayInit node) -> void {
     out_ << " ";
-    auto [src_id, refs_id] = node.GetAsArrayInit();
-    FormatArg(src_id);
+    FormatArg(node.tuple_id);
 
-    llvm::ArrayRef<NodeId> refs = semantics_ir_.GetNodeBlock(refs_id);
-    auto inits = refs.drop_back(1);
-    auto return_slot_id = refs.back();
+    llvm::ArrayRef<NodeId> inits_and_return_slot =
+        semantics_ir_.GetNodeBlock(node.inits_and_return_slot_id);
+    auto inits = inits_and_return_slot.drop_back(1);
+    auto return_slot_id = inits_and_return_slot.back();
 
     out_ << ", (";
     llvm::ListSeparator sep;
@@ -648,16 +638,14 @@ class Formatter {
     FormatReturnSlot(return_slot_id);
   }
 
-  template <>
-  auto FormatInstructionRHS<Node::Call>(Node node) -> void {
+  auto FormatInstructionRHS(Call node) -> void {
     out_ << " ";
-    auto [args_id, callee_id] = node.GetAsCall();
-    FormatArg(callee_id);
+    FormatArg(node.function_id);
 
-    llvm::ArrayRef<NodeId> args = semantics_ir_.GetNodeBlock(args_id);
+    llvm::ArrayRef<NodeId> args = semantics_ir_.GetNodeBlock(node.args_id);
 
     bool has_return_slot =
-        semantics_ir_.GetFunction(callee_id).return_slot_id.is_valid();
+        semantics_ir_.GetFunction(node.function_id).return_slot_id.is_valid();
     NodeId return_slot_id = NodeId::Invalid;
     if (has_return_slot) {
       return_slot_id = args.back();
@@ -677,30 +665,24 @@ class Formatter {
     }
   }
 
-  template <>
-  auto FormatInstructionRHS<Node::InitializeFrom>(Node node) -> void {
-    auto [src_id, dest_id] = node.GetAsInitializeFrom();
-    FormatArgs(src_id);
-    FormatReturnSlot(dest_id);
+  auto FormatInstructionRHS(InitializeFrom node) -> void {
+    FormatArgs(node.src_id);
+    FormatReturnSlot(node.dest_id);
   }
 
-  template <>
-  auto FormatInstructionRHS<Node::CrossReference>(Node node) -> void {
+  auto FormatInstructionRHS(CrossReference node) -> void {
     // TODO: Figure out a way to make this meaningful. We'll need some way to
     // name cross-reference IRs, perhaps by the node ID of the import?
-    auto [xref_id, node_id] = node.GetAsCrossReference();
-    out_ << " " << xref_id << "." << node_id;
+    out_ << " " << node.ir_id << "." << node.node_id;
   }
 
-  template <>
-  auto FormatInstructionRHS<Node::SpliceBlock>(Node node) -> void {
-    auto [block_id, result_id] = node.GetAsSpliceBlock();
-    FormatArgs(result_id);
+  auto FormatInstructionRHS(SpliceBlock node) -> void {
+    FormatArgs(node.result_id);
     out_ << " {";
-    if (!semantics_ir_.GetNodeBlock(block_id).empty()) {
+    if (!semantics_ir_.GetNodeBlock(node.block_id).empty()) {
       out_ << "\n";
       indent_ += 2;
-      FormatCodeBlock(block_id);
+      FormatCodeBlock(node.block_id);
       indent_ -= 2;
       Indent();
     }
@@ -708,39 +690,29 @@ class Formatter {
   }
 
   // StructTypeFields are formatted as part of their StructType.
-  template <>
-  auto FormatInstruction<Node::StructTypeField>(NodeId /*node_id*/,
-                                                Node /*node*/) -> void {}
+  auto FormatInstruction(NodeId /*node_id*/, StructTypeField /*node*/) -> void {
+  }
 
-  template <>
-  auto FormatInstructionRHS<Node::StructType>(Node node) -> void {
+  auto FormatInstructionRHS(StructType node) -> void {
     out_ << " {";
     llvm::ListSeparator sep;
-    for (auto field_id : semantics_ir_.GetNodeBlock(node.GetAsStructType())) {
+    for (auto field_id : semantics_ir_.GetNodeBlock(node.fields_id)) {
       out_ << sep << ".";
-      auto [field_name_id, field_type_id] =
-          semantics_ir_.GetNode(field_id).GetAsStructTypeField();
-      FormatString(field_name_id);
+      auto field = semantics_ir_.GetNodeAs<StructTypeField>(field_id);
+      FormatString(field.name_id);
       out_ << ": ";
-      FormatType(field_type_id);
+      FormatType(field.type_id);
     }
     out_ << "}";
   }
 
-  auto FormatArgs(Node::NoArgs /*unused*/) -> void {}
+  auto FormatArgs() -> void {}
 
-  template <typename Arg1>
-  auto FormatArgs(Arg1 arg) -> void {
+  template <typename... Args>
+  auto FormatArgs(Args... args) -> void {
     out_ << ' ';
-    FormatArg(arg);
-  }
-
-  template <typename Arg1, typename Arg2>
-  auto FormatArgs(std::pair<Arg1, Arg2> args) -> void {
-    out_ << ' ';
-    FormatArg(args.first);
-    out_ << ",";
-    FormatArgs(args.second);
+    llvm::ListSeparator sep;
+    ((out_ << sep, FormatArg(args)), ...);
   }
 
   auto FormatArg(BoolValue v) -> void { out_ << v; }
@@ -749,8 +721,8 @@ class Formatter {
 
   auto FormatArg(FunctionId id) -> void { FormatFunctionName(id); }
 
-  auto FormatArg(IntegerLiteralId id) -> void {
-    semantics_ir_.GetIntegerLiteral(id).print(out_, /*isSigned=*/false);
+  auto FormatArg(IntegerId id) -> void {
+    semantics_ir_.GetInteger(id).print(out_, /*isSigned=*/false);
   }
 
   auto FormatArg(MemberIndex index) -> void { out_ << index; }
@@ -790,9 +762,9 @@ class Formatter {
     out_ << ')';
   }
 
-  auto FormatArg(RealLiteralId id) -> void {
+  auto FormatArg(RealId id) -> void {
     // TODO: Format with a `.` when the exponent is near zero.
-    const auto& real = semantics_ir_.GetRealLiteral(id);
+    const auto& real = semantics_ir_.GetReal(id);
     real.mantissa.print(out_, /*isSigned=*/false);
     out_ << (real.is_decimal ? 'e' : 'p') << real.exponent;
   }

+ 9 - 17
toolchain/sem_ir/node.cpp

@@ -6,28 +6,20 @@
 
 namespace Carbon::SemIR {
 
-static auto PrintArgs(llvm::raw_ostream& /*out*/,
-                      const Node::NoArgs /*no_args*/) -> void {}
-
-template <typename T>
-static auto PrintArgs(llvm::raw_ostream& out, T arg) -> void {
-  out << ", arg0: " << arg;
-}
-
-template <typename T0, typename T1>
-static auto PrintArgs(llvm::raw_ostream& out, std::pair<T0, T1> args) -> void {
-  PrintArgs(out, args.first);
-  out << ", arg1: " << args.second;
-}
-
 auto Node::Print(llvm::raw_ostream& out) const -> void {
   out << "{kind: " << kind_;
+
+  auto print_args = [&](auto... args) {
+    int n = 0;
+    ((out << ", arg" << n++ << ": " << args), ...);
+  };
+
   // clang warns on unhandled enum values; clang-tidy is incorrect here.
   // NOLINTNEXTLINE(bugprone-switch-missing-default-case)
   switch (kind_) {
-#define CARBON_SEMANTICS_NODE_KIND(Name) \
-  case NodeKind::Name:                   \
-    PrintArgs(out, GetAs##Name());       \
+#define CARBON_SEMANTICS_NODE_KIND(Name)                    \
+  case NodeKind::Name:                                      \
+    std::apply(print_args, As<SemIR::Name>().args_tuple()); \
     break;
 #include "toolchain/sem_ir/node_kind.def"
   }

+ 499 - 219
toolchain/sem_ir/node.h

@@ -9,6 +9,7 @@
 
 #include "common/check.h"
 #include "common/ostream.h"
+#include "common/struct_reflection.h"
 #include "toolchain/base/index_base.h"
 #include "toolchain/parse/tree.h"
 #include "toolchain/sem_ir/builtin_kind.h"
@@ -91,8 +92,8 @@ struct BoolValue : public IndexBase, public Printable<BoolValue> {
 constexpr BoolValue BoolValue::False = BoolValue(0);
 constexpr BoolValue BoolValue::True = BoolValue(1);
 
-// The ID of an integer literal.
-struct IntegerLiteralId : public IndexBase, public Printable<IntegerLiteralId> {
+// The ID of an integer value.
+struct IntegerId : public IndexBase, public Printable<IntegerId> {
   using IndexBase::IndexBase;
   auto Print(llvm::raw_ostream& out) const -> void {
     out << "int";
@@ -143,8 +144,8 @@ constexpr NodeBlockId NodeBlockId::Invalid =
 constexpr NodeBlockId NodeBlockId::Unreachable =
     NodeBlockId(NodeBlockId::InvalidIndex - 1);
 
-// The ID of a real literal.
-struct RealLiteralId : public IndexBase, public Printable<RealLiteralId> {
+// The ID of a real number value.
+struct RealId : public IndexBase, public Printable<RealId> {
   using IndexBase::IndexBase;
   auto Print(llvm::raw_ostream& out) const -> void {
     out << "real";
@@ -207,271 +208,323 @@ struct MemberIndex : public IndexBase, public Printable<MemberIndex> {
   }
 };
 
-// The standard structure for Node. This is trying to provide a minimal
-// amount of information for a node:
+// Data storage for the operands of each kind of node.
 //
-// - 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.
-//
-// For each Kind in NodeKind, a typical flow looks like:
+// 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.
 //
-// - Create a `Node` using `Node::Kind::Make()`
-// - Access cross-Kind members using `node.type_id()` and similar.
-// - Access Kind-specific members using `node.GetAsKind()`, which depending on
-//   the number of members will return one of NoArgs, a single value, or a
-//   `std::pair` of values.
-//   - Using the wrong `node.GetAsKind()` is a programming error, and should
-//     CHECK-fail in debug modes (opt may too, but it's not an API guarantee).
+// A typed node also has:
 //
-// Internally, each Kind uses the `Factory*` types to provide a boilerplate
-// `Make` and `Get` methods.
-class Node : public Printable<Node> {
- public:
-  struct NoArgs {};
+// -  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;
+};
 
-  // Factory base classes are private, then used for public classes. This class
-  // has two public and two private sections to prevent accidents.
- private:
-  // Provides Make and Get to support 0, 1, or 2 arguments for a Node.
-  // These are protected so that child factories can opt in to what pieces they
-  // want to use.
-  template <NodeKind::RawEnumType Kind, typename... ArgTypes>
-  class FactoryBase {
-   protected:
-    static auto Make(Parse::Node parse_node, TypeId type_id,
-                     ArgTypes... arg_ids) -> Node {
-      return Node(parse_node, NodeKind::Create(Kind), type_id,
-                  arg_ids.index...);
-    }
+struct ArrayIndex {
+  NodeId array_id;
+  NodeId index_id;
+};
 
-    static auto Get(Node node) {
-      struct Unused {};
-      return GetImpl<ArgTypes..., Unused>(node);
-    }
+// 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;
+};
 
-   private:
-    // GetImpl handles the different return types based on ArgTypes.
-    template <typename Arg0Type, typename Arg1Type, typename>
-    static auto GetImpl(Node node) -> std::pair<Arg0Type, Arg1Type> {
-      CARBON_CHECK(node.kind() == Kind);
-      return {Arg0Type(node.arg0_), Arg1Type(node.arg1_)};
-    }
-    template <typename Arg0Type, typename>
-    static auto GetImpl(Node node) -> Arg0Type {
-      CARBON_CHECK(node.kind() == Kind);
-      return Arg0Type(node.arg0_);
-    }
-    template <typename>
-    static auto GetImpl(Node node) -> NoArgs {
-      CARBON_CHECK(node.kind() == Kind);
-      return NoArgs();
-    }
-  };
-
-  // Provide Get along with a Make that requires a type.
-  template <NodeKind::RawEnumType Kind, typename... ArgTypes>
-  class Factory : public FactoryBase<Kind, ArgTypes...> {
-   public:
-    using FactoryBase<Kind, ArgTypes...>::Make;
-    using FactoryBase<Kind, ArgTypes...>::Get;
-  };
-
-  // Provides Get along with a Make that assumes the node doesn't produce a
-  // typed value.
-  template <NodeKind::RawEnumType Kind, typename... ArgTypes>
-  class FactoryNoType : public FactoryBase<Kind, ArgTypes...> {
-   public:
-    static auto Make(Parse::Node parse_node, ArgTypes... args) {
-      return FactoryBase<Kind, ArgTypes...>::Make(parse_node, TypeId::Invalid,
-                                                  args...);
-    }
-    using FactoryBase<Kind, ArgTypes...>::Get;
-  };
+struct ArrayType {
+  NodeId bound_id;
+  TypeId element_type_id;
+};
 
- public:
-  // Invalid is in the NodeKind enum, but should never be used.
-  class Invalid {
-   public:
-    static auto Get(Node /*node*/) -> Node::NoArgs {
-      CARBON_FATAL() << "Invalid access";
-    }
-  };
+// 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;
 
-  using AddressOf = Node::Factory<NodeKind::AddressOf, NodeId /*lvalue_id*/>;
+  NodeId lhs_id;
+  NodeId rhs_id;
+};
 
-  using ArrayIndex =
-      Factory<NodeKind::ArrayIndex, NodeId /*array_id*/, NodeId /*index*/>;
+struct BinaryOperatorAdd {
+  NodeId lhs_id;
+  NodeId rhs_id;
+};
 
-  // Initializes an array from a tuple. `tuple_id` is the source tuple
-  // expression. `refs_id` contains one initializer per array element, plus a
-  // final element that is the return slot for the initialization.
-  using ArrayInit = Factory<NodeKind::ArrayInit, NodeId /*tuple_id*/,
-                            NodeBlockId /*refs_id*/>;
+struct BindName {
+  StringId name_id;
+  NodeId value_id;
+};
 
-  using ArrayType = Node::Factory<NodeKind::ArrayType, NodeId /*bound_node_id*/,
-                                  TypeId /*array_element_type_id*/>;
+struct BindValue {
+  NodeId value_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`.
-  using Assign = Node::FactoryNoType<NodeKind::Assign, NodeId /*lhs_id*/,
-                                     NodeId /*rhs_id*/>;
+struct BlockArg {
+  NodeBlockId block_id;
+};
 
-  using BinaryOperatorAdd = Node::Factory<NodeKind::BinaryOperatorAdd,
-                                          NodeId /*lhs_id*/, NodeId /*rhs_id*/>;
+struct BoolLiteral {
+  BoolValue value;
+};
 
-  using BindName =
-      Factory<NodeKind::BindName, StringId /*name_id*/, NodeId /*value_id*/>;
+struct Branch {
+  using HasType = std::false_type;
 
-  using BindValue = Factory<NodeKind::BindValue, NodeId /*value_id*/>;
+  NodeBlockId target_id;
+};
 
-  using BlockArg = Factory<NodeKind::BlockArg, NodeBlockId /*block_id*/>;
+struct BranchIf {
+  using HasType = std::false_type;
 
-  using BoolLiteral = Factory<NodeKind::BoolLiteral, BoolValue /*value*/>;
+  NodeBlockId target_id;
+  NodeId cond_id;
+};
 
-  using Branch = FactoryNoType<NodeKind::Branch, NodeBlockId /*target_id*/>;
+struct BranchWithArg {
+  using HasType = std::false_type;
 
-  using BranchIf = FactoryNoType<NodeKind::BranchIf, NodeBlockId /*target_id*/,
-                                 NodeId /*cond_id*/>;
+  NodeBlockId target_id;
+  NodeId arg_id;
+};
 
-  using BranchWithArg =
-      FactoryNoType<NodeKind::BranchWithArg, NodeBlockId /*target_id*/,
-                    NodeId /*arg*/>;
+struct Builtin {
+  // Builtins don't have a parse node associated with them.
+  using HasParseNode = std::false_type;
 
-  class Builtin {
-   public:
-    static auto Make(BuiltinKind builtin_kind, TypeId type_id) -> Node {
-      // Builtins won't have a Parse::Tree node associated, so we provide the
-      // default invalid one.
-      // This can't use the standard Make function because of the `AsInt()` cast
-      // instead of `.index`.
-      return Node(Parse::Node::Invalid, NodeKind::Builtin, type_id,
-                  builtin_kind.AsInt());
-    }
-    static auto Get(Node node) -> BuiltinKind {
-      return BuiltinKind::FromInt(node.arg0_);
-    }
-  };
-
-  using Call = Factory<NodeKind::Call, NodeBlockId /*refs_id*/,
-                       FunctionId /*function_id*/>;
-
-  using ConstType = Factory<NodeKind::ConstType, TypeId /*inner_id*/>;
-
-  class CrossReference
-      : public FactoryBase<NodeKind::CrossReference,
-                           CrossReferenceIRId /*ir_id*/, NodeId /*node_id*/> {
-   public:
-    static auto Make(TypeId type_id, CrossReferenceIRId ir_id, NodeId node_id)
-        -> 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.
-      return FactoryBase::Make(Parse::Node::Invalid, type_id, ir_id, node_id);
-    }
-    using FactoryBase::Get;
-  };
+  BuiltinKind builtin_kind;
+};
 
-  using Dereference = Factory<NodeKind::Dereference, NodeId /*pointer_id*/>;
+struct Call {
+  NodeBlockId args_id;
+  FunctionId function_id;
+};
 
-  using FunctionDeclaration =
-      FactoryNoType<NodeKind::FunctionDeclaration, FunctionId /*function_id*/>;
+struct ConstType {
+  TypeId inner_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.
-  using InitializeFrom =
-      Factory<NodeKind::InitializeFrom, NodeId /*src_id*/, NodeId /*dest_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;
 
-  using IntegerLiteral =
-      Factory<NodeKind::IntegerLiteral, IntegerLiteralId /*integer_id*/>;
+  CrossReferenceIRId ir_id;
+  NodeId node_id;
+};
 
-  using NameReference = Factory<NodeKind::NameReference, StringId /*name_id*/,
-                                NodeId /*value_id*/>;
+struct Dereference {
+  NodeId pointer_id;
+};
 
-  using NameReferenceUntyped =
-      Factory<NodeKind::NameReferenceUntyped, StringId /*name_id*/,
-              NodeId /*value_id*/>;
+struct FunctionDeclaration {
+  using HasType = std::false_type;
 
-  using Namespace =
-      FactoryNoType<NodeKind::Namespace, NameScopeId /*name_scope_id*/>;
+  FunctionId function_id;
+};
 
-  using NoOp = FactoryNoType<NodeKind::NoOp>;
+// 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;
+};
 
-  using Parameter = Factory<NodeKind::Parameter, StringId /*name_id*/>;
+struct NameReferenceUntyped {
+  StringId name_id;
+  NodeId value_id;
+};
 
-  using PointerType = Factory<NodeKind::PointerType, TypeId /*pointee_id*/>;
+struct Namespace {
+  using HasType = std::false_type;
 
-  using RealLiteral = Factory<NodeKind::RealLiteral, RealLiteralId /*real_id*/>;
+  NameScopeId name_scope_id;
+};
 
-  using Return = FactoryNoType<NodeKind::Return>;
+struct NoOp {
+  using HasType = std::false_type;
+};
 
-  using ReturnExpression =
-      FactoryNoType<NodeKind::ReturnExpression, NodeId /*expr_id*/>;
+struct Parameter {
+  StringId name_id;
+};
 
-  using SpliceBlock = Factory<NodeKind::SpliceBlock, NodeBlockId /*block_id*/,
-                              NodeId /*result_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;
+};
 
-  using StringLiteral =
-      Factory<NodeKind::StringLiteral, StringId /*string_id*/>;
+struct StringLiteral {
+  StringId string_id;
+};
 
-  using StructAccess = Factory<NodeKind::StructAccess, NodeId /*struct_id*/,
-                               MemberIndex /*ref_index*/>;
+struct StructAccess {
+  NodeId struct_id;
+  MemberIndex index;
+};
 
-  using StructInit = Factory<NodeKind::StructInit, NodeId /*literal_id*/,
-                             NodeBlockId /*converted_refs_id*/>;
+struct StructInit {
+  NodeId src_id;
+  NodeBlockId elements_id;
+};
 
-  using StructLiteral =
-      Factory<NodeKind::StructLiteral, NodeBlockId /*refs_id*/>;
+struct StructLiteral {
+  NodeBlockId elements_id;
+};
 
-  using StructType = Factory<NodeKind::StructType, NodeBlockId /*refs_id*/>;
+struct StructType {
+  NodeBlockId fields_id;
+};
 
-  using StructTypeField =
-      FactoryNoType<NodeKind::StructTypeField, StringId /*name_id*/,
-                    TypeId /*type_id*/>;
+struct StructTypeField {
+  using HasType = std::false_type;
 
-  using StructValue = Factory<NodeKind::StructValue, NodeId /*literal_id*/,
-                              NodeBlockId /*converted_refs_id*/>;
+  StringId name_id;
+  TypeId type_id;
+};
 
-  using Temporary =
-      Factory<NodeKind::Temporary, NodeId /*storage_id*/, NodeId /*init_id*/>;
+struct StructValue {
+  NodeId src_id;
+  NodeBlockId elements_id;
+};
 
-  using TemporaryStorage = Factory<NodeKind::TemporaryStorage>;
+struct Temporary {
+  NodeId storage_id;
+  NodeId init_id;
+};
 
-  using TupleAccess = Factory<NodeKind::TupleAccess, NodeId /*tuple_id*/,
-                              MemberIndex /*index*/>;
+struct TemporaryStorage {};
 
-  using TupleIndex =
-      Factory<NodeKind::TupleIndex, NodeId /*tuple_id*/, NodeId /*index*/>;
+struct TupleAccess {
+  NodeId tuple_id;
+  MemberIndex index;
+};
 
-  using TupleInit = Factory<NodeKind::TupleInit, NodeId /*literal_id*/,
-                            NodeBlockId /*converted_refs_id*/>;
+struct TupleIndex {
+  NodeId tuple_id;
+  NodeId index_id;
+};
 
-  using TupleLiteral = Factory<NodeKind::TupleLiteral, NodeBlockId /*refs_id*/>;
+struct TupleInit {
+  NodeId src_id;
+  NodeBlockId elements_id;
+};
 
-  using TupleType = Factory<NodeKind::TupleType, TypeBlockId /*refs_id*/>;
+struct TupleLiteral {
+  NodeBlockId elements_id;
+};
 
-  using TupleValue = Factory<NodeKind::TupleValue, NodeId /*literal_id*/,
-                             NodeBlockId /*converted_refs_id*/>;
+struct TupleType {
+  TypeBlockId elements_id;
+};
 
-  using UnaryOperatorNot =
-      Factory<NodeKind::UnaryOperatorNot, NodeId /*operand_id*/>;
+struct TupleValue {
+  NodeId src_id;
+  NodeBlockId elements_id;
+};
 
-  using ValueAsReference =
-      Factory<NodeKind::ValueAsReference, NodeId /*value_id*/>;
+struct UnaryOperatorNot {
+  NodeId operand_id;
+};
 
-  using VarStorage = Factory<NodeKind::VarStorage, StringId /*name_id*/>;
+struct ValueAsReference {
+  NodeId value_id;
+};
 
-  explicit Node()
-      : Node(Parse::Node::Invalid, NodeKind::Invalid, TypeId::Invalid) {}
+struct VarStorage {
+  StringId name_id;
+};
+}  // namespace NodeData
 
-  // Provide `node.GetAsKind()` as an instance method for all kinds, essentially
-  // an alias for`Node::Kind::Get(node)`.
-#define CARBON_SEMANTICS_NODE_KIND(Name) \
-  auto GetAs##Name() const { return Name::Get(*this); }
-#include "toolchain/sem_ir/node_kind.def"
+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:
+//
+// - `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.
+//
+// For each Kind in NodeKind, a typical flow looks like:
+//
+// - Create a specific kind of `Node` using the appropriate `TypedNode`
+//   constructor.
+// - Access cross-Kind members using `node.type_id()` and similar.
+// - 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
+//     guarantee).
+//   - Use `node.TryAs<Kind>()` 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()) {}
+
+  // 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(kind() == Typed::Kind) << "Casting node of kind " << kind()
+                                        << " to wrong kind " << Typed::Kind;
+    return Typed::FromRawData(parse_node_, type_id_, arg0_, 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 (kind() == Typed::Kind) {
+      return As<Typed>();
+    } else {
+      return std::nullopt;
+    }
+  }
 
   auto parse_node() const -> Parse::Node { return parse_node_; }
   auto kind() const -> NodeKind { return kind_; }
@@ -482,10 +535,6 @@ class Node : public Printable<Node> {
   auto Print(llvm::raw_ostream& out) const -> void;
 
  private:
-  // Builtins have peculiar construction, so they are a friend rather than using
-  // a factory base class.
-  friend struct NodeForBuiltin;
-
   explicit Node(Parse::Node parse_node, NodeKind kind, TypeId type_id,
                 int32_t arg0 = NodeId::InvalidIndex,
                 int32_t arg1 = NodeId::InvalidIndex)
@@ -499,7 +548,7 @@ class Node : public Printable<Node> {
   NodeKind kind_;
   TypeId type_id_;
 
-  // Use GetAsKind to access arg0 and arg1.
+  // Use `As` to access arg0 and arg1.
   int32_t arg0_;
   int32_t arg1_;
 };
@@ -510,6 +559,237 @@ 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_SEMANTICS_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
+// TypedNodeBase or UntypedNodeBase.
+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);
+}
+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
+
 // Provides base support for use of Id types as DenseMap/DenseSet keys.
 // Instantiated below.
 template <typename Id>

+ 0 - 2
toolchain/sem_ir/node_kind.def

@@ -40,8 +40,6 @@
 #error "Must define the x-macro to use this file."
 #endif
 
-CARBON_SEMANTICS_NODE_KIND_IMPL(Invalid, "invalid", None, NotTerminator)
-
 // A cross-reference between IRs.
 CARBON_SEMANTICS_NODE_KIND_IMPL(CrossReference, "xref", Typed, NotTerminator)