Commit d0c022f734

mlugg <mlugg@mlugg.co.uk>
2024-03-05 07:02:58
compiler: namespace type equivalence based on AST node + captures
This implements the accepted proposal #18816. Namespace-owning types (struct, enum, union, opaque) are no longer unique whenever analysed; instead, their identity is determined based on their AST node and the set of values they capture. Reified types (`@Type`) are deduplicated based on the structure of the type created. For instance, if two structs are created by the same reification with identical fields, layout, etc, they will be the same type. This commit does not produce a working compiler; the next commit, adding captures for decl references, is necessary. It felt appropriate to split this up. Resolves: #18816
1 parent 8ec6f73
src/codegen/llvm.zig
@@ -3548,12 +3548,11 @@ pub const Object = struct {
                     );
                     return ty;
                 },
-                .opaque_type => |opaque_type| {
+                .opaque_type => {
                     const gop = try o.type_map.getOrPut(o.gpa, t.toIntern());
                     if (!gop.found_existing) {
-                        const name = try o.builder.string(ip.stringToSlice(
-                            try mod.opaqueFullyQualifiedName(opaque_type),
-                        ));
+                        const decl = mod.declPtr(ip.loadOpaqueType(t.toIntern()).decl);
+                        const name = try o.builder.string(ip.stringToSlice(try decl.getFullyQualifiedName(mod)));
                         gop.value_ptr.* = try o.builder.opaqueType(name);
                     }
                     return gop.value_ptr.*;
src/InternPool.zig
@@ -344,6 +344,7 @@ const KeyAdapter = struct {
 
     pub fn eql(ctx: @This(), a: Key, b_void: void, b_map_index: usize) bool {
         _ = b_void;
+        if (ctx.intern_pool.items.items(.tag)[b_map_index] == .removed) return false;
         return ctx.intern_pool.indexToKey(@as(Index, @enumFromInt(b_map_index))).eql(a, ctx.intern_pool);
     }
 
@@ -551,14 +552,14 @@ pub const Key = union(enum) {
     /// This represents a struct that has been explicitly declared in source code,
     /// or was created with `@Type`. It is unique and based on a declaration.
     /// It may be a tuple, if declared like this: `struct {A, B, C}`.
-    struct_type: StructType,
+    struct_type: NamespaceType,
     /// This is an anonymous struct or tuple type which has no corresponding
     /// declaration. It is used for types that have no `struct` keyword in the
     /// source code, and were not created via `@Type`.
     anon_struct_type: AnonStructType,
-    union_type: Key.UnionType,
-    opaque_type: OpaqueType,
-    enum_type: EnumType,
+    union_type: NamespaceType,
+    opaque_type: NamespaceType,
+    enum_type: NamespaceType,
     func_type: FuncType,
     error_set_type: ErrorSetType,
     /// The payload is the function body, either a `func_decl` or `func_instance`.
@@ -703,66 +704,41 @@ pub const Key = union(enum) {
         }
     };
 
-    /// This is the hashmap key. To fetch other data associated with the struct, see `loadStructType`.
-    pub const StructType = struct {
-        /// The struct's owner Decl. `none` when the struct is `@TypeOf(.{})`.
-        decl: OptionalDeclIndex,
-    };
-
-    /// This is the hashmap key. To fetch other data associated with the opaque, see `loadOpaqueType`.
-    pub const OpaqueType = struct {
-        /// The opaque's owner Decl.
-        decl: DeclIndex,
-    };
-
-    /// This is the hashmap key. To fetch other data associated with the union, see `loadUnionType`.
-    pub const UnionType = struct {
-        /// The union's owner Decl.
-        decl: DeclIndex,
-    };
-
-    /// This is the hashmap key. To fetch other data associated with the enum, see `loadEnumType`.
-    pub const EnumType = struct {
-        /// The enum's owner Decl.
-        decl: DeclIndex,
-    };
-
-    pub const IncompleteEnumType = struct {
-        /// Same as corresponding `EnumType` field.
-        decl: DeclIndex,
-        /// Same as corresponding `EnumType` field.
-        namespace: OptionalNamespaceIndex,
-        /// The field names and field values are not known yet, but
-        /// the number of fields must be known ahead of time.
-        fields_len: u32,
-        /// This information is needed so that the size does not change
-        /// later when populating field values.
-        has_values: bool,
-        /// Same as corresponding `EnumType` field.
-        tag_mode: LoadedEnumType.TagMode,
-        /// This may be updated via `setTagType` later.
-        tag_ty: Index = .none,
-        zir_index: TrackedInst.Index.Optional,
-        captures: []const CaptureValue,
-
-        pub fn toEnumType(self: @This()) LoadedEnumType {
-            if (true) @compileError("AHHHH");
-            return .{
-                .decl = self.decl,
-                .namespace = self.namespace,
-                .tag_ty = self.tag_ty,
-                .tag_mode = self.tag_mode,
-                .names = .{ .start = 0, .len = 0 },
-                .values = .{ .start = 0, .len = 0 },
-                .zir_index = self.zir_index,
-            };
-        }
-
-        /// Only the decl is used for hashing and equality, so we can construct
-        /// this minimal key for use with `map`.
-        pub fn toKey(self: @This()) Key {
-            return .{ .enum_type = .{ .decl = self.decl } };
-        }
+    /// This is the hashmap key. To fetch other data associated with the type, see:
+    /// * `loadStructType`
+    /// * `loadUnionType`
+    /// * `loadEnumType`
+    /// * `loadOpaqueType`
+    pub const NamespaceType = union(enum) {
+        /// This type corresponds to an actual source declaration, e.g. `struct { ... }`.
+        /// It is hashed based on its ZIR instruction index and set of captures.
+        declared: struct {
+            /// A `struct_decl`, `union_decl`, `enum_decl`, or `opaque_decl` instruction.
+            zir_index: TrackedInst.Index,
+            /// The captured values of this type. These values must be fully resolved per the language spec.
+            captures: union(enum) {
+                owned: CaptureValue.Slice,
+                external: []const CaptureValue,
+            },
+        },
+        /// This type is an automatically-generated enum tag type for a union.
+        /// It is hashed based on the index of the union type it corresponds to.
+        generated_tag: struct {
+            /// The union for which this is a tag type.
+            union_type: Index,
+        },
+        /// This type originates from a reification via `@Type`.
+        /// It is hased based on its ZIR instruction index and fields, attributes, etc.
+        /// To avoid making this key overly complex, the type-specific data is hased by Sema.
+        reified: struct {
+            /// A `reify` instruction.
+            zir_index: TrackedInst.Index,
+            /// A hash of this type's attributes, fields, etc, generated by Sema.
+            type_hash: u64,
+        },
+        /// This type is `@TypeOf(.{})`.
+        /// TODO: can we change the language spec to not special-case this type?
+        empty_struct: void,
     };
 
     pub const FuncType = struct {
@@ -1113,12 +1089,37 @@ pub const Key = union(enum) {
                 .payload => |y| Hash.hash(seed + 1, asBytes(&x.ty) ++ asBytes(&y)),
             },
 
-            inline .opaque_type,
+            .variable => |variable| Hash.hash(seed, asBytes(&variable.decl)),
+
+            .opaque_type,
             .enum_type,
-            .variable,
             .union_type,
             .struct_type,
-            => |x| Hash.hash(seed, asBytes(&x.decl)),
+            => |namespace_type| {
+                var hasher = Hash.init(seed);
+                std.hash.autoHash(&hasher, std.meta.activeTag(namespace_type));
+                switch (namespace_type) {
+                    .declared => |declared| {
+                        std.hash.autoHash(&hasher, declared.zir_index);
+                        const captures = switch (declared.captures) {
+                            .owned => |cvs| cvs.get(ip),
+                            .external => |cvs| cvs,
+                        };
+                        for (captures) |cv| {
+                            std.hash.autoHash(&hasher, cv);
+                        }
+                    },
+                    .generated_tag => |generated_tag| {
+                        std.hash.autoHash(&hasher, generated_tag.union_type);
+                    },
+                    .reified => |reified| {
+                        std.hash.autoHash(&hasher, reified.zir_index);
+                        std.hash.autoHash(&hasher, reified.type_hash);
+                    },
+                    .empty_struct => {},
+                }
+                return hasher.final();
+            },
 
             .int => |int| {
                 var hasher = Hash.init(seed);
@@ -1523,21 +1524,31 @@ pub const Key = union(enum) {
                 }
             },
 
-            .opaque_type => |a_info| {
-                const b_info = b.opaque_type;
-                return a_info.decl == b_info.decl;
-            },
-            .enum_type => |a_info| {
-                const b_info = b.enum_type;
-                return a_info.decl == b_info.decl;
-            },
-            .union_type => |a_info| {
-                const b_info = b.union_type;
-                return a_info.decl == b_info.decl;
-            },
-            .struct_type => |a_info| {
-                const b_info = b.struct_type;
-                return a_info.decl == b_info.decl;
+            inline .opaque_type, .enum_type, .union_type, .struct_type => |a_info, a_tag_ct| {
+                const b_info = @field(b, @tagName(a_tag_ct));
+                if (std.meta.activeTag(a_info) != b_info) return false;
+                switch (a_info) {
+                    .declared => |a_d| {
+                        const b_d = b_info.declared;
+                        if (a_d.zir_index != b_d.zir_index) return false;
+                        const a_captures = switch (a_d.captures) {
+                            .owned => |s| s.get(ip),
+                            .external => |cvs| cvs,
+                        };
+                        const b_captures = switch (b_d.captures) {
+                            .owned => |s| s.get(ip),
+                            .external => |cvs| cvs,
+                        };
+                        return std.mem.eql(u32, @ptrCast(a_captures), @ptrCast(b_captures));
+                    },
+                    .generated_tag => |a_gt| return a_gt.union_type == b_info.generated_tag.union_type,
+                    .reified => |a_r| {
+                        const b_r = b_info.reified;
+                        return a_r.zir_index == b_r.zir_index and
+                            a_r.type_hash == b_r.type_hash;
+                    },
+                    .empty_struct => return true,
+                }
             },
             .aggregate => |a_info| {
                 const b_info = b.aggregate;
@@ -1685,7 +1696,7 @@ pub const LoadedUnionType = struct {
     /// The Decl that corresponds to the union itself.
     decl: DeclIndex,
     /// Represents the declarations inside this union.
-    namespace: NamespaceIndex,
+    namespace: OptionalNamespaceIndex,
     /// The enum tag type.
     enum_tag_ty: Index,
     /// List of field types in declaration order.
@@ -1695,8 +1706,8 @@ pub const LoadedUnionType = struct {
     /// `none` means the ABI alignment of the type.
     /// If this slice has length 0 it means all elements are `none`.
     field_aligns: Alignment.Slice,
-    /// Index of the union_decl ZIR instruction.
-    zir_index: TrackedInst.Index.Optional,
+    /// Index of the union_decl or reify ZIR instruction.
+    zir_index: TrackedInst.Index,
     captures: CaptureValue.Slice,
 
     pub const RuntimeTag = enum(u2) {
@@ -1845,6 +1856,9 @@ pub fn loadUnionType(ip: *const InternPool, index: Index) LoadedUnionType {
         .len = captures_len,
     };
     extra_index += captures_len;
+    if (type_union.data.flags.is_reified) {
+        extra_index += 2; // PackedU64
+    }
 
     const field_types: Index.Slice = .{
         .start = extra_index,
@@ -1880,7 +1894,8 @@ pub const LoadedStructType = struct {
     decl: OptionalDeclIndex,
     /// `none` when the struct has no declarations.
     namespace: OptionalNamespaceIndex,
-    /// Index of the `struct_decl` ZIR instruction.
+    /// Index of the `struct_decl` or `reify` ZIR instruction.
+    /// Only `none` when the struct is `@TypeOf(.{})`.
     zir_index: TrackedInst.Index.Optional,
     layout: std.builtin.Type.ContainerLayout,
     field_names: NullTerminatedString.Slice,
@@ -2239,6 +2254,9 @@ pub fn loadStructType(ip: *const InternPool, index: Index) LoadedStructType {
                 .len = captures_len,
             };
             extra_index += captures_len;
+            if (extra.data.flags.is_reified) {
+                extra_index += 2; // PackedU64
+            }
             const field_types: Index.Slice = .{
                 .start = extra_index,
                 .len = fields_len,
@@ -2286,7 +2304,7 @@ pub fn loadStructType(ip: *const InternPool, index: Index) LoadedStructType {
                 .extra_index = item.data,
                 .decl = extra.data.decl.toOptional(),
                 .namespace = namespace,
-                .zir_index = extra.data.zir_index,
+                .zir_index = extra.data.zir_index.toOptional(),
                 .layout = if (extra.data.flags.is_extern) .Extern else .Auto,
                 .field_names = names,
                 .field_types = field_types,
@@ -2314,6 +2332,9 @@ pub fn loadStructType(ip: *const InternPool, index: Index) LoadedStructType {
                 .len = captures_len,
             };
             extra_index += captures_len;
+            if (extra.data.flags.is_reified) {
+                extra_index += 2; // PackedU64
+            }
             const field_types: Index.Slice = .{
                 .start = extra_index,
                 .len = fields_len,
@@ -2336,7 +2357,7 @@ pub fn loadStructType(ip: *const InternPool, index: Index) LoadedStructType {
                 .extra_index = item.data,
                 .decl = extra.data.decl.toOptional(),
                 .namespace = extra.data.namespace,
-                .zir_index = extra.data.zir_index,
+                .zir_index = extra.data.zir_index.toOptional(),
                 .layout = .Packed,
                 .field_names = field_names,
                 .field_types = field_types,
@@ -2372,6 +2393,7 @@ const LoadedEnumType = struct {
     names_map: MapIndex,
     /// This is guaranteed to not be `.none` if explicit values are provided.
     values_map: OptionalMapIndex,
+    /// This is `none` only if this is a generated tag type.
     zir_index: TrackedInst.Index.Optional,
     captures: CaptureValue.Slice,
 
@@ -2425,15 +2447,23 @@ const LoadedEnumType = struct {
 
 pub fn loadEnumType(ip: *const InternPool, index: Index) LoadedEnumType {
     const item = ip.items.get(@intFromEnum(index));
-    switch (item.tag) {
+    const tag_mode: LoadedEnumType.TagMode = switch (item.tag) {
         .type_enum_auto => {
             const extra = ip.extraDataTrail(EnumAuto, item.data);
+            var extra_index: u32 = @intCast(extra.end);
+            if (extra.data.zir_index == .none) {
+                extra_index += 1; // owner_union
+            }
+            const captures_len = if (extra.data.captures_len == std.math.maxInt(u32)) c: {
+                extra_index += 2; // type_hash: PackedU64
+                break :c 0;
+            } else extra.data.captures_len;
             return .{
                 .decl = extra.data.decl,
                 .namespace = extra.data.namespace,
                 .tag_ty = extra.data.int_tag_type,
                 .names = .{
-                    .start = @intCast(extra.end + extra.data.captures_len),
+                    .start = extra_index + captures_len,
                     .len = extra.data.fields_len,
                 },
                 .values = .{ .start = 0, .len = 0 },
@@ -2442,41 +2472,45 @@ pub fn loadEnumType(ip: *const InternPool, index: Index) LoadedEnumType {
                 .values_map = .none,
                 .zir_index = extra.data.zir_index,
                 .captures = .{
-                    .start = @intCast(extra.end),
-                    .len = extra.data.captures_len,
-                },
-            };
-        },
-        .type_enum_explicit, .type_enum_nonexhaustive => {
-            const extra = ip.extraDataTrail(EnumExplicit, item.data);
-            return .{
-                .decl = extra.data.decl,
-                .namespace = extra.data.namespace,
-                .tag_ty = extra.data.int_tag_type,
-                .names = .{
-                    .start = @intCast(extra.end + extra.data.captures_len),
-                    .len = extra.data.fields_len,
-                },
-                .values = .{
-                    .start = @intCast(extra.end + extra.data.captures_len + extra.data.fields_len),
-                    .len = if (extra.data.values_map != .none) extra.data.fields_len else 0,
-                },
-                .tag_mode = switch (item.tag) {
-                    .type_enum_explicit => .explicit,
-                    .type_enum_nonexhaustive => .nonexhaustive,
-                    else => unreachable,
-                },
-                .names_map = extra.data.names_map,
-                .values_map = extra.data.values_map,
-                .zir_index = extra.data.zir_index,
-                .captures = .{
-                    .start = @intCast(extra.end),
-                    .len = extra.data.captures_len,
+                    .start = extra_index,
+                    .len = captures_len,
                 },
             };
         },
+        .type_enum_explicit => .explicit,
+        .type_enum_nonexhaustive => .nonexhaustive,
         else => unreachable,
-    }
+    };
+    const extra = ip.extraDataTrail(EnumExplicit, item.data);
+    var extra_index: u32 = @intCast(extra.end);
+    if (extra.data.zir_index == .none) {
+        extra_index += 1; // owner_union
+    }
+    const captures_len = if (extra.data.captures_len == std.math.maxInt(u32)) c: {
+        extra_index += 2; // type_hash: PackedU64
+        break :c 0;
+    } else extra.data.captures_len;
+    return .{
+        .decl = extra.data.decl,
+        .namespace = extra.data.namespace,
+        .tag_ty = extra.data.int_tag_type,
+        .names = .{
+            .start = extra_index + captures_len,
+            .len = extra.data.fields_len,
+        },
+        .values = .{
+            .start = extra_index + captures_len + extra.data.fields_len,
+            .len = if (extra.data.values_map != .none) extra.data.fields_len else 0,
+        },
+        .tag_mode = tag_mode,
+        .names_map = extra.data.names_map,
+        .values_map = extra.data.values_map,
+        .zir_index = extra.data.zir_index,
+        .captures = .{
+            .start = extra_index,
+            .len = captures_len,
+        },
+    };
 }
 
 /// Note that this type doubles as the payload for `Tag.type_opaque`.
@@ -2484,9 +2518,9 @@ pub const LoadedOpaqueType = struct {
     /// The opaque's owner Decl.
     decl: DeclIndex,
     /// Contains the declarations inside this opaque.
-    namespace: NamespaceIndex,
-    /// The index of the `opaque_decl` instruction.
-    zir_index: TrackedInst.Index.Optional,
+    namespace: OptionalNamespaceIndex,
+    /// Index of the `opaque_decl` or `reify` instruction.
+    zir_index: TrackedInst.Index,
     captures: CaptureValue.Slice,
 };
 
@@ -2494,13 +2528,17 @@ pub fn loadOpaqueType(ip: *const InternPool, index: Index) LoadedOpaqueType {
     assert(ip.items.items(.tag)[@intFromEnum(index)] == .type_opaque);
     const extra_index = ip.items.items(.data)[@intFromEnum(index)];
     const extra = ip.extraDataTrail(Tag.TypeOpaque, extra_index);
+    const captures_len = if (extra.data.captures_len == std.math.maxInt(u32))
+        0
+    else
+        extra.data.captures_len;
     return .{
         .decl = extra.data.decl,
         .namespace = extra.data.namespace,
         .zir_index = extra.data.zir_index,
         .captures = .{
             .start = extra.end,
-            .len = extra.data.captures_len,
+            .len = captures_len,
         },
     };
 }
@@ -2693,6 +2731,7 @@ pub const Index = enum(u32) {
             },
         };
 
+        removed: void,
         type_int_signed: struct { data: u32 },
         type_int_unsigned: struct { data: u32 },
         type_array_big: struct { data: *Array },
@@ -3100,6 +3139,12 @@ comptime {
 }
 
 pub const Tag = enum(u8) {
+    /// This special tag represents a value which was removed from this pool via
+    /// `InternPool.remove`. The item remains allocated to preserve indices, but
+    /// lookups will consider it not equal to any other item, and all queries
+    /// assert not this tag. `data` is unused.
+    removed,
+
     /// An integer type.
     /// data is number of bits
     type_int_signed,
@@ -3367,6 +3412,7 @@ pub const Tag = enum(u8) {
 
     fn Payload(comptime tag: Tag) type {
         return switch (tag) {
+            .removed => unreachable,
             .type_int_signed => unreachable,
             .type_int_unsigned => unreachable,
             .type_array_big => Array,
@@ -3544,8 +3590,9 @@ pub const Tag = enum(u8) {
     /// Trailing:
     /// 0. captures_len: u32 // if `any_captures`
     /// 1. capture: CaptureValue // for each `captures_len`
-    /// 2. field type: Index for each field; declaration order
-    /// 3. field align: Alignment for each field; declaration order
+    /// 2. type_hash: PackedU64 // if `is_reified`
+    /// 3. field type: Index for each field; declaration order
+    /// 4. field align: Alignment for each field; declaration order
     pub const TypeUnion = struct {
         flags: Flags,
         /// This could be provided through the tag type, but it is more convenient
@@ -3557,10 +3604,10 @@ pub const Tag = enum(u8) {
         /// Only valid after .have_layout
         padding: u32,
         decl: DeclIndex,
-        namespace: NamespaceIndex,
+        namespace: OptionalNamespaceIndex,
         /// The enum that provides the list of field names and values.
         tag_ty: Index,
-        zir_index: TrackedInst.Index.Optional,
+        zir_index: TrackedInst.Index,
 
         pub const Flags = packed struct(u32) {
             any_captures: bool,
@@ -3573,19 +3620,21 @@ pub const Tag = enum(u8) {
             assumed_runtime_bits: bool,
             assumed_pointer_aligned: bool,
             alignment: Alignment,
-            _: u13 = 0,
+            is_reified: bool,
+            _: u12 = 0,
         };
     };
 
     /// Trailing:
     /// 0. captures_len: u32 // if `any_captures`
     /// 1. capture: CaptureValue // for each `captures_len`
-    /// 2. type: Index for each fields_len
-    /// 3. name: NullTerminatedString for each fields_len
-    /// 4. init: Index for each fields_len // if tag is type_struct_packed_inits
+    /// 2. type_hash: PackedU64 // if `is_reified`
+    /// 3. type: Index for each fields_len
+    /// 4. name: NullTerminatedString for each fields_len
+    /// 5. init: Index for each fields_len // if tag is type_struct_packed_inits
     pub const TypeStructPacked = struct {
         decl: DeclIndex,
-        zir_index: TrackedInst.Index.Optional,
+        zir_index: TrackedInst.Index,
         fields_len: u32,
         namespace: OptionalNamespaceIndex,
         backing_int_ty: Index,
@@ -3597,7 +3646,8 @@ pub const Tag = enum(u8) {
             /// Dependency loop detection when resolving field inits.
             field_inits_wip: bool,
             inits_resolved: bool,
-            _: u29 = 0,
+            is_reified: bool,
+            _: u28 = 0,
         };
     };
 
@@ -3618,24 +3668,25 @@ pub const Tag = enum(u8) {
     /// Trailing:
     /// 0. captures_len: u32 // if `any_captures`
     /// 1. capture: CaptureValue // for each `captures_len`
-    /// 2. type: Index for each field in declared order
-    /// 3. if not is_tuple:
+    /// 2. type_hash: PackedU64 // if `is_reified`
+    /// 3. type: Index for each field in declared order
+    /// 4. if not is_tuple:
     ///    names_map: MapIndex,
     ///    name: NullTerminatedString // for each field in declared order
-    /// 4. if any_default_inits:
+    /// 5. if any_default_inits:
     ///    init: Index // for each field in declared order
-    /// 5. if has_namespace:
+    /// 6. if has_namespace:
     ///    namespace: NamespaceIndex
-    /// 6. if any_aligned_fields:
+    /// 7. if any_aligned_fields:
     ///    align: Alignment // for each field in declared order
-    /// 7. if any_comptime_fields:
+    /// 8. if any_comptime_fields:
     ///    field_is_comptime_bits: u32 // minimal number of u32s needed, LSB is field 0
-    /// 8. if not is_extern:
+    /// 9. if not is_extern:
     ///    field_index: RuntimeOrder // for each field in runtime order
-    /// 9. field_offset: u32 // for each field in declared order, undef until layout_resolved
+    /// 10. field_offset: u32 // for each field in declared order, undef until layout_resolved
     pub const TypeStruct = struct {
         decl: DeclIndex,
-        zir_index: TrackedInst.Index.Optional,
+        zir_index: TrackedInst.Index,
         fields_len: u32,
         flags: Flags,
         size: u32,
@@ -3670,8 +3721,8 @@ pub const Tag = enum(u8) {
             // The types and all its fields have had their layout resolved. Even through pointer,
             // which `layout_resolved` does not ensure.
             fully_resolved: bool,
-
-            _: u7 = 0,
+            is_reified: bool,
+            _: u6 = 0,
         };
     };
 
@@ -3681,9 +3732,10 @@ pub const Tag = enum(u8) {
         /// The opaque's owner Decl.
         decl: DeclIndex,
         /// Contains the declarations inside this opaque.
-        namespace: NamespaceIndex,
+        namespace: OptionalNamespaceIndex,
         /// The index of the `opaque_decl` instruction.
-        zir_index: TrackedInst.Index.Optional,
+        zir_index: TrackedInst.Index,
+        /// `std.math.maxInt(u32)` indicates this type is reified.
         captures_len: u32,
     };
 };
@@ -3992,12 +4044,15 @@ pub const Array = struct {
 };
 
 /// Trailing:
-/// 0. capture: CaptureValue // for each `captures_len`
-/// 1. field name: NullTerminatedString for each fields_len; declaration order
-/// 2. tag value: Index for each fields_len; declaration order
+/// 0. owner_union: Index // if `zir_index == .none`
+/// 1. capture: CaptureValue // for each `captures_len`
+/// 2. type_hash: PackedU64 // if reified (`captures_len == std.math.maxInt(u32)`)
+/// 3. field name: NullTerminatedString for each fields_len; declaration order
+/// 4. tag value: Index for each fields_len; declaration order
 pub const EnumExplicit = struct {
     /// The Decl that corresponds to the enum itself.
     decl: DeclIndex,
+    /// `std.math.maxInt(u32)` indicates this type is reified.
     captures_len: u32,
     /// This may be `none` if there are no declarations.
     namespace: OptionalNamespaceIndex,
@@ -4011,15 +4066,20 @@ pub const EnumExplicit = struct {
     /// If this is `none`, it means the trailing tag values are absent because
     /// they are auto-numbered.
     values_map: OptionalMapIndex,
+    /// `none` means this is a generated tag type.
+    /// There will be a trailing union type for which this is a tag.
     zir_index: TrackedInst.Index.Optional,
 };
 
 /// Trailing:
-/// 0. capture: CaptureValue // for each `captures_len`
-/// 1. field name: NullTerminatedString for each fields_len; declaration order
+/// 0. owner_union: Index // if `zir_index == .none`
+/// 1. capture: CaptureValue // for each `captures_len`
+/// 2. type_hash: PackedU64 // if reified (`captures_len == std.math.maxInt(u32)`)
+/// 3. field name: NullTerminatedString for each fields_len; declaration order
 pub const EnumAuto = struct {
     /// The Decl that corresponds to the enum itself.
     decl: DeclIndex,
+    /// `std.math.maxInt(u32)` indicates this type is reified.
     captures_len: u32,
     /// This may be `none` if there are no declarations.
     namespace: OptionalNamespaceIndex,
@@ -4029,6 +4089,8 @@ pub const EnumAuto = struct {
     fields_len: u32,
     /// Maps field names to declaration index.
     names_map: MapIndex,
+    /// `none` means this is a generated tag type.
+    /// There will be a trailing union type for which this is a tag.
     zir_index: TrackedInst.Index.Optional,
 };
 
@@ -4269,6 +4331,7 @@ pub fn indexToKey(ip: *const InternPool, index: Index) Key {
     const item = ip.items.get(@intFromEnum(index));
     const data = item.data;
     return switch (item.tag) {
+        .removed => unreachable,
         .type_int_signed => .{
             .int_type = .{
                 .signedness = .signed,
@@ -4330,31 +4393,123 @@ pub fn indexToKey(ip: *const InternPool, index: Index) Key {
             .inferred_error_set_type = @enumFromInt(data),
         },
 
-        .type_opaque => .{ .opaque_type = .{
-            .decl = ip.extraData(Tag.TypeOpaque, data).decl,
+        .type_opaque => .{ .opaque_type = ns: {
+            const extra = ip.extraDataTrail(Tag.TypeOpaque, data);
+            if (extra.data.captures_len == std.math.maxInt(u32)) {
+                break :ns .{ .reified = .{
+                    .zir_index = extra.data.zir_index,
+                    .type_hash = 0,
+                } };
+            }
+            break :ns .{ .declared = .{
+                .zir_index = extra.data.zir_index,
+                .captures = .{ .owned = .{
+                    .start = extra.end,
+                    .len = extra.data.captures_len,
+                } },
+            } };
         } },
 
-        .type_struct => .{ .struct_type = if (data == 0) .{
-            .decl = .none,
-        } else .{
-            .decl = ip.extraData(Tag.TypeStruct, data).decl.toOptional(),
+        .type_struct => .{ .struct_type = ns: {
+            if (data == 0) break :ns .empty_struct;
+            const extra = ip.extraDataTrail(Tag.TypeStruct, data);
+            if (extra.data.flags.is_reified) {
+                assert(!extra.data.flags.any_captures);
+                break :ns .{ .reified = .{
+                    .zir_index = extra.data.zir_index,
+                    .type_hash = ip.extraData(PackedU64, extra.end).get(),
+                } };
+            }
+            break :ns .{ .declared = .{
+                .zir_index = extra.data.zir_index,
+                .captures = .{ .owned = if (extra.data.flags.any_captures) .{
+                    .start = extra.end + 1,
+                    .len = ip.extra.items[extra.end],
+                } else .{ .start = 0, .len = 0 } },
+            } };
         } },
 
-        .type_struct_packed, .type_struct_packed_inits => .{ .struct_type = .{
-            .decl = ip.extraData(Tag.TypeStructPacked, data).decl.toOptional(),
+        .type_struct_packed, .type_struct_packed_inits => .{ .struct_type = ns: {
+            const extra = ip.extraDataTrail(Tag.TypeStructPacked, data);
+            if (extra.data.flags.is_reified) {
+                assert(!extra.data.flags.any_captures);
+                break :ns .{ .reified = .{
+                    .zir_index = extra.data.zir_index,
+                    .type_hash = ip.extraData(PackedU64, extra.end).get(),
+                } };
+            }
+            break :ns .{ .declared = .{
+                .zir_index = extra.data.zir_index,
+                .captures = .{ .owned = if (extra.data.flags.any_captures) .{
+                    .start = extra.end + 1,
+                    .len = ip.extra.items[extra.end],
+                } else .{ .start = 0, .len = 0 } },
+            } };
         } },
 
         .type_struct_anon => .{ .anon_struct_type = extraTypeStructAnon(ip, data) },
         .type_tuple_anon => .{ .anon_struct_type = extraTypeTupleAnon(ip, data) },
-        .type_union => .{ .union_type = .{
-            .decl = ip.extraData(Tag.TypeUnion, data).decl,
+        .type_union => .{ .union_type = ns: {
+            const extra = ip.extraDataTrail(Tag.TypeUnion, data);
+            if (extra.data.flags.is_reified) {
+                assert(!extra.data.flags.any_captures);
+                break :ns .{ .reified = .{
+                    .zir_index = extra.data.zir_index,
+                    .type_hash = ip.extraData(PackedU64, extra.end).get(),
+                } };
+            }
+            break :ns .{ .declared = .{
+                .zir_index = extra.data.zir_index,
+                .captures = .{ .owned = if (extra.data.flags.any_captures) .{
+                    .start = extra.end + 1,
+                    .len = ip.extra.items[extra.end],
+                } else .{ .start = 0, .len = 0 } },
+            } };
         } },
 
-        .type_enum_auto => .{ .enum_type = .{
-            .decl = ip.extraData(EnumAuto, data).decl,
+        .type_enum_auto => .{ .enum_type = ns: {
+            const extra = ip.extraDataTrail(EnumAuto, data);
+            const zir_index = extra.data.zir_index.unwrap() orelse {
+                assert(extra.data.captures_len == 0);
+                break :ns .{ .generated_tag = .{
+                    .union_type = @enumFromInt(ip.extra.items[extra.end]),
+                } };
+            };
+            if (extra.data.captures_len == std.math.maxInt(u32)) {
+                break :ns .{ .reified = .{
+                    .zir_index = zir_index,
+                    .type_hash = ip.extraData(PackedU64, extra.end).get(),
+                } };
+            }
+            break :ns .{ .declared = .{
+                .zir_index = zir_index,
+                .captures = .{ .owned = .{
+                    .start = extra.end,
+                    .len = extra.data.captures_len,
+                } },
+            } };
         } },
-        .type_enum_explicit, .type_enum_nonexhaustive => .{ .enum_type = .{
-            .decl = ip.extraData(EnumExplicit, data).decl,
+        .type_enum_explicit, .type_enum_nonexhaustive => .{ .enum_type = ns: {
+            const extra = ip.extraDataTrail(EnumExplicit, data);
+            const zir_index = extra.data.zir_index.unwrap() orelse {
+                assert(extra.data.captures_len == 0);
+                break :ns .{ .generated_tag = .{
+                    .union_type = @enumFromInt(ip.extra.items[extra.end]),
+                } };
+            };
+            if (extra.data.captures_len == std.math.maxInt(u32)) {
+                break :ns .{ .reified = .{
+                    .zir_index = zir_index,
+                    .type_hash = ip.extraData(PackedU64, extra.end).get(),
+                } };
+            }
+            break :ns .{ .declared = .{
+                .zir_index = zir_index,
+                .captures = .{ .owned = .{
+                    .start = extra.end,
+                    .len = extra.data.captures_len,
+                } },
+            } };
         } },
         .type_function => .{ .func_type = ip.extraFuncType(data) },
 
@@ -4979,7 +5134,7 @@ pub fn get(ip: *InternPool, gpa: Allocator, key: Key) Allocator.Error!Index {
         .union_type => unreachable, // use getUnionType() instead
         .opaque_type => unreachable, // use getOpaqueType() instead
 
-        .enum_type => unreachable, // use getEnum() or getIncompleteEnum() instead
+        .enum_type => unreachable, // use getEnumType() instead
         .func_type => unreachable, // use getFuncType() instead
         .extern_func => unreachable, // use getExternFunc() instead
         .func => unreachable, // use getFuncInstance() or getFuncDecl() instead
@@ -5652,9 +5807,7 @@ pub const UnionTypeInit = struct {
         assumed_pointer_aligned: bool,
         alignment: Alignment,
     },
-    decl: DeclIndex,
-    namespace: NamespaceIndex,
-    zir_index: TrackedInst.Index.Optional,
+    has_namespace: bool,
     fields_len: u32,
     enum_tag_ty: Index,
     /// May have length 0 which leaves the values unset until later.
@@ -5663,23 +5816,50 @@ pub const UnionTypeInit = struct {
     /// The logic for `any_aligned_fields` is asserted to have been done before
     /// calling this function.
     field_aligns: []const Alignment,
-    captures: []const CaptureValue,
+    key: union(enum) {
+        declared: struct {
+            zir_index: TrackedInst.Index,
+            captures: []const CaptureValue,
+        },
+        reified: struct {
+            zir_index: TrackedInst.Index,
+            type_hash: u64,
+        },
+    },
 };
 
-pub fn getUnionType(ip: *InternPool, gpa: Allocator, ini: UnionTypeInit) Allocator.Error!Index {
-    const prev_extra_len = ip.extra.items.len;
+pub fn getUnionType(ip: *InternPool, gpa: Allocator, ini: UnionTypeInit) Allocator.Error!WipNamespaceType.Result {
+    const adapter: KeyAdapter = .{ .intern_pool = ip };
+    const gop = try ip.map.getOrPutAdapted(gpa, Key{ .union_type = switch (ini.key) {
+        .declared => |d| .{ .declared = .{
+            .zir_index = d.zir_index,
+            .captures = .{ .external = d.captures },
+        } },
+        .reified => |r| .{ .reified = .{
+            .zir_index = r.zir_index,
+            .type_hash = r.type_hash,
+        } },
+    } }, adapter);
+    if (gop.found_existing) return .{ .existing = @enumFromInt(gop.index) };
+    errdefer _ = ip.map.pop();
+
     const align_elements_len = if (ini.flags.any_aligned_fields) (ini.fields_len + 3) / 4 else 0;
     const align_element: u32 = @bitCast([1]u8{@intFromEnum(Alignment.none)} ** 4);
     try ip.extra.ensureUnusedCapacity(gpa, @typeInfo(Tag.TypeUnion).Struct.fields.len +
-        @intFromBool(ini.captures.len != 0) + // captures_len
-        ini.captures.len + // captures
+        // TODO: fmt bug
+        // zig fmt: off
+        switch (ini.key) {
+            .declared => |d| @intFromBool(d.captures.len != 0) + d.captures.len,
+            .reified => 2, // type_hash: PackedU64
+        } +
+        // zig fmt: on
         ini.fields_len + // field types
         align_elements_len);
     try ip.items.ensureUnusedCapacity(gpa, 1);
 
-    const union_type_extra_index = ip.addExtraAssumeCapacity(Tag.TypeUnion{
+    const extra_index = ip.addExtraAssumeCapacity(Tag.TypeUnion{
         .flags = .{
-            .any_captures = ini.captures.len != 0,
+            .any_captures = ini.key == .declared and ini.key.declared.captures.len != 0,
             .runtime_tag = ini.flags.runtime_tag,
             .any_aligned_fields = ini.flags.any_aligned_fields,
             .layout = ini.flags.layout,
@@ -5688,19 +5868,30 @@ pub fn getUnionType(ip: *InternPool, gpa: Allocator, ini: UnionTypeInit) Allocat
             .assumed_runtime_bits = ini.flags.assumed_runtime_bits,
             .assumed_pointer_aligned = ini.flags.assumed_pointer_aligned,
             .alignment = ini.flags.alignment,
+            .is_reified = ini.key == .reified,
         },
         .fields_len = ini.fields_len,
         .size = std.math.maxInt(u32),
         .padding = std.math.maxInt(u32),
-        .decl = ini.decl,
-        .namespace = ini.namespace,
+        .decl = undefined, // set by `finish`
+        .namespace = .none, // set by `finish`
         .tag_ty = ini.enum_tag_ty,
-        .zir_index = ini.zir_index,
+        .zir_index = switch (ini.key) {
+            inline else => |x| x.zir_index,
+        },
+    });
+
+    ip.items.appendAssumeCapacity(.{
+        .tag = .type_union,
+        .data = extra_index,
     });
 
-    if (ini.captures.len != 0) {
-        ip.extra.appendAssumeCapacity(@intCast(ini.captures.len));
-        ip.extra.appendSliceAssumeCapacity(@ptrCast(ini.captures));
+    switch (ini.key) {
+        .declared => |d| if (d.captures.len != 0) {
+            ip.extra.appendAssumeCapacity(@intCast(d.captures.len));
+            ip.extra.appendSliceAssumeCapacity(@ptrCast(d.captures));
+        },
+        .reified => |r| _ = ip.addExtraAssumeCapacity(PackedU64.init(r.type_hash)),
     }
 
     // field types
@@ -5725,27 +5916,41 @@ pub fn getUnionType(ip: *InternPool, gpa: Allocator, ini: UnionTypeInit) Allocat
         assert(ini.field_aligns.len == 0);
     }
 
-    const adapter: KeyAdapter = .{ .intern_pool = ip };
-    const gop = try ip.map.getOrPutAdapted(gpa, Key{
-        .union_type = .{ .decl = ini.decl },
-    }, adapter);
-    if (gop.found_existing) {
-        ip.extra.items.len = prev_extra_len;
-        return @enumFromInt(gop.index);
+    return .{ .wip = .{
+        .index = @enumFromInt(ip.items.len - 1),
+        .decl_extra_index = extra_index + std.meta.fieldIndex(Tag.TypeUnion, "decl").?,
+        .namespace_extra_index = if (ini.has_namespace)
+            extra_index + std.meta.fieldIndex(Tag.TypeUnion, "namespace").?
+        else
+            null,
+    } };
+}
+
+pub const WipNamespaceType = struct {
+    index: Index,
+    decl_extra_index: u32,
+    namespace_extra_index: ?u32,
+    pub fn finish(wip: WipNamespaceType, ip: *InternPool, decl: DeclIndex, namespace: OptionalNamespaceIndex) Index {
+        ip.extra.items[wip.decl_extra_index] = @intFromEnum(decl);
+        if (wip.namespace_extra_index) |i| {
+            ip.extra.items[i] = @intFromEnum(namespace.unwrap().?);
+        } else {
+            assert(namespace == .none);
+        }
+        return wip.index;
+    }
+    pub fn cancel(wip: WipNamespaceType, ip: *InternPool) void {
+        ip.remove(wip.index);
     }
 
-    ip.items.appendAssumeCapacity(.{
-        .tag = .type_union,
-        .data = union_type_extra_index,
-    });
-    return @enumFromInt(ip.items.len - 1);
-}
+    pub const Result = union(enum) {
+        wip: WipNamespaceType,
+        existing: Index,
+    };
+};
 
 pub const StructTypeInit = struct {
-    decl: DeclIndex,
-    namespace: OptionalNamespaceIndex,
     layout: std.builtin.Type.ContainerLayout,
-    zir_index: TrackedInst.Index.Optional,
     fields_len: u32,
     known_non_opv: bool,
     requires_comptime: RequiresComptime,
@@ -5754,61 +5959,101 @@ pub const StructTypeInit = struct {
     any_default_inits: bool,
     inits_resolved: bool,
     any_aligned_fields: bool,
-    captures: []const CaptureValue,
+    has_namespace: bool,
+    key: union(enum) {
+        declared: struct {
+            zir_index: TrackedInst.Index,
+            captures: []const CaptureValue,
+        },
+        reified: struct {
+            zir_index: TrackedInst.Index,
+            type_hash: u64,
+        },
+    },
 };
 
 pub fn getStructType(
     ip: *InternPool,
     gpa: Allocator,
     ini: StructTypeInit,
-) Allocator.Error!Index {
+) Allocator.Error!WipNamespaceType.Result {
     const adapter: KeyAdapter = .{ .intern_pool = ip };
-    const key: Key = .{
-        .struct_type = .{ .decl = ini.decl.toOptional() },
-    };
+    const key: Key = .{ .struct_type = switch (ini.key) {
+        .declared => |d| .{ .declared = .{
+            .zir_index = d.zir_index,
+            .captures = .{ .external = d.captures },
+        } },
+        .reified => |r| .{ .reified = .{
+            .zir_index = r.zir_index,
+            .type_hash = r.type_hash,
+        } },
+    } };
     const gop = try ip.map.getOrPutAdapted(gpa, key, adapter);
-    if (gop.found_existing) return @enumFromInt(gop.index);
+    if (gop.found_existing) return .{ .existing = @enumFromInt(gop.index) };
     errdefer _ = ip.map.pop();
 
     const names_map = try ip.addMap(gpa, ini.fields_len);
     errdefer _ = ip.maps.pop();
 
+    const zir_index = switch (ini.key) {
+        inline else => |x| x.zir_index,
+    };
+
     const is_extern = switch (ini.layout) {
         .Auto => false,
         .Extern => true,
         .Packed => {
             try ip.extra.ensureUnusedCapacity(gpa, @typeInfo(Tag.TypeStructPacked).Struct.fields.len +
-                @intFromBool(ini.captures.len != 0) + // captures_len
-                ini.captures.len + // captures
+                // TODO: fmt bug
+                // zig fmt: off
+                switch (ini.key) {
+                    .declared => |d| @intFromBool(d.captures.len != 0) + d.captures.len,
+                    .reified => 2, // type_hash: PackedU64
+                } +
+                // zig fmt: on
                 ini.fields_len + // types
                 ini.fields_len + // names
                 ini.fields_len); // inits
+            const extra_index = ip.addExtraAssumeCapacity(Tag.TypeStructPacked{
+                .decl = undefined, // set by `finish`
+                .zir_index = zir_index,
+                .fields_len = ini.fields_len,
+                .namespace = .none,
+                .backing_int_ty = .none,
+                .names_map = names_map,
+                .flags = .{
+                    .any_captures = ini.key == .declared and ini.key.declared.captures.len != 0,
+                    .field_inits_wip = false,
+                    .inits_resolved = ini.inits_resolved,
+                    .is_reified = ini.key == .reified,
+                },
+            });
             try ip.items.append(gpa, .{
                 .tag = if (ini.any_default_inits) .type_struct_packed_inits else .type_struct_packed,
-                .data = ip.addExtraAssumeCapacity(Tag.TypeStructPacked{
-                    .decl = ini.decl,
-                    .zir_index = ini.zir_index,
-                    .fields_len = ini.fields_len,
-                    .namespace = ini.namespace,
-                    .backing_int_ty = .none,
-                    .names_map = names_map,
-                    .flags = .{
-                        .any_captures = ini.captures.len != 0,
-                        .field_inits_wip = false,
-                        .inits_resolved = ini.inits_resolved,
-                    },
-                }),
+                .data = extra_index,
             });
-            if (ini.captures.len != 0) {
-                ip.extra.appendAssumeCapacity(@intCast(ini.captures.len));
-                ip.extra.appendSliceAssumeCapacity(@ptrCast(ini.captures));
+            switch (ini.key) {
+                .declared => |d| if (d.captures.len != 0) {
+                    ip.extra.appendAssumeCapacity(@intCast(d.captures.len));
+                    ip.extra.appendSliceAssumeCapacity(@ptrCast(d.captures));
+                },
+                .reified => |r| {
+                    _ = ip.addExtraAssumeCapacity(PackedU64.init(r.type_hash));
+                },
             }
             ip.extra.appendNTimesAssumeCapacity(@intFromEnum(Index.none), ini.fields_len);
             ip.extra.appendNTimesAssumeCapacity(@intFromEnum(OptionalNullTerminatedString.none), ini.fields_len);
             if (ini.any_default_inits) {
                 ip.extra.appendNTimesAssumeCapacity(@intFromEnum(Index.none), ini.fields_len);
             }
-            return @enumFromInt(ip.items.len - 1);
+            return .{ .wip = .{
+                .index = @enumFromInt(ip.items.len - 1),
+                .decl_extra_index = extra_index + std.meta.fieldIndex(Tag.TypeStructPacked, "decl").?,
+                .namespace_extra_index = if (ini.has_namespace)
+                    extra_index + std.meta.fieldIndex(Tag.TypeStructPacked, "namespace").?
+                else
+                    null,
+            } };
         },
     };
 
@@ -5817,44 +6062,56 @@ pub fn getStructType(
     const comptime_elements_len = if (ini.any_comptime_fields) (ini.fields_len + 31) / 32 else 0;
 
     try ip.extra.ensureUnusedCapacity(gpa, @typeInfo(Tag.TypeStruct).Struct.fields.len +
-        @intFromBool(ini.captures.len != 0) + // captures_len
-        ini.captures.len + // captures
+        // TODO: fmt bug
+        // zig fmt: off
+        switch (ini.key) {
+            .declared => |d| @intFromBool(d.captures.len != 0) + d.captures.len,
+            .reified => 2, // type_hash: PackedU64
+        } +
+        // zig fmt: on
         (ini.fields_len * 5) + // types, names, inits, runtime order, offsets
         align_elements_len + comptime_elements_len +
         2); // names_map + namespace
+    const extra_index = ip.addExtraAssumeCapacity(Tag.TypeStruct{
+        .decl = undefined, // set by `finish`
+        .zir_index = zir_index,
+        .fields_len = ini.fields_len,
+        .size = std.math.maxInt(u32),
+        .flags = .{
+            .any_captures = ini.key == .declared and ini.key.declared.captures.len != 0,
+            .is_extern = is_extern,
+            .known_non_opv = ini.known_non_opv,
+            .requires_comptime = ini.requires_comptime,
+            .is_tuple = ini.is_tuple,
+            .assumed_runtime_bits = false,
+            .assumed_pointer_aligned = false,
+            .has_namespace = ini.has_namespace,
+            .any_comptime_fields = ini.any_comptime_fields,
+            .any_default_inits = ini.any_default_inits,
+            .any_aligned_fields = ini.any_aligned_fields,
+            .alignment = .none,
+            .alignment_wip = false,
+            .field_types_wip = false,
+            .layout_wip = false,
+            .layout_resolved = false,
+            .field_inits_wip = false,
+            .inits_resolved = ini.inits_resolved,
+            .fully_resolved = false,
+            .is_reified = ini.key == .reified,
+        },
+    });
     try ip.items.append(gpa, .{
         .tag = .type_struct,
-        .data = ip.addExtraAssumeCapacity(Tag.TypeStruct{
-            .decl = ini.decl,
-            .zir_index = ini.zir_index,
-            .fields_len = ini.fields_len,
-            .size = std.math.maxInt(u32),
-            .flags = .{
-                .any_captures = ini.captures.len != 0,
-                .is_extern = is_extern,
-                .known_non_opv = ini.known_non_opv,
-                .requires_comptime = ini.requires_comptime,
-                .is_tuple = ini.is_tuple,
-                .assumed_runtime_bits = false,
-                .assumed_pointer_aligned = false,
-                .has_namespace = ini.namespace != .none,
-                .any_comptime_fields = ini.any_comptime_fields,
-                .any_default_inits = ini.any_default_inits,
-                .any_aligned_fields = ini.any_aligned_fields,
-                .alignment = .none,
-                .alignment_wip = false,
-                .field_types_wip = false,
-                .layout_wip = false,
-                .layout_resolved = false,
-                .field_inits_wip = false,
-                .inits_resolved = ini.inits_resolved,
-                .fully_resolved = false,
-            },
-        }),
+        .data = extra_index,
     });
-    if (ini.captures.len != 0) {
-        ip.extra.appendAssumeCapacity(@intCast(ini.captures.len));
-        ip.extra.appendSliceAssumeCapacity(@ptrCast(ini.captures));
+    switch (ini.key) {
+        .declared => |d| if (d.captures.len != 0) {
+            ip.extra.appendAssumeCapacity(@intCast(d.captures.len));
+            ip.extra.appendSliceAssumeCapacity(@ptrCast(d.captures));
+        },
+        .reified => |r| {
+            _ = ip.addExtraAssumeCapacity(PackedU64.init(r.type_hash));
+        },
     }
     ip.extra.appendNTimesAssumeCapacity(@intFromEnum(Index.none), ini.fields_len);
     if (!ini.is_tuple) {
@@ -5864,9 +6121,10 @@ pub fn getStructType(
     if (ini.any_default_inits) {
         ip.extra.appendNTimesAssumeCapacity(@intFromEnum(Index.none), ini.fields_len);
     }
-    if (ini.namespace.unwrap()) |namespace| {
-        ip.extra.appendAssumeCapacity(@intFromEnum(namespace));
-    }
+    const namespace_extra_index: ?u32 = if (ini.has_namespace) i: {
+        ip.extra.appendAssumeCapacity(undefined); // set by `finish`
+        break :i @intCast(ip.extra.items.len - 1);
+    } else null;
     if (ini.any_aligned_fields) {
         ip.extra.appendNTimesAssumeCapacity(align_element, align_elements_len);
     }
@@ -5877,7 +6135,11 @@ pub fn getStructType(
         ip.extra.appendNTimesAssumeCapacity(@intFromEnum(LoadedStructType.RuntimeOrder.unresolved), ini.fields_len);
     }
     ip.extra.appendNTimesAssumeCapacity(std.math.maxInt(u32), ini.fields_len);
-    return @enumFromInt(ip.items.len - 1);
+    return .{ .wip = .{
+        .index = @enumFromInt(ip.items.len - 1),
+        .decl_extra_index = extra_index + std.meta.fieldIndex(Tag.TypeStruct, "decl").?,
+        .namespace_extra_index = namespace_extra_index,
+    } };
 }
 
 pub const AnonStructTypeInit = struct {
@@ -6513,282 +6775,399 @@ fn finishFuncInstance(
     return func_index;
 }
 
-/// Provides API for completing an enum type after calling `getIncompleteEnum`.
-pub const IncompleteEnumType = struct {
+pub const EnumTypeInit = struct {
+    has_namespace: bool,
+    has_values: bool,
+    tag_mode: LoadedEnumType.TagMode,
+    fields_len: u32,
+    key: union(enum) {
+        declared: struct {
+            zir_index: TrackedInst.Index,
+            captures: []const CaptureValue,
+        },
+        reified: struct {
+            zir_index: TrackedInst.Index,
+            type_hash: u64,
+        },
+    },
+};
+
+pub const WipEnumType = struct {
     index: Index,
     tag_ty_index: u32,
+    decl_index: u32,
+    namespace_index: ?u32,
     names_map: MapIndex,
     names_start: u32,
     values_map: OptionalMapIndex,
     values_start: u32,
+    expected_fields_len: if (std.debug.runtime_safety) u32 else void,
 
-    pub fn setTagType(self: @This(), ip: *InternPool, tag_ty: Index) void {
-        assert(tag_ty == .noreturn_type or ip.isIntegerType(tag_ty));
-        ip.extra.items[self.tag_ty_index] = @intFromEnum(tag_ty);
-    }
-
-    /// Returns the already-existing field with the same name, if any.
-    pub fn addFieldName(
-        self: @This(),
+    pub fn prepare(
+        wip: WipEnumType,
         ip: *InternPool,
-        name: NullTerminatedString,
-    ) ?u32 {
-        return ip.addFieldName(self.names_map, self.names_start, name);
+        decl: DeclIndex,
+        namespace: OptionalNamespaceIndex,
+        tag_ty: Index,
+    ) void {
+        assert(ip.isIntegerType(tag_ty));
+        ip.extra.items[wip.tag_ty_index] = @intFromEnum(tag_ty);
+        ip.extra.items[wip.decl_index] = @intFromEnum(decl);
+        if (wip.namespace_index) |i| {
+            ip.extra.items[i] = @intFromEnum(namespace.unwrap().?);
+        } else {
+            assert(namespace == .none);
+        }
     }
 
-    /// Returns the already-existing field with the same value, if any.
-    /// Make sure the type of the value has the integer tag type of the enum.
-    pub fn addFieldValue(
-        self: @This(),
-        ip: *InternPool,
-        value: Index,
-    ) ?u32 {
-        assert(ip.typeOf(value) == @as(Index, @enumFromInt(ip.extra.items[self.tag_ty_index])));
-        const map = &ip.maps.items[@intFromEnum(self.values_map.unwrap().?)];
+    pub const FieldConflict = struct {
+        kind: enum { name, value },
+        prev_field_idx: u32,
+    };
+
+    /// Returns the already-existing field with the same name or value, if any.
+    /// If the enum is automatially numbered, `value` must be `.none`.
+    /// Otherwise, the type of `value` must be the integer tag type of the enum.
+    pub fn nextField(wip: WipEnumType, ip: *InternPool, name: NullTerminatedString, value: Index) ?FieldConflict {
+        if (ip.addFieldName(wip.names_map, wip.names_start, name)) |conflict| {
+            return .{ .kind = .name, .prev_field_idx = conflict };
+        }
+        if (value == .none) {
+            assert(wip.values_map == .none);
+            return null;
+        }
+        assert(ip.typeOf(value) == @as(Index, @enumFromInt(ip.extra.items[wip.tag_ty_index])));
+        const map = &ip.maps.items[@intFromEnum(wip.values_map.unwrap().?)];
         const field_index = map.count();
-        const indexes = ip.extra.items[self.values_start..][0..field_index];
+        const indexes = ip.extra.items[wip.values_start..][0..field_index];
         const adapter: Index.Adapter = .{ .indexes = @ptrCast(indexes) };
         const gop = map.getOrPutAssumeCapacityAdapted(value, adapter);
-        if (gop.found_existing) return @intCast(gop.index);
-        ip.extra.items[self.values_start + field_index] = @intFromEnum(value);
+        if (gop.found_existing) {
+            return .{ .kind = .value, .prev_field_idx = @intCast(gop.index) };
+        }
+        ip.extra.items[wip.values_start + field_index] = @intFromEnum(value);
         return null;
     }
-};
 
-/// This is used to create an enum type in the `InternPool`, with the ability
-/// to update the tag type, field names, and field values later.
-pub fn getIncompleteEnum(
-    ip: *InternPool,
-    gpa: Allocator,
-    enum_type: Key.IncompleteEnumType,
-) Allocator.Error!IncompleteEnumType {
-    switch (enum_type.tag_mode) {
-        .auto => return getIncompleteEnumAuto(ip, gpa, enum_type),
-        .explicit => return getIncompleteEnumExplicit(ip, gpa, enum_type, .type_enum_explicit),
-        .nonexhaustive => return getIncompleteEnumExplicit(ip, gpa, enum_type, .type_enum_nonexhaustive),
+    pub fn finish(wip: WipEnumType, ip: *InternPool) Index {
+        if (std.debug.runtime_safety) {
+            const names_map = &ip.maps.items[@intFromEnum(wip.names_map)];
+            assert(names_map.count() == wip.expected_fields_len);
+            if (wip.values_map.unwrap()) |v| {
+                const values_map = &ip.maps.items[@intFromEnum(v)];
+                assert(values_map.count() == wip.expected_fields_len);
+            }
+        }
+        return wip.index;
     }
-}
 
-fn getIncompleteEnumAuto(
-    ip: *InternPool,
-    gpa: Allocator,
-    enum_type: Key.IncompleteEnumType,
-) Allocator.Error!IncompleteEnumType {
-    const int_tag_type = if (enum_type.tag_ty != .none)
-        enum_type.tag_ty
-    else
-        try ip.get(gpa, .{ .int_type = .{
-            .bits = if (enum_type.fields_len == 0) 0 else std.math.log2_int_ceil(u32, enum_type.fields_len),
-            .signedness = .unsigned,
-        } });
-
-    // We must keep the map in sync with `items`. The hash and equality functions
-    // for enum types only look at the decl field, which is present even in
-    // an `IncompleteEnumType`.
-    const adapter: KeyAdapter = .{ .intern_pool = ip };
-    const gop = try ip.map.getOrPutAdapted(gpa, enum_type.toKey(), adapter);
-    assert(!gop.found_existing);
-
-    const names_map = try ip.addMap(gpa, enum_type.fields_len);
-
-    const extra_fields_len: u32 = @typeInfo(EnumAuto).Struct.fields.len;
-    try ip.extra.ensureUnusedCapacity(gpa, extra_fields_len + enum_type.captures.len + enum_type.fields_len);
-    try ip.items.ensureUnusedCapacity(gpa, 1);
-
-    const extra_index = ip.addExtraAssumeCapacity(EnumAuto{
-        .decl = enum_type.decl,
-        .captures_len = @intCast(enum_type.captures.len),
-        .namespace = enum_type.namespace,
-        .int_tag_type = int_tag_type,
-        .names_map = names_map,
-        .fields_len = enum_type.fields_len,
-        .zir_index = enum_type.zir_index,
-    });
+    pub fn cancel(wip: WipEnumType, ip: *InternPool) void {
+        ip.remove(wip.index);
+    }
 
-    ip.items.appendAssumeCapacity(.{
-        .tag = .type_enum_auto,
-        .data = extra_index,
-    });
-    ip.extra.appendSliceAssumeCapacity(@ptrCast(enum_type.captures));
-    ip.extra.appendNTimesAssumeCapacity(@intFromEnum(Index.none), enum_type.fields_len);
-    return .{
-        .index = @enumFromInt(ip.items.len - 1),
-        .tag_ty_index = extra_index + std.meta.fieldIndex(EnumAuto, "int_tag_type").?,
-        .names_map = names_map,
-        .names_start = extra_index + extra_fields_len,
-        .values_map = .none,
-        .values_start = undefined,
+    pub const Result = union(enum) {
+        wip: WipEnumType,
+        existing: Index,
     };
-}
+};
 
-fn getIncompleteEnumExplicit(
+pub fn getEnumType(
     ip: *InternPool,
     gpa: Allocator,
-    enum_type: Key.IncompleteEnumType,
-    tag: Tag,
-) Allocator.Error!IncompleteEnumType {
-    // We must keep the map in sync with `items`. The hash and equality functions
-    // for enum types only look at the decl field, which is present even in
-    // an `IncompleteEnumType`.
+    ini: EnumTypeInit,
+) Allocator.Error!WipEnumType.Result {
     const adapter: KeyAdapter = .{ .intern_pool = ip };
-    const gop = try ip.map.getOrPutAdapted(gpa, enum_type.toKey(), adapter);
-    assert(!gop.found_existing);
-
-    const names_map = try ip.addMap(gpa, enum_type.fields_len);
-    const values_map: OptionalMapIndex = if (!enum_type.has_values) .none else m: {
-        const values_map = try ip.addMap(gpa, enum_type.fields_len);
-        break :m values_map.toOptional();
-    };
-
-    const reserved_len = enum_type.fields_len +
-        if (enum_type.has_values) enum_type.fields_len else 0;
+    const gop = try ip.map.getOrPutAdapted(gpa, Key{ .enum_type = switch (ini.key) {
+        .declared => |d| .{ .declared = .{
+            .zir_index = d.zir_index,
+            .captures = .{ .external = d.captures },
+        } },
+        .reified => |r| .{ .reified = .{
+            .zir_index = r.zir_index,
+            .type_hash = r.type_hash,
+        } },
+    } }, adapter);
+    if (gop.found_existing) return .{ .existing = @enumFromInt(gop.index) };
+    assert(gop.index == ip.items.len);
+    errdefer _ = ip.map.pop();
 
-    const extra_fields_len: u32 = @typeInfo(EnumExplicit).Struct.fields.len;
-    try ip.extra.ensureUnusedCapacity(gpa, extra_fields_len + enum_type.captures.len + reserved_len);
     try ip.items.ensureUnusedCapacity(gpa, 1);
 
-    const extra_index = ip.addExtraAssumeCapacity(EnumExplicit{
-        .decl = enum_type.decl,
-        .captures_len = @intCast(enum_type.captures.len),
-        .namespace = enum_type.namespace,
-        .int_tag_type = enum_type.tag_ty,
-        .fields_len = enum_type.fields_len,
-        .names_map = names_map,
-        .values_map = values_map,
-        .zir_index = enum_type.zir_index,
-    });
+    const names_map = try ip.addMap(gpa, ini.fields_len);
+    errdefer _ = ip.maps.pop();
 
-    ip.items.appendAssumeCapacity(.{
-        .tag = tag,
-        .data = extra_index,
-    });
-    ip.extra.appendSliceAssumeCapacity(@ptrCast(enum_type.captures));
-    // This is both fields and values (if present).
-    ip.extra.appendNTimesAssumeCapacity(@intFromEnum(Index.none), reserved_len);
-    return .{
-        .index = @enumFromInt(ip.items.len - 1),
-        .tag_ty_index = extra_index + std.meta.fieldIndex(EnumExplicit, "int_tag_type").?,
-        .names_map = names_map,
-        .names_start = extra_index + extra_fields_len,
-        .values_map = values_map,
-        .values_start = extra_index + extra_fields_len + enum_type.fields_len,
-    };
+    switch (ini.tag_mode) {
+        .auto => {
+            assert(!ini.has_values);
+            try ip.extra.ensureUnusedCapacity(gpa, @typeInfo(EnumAuto).Struct.fields.len +
+                // TODO: fmt bug
+                // zig fmt: off
+                switch (ini.key) {
+                    .declared => |d| d.captures.len,
+                    .reified => 2, // type_hash: PackedU64
+                } +
+                // zig fmt: on
+                ini.fields_len); // field types
+
+            const extra_index = ip.addExtraAssumeCapacity(EnumAuto{
+                .decl = undefined, // set by `prepare`
+                .captures_len = switch (ini.key) {
+                    .declared => |d| @intCast(d.captures.len),
+                    .reified => std.math.maxInt(u32),
+                },
+                .namespace = .none,
+                .int_tag_type = .none, // set by `prepare`
+                .fields_len = ini.fields_len,
+                .names_map = names_map,
+                .zir_index = switch (ini.key) {
+                    inline else => |x| x.zir_index,
+                }.toOptional(),
+            });
+            ip.items.appendAssumeCapacity(.{
+                .tag = .type_enum_auto,
+                .data = extra_index,
+            });
+            switch (ini.key) {
+                .declared => |d| ip.extra.appendSliceAssumeCapacity(@ptrCast(d.captures)),
+                .reified => |r| _ = ip.addExtraAssumeCapacity(PackedU64.init(r.type_hash)),
+            }
+            const names_start = ip.extra.items.len;
+            ip.extra.appendNTimesAssumeCapacity(undefined, ini.fields_len);
+            return .{ .wip = .{
+                .index = @enumFromInt(gop.index),
+                .tag_ty_index = extra_index + std.meta.fieldIndex(EnumAuto, "int_tag_type").?,
+                .decl_index = extra_index + std.meta.fieldIndex(EnumAuto, "decl").?,
+                .namespace_index = if (ini.has_namespace) extra_index + std.meta.fieldIndex(EnumAuto, "namespace").? else null,
+                .names_map = names_map,
+                .names_start = @intCast(names_start),
+                .values_map = .none,
+                .values_start = undefined,
+                .expected_fields_len = if (std.debug.runtime_safety) ini.fields_len else {},
+            } };
+        },
+        .explicit, .nonexhaustive => {
+            const values_map: OptionalMapIndex = if (!ini.has_values) .none else m: {
+                const values_map = try ip.addMap(gpa, ini.fields_len);
+                break :m values_map.toOptional();
+            };
+            errdefer if (ini.has_values) {
+                _ = ip.map.pop();
+            };
+
+            try ip.extra.ensureUnusedCapacity(gpa, @typeInfo(EnumExplicit).Struct.fields.len +
+                // TODO: fmt bug
+                // zig fmt: off
+                switch (ini.key) {
+                    .declared => |d| d.captures.len,
+                    .reified => 2, // type_hash: PackedU64
+                } +
+                // zig fmt: on
+                ini.fields_len + // field types
+                ini.fields_len * @intFromBool(ini.has_values)); // field values
+
+            const extra_index = ip.addExtraAssumeCapacity(EnumExplicit{
+                .decl = undefined, // set by `prepare`
+                .captures_len = switch (ini.key) {
+                    .declared => |d| @intCast(d.captures.len),
+                    .reified => std.math.maxInt(u32),
+                },
+                .namespace = .none,
+                .int_tag_type = .none, // set by `prepare`
+                .fields_len = ini.fields_len,
+                .names_map = names_map,
+                .values_map = values_map,
+                .zir_index = switch (ini.key) {
+                    inline else => |x| x.zir_index,
+                }.toOptional(),
+            });
+            ip.items.appendAssumeCapacity(.{
+                .tag = switch (ini.tag_mode) {
+                    .auto => unreachable,
+                    .explicit => .type_enum_explicit,
+                    .nonexhaustive => .type_enum_nonexhaustive,
+                },
+                .data = extra_index,
+            });
+            switch (ini.key) {
+                .declared => |d| ip.extra.appendSliceAssumeCapacity(@ptrCast(d.captures)),
+                .reified => |r| _ = ip.addExtraAssumeCapacity(PackedU64.init(r.type_hash)),
+            }
+            const names_start = ip.extra.items.len;
+            ip.extra.appendNTimesAssumeCapacity(undefined, ini.fields_len);
+            const values_start = ip.extra.items.len;
+            if (ini.has_values) {
+                ip.extra.appendNTimesAssumeCapacity(undefined, ini.fields_len);
+            }
+            return .{ .wip = .{
+                .index = @enumFromInt(gop.index),
+                .tag_ty_index = extra_index + std.meta.fieldIndex(EnumAuto, "int_tag_type").?,
+                .decl_index = extra_index + std.meta.fieldIndex(EnumAuto, "decl").?,
+                .namespace_index = if (ini.has_namespace) extra_index + std.meta.fieldIndex(EnumAuto, "namespace").? else null,
+                .names_map = names_map,
+                .names_start = @intCast(names_start),
+                .values_map = values_map,
+                .values_start = @intCast(values_start),
+                .expected_fields_len = if (std.debug.runtime_safety) ini.fields_len else {},
+            } };
+        },
+    }
 }
 
-pub const GetEnumInit = struct {
+const GeneratedTagEnumTypeInit = struct {
     decl: DeclIndex,
-    namespace: OptionalNamespaceIndex,
+    owner_union_ty: Index,
     tag_ty: Index,
     names: []const NullTerminatedString,
     values: []const Index,
     tag_mode: LoadedEnumType.TagMode,
-    zir_index: TrackedInst.Index.Optional,
-    captures: []const CaptureValue,
 };
 
-pub fn getEnum(ip: *InternPool, gpa: Allocator, ini: GetEnumInit) Allocator.Error!Index {
-    const adapter: KeyAdapter = .{ .intern_pool = ip };
-    const gop = try ip.map.getOrPutAdapted(gpa, Key{
-        .enum_type = .{ .decl = ini.decl },
-    }, adapter);
-    if (gop.found_existing) return @enumFromInt(gop.index);
-    errdefer _ = ip.map.pop();
+/// Creates an enum type which was automatically-generated as the tag type of a
+/// `union` with no explicit tag type. Since this is only called once per union
+/// type, it asserts that no matching type yet exists.
+pub fn getGeneratedTagEnumType(ip: *InternPool, gpa: Allocator, ini: GeneratedTagEnumTypeInit) Allocator.Error!Index {
+    assert(ip.isUnion(ini.owner_union_ty));
+    assert(ip.isIntegerType(ini.tag_ty));
+    for (ini.values) |val| assert(ip.typeOf(val) == ini.tag_ty);
+
+    try ip.map.ensureUnusedCapacity(gpa, 1);
     try ip.items.ensureUnusedCapacity(gpa, 1);
 
-    assert(ini.tag_ty == .noreturn_type or ip.isIntegerType(ini.tag_ty));
-    for (ini.values) |value| assert(ip.typeOf(value) == ini.tag_ty);
+    const names_map = try ip.addMap(gpa, ini.names.len);
+    errdefer _ = ip.maps.pop();
+    ip.addStringsToMap(names_map, ini.names);
+
+    const fields_len: u32 = @intCast(ini.names.len);
 
     switch (ini.tag_mode) {
         .auto => {
-            const names_map = try ip.addMap(gpa, ini.names.len);
-            addStringsToMap(ip, names_map, ini.names);
-
-            const fields_len: u32 = @intCast(ini.names.len);
             try ip.extra.ensureUnusedCapacity(gpa, @typeInfo(EnumAuto).Struct.fields.len +
-                ini.captures.len + fields_len);
+                1 + // owner_union
+                fields_len); // field names
             ip.items.appendAssumeCapacity(.{
                 .tag = .type_enum_auto,
                 .data = ip.addExtraAssumeCapacity(EnumAuto{
                     .decl = ini.decl,
-                    .captures_len = @intCast(ini.captures.len),
-                    .namespace = ini.namespace,
+                    .captures_len = 0,
+                    .namespace = .none,
                     .int_tag_type = ini.tag_ty,
+                    .fields_len = fields_len,
                     .names_map = names_map,
+                    .zir_index = .none,
+                }),
+            });
+            ip.extra.appendAssumeCapacity(@intFromEnum(ini.owner_union_ty));
+            ip.extra.appendSliceAssumeCapacity(@ptrCast(ini.names));
+        },
+        .explicit, .nonexhaustive => {
+            try ip.extra.ensureUnusedCapacity(gpa, @typeInfo(EnumExplicit).Struct.fields.len +
+                1 + // owner_union
+                fields_len + // field names
+                ini.values.len); // field values
+
+            const values_map: OptionalMapIndex = if (ini.values.len != 0) m: {
+                const map = try ip.addMap(gpa, ini.values.len);
+                addIndexesToMap(ip, map, ini.values);
+                break :m map.toOptional();
+            } else .none;
+            // We don't clean up the values map on error!
+            errdefer @compileError("error path leaks values_map");
+
+            ip.items.appendAssumeCapacity(.{
+                .tag = switch (ini.tag_mode) {
+                    .explicit => .type_enum_explicit,
+                    .nonexhaustive => .type_enum_nonexhaustive,
+                    .auto => unreachable,
+                },
+                .data = ip.addExtraAssumeCapacity(EnumExplicit{
+                    .decl = ini.decl,
+                    .captures_len = 0,
+                    .namespace = .none,
+                    .int_tag_type = ini.tag_ty,
                     .fields_len = fields_len,
-                    .zir_index = ini.zir_index,
+                    .names_map = names_map,
+                    .values_map = values_map,
+                    .zir_index = .none,
                 }),
             });
-            ip.extra.appendSliceAssumeCapacity(@ptrCast(ini.captures));
+            ip.extra.appendAssumeCapacity(@intFromEnum(ini.owner_union_ty));
             ip.extra.appendSliceAssumeCapacity(@ptrCast(ini.names));
-            return @enumFromInt(ip.items.len - 1);
+            ip.extra.appendSliceAssumeCapacity(@ptrCast(ini.values));
         },
-        .explicit => return finishGetEnum(ip, gpa, ini, .type_enum_explicit),
-        .nonexhaustive => return finishGetEnum(ip, gpa, ini, .type_enum_nonexhaustive),
     }
-}
-
-fn finishGetEnum(
-    ip: *InternPool,
-    gpa: Allocator,
-    ini: GetEnumInit,
-    tag: Tag,
-) Allocator.Error!Index {
-    const names_map = try ip.addMap(gpa, ini.names.len);
-    addStringsToMap(ip, names_map, ini.names);
+    // Same as above
+    errdefer @compileError("error path leaks values_map and extra data");
 
-    const values_map: OptionalMapIndex = if (ini.values.len == 0) .none else m: {
-        const values_map = try ip.addMap(gpa, ini.values.len);
-        addIndexesToMap(ip, values_map, ini.values);
-        break :m values_map.toOptional();
-    };
-    const fields_len: u32 = @intCast(ini.names.len);
-    try ip.extra.ensureUnusedCapacity(gpa, @typeInfo(EnumExplicit).Struct.fields.len +
-        ini.captures.len + fields_len);
-    ip.items.appendAssumeCapacity(.{
-        .tag = tag,
-        .data = ip.addExtraAssumeCapacity(EnumExplicit{
-            .decl = ini.decl,
-            .captures_len = @intCast(ini.captures.len),
-            .namespace = ini.namespace,
-            .int_tag_type = ini.tag_ty,
-            .fields_len = fields_len,
-            .names_map = names_map,
-            .values_map = values_map,
-            .zir_index = ini.zir_index,
-        }),
-    });
-    ip.extra.appendSliceAssumeCapacity(@ptrCast(ini.captures));
-    ip.extra.appendSliceAssumeCapacity(@ptrCast(ini.names));
-    ip.extra.appendSliceAssumeCapacity(@ptrCast(ini.values));
-    return @enumFromInt(ip.items.len - 1);
+    // Capacity for this was ensured earlier
+    const adapter: KeyAdapter = .{ .intern_pool = ip };
+    const gop = ip.map.getOrPutAssumeCapacityAdapted(Key{ .enum_type = .{
+        .generated_tag = .{ .union_type = ini.owner_union_ty },
+    } }, adapter);
+    assert(!gop.found_existing);
+    assert(gop.index == ip.items.len - 1);
+    return @enumFromInt(gop.index);
 }
 
 pub const OpaqueTypeIni = struct {
-    decl: DeclIndex,
-    namespace: NamespaceIndex,
-    zir_index: TrackedInst.Index.Optional,
-    captures: []const CaptureValue,
+    has_namespace: bool,
+    key: union(enum) {
+        declared: struct {
+            zir_index: TrackedInst.Index,
+            captures: []const CaptureValue,
+        },
+        reified: struct {
+            zir_index: TrackedInst.Index,
+            // No type hash since reifid opaques have no data other than the `@Type` location
+        },
+    },
 };
 
-pub fn getOpaqueType(ip: *InternPool, gpa: Allocator, ini: OpaqueTypeIni) Allocator.Error!Index {
+pub fn getOpaqueType(ip: *InternPool, gpa: Allocator, ini: OpaqueTypeIni) Allocator.Error!WipNamespaceType.Result {
     const adapter: KeyAdapter = .{ .intern_pool = ip };
-    try ip.extra.ensureUnusedCapacity(gpa, @typeInfo(LoadedOpaqueType).Struct.fields.len + ini.captures.len);
+    const gop = try ip.map.getOrPutAdapted(gpa, Key{ .opaque_type = switch (ini.key) {
+        .declared => |d| .{ .declared = .{
+            .zir_index = d.zir_index,
+            .captures = .{ .external = d.captures },
+        } },
+        .reified => |r| .{ .reified = .{
+            .zir_index = r.zir_index,
+            .type_hash = 0,
+        } },
+    } }, adapter);
+    if (gop.found_existing) return .{ .existing = @enumFromInt(gop.index) };
+    errdefer _ = ip.map.pop();
     try ip.items.ensureUnusedCapacity(gpa, 1);
-    const gop = try ip.map.getOrPutAdapted(gpa, Key{
-        .opaque_type = .{ .decl = ini.decl },
-    }, adapter);
-    if (gop.found_existing) return @enumFromInt(gop.index);
+    try ip.extra.ensureUnusedCapacity(gpa, @typeInfo(Tag.TypeOpaque).Struct.fields.len + switch (ini.key) {
+        .declared => |d| d.captures.len,
+        .reified => 0,
+    });
+    const extra_index = ip.addExtraAssumeCapacity(Tag.TypeOpaque{
+        .decl = undefined, // set by `finish`
+        .namespace = .none,
+        .zir_index = switch (ini.key) {
+            inline else => |x| x.zir_index,
+        },
+        .captures_len = switch (ini.key) {
+            .declared => |d| @intCast(d.captures.len),
+            .reified => std.math.maxInt(u32),
+        },
+    });
     ip.items.appendAssumeCapacity(.{
         .tag = .type_opaque,
-        .data = ip.addExtraAssumeCapacity(Tag.TypeOpaque{
-            .decl = ini.decl,
-            .namespace = ini.namespace,
-            .zir_index = ini.zir_index,
-            .captures_len = @intCast(ini.captures.len),
-        }),
+        .data = extra_index,
     });
-    ip.extra.appendSliceAssumeCapacity(@ptrCast(ini.captures));
-    return @enumFromInt(gop.index);
+    switch (ini.key) {
+        .declared => |d| ip.extra.appendSliceAssumeCapacity(@ptrCast(d.captures)),
+        .reified => {},
+    }
+    return .{ .wip = .{
+        .index = @enumFromInt(gop.index),
+        .decl_extra_index = extra_index + std.meta.fieldIndex(Tag.TypeOpaque, "decl").?,
+        .namespace_extra_index = if (ini.has_namespace)
+            extra_index + std.meta.fieldIndex(Tag.TypeOpaque, "namespace").?
+        else
+            null,
+    } };
 }
 
 pub fn getIfExists(ip: *const InternPool, key: Key) ?Index {
@@ -6837,8 +7216,34 @@ fn addMap(ip: *InternPool, gpa: Allocator, cap: usize) Allocator.Error!MapIndex
 
 /// This operation only happens under compile error conditions.
 /// Leak the index until the next garbage collection.
-/// TODO: this is a bit problematic to implement, can we get away without it?
-pub const remove = @compileError("InternPool.remove is not currently a supported operation; put a TODO there instead");
+/// Invalidates all references to this index.
+pub fn remove(ip: *InternPool, index: Index) void {
+    if (@intFromEnum(index) < static_keys.len) {
+        // The item being removed replaced a special index via `InternPool.resolveBuiltinType`.
+        // Restore the original item at this index.
+        switch (static_keys[@intFromEnum(index)]) {
+            .simple_type => |s| {
+                ip.items.set(@intFromEnum(index), .{
+                    .tag = .simple_type,
+                    .data = @intFromEnum(s),
+                });
+            },
+            else => unreachable,
+        }
+        return;
+    }
+
+    if (@intFromEnum(index) == ip.items.len - 1) {
+        // Happy case - we can just drop the item without affecting any other indices.
+        ip.items.len -= 1;
+        _ = ip.map.pop();
+    } else {
+        // We must preserve the item so that indices following it remain valid.
+        // Thus, we will rewrite the tag to `removed`, leaking the item until
+        // next GC but causing `KeyAdapter` to ignore it.
+        ip.items.set(@intFromEnum(index), .{ .tag = .removed, .data = undefined });
+    }
+}
 
 fn addInt(ip: *InternPool, gpa: Allocator, ty: Index, tag: Tag, limbs: []const Limb) !void {
     const limbs_len = @as(u32, @intCast(limbs.len));
@@ -7635,6 +8040,10 @@ fn dumpStatsFallible(ip: *const InternPool, arena: Allocator) anyerror!void {
         if (!gop.found_existing) gop.value_ptr.* = .{};
         gop.value_ptr.count += 1;
         gop.value_ptr.bytes += 1 + 4 + @as(usize, switch (tag) {
+            // Note that in this case, we have technically leaked some extra data
+            // bytes which we do not account for here.
+            .removed => 0,
+
             .type_int_signed => 0,
             .type_int_unsigned => 0,
             .type_array_small => @sizeOf(Vector),
@@ -7852,6 +8261,8 @@ fn dumpAllFallible(ip: *const InternPool) anyerror!void {
     for (tags, datas, 0..) |tag, data, i| {
         try w.print("${d} = {s}(", .{ i, @tagName(tag) });
         switch (tag) {
+            .removed => {},
+
             .simple_type => try w.print("{s}", .{@tagName(@as(SimpleType, @enumFromInt(data)))}),
             .simple_value => try w.print("{s}", .{@tagName(@as(SimpleValue, @enumFromInt(data)))}),
 
@@ -8258,6 +8669,8 @@ pub fn typeOf(ip: *const InternPool, index: Index) Index {
         // This optimization on tags is needed so that indexToKey can call
         // typeOf without being recursive.
         _ => switch (ip.items.items(.tag)[@intFromEnum(index)]) {
+            .removed => unreachable,
+
             .type_int_signed,
             .type_int_unsigned,
             .type_array_big,
@@ -8575,6 +8988,8 @@ pub fn zigTypeTagOrPoison(ip: *const InternPool, index: Index) error{GenericPois
         .var_args_param_type => unreachable, // special tag
 
         _ => switch (ip.items.items(.tag)[@intFromEnum(index)]) {
+            .removed => unreachable,
+
             .type_int_signed,
             .type_int_unsigned,
             => .Int,
src/Module.zig
@@ -568,9 +568,9 @@ pub const Decl = struct {
             .empty_struct_type => .none,
             .none => .none,
             else => switch (ip.indexToKey(decl.val.toIntern())) {
-                .opaque_type => ip.loadOpaqueType(decl.val.toIntern()).namespace.toOptional(),
+                .opaque_type => ip.loadOpaqueType(decl.val.toIntern()).namespace,
                 .struct_type => ip.loadStructType(decl.val.toIntern()).namespace,
-                .union_type => ip.loadUnionType(decl.val.toIntern()).namespace.toOptional(),
+                .union_type => ip.loadUnionType(decl.val.toIntern()).namespace,
                 .enum_type => ip.loadEnumType(decl.val.toIntern()).namespace,
                 else => .none,
             },
@@ -3302,6 +3302,70 @@ pub fn semaPkg(mod: *Module, pkg: *Package.Module) !void {
     return mod.semaFile(file);
 }
 
+fn getFileRootStruct(zcu: *Zcu, decl_index: Decl.Index, namespace_index: Namespace.Index, file: *File) Allocator.Error!InternPool.Index {
+    const gpa = zcu.gpa;
+    const ip = &zcu.intern_pool;
+    const extended = file.zir.instructions.items(.data)[@intFromEnum(Zir.Inst.Index.main_struct_inst)].extended;
+    assert(extended.opcode == .struct_decl);
+    const small: Zir.Inst.StructDecl.Small = @bitCast(extended.small);
+    assert(!small.has_captures_len);
+    assert(!small.has_backing_int);
+    assert(small.layout == .Auto);
+    var extra_index: usize = extended.operand + @typeInfo(Zir.Inst.StructDecl).Struct.fields.len;
+    const fields_len = if (small.has_fields_len) blk: {
+        const fields_len = file.zir.extra[extra_index];
+        extra_index += 1;
+        break :blk fields_len;
+    } else 0;
+    const decls_len = if (small.has_decls_len) blk: {
+        const decls_len = file.zir.extra[extra_index];
+        extra_index += 1;
+        break :blk decls_len;
+    } else 0;
+    const decls = file.zir.bodySlice(extra_index, decls_len);
+    extra_index += decls_len;
+
+    const tracked_inst = try ip.trackZir(gpa, file, .main_struct_inst);
+    const wip_ty = switch (try ip.getStructType(gpa, .{
+        .layout = .Auto,
+        .fields_len = fields_len,
+        .known_non_opv = small.known_non_opv,
+        .requires_comptime = if (small.known_comptime_only) .yes else .unknown,
+        .is_tuple = small.is_tuple,
+        .any_comptime_fields = small.any_comptime_fields,
+        .any_default_inits = small.any_default_inits,
+        .inits_resolved = false,
+        .any_aligned_fields = small.any_aligned_fields,
+        .has_namespace = true,
+        .key = .{ .declared = .{
+            .zir_index = tracked_inst,
+            .captures = &.{},
+        } },
+    })) {
+        .existing => unreachable, // we wouldn't be analysing the file root if this type existed
+        .wip => |wip| wip,
+    };
+    errdefer wip_ty.cancel(ip);
+
+    if (zcu.comp.debug_incremental) {
+        try ip.addDependency(
+            gpa,
+            InternPool.Depender.wrap(.{ .decl = decl_index }),
+            .{ .src_hash = tracked_inst },
+        );
+    }
+
+    const decl = zcu.declPtr(decl_index);
+    decl.val = Value.fromInterned(wip_ty.index);
+    decl.has_tv = true;
+    decl.owns_tv = true;
+    decl.analysis = .complete;
+
+    try zcu.scanNamespace(namespace_index, decls, decl);
+
+    return wip_ty.finish(ip, decl_index, namespace_index.toOptional());
+}
+
 /// Regardless of the file status, will create a `Decl` so that we
 /// can track dependencies and re-analyze when the file becomes outdated.
 pub fn semaFile(mod: *Module, file: *File) SemaError!void {
@@ -3323,7 +3387,6 @@ pub fn semaFile(mod: *Module, file: *File) SemaError!void {
         .decl_index = undefined,
         .file_scope = file,
     });
-    const new_namespace = mod.namespacePtr(new_namespace_index);
     errdefer mod.destroyNamespace(new_namespace_index);
 
     const new_decl_index = try mod.allocateNewDecl(new_namespace_index, 0);
@@ -3331,7 +3394,7 @@ pub fn semaFile(mod: *Module, file: *File) SemaError!void {
     errdefer @panic("TODO error handling");
 
     file.root_decl = new_decl_index.toOptional();
-    new_namespace.decl_index = new_decl_index;
+    mod.namespacePtr(new_namespace_index).decl_index = new_decl_index;
 
     new_decl.name = try file.fullyQualifiedName(mod);
     new_decl.name_fully_qualified = true;
@@ -3350,63 +3413,10 @@ pub fn semaFile(mod: *Module, file: *File) SemaError!void {
     }
     assert(file.zir_loaded);
 
-    var sema_arena = std.heap.ArenaAllocator.init(gpa);
-    defer sema_arena.deinit();
-    const sema_arena_allocator = sema_arena.allocator();
-
-    var comptime_mutable_decls = std.ArrayList(Decl.Index).init(gpa);
-    defer comptime_mutable_decls.deinit();
-
-    var comptime_err_ret_trace = std.ArrayList(SrcLoc).init(gpa);
-    defer comptime_err_ret_trace.deinit();
+    const struct_ty = try mod.getFileRootStruct(new_decl_index, new_namespace_index, file);
+    errdefer mod.intern_pool.remove(struct_ty);
 
-    var sema: Sema = .{
-        .mod = mod,
-        .gpa = gpa,
-        .arena = sema_arena_allocator,
-        .code = file.zir,
-        .owner_decl = new_decl,
-        .owner_decl_index = new_decl_index,
-        .func_index = .none,
-        .func_is_naked = false,
-        .fn_ret_ty = Type.void,
-        .fn_ret_ty_ies = null,
-        .owner_func_index = .none,
-        .comptime_mutable_decls = &comptime_mutable_decls,
-        .comptime_err_ret_trace = &comptime_err_ret_trace,
-    };
-    defer sema.deinit();
-
-    const struct_ty = sema.getStructType(
-        new_decl_index,
-        new_namespace_index,
-        null,
-        try mod.intern_pool.trackZir(gpa, file, .main_struct_inst),
-    ) catch |err| switch (err) {
-        error.OutOfMemory => return error.OutOfMemory,
-        // The following errors are from resolving capture values, but the root
-        // struct of a file has no captures.
-        error.AnalysisFail,
-        error.NeededSourceLocation,
-        error.GenericPoison,
-        error.ComptimeReturn,
-        error.ComptimeBreak,
-        => unreachable,
-    };
-    // TODO: figure out InternPool removals for incremental compilation
-    //errdefer ip.remove(struct_ty);
-    for (comptime_mutable_decls.items) |decl_index| {
-        const decl = mod.declPtr(decl_index);
-        _ = try decl.internValue(mod);
-    }
-
-    new_decl.val = Value.fromInterned(struct_ty);
-    new_decl.has_tv = true;
-    new_decl.owns_tv = true;
-    new_decl.analysis = .complete;
-
-    const comp = mod.comp;
-    switch (comp.cache_use) {
+    switch (mod.comp.cache_use) {
         .whole => |whole| if (whole.cache_manifest) |man| {
             const source = file.getSource(gpa) catch |err| {
                 try reportRetryableFileError(mod, file, "unable to load source: {s}", .{@errorName(err)});
@@ -5940,14 +5950,6 @@ pub fn atomicPtrAlignment(
     return .none;
 }
 
-pub fn opaqueSrcLoc(mod: *Module, opaque_type: InternPool.Key.OpaqueType) SrcLoc {
-    return mod.declPtr(opaque_type.decl).srcLoc(mod);
-}
-
-pub fn opaqueFullyQualifiedName(mod: *Module, opaque_type: InternPool.Key.OpaqueType) !InternPool.NullTerminatedString {
-    return mod.declPtr(opaque_type.decl).fullyQualifiedName(mod);
-}
-
 pub fn declFileScope(mod: *Module, decl_index: Decl.Index) *File {
     return mod.declPtr(decl_index).getFileScope(mod);
 }
src/Sema.zig
@@ -2685,7 +2685,7 @@ fn getCaptures(sema: *Sema, parent_namespace: ?InternPool.NamespaceIndex, extra_
         capture.* = switch (zir_capture.unwrap()) {
             .inst => |inst| InternPool.CaptureValue.wrap(capture: {
                 const air_ref = try sema.resolveInst(inst.toRef());
-                if (try sema.resolveValue(air_ref)) |val| {
+                if (try sema.resolveValueResolveLazy(air_ref)) |val| {
                     break :capture .{ .@"comptime" = val.toIntern() };
                 }
                 break :capture .{ .runtime = sema.typeOf(air_ref).toIntern() };
@@ -2697,23 +2697,20 @@ fn getCaptures(sema: *Sema, parent_namespace: ?InternPool.NamespaceIndex, extra_
     return captures;
 }
 
-pub fn getStructType(
+fn zirStructDecl(
     sema: *Sema,
-    decl: InternPool.DeclIndex,
-    namespace: InternPool.NamespaceIndex,
-    /// The direct parent Namespace for resolving nested capture values.
-    parent_namespace: ?InternPool.NamespaceIndex,
-    tracked_inst: InternPool.TrackedInst.Index,
-) !InternPool.Index {
+    block: *Block,
+    extended: Zir.Inst.Extended.InstData,
+    inst: Zir.Inst.Index,
+) CompileError!Air.Inst.Ref {
     const mod = sema.mod;
     const gpa = sema.gpa;
     const ip = &mod.intern_pool;
-    const zir_index = tracked_inst.resolve(ip);
-    const extended = sema.code.instructions.items(.data)[@intFromEnum(zir_index)].extended;
-    assert(extended.opcode == .struct_decl);
     const small: Zir.Inst.StructDecl.Small = @bitCast(extended.small);
+    const extra = sema.code.extraData(Zir.Inst.StructDecl, extended.operand);
+    const src = extra.data.src();
+    var extra_index = extra.end;
 
-    var extra_index: usize = extended.operand + @typeInfo(Zir.Inst.StructDecl).Struct.fields.len;
     const captures_len = if (small.has_captures_len) blk: {
         const captures_len = sema.code.extra[extra_index];
         extra_index += 1;
@@ -2730,7 +2727,7 @@ pub fn getStructType(
         break :blk decls_len;
     } else 0;
 
-    const captures = try sema.getCaptures(parent_namespace, extra_index, captures_len);
+    const captures = try sema.getCaptures(block.namespace, extra_index, captures_len);
     extra_index += captures_len;
 
     if (small.has_backing_int) {
@@ -2743,50 +2740,38 @@ pub fn getStructType(
         }
     }
 
-    const decls = sema.code.bodySlice(extra_index, decls_len);
-    try mod.scanNamespace(namespace, decls, mod.declPtr(decl));
-    extra_index += decls_len;
-
-    const ty = try ip.getStructType(gpa, .{
-        .decl = decl,
-        .namespace = namespace.toOptional(),
-        .zir_index = tracked_inst.toOptional(),
+    const wip_ty = switch (try ip.getStructType(gpa, .{
         .layout = small.layout,
-        .known_non_opv = small.known_non_opv,
-        .is_tuple = small.is_tuple,
         .fields_len = fields_len,
+        .known_non_opv = small.known_non_opv,
         .requires_comptime = if (small.known_comptime_only) .yes else .unknown,
-        .any_default_inits = small.any_default_inits,
+        .is_tuple = small.is_tuple,
         .any_comptime_fields = small.any_comptime_fields,
+        .any_default_inits = small.any_default_inits,
         .inits_resolved = false,
         .any_aligned_fields = small.any_aligned_fields,
-        .captures = captures,
-    });
-
-    return ty;
-}
-
-fn zirStructDecl(
-    sema: *Sema,
-    block: *Block,
-    extended: Zir.Inst.Extended.InstData,
-    inst: Zir.Inst.Index,
-) CompileError!Air.Inst.Ref {
-    const mod = sema.mod;
-    const ip = &mod.intern_pool;
-    const small: Zir.Inst.StructDecl.Small = @bitCast(extended.small);
-    const src = sema.code.extraData(Zir.Inst.StructDecl, extended.operand).data.src();
-
-    // Because these three things each reference each other, `undefined`
-    // placeholders are used before being set after the struct type gains an
-    // InternPool index.
+        .has_namespace = true or decls_len > 0, // TODO: see below
+        .key = .{ .declared = .{
+            .zir_index = try ip.trackZir(gpa, block.getFileScope(mod), inst),
+            .captures = captures,
+        } },
+    })) {
+        .existing => |ty| return Air.internedToRef(ty),
+        .wip => |wip| wip: {
+            if (sema.builtin_type_target_index == .none) break :wip wip;
+            var new = wip;
+            new.index = sema.builtin_type_target_index;
+            ip.resolveBuiltinType(new.index, wip.index);
+            break :wip new;
+        },
+    };
+    errdefer wip_ty.cancel(ip);
 
     const new_decl_index = try sema.createAnonymousDeclTypeNamed(block, src, .{
-        .ty = Type.noreturn,
-        .val = Value.@"unreachable",
+        .ty = Type.type,
+        .val = Value.fromInterned(wip_ty.index),
     }, small.name_strategy, "struct", inst);
-    const new_decl = mod.declPtr(new_decl_index);
-    new_decl.owns_tv = true;
+    mod.declPtr(new_decl_index).owns_tv = true;
     errdefer mod.abortAnonDecl(new_decl_index);
 
     if (sema.mod.comp.debug_incremental) {
@@ -2797,31 +2782,21 @@ fn zirStructDecl(
         );
     }
 
-    const new_namespace_index = try mod.createNamespace(.{
+    // TODO: if AstGen tells us `@This` was not used in the fields, we can elide the namespace.
+    const new_namespace_index: InternPool.OptionalNamespaceIndex = if (true or decls_len > 0) (try mod.createNamespace(.{
         .parent = block.namespace.toOptional(),
         .decl_index = new_decl_index,
         .file_scope = block.getFileScope(mod),
-    });
+    })).toOptional() else .none;
     errdefer mod.destroyNamespace(new_namespace_index);
 
-    const struct_ty = ty: {
-        const tracked_inst = try ip.trackZir(mod.gpa, block.getFileScope(mod), inst);
-        const ty = try sema.getStructType(new_decl_index, new_namespace_index, block.namespace, tracked_inst);
-        if (sema.builtin_type_target_index != .none) {
-            ip.resolveBuiltinType(sema.builtin_type_target_index, ty);
-            break :ty sema.builtin_type_target_index;
-        }
-        break :ty ty;
-    };
-    // TODO: figure out InternPool removals for incremental compilation
-    //errdefer ip.remove(struct_ty);
-
-    new_decl.ty = Type.type;
-    new_decl.val = Value.fromInterned(struct_ty);
+    if (new_namespace_index.unwrap()) |ns| {
+        const decls = sema.code.bodySlice(extra_index, decls_len);
+        try mod.scanNamespace(ns, decls, mod.declPtr(new_decl_index));
+    }
 
-    const decl_val = sema.analyzeDeclVal(block, src, new_decl_index);
     try mod.finalizeAnonDecl(new_decl_index);
-    return decl_val;
+    return Air.internedToRef(wip_ty.finish(ip, new_decl_index, new_namespace_index));
 }
 
 fn createAnonymousDeclTypeNamed(
@@ -2931,6 +2906,7 @@ fn zirEnumDecl(
 
     const mod = sema.mod;
     const gpa = sema.gpa;
+    const ip = &mod.intern_pool;
     const small: Zir.Inst.EnumDecl.Small = @bitCast(extended.small);
     const extra = sema.code.extraData(Zir.Inst.EnumDecl, extended.operand);
     var extra_index: usize = extra.end;
@@ -2968,39 +2944,10 @@ fn zirEnumDecl(
         break :blk decls_len;
     } else 0;
 
-    // Because these three things each reference each other, `undefined`
-    // placeholders are used before being set after the enum type gains an
-    // InternPool index.
-
-    var done = false;
-    const new_decl_index = try sema.createAnonymousDeclTypeNamed(block, src, .{
-        .ty = Type.noreturn,
-        .val = Value.@"unreachable",
-    }, small.name_strategy, "enum", inst);
-    const new_decl = mod.declPtr(new_decl_index);
-    new_decl.owns_tv = true;
-    errdefer if (!done) mod.abortAnonDecl(new_decl_index);
-
-    if (sema.mod.comp.debug_incremental) {
-        try mod.intern_pool.addDependency(
-            sema.gpa,
-            InternPool.Depender.wrap(.{ .decl = new_decl_index }),
-            .{ .src_hash = try mod.intern_pool.trackZir(sema.gpa, block.getFileScope(mod), inst) },
-        );
-    }
-
     const captures = try sema.getCaptures(block.namespace, extra_index, captures_len);
     extra_index += captures_len;
 
-    const new_namespace_index = try mod.createNamespace(.{
-        .parent = block.namespace.toOptional(),
-        .decl_index = new_decl_index,
-        .file_scope = block.getFileScope(mod),
-    });
-    errdefer if (!done) mod.destroyNamespace(new_namespace_index);
-
     const decls = sema.code.bodySlice(extra_index, decls_len);
-    try mod.scanNamespace(new_namespace_index, decls, new_decl);
     extra_index += decls_len;
 
     const body = sema.code.bodySlice(extra_index, body_len);
@@ -3014,36 +2961,59 @@ fn zirEnumDecl(
         if (bag != 0) break true;
     } else false;
 
-    const incomplete_enum = incomplete_enum: {
-        var incomplete_enum = try mod.intern_pool.getIncompleteEnum(gpa, .{
-            .decl = new_decl_index,
-            .namespace = new_namespace_index.toOptional(),
-            .fields_len = fields_len,
-            .has_values = any_values,
-            .tag_mode = if (small.nonexhaustive)
-                .nonexhaustive
-            else if (tag_type_ref == .none)
-                .auto
-            else
-                .explicit,
-            .zir_index = (try mod.intern_pool.trackZir(sema.gpa, block.getFileScope(mod), inst)).toOptional(),
+    const wip_ty = switch (try ip.getEnumType(gpa, .{
+        .has_namespace = true or decls_len > 0, // TODO: see below
+        .has_values = any_values,
+        .tag_mode = if (small.nonexhaustive)
+            .nonexhaustive
+        else if (tag_type_ref == .none)
+            .auto
+        else
+            .explicit,
+        .fields_len = fields_len,
+        .key = .{ .declared = .{
+            .zir_index = try mod.intern_pool.trackZir(sema.gpa, block.getFileScope(mod), inst),
             .captures = captures,
-        });
-        if (sema.builtin_type_target_index != .none) {
-            mod.intern_pool.resolveBuiltinType(sema.builtin_type_target_index, incomplete_enum.index);
-            incomplete_enum.index = sema.builtin_type_target_index;
-        }
-        break :incomplete_enum incomplete_enum;
+        } },
+    })) {
+        .wip => |wip| wip: {
+            if (sema.builtin_type_target_index == .none) break :wip wip;
+            var new = wip;
+            new.index = sema.builtin_type_target_index;
+            ip.resolveBuiltinType(new.index, wip.index);
+            break :wip new;
+        },
+        .existing => |ty| return Air.internedToRef(ty),
     };
-    // TODO: figure out InternPool removals for incremental compilation
-    //errdefer if (!done) mod.intern_pool.remove(incomplete_enum.index);
+    errdefer wip_ty.cancel(ip);
 
-    new_decl.ty = Type.type;
-    new_decl.val = Value.fromInterned(incomplete_enum.index);
+    const new_decl_index = try sema.createAnonymousDeclTypeNamed(block, src, .{
+        .ty = Type.type,
+        .val = Value.fromInterned(wip_ty.index),
+    }, small.name_strategy, "enum", inst);
+    const new_decl = mod.declPtr(new_decl_index);
+    new_decl.owns_tv = true;
+    errdefer mod.abortAnonDecl(new_decl_index);
 
-    const decl_val = try sema.analyzeDeclVal(block, src, new_decl_index);
-    try mod.finalizeAnonDecl(new_decl_index);
-    done = true;
+    if (sema.mod.comp.debug_incremental) {
+        try mod.intern_pool.addDependency(
+            sema.gpa,
+            InternPool.Depender.wrap(.{ .decl = new_decl_index }),
+            .{ .src_hash = try mod.intern_pool.trackZir(sema.gpa, block.getFileScope(mod), inst) },
+        );
+    }
+
+    // TODO: if AstGen tells us `@This` was not used in the fields, we can elide the namespace.
+    const new_namespace_index: InternPool.OptionalNamespaceIndex = if (true or decls_len > 0) (try mod.createNamespace(.{
+        .parent = block.namespace.toOptional(),
+        .decl_index = new_decl_index,
+        .file_scope = block.getFileScope(mod),
+    })).toOptional() else .none;
+    errdefer if (new_namespace_index.unwrap()) |ns| mod.destroyNamespace(ns);
+
+    if (new_namespace_index.unwrap()) |ns| {
+        try mod.scanNamespace(ns, decls, new_decl);
+    }
 
     const int_tag_ty = ty: {
         // We create a block for the field type instructions because they
@@ -3072,7 +3042,7 @@ fn zirEnumDecl(
             .parent = null,
             .sema = sema,
             .src_decl = new_decl_index,
-            .namespace = new_namespace_index,
+            .namespace = new_namespace_index.unwrap() orelse block.namespace,
             .instructions = .{},
             .inlining = null,
             .is_comptime = true,
@@ -3088,7 +3058,6 @@ fn zirEnumDecl(
             if (ty.zigTypeTag(mod) != .Int and ty.zigTypeTag(mod) != .ComptimeInt) {
                 return sema.fail(block, tag_ty_src, "expected integer tag type, found '{}'", .{ty.fmt(sema.mod)});
             }
-            incomplete_enum.setTagType(&mod.intern_pool, ty.toIntern());
             break :ty ty;
         } else if (fields_len == 0) {
             break :ty try mod.intType(.unsigned, 0);
@@ -3098,6 +3067,8 @@ fn zirEnumDecl(
         }
     };
 
+    wip_ty.prepare(ip, new_decl_index, new_namespace_index, int_tag_ty.toIntern());
+
     if (small.nonexhaustive and int_tag_ty.toIntern() != .comptime_int_type) {
         if (fields_len > 1 and std.math.log2_int(u64, fields_len) == int_tag_ty.bitSize(mod)) {
             return sema.fail(block, src, "non-exhaustive enum specifies every value", .{});
@@ -3121,7 +3092,6 @@ fn zirEnumDecl(
         extra_index += 2; // field name, doc comment
 
         const field_name = try mod.intern_pool.getOrPutString(gpa, field_name_zir);
-        assert(incomplete_enum.addFieldName(&mod.intern_pool, field_name) == null);
 
         const tag_overflow = if (has_tag_value) overflow: {
             const tag_val_ref: Zir.Inst.Ref = @enumFromInt(sema.code.extra[extra_index]);
@@ -3142,12 +3112,13 @@ fn zirEnumDecl(
             };
             if (!(try sema.intFitsInType(last_tag_val.?, int_tag_ty, null))) break :overflow true;
             last_tag_val = try mod.getCoerced(last_tag_val.?, int_tag_ty);
-            if (incomplete_enum.addFieldValue(&mod.intern_pool, last_tag_val.?.toIntern())) |other_index| {
+            if (wip_ty.nextField(&mod.intern_pool, field_name, last_tag_val.?.toIntern())) |conflict| {
+                assert(conflict.kind == .value); // AstGen validated names are unique
                 const value_src = mod.fieldSrcLoc(new_decl_index, .{
                     .index = field_i,
                     .range = .value,
                 }).lazy;
-                const other_field_src = mod.fieldSrcLoc(new_decl_index, .{ .index = other_index }).lazy;
+                const other_field_src = mod.fieldSrcLoc(new_decl_index, .{ .index = conflict.prev_field_idx }).lazy;
                 const msg = msg: {
                     const msg = try sema.errMsg(block, value_src, "enum tag value {} already taken", .{last_tag_val.?.fmtValue(int_tag_ty, sema.mod)});
                     errdefer msg.destroy(gpa);
@@ -3164,9 +3135,10 @@ fn zirEnumDecl(
             else
                 try mod.intValue(int_tag_ty, 0);
             if (overflow != null) break :overflow true;
-            if (incomplete_enum.addFieldValue(&mod.intern_pool, last_tag_val.?.toIntern())) |other_index| {
+            if (wip_ty.nextField(&mod.intern_pool, field_name, last_tag_val.?.toIntern())) |conflict| {
+                assert(conflict.kind == .value); // AstGen validated names are unique
                 const field_src = mod.fieldSrcLoc(new_decl_index, .{ .index = field_i }).lazy;
-                const other_field_src = mod.fieldSrcLoc(new_decl_index, .{ .index = other_index }).lazy;
+                const other_field_src = mod.fieldSrcLoc(new_decl_index, .{ .index = conflict.prev_field_idx }).lazy;
                 const msg = msg: {
                     const msg = try sema.errMsg(block, field_src, "enum tag value {} already taken", .{last_tag_val.?.fmtValue(int_tag_ty, sema.mod)});
                     errdefer msg.destroy(gpa);
@@ -3177,6 +3149,7 @@ fn zirEnumDecl(
             }
             break :overflow false;
         } else overflow: {
+            assert(wip_ty.nextField(&mod.intern_pool, field_name, .none) == null);
             last_tag_val = try mod.intValue(Type.comptime_int, field_i);
             if (!try sema.intFitsInType(last_tag_val.?, int_tag_ty, null)) break :overflow true;
             last_tag_val = try mod.getCoerced(last_tag_val.?, int_tag_ty);
@@ -3194,7 +3167,9 @@ fn zirEnumDecl(
             return sema.failWithOwnedErrorMsg(block, msg);
         }
     }
-    return decl_val;
+
+    try mod.finalizeAnonDecl(new_decl_index);
+    return Air.internedToRef(wip_ty.finish(ip));
 }
 
 fn zirUnionDecl(
@@ -3208,6 +3183,7 @@ fn zirUnionDecl(
 
     const mod = sema.mod;
     const gpa = sema.gpa;
+    const ip = &mod.intern_pool;
     const small: Zir.Inst.UnionDecl.Small = @bitCast(extended.small);
     const extra = sema.code.extraData(Zir.Inst.UnionDecl, extended.operand);
     var extra_index: usize = extra.end;
@@ -3233,16 +3209,53 @@ fn zirUnionDecl(
         break :blk decls_len;
     } else 0;
 
-    // Because these three things each reference each other, `undefined`
-    // placeholders are used before being set after the union type gains an
-    // InternPool index.
+    const captures = try sema.getCaptures(block.namespace, extra_index, captures_len);
+    extra_index += captures_len;
+
+    const wip_ty = switch (try ip.getUnionType(gpa, .{
+        .flags = .{
+            .layout = small.layout,
+            .status = .none,
+            .runtime_tag = if (small.has_tag_type or small.auto_enum_tag)
+                .tagged
+            else if (small.layout != .Auto)
+                .none
+            else switch (block.wantSafety()) {
+                true => .safety,
+                false => .none,
+            },
+            .any_aligned_fields = small.any_aligned_fields,
+            .requires_comptime = .unknown,
+            .assumed_runtime_bits = false,
+            .assumed_pointer_aligned = false,
+            .alignment = .none,
+        },
+        .has_namespace = true or decls_len != 0, // TODO: see below
+        .fields_len = fields_len,
+        .enum_tag_ty = .none, // set later
+        .field_types = &.{}, // set later
+        .field_aligns = &.{}, // set later
+        .key = .{ .declared = .{
+            .zir_index = try ip.trackZir(gpa, block.getFileScope(mod), inst),
+            .captures = captures,
+        } },
+    })) {
+        .wip => |wip| wip: {
+            if (sema.builtin_type_target_index == .none) break :wip wip;
+            var new = wip;
+            new.index = sema.builtin_type_target_index;
+            ip.resolveBuiltinType(new.index, wip.index);
+            break :wip new;
+        },
+        .existing => |ty| return Air.internedToRef(ty),
+    };
+    errdefer wip_ty.cancel(ip);
 
     const new_decl_index = try sema.createAnonymousDeclTypeNamed(block, src, .{
-        .ty = Type.noreturn,
-        .val = Value.@"unreachable",
+        .ty = Type.type,
+        .val = Value.fromInterned(wip_ty.index),
     }, small.name_strategy, "union", inst);
-    const new_decl = mod.declPtr(new_decl_index);
-    new_decl.owns_tv = true;
+    mod.declPtr(new_decl_index).owns_tv = true;
     errdefer mod.abortAnonDecl(new_decl_index);
 
     if (sema.mod.comp.debug_incremental) {
@@ -3253,62 +3266,22 @@ fn zirUnionDecl(
         );
     }
 
-    const captures = try sema.getCaptures(block.namespace, extra_index, captures_len);
-    extra_index += captures_len;
-
-    const new_namespace_index = try mod.createNamespace(.{
+    // TODO: if AstGen tells us `@This` was not used in the fields, we can elide the namespace.
+    const new_namespace_index: InternPool.OptionalNamespaceIndex = if (true or decls_len > 0) (try mod.createNamespace(.{
         .parent = block.namespace.toOptional(),
         .decl_index = new_decl_index,
         .file_scope = block.getFileScope(mod),
-    });
-    errdefer mod.destroyNamespace(new_namespace_index);
-
-    const union_ty = ty: {
-        const ty = try mod.intern_pool.getUnionType(gpa, .{
-            .flags = .{
-                .layout = small.layout,
-                .status = .none,
-                .runtime_tag = if (small.has_tag_type or small.auto_enum_tag)
-                    .tagged
-                else if (small.layout != .Auto)
-                    .none
-                else switch (block.wantSafety()) {
-                    true => .safety,
-                    false => .none,
-                },
-                .any_aligned_fields = small.any_aligned_fields,
-                .requires_comptime = .unknown,
-                .assumed_runtime_bits = false,
-                .assumed_pointer_aligned = false,
-                .alignment = .none,
-            },
-            .decl = new_decl_index,
-            .namespace = new_namespace_index,
-            .zir_index = (try mod.intern_pool.trackZir(gpa, block.getFileScope(mod), inst)).toOptional(),
-            .fields_len = fields_len,
-            .enum_tag_ty = .none,
-            .field_types = &.{},
-            .field_aligns = &.{},
-            .captures = captures,
-        });
-        if (sema.builtin_type_target_index != .none) {
-            mod.intern_pool.resolveBuiltinType(sema.builtin_type_target_index, ty);
-            break :ty sema.builtin_type_target_index;
-        }
-        break :ty ty;
-    };
-    // TODO: figure out InternPool removals for incremental compilation
-    //errdefer mod.intern_pool.remove(union_ty);
+    })).toOptional() else .none;
+    errdefer if (new_namespace_index.unwrap()) |ns| mod.destroyNamespace(ns);
 
-    new_decl.ty = Type.type;
-    new_decl.val = Value.fromInterned(union_ty);
-
-    const decls = sema.code.bodySlice(extra_index, decls_len);
-    try mod.scanNamespace(new_namespace_index, decls, new_decl);
+    if (new_namespace_index.unwrap()) |ns| {
+        const decls = sema.code.bodySlice(extra_index, decls_len);
+        try mod.scanNamespace(ns, decls, mod.declPtr(new_decl_index));
+    }
 
-    const decl_val = sema.analyzeDeclVal(block, src, new_decl_index);
     try mod.finalizeAnonDecl(new_decl_index);
-    return decl_val;
+
+    return Air.internedToRef(wip_ty.finish(ip, new_decl_index, new_namespace_index));
 }
 
 fn zirOpaqueDecl(
@@ -3321,6 +3294,9 @@ fn zirOpaqueDecl(
     defer tracy.end();
 
     const mod = sema.mod;
+    const gpa = sema.gpa;
+    const ip = &mod.intern_pool;
+
     const small: Zir.Inst.OpaqueDecl.Small = @bitCast(extended.small);
     const extra = sema.code.extraData(Zir.Inst.OpaqueDecl, extended.operand);
     var extra_index: usize = extra.end;
@@ -3339,54 +3315,51 @@ fn zirOpaqueDecl(
         break :blk decls_len;
     } else 0;
 
-    // Because these three things each reference each other, `undefined`
-    // placeholders are used in two places before being set after the opaque
-    // type gains an InternPool index.
+    const captures = try sema.getCaptures(block.namespace, extra_index, captures_len);
+    extra_index += captures_len;
+
+    const wip_ty = switch (try ip.getOpaqueType(gpa, .{
+        .has_namespace = decls_len != 0,
+        .key = .{ .declared = .{
+            .zir_index = try ip.trackZir(gpa, block.getFileScope(mod), inst),
+            .captures = captures,
+        } },
+    })) {
+        .wip => |wip| wip,
+        .existing => |ty| return Air.internedToRef(ty),
+    };
+    errdefer wip_ty.cancel(ip);
 
     const new_decl_index = try sema.createAnonymousDeclTypeNamed(block, src, .{
-        .ty = Type.noreturn,
-        .val = Value.@"unreachable",
+        .ty = Type.type,
+        .val = Value.fromInterned(wip_ty.index),
     }, small.name_strategy, "opaque", inst);
-    const new_decl = mod.declPtr(new_decl_index);
-    new_decl.owns_tv = true;
+    mod.declPtr(new_decl_index).owns_tv = true;
     errdefer mod.abortAnonDecl(new_decl_index);
 
     if (sema.mod.comp.debug_incremental) {
-        try mod.intern_pool.addDependency(
-            sema.gpa,
+        try ip.addDependency(
+            gpa,
             InternPool.Depender.wrap(.{ .decl = new_decl_index }),
-            .{ .src_hash = try mod.intern_pool.trackZir(sema.gpa, block.getFileScope(mod), inst) },
+            .{ .src_hash = try ip.trackZir(gpa, block.getFileScope(mod), inst) },
         );
     }
 
-    const captures = try sema.getCaptures(block.namespace, extra_index, captures_len);
-    extra_index += captures_len;
-
-    const new_namespace_index = try mod.createNamespace(.{
+    const new_namespace_index: InternPool.OptionalNamespaceIndex = if (decls_len > 0) (try mod.createNamespace(.{
         .parent = block.namespace.toOptional(),
         .decl_index = new_decl_index,
         .file_scope = block.getFileScope(mod),
-    });
-    errdefer mod.destroyNamespace(new_namespace_index);
-
-    const opaque_ty = try mod.intern_pool.getOpaqueType(sema.gpa, .{
-        .decl = new_decl_index,
-        .namespace = new_namespace_index,
-        .zir_index = (try mod.intern_pool.trackZir(sema.gpa, block.getFileScope(mod), inst)).toOptional(),
-        .captures = captures,
-    });
-    // TODO: figure out InternPool removals for incremental compilation
-    //errdefer mod.intern_pool.remove(opaque_ty);
-
-    new_decl.ty = Type.type;
-    new_decl.val = Value.fromInterned(opaque_ty);
+    })).toOptional() else .none;
+    errdefer if (new_namespace_index.unwrap()) |ns| mod.destroyNamespace(ns);
 
-    const decls = sema.code.bodySlice(extra_index, decls_len);
-    try mod.scanNamespace(new_namespace_index, decls, new_decl);
+    if (new_namespace_index.unwrap()) |ns| {
+        const decls = sema.code.bodySlice(extra_index, decls_len);
+        try mod.scanNamespace(ns, decls, mod.declPtr(new_decl_index));
+    }
 
-    const decl_val = sema.analyzeDeclVal(block, src, new_decl_index);
     try mod.finalizeAnonDecl(new_decl_index);
-    return decl_val;
+
+    return Air.internedToRef(wip_ty.finish(ip, new_decl_index, new_namespace_index));
 }
 
 fn zirErrorSetDecl(
@@ -21322,103 +21295,11 @@ fn zirReify(
                 try ip.getOrPutString(gpa, "is_exhaustive"),
             ).?);
 
-            // Decls
             if (decls_val.sliceLen(mod) > 0) {
                 return sema.fail(block, src, "reified enums must have no decls", .{});
             }
 
-            const int_tag_ty = tag_type_val.toType();
-            if (int_tag_ty.zigTypeTag(mod) != .Int) {
-                return sema.fail(block, src, "Type.Enum.tag_type must be an integer type", .{});
-            }
-
-            // Because these things each reference each other, `undefined`
-            // placeholders are used before being set after the enum type gains
-            // an InternPool index.
-
-            const new_decl_index = try sema.createAnonymousDeclTypeNamed(block, src, .{
-                .ty = Type.noreturn,
-                .val = Value.@"unreachable",
-            }, name_strategy, "enum", inst);
-            const new_decl = mod.declPtr(new_decl_index);
-            new_decl.owns_tv = true;
-            errdefer {
-                new_decl.has_tv = false; // namespace and val were destroyed by later errdefers
-                mod.abortAnonDecl(new_decl_index);
-            }
-
-            // Define our empty enum decl
-            const fields_len: u32 = @intCast(try sema.usizeCast(block, src, fields_val.sliceLen(mod)));
-            const incomplete_enum = try ip.getIncompleteEnum(gpa, .{
-                .decl = new_decl_index,
-                .namespace = .none,
-                .fields_len = fields_len,
-                .has_values = true,
-                .tag_mode = if (!is_exhaustive_val.toBool())
-                    .nonexhaustive
-                else
-                    .explicit,
-                .tag_ty = int_tag_ty.toIntern(),
-                .zir_index = .none,
-                .captures = &.{},
-            });
-            // TODO: figure out InternPool removals for incremental compilation
-            //errdefer ip.remove(incomplete_enum.index);
-
-            new_decl.ty = Type.type;
-            new_decl.val = Value.fromInterned(incomplete_enum.index);
-
-            for (0..fields_len) |field_i| {
-                const elem_val = try fields_val.elemValue(mod, field_i);
-                const elem_struct_type = ip.loadStructType(ip.typeOf(elem_val.toIntern()));
-                const name_val = try elem_val.fieldValue(mod, elem_struct_type.nameIndex(
-                    ip,
-                    try ip.getOrPutString(gpa, "name"),
-                ).?);
-                const value_val = try elem_val.fieldValue(mod, elem_struct_type.nameIndex(
-                    ip,
-                    try ip.getOrPutString(gpa, "value"),
-                ).?);
-
-                const field_name = try name_val.toIpString(Type.slice_const_u8, mod);
-
-                if (!try sema.intFitsInType(value_val, int_tag_ty, null)) {
-                    // TODO: better source location
-                    return sema.fail(block, src, "field '{}' with enumeration value '{}' is too large for backing int type '{}'", .{
-                        field_name.fmt(ip),
-                        value_val.fmtValue(Type.comptime_int, mod),
-                        int_tag_ty.fmt(mod),
-                    });
-                }
-
-                if (incomplete_enum.addFieldName(ip, field_name)) |other_index| {
-                    const msg = msg: {
-                        const msg = try sema.errMsg(block, src, "duplicate enum field '{}'", .{
-                            field_name.fmt(ip),
-                        });
-                        errdefer msg.destroy(gpa);
-                        _ = other_index; // TODO: this note is incorrect
-                        try sema.errNote(block, src, msg, "other field here", .{});
-                        break :msg msg;
-                    };
-                    return sema.failWithOwnedErrorMsg(block, msg);
-                }
-
-                if (incomplete_enum.addFieldValue(ip, (try mod.getCoerced(value_val, int_tag_ty)).toIntern())) |other| {
-                    const msg = msg: {
-                        const msg = try sema.errMsg(block, src, "enum tag value {} already taken", .{value_val.fmtValue(Type.comptime_int, mod)});
-                        errdefer msg.destroy(gpa);
-                        _ = other; // TODO: this note is incorrect
-                        try sema.errNote(block, src, msg, "other enum tag value here", .{});
-                        break :msg msg;
-                    };
-                    return sema.failWithOwnedErrorMsg(block, msg);
-                }
-            }
-
-            const decl_val = sema.analyzeDeclVal(block, src, new_decl_index);
-            try mod.finalizeAnonDecl(new_decl_index);
-            return decl_val;
+            return sema.reifyEnum(block, inst, src, tag_type_val.toType(), is_exhaustive_val.toBool(), fields_val, name_strategy);
         },
         .Opaque => {
             const struct_type = ip.loadStructType(ip.typeOf(union_val.val));
@@ -21432,43 +21313,27 @@ fn zirReify(
                 return sema.fail(block, src, "reified opaque must have no decls", .{});
             }
 
-            // Because these three things each reference each other,
-            // `undefined` placeholders are used in two places before being set
-            // after the opaque type gains an InternPool index.
+            const wip_ty = switch (try ip.getOpaqueType(gpa, .{
+                .has_namespace = false,
+                .key = .{ .reified = .{
+                    .zir_index = try ip.trackZir(gpa, block.getFileScope(mod), inst),
+                } },
+            })) {
+                .existing => |ty| return Air.internedToRef(ty),
+                .wip => |wip| wip,
+            };
+            errdefer wip_ty.cancel(ip);
 
             const new_decl_index = try sema.createAnonymousDeclTypeNamed(block, src, .{
-                .ty = Type.noreturn,
-                .val = Value.@"unreachable",
+                .ty = Type.type,
+                .val = Value.fromInterned(wip_ty.index),
             }, name_strategy, "opaque", inst);
-            const new_decl = mod.declPtr(new_decl_index);
-            new_decl.owns_tv = true;
-            errdefer {
-                new_decl.has_tv = false; // namespace and val were destroyed by later errdefers
-                mod.abortAnonDecl(new_decl_index);
-            }
-
-            const new_namespace_index = try mod.createNamespace(.{
-                .parent = block.namespace.toOptional(),
-                .decl_index = new_decl_index,
-                .file_scope = block.getFileScope(mod),
-            });
-            errdefer mod.destroyNamespace(new_namespace_index);
-
-            const opaque_ty = try ip.getOpaqueType(gpa, .{
-                .decl = new_decl_index,
-                .namespace = new_namespace_index,
-                .zir_index = .none,
-                .captures = &.{},
-            });
-            // TODO: figure out InternPool removals for incremental compilation
-            //errdefer ip.remove(opaque_ty);
-
-            new_decl.ty = Type.type;
-            new_decl.val = Value.fromInterned(opaque_ty);
+            mod.declPtr(new_decl_index).owns_tv = true;
+            errdefer mod.abortAnonDecl(new_decl_index);
 
-            const decl_val = sema.analyzeDeclVal(block, src, new_decl_index);
             try mod.finalizeAnonDecl(new_decl_index);
-            return decl_val;
+
+            return Air.internedToRef(wip_ty.finish(ip, new_decl_index, .none));
         },
         .Union => {
             const struct_type = ip.loadStructType(ip.typeOf(union_val.val));
@@ -21489,214 +21354,12 @@ fn zirReify(
                 try ip.getOrPutString(gpa, "decls"),
             ).?);
 
-            // Decls
             if (decls_val.sliceLen(mod) > 0) {
                 return sema.fail(block, src, "reified unions must have no decls", .{});
             }
             const layout = mod.toEnum(std.builtin.Type.ContainerLayout, layout_val);
-            const fields_len: u32 = @intCast(try sema.usizeCast(block, src, fields_val.sliceLen(mod)));
-
-            // Tag type
-            var explicit_tags_seen: []bool = &.{};
-            var enum_field_names: []InternPool.NullTerminatedString = &.{};
-            var enum_tag_ty: InternPool.Index = .none;
-            if (tag_type_val.optionalValue(mod)) |payload_val| {
-                enum_tag_ty = payload_val.toType().toIntern();
-
-                const enum_type = switch (ip.indexToKey(enum_tag_ty)) {
-                    .enum_type => ip.loadEnumType(enum_tag_ty),
-                    else => return sema.fail(block, src, "Type.Union.tag_type must be an enum type", .{}),
-                };
-
-                explicit_tags_seen = try sema.arena.alloc(bool, enum_type.names.len);
-                @memset(explicit_tags_seen, false);
-            } else {
-                enum_field_names = try sema.arena.alloc(InternPool.NullTerminatedString, fields_len);
-            }
-
-            // Fields
-            var any_aligned_fields: bool = false;
-            var union_fields: std.MultiArrayList(struct {
-                type: InternPool.Index,
-                alignment: InternPool.Alignment,
-            }) = .{};
-            var field_name_table: std.AutoArrayHashMapUnmanaged(InternPool.NullTerminatedString, void) = .{};
-            try field_name_table.ensureTotalCapacity(sema.arena, fields_len);
 
-            for (0..fields_len) |i| {
-                const elem_val = try fields_val.elemValue(mod, i);
-                const elem_struct_type = ip.loadStructType(ip.typeOf(elem_val.toIntern()));
-                const name_val = try elem_val.fieldValue(mod, elem_struct_type.nameIndex(
-                    ip,
-                    try ip.getOrPutString(gpa, "name"),
-                ).?);
-                const type_val = try elem_val.fieldValue(mod, elem_struct_type.nameIndex(
-                    ip,
-                    try ip.getOrPutString(gpa, "type"),
-                ).?);
-                const alignment_val = try elem_val.fieldValue(mod, elem_struct_type.nameIndex(
-                    ip,
-                    try ip.getOrPutString(gpa, "alignment"),
-                ).?);
-
-                const field_name = try name_val.toIpString(Type.slice_const_u8, mod);
-
-                if (enum_field_names.len != 0) {
-                    enum_field_names[i] = field_name;
-                }
-
-                if (enum_tag_ty != .none) {
-                    const tag_info = ip.loadEnumType(enum_tag_ty);
-                    const enum_index = tag_info.nameIndex(ip, field_name) orelse {
-                        return sema.fail(block, src, "no field named '{}' in enum '{}'", .{
-                            field_name.fmt(ip), Type.fromInterned(enum_tag_ty).fmt(mod),
-                        });
-                    };
-                    assert(explicit_tags_seen.len == tag_info.names.len);
-                    // No check for duplicate because the check already happened in order
-                    // to create the enum type in the first place.
-                    assert(!explicit_tags_seen[enum_index]);
-                    explicit_tags_seen[enum_index] = true;
-                }
-
-                const gop = field_name_table.getOrPutAssumeCapacity(field_name);
-                if (gop.found_existing) {
-                    // TODO: better source location
-                    return sema.fail(block, src, "duplicate union field {}", .{field_name.fmt(ip)});
-                }
-
-                const field_ty = type_val.toType();
-                const alignment_val_int = (try alignment_val.getUnsignedIntAdvanced(mod, sema)).?;
-                if (alignment_val_int > 0 and !math.isPowerOfTwo(alignment_val_int)) {
-                    // TODO: better source location
-                    return sema.fail(block, src, "alignment value '{d}' is not a power of two or zero", .{
-                        alignment_val_int,
-                    });
-                }
-                const field_align = Alignment.fromByteUnits(alignment_val_int);
-                any_aligned_fields = any_aligned_fields or field_align != .none;
-
-                try union_fields.append(sema.arena, .{
-                    .type = field_ty.toIntern(),
-                    .alignment = field_align,
-                });
-
-                if (field_ty.zigTypeTag(mod) == .Opaque) {
-                    const msg = msg: {
-                        const msg = try sema.errMsg(block, src, "opaque types have unknown size and therefore cannot be directly embedded in unions", .{});
-                        errdefer msg.destroy(gpa);
-
-                        try sema.addDeclaredHereNote(msg, field_ty);
-                        break :msg msg;
-                    };
-                    return sema.failWithOwnedErrorMsg(block, msg);
-                }
-                if (layout == .Extern and !try sema.validateExternType(field_ty, .union_field)) {
-                    const msg = msg: {
-                        const msg = try sema.errMsg(block, src, "extern unions cannot contain fields of type '{}'", .{field_ty.fmt(mod)});
-                        errdefer msg.destroy(gpa);
-
-                        const src_decl = mod.declPtr(block.src_decl);
-                        try sema.explainWhyTypeIsNotExtern(msg, src_decl.toSrcLoc(src, mod), field_ty, .union_field);
-
-                        try sema.addDeclaredHereNote(msg, field_ty);
-                        break :msg msg;
-                    };
-                    return sema.failWithOwnedErrorMsg(block, msg);
-                } else if (layout == .Packed and !try sema.validatePackedType(field_ty)) {
-                    const msg = msg: {
-                        const msg = try sema.errMsg(block, src, "packed unions cannot contain fields of type '{}'", .{field_ty.fmt(mod)});
-                        errdefer msg.destroy(gpa);
-
-                        const src_decl = mod.declPtr(block.src_decl);
-                        try sema.explainWhyTypeIsNotPacked(msg, src_decl.toSrcLoc(src, mod), field_ty);
-
-                        try sema.addDeclaredHereNote(msg, field_ty);
-                        break :msg msg;
-                    };
-                    return sema.failWithOwnedErrorMsg(block, msg);
-                }
-            }
-
-            if (enum_tag_ty != .none) {
-                const tag_info = ip.loadEnumType(enum_tag_ty);
-                if (tag_info.names.len > fields_len) {
-                    const msg = msg: {
-                        const msg = try sema.errMsg(block, src, "enum field(s) missing in union", .{});
-                        errdefer msg.destroy(gpa);
-
-                        assert(explicit_tags_seen.len == tag_info.names.len);
-                        for (tag_info.names.get(ip), 0..) |field_name, field_index| {
-                            if (explicit_tags_seen[field_index]) continue;
-                            try sema.addFieldErrNote(Type.fromInterned(enum_tag_ty), field_index, msg, "field '{}' missing, declared here", .{
-                                field_name.fmt(ip),
-                            });
-                        }
-                        try sema.addDeclaredHereNote(msg, Type.fromInterned(enum_tag_ty));
-                        break :msg msg;
-                    };
-                    return sema.failWithOwnedErrorMsg(block, msg);
-                }
-            } else {
-                enum_tag_ty = try sema.generateUnionTagTypeSimple(block, enum_field_names, .none);
-            }
-
-            // Because these three things each reference each other, `undefined`
-            // placeholders are used before being set after the union type gains an
-            // InternPool index.
-
-            const new_decl_index = try sema.createAnonymousDeclTypeNamed(block, src, .{
-                .ty = Type.noreturn,
-                .val = Value.@"unreachable",
-            }, name_strategy, "union", inst);
-            const new_decl = mod.declPtr(new_decl_index);
-            new_decl.owns_tv = true;
-            errdefer {
-                new_decl.has_tv = false; // namespace and val were destroyed by later errdefers
-                mod.abortAnonDecl(new_decl_index);
-            }
-
-            const new_namespace_index = try mod.createNamespace(.{
-                .parent = block.namespace.toOptional(),
-                .decl_index = new_decl_index,
-                .file_scope = block.getFileScope(mod),
-            });
-            errdefer mod.destroyNamespace(new_namespace_index);
-
-            const union_ty = try ip.getUnionType(gpa, .{
-                .decl = new_decl_index,
-                .namespace = new_namespace_index,
-                .enum_tag_ty = enum_tag_ty,
-                .fields_len = fields_len,
-                .zir_index = .none,
-                .flags = .{
-                    .layout = layout,
-                    .status = .have_field_types,
-                    .runtime_tag = if (!tag_type_val.isNull(mod))
-                        .tagged
-                    else if (layout != .Auto)
-                        .none
-                    else switch (block.wantSafety()) {
-                        true => .safety,
-                        false => .none,
-                    },
-                    .any_aligned_fields = any_aligned_fields,
-                    .requires_comptime = .unknown,
-                    .assumed_runtime_bits = false,
-                    .assumed_pointer_aligned = false,
-                    .alignment = .none,
-                },
-                .field_types = union_fields.items(.type),
-                .field_aligns = if (any_aligned_fields) union_fields.items(.alignment) else &.{},
-                .captures = &.{},
-            });
-
-            new_decl.ty = Type.type;
-            new_decl.val = Value.fromInterned(union_ty);
-
-            const decl_val = sema.analyzeDeclVal(block, src, new_decl_index);
-            try mod.finalizeAnonDecl(new_decl_index);
-            return decl_val;
+            return sema.reifyUnion(block, inst, src, layout, tag_type_val, fields_val, name_strategy);
         },
         .Fn => {
             const struct_type = ip.loadStructType(ip.typeOf(union_val.val));
@@ -21795,127 +21458,491 @@ fn zirReify(
     }
 }
 
-fn reifyStruct(
+fn reifyEnum(
     sema: *Sema,
     block: *Block,
     inst: Zir.Inst.Index,
     src: LazySrcLoc,
-    layout: std.builtin.Type.ContainerLayout,
-    backing_int_val: Value,
+    tag_ty: Type,
+    is_exhaustive: bool,
     fields_val: Value,
     name_strategy: Zir.Inst.NameStrategy,
-    is_tuple: bool,
 ) CompileError!Air.Inst.Ref {
     const mod = sema.mod;
     const gpa = sema.gpa;
     const ip = &mod.intern_pool;
 
-    if (is_tuple) switch (layout) {
-        .Extern => return sema.fail(block, src, "extern tuples are not supported", .{}),
-        .Packed => return sema.fail(block, src, "packed tuples are not supported", .{}),
-        .Auto => {},
+    // This logic must stay in sync with the structure of `std.builtin.Type.Enum` - search for `fieldValue`.
+
+    const fields_len: u32 = @intCast(fields_val.sliceLen(mod));
+
+    // The validation work here is non-trivial, and it's possible the type already exists.
+    // So in this first pass, let's just construct a hash to optimize for this case. If the
+    // inputs turn out to be invalid, we can cancel the WIP type later.
+
+    // For deduplication purposes, we must create a hash including all details of this type.
+    // TODO: use a longer hash!
+    var hasher = std.hash.Wyhash.init(0);
+    std.hash.autoHash(&hasher, tag_ty.toIntern());
+    std.hash.autoHash(&hasher, is_exhaustive);
+    std.hash.autoHash(&hasher, fields_len);
+
+    for (0..fields_len) |field_idx| {
+        const field_info = try fields_val.elemValue(mod, field_idx);
+
+        const field_name_val = try field_info.fieldValue(mod, 0);
+        const field_value_val = try sema.resolveLazyValue(try field_info.fieldValue(mod, 1));
+
+        const field_name = try field_name_val.toIpString(Type.slice_const_u8, mod);
+
+        std.hash.autoHash(&hasher, .{
+            field_name,
+            field_value_val.toIntern(),
+        });
+    }
+
+    const wip_ty = switch (try ip.getEnumType(gpa, .{
+        .has_namespace = false,
+        .has_values = true,
+        .tag_mode = if (is_exhaustive) .explicit else .nonexhaustive,
+        .fields_len = fields_len,
+        .key = .{ .reified = .{
+            .zir_index = try ip.trackZir(gpa, block.getFileScope(mod), inst),
+            .type_hash = hasher.final(),
+        } },
+    })) {
+        .wip => |wip| wip,
+        .existing => |ty| return Air.internedToRef(ty),
     };
+    errdefer wip_ty.cancel(ip);
+
+    if (tag_ty.zigTypeTag(mod) != .Int) {
+        return sema.fail(block, src, "Type.Enum.tag_type must be an integer type", .{});
+    }
+
+    const new_decl_index = try sema.createAnonymousDeclTypeNamed(block, src, .{
+        .ty = Type.type,
+        .val = Value.fromInterned(wip_ty.index),
+    }, name_strategy, "enum", inst);
+    mod.declPtr(new_decl_index).owns_tv = true;
+    errdefer mod.abortAnonDecl(new_decl_index);
+
+    wip_ty.prepare(ip, new_decl_index, .none, tag_ty.toIntern());
+
+    for (0..fields_len) |field_idx| {
+        const field_info = try fields_val.elemValue(mod, field_idx);
 
-    const fields_len: u32 = @intCast(try sema.usizeCast(block, src, fields_val.sliceLen(mod)));
+        const field_name_val = try field_info.fieldValue(mod, 0);
+        const field_value_val = try sema.resolveLazyValue(try field_info.fieldValue(mod, 1));
 
-    // Because these three things each reference each other, `undefined`
-    // placeholders are used before being set after the struct type gains an
-    // InternPool index.
+        const field_name = try field_name_val.toIpString(Type.slice_const_u8, mod);
+
+        if (!try sema.intFitsInType(field_value_val, tag_ty, null)) {
+            // TODO: better source location
+            return sema.fail(block, src, "field '{}' with enumeration value '{}' is too large for backing int type '{}'", .{
+                field_name.fmt(ip),
+                field_value_val.fmtValue(Type.comptime_int, mod),
+                tag_ty.fmt(mod),
+            });
+        }
+
+        const coerced_field_val = try mod.getCoerced(field_value_val, tag_ty);
+        if (wip_ty.nextField(ip, field_name, coerced_field_val.toIntern())) |conflict| {
+            return sema.failWithOwnedErrorMsg(block, switch (conflict.kind) {
+                .name => msg: {
+                    const msg = try sema.errMsg(block, src, "duplicate enum field '{}'", .{field_name.fmt(ip)});
+                    errdefer msg.destroy(gpa);
+                    _ = conflict.prev_field_idx; // TODO: this note is incorrect
+                    try sema.errNote(block, src, msg, "other field here", .{});
+                    break :msg msg;
+                },
+                .value => msg: {
+                    const msg = try sema.errMsg(block, src, "enum tag value {} already taken", .{field_value_val.fmtValue(Type.comptime_int, mod)});
+                    errdefer msg.destroy(gpa);
+                    _ = conflict.prev_field_idx; // TODO: this note is incorrect
+                    try sema.errNote(block, src, msg, "other enum tag value here", .{});
+                    break :msg msg;
+                },
+            });
+        }
+    }
+
+    if (!is_exhaustive and fields_len > 1 and std.math.log2_int(u64, fields_len) == tag_ty.bitSize(mod)) {
+        return sema.fail(block, src, "non-exhaustive enum specified every value", .{});
+    }
+
+    try mod.finalizeAnonDecl(new_decl_index);
+    return Air.internedToRef(wip_ty.finish(ip));
+}
+
+fn reifyUnion(
+    sema: *Sema,
+    block: *Block,
+    inst: Zir.Inst.Index,
+    src: LazySrcLoc,
+    layout: std.builtin.Type.ContainerLayout,
+    opt_tag_type_val: Value,
+    fields_val: Value,
+    name_strategy: Zir.Inst.NameStrategy,
+) CompileError!Air.Inst.Ref {
+    const mod = sema.mod;
+    const gpa = sema.gpa;
+    const ip = &mod.intern_pool;
+
+    // This logic must stay in sync with the structure of `std.builtin.Type.Union` - search for `fieldValue`.
+
+    const fields_len: u32 = @intCast(fields_val.sliceLen(mod));
+
+    // The validation work here is non-trivial, and it's possible the type already exists.
+    // So in this first pass, let's just construct a hash to optimize for this case. If the
+    // inputs turn out to be invalid, we can cancel the WIP type later.
+
+    // For deduplication purposes, we must create a hash including all details of this type.
+    // TODO: use a longer hash!
+    var hasher = std.hash.Wyhash.init(0);
+    std.hash.autoHash(&hasher, layout);
+    std.hash.autoHash(&hasher, opt_tag_type_val.toIntern());
+    std.hash.autoHash(&hasher, fields_len);
+
+    var any_aligns = false;
+
+    for (0..fields_len) |field_idx| {
+        const field_info = try fields_val.elemValue(mod, field_idx);
+
+        const field_name_val = try field_info.fieldValue(mod, 0);
+        const field_type_val = try field_info.fieldValue(mod, 1);
+        const field_align_val = try sema.resolveLazyValue(try field_info.fieldValue(mod, 2));
+
+        const field_name = try field_name_val.toIpString(Type.slice_const_u8, mod);
+
+        std.hash.autoHash(&hasher, .{
+            field_name,
+            field_type_val.toIntern(),
+            field_align_val.toIntern(),
+        });
+
+        if (field_align_val.toUnsignedInt(mod) != 0) {
+            any_aligns = true;
+        }
+    }
+
+    const wip_ty = switch (try ip.getUnionType(gpa, .{
+        .flags = .{
+            .layout = layout,
+            .status = .none,
+            .runtime_tag = if (opt_tag_type_val.optionalValue(mod) != null)
+                .tagged
+            else if (layout != .Auto)
+                .none
+            else switch (block.wantSafety()) {
+                true => .safety,
+                false => .none,
+            },
+            .any_aligned_fields = any_aligns,
+            .requires_comptime = .unknown,
+            .assumed_runtime_bits = false,
+            .assumed_pointer_aligned = false,
+            .alignment = .none,
+        },
+        .has_namespace = false,
+        .fields_len = fields_len,
+        .enum_tag_ty = .none, // set later because not yet validated
+        .field_types = &.{}, // set later
+        .field_aligns = &.{}, // set later
+        .key = .{ .reified = .{
+            .zir_index = try ip.trackZir(gpa, block.getFileScope(mod), inst),
+            .type_hash = hasher.final(),
+        } },
+    })) {
+        .wip => |wip| wip,
+        .existing => |ty| return Air.internedToRef(ty),
+    };
+    errdefer wip_ty.cancel(ip);
 
     const new_decl_index = try sema.createAnonymousDeclTypeNamed(block, src, .{
-        .ty = Type.noreturn,
-        .val = Value.@"unreachable",
-    }, name_strategy, "struct", inst);
-    const new_decl = mod.declPtr(new_decl_index);
-    new_decl.owns_tv = true;
-    errdefer {
-        new_decl.has_tv = false; // namespace and val were destroyed by later errdefers
-        mod.abortAnonDecl(new_decl_index);
+        .ty = Type.type,
+        .val = Value.fromInterned(wip_ty.index),
+    }, name_strategy, "union", inst);
+    mod.declPtr(new_decl_index).owns_tv = true;
+    errdefer mod.abortAnonDecl(new_decl_index);
+
+    const field_types = try sema.arena.alloc(InternPool.Index, fields_len);
+    const field_aligns = if (any_aligns) try sema.arena.alloc(InternPool.Alignment, fields_len) else undefined;
+
+    const enum_tag_ty, const has_explicit_tag = if (opt_tag_type_val.optionalValue(mod)) |tag_type_val| tag_ty: {
+        switch (ip.indexToKey(tag_type_val.toIntern())) {
+            .enum_type => {},
+            else => return sema.fail(block, src, "Type.Union.tag_type must be an enum type", .{}),
+        }
+        const enum_tag_ty = tag_type_val.toType();
+
+        // We simply track which fields of the tag type have been seen.
+        const tag_ty_fields_len = enum_tag_ty.enumFieldCount(mod);
+        var seen_tags = try std.DynamicBitSetUnmanaged.initEmpty(sema.arena, tag_ty_fields_len);
+
+        for (field_types, 0..) |*field_ty, field_idx| {
+            const field_info = try fields_val.elemValue(mod, field_idx);
+
+            const field_name_val = try field_info.fieldValue(mod, 0);
+            const field_type_val = try field_info.fieldValue(mod, 1);
+
+            const field_name = try field_name_val.toIpString(Type.slice_const_u8, mod);
+
+            const enum_index = enum_tag_ty.enumFieldIndex(field_name, mod) orelse {
+                // TODO: better source location
+                return sema.fail(block, src, "no field named '{}' in enum '{}'", .{
+                    field_name.fmt(ip), enum_tag_ty.fmt(mod),
+                });
+            };
+            if (seen_tags.isSet(enum_index)) {
+                // TODO: better source location
+                return sema.fail(block, src, "duplicate union field {}", .{field_name.fmt(ip)});
+            }
+            seen_tags.set(enum_index);
+
+            field_ty.* = field_type_val.toIntern();
+            if (any_aligns) {
+                const byte_align = try (try field_info.fieldValue(mod, 2)).toUnsignedIntAdvanced(sema);
+                if (byte_align > 0 and !math.isPowerOfTwo(byte_align)) {
+                    // TODO: better source location
+                    return sema.fail(block, src, "alignment value '{d}' is not a power of two or zero", .{byte_align});
+                }
+                field_aligns[field_idx] = Alignment.fromByteUnits(byte_align);
+            }
+        }
+
+        if (tag_ty_fields_len > fields_len) return sema.failWithOwnedErrorMsg(block, msg: {
+            const msg = try sema.errMsg(block, src, "enum fields missing in union", .{});
+            errdefer msg.destroy(gpa);
+            var it = seen_tags.iterator(.{ .kind = .unset });
+            while (it.next()) |enum_index| {
+                const field_name = enum_tag_ty.enumFieldName(enum_index, mod);
+                try sema.addFieldErrNote(enum_tag_ty, enum_index, msg, "field '{}' missing, declared here", .{
+                    field_name.fmt(ip),
+                });
+            }
+            try sema.addDeclaredHereNote(msg, enum_tag_ty);
+            break :msg msg;
+        });
+
+        break :tag_ty .{ enum_tag_ty.toIntern(), true };
+    } else tag_ty: {
+        // We must track field names and set up the tag type ourselves.
+        var field_names: std.AutoArrayHashMapUnmanaged(InternPool.NullTerminatedString, void) = .{};
+        try field_names.ensureTotalCapacity(sema.arena, fields_len);
+
+        for (field_types, 0..) |*field_ty, field_idx| {
+            const field_info = try fields_val.elemValue(mod, field_idx);
+
+            const field_name_val = try field_info.fieldValue(mod, 0);
+            const field_type_val = try field_info.fieldValue(mod, 1);
+
+            const field_name = try field_name_val.toIpString(Type.slice_const_u8, mod);
+            const gop = field_names.getOrPutAssumeCapacity(field_name);
+            if (gop.found_existing) {
+                // TODO: better source location
+                return sema.fail(block, src, "duplicate union field {}", .{field_name.fmt(ip)});
+            }
+
+            field_ty.* = field_type_val.toIntern();
+            if (any_aligns) {
+                const byte_align = try (try field_info.fieldValue(mod, 2)).toUnsignedIntAdvanced(sema);
+                if (byte_align > 0 and !math.isPowerOfTwo(byte_align)) {
+                    // TODO: better source location
+                    return sema.fail(block, src, "alignment value '{d}' is not a power of two or zero", .{byte_align});
+                }
+                field_aligns[field_idx] = Alignment.fromByteUnits(byte_align);
+            }
+        }
+
+        const enum_tag_ty = try sema.generateUnionTagTypeSimple(block, field_names.keys(), mod.declPtr(new_decl_index));
+        break :tag_ty .{ enum_tag_ty, false };
+    };
+    errdefer if (!has_explicit_tag) ip.remove(enum_tag_ty); // remove generated tag type on error
+
+    for (field_types) |field_ty_ip| {
+        const field_ty = Type.fromInterned(field_ty_ip);
+        if (field_ty.zigTypeTag(mod) == .Opaque) {
+            return sema.failWithOwnedErrorMsg(block, msg: {
+                const msg = try sema.errMsg(block, src, "opaque types have unknown size and therefore cannot be directly embedded in unions", .{});
+                errdefer msg.destroy(gpa);
+
+                try sema.addDeclaredHereNote(msg, field_ty);
+                break :msg msg;
+            });
+        }
+        if (layout == .Extern and !try sema.validateExternType(field_ty, .union_field)) {
+            return sema.failWithOwnedErrorMsg(block, msg: {
+                const msg = try sema.errMsg(block, src, "extern unions cannot contain fields of type '{}'", .{field_ty.fmt(mod)});
+                errdefer msg.destroy(gpa);
+
+                const src_decl = mod.declPtr(block.src_decl);
+                try sema.explainWhyTypeIsNotExtern(msg, src.toSrcLoc(src_decl, mod), field_ty, .union_field);
+
+                try sema.addDeclaredHereNote(msg, field_ty);
+                break :msg msg;
+            });
+        } else if (layout == .Packed and !try sema.validatePackedType(field_ty)) {
+            return sema.failWithOwnedErrorMsg(block, msg: {
+                const msg = try sema.errMsg(block, src, "packed unions cannot contain fields of type '{}'", .{field_ty.fmt(mod)});
+                errdefer msg.destroy(gpa);
+
+                const src_decl = mod.declPtr(block.src_decl);
+                try sema.explainWhyTypeIsNotPacked(msg, src.toSrcLoc(src_decl, mod), field_ty);
+
+                try sema.addDeclaredHereNote(msg, field_ty);
+                break :msg msg;
+            });
+        }
     }
 
-    const ty = try ip.getStructType(gpa, .{
-        .decl = new_decl_index,
-        .namespace = .none,
-        .zir_index = .none,
+    const loaded_union = ip.loadUnionType(wip_ty.index);
+    loaded_union.setFieldTypes(ip, field_types);
+    if (any_aligns) {
+        loaded_union.setFieldAligns(ip, field_aligns);
+    }
+    loaded_union.tagTypePtr(ip).* = enum_tag_ty;
+    loaded_union.flagsPtr(ip).status = .have_field_types;
+
+    try mod.finalizeAnonDecl(new_decl_index);
+    return Air.internedToRef(wip_ty.finish(ip, new_decl_index, .none));
+}
+
+fn reifyStruct(
+    sema: *Sema,
+    block: *Block,
+    inst: Zir.Inst.Index,
+    src: LazySrcLoc,
+    layout: std.builtin.Type.ContainerLayout,
+    opt_backing_int_val: Value,
+    fields_val: Value,
+    name_strategy: Zir.Inst.NameStrategy,
+    is_tuple: bool,
+) CompileError!Air.Inst.Ref {
+    const mod = sema.mod;
+    const gpa = sema.gpa;
+    const ip = &mod.intern_pool;
+
+    // This logic must stay in sync with the structure of `std.builtin.Type.Struct` - search for `fieldValue`.
+
+    const fields_len: u32 = @intCast(fields_val.sliceLen(mod));
+
+    // The validation work here is non-trivial, and it's possible the type already exists.
+    // So in this first pass, let's just construct a hash to optimize for this case. If the
+    // inputs turn out to be invalid, we can cancel the WIP type later.
+
+    // For deduplication purposes, we must create a hash including all details of this type.
+    // TODO: use a longer hash!
+    var hasher = std.hash.Wyhash.init(0);
+    std.hash.autoHash(&hasher, layout);
+    std.hash.autoHash(&hasher, opt_backing_int_val.toIntern());
+    std.hash.autoHash(&hasher, is_tuple);
+    std.hash.autoHash(&hasher, fields_len);
+
+    var any_comptime_fields = false;
+    var any_default_inits = false;
+    var any_aligned_fields = false;
+
+    for (0..fields_len) |field_idx| {
+        const field_info = try fields_val.elemValue(mod, field_idx);
+
+        const field_name_val = try field_info.fieldValue(mod, 0);
+        const field_type_val = try field_info.fieldValue(mod, 1);
+        const field_default_value_val = try field_info.fieldValue(mod, 2);
+        const field_is_comptime_val = try field_info.fieldValue(mod, 3);
+        const field_alignment_val = try sema.resolveLazyValue(try field_info.fieldValue(mod, 4));
+
+        const field_name = try field_name_val.toIpString(Type.slice_const_u8, mod);
+        const field_is_comptime = field_is_comptime_val.toBool();
+        const field_default_value: InternPool.Index = if (field_default_value_val.optionalValue(mod)) |ptr_val| d: {
+            const ptr_ty = try mod.singleConstPtrType(field_type_val.toType());
+            // We need to do this deref here, so we won't check for this error case later on.
+            const val = try sema.pointerDeref(block, src, ptr_val, ptr_ty) orelse return sema.failWithNeededComptime(
+                block,
+                src,
+                .{ .needed_comptime_reason = "struct field default value must be comptime-known" },
+            );
+            // Resolve the value so that lazy values do not create distinct types.
+            break :d (try sema.resolveLazyValue(val)).toIntern();
+        } else .none;
+
+        std.hash.autoHash(&hasher, .{
+            field_name,
+            field_type_val.toIntern(),
+            field_default_value,
+            field_is_comptime,
+            field_alignment_val.toIntern(),
+        });
+
+        if (field_is_comptime) any_comptime_fields = true;
+        if (field_default_value != .none) any_default_inits = true;
+        switch (try field_alignment_val.orderAgainstZeroAdvanced(mod, sema)) {
+            .eq => {},
+            .gt => any_aligned_fields = true,
+            .lt => unreachable,
+        }
+    }
+
+    const wip_ty = switch (try ip.getStructType(gpa, .{
         .layout = layout,
-        .known_non_opv = false,
         .fields_len = fields_len,
+        .known_non_opv = false,
         .requires_comptime = .unknown,
         .is_tuple = is_tuple,
-        // So that we don't have to scan ahead, we allocate space in the struct
-        // type for alignments, comptime fields, and default inits. This might
-        // result in wasted space, however, this is a permitted encoding of
-        // struct types.
-        .any_comptime_fields = true,
-        .any_default_inits = true,
+        .any_comptime_fields = any_comptime_fields,
+        .any_default_inits = any_default_inits,
+        .any_aligned_fields = any_aligned_fields,
         .inits_resolved = true,
-        .any_aligned_fields = true,
-        .captures = &.{},
-    });
-    // TODO: figure out InternPool removals for incremental compilation
-    //errdefer ip.remove(ty);
-    const struct_type = ip.loadStructType(ty);
+        .has_namespace = false,
+        .key = .{ .reified = .{
+            .zir_index = try ip.trackZir(gpa, block.getFileScope(mod), inst),
+            .type_hash = hasher.final(),
+        } },
+    })) {
+        .wip => |wip| wip,
+        .existing => |ty| return Air.internedToRef(ty),
+    };
+    errdefer wip_ty.cancel(ip);
 
-    new_decl.ty = Type.type;
-    new_decl.val = Value.fromInterned(ty);
-
-    // Fields
-    for (0..fields_len) |i| {
-        const elem_val = try fields_val.elemValue(mod, i);
-        const elem_struct_type = ip.loadStructType(ip.typeOf(elem_val.toIntern()));
-        const name_val = try elem_val.fieldValue(mod, elem_struct_type.nameIndex(
-            ip,
-            try ip.getOrPutString(gpa, "name"),
-        ).?);
-        const type_val = try elem_val.fieldValue(mod, elem_struct_type.nameIndex(
-            ip,
-            try ip.getOrPutString(gpa, "type"),
-        ).?);
-        const default_value_val = try elem_val.fieldValue(mod, elem_struct_type.nameIndex(
-            ip,
-            try ip.getOrPutString(gpa, "default_value"),
-        ).?);
-        const is_comptime_val = try elem_val.fieldValue(mod, elem_struct_type.nameIndex(
-            ip,
-            try ip.getOrPutString(gpa, "is_comptime"),
-        ).?);
-        const alignment_val = try elem_val.fieldValue(mod, elem_struct_type.nameIndex(
-            ip,
-            try ip.getOrPutString(gpa, "alignment"),
-        ).?);
-
-        if (!try sema.intFitsInType(alignment_val, Type.u32, null)) {
-            return sema.fail(block, src, "alignment must fit in 'u32'", .{});
-        }
-        const abi_align = (try alignment_val.getUnsignedIntAdvanced(mod, sema)).?;
-
-        if (layout == .Packed) {
-            if (abi_align != 0) return sema.fail(block, src, "alignment in a packed struct field must be set to 0", .{});
-            if (is_comptime_val.toBool()) return sema.fail(block, src, "packed struct fields cannot be marked comptime", .{});
-        } else {
-            if (abi_align > 0 and !math.isPowerOfTwo(abi_align)) return sema.fail(block, src, "alignment value '{d}' is not a power of two or zero", .{abi_align});
-            struct_type.field_aligns.get(ip)[i] = Alignment.fromByteUnits(abi_align);
-        }
-        if (layout == .Extern and is_comptime_val.toBool()) {
-            return sema.fail(block, src, "extern struct fields cannot be marked comptime", .{});
-        }
+    if (is_tuple) switch (layout) {
+        .Extern => return sema.fail(block, src, "extern tuples are not supported", .{}),
+        .Packed => return sema.fail(block, src, "packed tuples are not supported", .{}),
+        .Auto => {},
+    };
+
+    const new_decl_index = try sema.createAnonymousDeclTypeNamed(block, src, .{
+        .ty = Type.type,
+        .val = Value.fromInterned(wip_ty.index),
+    }, name_strategy, "struct", inst);
+    mod.declPtr(new_decl_index).owns_tv = true;
+    errdefer mod.abortAnonDecl(new_decl_index);
+
+    const struct_type = ip.loadStructType(wip_ty.index);
+
+    for (0..fields_len) |field_idx| {
+        const field_info = try fields_val.elemValue(mod, field_idx);
 
-        const field_name = try name_val.toIpString(Type.slice_const_u8, mod);
+        const field_name_val = try field_info.fieldValue(mod, 0);
+        const field_type_val = try field_info.fieldValue(mod, 1);
+        const field_default_value_val = try field_info.fieldValue(mod, 2);
+        const field_is_comptime_val = try field_info.fieldValue(mod, 3);
+        const field_alignment_val = try field_info.fieldValue(mod, 4);
 
+        const field_ty = field_type_val.toType();
+        const field_name = try field_name_val.toIpString(Type.slice_const_u8, mod);
         if (is_tuple) {
-            const field_index = field_name.toUnsigned(ip) orelse return sema.fail(
+            const field_name_index = field_name.toUnsigned(ip) orelse return sema.fail(
                 block,
                 src,
                 "tuple cannot have non-numeric field '{}'",
                 .{field_name.fmt(ip)},
             );
-
-            if (field_index >= fields_len) {
+            if (field_name_index != field_idx) {
                 return sema.fail(
                     block,
                     src,
-                    "tuple field {} exceeds tuple field count",
-                    .{field_index},
+                    "tuple field name '{}' does not match field index {}",
+                    .{ field_name_index, field_idx },
                 );
             }
         } else if (struct_type.addFieldName(ip, field_name)) |prev_index| {
@@ -21923,45 +21950,72 @@ fn reifyStruct(
             return sema.fail(block, src, "duplicate struct field name {}", .{field_name.fmt(ip)});
         }
 
-        const field_ty = type_val.toType();
-        const default_val = if (default_value_val.optionalValue(mod)) |opt_val|
-            (try sema.pointerDeref(block, src, opt_val, try mod.singleConstPtrType(field_ty)) orelse
-                return sema.failWithNeededComptime(block, src, .{
-                .needed_comptime_reason = "struct field default value must be comptime-known",
-            })).toIntern()
-        else
-            .none;
-        if (is_comptime_val.toBool() and default_val == .none) {
+        if (any_aligned_fields) {
+            if (!try sema.intFitsInType(field_alignment_val, Type.u32, null)) {
+                return sema.fail(block, src, "alignment must fit in 'u32'", .{});
+            }
+
+            const byte_align = try field_alignment_val.toUnsignedIntAdvanced(sema);
+            if (byte_align == 0) {
+                if (layout != .Packed) {
+                    struct_type.field_aligns.get(ip)[field_idx] = .none;
+                }
+            } else {
+                if (layout == .Packed) return sema.fail(block, src, "alignment in a packed struct field must be set to 0", .{});
+                if (!math.isPowerOfTwo(byte_align)) return sema.fail(block, src, "alignment value '{d}' is not a power of two or zero", .{byte_align});
+                struct_type.field_aligns.get(ip)[field_idx] = Alignment.fromNonzeroByteUnits(byte_align);
+            }
+        }
+
+        const field_is_comptime = field_is_comptime_val.toBool();
+        if (field_is_comptime) {
+            assert(any_comptime_fields);
+            switch (layout) {
+                .Extern => return sema.fail(block, src, "extern struct fields cannot be marked comptime", .{}),
+                .Packed => return sema.fail(block, src, "packed struct fields cannot be marked comptime", .{}),
+                .Auto => struct_type.setFieldComptime(ip, field_idx),
+            }
+        }
+
+        const field_default: InternPool.Index = d: {
+            if (!any_default_inits) break :d .none;
+            const ptr_val = field_default_value_val.optionalValue(mod) orelse break :d .none;
+            const ptr_ty = try mod.singleConstPtrType(field_ty);
+            // Asserted comptime-dereferencable above.
+            const val = (try sema.pointerDeref(block, src, ptr_val, ptr_ty)).?;
+            // We already resolved this for deduplication, so we may as well do it now.
+            break :d (try sema.resolveLazyValue(val)).toIntern();
+        };
+
+        if (field_is_comptime and field_default == .none) {
             return sema.fail(block, src, "comptime field without default initialization value", .{});
         }
 
-        struct_type.field_types.get(ip)[i] = field_ty.toIntern();
-        struct_type.field_inits.get(ip)[i] = default_val;
-        if (is_comptime_val.toBool())
-            struct_type.setFieldComptime(ip, i);
+        struct_type.field_types.get(ip)[field_idx] = field_type_val.toIntern();
+        if (field_default != .none) {
+            struct_type.field_inits.get(ip)[field_idx] = field_default;
+        }
 
         if (field_ty.zigTypeTag(mod) == .Opaque) {
-            const msg = msg: {
+            return sema.failWithOwnedErrorMsg(block, msg: {
                 const msg = try sema.errMsg(block, src, "opaque types have unknown size and therefore cannot be directly embedded in structs", .{});
                 errdefer msg.destroy(gpa);
 
                 try sema.addDeclaredHereNote(msg, field_ty);
                 break :msg msg;
-            };
-            return sema.failWithOwnedErrorMsg(block, msg);
+            });
         }
         if (field_ty.zigTypeTag(mod) == .NoReturn) {
-            const msg = msg: {
+            return sema.failWithOwnedErrorMsg(block, msg: {
                 const msg = try sema.errMsg(block, src, "struct fields cannot be 'noreturn'", .{});
                 errdefer msg.destroy(gpa);
 
                 try sema.addDeclaredHereNote(msg, field_ty);
                 break :msg msg;
-            };
-            return sema.failWithOwnedErrorMsg(block, msg);
+            });
         }
         if (layout == .Extern and !try sema.validateExternType(field_ty, .struct_field)) {
-            const msg = msg: {
+            return sema.failWithOwnedErrorMsg(block, msg: {
                 const msg = try sema.errMsg(block, src, "extern structs cannot contain fields of type '{}'", .{field_ty.fmt(sema.mod)});
                 errdefer msg.destroy(gpa);
 
@@ -21970,10 +22024,9 @@ fn reifyStruct(
 
                 try sema.addDeclaredHereNote(msg, field_ty);
                 break :msg msg;
-            };
-            return sema.failWithOwnedErrorMsg(block, msg);
+            });
         } else if (layout == .Packed and !try sema.validatePackedType(field_ty)) {
-            const msg = msg: {
+            return sema.failWithOwnedErrorMsg(block, msg: {
                 const msg = try sema.errMsg(block, src, "packed structs cannot contain fields of type '{}'", .{field_ty.fmt(sema.mod)});
                 errdefer msg.destroy(gpa);
 
@@ -21982,32 +22035,27 @@ fn reifyStruct(
 
                 try sema.addDeclaredHereNote(msg, field_ty);
                 break :msg msg;
-            };
-            return sema.failWithOwnedErrorMsg(block, msg);
+            });
         }
     }
 
     if (layout == .Packed) {
-        for (0..struct_type.field_types.len) |index| {
-            const field_ty = Type.fromInterned(struct_type.field_types.get(ip)[index]);
+        var fields_bit_sum: u64 = 0;
+        for (0..struct_type.field_types.len) |field_idx| {
+            const field_ty = Type.fromInterned(struct_type.field_types.get(ip)[field_idx]);
             sema.resolveTypeLayout(field_ty) catch |err| switch (err) {
                 error.AnalysisFail => {
                     const msg = sema.err orelse return err;
-                    try sema.addFieldErrNote(Type.fromInterned(ty), index, msg, "while checking this field", .{});
+                    try sema.errNote(block, src, msg, "while checking a field of this struct", .{});
                     return err;
                 },
                 else => return err,
             };
-        }
-
-        var fields_bit_sum: u64 = 0;
-        for (0..struct_type.field_types.len) |i| {
-            const field_ty = Type.fromInterned(struct_type.field_types.get(ip)[i]);
             fields_bit_sum += field_ty.bitSize(mod);
         }
 
-        if (backing_int_val.optionalValue(mod)) |backing_int_ty_val| {
-            const backing_int_ty = backing_int_ty_val.toType();
+        if (opt_backing_int_val.optionalValue(mod)) |backing_int_val| {
+            const backing_int_ty = backing_int_val.toType();
             try sema.checkBackingIntType(block, src, backing_int_ty, fields_bit_sum);
             struct_type.backingIntType(ip).* = backing_int_ty.toIntern();
         } else {
@@ -22016,9 +22064,8 @@ fn reifyStruct(
         }
     }
 
-    const decl_val = sema.analyzeDeclVal(block, src, new_decl_index);
     try mod.finalizeAnonDecl(new_decl_index);
-    return decl_val;
+    return Air.internedToRef(wip_ty.finish(ip, new_decl_index, .none));
 }
 
 fn resolveVaListRef(sema: *Sema, block: *Block, src: LazySrcLoc, zir_ref: Zir.Inst.Ref) CompileError!Air.Inst.Ref {
@@ -36963,8 +37010,8 @@ fn semaUnionFields(mod: *Module, arena: Allocator, union_type: InternPool.Loaded
     const gpa = mod.gpa;
     const ip = &mod.intern_pool;
     const decl_index = union_type.decl;
-    const zir = mod.namespacePtr(union_type.namespace).file_scope.zir;
-    const zir_index = union_type.zir_index.unwrap().?.resolve(ip);
+    const zir = mod.namespacePtr(union_type.namespace.unwrap().?).file_scope.zir;
+    const zir_index = union_type.zir_index.resolve(ip);
     const extended = zir.instructions.items(.data)[@intFromEnum(zir_index)].extended;
     assert(extended.opcode == .union_decl);
     const small: Zir.Inst.UnionDecl.Small = @bitCast(extended.small);
@@ -37037,7 +37084,7 @@ fn semaUnionFields(mod: *Module, arena: Allocator, union_type: InternPool.Loaded
         .parent = null,
         .sema = &sema,
         .src_decl = decl_index,
-        .namespace = union_type.namespace,
+        .namespace = union_type.namespace.unwrap().?,
         .instructions = .{},
         .inlining = null,
         .is_comptime = true,
@@ -37357,7 +37404,7 @@ fn semaUnionFields(mod: *Module, arena: Allocator, union_type: InternPool.Loaded
         const enum_ty = try sema.generateUnionTagTypeNumbered(&block_scope, enum_field_names, enum_field_vals.keys(), mod.declPtr(union_type.decl));
         union_type.tagTypePtr(ip).* = enum_ty;
     } else {
-        const enum_ty = try sema.generateUnionTagTypeSimple(&block_scope, enum_field_names, union_type.decl.toOptional());
+        const enum_ty = try sema.generateUnionTagTypeSimple(&block_scope, enum_field_names, mod.declPtr(union_type.decl));
         union_type.tagTypePtr(ip).* = enum_ty;
     }
 }
@@ -37374,7 +37421,7 @@ fn generateUnionTagTypeNumbered(
     block: *Block,
     enum_field_names: []const InternPool.NullTerminatedString,
     enum_field_vals: []const InternPool.Index,
-    decl: *Module.Decl,
+    union_owner_decl: *Module.Decl,
 ) !InternPool.Index {
     const mod = sema.mod;
     const gpa = sema.gpa;
@@ -37383,7 +37430,7 @@ fn generateUnionTagTypeNumbered(
     const src_decl = mod.declPtr(block.src_decl);
     const new_decl_index = try mod.allocateNewDecl(block.namespace, src_decl.src_node);
     errdefer mod.destroyDecl(new_decl_index);
-    const fqn = try decl.fullyQualifiedName(mod);
+    const fqn = try union_owner_decl.fullyQualifiedName(mod);
     const name = try ip.getOrPutStringFmt(gpa, "@typeInfo({}).Union.tag_type.?", .{fqn.fmt(ip)});
     try mod.initNewAnonDecl(new_decl_index, src_decl.src_line, .{
         .ty = Type.noreturn,
@@ -37395,9 +37442,9 @@ fn generateUnionTagTypeNumbered(
     new_decl.owns_tv = true;
     new_decl.name_fully_qualified = true;
 
-    const enum_ty = try ip.getEnum(gpa, .{
+    const enum_ty = try ip.getGeneratedTagEnumType(gpa, .{
         .decl = new_decl_index,
-        .namespace = .none,
+        .owner_union_ty = union_owner_decl.val.toIntern(),
         .tag_ty = if (enum_field_vals.len == 0)
             (try mod.intType(.unsigned, 0)).toIntern()
         else
@@ -37405,8 +37452,6 @@ fn generateUnionTagTypeNumbered(
         .names = enum_field_names,
         .values = enum_field_vals,
         .tag_mode = .explicit,
-        .zir_index = .none,
-        .captures = &.{},
     });
 
     new_decl.ty = Type.type;
@@ -37420,20 +37465,14 @@ fn generateUnionTagTypeSimple(
     sema: *Sema,
     block: *Block,
     enum_field_names: []const InternPool.NullTerminatedString,
-    maybe_decl_index: InternPool.OptionalDeclIndex,
+    union_owner_decl: *Module.Decl,
 ) !InternPool.Index {
     const mod = sema.mod;
     const ip = &mod.intern_pool;
     const gpa = sema.gpa;
 
     const new_decl_index = new_decl_index: {
-        const decl_index = maybe_decl_index.unwrap() orelse {
-            break :new_decl_index try mod.createAnonymousDecl(block, .{
-                .ty = Type.noreturn,
-                .val = Value.@"unreachable",
-            });
-        };
-        const fqn = try mod.declPtr(decl_index).fullyQualifiedName(mod);
+        const fqn = try union_owner_decl.fullyQualifiedName(mod);
         const src_decl = mod.declPtr(block.src_decl);
         const new_decl_index = try mod.allocateNewDecl(block.namespace, src_decl.src_node);
         errdefer mod.destroyDecl(new_decl_index);
@@ -37447,9 +37486,9 @@ fn generateUnionTagTypeSimple(
     };
     errdefer mod.abortAnonDecl(new_decl_index);
 
-    const enum_ty = try ip.getEnum(gpa, .{
+    const enum_ty = try ip.getGeneratedTagEnumType(gpa, .{
         .decl = new_decl_index,
-        .namespace = .none,
+        .owner_union_ty = union_owner_decl.val.toIntern(),
         .tag_ty = if (enum_field_names.len == 0)
             (try mod.intType(.unsigned, 0)).toIntern()
         else
@@ -37457,8 +37496,6 @@ fn generateUnionTagTypeSimple(
         .names = enum_field_names,
         .values = &.{},
         .tag_mode = .auto,
-        .zir_index = .none,
-        .captures = &.{},
     });
 
     const new_decl = mod.declPtr(new_decl_index);
@@ -37643,6 +37680,8 @@ pub fn typeHasOnePossibleValue(sema: *Sema, ty: Type) CompileError!?Value {
         => unreachable,
 
         _ => switch (ip.items.items(.tag)[@intFromEnum(ty.toIntern())]) {
+            .removed => unreachable,
+
             .type_int_signed, // i0 handled above
             .type_int_unsigned, // u0 handled above
             .type_pointer,
src/type.zig
@@ -355,16 +355,16 @@ pub const Type = struct {
                 try writer.writeAll("}");
             },
 
-            .union_type => |union_type| {
-                const decl = mod.declPtr(union_type.decl);
+            .union_type => {
+                const decl = mod.declPtr(ip.loadUnionType(ty.toIntern()).decl);
                 try decl.renderFullyQualifiedName(mod, writer);
             },
-            .opaque_type => |opaque_type| {
-                const decl = mod.declPtr(opaque_type.decl);
+            .opaque_type => {
+                const decl = mod.declPtr(ip.loadOpaqueType(ty.toIntern()).decl);
                 try decl.renderFullyQualifiedName(mod, writer);
             },
-            .enum_type => |enum_type| {
-                const decl = mod.declPtr(enum_type.decl);
+            .enum_type => {
+                const decl = mod.declPtr(ip.loadEnumType(ty.toIntern()).decl);
                 try decl.renderFullyQualifiedName(mod, writer);
             },
             .func_type => |fn_info| {
@@ -2845,9 +2845,9 @@ pub const Type = struct {
     pub fn getNamespaceIndex(ty: Type, mod: *Module) InternPool.OptionalNamespaceIndex {
         const ip = &mod.intern_pool;
         return switch (ip.indexToKey(ty.toIntern())) {
-            .opaque_type => ip.loadOpaqueType(ty.toIntern()).namespace.toOptional(),
+            .opaque_type => ip.loadOpaqueType(ty.toIntern()).namespace,
             .struct_type => ip.loadStructType(ty.toIntern()).namespace,
-            .union_type => ip.loadUnionType(ty.toIntern()).namespace.toOptional(),
+            .union_type => ip.loadUnionType(ty.toIntern()).namespace,
             .enum_type => ip.loadEnumType(ty.toIntern()).namespace,
 
             else => .none,
@@ -3180,17 +3180,8 @@ pub const Type = struct {
     }
 
     pub fn declSrcLocOrNull(ty: Type, mod: *Module) ?Module.SrcLoc {
-        return switch (mod.intern_pool.indexToKey(ty.toIntern())) {
-            .struct_type => |struct_type| {
-                return mod.declPtr(struct_type.decl.unwrap() orelse return null).srcLoc(mod);
-            },
-            .union_type => |union_type| {
-                return mod.declPtr(union_type.decl).srcLoc(mod);
-            },
-            .opaque_type => |opaque_type| mod.opaqueSrcLoc(opaque_type),
-            .enum_type => |enum_type| mod.declPtr(enum_type.decl).srcLoc(mod),
-            else => null,
-        };
+        const decl = ty.getOwnerDeclOrNull(mod) orelse return null;
+        return mod.declPtr(decl).srcLoc(mod);
     }
 
     pub fn getOwnerDecl(ty: Type, mod: *Module) InternPool.DeclIndex {
@@ -3198,11 +3189,12 @@ pub const Type = struct {
     }
 
     pub fn getOwnerDeclOrNull(ty: Type, mod: *Module) ?InternPool.DeclIndex {
-        return switch (mod.intern_pool.indexToKey(ty.toIntern())) {
-            .struct_type => |struct_type| struct_type.decl.unwrap(),
-            .union_type => |union_type| union_type.decl,
-            .opaque_type => |opaque_type| opaque_type.decl,
-            .enum_type => |enum_type| enum_type.decl,
+        const ip = &mod.intern_pool;
+        return switch (ip.indexToKey(ty.toIntern())) {
+            .struct_type => ip.loadStructType(ty.toIntern()).decl.unwrap(),
+            .union_type => ip.loadUnionType(ty.toIntern()).decl,
+            .opaque_type => ip.loadOpaqueType(ty.toIntern()).decl,
+            .enum_type => ip.loadEnumType(ty.toIntern()).decl,
             else => null,
         };
     }
@@ -3287,9 +3279,9 @@ pub const Type = struct {
         const ip = &zcu.intern_pool;
         return switch (ip.indexToKey(ty.toIntern())) {
             .struct_type => ip.loadStructType(ty.toIntern()).zir_index.unwrap(),
-            .union_type => ip.loadUnionType(ty.toIntern()).zir_index.unwrap(),
+            .union_type => ip.loadUnionType(ty.toIntern()).zir_index,
             .enum_type => ip.loadEnumType(ty.toIntern()).zir_index.unwrap(),
-            .opaque_type => ip.loadOpaqueType(ty.toIntern()).zir_index.unwrap(),
+            .opaque_type => ip.loadOpaqueType(ty.toIntern()).zir_index,
             else => null,
         };
     }