Commit 5881a2d637

Andrew Kelley <andrew@ziglang.org>
2023-05-12 09:07:32
stage2: move enum types into the InternPool
Unlike unions and structs, enums are actually *encoded* into the InternPool directly, rather than using the SegmentedList trick. This results in them being quite compact, and greatly improved the ergonomics of using enum types throughout the compiler. It did however require introducing a new concept to the InternPool which is an "incomplete" item - something that is added to gain a permanent Index, but which is then mutated in place. This was necessary because enum tag values and tag types may reference the namespaces created by the enum itself, which required constructing the namespace, decl, and calling analyzeDecl on the decl, which required the decl value, which required the enum type, which required an InternPool index to be assigned and for it to be meaningful. The API for updating enums in place turned out to be quite slick and efficient - the methods directly populate pre-allocated arrays and return the information necessary to output the same compilation errors as before.
1 parent 404cbc3
src/arch/wasm/CodeGen.zig
@@ -3101,24 +3101,12 @@ fn lowerConstant(func: *CodeGen, arg_val: Value, ty: Type) InnerError!WValue {
         },
         .Enum => {
             if (val.castTag(.enum_field_index)) |field_index| {
-                switch (ty.tag()) {
-                    .enum_simple => return WValue{ .imm32 = field_index.data },
-                    .enum_full, .enum_nonexhaustive => {
-                        const enum_full = ty.cast(Type.Payload.EnumFull).?.data;
-                        if (enum_full.values.count() != 0) {
-                            const tag_val = enum_full.values.keys()[field_index.data];
-                            return func.lowerConstant(tag_val, enum_full.tag_ty);
-                        } else {
-                            return WValue{ .imm32 = field_index.data };
-                        }
-                    },
-                    .enum_numbered => {
-                        const index = field_index.data;
-                        const enum_data = ty.castTag(.enum_numbered).?.data;
-                        const enum_val = enum_data.values.keys()[index];
-                        return func.lowerConstant(enum_val, enum_data.tag_ty);
-                    },
-                    else => return func.fail("TODO: lowerConstant for enum tag: {}", .{ty.tag()}),
+                const enum_type = mod.intern_pool.indexToKey(ty.ip_index).enum_type;
+                if (enum_type.values.len != 0) {
+                    const tag_val = enum_type.values[field_index.data];
+                    return func.lowerConstant(tag_val.toValue(), enum_type.tag_ty.toType());
+                } else {
+                    return WValue{ .imm32 = field_index.data };
                 }
             } else {
                 const int_tag_ty = try ty.intTagType(mod);
@@ -3240,21 +3228,12 @@ fn valueAsI32(func: *const CodeGen, val: Value, ty: Type) !i32 {
     switch (ty.zigTypeTag(mod)) {
         .Enum => {
             if (val.castTag(.enum_field_index)) |field_index| {
-                switch (ty.tag()) {
-                    .enum_simple => return @bitCast(i32, field_index.data),
-                    .enum_full, .enum_nonexhaustive => {
-                        const enum_full = ty.cast(Type.Payload.EnumFull).?.data;
-                        if (enum_full.values.count() != 0) {
-                            const tag_val = enum_full.values.keys()[field_index.data];
-                            return func.valueAsI32(tag_val, enum_full.tag_ty);
-                        } else return @bitCast(i32, field_index.data);
-                    },
-                    .enum_numbered => {
-                        const index = field_index.data;
-                        const enum_data = ty.castTag(.enum_numbered).?.data;
-                        return func.valueAsI32(enum_data.values.keys()[index], enum_data.tag_ty);
-                    },
-                    else => unreachable,
+                const enum_type = mod.intern_pool.indexToKey(ty.ip_index).enum_type;
+                if (enum_type.values.len != 0) {
+                    const tag_val = enum_type.values[field_index.data];
+                    return func.valueAsI32(tag_val.toValue(), enum_type.tag_ty.toType());
+                } else {
+                    return @bitCast(i32, field_index.data);
                 }
             } else {
                 const int_tag_ty = try ty.intTagType(mod);
@@ -6836,7 +6815,8 @@ fn getTagNameFunction(func: *CodeGen, enum_ty: Type) InnerError!u32 {
 
     // TODO: Make switch implementation generic so we can use a jump table for this when the tags are not sparse.
     // generate an if-else chain for each tag value as well as constant.
-    for (enum_ty.enumFields().keys(), 0..) |tag_name, field_index| {
+    for (enum_ty.enumFields(mod), 0..) |tag_name_ip, field_index| {
+        const tag_name = mod.intern_pool.stringToSlice(tag_name_ip);
         // for each tag name, create an unnamed const,
         // and then get a pointer to its value.
         const name_ty = try mod.arrayType(.{
@@ -6846,7 +6826,7 @@ fn getTagNameFunction(func: *CodeGen, enum_ty: Type) InnerError!u32 {
         });
         const string_bytes = &mod.string_literal_bytes;
         try string_bytes.ensureUnusedCapacity(mod.gpa, tag_name.len);
-        const gop = try mod.string_literal_table.getOrPutContextAdapted(mod.gpa, tag_name, Module.StringLiteralAdapter{
+        const gop = try mod.string_literal_table.getOrPutContextAdapted(mod.gpa, @as([]const u8, tag_name), Module.StringLiteralAdapter{
             .bytes = string_bytes,
         }, Module.StringLiteralContext{
             .bytes = string_bytes,
src/arch/x86_64/CodeGen.zig
@@ -2016,7 +2016,7 @@ fn genLazy(self: *Self, lazy_sym: link.File.LazySymbol) InnerError!void {
             const ret_reg = param_regs[0];
             const enum_mcv = MCValue{ .register = param_regs[1] };
 
-            var exitlude_jump_relocs = try self.gpa.alloc(u32, enum_ty.enumFieldCount());
+            var exitlude_jump_relocs = try self.gpa.alloc(u32, enum_ty.enumFieldCount(mod));
             defer self.gpa.free(exitlude_jump_relocs);
 
             const data_reg = try self.register_manager.allocReg(null, gp);
@@ -2027,9 +2027,10 @@ fn genLazy(self: *Self, lazy_sym: link.File.LazySymbol) InnerError!void {
             var data_off: i32 = 0;
             for (
                 exitlude_jump_relocs,
-                enum_ty.enumFields().keys(),
+                enum_ty.enumFields(mod),
                 0..,
-            ) |*exitlude_jump_reloc, tag_name, index| {
+            ) |*exitlude_jump_reloc, tag_name_ip, index| {
+                const tag_name = mod.intern_pool.stringToSlice(tag_name_ip);
                 var tag_pl = Value.Payload.U32{
                     .base = .{ .tag = .enum_field_index },
                     .data = @intCast(u32, index),
@@ -11413,7 +11414,7 @@ fn airUnionInit(self: *Self, inst: Air.Inst.Index) !void {
         const union_obj = mod.typeToUnion(union_ty).?;
         const field_name = union_obj.fields.keys()[extra.field_index];
         const tag_ty = union_obj.tag_ty;
-        const field_index = @intCast(u32, tag_ty.enumFieldIndex(field_name).?);
+        const field_index = @intCast(u32, tag_ty.enumFieldIndex(field_name, mod).?);
         var tag_pl = Value.Payload.U32{ .base = .{ .tag = .enum_field_index }, .data = field_index };
         const tag_val = Value.initPayload(&tag_pl.base);
         const tag_int_val = try tag_val.enumToInt(tag_ty, mod);
src/codegen/c.zig
@@ -1288,27 +1288,12 @@ pub const DeclGen = struct {
                 switch (val.tag()) {
                     .enum_field_index => {
                         const field_index = val.castTag(.enum_field_index).?.data;
-                        switch (ty.tag()) {
-                            .enum_simple => return writer.print("{d}", .{field_index}),
-                            .enum_full, .enum_nonexhaustive => {
-                                const enum_full = ty.cast(Type.Payload.EnumFull).?.data;
-                                if (enum_full.values.count() != 0) {
-                                    const tag_val = enum_full.values.keys()[field_index];
-                                    return dg.renderValue(writer, enum_full.tag_ty, tag_val, location);
-                                } else {
-                                    return writer.print("{d}", .{field_index});
-                                }
-                            },
-                            .enum_numbered => {
-                                const enum_obj = ty.castTag(.enum_numbered).?.data;
-                                if (enum_obj.values.count() != 0) {
-                                    const tag_val = enum_obj.values.keys()[field_index];
-                                    return dg.renderValue(writer, enum_obj.tag_ty, tag_val, location);
-                                } else {
-                                    return writer.print("{d}", .{field_index});
-                                }
-                            },
-                            else => unreachable,
+                        const enum_type = mod.intern_pool.indexToKey(ty.ip_index).enum_type;
+                        if (enum_type.values.len != 0) {
+                            const tag_val = enum_type.values[field_index];
+                            return dg.renderValue(writer, enum_type.tag_ty.toType(), tag_val.toValue(), location);
+                        } else {
+                            return writer.print("{d}", .{field_index});
                         }
                     },
                     else => {
@@ -2539,7 +2524,8 @@ pub fn genLazyFn(o: *Object, lazy_fn: LazyFnMap.Entry) !void {
             try w.writeByte('(');
             try o.dg.renderTypeAndName(w, enum_ty, .{ .identifier = "tag" }, Const, 0, .complete);
             try w.writeAll(") {\n switch (tag) {\n");
-            for (enum_ty.enumFields().keys(), 0..) |name, index| {
+            for (enum_ty.enumFields(mod), 0..) |name_ip, index| {
+                const name = mod.intern_pool.stringToSlice(name_ip);
                 var tag_pl: Value.Payload.U32 = .{
                     .base = .{ .tag = .enum_field_index },
                     .data = @intCast(u32, index),
@@ -6930,7 +6916,7 @@ fn airUnionInit(f: *Function, inst: Air.Inst.Index) !CValue {
     const field: CValue = if (union_ty.unionTagTypeSafety(mod)) |tag_ty| field: {
         const layout = union_ty.unionGetLayout(mod);
         if (layout.tag_size != 0) {
-            const field_index = tag_ty.enumFieldIndex(field_name).?;
+            const field_index = tag_ty.enumFieldIndex(field_name, mod).?;
 
             var tag_pl: Value.Payload.U32 = .{
                 .base = .{ .tag = .enum_field_index },
src/codegen/llvm.zig
@@ -1516,30 +1516,25 @@ pub const Object = struct {
                     return enum_di_ty;
                 }
 
-                const field_names = ty.enumFields().keys();
+                const ip = &mod.intern_pool;
+                const enum_type = ip.indexToKey(ty.ip_index).enum_type;
 
-                const enumerators = try gpa.alloc(*llvm.DIEnumerator, field_names.len);
+                const enumerators = try gpa.alloc(*llvm.DIEnumerator, enum_type.names.len);
                 defer gpa.free(enumerators);
 
-                var buf_field_index: Value.Payload.U32 = .{
-                    .base = .{ .tag = .enum_field_index },
-                    .data = undefined,
-                };
-                const field_index_val = Value.initPayload(&buf_field_index.base);
-
-                const int_ty = try ty.intTagType(mod);
+                const int_ty = enum_type.tag_ty.toType();
                 const int_info = ty.intInfo(mod);
                 assert(int_info.bits != 0);
 
-                for (field_names, 0..) |field_name, i| {
-                    const field_name_z = try gpa.dupeZ(u8, field_name);
-                    defer gpa.free(field_name_z);
+                for (enum_type.names, 0..) |field_name_ip, i| {
+                    const field_name_z = ip.stringToSlice(field_name_ip);
 
-                    buf_field_index.data = @intCast(u32, i);
-                    const field_int_val = try field_index_val.enumToInt(ty, mod);
-
-                    var bigint_space: Value.BigIntSpace = undefined;
-                    const bigint = field_int_val.toBigInt(&bigint_space, mod);
+                    var bigint_space: InternPool.Key.Int.Storage.BigIntSpace = undefined;
+                    const storage = if (enum_type.values.len != 0)
+                        ip.indexToKey(enum_type.values[i]).int.storage
+                    else
+                        InternPool.Key.Int.Storage{ .u64 = i };
+                    const bigint = storage.toBigInt(&bigint_space);
 
                     if (bigint.limbs.len == 1) {
                         enumerators[i] = dib.createEnumerator(field_name_z, bigint.limbs[0], int_info.signedness == .unsigned);
@@ -8852,23 +8847,22 @@ pub const FuncGen = struct {
 
     fn getIsNamedEnumValueFunction(self: *FuncGen, enum_ty: Type) !*llvm.Value {
         const mod = self.dg.module;
-        const enum_decl = enum_ty.getOwnerDecl(mod);
+        const enum_type = mod.intern_pool.indexToKey(enum_ty.ip_index).enum_type;
 
         // TODO: detect when the type changes and re-emit this function.
-        const gop = try self.dg.object.named_enum_map.getOrPut(self.dg.gpa, enum_decl);
+        const gop = try self.dg.object.named_enum_map.getOrPut(self.dg.gpa, enum_type.decl);
         if (gop.found_existing) return gop.value_ptr.*;
-        errdefer assert(self.dg.object.named_enum_map.remove(enum_decl));
+        errdefer assert(self.dg.object.named_enum_map.remove(enum_type.decl));
 
         var arena_allocator = std.heap.ArenaAllocator.init(self.gpa);
         defer arena_allocator.deinit();
         const arena = arena_allocator.allocator();
 
-        const fqn = try mod.declPtr(enum_decl).getFullyQualifiedName(mod);
+        const fqn = try mod.declPtr(enum_type.decl).getFullyQualifiedName(mod);
         defer self.gpa.free(fqn);
         const llvm_fn_name = try std.fmt.allocPrintZ(arena, "__zig_is_named_enum_value_{s}", .{fqn});
 
-        const int_tag_ty = try enum_ty.intTagType(mod);
-        const param_types = [_]*llvm.Type{try self.dg.lowerType(int_tag_ty)};
+        const param_types = [_]*llvm.Type{try self.dg.lowerType(enum_type.tag_ty.toType())};
 
         const llvm_ret_ty = try self.dg.lowerType(Type.bool);
         const fn_type = llvm.functionType(llvm_ret_ty, &param_types, param_types.len, .False);
@@ -8891,13 +8885,12 @@ pub const FuncGen = struct {
         self.builder.positionBuilderAtEnd(entry_block);
         self.builder.clearCurrentDebugLocation();
 
-        const fields = enum_ty.enumFields();
         const named_block = self.context.appendBasicBlock(fn_val, "Named");
         const unnamed_block = self.context.appendBasicBlock(fn_val, "Unnamed");
         const tag_int_value = fn_val.getParam(0);
-        const switch_instr = self.builder.buildSwitch(tag_int_value, unnamed_block, @intCast(c_uint, fields.count()));
+        const switch_instr = self.builder.buildSwitch(tag_int_value, unnamed_block, @intCast(c_uint, enum_type.names.len));
 
-        for (fields.keys(), 0..) |_, field_index| {
+        for (enum_type.names, 0..) |_, field_index| {
             const this_tag_int_value = int: {
                 var tag_val_payload: Value.Payload.U32 = .{
                     .base = .{ .tag = .enum_field_index },
@@ -8930,18 +8923,18 @@ pub const FuncGen = struct {
 
     fn getEnumTagNameFunction(self: *FuncGen, enum_ty: Type) !*llvm.Value {
         const mod = self.dg.module;
-        const enum_decl = enum_ty.getOwnerDecl(mod);
+        const enum_type = mod.intern_pool.indexToKey(enum_ty.ip_index).enum_type;
 
         // TODO: detect when the type changes and re-emit this function.
-        const gop = try self.dg.object.decl_map.getOrPut(self.dg.gpa, enum_decl);
+        const gop = try self.dg.object.decl_map.getOrPut(self.dg.gpa, enum_type.decl);
         if (gop.found_existing) return gop.value_ptr.*;
-        errdefer assert(self.dg.object.decl_map.remove(enum_decl));
+        errdefer assert(self.dg.object.decl_map.remove(enum_type.decl));
 
         var arena_allocator = std.heap.ArenaAllocator.init(self.gpa);
         defer arena_allocator.deinit();
         const arena = arena_allocator.allocator();
 
-        const fqn = try mod.declPtr(enum_decl).getFullyQualifiedName(mod);
+        const fqn = try mod.declPtr(enum_type.decl).getFullyQualifiedName(mod);
         defer self.gpa.free(fqn);
         const llvm_fn_name = try std.fmt.allocPrintZ(arena, "__zig_tag_name_{s}", .{fqn});
 
@@ -8950,8 +8943,7 @@ pub const FuncGen = struct {
         const usize_llvm_ty = try self.dg.lowerType(Type.usize);
         const slice_alignment = slice_ty.abiAlignment(mod);
 
-        const int_tag_ty = try enum_ty.intTagType(mod);
-        const param_types = [_]*llvm.Type{try self.dg.lowerType(int_tag_ty)};
+        const param_types = [_]*llvm.Type{try self.dg.lowerType(enum_type.tag_ty.toType())};
 
         const fn_type = llvm.functionType(llvm_ret_ty, &param_types, param_types.len, .False);
         const fn_val = self.dg.object.llvm_module.addFunction(llvm_fn_name, fn_type);
@@ -8973,16 +8965,16 @@ pub const FuncGen = struct {
         self.builder.positionBuilderAtEnd(entry_block);
         self.builder.clearCurrentDebugLocation();
 
-        const fields = enum_ty.enumFields();
         const bad_value_block = self.context.appendBasicBlock(fn_val, "BadValue");
         const tag_int_value = fn_val.getParam(0);
-        const switch_instr = self.builder.buildSwitch(tag_int_value, bad_value_block, @intCast(c_uint, fields.count()));
+        const switch_instr = self.builder.buildSwitch(tag_int_value, bad_value_block, @intCast(c_uint, enum_type.names.len));
 
         const array_ptr_indices = [_]*llvm.Value{
             usize_llvm_ty.constNull(), usize_llvm_ty.constNull(),
         };
 
-        for (fields.keys(), 0..) |name, field_index| {
+        for (enum_type.names, 0..) |name_ip, field_index| {
+            const name = mod.intern_pool.stringToSlice(name_ip);
             const str_init = self.context.constString(name.ptr, @intCast(c_uint, name.len), .False);
             const str_init_llvm_ty = str_init.typeOf();
             const str_global = self.dg.object.llvm_module.addGlobal(str_init_llvm_ty, "");
@@ -9429,7 +9421,7 @@ pub const FuncGen = struct {
         const tag_int = blk: {
             const tag_ty = union_ty.unionTagTypeHypothetical(mod);
             const union_field_name = union_obj.fields.keys()[extra.field_index];
-            const enum_field_index = tag_ty.enumFieldIndex(union_field_name).?;
+            const enum_field_index = tag_ty.enumFieldIndex(union_field_name, mod).?;
             var tag_val_payload: Value.Payload.U32 = .{
                 .base = .{ .tag = .enum_field_index },
                 .data = @intCast(u32, enum_field_index),
src/link/Dwarf.zig
@@ -401,14 +401,9 @@ pub const DeclState = struct {
                 dbg_info_buffer.appendSliceAssumeCapacity(enum_name);
                 dbg_info_buffer.appendAssumeCapacity(0);
 
-                const fields = ty.enumFields();
-                const values: ?Module.EnumFull.ValueMap = switch (ty.tag()) {
-                    .enum_full, .enum_nonexhaustive => ty.cast(Type.Payload.EnumFull).?.data.values,
-                    .enum_simple => null,
-                    .enum_numbered => ty.castTag(.enum_numbered).?.data.values,
-                    else => unreachable,
-                };
-                for (fields.keys(), 0..) |field_name, field_i| {
+                const enum_type = mod.intern_pool.indexToKey(ty.ip_index).enum_type;
+                for (enum_type.names, 0..) |field_name_index, field_i| {
+                    const field_name = mod.intern_pool.stringToSlice(field_name_index);
                     // DW.AT.enumerator
                     try dbg_info_buffer.ensureUnusedCapacity(field_name.len + 2 + @sizeOf(u64));
                     dbg_info_buffer.appendAssumeCapacity(@enumToInt(AbbrevKind.enum_variant));
@@ -416,14 +411,14 @@ pub const DeclState = struct {
                     dbg_info_buffer.appendSliceAssumeCapacity(field_name);
                     dbg_info_buffer.appendAssumeCapacity(0);
                     // DW.AT.const_value, DW.FORM.data8
-                    const value: u64 = if (values) |vals| value: {
-                        if (vals.count() == 0) break :value @intCast(u64, field_i); // auto-numbered
-                        const value = vals.keys()[field_i];
+                    const value: u64 = value: {
+                        if (enum_type.values.len == 0) break :value field_i; // auto-numbered
+                        const value = enum_type.values[field_i];
                         // TODO do not assume a 64bit enum value - could be bigger.
                         // See https://github.com/ziglang/zig/issues/645
-                        const field_int_val = try value.enumToInt(ty, mod);
+                        const field_int_val = try value.toValue().enumToInt(ty, mod);
                         break :value @bitCast(u64, field_int_val.toSignedInt(mod));
-                    } else @intCast(u64, field_i);
+                    };
                     mem.writeInt(u64, dbg_info_buffer.addManyAsArrayAssumeCapacity(8), value, target_endian);
                 }
 
src/AstGen.zig
@@ -10694,8 +10694,8 @@ fn identAsString(astgen: *AstGen, ident_token: Ast.TokenIndex) !u32 {
     const string_bytes = &astgen.string_bytes;
     const str_index = @intCast(u32, string_bytes.items.len);
     try astgen.appendIdentStr(ident_token, string_bytes);
-    const key = string_bytes.items[str_index..];
-    const gop = try astgen.string_table.getOrPutContextAdapted(gpa, @as([]const u8, key), StringIndexAdapter{
+    const key: []const u8 = string_bytes.items[str_index..];
+    const gop = try astgen.string_table.getOrPutContextAdapted(gpa, key, StringIndexAdapter{
         .bytes = string_bytes,
     }, StringIndexContext{
         .bytes = string_bytes,
src/codegen.zig
@@ -156,7 +156,8 @@ pub fn generateLazySymbol(
         return Result.ok;
     } else if (lazy_sym.ty.zigTypeTag(mod) == .Enum) {
         alignment.* = 1;
-        for (lazy_sym.ty.enumFields().keys()) |tag_name| {
+        for (lazy_sym.ty.enumFields(mod)) |tag_name_ip| {
+            const tag_name = mod.intern_pool.stringToSlice(tag_name_ip);
             try code.ensureUnusedCapacity(tag_name.len + 1);
             code.appendSliceAssumeCapacity(tag_name);
             code.appendAssumeCapacity(0);
@@ -1229,26 +1230,15 @@ pub fn genTypedValue(
         },
         .Enum => {
             if (typed_value.val.castTag(.enum_field_index)) |field_index| {
-                switch (typed_value.ty.tag()) {
-                    .enum_simple => {
-                        return GenResult.mcv(.{ .immediate = field_index.data });
-                    },
-                    .enum_numbered, .enum_full, .enum_nonexhaustive => {
-                        const enum_values = if (typed_value.ty.castTag(.enum_numbered)) |pl|
-                            pl.data.values
-                        else
-                            typed_value.ty.cast(Type.Payload.EnumFull).?.data.values;
-                        if (enum_values.count() != 0) {
-                            const tag_val = enum_values.keys()[field_index.data];
-                            return genTypedValue(bin_file, src_loc, .{
-                                .ty = try typed_value.ty.intTagType(mod),
-                                .val = tag_val,
-                            }, owner_decl_index);
-                        } else {
-                            return GenResult.mcv(.{ .immediate = field_index.data });
-                        }
-                    },
-                    else => unreachable,
+                const enum_type = mod.intern_pool.indexToKey(typed_value.ty.ip_index).enum_type;
+                if (enum_type.values.len != 0) {
+                    const tag_val = enum_type.values[field_index.data];
+                    return genTypedValue(bin_file, src_loc, .{
+                        .ty = enum_type.tag_ty.toType(),
+                        .val = tag_val.toValue(),
+                    }, owner_decl_index);
+                } else {
+                    return GenResult.mcv(.{ .immediate = field_index.data });
                 }
             } else {
                 const int_tag_ty = try typed_value.ty.intTagType(mod);
src/InternPool.zig
@@ -40,6 +40,14 @@ unions_free_list: std.ArrayListUnmanaged(Module.Union.Index) = .{},
 /// to provide lookup.
 maps: std.ArrayListUnmanaged(std.AutoArrayHashMapUnmanaged(void, void)) = .{},
 
+/// Used for finding the index inside `string_bytes`.
+string_table: std.HashMapUnmanaged(
+    u32,
+    void,
+    std.hash_map.StringIndexContext,
+    std.hash_map.default_max_load_percentage,
+) = .{},
+
 const std = @import("std");
 const Allocator = std.mem.Allocator;
 const assert = std.debug.assert;
@@ -68,6 +76,11 @@ const KeyAdapter = struct {
 pub const OptionalMapIndex = enum(u32) {
     none = std.math.maxInt(u32),
     _,
+
+    pub fn unwrap(oi: OptionalMapIndex) ?MapIndex {
+        if (oi == .none) return null;
+        return @intToEnum(MapIndex, @enumToInt(oi));
+    }
 };
 
 /// An index into `maps`.
@@ -83,6 +96,10 @@ pub const MapIndex = enum(u32) {
 pub const NullTerminatedString = enum(u32) {
     _,
 
+    pub fn toOptional(self: NullTerminatedString) OptionalNullTerminatedString {
+        return @intToEnum(OptionalNullTerminatedString, @enumToInt(self));
+    }
+
     const Adapter = struct {
         strings: []const NullTerminatedString,
 
@@ -102,6 +119,11 @@ pub const NullTerminatedString = enum(u32) {
 pub const OptionalNullTerminatedString = enum(u32) {
     none = std.math.maxInt(u32),
     _,
+
+    pub fn unwrap(oi: OptionalNullTerminatedString) ?NullTerminatedString {
+        if (oi == .none) return null;
+        return @intToEnum(NullTerminatedString, @enumToInt(oi));
+    }
 };
 
 pub const Key = union(enum) {
@@ -242,13 +264,75 @@ pub const Key = union(enum) {
         /// Entries are in declaration order, same as `fields`.
         /// If this is empty, it means the enum tags are auto-numbered.
         values: []const Index,
-        /// true if zig inferred this tag type, false if user specified it
-        tag_ty_inferred: bool,
+        tag_mode: TagMode,
         /// This is ignored by `get` but will always be provided by `indexToKey`.
         names_map: OptionalMapIndex = .none,
         /// This is ignored by `get` but will be provided by `indexToKey` when
         /// a value map exists.
         values_map: OptionalMapIndex = .none,
+
+        pub const TagMode = enum {
+            /// The integer tag type was auto-numbered by zig.
+            auto,
+            /// The integer tag type was provided by the enum declaration, and the enum
+            /// is exhaustive.
+            explicit,
+            /// The integer tag type was provided by the enum declaration, and the enum
+            /// is non-exhaustive.
+            nonexhaustive,
+        };
+
+        /// Look up field index based on field name.
+        pub fn nameIndex(self: EnumType, ip: InternPool, name: NullTerminatedString) ?usize {
+            const map = &ip.maps.items[@enumToInt(self.names_map.unwrap().?)];
+            const adapter: NullTerminatedString.Adapter = .{ .strings = self.names };
+            return map.getIndexAdapted(name, adapter);
+        }
+
+        /// Look up field index based on tag value.
+        /// Asserts that `values_map` is not `none`.
+        /// This function returns `null` when `tag_val` does not have the
+        /// integer tag type of the enum.
+        pub fn tagValueIndex(self: EnumType, ip: InternPool, tag_val: Index) ?usize {
+            assert(tag_val != .none);
+            const map = &ip.maps.items[@enumToInt(self.values_map.unwrap().?)];
+            const adapter: Index.Adapter = .{ .indexes = self.values };
+            return map.getIndexAdapted(tag_val, adapter);
+        }
+    };
+
+    pub const IncompleteEnumType = struct {
+        /// Same as corresponding `EnumType` field.
+        decl: Module.Decl.Index,
+        /// Same as corresponding `EnumType` field.
+        namespace: Module.Namespace.OptionalIndex,
+        /// 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: EnumType.TagMode,
+        /// This may be updated via `setTagType` later.
+        tag_ty: Index = .none,
+
+        pub fn toEnumType(self: @This()) EnumType {
+            return .{
+                .decl = self.decl,
+                .namespace = self.namespace,
+                .tag_ty = self.tag_ty,
+                .tag_mode = self.tag_mode,
+                .names = &.{},
+                .values = &.{},
+            };
+        }
+
+        /// 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 = self.toEnumType() };
+        }
     };
 
     pub const Int = struct {
@@ -946,12 +1030,18 @@ pub const Tag = enum(u8) {
     /// An error union type.
     /// data is payload to ErrorUnion.
     type_error_union,
-    /// An enum type with an explicitly provided integer tag type.
-    /// data is payload index to `EnumExplicit`.
-    type_enum_explicit,
     /// An enum type with auto-numbered tag values.
+    /// The enum is exhaustive.
     /// data is payload index to `EnumAuto`.
     type_enum_auto,
+    /// An enum type with an explicitly provided integer tag type.
+    /// The enum is exhaustive.
+    /// data is payload index to `EnumExplicit`.
+    type_enum_explicit,
+    /// An enum type with an explicitly provided integer tag type.
+    /// The enum is non-exhaustive.
+    /// data is payload index to `EnumExplicit`.
+    type_enum_nonexhaustive,
     /// A type that can be represented with only an enum tag.
     /// data is SimpleType enum value.
     simple_type,
@@ -1302,9 +1392,11 @@ pub fn deinit(ip: *InternPool, gpa: Allocator) void {
     ip.unions_free_list.deinit(gpa);
     ip.allocated_unions.deinit(gpa);
 
-    for (ip.maps) |*map| map.deinit(gpa);
+    for (ip.maps.items) |*map| map.deinit(gpa);
     ip.maps.deinit(gpa);
 
+    ip.string_table.deinit(gpa);
+
     ip.* = undefined;
 }
 
@@ -1421,33 +1513,13 @@ pub fn indexToKey(ip: InternPool, index: Index) Key {
                 .tag_ty = ip.getEnumIntTagType(enum_auto.data.fields_len),
                 .names = names,
                 .values = &.{},
-                .tag_ty_inferred = true,
+                .tag_mode = .auto,
                 .names_map = enum_auto.data.names_map.toOptional(),
                 .values_map = .none,
             } };
         },
-        .type_enum_explicit => {
-            const enum_explicit = ip.extraDataTrail(EnumExplicit, data);
-            const names = @ptrCast(
-                []const NullTerminatedString,
-                ip.extra.items[enum_explicit.end..][0..enum_explicit.data.fields_len],
-            );
-            const values = if (enum_explicit.data.values_map != .none) @ptrCast(
-                []const Index,
-                ip.extra.items[enum_explicit.end + names.len ..][0..enum_explicit.data.fields_len],
-            ) else &[0]Index{};
-
-            return .{ .enum_type = .{
-                .decl = enum_explicit.data.decl,
-                .namespace = enum_explicit.data.namespace,
-                .tag_ty = enum_explicit.data.int_tag_type,
-                .names = names,
-                .values = values,
-                .tag_ty_inferred = false,
-                .names_map = enum_explicit.data.names_map.toOptional(),
-                .values_map = enum_explicit.data.values_map,
-            } };
-        },
+        .type_enum_explicit => indexToKeyEnum(ip, data, .explicit),
+        .type_enum_nonexhaustive => indexToKeyEnum(ip, data, .nonexhaustive),
 
         .opt_null => .{ .opt = .{
             .ty = @intToEnum(Index, data),
@@ -1531,6 +1603,29 @@ fn getEnumIntTagType(ip: InternPool, fields_len: u32) Index {
     } });
 }
 
+fn indexToKeyEnum(ip: InternPool, data: u32, tag_mode: Key.EnumType.TagMode) Key {
+    const enum_explicit = ip.extraDataTrail(EnumExplicit, data);
+    const names = @ptrCast(
+        []const NullTerminatedString,
+        ip.extra.items[enum_explicit.end..][0..enum_explicit.data.fields_len],
+    );
+    const values = if (enum_explicit.data.values_map != .none) @ptrCast(
+        []const Index,
+        ip.extra.items[enum_explicit.end + names.len ..][0..enum_explicit.data.fields_len],
+    ) else &[0]Index{};
+
+    return .{ .enum_type = .{
+        .decl = enum_explicit.data.decl,
+        .namespace = enum_explicit.data.namespace,
+        .tag_ty = enum_explicit.data.int_tag_type,
+        .names = names,
+        .values = values,
+        .tag_mode = tag_mode,
+        .names_map = enum_explicit.data.names_map.toOptional(),
+        .values_map = enum_explicit.data.values_map,
+    } };
+}
+
 fn indexToKeyBigInt(ip: InternPool, limb_index: u32, positive: bool) Key {
     const int_info = ip.limbData(Int, limb_index);
     return .{ .int = .{
@@ -1696,47 +1791,29 @@ pub fn get(ip: *InternPool, gpa: Allocator, key: Key) Allocator.Error!Index {
             assert(enum_type.names_map == .none);
             assert(enum_type.values_map == .none);
 
-            const names_map = try ip.addMap(gpa);
-            try addStringsToMap(ip, gpa, names_map, enum_type.names);
+            switch (enum_type.tag_mode) {
+                .auto => {
+                    const names_map = try ip.addMap(gpa);
+                    try addStringsToMap(ip, gpa, names_map, enum_type.names);
 
-            const fields_len = @intCast(u32, enum_type.names.len);
-
-            if (enum_type.tag_ty_inferred) {
-                try ip.extra.ensureUnusedCapacity(gpa, @typeInfo(EnumAuto).Struct.fields.len +
-                    fields_len);
-                ip.items.appendAssumeCapacity(.{
-                    .tag = .type_enum_auto,
-                    .data = ip.addExtraAssumeCapacity(EnumAuto{
-                        .decl = enum_type.decl,
-                        .namespace = enum_type.namespace,
-                        .names_map = names_map,
-                        .fields_len = fields_len,
-                    }),
-                });
-                ip.extra.appendSliceAssumeCapacity(@ptrCast([]const u32, enum_type.names));
-                return @intToEnum(Index, ip.items.len - 1);
+                    const fields_len = @intCast(u32, enum_type.names.len);
+                    try ip.extra.ensureUnusedCapacity(gpa, @typeInfo(EnumAuto).Struct.fields.len +
+                        fields_len);
+                    ip.items.appendAssumeCapacity(.{
+                        .tag = .type_enum_auto,
+                        .data = ip.addExtraAssumeCapacity(EnumAuto{
+                            .decl = enum_type.decl,
+                            .namespace = enum_type.namespace,
+                            .names_map = names_map,
+                            .fields_len = fields_len,
+                        }),
+                    });
+                    ip.extra.appendSliceAssumeCapacity(@ptrCast([]const u32, enum_type.names));
+                    return @intToEnum(Index, ip.items.len - 1);
+                },
+                .explicit => return finishGetEnum(ip, gpa, enum_type, .type_enum_explicit),
+                .nonexhaustive => return finishGetEnum(ip, gpa, enum_type, .type_enum_nonexhaustive),
             }
-
-            const values_map: OptionalMapIndex = if (enum_type.values.len == 0) .none else m: {
-                const values_map = try ip.addMap(gpa);
-                try addIndexesToMap(ip, gpa, values_map, enum_type.values);
-                break :m values_map.toOptional();
-            };
-            try ip.extra.ensureUnusedCapacity(gpa, @typeInfo(EnumExplicit).Struct.fields.len +
-                fields_len);
-            ip.items.appendAssumeCapacity(.{
-                .tag = .type_enum_auto,
-                .data = ip.addExtraAssumeCapacity(EnumExplicit{
-                    .decl = enum_type.decl,
-                    .namespace = enum_type.namespace,
-                    .int_tag_type = enum_type.tag_ty,
-                    .fields_len = fields_len,
-                    .names_map = names_map,
-                    .values_map = values_map,
-                }),
-            });
-            ip.extra.appendSliceAssumeCapacity(@ptrCast([]const u32, enum_type.names));
-            ip.extra.appendSliceAssumeCapacity(@ptrCast([]const u32, enum_type.values));
         },
 
         .extern_func => @panic("TODO"),
@@ -1934,8 +2011,206 @@ pub fn get(ip: *InternPool, gpa: Allocator, key: Key) Allocator.Error!Index {
     return @intToEnum(Index, ip.items.len - 1);
 }
 
-pub fn getAssumeExists(ip: InternPool, key: Key) Index {
-    const adapter: KeyAdapter = .{ .intern_pool = &ip };
+/// Provides API for completing an enum type after calling `getIncompleteEnum`.
+pub const IncompleteEnumType = struct {
+    index: Index,
+    tag_ty_index: u32,
+    names_map: MapIndex,
+    names_start: u32,
+    values_map: OptionalMapIndex,
+    values_start: u32,
+
+    pub fn setTagType(self: @This(), ip: *InternPool, tag_ty: Index) void {
+        assert(tag_ty != .none);
+        ip.extra.items[self.tag_ty_index] = @enumToInt(tag_ty);
+    }
+
+    /// Returns the already-existing field with the same name, if any.
+    pub fn addFieldName(
+        self: @This(),
+        ip: *InternPool,
+        gpa: Allocator,
+        name: NullTerminatedString,
+    ) Allocator.Error!?u32 {
+        const map = &ip.maps.items[@enumToInt(self.names_map)];
+        const field_index = map.count();
+        const strings = ip.extra.items[self.names_start..][0..field_index];
+        const adapter: NullTerminatedString.Adapter = .{
+            .strings = @ptrCast([]const NullTerminatedString, strings),
+        };
+        const gop = try map.getOrPutAdapted(gpa, name, adapter);
+        if (gop.found_existing) return @intCast(u32, gop.index);
+        ip.extra.items[self.names_start + field_index] = @enumToInt(name);
+        return null;
+    }
+
+    /// 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,
+        gpa: Allocator,
+        value: Index,
+    ) Allocator.Error!?u32 {
+        const map = &ip.maps.items[@enumToInt(self.values_map.unwrap().?)];
+        const field_index = map.count();
+        const indexes = ip.extra.items[self.values_start..][0..field_index];
+        const adapter: Index.Adapter = .{
+            .indexes = @ptrCast([]const Index, indexes),
+        };
+        const gop = try map.getOrPutAdapted(gpa, value, adapter);
+        if (gop.found_existing) return @intCast(u32, gop.index);
+        ip.extra.items[self.values_start + field_index] = @enumToInt(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!InternPool.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 getIncompleteEnumAuto(
+    ip: *InternPool,
+    gpa: Allocator,
+    enum_type: Key.IncompleteEnumType,
+) Allocator.Error!InternPool.IncompleteEnumType {
+    // Although the integer tag type will not be stored in the `EnumAuto` struct,
+    // `InternPool` logic depends on it being present so that `typeOf` can be infallible.
+    // Ensure it is present here:
+    _ = 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);
+
+    const extra_fields_len: u32 = @typeInfo(EnumAuto).Struct.fields.len;
+    try ip.extra.ensureUnusedCapacity(gpa, extra_fields_len + enum_type.fields_len);
+
+    const extra_index = ip.addExtraAssumeCapacity(EnumAuto{
+        .decl = enum_type.decl,
+        .namespace = enum_type.namespace,
+        .names_map = names_map,
+        .fields_len = enum_type.fields_len,
+    });
+
+    ip.items.appendAssumeCapacity(.{
+        .tag = .type_enum_auto,
+        .data = extra_index,
+    });
+    ip.extra.appendNTimesAssumeCapacity(@enumToInt(Index.none), enum_type.fields_len);
+    return .{
+        .index = @intToEnum(Index, ip.items.len - 1),
+        .tag_ty_index = undefined,
+        .names_map = names_map,
+        .names_start = extra_index + extra_fields_len,
+        .values_map = .none,
+        .values_start = undefined,
+    };
+}
+
+pub fn getIncompleteEnumExplicit(
+    ip: *InternPool,
+    gpa: Allocator,
+    enum_type: Key.IncompleteEnumType,
+    tag: Tag,
+) Allocator.Error!InternPool.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`.
+    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);
+    const values_map: OptionalMapIndex = if (!enum_type.has_values) .none else m: {
+        const values_map = try ip.addMap(gpa);
+        break :m values_map.toOptional();
+    };
+
+    const reserved_len = enum_type.fields_len +
+        if (enum_type.has_values) enum_type.fields_len else 0;
+
+    const extra_fields_len: u32 = @typeInfo(EnumExplicit).Struct.fields.len;
+    try ip.extra.ensureUnusedCapacity(gpa, extra_fields_len + reserved_len);
+
+    const extra_index = ip.addExtraAssumeCapacity(EnumExplicit{
+        .decl = enum_type.decl,
+        .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,
+    });
+
+    ip.items.appendAssumeCapacity(.{
+        .tag = tag,
+        .data = extra_index,
+    });
+    // This is both fields and values (if present).
+    ip.extra.appendNTimesAssumeCapacity(@enumToInt(Index.none), reserved_len);
+    return .{
+        .index = @intToEnum(Index, 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,
+    };
+}
+
+pub fn finishGetEnum(
+    ip: *InternPool,
+    gpa: Allocator,
+    enum_type: Key.EnumType,
+    tag: Tag,
+) Allocator.Error!Index {
+    const names_map = try ip.addMap(gpa);
+    try addStringsToMap(ip, gpa, names_map, enum_type.names);
+
+    const values_map: OptionalMapIndex = if (enum_type.values.len == 0) .none else m: {
+        const values_map = try ip.addMap(gpa);
+        try addIndexesToMap(ip, gpa, values_map, enum_type.values);
+        break :m values_map.toOptional();
+    };
+    const fields_len = @intCast(u32, enum_type.names.len);
+    try ip.extra.ensureUnusedCapacity(gpa, @typeInfo(EnumExplicit).Struct.fields.len +
+        fields_len);
+    ip.items.appendAssumeCapacity(.{
+        .tag = tag,
+        .data = ip.addExtraAssumeCapacity(EnumExplicit{
+            .decl = enum_type.decl,
+            .namespace = enum_type.namespace,
+            .int_tag_type = enum_type.tag_ty,
+            .fields_len = fields_len,
+            .names_map = names_map,
+            .values_map = values_map,
+        }),
+    });
+    ip.extra.appendSliceAssumeCapacity(@ptrCast([]const u32, enum_type.names));
+    ip.extra.appendSliceAssumeCapacity(@ptrCast([]const u32, enum_type.values));
+    return @intToEnum(Index, ip.items.len - 1);
+}
+
+pub fn getAssumeExists(ip: *const InternPool, key: Key) Index {
+    const adapter: KeyAdapter = .{ .intern_pool = ip };
     const index = ip.map.getIndexAdapted(key, adapter).?;
     return @intToEnum(Index, index);
 }
@@ -1979,6 +2254,7 @@ fn addMap(ip: *InternPool, gpa: Allocator) Allocator.Error!MapIndex {
 pub fn remove(ip: *InternPool, index: Index) void {
     _ = ip;
     _ = index;
+    @setCold(true);
     @panic("TODO this is a bit problematic to implement, could we maybe just never support a remove() operation on InternPool?");
 }
 
@@ -2336,7 +2612,7 @@ fn dumpFallible(ip: InternPool, arena: Allocator) anyerror!void {
             .type_slice => 0,
             .type_optional => 0,
             .type_error_union => @sizeOf(ErrorUnion),
-            .type_enum_explicit => @sizeOf(EnumExplicit),
+            .type_enum_explicit, .type_enum_nonexhaustive => @sizeOf(EnumExplicit),
             .type_enum_auto => @sizeOf(EnumAuto),
             .type_opaque => @sizeOf(Key.OpaqueType),
             .type_struct => @sizeOf(Module.Struct) + @sizeOf(Module.Namespace) + @sizeOf(Module.Decl),
@@ -2448,3 +2724,50 @@ pub fn destroyUnion(ip: *InternPool, gpa: Allocator, index: Module.Union.Index)
         // allocation failures here, instead leaking the Union until garbage collection.
     };
 }
+
+pub fn getOrPutString(
+    ip: *InternPool,
+    gpa: Allocator,
+    s: []const u8,
+) Allocator.Error!NullTerminatedString {
+    const string_bytes = &ip.string_bytes;
+    const str_index = @intCast(u32, string_bytes.items.len);
+    try string_bytes.ensureUnusedCapacity(gpa, s.len + 1);
+    string_bytes.appendSliceAssumeCapacity(s);
+    const key: []const u8 = string_bytes.items[str_index..];
+    const gop = try ip.string_table.getOrPutContextAdapted(gpa, key, std.hash_map.StringIndexAdapter{
+        .bytes = string_bytes,
+    }, std.hash_map.StringIndexContext{
+        .bytes = string_bytes,
+    });
+    if (gop.found_existing) {
+        string_bytes.shrinkRetainingCapacity(str_index);
+        return @intToEnum(NullTerminatedString, gop.key_ptr.*);
+    } else {
+        gop.key_ptr.* = str_index;
+        string_bytes.appendAssumeCapacity(0);
+        return @intToEnum(NullTerminatedString, str_index);
+    }
+}
+
+pub fn getString(ip: *InternPool, s: []const u8) OptionalNullTerminatedString {
+    if (ip.string_table.getKeyAdapted(s, std.hash_map.StringIndexAdapter{
+        .bytes = &ip.string_bytes,
+    })) |index| {
+        return @intToEnum(NullTerminatedString, index).toOptional();
+    } else {
+        return .none;
+    }
+}
+
+pub fn stringToSlice(ip: InternPool, s: NullTerminatedString) [:0]const u8 {
+    const string_bytes = ip.string_bytes.items;
+    const start = @enumToInt(s);
+    var end: usize = start;
+    while (string_bytes[end] != 0) end += 1;
+    return string_bytes[start..end :0];
+}
+
+pub fn typeOf(ip: InternPool, index: Index) Index {
+    return ip.indexToKey(index).typeOf();
+}
src/Module.zig
@@ -886,29 +886,17 @@ pub const Decl = struct {
     /// Only returns it if the Decl is the owner.
     pub fn getInnerNamespaceIndex(decl: *Decl, mod: *Module) Namespace.OptionalIndex {
         if (!decl.owns_tv) return .none;
-        switch (decl.val.ip_index) {
-            .empty_struct_type => return .none,
-            .none => {
-                const ty = (decl.val.castTag(.ty) orelse return .none).data;
-                switch (ty.tag()) {
-                    .enum_full, .enum_nonexhaustive => {
-                        const enum_obj = ty.cast(Type.Payload.EnumFull).?.data;
-                        return enum_obj.namespace.toOptional();
-                    },
-
-                    else => return .none,
-                }
-            },
-            else => return switch (mod.intern_pool.indexToKey(decl.val.ip_index)) {
+        return switch (decl.val.ip_index) {
+            .empty_struct_type => .none,
+            .none => .none,
+            else => switch (mod.intern_pool.indexToKey(decl.val.ip_index)) {
                 .opaque_type => |opaque_type| opaque_type.namespace.toOptional(),
                 .struct_type => |struct_type| struct_type.namespace,
-                .union_type => |union_type| {
-                    const union_obj = mod.unionPtr(union_type.index);
-                    return union_obj.namespace.toOptional();
-                },
+                .union_type => |union_type| mod.unionPtr(union_type.index).namespace.toOptional(),
+                .enum_type => |enum_type| enum_type.namespace,
                 else => .none,
             },
-        }
+        };
     }
 
     /// Same as `getInnerNamespaceIndex` but additionally obtains the pointer.
@@ -1135,28 +1123,6 @@ pub const Struct = struct {
         return mod.declPtr(s.owner_decl).srcLoc(mod);
     }
 
-    pub fn fieldSrcLoc(s: Struct, mod: *Module, query: FieldSrcQuery) SrcLoc {
-        @setCold(true);
-        const owner_decl = mod.declPtr(s.owner_decl);
-        const file = owner_decl.getFileScope(mod);
-        const tree = file.getTree(mod.gpa) catch |err| {
-            // In this case we emit a warning + a less precise source location.
-            log.warn("unable to load {s}: {s}", .{
-                file.sub_file_path, @errorName(err),
-            });
-            return s.srcLoc(mod);
-        };
-        const node = owner_decl.relativeToNodeIndex(0);
-
-        var buf: [2]Ast.Node.Index = undefined;
-        if (tree.fullContainerDecl(&buf, node)) |container_decl| {
-            return queryFieldSrc(tree.*, query, file, container_decl);
-        } else {
-            // This struct was generated using @Type
-            return s.srcLoc(mod);
-        }
-    }
-
     pub fn haveFieldTypes(s: Struct) bool {
         return switch (s.status) {
             .none,
@@ -1237,110 +1203,6 @@ pub const Struct = struct {
     }
 };
 
-/// Represents the data that an enum declaration provides, when the fields
-/// are auto-numbered, and there are no declarations. The integer tag type
-/// is inferred to be the smallest power of two unsigned int that fits
-/// the number of fields.
-pub const EnumSimple = struct {
-    /// The Decl that corresponds to the enum itself.
-    owner_decl: Decl.Index,
-    /// Set of field names in declaration order.
-    fields: NameMap,
-
-    pub const NameMap = EnumFull.NameMap;
-
-    pub fn srcLoc(self: EnumSimple, mod: *Module) SrcLoc {
-        const owner_decl = mod.declPtr(self.owner_decl);
-        return .{
-            .file_scope = owner_decl.getFileScope(mod),
-            .parent_decl_node = owner_decl.src_node,
-            .lazy = LazySrcLoc.nodeOffset(0),
-        };
-    }
-};
-
-/// Represents the data that an enum declaration provides, when there are no
-/// declarations. However an integer tag type is provided, and the enum tag values
-/// are explicitly provided.
-pub const EnumNumbered = struct {
-    /// The Decl that corresponds to the enum itself.
-    owner_decl: Decl.Index,
-    /// An integer type which is used for the numerical value of the enum.
-    /// Whether zig chooses this type or the user specifies it, it is stored here.
-    tag_ty: Type,
-    /// Set of field names in declaration order.
-    fields: NameMap,
-    /// Maps integer tag value to field index.
-    /// Entries are in declaration order, same as `fields`.
-    /// If this hash map is empty, it means the enum tags are auto-numbered.
-    values: ValueMap,
-
-    pub const NameMap = EnumFull.NameMap;
-    pub const ValueMap = EnumFull.ValueMap;
-
-    pub fn srcLoc(self: EnumNumbered, mod: *Module) SrcLoc {
-        const owner_decl = mod.declPtr(self.owner_decl);
-        return .{
-            .file_scope = owner_decl.getFileScope(mod),
-            .parent_decl_node = owner_decl.src_node,
-            .lazy = LazySrcLoc.nodeOffset(0),
-        };
-    }
-};
-
-/// Represents the data that an enum declaration provides, when there is
-/// at least one tag value explicitly specified, or at least one declaration.
-pub const EnumFull = struct {
-    /// The Decl that corresponds to the enum itself.
-    owner_decl: Decl.Index,
-    /// An integer type which is used for the numerical value of the enum.
-    /// Whether zig chooses this type or the user specifies it, it is stored here.
-    tag_ty: Type,
-    /// Set of field names in declaration order.
-    fields: NameMap,
-    /// Maps integer tag value to field index.
-    /// Entries are in declaration order, same as `fields`.
-    /// If this hash map is empty, it means the enum tags are auto-numbered.
-    values: ValueMap,
-    /// Represents the declarations inside this enum.
-    namespace: Namespace.Index,
-    /// true if zig inferred this tag type, false if user specified it
-    tag_ty_inferred: bool,
-
-    pub const NameMap = std.StringArrayHashMapUnmanaged(void);
-    pub const ValueMap = std.ArrayHashMapUnmanaged(Value, void, Value.ArrayHashContext, false);
-
-    pub fn srcLoc(self: EnumFull, mod: *Module) SrcLoc {
-        const owner_decl = mod.declPtr(self.owner_decl);
-        return .{
-            .file_scope = owner_decl.getFileScope(mod),
-            .parent_decl_node = owner_decl.src_node,
-            .lazy = LazySrcLoc.nodeOffset(0),
-        };
-    }
-
-    pub fn fieldSrcLoc(e: EnumFull, mod: *Module, query: FieldSrcQuery) SrcLoc {
-        @setCold(true);
-        const owner_decl = mod.declPtr(e.owner_decl);
-        const file = owner_decl.getFileScope(mod);
-        const tree = file.getTree(mod.gpa) catch |err| {
-            // In this case we emit a warning + a less precise source location.
-            log.warn("unable to load {s}: {s}", .{
-                file.sub_file_path, @errorName(err),
-            });
-            return e.srcLoc(mod);
-        };
-        const node = owner_decl.relativeToNodeIndex(0);
-        var buf: [2]Ast.Node.Index = undefined;
-        if (tree.fullContainerDecl(&buf, node)) |container_decl| {
-            return queryFieldSrc(tree.*, query, file, container_decl);
-        } else {
-            // This enum was generated using @Type
-            return e.srcLoc(mod);
-        }
-    }
-};
-
 pub const Union = struct {
     /// An enum type which is used for the tag of the union.
     /// This type is created even for untagged unions, even when the memory
@@ -1427,28 +1289,6 @@ pub const Union = struct {
         };
     }
 
-    pub fn fieldSrcLoc(u: Union, mod: *Module, query: FieldSrcQuery) SrcLoc {
-        @setCold(true);
-        const owner_decl = mod.declPtr(u.owner_decl);
-        const file = owner_decl.getFileScope(mod);
-        const tree = file.getTree(mod.gpa) catch |err| {
-            // In this case we emit a warning + a less precise source location.
-            log.warn("unable to load {s}: {s}", .{
-                file.sub_file_path, @errorName(err),
-            });
-            return u.srcLoc(mod);
-        };
-        const node = owner_decl.relativeToNodeIndex(0);
-
-        var buf: [2]Ast.Node.Index = undefined;
-        if (tree.fullContainerDecl(&buf, node)) |container_decl| {
-            return queryFieldSrc(tree.*, query, file, container_decl);
-        } else {
-            // This union was generated using @Type
-            return u.srcLoc(mod);
-        }
-    }
-
     pub fn haveFieldTypes(u: Union) bool {
         return switch (u.status) {
             .none,
@@ -7313,3 +7153,24 @@ pub fn typeToUnion(mod: *Module, ty: Type) ?*Union {
     const union_index = mod.intern_pool.indexToUnion(ty.ip_index).unwrap() orelse return null;
     return mod.unionPtr(union_index);
 }
+
+pub fn fieldSrcLoc(mod: *Module, owner_decl_index: Decl.Index, query: FieldSrcQuery) SrcLoc {
+    @setCold(true);
+    const owner_decl = mod.declPtr(owner_decl_index);
+    const file = owner_decl.getFileScope(mod);
+    const tree = file.getTree(mod.gpa) catch |err| {
+        // In this case we emit a warning + a less precise source location.
+        log.warn("unable to load {s}: {s}", .{
+            file.sub_file_path, @errorName(err),
+        });
+        return owner_decl.srcLoc(mod);
+    };
+    const node = owner_decl.relativeToNodeIndex(0);
+    var buf: [2]Ast.Node.Index = undefined;
+    if (tree.fullContainerDecl(&buf, node)) |container_decl| {
+        return queryFieldSrc(tree.*, query, file, container_decl);
+    } else {
+        // This type was generated using @Type
+        return owner_decl.srcLoc(mod);
+    }
+}
src/Sema.zig
@@ -2096,7 +2096,7 @@ fn failWithInvalidComptimeFieldStore(sema: *Sema, block: *Block, init_src: LazyS
         errdefer msg.destroy(sema.gpa);
 
         const struct_ty = mod.typeToStruct(container_ty) orelse break :msg msg;
-        const default_value_src = struct_ty.fieldSrcLoc(mod, .{
+        const default_value_src = mod.fieldSrcLoc(struct_ty.owner_decl, .{
             .index = field_index,
             .range = .value,
         });
@@ -2875,50 +2875,28 @@ fn zirEnumDecl(
         break :blk decls_len;
     } else 0;
 
-    var done = false;
-
-    var new_decl_arena = std.heap.ArenaAllocator.init(gpa);
-    errdefer if (!done) new_decl_arena.deinit();
-    const new_decl_arena_allocator = new_decl_arena.allocator();
+    // Because these three things each reference each other, `undefined`
+    // placeholders are used before being set after the enum type gains an
+    // InternPool index.
 
-    const enum_obj = try new_decl_arena_allocator.create(Module.EnumFull);
-    const enum_ty_payload = try new_decl_arena_allocator.create(Type.Payload.EnumFull);
-    enum_ty_payload.* = .{
-        .base = .{ .tag = if (small.nonexhaustive) .enum_nonexhaustive else .enum_full },
-        .data = enum_obj,
-    };
-    const enum_ty = Type.initPayload(&enum_ty_payload.base);
-    const enum_val = try Value.Tag.ty.create(new_decl_arena_allocator, enum_ty);
+    var done = false;
     const new_decl_index = try sema.createAnonymousDeclTypeNamed(block, src, .{
         .ty = Type.type,
-        .val = enum_val,
+        .val = undefined,
     }, 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);
 
-    enum_obj.* = .{
-        .owner_decl = new_decl_index,
-        .tag_ty = Type.null,
-        .tag_ty_inferred = true,
-        .fields = .{},
-        .values = .{},
-        .namespace = try mod.createNamespace(.{
-            .parent = block.namespace.toOptional(),
-            .ty = enum_ty,
-            .file_scope = block.getFileScope(mod),
-        }),
-    };
-
-    try new_decl.finalizeNewArena(&new_decl_arena);
-    const decl_val = try sema.analyzeDeclVal(block, src, new_decl_index);
-    done = true;
-
-    var decl_arena: std.heap.ArenaAllocator = undefined;
-    const decl_arena_allocator = new_decl.value_arena.?.acquire(gpa, &decl_arena);
-    defer new_decl.value_arena.?.release(&decl_arena);
+    const new_namespace_index = try mod.createNamespace(.{
+        .parent = block.namespace.toOptional(),
+        .ty = undefined,
+        .file_scope = block.getFileScope(mod),
+    });
+    const new_namespace = mod.namespacePtr(new_namespace_index);
+    errdefer if (!done) mod.destroyNamespace(new_namespace_index);
 
-    extra_index = try mod.scanNamespace(enum_obj.namespace, extra_index, decls_len, new_decl);
+    extra_index = try mod.scanNamespace(new_namespace_index, extra_index, decls_len, new_decl);
 
     const body = sema.code.extra[extra_index..][0..body_len];
     extra_index += body.len;
@@ -2927,7 +2905,31 @@ fn zirEnumDecl(
     const body_end = extra_index;
     extra_index += bit_bags_count;
 
-    {
+    const any_values = for (sema.code.extra[body_end..][0..bit_bags_count]) |bag| {
+        if (bag != 0) break true;
+    } else false;
+
+    const 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,
+    });
+    errdefer if (!done) mod.intern_pool.remove(incomplete_enum.index);
+
+    new_decl.val = incomplete_enum.index.toValue();
+    new_namespace.ty = incomplete_enum.index.toType();
+
+    const decl_val = try sema.analyzeDeclVal(block, src, new_decl_index);
+    done = true;
+
+    const int_tag_ty = ty: {
         // We create a block for the field type instructions because they
         // may need to reference Decls from inside the enum namespace.
         // Within the field type, default value, and alignment expressions, the "owner decl"
@@ -2957,7 +2959,7 @@ fn zirEnumDecl(
             .parent = null,
             .sema = sema,
             .src_decl = new_decl_index,
-            .namespace = enum_obj.namespace,
+            .namespace = new_namespace_index,
             .wip_capture_scope = wip_captures.scope,
             .instructions = .{},
             .inlining = null,
@@ -2976,35 +2978,22 @@ 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)});
             }
-            enum_obj.tag_ty = try ty.copy(decl_arena_allocator);
-            enum_obj.tag_ty_inferred = false;
+            incomplete_enum.setTagType(&mod.intern_pool, ty.ip_index);
+            break :ty ty;
         } else if (fields_len == 0) {
-            enum_obj.tag_ty = try mod.intType(.unsigned, 0);
-            enum_obj.tag_ty_inferred = true;
+            break :ty try mod.intType(.unsigned, 0);
         } else {
             const bits = std.math.log2_int_ceil(usize, fields_len);
-            enum_obj.tag_ty = try mod.intType(.unsigned, bits);
-            enum_obj.tag_ty_inferred = true;
+            break :ty try mod.intType(.unsigned, bits);
         }
-    }
+    };
 
-    if (small.nonexhaustive and enum_obj.tag_ty.zigTypeTag(mod) != .ComptimeInt) {
-        if (fields_len > 1 and std.math.log2_int(u64, fields_len) == enum_obj.tag_ty.bitSize(mod)) {
+    if (small.nonexhaustive and int_tag_ty.ip_index != .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", .{});
         }
     }
 
-    try enum_obj.fields.ensureTotalCapacity(decl_arena_allocator, fields_len);
-    const any_values = for (sema.code.extra[body_end..][0..bit_bags_count]) |bag| {
-        if (bag != 0) break true;
-    } else false;
-    if (any_values) {
-        try enum_obj.values.ensureTotalCapacityContext(decl_arena_allocator, fields_len, .{
-            .ty = enum_obj.tag_ty,
-            .mod = mod,
-        });
-    }
-
     var bit_bag_index: usize = body_end;
     var cur_bit_bag: u32 = undefined;
     var field_i: u32 = 0;
@@ -3023,15 +3012,12 @@ fn zirEnumDecl(
         // doc comment
         extra_index += 1;
 
-        // This string needs to outlive the ZIR code.
-        const field_name = try decl_arena_allocator.dupe(u8, field_name_zir);
-
-        const gop_field = enum_obj.fields.getOrPutAssumeCapacity(field_name);
-        if (gop_field.found_existing) {
-            const field_src = enum_obj.fieldSrcLoc(sema.mod, .{ .index = field_i }).lazy;
-            const other_field_src = enum_obj.fieldSrcLoc(sema.mod, .{ .index = gop_field.index }).lazy;
+        const field_name = try mod.intern_pool.getOrPutString(gpa, field_name_zir);
+        if (try incomplete_enum.addFieldName(&mod.intern_pool, gpa, field_name)) |other_index| {
+            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 msg = msg: {
-                const msg = try sema.errMsg(block, field_src, "duplicate enum field '{s}'", .{field_name});
+                const msg = try sema.errMsg(block, field_src, "duplicate enum field '{s}'", .{field_name_zir});
                 errdefer msg.destroy(gpa);
                 try sema.errNote(block, other_field_src, msg, "other field here", .{});
                 break :msg msg;
@@ -3045,7 +3031,7 @@ fn zirEnumDecl(
             const tag_inst = try sema.resolveInst(tag_val_ref);
             const tag_val = sema.resolveConstValue(block, .unneeded, tag_inst, "") catch |err| switch (err) {
                 error.NeededSourceLocation => {
-                    const value_src = enum_obj.fieldSrcLoc(sema.mod, .{
+                    const value_src = mod.fieldSrcLoc(new_decl_index, .{
                         .index = field_i,
                         .range = .value,
                     }).lazy;
@@ -3055,19 +3041,14 @@ fn zirEnumDecl(
                 else => |e| return e,
             };
             last_tag_val = tag_val;
-            const copied_tag_val = try tag_val.copy(decl_arena_allocator);
-            const gop_val = enum_obj.values.getOrPutAssumeCapacityContext(copied_tag_val, .{
-                .ty = enum_obj.tag_ty,
-                .mod = mod,
-            });
-            if (gop_val.found_existing) {
-                const value_src = enum_obj.fieldSrcLoc(sema.mod, .{
+            if (try incomplete_enum.addFieldValue(&mod.intern_pool, gpa, tag_val.ip_index)) |other_index| {
+                const value_src = mod.fieldSrcLoc(new_decl_index, .{
                     .index = field_i,
                     .range = .value,
                 }).lazy;
-                const other_field_src = enum_obj.fieldSrcLoc(sema.mod, .{ .index = gop_val.index }).lazy;
+                const other_field_src = mod.fieldSrcLoc(new_decl_index, .{ .index = other_index }).lazy;
                 const msg = msg: {
-                    const msg = try sema.errMsg(block, value_src, "enum tag value {} already taken", .{tag_val.fmtValue(enum_obj.tag_ty, sema.mod)});
+                    const msg = try sema.errMsg(block, value_src, "enum tag value {} already taken", .{tag_val.fmtValue(int_tag_ty, sema.mod)});
                     errdefer msg.destroy(gpa);
                     try sema.errNote(block, other_field_src, msg, "other occurrence here", .{});
                     break :msg msg;
@@ -3076,20 +3057,15 @@ fn zirEnumDecl(
             }
         } else if (any_values) {
             const tag_val = if (last_tag_val) |val|
-                try sema.intAdd(val, try mod.intValue(enum_obj.tag_ty, 1), enum_obj.tag_ty)
+                try sema.intAdd(val, try mod.intValue(int_tag_ty, 1), int_tag_ty)
             else
-                try mod.intValue(enum_obj.tag_ty, 0);
+                try mod.intValue(int_tag_ty, 0);
             last_tag_val = tag_val;
-            const copied_tag_val = try tag_val.copy(decl_arena_allocator);
-            const gop_val = enum_obj.values.getOrPutAssumeCapacityContext(copied_tag_val, .{
-                .ty = enum_obj.tag_ty,
-                .mod = mod,
-            });
-            if (gop_val.found_existing) {
-                const field_src = enum_obj.fieldSrcLoc(sema.mod, .{ .index = field_i }).lazy;
-                const other_field_src = enum_obj.fieldSrcLoc(sema.mod, .{ .index = gop_val.index }).lazy;
+            if (try incomplete_enum.addFieldValue(&mod.intern_pool, gpa, tag_val.ip_index)) |other_index| {
+                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 msg = msg: {
-                    const msg = try sema.errMsg(block, field_src, "enum tag value {} already taken", .{tag_val.fmtValue(enum_obj.tag_ty, sema.mod)});
+                    const msg = try sema.errMsg(block, field_src, "enum tag value {} already taken", .{tag_val.fmtValue(int_tag_ty, sema.mod)});
                     errdefer msg.destroy(gpa);
                     try sema.errNote(block, other_field_src, msg, "other occurrence here", .{});
                     break :msg msg;
@@ -3097,16 +3073,16 @@ fn zirEnumDecl(
                 return sema.failWithOwnedErrorMsg(msg);
             }
         } else {
-            last_tag_val = try mod.intValue(enum_obj.tag_ty, field_i);
+            last_tag_val = try mod.intValue(int_tag_ty, field_i);
         }
 
-        if (!(try sema.intFitsInType(last_tag_val.?, enum_obj.tag_ty, null))) {
-            const value_src = enum_obj.fieldSrcLoc(sema.mod, .{
+        if (!(try sema.intFitsInType(last_tag_val.?, int_tag_ty, null))) {
+            const value_src = mod.fieldSrcLoc(new_decl_index, .{
                 .index = field_i,
                 .range = if (has_tag_value) .value else .name,
             }).lazy;
             const msg = try sema.errMsg(block, value_src, "enumeration value '{}' too large for type '{}'", .{
-                last_tag_val.?.fmtValue(enum_obj.tag_ty, mod), enum_obj.tag_ty.fmt(mod),
+                last_tag_val.?.fmtValue(int_tag_ty, mod), int_tag_ty.fmt(mod),
             });
             return sema.failWithOwnedErrorMsg(msg);
         }
@@ -4356,7 +4332,7 @@ fn validateUnionInit(
     }
 
     const tag_ty = union_ty.unionTagTypeHypothetical(mod);
-    const enum_field_index = @intCast(u32, tag_ty.enumFieldIndex(field_name).?);
+    const enum_field_index = @intCast(u32, tag_ty.enumFieldIndex(field_name, mod).?);
     const tag_val = try Value.Tag.enum_field_index.create(sema.arena, enum_field_index);
 
     if (init_val) |val| {
@@ -8334,7 +8310,7 @@ fn zirIntToEnum(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!A
     _ = try sema.checkIntType(block, operand_src, sema.typeOf(operand));
 
     if (try sema.resolveMaybeUndefVal(operand)) |int_val| {
-        if (dest_ty.isNonexhaustiveEnum()) {
+        if (dest_ty.isNonexhaustiveEnum(mod)) {
             const int_tag_ty = try dest_ty.intTagType(mod);
             if (try sema.intFitsInType(int_val, int_tag_ty, null)) {
                 return sema.addConstant(dest_ty, int_val);
@@ -8383,7 +8359,7 @@ fn zirIntToEnum(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!A
 
     try sema.requireRuntimeBlock(block, src, operand_src);
     const result = try block.addTyOp(.intcast, dest_ty, operand);
-    if (block.wantSafety() and !dest_ty.isNonexhaustiveEnum() and
+    if (block.wantSafety() and !dest_ty.isNonexhaustiveEnum(mod) and
         sema.mod.backendSupportsFeature(.is_named_enum_value))
     {
         const ok = try block.addUnOp(.is_named_enum_value, result);
@@ -10518,7 +10494,7 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError
     var else_error_ty: ?Type = null;
 
     // Validate usage of '_' prongs.
-    if (special_prong == .under and (!operand_ty.isNonexhaustiveEnum() or union_originally)) {
+    if (special_prong == .under and (!operand_ty.isNonexhaustiveEnum(mod) or union_originally)) {
         const msg = msg: {
             const msg = try sema.errMsg(
                 block,
@@ -10543,8 +10519,8 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError
     switch (operand_ty.zigTypeTag(mod)) {
         .Union => unreachable, // handled in zirSwitchCond
         .Enum => {
-            seen_enum_fields = try gpa.alloc(?Module.SwitchProngSrc, operand_ty.enumFieldCount());
-            empty_enum = seen_enum_fields.len == 0 and !operand_ty.isNonexhaustiveEnum();
+            seen_enum_fields = try gpa.alloc(?Module.SwitchProngSrc, operand_ty.enumFieldCount(mod));
+            empty_enum = seen_enum_fields.len == 0 and !operand_ty.isNonexhaustiveEnum(mod);
             @memset(seen_enum_fields, null);
             // `range_set` is used for non-exhaustive enum values that do not correspond to any tags.
 
@@ -10599,7 +10575,7 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError
             } else true;
 
             if (special_prong == .@"else") {
-                if (all_tags_handled and !operand_ty.isNonexhaustiveEnum()) return sema.fail(
+                if (all_tags_handled and !operand_ty.isNonexhaustiveEnum(mod)) return sema.fail(
                     block,
                     special_prong_src,
                     "unreachable else prong; all cases already handled",
@@ -10617,7 +10593,7 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError
                     for (seen_enum_fields, 0..) |seen_src, i| {
                         if (seen_src != null) continue;
 
-                        const field_name = operand_ty.enumFieldName(i);
+                        const field_name = operand_ty.enumFieldName(i, mod);
                         try sema.addFieldErrNote(
                             operand_ty,
                             i,
@@ -10635,7 +10611,7 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError
                     break :msg msg;
                 };
                 return sema.failWithOwnedErrorMsg(msg);
-            } else if (special_prong == .none and operand_ty.isNonexhaustiveEnum() and !union_originally) {
+            } else if (special_prong == .none and operand_ty.isNonexhaustiveEnum(mod) and !union_originally) {
                 return sema.fail(
                     block,
                     src,
@@ -11159,7 +11135,7 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError
             return Air.Inst.Ref.unreachable_value;
         }
         if (mod.backendSupportsFeature(.is_named_enum_value) and block.wantSafety() and operand_ty.zigTypeTag(mod) == .Enum and
-            (!operand_ty.isNonexhaustiveEnum() or union_originally))
+            (!operand_ty.isNonexhaustiveEnum(mod) or union_originally))
         {
             try sema.zirDbgStmt(block, cond_dbg_node_index);
             const ok = try block.addUnOp(.is_named_enum_value, operand);
@@ -11489,7 +11465,7 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError
         var emit_bb = false;
         if (special.is_inline) switch (operand_ty.zigTypeTag(mod)) {
             .Enum => {
-                if (operand_ty.isNonexhaustiveEnum() and !union_originally) {
+                if (operand_ty.isNonexhaustiveEnum(mod) and !union_originally) {
                     return sema.fail(block, special_prong_src, "cannot enumerate values of type '{}' for 'inline else'", .{
                         operand_ty.fmt(mod),
                     });
@@ -11629,7 +11605,7 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError
         case_block.inline_case_capture = .none;
 
         if (mod.backendSupportsFeature(.is_named_enum_value) and special.body.len != 0 and block.wantSafety() and
-            operand_ty.zigTypeTag(mod) == .Enum and (!operand_ty.isNonexhaustiveEnum() or union_originally))
+            operand_ty.zigTypeTag(mod) == .Enum and (!operand_ty.isNonexhaustiveEnum(mod) or union_originally))
         {
             try sema.zirDbgStmt(&case_block, cond_dbg_node_index);
             const ok = try case_block.addUnOp(.is_named_enum_value, operand);
@@ -12081,7 +12057,7 @@ fn zirHasField(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Ai
         break :hf switch (ty.zigTypeTag(mod)) {
             .Struct => ty.structFields(mod).contains(field_name),
             .Union => ty.unionFields(mod).contains(field_name),
-            .Enum => ty.enumFields().contains(field_name),
+            .Enum => ty.enumFieldIndex(field_name, mod) != null,
             .Array => mem.eql(u8, field_name, "len"),
             else => return sema.fail(block, ty_src, "type '{}' does not support '@hasField'", .{
                 ty.fmt(sema.mod),
@@ -16300,9 +16276,9 @@ fn zirTypeInfo(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Ai
         },
         .Enum => {
             // TODO: look into memoizing this result.
-            const int_tag_ty = try ty.intTagType(mod);
+            const enum_type = mod.intern_pool.indexToKey(ty.ip_index).enum_type;
 
-            const is_exhaustive = Value.makeBool(!ty.isNonexhaustiveEnum());
+            const is_exhaustive = Value.makeBool(enum_type.tag_mode != .nonexhaustive);
 
             var fields_anon_decl = try block.startAnonDecl();
             defer fields_anon_decl.deinit();
@@ -16320,25 +16296,17 @@ fn zirTypeInfo(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Ai
                 break :t try enum_field_ty_decl.val.toType().copy(fields_anon_decl.arena());
             };
 
-            const enum_fields = ty.enumFields();
-            const enum_field_vals = try fields_anon_decl.arena().alloc(Value, enum_fields.count());
+            const enum_field_vals = try fields_anon_decl.arena().alloc(Value, enum_type.names.len);
 
             for (enum_field_vals, 0..) |*field_val, i| {
-                var tag_val_payload: Value.Payload.U32 = .{
-                    .base = .{ .tag = .enum_field_index },
-                    .data = @intCast(u32, i),
-                };
-                const tag_val = Value.initPayload(&tag_val_payload.base);
-
-                const int_val = try tag_val.enumToInt(ty, mod);
-
-                const name = enum_fields.keys()[i];
+                const name_ip = enum_type.names[i];
+                const name = mod.intern_pool.stringToSlice(name_ip);
                 const name_val = v: {
                     var anon_decl = try block.startAnonDecl();
                     defer anon_decl.deinit();
                     const bytes = try anon_decl.arena().dupeZ(u8, name);
                     const new_decl = try anon_decl.finish(
-                        try Type.array(anon_decl.arena(), bytes.len, try mod.intValue(Type.u8, 0), Type.u8, mod),
+                        try Type.array(anon_decl.arena(), bytes.len, Value.zero_u8, Type.u8, mod),
                         try Value.Tag.bytes.create(anon_decl.arena(), bytes[0 .. bytes.len + 1]),
                         0, // default alignment
                     );
@@ -16350,7 +16318,7 @@ fn zirTypeInfo(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Ai
                     // name: []const u8,
                     name_val,
                     // value: comptime_int,
-                    int_val,
+                    try mod.intValue(Type.comptime_int, i),
                 };
                 field_val.* = try Value.Tag.aggregate.create(fields_anon_decl.arena(), enum_field_fields);
             }
@@ -16370,12 +16338,12 @@ fn zirTypeInfo(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Ai
                 break :v try Value.Tag.decl_ref.create(sema.arena, new_decl);
             };
 
-            const decls_val = try sema.typeInfoDecls(block, src, type_info_ty, ty.getNamespace(mod));
+            const decls_val = try sema.typeInfoDecls(block, src, type_info_ty, enum_type.namespace);
 
             const field_values = try sema.arena.create([4]Value);
             field_values.* = .{
                 // tag_type: type,
-                try Value.Tag.ty.create(sema.arena, int_tag_ty),
+                enum_type.tag_ty.toValue(),
                 // fields: []const EnumField,
                 fields_val,
                 // decls: []const Declaration,
@@ -16468,7 +16436,7 @@ fn zirTypeInfo(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Ai
                 });
             };
 
-            const decls_val = try sema.typeInfoDecls(block, src, type_info_ty, union_ty.getNamespace(mod));
+            const decls_val = try sema.typeInfoDecls(block, src, type_info_ty, union_ty.getNamespaceIndex(mod));
 
             const enum_tag_ty_val = if (union_ty.unionTagType(mod)) |tag_ty| v: {
                 const ty_val = try Value.Tag.ty.create(sema.arena, tag_ty);
@@ -16631,7 +16599,7 @@ fn zirTypeInfo(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Ai
                 });
             };
 
-            const decls_val = try sema.typeInfoDecls(block, src, type_info_ty, struct_ty.getNamespace(mod));
+            const decls_val = try sema.typeInfoDecls(block, src, type_info_ty, struct_ty.getNamespaceIndex(mod));
 
             const backing_integer_val = blk: {
                 if (layout == .Packed) {
@@ -16674,7 +16642,7 @@ fn zirTypeInfo(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Ai
             // TODO: look into memoizing this result.
 
             const opaque_ty = try sema.resolveTypeFields(ty);
-            const decls_val = try sema.typeInfoDecls(block, src, type_info_ty, opaque_ty.getNamespace(mod));
+            const decls_val = try sema.typeInfoDecls(block, src, type_info_ty, opaque_ty.getNamespaceIndex(mod));
 
             const field_values = try sema.arena.create([1]Value);
             field_values.* = .{
@@ -16700,7 +16668,7 @@ fn typeInfoDecls(
     block: *Block,
     src: LazySrcLoc,
     type_info_ty: Type,
-    opt_namespace: ?*Module.Namespace,
+    opt_namespace: Module.Namespace.OptionalIndex,
 ) CompileError!Value {
     const mod = sema.mod;
     var decls_anon_decl = try block.startAnonDecl();
@@ -16726,8 +16694,9 @@ fn typeInfoDecls(
     var seen_namespaces = std.AutoHashMap(*Namespace, void).init(sema.gpa);
     defer seen_namespaces.deinit();
 
-    if (opt_namespace) |some| {
-        try sema.typeInfoNamespaceDecls(block, decls_anon_decl.arena(), some, &decl_vals, &seen_namespaces);
+    if (opt_namespace.unwrap()) |namespace_index| {
+        const namespace = mod.namespacePtr(namespace_index);
+        try sema.typeInfoNamespaceDecls(block, decls_anon_decl.arena(), namespace, &decl_vals, &seen_namespaces);
     }
 
     const new_decl = try decls_anon_decl.finish(
@@ -17896,7 +17865,7 @@ fn unionInit(
 
     if (try sema.resolveMaybeUndefVal(init)) |init_val| {
         const tag_ty = union_ty.unionTagTypeHypothetical(mod);
-        const enum_field_index = @intCast(u32, tag_ty.enumFieldIndex(field_name).?);
+        const enum_field_index = @intCast(u32, tag_ty.enumFieldIndex(field_name, mod).?);
         const tag_val = try Value.Tag.enum_field_index.create(sema.arena, enum_field_index);
         return sema.addConstant(union_ty, try Value.Tag.@"union".create(sema.arena, .{
             .tag = tag_val,
@@ -17997,7 +17966,7 @@ fn zirStructInit(
         const field_name = sema.code.nullTerminatedString(field_type_extra.name_start);
         const field_index = try sema.unionFieldIndex(block, resolved_ty, field_name, field_src);
         const tag_ty = resolved_ty.unionTagTypeHypothetical(mod);
-        const enum_field_index = @intCast(u32, tag_ty.enumFieldIndex(field_name).?);
+        const enum_field_index = @intCast(u32, tag_ty.enumFieldIndex(field_name, mod).?);
         const tag_val = try Value.Tag.enum_field_index.create(sema.arena, enum_field_index);
 
         const init_inst = try sema.resolveInst(item.data.init);
@@ -18754,7 +18723,7 @@ fn zirTagName(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air
             operand_ty.fmt(mod),
         }),
     };
-    if (enum_ty.enumFieldCount() == 0) {
+    if (enum_ty.enumFieldCount(mod) == 0) {
         // TODO I don't think this is the correct way to handle this but
         // it prevents a crash.
         return sema.fail(block, operand_src, "cannot get @tagName of empty enum '{}'", .{
@@ -18776,7 +18745,7 @@ fn zirTagName(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air
             };
             return sema.failWithOwnedErrorMsg(msg);
         };
-        const field_name = enum_ty.enumFieldName(field_index);
+        const field_name = enum_ty.enumFieldName(field_index, mod);
         return sema.addStrLit(block, field_name);
     }
     try sema.requireRuntimeBlock(block, src, operand_src);
@@ -19081,63 +19050,41 @@ fn zirReify(
                 return sema.fail(block, src, "reified enums must have no decls", .{});
             }
 
-            var new_decl_arena = std.heap.ArenaAllocator.init(gpa);
-            errdefer new_decl_arena.deinit();
-            const new_decl_arena_allocator = new_decl_arena.allocator();
+            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.
 
-            // Define our empty enum decl
-            const enum_obj = try new_decl_arena_allocator.create(Module.EnumFull);
-            const enum_ty_payload = try new_decl_arena_allocator.create(Type.Payload.EnumFull);
-            enum_ty_payload.* = .{
-                .base = .{
-                    .tag = if (!is_exhaustive_val.toBool(mod))
-                        .enum_nonexhaustive
-                    else
-                        .enum_full,
-                },
-                .data = enum_obj,
-            };
-            const enum_ty = Type.initPayload(&enum_ty_payload.base);
-            const enum_val = try Value.Tag.ty.create(new_decl_arena_allocator, enum_ty);
             const new_decl_index = try sema.createAnonymousDeclTypeNamed(block, src, .{
                 .ty = Type.type,
-                .val = enum_val,
+                .val = undefined,
             }, name_strategy, "enum", inst);
             const new_decl = mod.declPtr(new_decl_index);
             new_decl.owns_tv = true;
             errdefer mod.abortAnonDecl(new_decl_index);
 
-            enum_obj.* = .{
-                .owner_decl = new_decl_index,
-                .tag_ty = Type.null,
-                .tag_ty_inferred = false,
-                .fields = .{},
-                .values = .{},
-                .namespace = try mod.createNamespace(.{
-                    .parent = block.namespace.toOptional(),
-                    .ty = enum_ty,
-                    .file_scope = block.getFileScope(mod),
-                }),
-            };
-
-            // Enum tag type
-            const int_tag_ty = try tag_type_val.toType().copy(new_decl_arena_allocator);
-
-            if (int_tag_ty.zigTypeTag(mod) != .Int) {
-                return sema.fail(block, src, "Type.Enum.tag_type must be an integer type", .{});
-            }
-            enum_obj.tag_ty = int_tag_ty;
-
-            // Fields
-            const fields_len = try sema.usizeCast(block, src, fields_val.sliceLen(mod));
-            try enum_obj.fields.ensureTotalCapacity(new_decl_arena_allocator, fields_len);
-            try enum_obj.values.ensureTotalCapacityContext(new_decl_arena_allocator, fields_len, .{
-                .ty = enum_obj.tag_ty,
-                .mod = mod,
+            // Define our empty enum decl
+            const fields_len = @intCast(u32, try sema.usizeCast(block, src, fields_val.sliceLen(mod)));
+            const incomplete_enum = try mod.intern_pool.getIncompleteEnum(gpa, .{
+                .decl = new_decl_index,
+                .namespace = .none,
+                .fields_len = fields_len,
+                .has_values = true,
+                .tag_mode = if (!is_exhaustive_val.toBool(mod))
+                    .nonexhaustive
+                else
+                    .explicit,
+                .tag_ty = int_tag_ty.ip_index,
             });
+            errdefer mod.intern_pool.remove(incomplete_enum.index);
 
-            var field_i: usize = 0;
-            while (field_i < fields_len) : (field_i += 1) {
+            new_decl.val = incomplete_enum.index.toValue();
+
+            for (0..fields_len) |field_i| {
                 const elem_val = try fields_val.elemValue(mod, field_i);
                 const field_struct_val: []const Value = elem_val.castTag(.aggregate).?.data;
                 // TODO use reflection instead of magic numbers here
@@ -19148,39 +19095,36 @@ fn zirReify(
 
                 const field_name = try name_val.toAllocatedBytes(
                     Type.const_slice_u8,
-                    new_decl_arena_allocator,
+                    sema.arena,
                     mod,
                 );
+                const field_name_ip = try mod.intern_pool.getOrPutString(gpa, field_name);
 
-                if (!try sema.intFitsInType(value_val, enum_obj.tag_ty, null)) {
+                if (!try sema.intFitsInType(value_val, int_tag_ty, null)) {
                     // TODO: better source location
                     return sema.fail(block, src, "field '{s}' with enumeration value '{}' is too large for backing int type '{}'", .{
                         field_name,
                         value_val.fmtValue(Type.comptime_int, mod),
-                        enum_obj.tag_ty.fmt(mod),
+                        int_tag_ty.fmt(mod),
                     });
                 }
 
-                const gop_field = enum_obj.fields.getOrPutAssumeCapacity(field_name);
-                if (gop_field.found_existing) {
+                if (try incomplete_enum.addFieldName(&mod.intern_pool, gpa, field_name_ip)) |other_index| {
                     const msg = msg: {
                         const msg = try sema.errMsg(block, src, "duplicate enum field '{s}'", .{field_name});
                         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(msg);
                 }
 
-                const copied_tag_val = try value_val.copy(new_decl_arena_allocator);
-                const gop_val = enum_obj.values.getOrPutAssumeCapacityContext(copied_tag_val, .{
-                    .ty = enum_obj.tag_ty,
-                    .mod = mod,
-                });
-                if (gop_val.found_existing) {
+                if (try incomplete_enum.addFieldValue(&mod.intern_pool, gpa, value_val.ip_index)) |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;
                     };
@@ -19188,7 +19132,6 @@ fn zirReify(
                 }
             }
 
-            try new_decl.finalizeNewArena(&new_decl_arena);
             return sema.analyzeDeclVal(block, src, new_decl_index);
         },
         .Opaque => {
@@ -19307,26 +19250,29 @@ fn zirReify(
             new_namespace.ty = union_ty.toType();
 
             // Tag type
-            var tag_ty_field_names: ?Module.EnumFull.NameMap = null;
-            var enum_field_names: ?*Module.EnumNumbered.NameMap = null;
             const fields_len = try sema.usizeCast(block, src, fields_val.sliceLen(mod));
+            var explicit_tags_seen: []bool = &.{};
+            var explicit_enum_info: ?InternPool.Key.EnumType = null;
+            var enum_field_names: []InternPool.NullTerminatedString = &.{};
             if (tag_type_val.optionalValue(mod)) |payload_val| {
-                union_obj.tag_ty = try payload_val.toType().copy(new_decl_arena_allocator);
+                union_obj.tag_ty = payload_val.toType();
 
-                if (union_obj.tag_ty.zigTypeTag(mod) != .Enum) {
-                    return sema.fail(block, src, "Type.Union.tag_type must be an enum type", .{});
-                }
-                tag_ty_field_names = try union_obj.tag_ty.enumFields().clone(sema.arena);
+                const enum_type = switch (mod.intern_pool.indexToKey(union_obj.tag_ty.ip_index)) {
+                    .enum_type => |x| x,
+                    else => return sema.fail(block, src, "Type.Union.tag_type must be an enum type", .{}),
+                };
+
+                explicit_enum_info = enum_type;
+                explicit_tags_seen = try sema.arena.alloc(bool, enum_type.names.len);
+                @memset(explicit_tags_seen, false);
             } else {
-                union_obj.tag_ty = try sema.generateUnionTagTypeSimple(block, fields_len, null);
-                enum_field_names = &union_obj.tag_ty.castTag(.enum_simple).?.data.fields;
+                enum_field_names = try sema.arena.alloc(InternPool.NullTerminatedString, fields_len);
             }
 
             // Fields
             try union_obj.fields.ensureTotalCapacity(new_decl_arena_allocator, fields_len);
 
-            var i: usize = 0;
-            while (i < fields_len) : (i += 1) {
+            for (0..fields_len) |i| {
                 const elem_val = try fields_val.elemValue(mod, i);
                 const field_struct_val = elem_val.castTag(.aggregate).?.data;
                 // TODO use reflection instead of magic numbers here
@@ -19343,13 +19289,14 @@ fn zirReify(
                     mod,
                 );
 
-                if (enum_field_names) |set| {
-                    set.putAssumeCapacity(field_name, {});
+                const field_name_ip = try mod.intern_pool.getOrPutString(gpa, field_name);
+
+                if (enum_field_names.len != 0) {
+                    enum_field_names[i] = field_name_ip;
                 }
 
-                if (tag_ty_field_names) |*names| {
-                    const enum_has_field = names.orderedRemove(field_name);
-                    if (!enum_has_field) {
+                if (explicit_enum_info) |tag_info| {
+                    const enum_index = tag_info.nameIndex(mod.intern_pool, field_name_ip) orelse {
                         const msg = msg: {
                             const msg = try sema.errMsg(block, src, "no field named '{s}' in enum '{}'", .{ field_name, union_obj.tag_ty.fmt(mod) });
                             errdefer msg.destroy(gpa);
@@ -19357,7 +19304,11 @@ fn zirReify(
                             break :msg msg;
                         };
                         return sema.failWithOwnedErrorMsg(msg);
-                    }
+                    };
+                    // 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 = union_obj.fields.getOrPutAssumeCapacity(field_name);
@@ -19409,22 +19360,26 @@ fn zirReify(
                 }
             }
 
-            if (tag_ty_field_names) |names| {
-                if (names.count() > 0) {
+            if (explicit_enum_info) |tag_info| {
+                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);
 
                         const enum_ty = union_obj.tag_ty;
-                        for (names.keys()) |field_name| {
-                            const field_index = enum_ty.enumFieldIndex(field_name).?;
-                            try sema.addFieldErrNote(enum_ty, field_index, msg, "field '{s}' missing, declared here", .{field_name});
+                        for (tag_info.names, 0..) |field_name, field_index| {
+                            if (explicit_tags_seen[field_index]) continue;
+                            try sema.addFieldErrNote(enum_ty, field_index, msg, "field '{s}' missing, declared here", .{
+                                mod.intern_pool.stringToSlice(field_name),
+                            });
                         }
                         try sema.addDeclaredHereNote(msg, union_obj.tag_ty);
                         break :msg msg;
                     };
                     return sema.failWithOwnedErrorMsg(msg);
                 }
+            } else {
+                union_obj.tag_ty = try sema.generateUnionTagTypeSimple(block, enum_field_names, null);
             }
 
             try new_decl.finalizeNewArena(&new_decl_arena);
@@ -23450,7 +23405,7 @@ fn explainWhyTypeIsComptimeInner(
 
             if (mod.typeToStruct(ty)) |struct_obj| {
                 for (struct_obj.fields.values(), 0..) |field, i| {
-                    const field_src_loc = struct_obj.fieldSrcLoc(sema.mod, .{
+                    const field_src_loc = mod.fieldSrcLoc(struct_obj.owner_decl, .{
                         .index = i,
                         .range = .type,
                     });
@@ -23469,7 +23424,7 @@ fn explainWhyTypeIsComptimeInner(
 
             if (mod.typeToUnion(ty)) |union_obj| {
                 for (union_obj.fields.values(), 0..) |field, i| {
-                    const field_src_loc = union_obj.fieldSrcLoc(sema.mod, .{
+                    const field_src_loc = mod.fieldSrcLoc(union_obj.owner_decl, .{
                         .index = i,
                         .range = .type,
                     });
@@ -24168,7 +24123,7 @@ fn fieldVal(
                     }
                     const union_ty = try sema.resolveTypeFields(child_type);
                     if (union_ty.unionTagType(mod)) |enum_ty| {
-                        if (enum_ty.enumFieldIndex(field_name)) |field_index_usize| {
+                        if (enum_ty.enumFieldIndex(field_name, mod)) |field_index_usize| {
                             const field_index = @intCast(u32, field_index_usize);
                             return sema.addConstant(
                                 enum_ty,
@@ -24184,7 +24139,7 @@ fn fieldVal(
                             return inst;
                         }
                     }
-                    const field_index_usize = child_type.enumFieldIndex(field_name) orelse
+                    const field_index_usize = child_type.enumFieldIndex(field_name, mod) orelse
                         return sema.failWithBadMemberAccess(block, child_type, field_name_src, field_name);
                     const field_index = @intCast(u32, field_index_usize);
                     const enum_val = try Value.Tag.enum_field_index.create(arena, field_index);
@@ -24382,7 +24337,7 @@ fn fieldPtr(
                     }
                     const union_ty = try sema.resolveTypeFields(child_type);
                     if (union_ty.unionTagType(mod)) |enum_ty| {
-                        if (enum_ty.enumFieldIndex(field_name)) |field_index| {
+                        if (enum_ty.enumFieldIndex(field_name, mod)) |field_index| {
                             const field_index_u32 = @intCast(u32, field_index);
                             var anon_decl = try block.startAnonDecl();
                             defer anon_decl.deinit();
@@ -24401,7 +24356,7 @@ fn fieldPtr(
                             return inst;
                         }
                     }
-                    const field_index = child_type.enumFieldIndex(field_name) orelse {
+                    const field_index = child_type.enumFieldIndex(field_name, mod) orelse {
                         return sema.failWithBadMemberAccess(block, child_type, field_name_src, field_name);
                     };
                     const field_index_u32 = @intCast(u32, field_index);
@@ -24996,7 +24951,7 @@ fn unionFieldPtr(
         .@"volatile" = union_ptr_ty.isVolatilePtr(mod),
         .@"addrspace" = union_ptr_ty.ptrAddressSpace(mod),
     });
-    const enum_field_index = @intCast(u32, union_obj.tag_ty.enumFieldIndex(field_name).?);
+    const enum_field_index = @intCast(u32, union_obj.tag_ty.enumFieldIndex(field_name, mod).?);
 
     if (initializing and field.ty.zigTypeTag(mod) == .NoReturn) {
         const msg = msg: {
@@ -25028,7 +24983,7 @@ fn unionFieldPtr(
                 if (!tag_matches) {
                     const msg = msg: {
                         const active_index = tag_and_val.tag.castTag(.enum_field_index).?.data;
-                        const active_field_name = union_obj.tag_ty.enumFieldName(active_index);
+                        const active_field_name = union_obj.tag_ty.enumFieldName(active_index, mod);
                         const msg = try sema.errMsg(block, src, "access of union field '{s}' while field '{s}' is active", .{ field_name, active_field_name });
                         errdefer msg.destroy(sema.gpa);
                         try sema.addDeclaredHereNote(msg, union_ty);
@@ -25083,7 +25038,7 @@ fn unionFieldVal(
     const union_obj = mod.typeToUnion(union_ty).?;
     const field_index = try sema.unionFieldIndex(block, union_ty, field_name, field_name_src);
     const field = union_obj.fields.values()[field_index];
-    const enum_field_index = @intCast(u32, union_obj.tag_ty.enumFieldIndex(field_name).?);
+    const enum_field_index = @intCast(u32, union_obj.tag_ty.enumFieldIndex(field_name, mod).?);
 
     if (try sema.resolveMaybeUndefVal(union_byval)) |union_val| {
         if (union_val.isUndef()) return sema.addConstUndef(field.ty);
@@ -25102,7 +25057,7 @@ fn unionFieldVal(
                 } else {
                     const msg = msg: {
                         const active_index = tag_and_val.tag.castTag(.enum_field_index).?.data;
-                        const active_field_name = union_obj.tag_ty.enumFieldName(active_index);
+                        const active_field_name = union_obj.tag_ty.enumFieldName(active_index, mod);
                         const msg = try sema.errMsg(block, src, "access of union field '{s}' while field '{s}' is active", .{ field_name, active_field_name });
                         errdefer msg.destroy(sema.gpa);
                         try sema.addDeclaredHereNote(msg, union_ty);
@@ -26191,7 +26146,7 @@ fn coerceExtra(
                 // enum literal to enum
                 const val = try sema.resolveConstValue(block, .unneeded, inst, "");
                 const bytes = val.castTag(.enum_literal).?.data;
-                const field_index = dest_ty.enumFieldIndex(bytes) orelse {
+                const field_index = dest_ty.enumFieldIndex(bytes, mod) orelse {
                     const msg = msg: {
                         const msg = try sema.errMsg(
                             block,
@@ -28707,7 +28662,7 @@ fn coerceEnumToUnion(
 
     try sema.requireRuntimeBlock(block, inst_src, null);
 
-    if (tag_ty.isNonexhaustiveEnum()) {
+    if (tag_ty.isNonexhaustiveEnum(mod)) {
         const msg = msg: {
             const msg = try sema.errMsg(block, inst_src, "runtime coercion to union '{}' from non-exhaustive enum", .{
                 union_ty.fmt(sema.mod),
@@ -31605,7 +31560,6 @@ pub fn resolveTypeRequiresComptime(sema: *Sema, ty: Type) CompileError!bool {
             .error_set_single,
             .error_set_inferred,
             .error_set_merged,
-            .enum_simple,
             => false,
 
             .function => true,
@@ -31646,14 +31600,6 @@ pub fn resolveTypeRequiresComptime(sema: *Sema, ty: Type) CompileError!bool {
                 const child_ty = ty.castTag(.anyframe_T).?.data;
                 return sema.resolveTypeRequiresComptime(child_ty);
             },
-            .enum_numbered => {
-                const tag_ty = ty.castTag(.enum_numbered).?.data.tag_ty;
-                return sema.resolveTypeRequiresComptime(tag_ty);
-            },
-            .enum_full, .enum_nonexhaustive => {
-                const tag_ty = ty.cast(Type.Payload.EnumFull).?.data.tag_ty;
-                return sema.resolveTypeRequiresComptime(tag_ty);
-            },
         },
         else => switch (mod.intern_pool.indexToKey(ty.ip_index)) {
             .int_type => false,
@@ -31760,7 +31706,7 @@ pub fn resolveTypeRequiresComptime(sema: *Sema, ty: Type) CompileError!bool {
 
             .opaque_type => false,
 
-            .enum_type => @panic("TODO"),
+            .enum_type => |enum_type| try sema.resolveTypeRequiresComptime(enum_type.tag_ty.toType()),
 
             // values, not types
             .un => unreachable,
@@ -32284,12 +32230,12 @@ fn semaStructFields(mod: *Module, struct_obj: *Module.Struct) CompileError!void
             const gop = struct_obj.fields.getOrPutAssumeCapacity(field_name);
             if (gop.found_existing) {
                 const msg = msg: {
-                    const field_src = struct_obj.fieldSrcLoc(sema.mod, .{ .index = field_i }).lazy;
+                    const field_src = mod.fieldSrcLoc(struct_obj.owner_decl, .{ .index = field_i }).lazy;
                     const msg = try sema.errMsg(&block_scope, field_src, "duplicate struct field: '{s}'", .{field_name});
                     errdefer msg.destroy(gpa);
 
                     const prev_field_index = struct_obj.fields.getIndex(field_name).?;
-                    const prev_field_src = struct_obj.fieldSrcLoc(sema.mod, .{ .index = prev_field_index });
+                    const prev_field_src = mod.fieldSrcLoc(struct_obj.owner_decl, .{ .index = prev_field_index });
                     try sema.mod.errNoteNonLazy(prev_field_src, msg, "other field here", .{});
                     try sema.errNote(&block_scope, src, msg, "struct declared here", .{});
                     break :msg msg;
@@ -32325,7 +32271,7 @@ fn semaStructFields(mod: *Module, struct_obj: *Module.Struct) CompileError!void
             if (zir_field.type_ref != .none) {
                 break :ty sema.resolveType(&block_scope, .unneeded, zir_field.type_ref) catch |err| switch (err) {
                     error.NeededSourceLocation => {
-                        const ty_src = struct_obj.fieldSrcLoc(sema.mod, .{
+                        const ty_src = mod.fieldSrcLoc(struct_obj.owner_decl, .{
                             .index = field_i,
                             .range = .type,
                         }).lazy;
@@ -32341,7 +32287,7 @@ fn semaStructFields(mod: *Module, struct_obj: *Module.Struct) CompileError!void
             const ty_ref = try sema.resolveBody(&block_scope, body, struct_obj.zir_index);
             break :ty sema.analyzeAsType(&block_scope, .unneeded, ty_ref) catch |err| switch (err) {
                 error.NeededSourceLocation => {
-                    const ty_src = struct_obj.fieldSrcLoc(sema.mod, .{
+                    const ty_src = mod.fieldSrcLoc(struct_obj.owner_decl, .{
                         .index = field_i,
                         .range = .type,
                     }).lazy;
@@ -32360,7 +32306,7 @@ fn semaStructFields(mod: *Module, struct_obj: *Module.Struct) CompileError!void
 
         if (field_ty.zigTypeTag(mod) == .Opaque) {
             const msg = msg: {
-                const ty_src = struct_obj.fieldSrcLoc(sema.mod, .{
+                const ty_src = mod.fieldSrcLoc(struct_obj.owner_decl, .{
                     .index = field_i,
                     .range = .type,
                 }).lazy;
@@ -32374,7 +32320,7 @@ fn semaStructFields(mod: *Module, struct_obj: *Module.Struct) CompileError!void
         }
         if (field_ty.zigTypeTag(mod) == .NoReturn) {
             const msg = msg: {
-                const ty_src = struct_obj.fieldSrcLoc(sema.mod, .{
+                const ty_src = mod.fieldSrcLoc(struct_obj.owner_decl, .{
                     .index = field_i,
                     .range = .type,
                 }).lazy;
@@ -32388,7 +32334,7 @@ fn semaStructFields(mod: *Module, struct_obj: *Module.Struct) CompileError!void
         }
         if (struct_obj.layout == .Extern and !try sema.validateExternType(field.ty, .struct_field)) {
             const msg = msg: {
-                const ty_src = struct_obj.fieldSrcLoc(sema.mod, .{
+                const ty_src = mod.fieldSrcLoc(struct_obj.owner_decl, .{
                     .index = field_i,
                     .range = .type,
                 });
@@ -32403,7 +32349,7 @@ fn semaStructFields(mod: *Module, struct_obj: *Module.Struct) CompileError!void
             return sema.failWithOwnedErrorMsg(msg);
         } else if (struct_obj.layout == .Packed and !(validatePackedType(field.ty, mod))) {
             const msg = msg: {
-                const ty_src = struct_obj.fieldSrcLoc(sema.mod, .{
+                const ty_src = mod.fieldSrcLoc(struct_obj.owner_decl, .{
                     .index = field_i,
                     .range = .type,
                 });
@@ -32424,7 +32370,7 @@ fn semaStructFields(mod: *Module, struct_obj: *Module.Struct) CompileError!void
             const align_ref = try sema.resolveBody(&block_scope, body, struct_obj.zir_index);
             field.abi_align = sema.analyzeAsAlign(&block_scope, .unneeded, align_ref) catch |err| switch (err) {
                 error.NeededSourceLocation => {
-                    const align_src = struct_obj.fieldSrcLoc(sema.mod, .{
+                    const align_src = mod.fieldSrcLoc(struct_obj.owner_decl, .{
                         .index = field_i,
                         .range = .alignment,
                     }).lazy;
@@ -32452,7 +32398,7 @@ fn semaStructFields(mod: *Module, struct_obj: *Module.Struct) CompileError!void
                 const field = &struct_obj.fields.values()[field_i];
                 const coerced = sema.coerce(&block_scope, field.ty, init, .unneeded) catch |err| switch (err) {
                     error.NeededSourceLocation => {
-                        const init_src = struct_obj.fieldSrcLoc(sema.mod, .{
+                        const init_src = mod.fieldSrcLoc(struct_obj.owner_decl, .{
                             .index = field_i,
                             .range = .value,
                         }).lazy;
@@ -32462,7 +32408,7 @@ fn semaStructFields(mod: *Module, struct_obj: *Module.Struct) CompileError!void
                     else => |e| return e,
                 };
                 const default_val = (try sema.resolveMaybeUndefVal(coerced)) orelse {
-                    const init_src = struct_obj.fieldSrcLoc(sema.mod, .{
+                    const init_src = mod.fieldSrcLoc(struct_obj.owner_decl, .{
                         .index = field_i,
                         .range = .value,
                     }).lazy;
@@ -32573,9 +32519,11 @@ fn semaUnionFields(mod: *Module, union_obj: *Module.Union) CompileError!void {
     try union_obj.fields.ensureTotalCapacity(decl_arena_allocator, fields_len);
 
     var int_tag_ty: Type = undefined;
-    var enum_field_names: ?*Module.EnumNumbered.NameMap = null;
-    var enum_value_map: ?*Module.EnumNumbered.ValueMap = null;
-    var tag_ty_field_names: ?Module.EnumFull.NameMap = null;
+    var enum_field_names: []InternPool.NullTerminatedString = &.{};
+    var enum_field_vals: []InternPool.Index = &.{};
+    var enum_field_vals_map: std.ArrayHashMapUnmanaged(Value, void, Value.ArrayHashContext, false) = .{};
+    var explicit_tags_seen: []bool = &.{};
+    var explicit_enum_info: ?InternPool.Key.EnumType = null;
     if (tag_type_ref != .none) {
         const tag_ty_src: LazySrcLoc = .{ .node_offset_container_tag = src.node_offset.x };
         const provided_ty = try sema.resolveType(&block_scope, tag_ty_src, tag_type_ref);
@@ -32601,27 +32549,26 @@ fn semaUnionFields(mod: *Module, union_obj: *Module.Union) CompileError!void {
                     return sema.failWithOwnedErrorMsg(msg);
                 }
             }
-            union_obj.tag_ty = try sema.generateUnionTagTypeNumbered(&block_scope, fields_len, provided_ty, union_obj);
-            const enum_obj = union_obj.tag_ty.castTag(.enum_numbered).?.data;
-            enum_field_names = &enum_obj.fields;
-            enum_value_map = &enum_obj.values;
+            enum_field_names = try sema.arena.alloc(InternPool.NullTerminatedString, fields_len);
+            enum_field_vals = try sema.arena.alloc(InternPool.Index, fields_len);
         } else {
             // The provided type is the enum tag type.
-            union_obj.tag_ty = try provided_ty.copy(decl_arena_allocator);
-            if (union_obj.tag_ty.zigTypeTag(mod) != .Enum) {
-                return sema.fail(&block_scope, tag_ty_src, "expected enum tag type, found '{}'", .{union_obj.tag_ty.fmt(sema.mod)});
-            }
+            union_obj.tag_ty = provided_ty;
+            const enum_type = switch (mod.intern_pool.indexToKey(union_obj.tag_ty.ip_index)) {
+                .enum_type => |x| x,
+                else => return sema.fail(&block_scope, tag_ty_src, "expected enum tag type, found '{}'", .{union_obj.tag_ty.fmt(sema.mod)}),
+            };
             // The fields of the union must match the enum exactly.
-            // Store a copy of the enum field names so we can check for
-            // missing or extraneous fields later.
-            tag_ty_field_names = try union_obj.tag_ty.enumFields().clone(sema.arena);
+            // A flag per field is used to check for missing and extraneous fields.
+            explicit_enum_info = enum_type;
+            explicit_tags_seen = try sema.arena.alloc(bool, enum_type.names.len);
+            @memset(explicit_tags_seen, false);
         }
     } else {
         // If auto_enum_tag is false, this is an untagged union. However, for semantic analysis
         // purposes, we still auto-generate an enum tag type the same way. That the union is
         // untagged is represented by the Type tag (union vs union_tagged).
-        union_obj.tag_ty = try sema.generateUnionTagTypeSimple(&block_scope, fields_len, union_obj);
-        enum_field_names = &union_obj.tag_ty.castTag(.enum_simple).?.data.fields;
+        enum_field_names = try sema.arena.alloc(InternPool.NullTerminatedString, fields_len);
     }
 
     if (fields_len == 0) {
@@ -32675,11 +32622,11 @@ fn semaUnionFields(mod: *Module, union_obj: *Module.Union) CompileError!void {
             break :blk try sema.resolveInst(tag_ref);
         } else .none;
 
-        if (enum_value_map) |map| {
+        if (enum_field_vals.len != 0) {
             const copied_val = if (tag_ref != .none) blk: {
                 const val = sema.semaUnionFieldVal(&block_scope, .unneeded, int_tag_ty, tag_ref) catch |err| switch (err) {
                     error.NeededSourceLocation => {
-                        const val_src = union_obj.fieldSrcLoc(sema.mod, .{
+                        const val_src = mod.fieldSrcLoc(union_obj.owner_decl, .{
                             .index = field_i,
                             .range = .value,
                         }).lazy;
@@ -32690,25 +32637,24 @@ fn semaUnionFields(mod: *Module, union_obj: *Module.Union) CompileError!void {
                 };
                 last_tag_val = val;
 
-                // This puts the memory into the union arena, not the enum arena, but
-                // it is OK since they share the same lifetime.
-                break :blk try val.copy(decl_arena_allocator);
+                break :blk val;
             } else blk: {
                 const val = if (last_tag_val) |val|
-                    try sema.intAdd(val, try mod.intValue(int_tag_ty, 1), int_tag_ty)
+                    try sema.intAdd(val, Value.one_comptime_int, int_tag_ty)
                 else
                     try mod.intValue(int_tag_ty, 0);
                 last_tag_val = val;
 
-                break :blk try val.copy(decl_arena_allocator);
+                break :blk val;
             };
-            const gop = map.getOrPutAssumeCapacityContext(copied_val, .{
+            enum_field_vals[field_i] = copied_val.ip_index;
+            const gop = enum_field_vals_map.getOrPutAssumeCapacityContext(copied_val, .{
                 .ty = int_tag_ty,
                 .mod = mod,
             });
             if (gop.found_existing) {
-                const field_src = union_obj.fieldSrcLoc(sema.mod, .{ .index = field_i }).lazy;
-                const other_field_src = union_obj.fieldSrcLoc(sema.mod, .{ .index = gop.index }).lazy;
+                const field_src = mod.fieldSrcLoc(union_obj.owner_decl, .{ .index = field_i }).lazy;
+                const other_field_src = mod.fieldSrcLoc(union_obj.owner_decl, .{ .index = gop.index }).lazy;
                 const msg = msg: {
                     const msg = try sema.errMsg(&block_scope, field_src, "enum tag value {} already taken", .{copied_val.fmtValue(int_tag_ty, sema.mod)});
                     errdefer msg.destroy(gpa);
@@ -32721,8 +32667,9 @@ fn semaUnionFields(mod: *Module, union_obj: *Module.Union) CompileError!void {
 
         // This string needs to outlive the ZIR code.
         const field_name = try decl_arena_allocator.dupe(u8, field_name_zir);
-        if (enum_field_names) |set| {
-            set.putAssumeCapacity(field_name, {});
+        const field_name_ip = try mod.intern_pool.getOrPutString(gpa, field_name);
+        if (enum_field_names.len != 0) {
+            enum_field_names[field_i] = field_name_ip;
         }
 
         const field_ty: Type = if (!has_type)
@@ -32732,7 +32679,7 @@ fn semaUnionFields(mod: *Module, union_obj: *Module.Union) CompileError!void {
         else
             sema.resolveType(&block_scope, .unneeded, field_type_ref) catch |err| switch (err) {
                 error.NeededSourceLocation => {
-                    const ty_src = union_obj.fieldSrcLoc(sema.mod, .{
+                    const ty_src = mod.fieldSrcLoc(union_obj.owner_decl, .{
                         .index = field_i,
                         .range = .type,
                     }).lazy;
@@ -32749,12 +32696,12 @@ fn semaUnionFields(mod: *Module, union_obj: *Module.Union) CompileError!void {
         const gop = union_obj.fields.getOrPutAssumeCapacity(field_name);
         if (gop.found_existing) {
             const msg = msg: {
-                const field_src = union_obj.fieldSrcLoc(sema.mod, .{ .index = field_i }).lazy;
+                const field_src = mod.fieldSrcLoc(union_obj.owner_decl, .{ .index = field_i }).lazy;
                 const msg = try sema.errMsg(&block_scope, field_src, "duplicate union field: '{s}'", .{field_name});
                 errdefer msg.destroy(gpa);
 
                 const prev_field_index = union_obj.fields.getIndex(field_name).?;
-                const prev_field_src = union_obj.fieldSrcLoc(sema.mod, .{ .index = prev_field_index }).lazy;
+                const prev_field_src = mod.fieldSrcLoc(union_obj.owner_decl, .{ .index = prev_field_index }).lazy;
                 try sema.mod.errNoteNonLazy(prev_field_src.toSrcLoc(decl, mod), msg, "other field here", .{});
                 try sema.errNote(&block_scope, src, msg, "union declared here", .{});
                 break :msg msg;
@@ -32762,26 +32709,31 @@ fn semaUnionFields(mod: *Module, union_obj: *Module.Union) CompileError!void {
             return sema.failWithOwnedErrorMsg(msg);
         }
 
-        if (tag_ty_field_names) |*names| {
-            const enum_has_field = names.orderedRemove(field_name);
-            if (!enum_has_field) {
+        if (explicit_enum_info) |tag_info| {
+            const enum_index = tag_info.nameIndex(mod.intern_pool, field_name_ip) orelse {
                 const msg = msg: {
-                    const ty_src = union_obj.fieldSrcLoc(sema.mod, .{
+                    const ty_src = mod.fieldSrcLoc(union_obj.owner_decl, .{
                         .index = field_i,
                         .range = .type,
                     }).lazy;
-                    const msg = try sema.errMsg(&block_scope, ty_src, "no field named '{s}' in enum '{}'", .{ field_name, union_obj.tag_ty.fmt(sema.mod) });
+                    const msg = try sema.errMsg(&block_scope, ty_src, "no field named '{s}' in enum '{}'", .{
+                        field_name, union_obj.tag_ty.fmt(sema.mod),
+                    });
                     errdefer msg.destroy(sema.gpa);
                     try sema.addDeclaredHereNote(msg, union_obj.tag_ty);
                     break :msg msg;
                 };
                 return sema.failWithOwnedErrorMsg(msg);
-            }
+            };
+            // 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;
         }
 
         if (field_ty.zigTypeTag(mod) == .Opaque) {
             const msg = msg: {
-                const ty_src = union_obj.fieldSrcLoc(sema.mod, .{
+                const ty_src = mod.fieldSrcLoc(union_obj.owner_decl, .{
                     .index = field_i,
                     .range = .type,
                 }).lazy;
@@ -32795,7 +32747,7 @@ fn semaUnionFields(mod: *Module, union_obj: *Module.Union) CompileError!void {
         }
         if (union_obj.layout == .Extern and !try sema.validateExternType(field_ty, .union_field)) {
             const msg = msg: {
-                const ty_src = union_obj.fieldSrcLoc(sema.mod, .{
+                const ty_src = mod.fieldSrcLoc(union_obj.owner_decl, .{
                     .index = field_i,
                     .range = .type,
                 });
@@ -32810,7 +32762,7 @@ fn semaUnionFields(mod: *Module, union_obj: *Module.Union) CompileError!void {
             return sema.failWithOwnedErrorMsg(msg);
         } else if (union_obj.layout == .Packed and !(validatePackedType(field_ty, mod))) {
             const msg = msg: {
-                const ty_src = union_obj.fieldSrcLoc(sema.mod, .{
+                const ty_src = mod.fieldSrcLoc(union_obj.owner_decl, .{
                     .index = field_i,
                     .range = .type,
                 });
@@ -32833,7 +32785,7 @@ fn semaUnionFields(mod: *Module, union_obj: *Module.Union) CompileError!void {
         if (align_ref != .none) {
             gop.value_ptr.abi_align = sema.resolveAlign(&block_scope, .unneeded, align_ref) catch |err| switch (err) {
                 error.NeededSourceLocation => {
-                    const align_src = union_obj.fieldSrcLoc(sema.mod, .{
+                    const align_src = mod.fieldSrcLoc(union_obj.owner_decl, .{
                         .index = field_i,
                         .range = .alignment,
                     }).lazy;
@@ -32847,22 +32799,28 @@ fn semaUnionFields(mod: *Module, union_obj: *Module.Union) CompileError!void {
         }
     }
 
-    if (tag_ty_field_names) |names| {
-        if (names.count() > 0) {
+    if (explicit_enum_info) |tag_info| {
+        if (tag_info.names.len > fields_len) {
             const msg = msg: {
                 const msg = try sema.errMsg(&block_scope, src, "enum field(s) missing in union", .{});
                 errdefer msg.destroy(sema.gpa);
 
                 const enum_ty = union_obj.tag_ty;
-                for (names.keys()) |field_name| {
-                    const field_index = enum_ty.enumFieldIndex(field_name).?;
-                    try sema.addFieldErrNote(enum_ty, field_index, msg, "field '{s}' missing, declared here", .{field_name});
+                for (tag_info.names, 0..) |field_name, field_index| {
+                    if (explicit_tags_seen[field_index]) continue;
+                    try sema.addFieldErrNote(enum_ty, field_index, msg, "field '{s}' missing, declared here", .{
+                        mod.intern_pool.stringToSlice(field_name),
+                    });
                 }
                 try sema.addDeclaredHereNote(msg, union_obj.tag_ty);
                 break :msg msg;
             };
             return sema.failWithOwnedErrorMsg(msg);
         }
+    } else if (enum_field_vals.len != 0) {
+        union_obj.tag_ty = try sema.generateUnionTagTypeNumbered(&block_scope, enum_field_names, enum_field_vals, union_obj);
+    } else {
+        union_obj.tag_ty = try sema.generateUnionTagTypeSimple(&block_scope, enum_field_names, union_obj);
     }
 }
 
@@ -32874,25 +32832,12 @@ fn semaUnionFieldVal(sema: *Sema, block: *Block, src: LazySrcLoc, int_tag_ty: Ty
 fn generateUnionTagTypeNumbered(
     sema: *Sema,
     block: *Block,
-    fields_len: u32,
-    int_ty: Type,
+    enum_field_names: []const InternPool.NullTerminatedString,
+    enum_field_vals: []const InternPool.Index,
     union_obj: *Module.Union,
 ) !Type {
     const mod = sema.mod;
 
-    var new_decl_arena = std.heap.ArenaAllocator.init(sema.gpa);
-    errdefer new_decl_arena.deinit();
-    const new_decl_arena_allocator = new_decl_arena.allocator();
-
-    const enum_obj = try new_decl_arena_allocator.create(Module.EnumNumbered);
-    const enum_ty_payload = try new_decl_arena_allocator.create(Type.Payload.EnumNumbered);
-    enum_ty_payload.* = .{
-        .base = .{ .tag = .enum_numbered },
-        .data = enum_obj,
-    };
-    const enum_ty = Type.initPayload(&enum_ty_payload.base);
-    const enum_val = try Value.Tag.ty.create(new_decl_arena_allocator, enum_ty);
-
     const src_decl = mod.declPtr(block.src_decl);
     const new_decl_index = try mod.allocateNewDecl(block.namespace, src_decl.src_node, block.wip_capture_scope);
     errdefer mod.destroyDecl(new_decl_index);
@@ -32903,53 +32848,45 @@ fn generateUnionTagTypeNumbered(
     };
     try mod.initNewAnonDecl(new_decl_index, src_decl.src_line, block.namespace, .{
         .ty = Type.type,
-        .val = enum_val,
+        .val = undefined,
     }, name);
-    sema.mod.declPtr(new_decl_index).name_fully_qualified = true;
-
     const new_decl = mod.declPtr(new_decl_index);
+    new_decl.name_fully_qualified = true;
     new_decl.owns_tv = true;
     new_decl.name_fully_qualified = true;
     errdefer mod.abortAnonDecl(new_decl_index);
 
-    const copied_int_ty = try int_ty.copy(new_decl_arena_allocator);
-    enum_obj.* = .{
-        .owner_decl = new_decl_index,
-        .tag_ty = copied_int_ty,
-        .fields = .{},
-        .values = .{},
-    };
-    // Here we pre-allocate the maps using the decl arena.
-    try enum_obj.fields.ensureTotalCapacity(new_decl_arena_allocator, fields_len);
-    try enum_obj.values.ensureTotalCapacityContext(new_decl_arena_allocator, fields_len, .{
-        .ty = copied_int_ty,
-        .mod = mod,
-    });
-    try new_decl.finalizeNewArena(&new_decl_arena);
-    return enum_ty;
-}
+    const enum_ty = try mod.intern(.{ .enum_type = .{
+        .decl = new_decl_index,
+        .namespace = .none,
+        .tag_ty = if (enum_field_vals.len == 0)
+            .noreturn_type
+        else
+            mod.intern_pool.typeOf(enum_field_vals[0]),
+        .names = enum_field_names,
+        .values = enum_field_vals,
+        .tag_mode = .explicit,
+    } });
+    errdefer mod.intern_pool.remove(enum_ty);
 
-fn generateUnionTagTypeSimple(sema: *Sema, block: *Block, fields_len: usize, maybe_union_obj: ?*Module.Union) !Type {
-    const mod = sema.mod;
+    new_decl.val = enum_ty.toValue();
 
-    var new_decl_arena = std.heap.ArenaAllocator.init(sema.gpa);
-    errdefer new_decl_arena.deinit();
-    const new_decl_arena_allocator = new_decl_arena.allocator();
+    return enum_ty.toType();
+}
 
-    const enum_obj = try new_decl_arena_allocator.create(Module.EnumSimple);
-    const enum_ty_payload = try new_decl_arena_allocator.create(Type.Payload.EnumSimple);
-    enum_ty_payload.* = .{
-        .base = .{ .tag = .enum_simple },
-        .data = enum_obj,
-    };
-    const enum_ty = Type.initPayload(&enum_ty_payload.base);
-    const enum_val = try Value.Tag.ty.create(new_decl_arena_allocator, enum_ty);
+fn generateUnionTagTypeSimple(
+    sema: *Sema,
+    block: *Block,
+    enum_field_names: []const InternPool.NullTerminatedString,
+    maybe_union_obj: ?*Module.Union,
+) !Type {
+    const mod = sema.mod;
 
     const new_decl_index = new_decl_index: {
         const union_obj = maybe_union_obj orelse {
             break :new_decl_index try mod.createAnonymousDecl(block, .{
                 .ty = Type.type,
-                .val = enum_val,
+                .val = undefined,
             });
         };
         const src_decl = mod.declPtr(block.src_decl);
@@ -32962,24 +32899,31 @@ fn generateUnionTagTypeSimple(sema: *Sema, block: *Block, fields_len: usize, may
         };
         try mod.initNewAnonDecl(new_decl_index, src_decl.src_line, block.namespace, .{
             .ty = Type.type,
-            .val = enum_val,
+            .val = undefined,
         }, name);
-        sema.mod.declPtr(new_decl_index).name_fully_qualified = true;
+        mod.declPtr(new_decl_index).name_fully_qualified = true;
         break :new_decl_index new_decl_index;
     };
 
+    const enum_ty = try mod.intern(.{ .enum_type = .{
+        .decl = new_decl_index,
+        .namespace = .none,
+        .tag_ty = if (enum_field_names.len == 0)
+            .noreturn_type
+        else
+            (try mod.smallestUnsignedInt(enum_field_names.len - 1)).ip_index,
+        .names = enum_field_names,
+        .values = &.{},
+        .tag_mode = .auto,
+    } });
+    errdefer mod.intern_pool.remove(enum_ty);
+
     const new_decl = mod.declPtr(new_decl_index);
     new_decl.owns_tv = true;
+    new_decl.val = enum_ty.toValue();
     errdefer mod.abortAnonDecl(new_decl_index);
 
-    enum_obj.* = .{
-        .owner_decl = new_decl_index,
-        .fields = .{},
-    };
-    // Here we pre-allocate the maps using the decl arena.
-    try enum_obj.fields.ensureTotalCapacity(new_decl_arena_allocator, fields_len);
-    try new_decl.finalizeNewArena(&new_decl_arena);
-    return enum_ty;
+    return enum_ty.toType();
 }
 
 fn getBuiltin(sema: *Sema, name: []const u8) CompileError!Air.Inst.Ref {
@@ -33098,57 +33042,6 @@ pub fn typeHasOnePossibleValue(sema: *Sema, ty: Type) CompileError!?Value {
                 return Value.empty_struct;
             },
 
-            .enum_numbered => {
-                const resolved_ty = try sema.resolveTypeFields(ty);
-                const enum_obj = resolved_ty.castTag(.enum_numbered).?.data;
-                // An explicit tag type is always provided for enum_numbered.
-                if (!(try sema.typeHasRuntimeBits(enum_obj.tag_ty))) {
-                    return null;
-                }
-                if (enum_obj.fields.count() == 1) {
-                    if (enum_obj.values.count() == 0) {
-                        return Value.enum_field_0; // auto-numbered
-                    } else {
-                        return enum_obj.values.keys()[0];
-                    }
-                } else {
-                    return null;
-                }
-            },
-            .enum_full => {
-                const resolved_ty = try sema.resolveTypeFields(ty);
-                const enum_obj = resolved_ty.castTag(.enum_full).?.data;
-                if (!(try sema.typeHasRuntimeBits(enum_obj.tag_ty))) {
-                    return null;
-                }
-                switch (enum_obj.fields.count()) {
-                    0 => return Value.@"unreachable",
-                    1 => if (enum_obj.values.count() == 0) {
-                        return Value.enum_field_0; // auto-numbered
-                    } else {
-                        return enum_obj.values.keys()[0];
-                    },
-                    else => return null,
-                }
-            },
-            .enum_simple => {
-                const resolved_ty = try sema.resolveTypeFields(ty);
-                const enum_simple = resolved_ty.castTag(.enum_simple).?.data;
-                switch (enum_simple.fields.count()) {
-                    0 => return Value.@"unreachable",
-                    1 => return Value.enum_field_0,
-                    else => return null,
-                }
-            },
-            .enum_nonexhaustive => {
-                const tag_ty = ty.castTag(.enum_nonexhaustive).?.data.tag_ty;
-                if (tag_ty.zigTypeTag(mod) != .ComptimeInt and !(try sema.typeHasRuntimeBits(tag_ty))) {
-                    return Value.enum_field_0;
-                } else {
-                    return null;
-                }
-            },
-
             .array => {
                 if (ty.arrayLen(mod) == 0)
                     return Value.initTag(.empty_array);
@@ -33295,7 +33188,28 @@ pub fn typeHasOnePossibleValue(sema: *Sema, ty: Type) CompileError!?Value {
                 return only.toValue();
             },
             .opaque_type => null,
-            .enum_type => @panic("TODO"),
+            .enum_type => |enum_type| switch (enum_type.tag_mode) {
+                .nonexhaustive => {
+                    if (enum_type.tag_ty != .comptime_int_type and
+                        !(try sema.typeHasRuntimeBits(enum_type.tag_ty.toType())))
+                    {
+                        return Value.enum_field_0;
+                    } else {
+                        return null;
+                    }
+                },
+                .auto, .explicit => switch (enum_type.names.len) {
+                    0 => return Value.@"unreachable",
+                    1 => {
+                        if (enum_type.values.len == 0) {
+                            return Value.enum_field_0; // auto-numbered
+                        } else {
+                            return enum_type.values[0].toValue();
+                        }
+                    },
+                    else => return null,
+                },
+            },
 
             // values, not types
             .un => unreachable,
@@ -33701,7 +33615,6 @@ pub fn typeRequiresComptime(sema: *Sema, ty: Type) CompileError!bool {
             .error_set_single,
             .error_set_inferred,
             .error_set_merged,
-            .enum_simple,
             => false,
 
             .function => true,
@@ -33742,14 +33655,6 @@ pub fn typeRequiresComptime(sema: *Sema, ty: Type) CompileError!bool {
                 const child_ty = ty.castTag(.anyframe_T).?.data;
                 return sema.typeRequiresComptime(child_ty);
             },
-            .enum_numbered => {
-                const tag_ty = ty.castTag(.enum_numbered).?.data.tag_ty;
-                return sema.typeRequiresComptime(tag_ty);
-            },
-            .enum_full, .enum_nonexhaustive => {
-                const tag_ty = ty.cast(Type.Payload.EnumFull).?.data.tag_ty;
-                return sema.typeRequiresComptime(tag_ty);
-            },
         },
         else => switch (mod.intern_pool.indexToKey(ty.ip_index)) {
             .int_type => return false,
@@ -33865,7 +33770,7 @@ pub fn typeRequiresComptime(sema: *Sema, ty: Type) CompileError!bool {
             },
 
             .opaque_type => false,
-            .enum_type => @panic("TODO"),
+            .enum_type => |enum_type| try sema.typeRequiresComptime(enum_type.tag_ty.toType()),
 
             // values, not types
             .un => unreachable,
@@ -34435,42 +34340,19 @@ fn intInRange(sema: *Sema, tag_ty: Type, int_val: Value, end: usize) !bool {
 /// Asserts the type is an enum.
 fn enumHasInt(sema: *Sema, ty: Type, int: Value) CompileError!bool {
     const mod = sema.mod;
-    switch (ty.tag()) {
-        .enum_nonexhaustive => unreachable,
-        .enum_full => {
-            const enum_full = ty.castTag(.enum_full).?.data;
-            const tag_ty = enum_full.tag_ty;
-            if (enum_full.values.count() == 0) {
-                return sema.intInRange(tag_ty, int, enum_full.fields.count());
-            } else {
-                return enum_full.values.containsContext(int, .{
-                    .ty = tag_ty,
-                    .mod = sema.mod,
-                });
-            }
-        },
-        .enum_numbered => {
-            const enum_obj = ty.castTag(.enum_numbered).?.data;
-            const tag_ty = enum_obj.tag_ty;
-            if (enum_obj.values.count() == 0) {
-                return sema.intInRange(tag_ty, int, enum_obj.fields.count());
-            } else {
-                return enum_obj.values.containsContext(int, .{
-                    .ty = tag_ty,
-                    .mod = sema.mod,
-                });
-            }
-        },
-        .enum_simple => {
-            const enum_simple = ty.castTag(.enum_simple).?.data;
-            const fields_len = enum_simple.fields.count();
-            const bits = std.math.log2_int_ceil(usize, fields_len);
-            const tag_ty = try mod.intType(.unsigned, bits);
-            return sema.intInRange(tag_ty, int, fields_len);
-        },
-
-        else => unreachable,
+    const enum_type = mod.intern_pool.indexToKey(ty.ip_index).enum_type;
+    assert(enum_type.tag_mode != .nonexhaustive);
+    if (enum_type.values.len == 0) {
+        // auto-numbered
+        return sema.intInRange(enum_type.tag_ty.toType(), int, enum_type.names.len);
     }
+
+    // The `tagValueIndex` function call below relies on the type being the integer tag type.
+    // `getCoerced` assumes the value will fit the new type.
+    if (!(try sema.intFitsInType(int, enum_type.tag_ty.toType(), null))) return false;
+    const int_coerced = try mod.intern_pool.getCoerced(sema.gpa, int.ip_index, enum_type.tag_ty);
+
+    return enum_type.tagValueIndex(mod.intern_pool, int_coerced) != null;
 }
 
 fn intAddWithOverflow(
src/type.zig
@@ -62,12 +62,6 @@ pub const Type = struct {
                 .tuple,
                 .anon_struct,
                 => return .Struct,
-
-                .enum_full,
-                .enum_nonexhaustive,
-                .enum_simple,
-                .enum_numbered,
-                => return .Enum,
             },
             else => switch (mod.intern_pool.indexToKey(ty.ip_index)) {
                 .int_type => return .Int,
@@ -566,22 +560,6 @@ pub const Type = struct {
 
                 return true;
             },
-
-            .enum_full, .enum_nonexhaustive => {
-                const a_enum_obj = a.cast(Payload.EnumFull).?.data;
-                const b_enum_obj = (b.cast(Payload.EnumFull) orelse return false).data;
-                return a_enum_obj == b_enum_obj;
-            },
-            .enum_simple => {
-                const a_enum_obj = a.cast(Payload.EnumSimple).?.data;
-                const b_enum_obj = (b.cast(Payload.EnumSimple) orelse return false).data;
-                return a_enum_obj == b_enum_obj;
-            },
-            .enum_numbered => {
-                const a_enum_obj = a.cast(Payload.EnumNumbered).?.data;
-                const b_enum_obj = (b.cast(Payload.EnumNumbered) orelse return false).data;
-                return a_enum_obj == b_enum_obj;
-            },
         }
     }
 
@@ -727,22 +705,6 @@ pub const Type = struct {
                     field_val.hash(field_ty, hasher, mod);
                 }
             },
-
-            .enum_full, .enum_nonexhaustive => {
-                const enum_obj: *const Module.EnumFull = ty.cast(Payload.EnumFull).?.data;
-                std.hash.autoHash(hasher, std.builtin.TypeId.Enum);
-                std.hash.autoHash(hasher, enum_obj);
-            },
-            .enum_simple => {
-                const enum_obj: *const Module.EnumSimple = ty.cast(Payload.EnumSimple).?.data;
-                std.hash.autoHash(hasher, std.builtin.TypeId.Enum);
-                std.hash.autoHash(hasher, enum_obj);
-            },
-            .enum_numbered => {
-                const enum_obj: *const Module.EnumNumbered = ty.cast(Payload.EnumNumbered).?.data;
-                std.hash.autoHash(hasher, std.builtin.TypeId.Enum);
-                std.hash.autoHash(hasher, enum_obj);
-            },
         }
     }
 
@@ -920,9 +882,6 @@ pub const Type = struct {
             .error_set => return self.copyPayloadShallow(allocator, Payload.ErrorSet),
             .error_set_inferred => return self.copyPayloadShallow(allocator, Payload.ErrorSetInferred),
             .error_set_single => return self.copyPayloadShallow(allocator, Payload.Name),
-            .enum_simple => return self.copyPayloadShallow(allocator, Payload.EnumSimple),
-            .enum_numbered => return self.copyPayloadShallow(allocator, Payload.EnumNumbered),
-            .enum_full, .enum_nonexhaustive => return self.copyPayloadShallow(allocator, Payload.EnumFull),
         }
     }
 
@@ -995,25 +954,6 @@ pub const Type = struct {
         while (true) {
             const t = ty.tag();
             switch (t) {
-                .enum_full, .enum_nonexhaustive => {
-                    const enum_full = ty.cast(Payload.EnumFull).?.data;
-                    return writer.print("({s} decl={d})", .{
-                        @tagName(t), enum_full.owner_decl,
-                    });
-                },
-                .enum_simple => {
-                    const enum_simple = ty.castTag(.enum_simple).?.data;
-                    return writer.print("({s} decl={d})", .{
-                        @tagName(t), enum_simple.owner_decl,
-                    });
-                },
-                .enum_numbered => {
-                    const enum_numbered = ty.castTag(.enum_numbered).?.data;
-                    return writer.print("({s} decl={d})", .{
-                        @tagName(t), enum_numbered.owner_decl,
-                    });
-                },
-
                 .function => {
                     const payload = ty.castTag(.function).?.data;
                     try writer.writeAll("fn(");
@@ -1199,22 +1139,6 @@ pub const Type = struct {
                 .inferred_alloc_const => unreachable,
                 .inferred_alloc_mut => unreachable,
 
-                .enum_full, .enum_nonexhaustive => {
-                    const enum_full = ty.cast(Payload.EnumFull).?.data;
-                    const decl = mod.declPtr(enum_full.owner_decl);
-                    try decl.renderFullyQualifiedName(mod, writer);
-                },
-                .enum_simple => {
-                    const enum_simple = ty.castTag(.enum_simple).?.data;
-                    const decl = mod.declPtr(enum_simple.owner_decl);
-                    try decl.renderFullyQualifiedName(mod, writer);
-                },
-                .enum_numbered => {
-                    const enum_numbered = ty.castTag(.enum_numbered).?.data;
-                    const decl = mod.declPtr(enum_numbered.owner_decl);
-                    try decl.renderFullyQualifiedName(mod, writer);
-                },
-
                 .error_set_inferred => {
                     const func = ty.castTag(.error_set_inferred).?.data.func;
 
@@ -1500,7 +1424,10 @@ pub const Type = struct {
                     const decl = mod.declPtr(opaque_type.decl);
                     try decl.renderFullyQualifiedName(mod, writer);
                 },
-                .enum_type => @panic("TODO"),
+                .enum_type => |enum_type| {
+                    const decl = mod.declPtr(enum_type.decl);
+                    try decl.renderFullyQualifiedName(mod, writer);
+                },
 
                 // values, not types
                 .un => unreachable,
@@ -1593,19 +1520,6 @@ pub const Type = struct {
                     }
                 },
 
-                .enum_full => {
-                    const enum_full = ty.castTag(.enum_full).?.data;
-                    return enum_full.tag_ty.hasRuntimeBitsAdvanced(mod, ignore_comptime_only, strat);
-                },
-                .enum_simple => {
-                    const enum_simple = ty.castTag(.enum_simple).?.data;
-                    return enum_simple.fields.count() >= 2;
-                },
-                .enum_numbered, .enum_nonexhaustive => {
-                    const int_tag_ty = try ty.intTagType(mod);
-                    return int_tag_ty.hasRuntimeBitsAdvanced(mod, ignore_comptime_only, strat);
-                },
-
                 .array => return ty.arrayLen(mod) != 0 and
                     try ty.childType(mod).hasRuntimeBitsAdvanced(mod, ignore_comptime_only, strat),
                 .array_sentinel => return ty.childType(mod).hasRuntimeBitsAdvanced(mod, ignore_comptime_only, strat),
@@ -1766,7 +1680,7 @@ pub const Type = struct {
                 },
 
                 .opaque_type => true,
-                .enum_type => @panic("TODO"),
+                .enum_type => |enum_type| enum_type.tag_ty.toType().hasRuntimeBitsAdvanced(mod, ignore_comptime_only, strat),
 
                 // values, not types
                 .un => unreachable,
@@ -1789,9 +1703,7 @@ pub const Type = struct {
             .empty_struct_type => false,
 
             .none => switch (ty.tag()) {
-                .pointer,
-                .enum_numbered,
-                => true,
+                .pointer => true,
 
                 .error_set,
                 .error_set_single,
@@ -1799,17 +1711,12 @@ pub const Type = struct {
                 .error_set_merged,
                 // These are function bodies, not function pointers.
                 .function,
-                .enum_simple,
                 .error_union,
                 .anyframe_T,
                 .tuple,
                 .anon_struct,
                 => false,
 
-                .enum_full,
-                .enum_nonexhaustive,
-                => !ty.cast(Payload.EnumFull).?.data.tag_ty_inferred,
-
                 .inferred_alloc_mut => unreachable,
                 .inferred_alloc_const => unreachable,
 
@@ -1886,7 +1793,10 @@ pub const Type = struct {
                     .tagged => false,
                 },
                 .opaque_type => false,
-                .enum_type => @panic("TODO"),
+                .enum_type => |enum_type| switch (enum_type.tag_mode) {
+                    .auto => false,
+                    .explicit, .nonexhaustive => true,
+                },
 
                 // values, not types
                 .un => unreachable,
@@ -2116,11 +2026,6 @@ pub const Type = struct {
                     return AbiAlignmentAdvanced{ .scalar = big_align };
                 },
 
-                .enum_full, .enum_nonexhaustive, .enum_simple, .enum_numbered => {
-                    const int_tag_ty = try ty.intTagType(mod);
-                    return AbiAlignmentAdvanced{ .scalar = int_tag_ty.abiAlignment(mod) };
-                },
-
                 .inferred_alloc_const,
                 .inferred_alloc_mut,
                 => unreachable,
@@ -2283,7 +2188,7 @@ pub const Type = struct {
                     return abiAlignmentAdvancedUnion(ty, mod, strat, union_obj, union_type.hasTag());
                 },
                 .opaque_type => return AbiAlignmentAdvanced{ .scalar = 1 },
-                .enum_type => @panic("TODO"),
+                .enum_type => |enum_type| return AbiAlignmentAdvanced{ .scalar = enum_type.tag_ty.toType().abiAlignment(mod) },
 
                 // values, not types
                 .un => unreachable,
@@ -2475,11 +2380,6 @@ pub const Type = struct {
                     return AbiSizeAdvanced{ .scalar = ty.structFieldOffset(field_count, mod) };
                 },
 
-                .enum_simple, .enum_full, .enum_nonexhaustive, .enum_numbered => {
-                    const int_tag_ty = try ty.intTagType(mod);
-                    return AbiSizeAdvanced{ .scalar = int_tag_ty.abiSize(mod) };
-                },
-
                 .array => {
                     const payload = ty.castTag(.array).?.data;
                     switch (try payload.elem_type.abiSizeAdvanced(mod, strat)) {
@@ -2705,7 +2605,7 @@ pub const Type = struct {
                     return abiSizeAdvancedUnion(ty, mod, strat, union_obj, union_type.hasTag());
                 },
                 .opaque_type => unreachable, // no size available
-                .enum_type => @panic("TODO"),
+                .enum_type => |enum_type| return AbiSizeAdvanced{ .scalar = enum_type.tag_ty.toType().abiSize(mod) },
 
                 // values, not types
                 .un => unreachable,
@@ -2823,11 +2723,6 @@ pub const Type = struct {
                     return total;
                 },
 
-                .enum_simple, .enum_full, .enum_nonexhaustive, .enum_numbered => {
-                    const int_tag_ty = try ty.intTagType(mod);
-                    return try bitSizeAdvanced(int_tag_ty, mod, opt_sema);
-                },
-
                 .array => {
                     const payload = ty.castTag(.array).?.data;
                     const elem_size = std.math.max(payload.elem_type.abiAlignment(mod), payload.elem_type.abiSize(mod));
@@ -2964,7 +2859,7 @@ pub const Type = struct {
                     return size;
                 },
                 .opaque_type => unreachable,
-                .enum_type => @panic("TODO"),
+                .enum_type => |enum_type| return bitSizeAdvanced(enum_type.tag_ty.toType(), mod, opt_sema),
 
                 // values, not types
                 .un => unreachable,
@@ -3433,7 +3328,7 @@ pub const Type = struct {
     pub fn unionTagFieldIndex(ty: Type, enum_tag: Value, mod: *Module) ?usize {
         const union_obj = mod.typeToUnion(ty).?;
         const index = union_obj.tag_ty.enumTagFieldIndex(enum_tag, mod) orelse return null;
-        const name = union_obj.tag_ty.enumFieldName(index);
+        const name = union_obj.tag_ty.enumFieldName(index, mod);
         return union_obj.fields.getIndex(name);
     }
 
@@ -3690,15 +3585,6 @@ pub const Type = struct {
 
         while (true) switch (ty.ip_index) {
             .none => switch (ty.tag()) {
-                .enum_full, .enum_nonexhaustive => ty = ty.cast(Payload.EnumFull).?.data.tag_ty,
-                .enum_numbered => ty = ty.castTag(.enum_numbered).?.data.tag_ty,
-                .enum_simple => {
-                    const enum_obj = ty.castTag(.enum_simple).?.data;
-                    const field_count = enum_obj.fields.count();
-                    if (field_count == 0) return .{ .signedness = .unsigned, .bits = 0 };
-                    return .{ .signedness = .unsigned, .bits = smallestUnsignedBits(field_count - 1) };
-                },
-
                 .error_set, .error_set_single, .error_set_inferred, .error_set_merged => {
                     // TODO revisit this when error sets support custom int types
                     return .{ .signedness = .unsigned, .bits = 16 };
@@ -3728,7 +3614,7 @@ pub const Type = struct {
                     assert(struct_obj.layout == .Packed);
                     ty = struct_obj.backing_int_ty;
                 },
-                .enum_type => @panic("TODO"),
+                .enum_type => |enum_type| ty = enum_type.tag_ty.toType(),
 
                 .ptr_type => unreachable,
                 .array_type => unreachable,
@@ -3964,47 +3850,6 @@ pub const Type = struct {
                     return Value.empty_struct;
                 },
 
-                .enum_numbered => {
-                    const enum_numbered = ty.castTag(.enum_numbered).?.data;
-                    // An explicit tag type is always provided for enum_numbered.
-                    if (enum_numbered.tag_ty.hasRuntimeBits(mod)) {
-                        return null;
-                    }
-                    assert(enum_numbered.fields.count() == 1);
-                    return enum_numbered.values.keys()[0];
-                },
-                .enum_full => {
-                    const enum_full = ty.castTag(.enum_full).?.data;
-                    if (enum_full.tag_ty.hasRuntimeBits(mod)) {
-                        return null;
-                    }
-                    switch (enum_full.fields.count()) {
-                        0 => return Value.@"unreachable",
-                        1 => if (enum_full.values.count() == 0) {
-                            return Value.enum_field_0; // auto-numbered
-                        } else {
-                            return enum_full.values.keys()[0];
-                        },
-                        else => return null,
-                    }
-                },
-                .enum_simple => {
-                    const enum_simple = ty.castTag(.enum_simple).?.data;
-                    switch (enum_simple.fields.count()) {
-                        0 => return Value.@"unreachable",
-                        1 => return Value.enum_field_0,
-                        else => return null,
-                    }
-                },
-                .enum_nonexhaustive => {
-                    const tag_ty = ty.castTag(.enum_nonexhaustive).?.data.tag_ty;
-                    if (!tag_ty.hasRuntimeBits(mod)) {
-                        return Value.enum_field_0;
-                    } else {
-                        return null;
-                    }
-                },
-
                 .array => {
                     if (ty.arrayLen(mod) == 0)
                         return Value.initTag(.empty_array);
@@ -4123,7 +3968,28 @@ pub const Type = struct {
                     return only.toValue();
                 },
                 .opaque_type => return null,
-                .enum_type => @panic("TODO"),
+                .enum_type => |enum_type| switch (enum_type.tag_mode) {
+                    .nonexhaustive => {
+                        if (enum_type.tag_ty != .comptime_int_type and
+                            !enum_type.tag_ty.toType().hasRuntimeBits(mod))
+                        {
+                            return Value.enum_field_0;
+                        } else {
+                            return null;
+                        }
+                    },
+                    .auto, .explicit => switch (enum_type.names.len) {
+                        0 => return Value.@"unreachable",
+                        1 => {
+                            if (enum_type.values.len == 0) {
+                                return Value.enum_field_0; // auto-numbered
+                            } else {
+                                return enum_type.values[0].toValue();
+                            }
+                        },
+                        else => return null,
+                    },
+                },
 
                 // values, not types
                 .un => unreachable,
@@ -4151,7 +4017,6 @@ pub const Type = struct {
                 .error_set_single,
                 .error_set_inferred,
                 .error_set_merged,
-                .enum_simple,
                 => false,
 
                 // These are function bodies, not function pointers.
@@ -4191,14 +4056,6 @@ pub const Type = struct {
                     const child_ty = ty.castTag(.anyframe_T).?.data;
                     return child_ty.comptimeOnly(mod);
                 },
-                .enum_numbered => {
-                    const tag_ty = ty.castTag(.enum_numbered).?.data.tag_ty;
-                    return tag_ty.comptimeOnly(mod);
-                },
-                .enum_full, .enum_nonexhaustive => {
-                    const tag_ty = ty.cast(Type.Payload.EnumFull).?.data.tag_ty;
-                    return tag_ty.comptimeOnly(mod);
-                },
             },
             else => switch (mod.intern_pool.indexToKey(ty.ip_index)) {
                 .int_type => false,
@@ -4293,7 +4150,7 @@ pub const Type = struct {
 
                 .opaque_type => false,
 
-                .enum_type => @panic("TODO"),
+                .enum_type => |enum_type| enum_type.tag_ty.toType().comptimeOnly(mod),
 
                 // values, not types
                 .un => unreachable,
@@ -4346,19 +4203,14 @@ pub const Type = struct {
 
     /// Returns null if the type has no namespace.
     pub fn getNamespaceIndex(ty: Type, mod: *Module) Module.Namespace.OptionalIndex {
-        return switch (ty.ip_index) {
-            .none => switch (ty.tag()) {
-                .enum_full => ty.castTag(.enum_full).?.data.namespace.toOptional(),
-                .enum_nonexhaustive => ty.castTag(.enum_nonexhaustive).?.data.namespace.toOptional(),
-                else => .none,
-            },
-            else => switch (mod.intern_pool.indexToKey(ty.ip_index)) {
-                .opaque_type => |opaque_type| opaque_type.namespace.toOptional(),
-                .struct_type => |struct_type| struct_type.namespace,
-                .union_type => |union_type| mod.unionPtr(union_type.index).namespace.toOptional(),
+        if (ty.ip_index == .none) return .none;
+        return switch (mod.intern_pool.indexToKey(ty.ip_index)) {
+            .opaque_type => |opaque_type| opaque_type.namespace.toOptional(),
+            .struct_type => |struct_type| struct_type.namespace,
+            .union_type => |union_type| mod.unionPtr(union_type.index).namespace.toOptional(),
+            .enum_type => |enum_type| enum_type.namespace,
 
-                else => .none,
-            },
+            else => .none,
         };
     }
 
@@ -4444,29 +4296,23 @@ pub const Type = struct {
 
     /// Asserts the type is an enum or a union.
     pub fn intTagType(ty: Type, mod: *Module) !Type {
-        return switch (ty.ip_index) {
-            .none => switch (ty.tag()) {
-                .enum_full, .enum_nonexhaustive => ty.cast(Payload.EnumFull).?.data.tag_ty,
-                .enum_numbered => ty.castTag(.enum_numbered).?.data.tag_ty,
-                .enum_simple => {
-                    const enum_simple = ty.castTag(.enum_simple).?.data;
-                    const field_count = enum_simple.fields.count();
-                    const bits: u16 = if (field_count == 0) 0 else std.math.log2_int_ceil(usize, field_count);
-                    return mod.intType(.unsigned, bits);
-                },
-                else => unreachable,
-            },
-            else => switch (mod.intern_pool.indexToKey(ty.ip_index)) {
-                .union_type => |union_type| mod.unionPtr(union_type.index).tag_ty.intTagType(mod),
-                else => unreachable,
-            },
+        return switch (mod.intern_pool.indexToKey(ty.ip_index)) {
+            .union_type => |union_type| mod.unionPtr(union_type.index).tag_ty.intTagType(mod),
+            .enum_type => |enum_type| enum_type.tag_ty.toType(),
+            else => unreachable,
         };
     }
 
-    pub fn isNonexhaustiveEnum(ty: Type) bool {
-        return switch (ty.tag()) {
-            .enum_nonexhaustive => true,
-            else => false,
+    pub fn isNonexhaustiveEnum(ty: Type, mod: *Module) bool {
+        return switch (ty.ip_index) {
+            .none => false,
+            else => switch (mod.intern_pool.indexToKey(ty.ip_index)) {
+                .enum_type => |enum_type| switch (enum_type.tag_mode) {
+                    .nonexhaustive => true,
+                    .auto, .explicit => false,
+                },
+                else => false,
+            },
         };
     }
 
@@ -4510,25 +4356,26 @@ pub const Type = struct {
         return try Tag.error_set_merged.create(arena, names);
     }
 
-    pub fn enumFields(ty: Type) Module.EnumFull.NameMap {
-        return switch (ty.tag()) {
-            .enum_full, .enum_nonexhaustive => ty.cast(Payload.EnumFull).?.data.fields,
-            .enum_simple => ty.castTag(.enum_simple).?.data.fields,
-            .enum_numbered => ty.castTag(.enum_numbered).?.data.fields,
-            else => unreachable,
-        };
+    pub fn enumFields(ty: Type, mod: *Module) []const InternPool.NullTerminatedString {
+        return mod.intern_pool.indexToKey(ty.ip_index).enum_type.names;
     }
 
-    pub fn enumFieldCount(ty: Type) usize {
-        return ty.enumFields().count();
+    pub fn enumFieldCount(ty: Type, mod: *Module) usize {
+        return mod.intern_pool.indexToKey(ty.ip_index).enum_type.names.len;
     }
 
-    pub fn enumFieldName(ty: Type, field_index: usize) []const u8 {
-        return ty.enumFields().keys()[field_index];
+    pub fn enumFieldName(ty: Type, field_index: usize, mod: *Module) [:0]const u8 {
+        const ip = &mod.intern_pool;
+        const field_name = ip.indexToKey(ty.ip_index).enum_type.names[field_index];
+        return ip.stringToSlice(field_name);
     }
 
-    pub fn enumFieldIndex(ty: Type, field_name: []const u8) ?usize {
-        return ty.enumFields().getIndex(field_name);
+    pub fn enumFieldIndex(ty: Type, field_name: []const u8, mod: *Module) ?usize {
+        const ip = &mod.intern_pool;
+        const enum_type = ip.indexToKey(ty.ip_index).enum_type;
+        // If the string is not interned, then the field certainly is not present.
+        const field_name_interned = ip.getString(field_name).unwrap() orelse return null;
+        return enum_type.nameIndex(ip.*, field_name_interned);
     }
 
     /// Asserts `ty` is an enum. `enum_tag` can either be `enum_field_index` or
@@ -4538,50 +4385,20 @@ pub const Type = struct {
         if (enum_tag.castTag(.enum_field_index)) |payload| {
             return @as(usize, payload.data);
         }
-        const S = struct {
-            fn fieldWithRange(int_ty: Type, int_val: Value, end: usize, m: *Module) ?usize {
-                if (int_val.compareAllWithZero(.lt, m)) return null;
-                const end_val = m.intValue(int_ty, end) catch |err| switch (err) {
-                    // TODO: eliminate this failure condition
-                    error.OutOfMemory => @panic("OOM"),
-                };
-                if (int_val.compareScalar(.gte, end_val, int_ty, m)) return null;
-                return @intCast(usize, int_val.toUnsignedInt(m));
-            }
-        };
-        switch (ty.tag()) {
-            .enum_full, .enum_nonexhaustive => {
-                const enum_full = ty.cast(Payload.EnumFull).?.data;
-                const tag_ty = enum_full.tag_ty;
-                if (enum_full.values.count() == 0) {
-                    return S.fieldWithRange(tag_ty, enum_tag, enum_full.fields.count(), mod);
-                } else {
-                    return enum_full.values.getIndexContext(enum_tag, .{
-                        .ty = tag_ty,
-                        .mod = mod,
-                    });
-                }
-            },
-            .enum_numbered => {
-                const enum_obj = ty.castTag(.enum_numbered).?.data;
-                const tag_ty = enum_obj.tag_ty;
-                if (enum_obj.values.count() == 0) {
-                    return S.fieldWithRange(tag_ty, enum_tag, enum_obj.fields.count(), mod);
-                } else {
-                    return enum_obj.values.getIndexContext(enum_tag, .{
-                        .ty = tag_ty,
-                        .mod = mod,
-                    });
-                }
-            },
-            .enum_simple => {
-                const enum_simple = ty.castTag(.enum_simple).?.data;
-                const fields_len = enum_simple.fields.count();
-                const bits = std.math.log2_int_ceil(usize, fields_len);
-                const tag_ty = mod.intType(.unsigned, bits) catch @panic("TODO: handle OOM here");
-                return S.fieldWithRange(tag_ty, enum_tag, fields_len, mod);
-            },
-            else => unreachable,
+        const ip = &mod.intern_pool;
+        const enum_type = ip.indexToKey(ty.ip_index).enum_type;
+        const tag_ty = enum_type.tag_ty.toType();
+        if (enum_type.values.len == 0) {
+            if (enum_tag.compareAllWithZero(.lt, mod)) return null;
+            const end_val = mod.intValue(tag_ty, enum_type.names.len) catch |err| switch (err) {
+                // TODO: eliminate this failure condition
+                error.OutOfMemory => @panic("OOM"),
+            };
+            if (enum_tag.compareScalar(.gte, end_val, tag_ty, mod)) return null;
+            return @intCast(usize, enum_tag.toUnsignedInt(mod));
+        } else {
+            assert(ip.typeOf(enum_tag.ip_index) == enum_type.tag_ty);
+            return enum_type.tagValueIndex(ip.*, enum_tag.ip_index);
         }
     }
 
@@ -4905,18 +4722,6 @@ pub const Type = struct {
         switch (ty.ip_index) {
             .empty_struct_type => return null,
             .none => switch (ty.tag()) {
-                .enum_full, .enum_nonexhaustive => {
-                    const enum_full = ty.cast(Payload.EnumFull).?.data;
-                    return enum_full.srcLoc(mod);
-                },
-                .enum_numbered => {
-                    const enum_numbered = ty.castTag(.enum_numbered).?.data;
-                    return enum_numbered.srcLoc(mod);
-                },
-                .enum_simple => {
-                    const enum_simple = ty.castTag(.enum_simple).?.data;
-                    return enum_simple.srcLoc(mod);
-                },
                 .error_set => {
                     const error_set = ty.castTag(.error_set).?.data;
                     return error_set.srcLoc(mod);
@@ -4934,6 +4739,7 @@ pub const Type = struct {
                     return union_obj.srcLoc(mod);
                 },
                 .opaque_type => |opaque_type| mod.opaqueSrcLoc(opaque_type),
+                .enum_type => |enum_type| mod.declPtr(enum_type.decl).srcLoc(mod),
                 else => null,
             },
         }
@@ -4946,15 +4752,6 @@ pub const Type = struct {
     pub fn getOwnerDeclOrNull(ty: Type, mod: *Module) ?Module.Decl.Index {
         switch (ty.ip_index) {
             .none => switch (ty.tag()) {
-                .enum_full, .enum_nonexhaustive => {
-                    const enum_full = ty.cast(Payload.EnumFull).?.data;
-                    return enum_full.owner_decl;
-                },
-                .enum_numbered => return ty.castTag(.enum_numbered).?.data.owner_decl,
-                .enum_simple => {
-                    const enum_simple = ty.castTag(.enum_simple).?.data;
-                    return enum_simple.owner_decl;
-                },
                 .error_set => {
                     const error_set = ty.castTag(.error_set).?.data;
                     return error_set.owner_decl;
@@ -4972,6 +4769,7 @@ pub const Type = struct {
                     return union_obj.owner_decl;
                 },
                 .opaque_type => |opaque_type| opaque_type.decl,
+                .enum_type => |enum_type| enum_type.decl,
                 else => null,
             },
         }
@@ -5012,10 +4810,6 @@ pub const Type = struct {
         /// The type is the inferred error set of a specific function.
         error_set_inferred,
         error_set_merged,
-        enum_simple,
-        enum_numbered,
-        enum_full,
-        enum_nonexhaustive,
 
         pub const last_no_payload_tag = Tag.inferred_alloc_const;
         pub const no_payload_count = @enumToInt(last_no_payload_tag) + 1;
@@ -5040,9 +4834,6 @@ pub const Type = struct {
                 .function => Payload.Function,
                 .error_union => Payload.ErrorUnion,
                 .error_set_single => Payload.Name,
-                .enum_full, .enum_nonexhaustive => Payload.EnumFull,
-                .enum_simple => Payload.EnumSimple,
-                .enum_numbered => Payload.EnumNumbered,
                 .tuple => Payload.Tuple,
                 .anon_struct => Payload.AnonStruct,
             };
@@ -5341,21 +5132,6 @@ pub const Type = struct {
                 values: []Value,
             };
         };
-
-        pub const EnumFull = struct {
-            base: Payload,
-            data: *Module.EnumFull,
-        };
-
-        pub const EnumSimple = struct {
-            base: Payload = .{ .tag = .enum_simple },
-            data: *Module.EnumSimple,
-        };
-
-        pub const EnumNumbered = struct {
-            base: Payload = .{ .tag = .enum_numbered },
-            data: *Module.EnumNumbered,
-        };
     };
 
     pub const @"u1": Type = .{ .ip_index = .u1_type, .legacy = undefined };
src/TypedValue.zig
@@ -198,7 +198,7 @@ pub fn print(
             .empty_array => return writer.writeAll(".{}"),
             .enum_literal => return writer.print(".{}", .{std.zig.fmtId(val.castTag(.enum_literal).?.data)}),
             .enum_field_index => {
-                return writer.print(".{s}", .{ty.enumFieldName(val.castTag(.enum_field_index).?.data)});
+                return writer.print(".{s}", .{ty.enumFieldName(val.castTag(.enum_field_index).?.data, mod)});
             },
             .bytes => return writer.print("\"{}\"", .{std.zig.fmtEscapes(val.castTag(.bytes).?.data)}),
             .str_lit => {
src/value.zig
@@ -675,80 +675,50 @@ pub const Value = struct {
         const field_index = switch (val.tag()) {
             .enum_field_index => val.castTag(.enum_field_index).?.data,
             .the_only_possible_value => blk: {
-                assert(ty.enumFieldCount() == 1);
+                assert(ty.enumFieldCount(mod) == 1);
                 break :blk 0;
             },
             .enum_literal => i: {
                 const name = val.castTag(.enum_literal).?.data;
-                break :i ty.enumFieldIndex(name).?;
+                break :i ty.enumFieldIndex(name, mod).?;
             },
             // Assume it is already an integer and return it directly.
             else => return val,
         };
 
-        switch (ty.tag()) {
-            .enum_full, .enum_nonexhaustive => {
-                const enum_full = ty.cast(Type.Payload.EnumFull).?.data;
-                if (enum_full.values.count() != 0) {
-                    return enum_full.values.keys()[field_index];
-                } else {
-                    // Field index and integer values are the same.
-                    return mod.intValue(enum_full.tag_ty, field_index);
-                }
-            },
-            .enum_numbered => {
-                const enum_obj = ty.castTag(.enum_numbered).?.data;
-                if (enum_obj.values.count() != 0) {
-                    return enum_obj.values.keys()[field_index];
-                } else {
-                    // Field index and integer values are the same.
-                    return mod.intValue(enum_obj.tag_ty, field_index);
-                }
-            },
-            .enum_simple => {
-                // Field index and integer values are the same.
-                const tag_ty = try ty.intTagType(mod);
-                return mod.intValue(tag_ty, field_index);
-            },
-            else => unreachable,
+        const enum_type = mod.intern_pool.indexToKey(ty.ip_index).enum_type;
+        if (enum_type.values.len != 0) {
+            return enum_type.values[field_index].toValue();
+        } else {
+            // Field index and integer values are the same.
+            return mod.intValue(enum_type.tag_ty.toType(), field_index);
         }
     }
 
     pub fn tagName(val: Value, ty: Type, mod: *Module) []const u8 {
         if (ty.zigTypeTag(mod) == .Union) return val.unionTag().tagName(ty.unionTagTypeHypothetical(mod), mod);
 
+        const enum_type = mod.intern_pool.indexToKey(ty.ip_index).enum_type;
+
         const field_index = switch (val.tag()) {
             .enum_field_index => val.castTag(.enum_field_index).?.data,
             .the_only_possible_value => blk: {
-                assert(ty.enumFieldCount() == 1);
+                assert(ty.enumFieldCount(mod) == 1);
                 break :blk 0;
             },
             .enum_literal => return val.castTag(.enum_literal).?.data,
             else => field_index: {
-                const values = switch (ty.tag()) {
-                    .enum_full, .enum_nonexhaustive => ty.cast(Type.Payload.EnumFull).?.data.values,
-                    .enum_numbered => ty.castTag(.enum_numbered).?.data.values,
-                    .enum_simple => Module.EnumFull.ValueMap{},
-                    else => unreachable,
-                };
-                if (values.entries.len == 0) {
+                if (enum_type.values.len == 0) {
                     // auto-numbered enum
                     break :field_index @intCast(u32, val.toUnsignedInt(mod));
                 }
-                const int_tag_ty = ty.intTagType(mod) catch |err| switch (err) {
-                    error.OutOfMemory => @panic("OOM"), // TODO handle this failure
-                };
-                break :field_index @intCast(u32, values.getIndexContext(val, .{ .ty = int_tag_ty, .mod = mod }).?);
+                const field_index = enum_type.tagValueIndex(mod.intern_pool, val.ip_index).?;
+                break :field_index @intCast(u32, field_index);
             },
         };
 
-        const fields = switch (ty.tag()) {
-            .enum_full, .enum_nonexhaustive => ty.cast(Type.Payload.EnumFull).?.data.fields,
-            .enum_numbered => ty.castTag(.enum_numbered).?.data.fields,
-            .enum_simple => ty.castTag(.enum_simple).?.data.fields,
-            else => unreachable,
-        };
-        return fields.keys()[field_index];
+        const field_name = enum_type.names[field_index];
+        return mod.intern_pool.stringToSlice(field_name);
     }
 
     /// Asserts the value is an integer.