Commit ada0010471

Andrew Kelley <andrew@ziglang.org>
2023-08-21 23:27:34
compiler: move unions into InternPool
There are a couple concepts here worth understanding: Key.UnionType - This type is available *before* resolving the union's fields. The enum tag type, number of fields, and field names, field types, and field alignments are not available with this. InternPool.UnionType - This one can be obtained from the above type with `InternPool.loadUnionType` which asserts that the union's enum tag type has been resolved. This one has all the information available. Additionally: * ZIR: Turn an unused bit into `any_aligned_fields` flag to help semantic analysis know whether a union has explicit alignment on any fields (usually not). * Sema: delete `resolveTypeRequiresComptime` which had the same type signature and near-duplicate logic to `typeRequiresComptime`. - Make opaque types not report comptime-only (this was inconsistent between the two implementations of this function). * Implement accepted proposal #12556 which is a breaking change.
1 parent 6a54639
lib/std/dwarf/call_frame.zig
@@ -69,16 +69,9 @@ pub const Instruction = union(Opcode) {
         register: u8,
         offset: u64,
     },
-    offset_extended: struct {
-        register: u8,
-        offset: u64,
-    },
     restore: struct {
         register: u8,
     },
-    restore_extended: struct {
-        register: u8,
-    },
     nop: void,
     set_loc: struct {
         address: u64,
@@ -92,6 +85,13 @@ pub const Instruction = union(Opcode) {
     advance_loc4: struct {
         delta: u32,
     },
+    offset_extended: struct {
+        register: u8,
+        offset: u64,
+    },
+    restore_extended: struct {
+        register: u8,
+    },
     undefined: struct {
         register: u8,
     },
lib/std/meta.zig
@@ -614,9 +614,9 @@ test "std.meta.FieldEnum" {
     const Tagged = union(enum) { a: u8, b: void, c: f32 };
     try testing.expectEqual(Tag(Tagged), FieldEnum(Tagged));
 
-    const Tag2 = enum { b, c, a };
+    const Tag2 = enum { a, b, c };
     const Tagged2 = union(Tag2) { a: u8, b: void, c: f32 };
-    try testing.expect(Tag(Tagged2) != FieldEnum(Tagged2));
+    try testing.expect(Tag(Tagged2) == FieldEnum(Tagged2));
 
     const Tag3 = enum(u8) { a, b, c = 7 };
     const Tagged3 = union(Tag3) { a: u8, b: void, c: f32 };
src/arch/aarch64/abi.zig
@@ -75,14 +75,15 @@ pub fn classifyType(ty: Type, mod: *Module) Class {
 
 const sret_float_count = 4;
 fn countFloats(ty: Type, mod: *Module, maybe_float_bits: *?u16) u8 {
+    const ip = &mod.intern_pool;
     const target = mod.getTarget();
     const invalid = std.math.maxInt(u8);
     switch (ty.zigTypeTag(mod)) {
         .Union => {
-            const fields = ty.unionFields(mod);
+            const union_obj = mod.typeToUnion(ty).?;
             var max_count: u8 = 0;
-            for (fields.values()) |field| {
-                const field_count = countFloats(field.ty, mod, maybe_float_bits);
+            for (union_obj.field_types.get(ip)) |field_ty| {
+                const field_count = countFloats(field_ty.toType(), mod, maybe_float_bits);
                 if (field_count == invalid) return invalid;
                 if (field_count > max_count) max_count = field_count;
                 if (max_count > sret_float_count) return invalid;
@@ -116,11 +117,12 @@ fn countFloats(ty: Type, mod: *Module, maybe_float_bits: *?u16) u8 {
 }
 
 pub fn getFloatArrayType(ty: Type, mod: *Module) ?Type {
+    const ip = &mod.intern_pool;
     switch (ty.zigTypeTag(mod)) {
         .Union => {
-            const fields = ty.unionFields(mod);
-            for (fields.values()) |field| {
-                if (getFloatArrayType(field.ty, mod)) |some| return some;
+            const union_obj = mod.typeToUnion(ty).?;
+            for (union_obj.field_types.get(ip)) |field_ty| {
+                if (getFloatArrayType(field_ty.toType(), mod)) |some| return some;
             }
             return null;
         },
src/arch/arm/abi.zig
@@ -29,6 +29,7 @@ pub fn classifyType(ty: Type, mod: *Module, ctx: Context) Class {
 
     var maybe_float_bits: ?u16 = null;
     const max_byval_size = 512;
+    const ip = &mod.intern_pool;
     switch (ty.zigTypeTag(mod)) {
         .Struct => {
             const bit_size = ty.bitSize(mod);
@@ -54,7 +55,8 @@ pub fn classifyType(ty: Type, mod: *Module, ctx: Context) Class {
         },
         .Union => {
             const bit_size = ty.bitSize(mod);
-            if (ty.containerLayout(mod) == .Packed) {
+            const union_obj = mod.typeToUnion(ty).?;
+            if (union_obj.getLayout(ip) == .Packed) {
                 if (bit_size > 64) return .memory;
                 return .byval;
             }
@@ -62,8 +64,10 @@ pub fn classifyType(ty: Type, mod: *Module, ctx: Context) Class {
             const float_count = countFloats(ty, mod, &maybe_float_bits);
             if (float_count <= byval_float_count) return .byval;
 
-            for (ty.unionFields(mod).values()) |field| {
-                if (field.ty.bitSize(mod) > 32 or field.normalAlignment(mod) > 32) {
+            for (union_obj.field_types.get(ip), 0..) |field_ty, field_index| {
+                if (field_ty.toType().bitSize(mod) > 32 or
+                    mod.unionFieldNormalAlignment(union_obj, @intCast(field_index)) > 32)
+                {
                     return Class.arrSize(bit_size, 64);
                 }
             }
@@ -117,14 +121,15 @@ pub fn classifyType(ty: Type, mod: *Module, ctx: Context) Class {
 
 const byval_float_count = 4;
 fn countFloats(ty: Type, mod: *Module, maybe_float_bits: *?u16) u32 {
+    const ip = &mod.intern_pool;
     const target = mod.getTarget();
     const invalid = std.math.maxInt(u32);
     switch (ty.zigTypeTag(mod)) {
         .Union => {
-            const fields = ty.unionFields(mod);
+            const union_obj = mod.typeToUnion(ty).?;
             var max_count: u32 = 0;
-            for (fields.values()) |field| {
-                const field_count = countFloats(field.ty, mod, maybe_float_bits);
+            for (union_obj.field_types.get(ip)) |field_ty| {
+                const field_count = countFloats(field_ty.toType(), mod, maybe_float_bits);
                 if (field_count == invalid) return invalid;
                 if (field_count > max_count) max_count = field_count;
                 if (max_count > byval_float_count) return invalid;
src/arch/wasm/abi.zig
@@ -6,6 +6,7 @@
 
 const std = @import("std");
 const Target = std.Target;
+const assert = std.debug.assert;
 
 const Type = @import("../../type.zig").Type;
 const Module = @import("../../Module.zig");
@@ -22,6 +23,7 @@ const direct: [2]Class = .{ .direct, .none };
 /// or returned as value within a wasm function.
 /// When all elements result in `.none`, no value must be passed in or returned.
 pub fn classifyType(ty: Type, mod: *Module) [2]Class {
+    const ip = &mod.intern_pool;
     const target = mod.getTarget();
     if (!ty.hasRuntimeBitsIgnoreComptime(mod)) return none;
     switch (ty.zigTypeTag(mod)) {
@@ -56,22 +58,24 @@ pub fn classifyType(ty: Type, mod: *Module) [2]Class {
         .Bool => return direct,
         .Array => return memory,
         .Optional => {
-            std.debug.assert(ty.isPtrLikeOptional(mod));
+            assert(ty.isPtrLikeOptional(mod));
             return direct;
         },
         .Pointer => {
-            std.debug.assert(!ty.isSlice(mod));
+            assert(!ty.isSlice(mod));
             return direct;
         },
         .Union => {
-            if (ty.containerLayout(mod) == .Packed) {
+            const union_obj = mod.typeToUnion(ty).?;
+            if (union_obj.getLayout(ip) == .Packed) {
                 if (ty.bitSize(mod) <= 64) return direct;
                 return .{ .direct, .direct };
             }
             const layout = ty.unionGetLayout(mod);
-            std.debug.assert(layout.tag_size == 0);
-            if (ty.unionFields(mod).count() > 1) return memory;
-            return classifyType(ty.unionFields(mod).values()[0].ty, mod);
+            assert(layout.tag_size == 0);
+            if (union_obj.field_names.len > 1) return memory;
+            const first_field_ty = union_obj.field_types.get(ip)[0].toType();
+            return classifyType(first_field_ty, mod);
         },
         .ErrorUnion,
         .Frame,
@@ -94,6 +98,7 @@ pub fn classifyType(ty: Type, mod: *Module) [2]Class {
 /// Asserts given type can be represented as scalar, such as
 /// a struct with a single scalar field.
 pub fn scalarType(ty: Type, mod: *Module) Type {
+    const ip = &mod.intern_pool;
     switch (ty.zigTypeTag(mod)) {
         .Struct => {
             switch (ty.containerLayout(mod)) {
@@ -102,20 +107,22 @@ pub fn scalarType(ty: Type, mod: *Module) Type {
                     return scalarType(struct_obj.backing_int_ty, mod);
                 },
                 else => {
-                    std.debug.assert(ty.structFieldCount(mod) == 1);
+                    assert(ty.structFieldCount(mod) == 1);
                     return scalarType(ty.structFieldType(0, mod), mod);
                 },
             }
         },
         .Union => {
-            if (ty.containerLayout(mod) != .Packed) {
-                const layout = ty.unionGetLayout(mod);
+            const union_obj = mod.typeToUnion(ty).?;
+            if (union_obj.getLayout(ip) != .Packed) {
+                const layout = mod.getUnionLayout(union_obj);
                 if (layout.payload_size == 0 and layout.tag_size != 0) {
                     return scalarType(ty.unionTagTypeSafety(mod).?, mod);
                 }
-                std.debug.assert(ty.unionFields(mod).count() == 1);
+                assert(union_obj.field_types.len == 1);
             }
-            return scalarType(ty.unionFields(mod).values()[0].ty, mod);
+            const first_field_ty = union_obj.field_types.get(ip)[0].toType();
+            return scalarType(first_field_ty, mod);
         },
         else => return ty,
     }
src/arch/wasm/CodeGen.zig
@@ -1717,6 +1717,7 @@ fn arch(func: *const CodeGen) std.Target.Cpu.Arch {
 /// For a given `Type`, will return true when the type will be passed
 /// by reference, rather than by value
 fn isByRef(ty: Type, mod: *Module) bool {
+    const ip = &mod.intern_pool;
     const target = mod.getTarget();
     switch (ty.zigTypeTag(mod)) {
         .Type,
@@ -1742,7 +1743,7 @@ fn isByRef(ty: Type, mod: *Module) bool {
         => return ty.hasRuntimeBitsIgnoreComptime(mod),
         .Union => {
             if (mod.typeToUnion(ty)) |union_obj| {
-                if (union_obj.layout == .Packed) {
+                if (union_obj.getLayout(ip) == .Packed) {
                     return ty.abiSize(mod) > 8;
                 }
             }
@@ -2974,7 +2975,7 @@ fn lowerParentPtr(func: *CodeGen, ptr_val: Value, offset: u32) InnerError!WValue
                 .Union => switch (parent_ty.containerLayout(mod)) {
                     .Packed => 0,
                     else => blk: {
-                        const layout: Module.Union.Layout = parent_ty.unionGetLayout(mod);
+                        const layout: Module.UnionLayout = parent_ty.unionGetLayout(mod);
                         if (layout.payload_size == 0) break :blk 0;
                         if (layout.payload_align > layout.tag_align) break :blk 0;
 
@@ -3058,8 +3059,9 @@ fn toTwosComplement(value: anytype, bits: u7) std.meta.Int(.unsigned, @typeInfo(
 
 fn lowerConstant(func: *CodeGen, arg_val: Value, ty: Type) InnerError!WValue {
     const mod = func.bin_file.base.options.module.?;
+    const ip = &mod.intern_pool;
     var val = arg_val;
-    switch (mod.intern_pool.indexToKey(val.ip_index)) {
+    switch (ip.indexToKey(val.ip_index)) {
         .runtime_value => |rt| val = rt.val.toValue(),
         else => {},
     }
@@ -3110,7 +3112,7 @@ fn lowerConstant(func: *CodeGen, arg_val: Value, ty: Type) InnerError!WValue {
         => unreachable, // comptime-only types
     };
 
-    switch (mod.intern_pool.indexToKey(val.ip_index)) {
+    switch (ip.indexToKey(val.ip_index)) {
         .int_type,
         .ptr_type,
         .array_type,
@@ -3198,7 +3200,7 @@ fn lowerConstant(func: *CodeGen, arg_val: Value, ty: Type) InnerError!WValue {
             return func.fail("Wasm TODO: lowerConstant error union with non-zero-bit payload type", .{});
         },
         .enum_tag => |enum_tag| {
-            const int_tag_ty = mod.intern_pool.typeOf(enum_tag.int);
+            const int_tag_ty = ip.typeOf(enum_tag.int);
             return func.lowerConstant(enum_tag.int.toValue(), int_tag_ty.toType());
         },
         .float => |float| switch (float.storage) {
@@ -3210,7 +3212,7 @@ fn lowerConstant(func: *CodeGen, arg_val: Value, ty: Type) InnerError!WValue {
         .ptr => |ptr| switch (ptr.addr) {
             .decl => |decl| return func.lowerDeclRefValue(.{ .ty = ty, .val = val }, decl, 0),
             .mut_decl => |mut_decl| return func.lowerDeclRefValue(.{ .ty = ty, .val = val }, mut_decl.decl, 0),
-            .int => |int| return func.lowerConstant(int.toValue(), mod.intern_pool.typeOf(int).toType()),
+            .int => |int| return func.lowerConstant(int.toValue(), ip.typeOf(int).toType()),
             .opt_payload, .elem, .field => return func.lowerParentPtr(val, 0),
             else => return func.fail("Wasm TODO: lowerConstant for other const addr tag {}", .{ptr.addr}),
         },
@@ -3224,7 +3226,7 @@ fn lowerConstant(func: *CodeGen, arg_val: Value, ty: Type) InnerError!WValue {
         } else {
             return WValue{ .imm32 = @intFromBool(!val.isNull(mod)) };
         },
-        .aggregate => switch (mod.intern_pool.indexToKey(ty.ip_index)) {
+        .aggregate => switch (ip.indexToKey(ty.ip_index)) {
             .array_type => return func.fail("Wasm TODO: LowerConstant for {}", .{ty.fmt(mod)}),
             .vector_type => {
                 assert(determineSimdStoreStrategy(ty, mod) == .direct);
@@ -3245,11 +3247,12 @@ fn lowerConstant(func: *CodeGen, arg_val: Value, ty: Type) InnerError!WValue {
             },
             else => unreachable,
         },
-        .un => |union_obj| {
+        .un => |un| {
             // in this case we have a packed union which will not be passed by reference.
-            const field_index = ty.unionTagFieldIndex(union_obj.tag.toValue(), func.bin_file.base.options.module.?).?;
-            const field_ty = ty.unionFields(mod).values()[field_index].ty;
-            return func.lowerConstant(union_obj.val.toValue(), field_ty);
+            const union_obj = mod.typeToUnion(ty).?;
+            const field_index = mod.unionTagFieldIndex(union_obj, un.tag.toValue()).?;
+            const field_ty = union_obj.field_types.get(ip)[field_index].toType();
+            return func.lowerConstant(un.val.toValue(), field_ty);
         },
         .memoized_call => unreachable,
     }
@@ -5163,6 +5166,7 @@ fn airAggregateInit(func: *CodeGen, inst: Air.Inst.Index) InnerError!void {
 
 fn airUnionInit(func: *CodeGen, inst: Air.Inst.Index) InnerError!void {
     const mod = func.bin_file.base.options.module.?;
+    const ip = &mod.intern_pool;
     const ty_pl = func.air.instructions.items(.data)[inst].ty_pl;
     const extra = func.air.extraData(Air.UnionInit, ty_pl.payload).data;
 
@@ -5170,8 +5174,8 @@ fn airUnionInit(func: *CodeGen, inst: Air.Inst.Index) InnerError!void {
         const union_ty = func.typeOfIndex(inst);
         const layout = union_ty.unionGetLayout(mod);
         const union_obj = mod.typeToUnion(union_ty).?;
-        const field = union_obj.fields.values()[extra.field_index];
-        const field_name = union_obj.fields.keys()[extra.field_index];
+        const field_ty = union_obj.field_types.get(ip)[extra.field_index].toType();
+        const field_name = union_obj.field_names.get(ip)[extra.field_index];
 
         const tag_int = blk: {
             const tag_ty = union_ty.unionTagTypeHypothetical(mod);
@@ -5191,24 +5195,24 @@ fn airUnionInit(func: *CodeGen, inst: Air.Inst.Index) InnerError!void {
             const result_ptr = try func.allocStack(union_ty);
             const payload = try func.resolveInst(extra.init);
             if (layout.tag_align >= layout.payload_align) {
-                if (isByRef(field.ty, mod)) {
+                if (isByRef(field_ty, mod)) {
                     const payload_ptr = try func.buildPointerOffset(result_ptr, layout.tag_size, .new);
-                    try func.store(payload_ptr, payload, field.ty, 0);
+                    try func.store(payload_ptr, payload, field_ty, 0);
                 } else {
-                    try func.store(result_ptr, payload, field.ty, @as(u32, @intCast(layout.tag_size)));
+                    try func.store(result_ptr, payload, field_ty, @intCast(layout.tag_size));
                 }
 
                 if (layout.tag_size > 0) {
-                    try func.store(result_ptr, tag_int, union_obj.tag_ty, 0);
+                    try func.store(result_ptr, tag_int, union_obj.enum_tag_ty.toType(), 0);
                 }
             } else {
-                try func.store(result_ptr, payload, field.ty, 0);
+                try func.store(result_ptr, payload, field_ty, 0);
                 if (layout.tag_size > 0) {
                     try func.store(
                         result_ptr,
                         tag_int,
-                        union_obj.tag_ty,
-                        @as(u32, @intCast(layout.payload_size)),
+                        union_obj.enum_tag_ty.toType(),
+                        @intCast(layout.payload_size),
                     );
                 }
             }
@@ -5216,18 +5220,18 @@ fn airUnionInit(func: *CodeGen, inst: Air.Inst.Index) InnerError!void {
         } else {
             const operand = try func.resolveInst(extra.init);
             const union_int_type = try mod.intType(.unsigned, @as(u16, @intCast(union_ty.bitSize(mod))));
-            if (field.ty.zigTypeTag(mod) == .Float) {
-                const int_type = try mod.intType(.unsigned, @as(u16, @intCast(field.ty.bitSize(mod))));
-                const bitcasted = try func.bitcast(field.ty, int_type, operand);
+            if (field_ty.zigTypeTag(mod) == .Float) {
+                const int_type = try mod.intType(.unsigned, @intCast(field_ty.bitSize(mod)));
+                const bitcasted = try func.bitcast(field_ty, int_type, operand);
                 const casted = try func.trunc(bitcasted, int_type, union_int_type);
-                break :result try casted.toLocal(func, field.ty);
-            } else if (field.ty.isPtrAtRuntime(mod)) {
-                const int_type = try mod.intType(.unsigned, @as(u16, @intCast(field.ty.bitSize(mod))));
+                break :result try casted.toLocal(func, field_ty);
+            } else if (field_ty.isPtrAtRuntime(mod)) {
+                const int_type = try mod.intType(.unsigned, @intCast(field_ty.bitSize(mod)));
                 const casted = try func.intcast(operand, int_type, union_int_type);
-                break :result try casted.toLocal(func, field.ty);
+                break :result try casted.toLocal(func, field_ty);
             }
-            const casted = try func.intcast(operand, field.ty, union_int_type);
-            break :result try casted.toLocal(func, field.ty);
+            const casted = try func.intcast(operand, field_ty, union_int_type);
+            break :result try casted.toLocal(func, field_ty);
         }
     };
 
src/arch/x86_64/abi.zig
@@ -69,6 +69,7 @@ pub const Context = enum { ret, arg, other };
 /// There are a maximum of 8 possible return slots. Returned values are in
 /// the beginning of the array; unused slots are filled with .none.
 pub fn classifySystemV(ty: Type, mod: *Module, ctx: Context) [8]Class {
+    const ip = &mod.intern_pool;
     const target = mod.getTarget();
     const memory_class = [_]Class{
         .memory, .none, .none, .none,
@@ -328,8 +329,9 @@ pub fn classifySystemV(ty: Type, mod: *Module, ctx: Context) [8]Class {
             // it contains unaligned fields, it has class MEMORY"
             // "If the size of the aggregate exceeds a single eightbyte, each is classified
             // separately.".
-            const ty_size = ty.abiSize(mod);
-            if (ty.containerLayout(mod) == .Packed) {
+            const union_obj = mod.typeToUnion(ty).?;
+            const ty_size = mod.unionAbiSize(union_obj);
+            if (union_obj.getLayout(ip) == .Packed) {
                 assert(ty_size <= 128);
                 result[0] = .integer;
                 if (ty_size > 64) result[1] = .integer;
@@ -338,15 +340,14 @@ pub fn classifySystemV(ty: Type, mod: *Module, ctx: Context) [8]Class {
             if (ty_size > 64)
                 return memory_class;
 
-            const fields = ty.unionFields(mod);
-            for (fields.values()) |field| {
-                if (field.abi_align != .none) {
-                    if (field.abi_align.toByteUnitsOptional().? < field.ty.abiAlignment(mod)) {
+            for (union_obj.field_types.get(ip), 0..) |field_ty, field_index| {
+                if (union_obj.fieldAlign(ip, @intCast(field_index)).toByteUnitsOptional()) |a| {
+                    if (a < field_ty.toType().abiAlignment(mod)) {
                         return memory_class;
                     }
                 }
                 // Combine this field with the previous one.
-                const field_class = classifySystemV(field.ty, mod, .other);
+                const field_class = classifySystemV(field_ty.toType(), mod, .other);
                 for (&result, 0..) |*result_item, i| {
                     const field_item = field_class[i];
                     // "If both classes are equal, this is the resulting class."
src/arch/x86_64/CodeGen.zig
@@ -11534,6 +11534,7 @@ fn airAggregateInit(self: *Self, inst: Air.Inst.Index) !void {
 
 fn airUnionInit(self: *Self, inst: Air.Inst.Index) !void {
     const mod = self.bin_file.options.module.?;
+    const ip = &mod.intern_pool;
     const ty_pl = self.air.instructions.items(.data)[inst].ty_pl;
     const extra = self.air.extraData(Air.UnionInit, ty_pl.payload).data;
     const result: MCValue = result: {
@@ -11553,8 +11554,8 @@ fn airUnionInit(self: *Self, inst: Air.Inst.Index) !void {
         const dst_mcv = try self.allocRegOrMem(inst, false);
 
         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_name = union_obj.field_names.get(ip)[extra.field_index];
+        const tag_ty = union_obj.enum_tag_ty.toType();
         const field_index = tag_ty.enumFieldIndex(field_name, mod).?;
         const tag_val = try mod.enumValueFieldIndex(tag_ty, field_index);
         const tag_int_val = try tag_val.intFromEnum(tag_ty, mod);
src/codegen/c/type.zig
@@ -303,7 +303,7 @@ pub const CType = extern union {
         }
         pub fn unionPayloadAlign(union_ty: Type, mod: *Module) AlignAs {
             const union_obj = mod.typeToUnion(union_ty).?;
-            const union_payload_align = union_obj.abiAlignment(mod, false);
+            const union_payload_align = mod.unionAbiAlignment(union_obj);
             return init(union_payload_align, union_payload_align);
         }
 
@@ -1499,7 +1499,7 @@ pub const CType = extern union {
                     if (lookup.isMutable()) {
                         for (0..switch (zig_ty_tag) {
                             .Struct => ty.structFieldCount(mod),
-                            .Union => ty.unionFields(mod).count(),
+                            .Union => mod.typeToUnion(ty).?.field_names.len,
                             else => unreachable,
                         }) |field_i| {
                             const field_ty = ty.structFieldType(field_i, mod);
@@ -1581,7 +1581,7 @@ pub const CType = extern union {
                             var is_packed = false;
                             for (0..switch (zig_ty_tag) {
                                 .Struct => ty.structFieldCount(mod),
-                                .Union => ty.unionFields(mod).count(),
+                                .Union => mod.typeToUnion(ty).?.field_names.len,
                                 else => unreachable,
                             }) |field_i| {
                                 const field_ty = ty.structFieldType(field_i, mod);
@@ -1912,6 +1912,7 @@ pub const CType = extern union {
         kind: Kind,
         convert: Convert,
     ) !CType {
+        const ip = &mod.intern_pool;
         const arena = store.arena.allocator();
         switch (convert.value) {
             .cty => |c| return c.copy(arena),
@@ -1932,7 +1933,7 @@ pub const CType = extern union {
                     const zig_ty_tag = ty.zigTypeTag(mod);
                     const fields_len = switch (zig_ty_tag) {
                         .Struct => ty.structFieldCount(mod),
-                        .Union => ty.unionFields(mod).count(),
+                        .Union => mod.typeToUnion(ty).?.field_names.len,
                         else => unreachable,
                     };
 
@@ -1956,9 +1957,9 @@ pub const CType = extern union {
                             .name = try if (ty.isSimpleTuple(mod))
                                 std.fmt.allocPrintZ(arena, "f{}", .{field_i})
                             else
-                                arena.dupeZ(u8, mod.intern_pool.stringToSlice(switch (zig_ty_tag) {
+                                arena.dupeZ(u8, ip.stringToSlice(switch (zig_ty_tag) {
                                     .Struct => ty.structFieldName(field_i, mod),
-                                    .Union => ty.unionFields(mod).keys()[field_i],
+                                    .Union => mod.typeToUnion(ty).?.field_names.get(ip)[field_i],
                                     else => unreachable,
                                 })),
                             .type = store.set.typeToIndex(field_ty, mod, switch (kind) {
@@ -2015,7 +2016,6 @@ pub const CType = extern union {
                 .function,
                 .varargs_function,
                 => {
-                    const ip = &mod.intern_pool;
                     const info = mod.typeToFunc(ty).?;
                     assert(!info.is_generic);
                     const param_kind: Kind = switch (kind) {
@@ -2068,6 +2068,7 @@ pub const CType = extern union {
 
         pub fn eql(self: @This(), ty: Type, cty: CType) bool {
             const mod = self.lookup.getModule();
+            const ip = &mod.intern_pool;
             switch (self.convert.value) {
                 .cty => |c| return c.eql(cty),
                 .tag => |t| {
@@ -2088,7 +2089,7 @@ pub const CType = extern union {
                             var c_field_i: usize = 0;
                             for (0..switch (zig_ty_tag) {
                                 .Struct => ty.structFieldCount(mod),
-                                .Union => ty.unionFields(mod).count(),
+                                .Union => mod.typeToUnion(ty).?.field_names.len,
                                 else => unreachable,
                             }) |field_i| {
                                 const field_ty = ty.structFieldType(field_i, mod);
@@ -2108,9 +2109,9 @@ pub const CType = extern union {
                                     if (ty.isSimpleTuple(mod))
                                         std.fmt.bufPrintZ(&name_buf, "f{}", .{field_i}) catch unreachable
                                     else
-                                        mod.intern_pool.stringToSlice(switch (zig_ty_tag) {
+                                        ip.stringToSlice(switch (zig_ty_tag) {
                                             .Struct => ty.structFieldName(field_i, mod),
-                                            .Union => ty.unionFields(mod).keys()[field_i],
+                                            .Union => mod.typeToUnion(ty).?.field_names.get(ip)[field_i],
                                             else => unreachable,
                                         }),
                                     mem.span(c_field.name),
@@ -2149,7 +2150,6 @@ pub const CType = extern union {
                         => {
                             if (ty.zigTypeTag(mod) != .Fn) return false;
 
-                            const ip = &mod.intern_pool;
                             const info = mod.typeToFunc(ty).?;
                             assert(!info.is_generic);
                             const data = cty.cast(Payload.Function).?.data;
@@ -2217,7 +2217,7 @@ pub const CType = extern union {
                             const zig_ty_tag = ty.zigTypeTag(mod);
                             for (0..switch (ty.zigTypeTag(mod)) {
                                 .Struct => ty.structFieldCount(mod),
-                                .Union => ty.unionFields(mod).count(),
+                                .Union => mod.typeToUnion(ty).?.field_names.len,
                                 else => unreachable,
                             }) |field_i| {
                                 const field_ty = ty.structFieldType(field_i, mod);
@@ -2235,7 +2235,7 @@ pub const CType = extern union {
                                 else
                                     mod.intern_pool.stringToSlice(switch (zig_ty_tag) {
                                         .Struct => ty.structFieldName(field_i, mod),
-                                        .Union => ty.unionFields(mod).keys()[field_i],
+                                        .Union => mod.typeToUnion(ty).?.field_names.get(ip)[field_i],
                                         else => unreachable,
                                     }));
                                 autoHash(hasher, AlignAs.fieldAlign(ty, field_i, mod).@"align");
src/codegen/c.zig
@@ -708,8 +708,10 @@ pub const DeclGen = struct {
         location: ValueRenderLocation,
     ) error{ OutOfMemory, AnalysisFail }!void {
         const mod = dg.module;
+        const ip = &mod.intern_pool;
+
         var val = arg_val;
-        switch (mod.intern_pool.indexToKey(val.ip_index)) {
+        switch (ip.indexToKey(val.ip_index)) {
             .runtime_value => |rt| val = rt.val.toValue(),
             else => {},
         }
@@ -836,9 +838,10 @@ pub const DeclGen = struct {
                         if (layout.tag_size != 0) try writer.writeByte(',');
                         try writer.writeAll(" .payload = {");
                     }
-                    for (ty.unionFields(mod).values()) |field| {
-                        if (!field.ty.hasRuntimeBits(mod)) continue;
-                        try dg.renderValue(writer, field.ty, val, initializer_type);
+                    const union_obj = mod.typeToUnion(ty).?;
+                    for (union_obj.field_types.get(ip)) |field_ty| {
+                        if (!field_ty.toType().hasRuntimeBits(mod)) continue;
+                        try dg.renderValue(writer, field_ty.toType(), val, initializer_type);
                         break;
                     }
                     if (ty.unionTagTypeSafety(mod)) |_| try writer.writeByte('}');
@@ -912,7 +915,7 @@ pub const DeclGen = struct {
             unreachable;
         }
 
-        switch (mod.intern_pool.indexToKey(val.ip_index)) {
+        switch (ip.indexToKey(val.ip_index)) {
             // types, not values
             .int_type,
             .ptr_type,
@@ -962,7 +965,7 @@ pub const DeclGen = struct {
                 },
             },
             .err => |err| try writer.print("zig_error_{}", .{
-                fmtIdent(mod.intern_pool.stringToSlice(err.name)),
+                fmtIdent(ip.stringToSlice(err.name)),
             }),
             .error_union => |error_union| {
                 const payload_ty = ty.errorUnionPayload(mod);
@@ -1024,8 +1027,8 @@ pub const DeclGen = struct {
                 try writer.writeAll(" }");
             },
             .enum_tag => {
-                const enum_tag = mod.intern_pool.indexToKey(val.ip_index).enum_tag;
-                const int_tag_ty = mod.intern_pool.typeOf(enum_tag.int);
+                const enum_tag = ip.indexToKey(val.ip_index).enum_tag;
+                const int_tag_ty = ip.typeOf(enum_tag.int);
                 try dg.renderValue(writer, int_tag_ty.toType(), enum_tag.int.toValue(), location);
             },
             .float => {
@@ -1205,7 +1208,7 @@ pub const DeclGen = struct {
                 try dg.renderValue(writer, Type.bool, is_null_val, initializer_type);
                 try writer.writeAll(" }");
             },
-            .aggregate => switch (mod.intern_pool.indexToKey(ty.ip_index)) {
+            .aggregate => switch (ip.indexToKey(ty.ip_index)) {
                 .array_type, .vector_type => {
                     if (location == .FunctionArgument) {
                         try writer.writeByte('(');
@@ -1278,8 +1281,8 @@ pub const DeclGen = struct {
 
                         if (!empty) try writer.writeByte(',');
 
-                        const field_val = switch (mod.intern_pool.indexToKey(val.ip_index).aggregate.storage) {
-                            .bytes => |bytes| try mod.intern_pool.get(mod.gpa, .{ .int = .{
+                        const field_val = switch (ip.indexToKey(val.ip_index).aggregate.storage) {
+                            .bytes => |bytes| try ip.get(mod.gpa, .{ .int = .{
                                 .ty = field_ty,
                                 .storage = .{ .u64 = bytes[field_i] },
                             } }),
@@ -1309,8 +1312,8 @@ pub const DeclGen = struct {
                                 if (!field.ty.hasRuntimeBitsIgnoreComptime(mod)) continue;
 
                                 if (!empty) try writer.writeByte(',');
-                                const field_val = switch (mod.intern_pool.indexToKey(val.ip_index).aggregate.storage) {
-                                    .bytes => |bytes| try mod.intern_pool.get(mod.gpa, .{ .int = .{
+                                const field_val = switch (ip.indexToKey(val.ip_index).aggregate.storage) {
+                                    .bytes => |bytes| try ip.get(mod.gpa, .{ .int = .{
                                         .ty = field.ty.toIntern(),
                                         .storage = .{ .u64 = bytes[field_i] },
                                     } }),
@@ -1358,8 +1361,8 @@ pub const DeclGen = struct {
                                     if (field.is_comptime) continue;
                                     if (!field.ty.hasRuntimeBitsIgnoreComptime(mod)) continue;
 
-                                    const field_val = switch (mod.intern_pool.indexToKey(val.ip_index).aggregate.storage) {
-                                        .bytes => |bytes| try mod.intern_pool.get(mod.gpa, .{ .int = .{
+                                    const field_val = switch (ip.indexToKey(val.ip_index).aggregate.storage) {
+                                        .bytes => |bytes| try ip.get(mod.gpa, .{ .int = .{
                                             .ty = field.ty.toIntern(),
                                             .storage = .{ .u64 = bytes[field_i] },
                                         } }),
@@ -1400,8 +1403,8 @@ pub const DeclGen = struct {
                                     try dg.renderType(writer, ty);
                                     try writer.writeByte(')');
 
-                                    const field_val = switch (mod.intern_pool.indexToKey(val.ip_index).aggregate.storage) {
-                                        .bytes => |bytes| try mod.intern_pool.get(mod.gpa, .{ .int = .{
+                                    const field_val = switch (ip.indexToKey(val.ip_index).aggregate.storage) {
+                                        .bytes => |bytes| try ip.get(mod.gpa, .{ .int = .{
                                             .ty = field.ty.toIntern(),
                                             .storage = .{ .u64 = bytes[field_i] },
                                         } }),
@@ -1435,10 +1438,11 @@ pub const DeclGen = struct {
                     try writer.writeByte(')');
                 }
 
-                const field_i = ty.unionTagFieldIndex(un.tag.toValue(), mod).?;
-                const field_ty = ty.unionFields(mod).values()[field_i].ty;
-                const field_name = ty.unionFields(mod).keys()[field_i];
-                if (ty.containerLayout(mod) == .Packed) {
+                const union_obj = mod.typeToUnion(ty).?;
+                const field_i = mod.unionTagFieldIndex(union_obj, un.tag.toValue()).?;
+                const field_ty = union_obj.field_types.get(ip)[field_i].toType();
+                const field_name = union_obj.field_names.get(ip)[field_i];
+                if (union_obj.getLayout(ip) == .Packed) {
                     if (field_ty.hasRuntimeBits(mod)) {
                         if (field_ty.isPtrAtRuntime(mod)) {
                             try writer.writeByte('(');
@@ -1458,7 +1462,7 @@ pub const DeclGen = struct {
 
                 try writer.writeByte('{');
                 if (ty.unionTagTypeSafety(mod)) |tag_ty| {
-                    const layout = ty.unionGetLayout(mod);
+                    const layout = mod.getUnionLayout(union_obj);
                     if (layout.tag_size != 0) {
                         try writer.writeAll(" .tag = ");
                         try dg.renderValue(writer, tag_ty, un.tag.toValue(), initializer_type);
@@ -1468,12 +1472,12 @@ pub const DeclGen = struct {
                     try writer.writeAll(" .payload = {");
                 }
                 if (field_ty.hasRuntimeBits(mod)) {
-                    try writer.print(" .{ } = ", .{fmtIdent(mod.intern_pool.stringToSlice(field_name))});
+                    try writer.print(" .{ } = ", .{fmtIdent(ip.stringToSlice(field_name))});
                     try dg.renderValue(writer, field_ty, un.val.toValue(), initializer_type);
                     try writer.writeByte(' ');
-                } else for (ty.unionFields(mod).values()) |field| {
-                    if (!field.ty.hasRuntimeBits(mod)) continue;
-                    try dg.renderValue(writer, field.ty, Value.undef, initializer_type);
+                } else for (union_obj.field_types.get(ip)) |this_field_ty| {
+                    if (!this_field_ty.toType().hasRuntimeBits(mod)) continue;
+                    try dg.renderValue(writer, this_field_ty.toType(), Value.undef, initializer_type);
                     break;
                 }
                 if (ty.unionTagTypeSafety(mod)) |_| try writer.writeByte('}');
@@ -5237,22 +5241,25 @@ fn fieldLocation(
             else
                 .begin,
         },
-        .Union => switch (container_ty.containerLayout(mod)) {
-            .Auto, .Extern => {
-                const field_ty = container_ty.structFieldType(field_index, mod);
-                if (!field_ty.hasRuntimeBitsIgnoreComptime(mod))
-                    return if (container_ty.unionTagTypeSafety(mod) != null and
-                        !container_ty.unionHasAllZeroBitFieldTypes(mod))
-                        .{ .field = .{ .identifier = "payload" } }
+        .Union => {
+            const union_obj = mod.typeToUnion(container_ty).?;
+            return switch (union_obj.getLayout(ip)) {
+                .Auto, .Extern => {
+                    const field_ty = union_obj.field_types.get(ip)[field_index].toType();
+                    if (!field_ty.hasRuntimeBitsIgnoreComptime(mod))
+                        return if (container_ty.unionTagTypeSafety(mod) != null and
+                            !container_ty.unionHasAllZeroBitFieldTypes(mod))
+                            .{ .field = .{ .identifier = "payload" } }
+                        else
+                            .begin;
+                    const field_name = union_obj.field_names.get(ip)[field_index];
+                    return .{ .field = if (container_ty.unionTagTypeSafety(mod)) |_|
+                        .{ .payload_identifier = ip.stringToSlice(field_name) }
                     else
-                        .begin;
-                const field_name = container_ty.unionFields(mod).keys()[field_index];
-                return .{ .field = if (container_ty.unionTagTypeSafety(mod)) |_|
-                    .{ .payload_identifier = ip.stringToSlice(field_name) }
-                else
-                    .{ .identifier = ip.stringToSlice(field_name) } };
-            },
-            .Packed => .begin,
+                        .{ .identifier = ip.stringToSlice(field_name) } };
+                },
+                .Packed => .begin,
+            };
         },
         .Pointer => switch (container_ty.ptrSize(mod)) {
             .Slice => switch (field_index) {
@@ -5479,8 +5486,8 @@ fn airStructFieldVal(f: *Function, inst: Air.Inst.Index) !CValue {
             .{ .identifier = ip.stringToSlice(struct_ty.structFieldName(extra.field_index, mod)) },
 
         .union_type => |union_type| field_name: {
-            const union_obj = mod.unionPtr(union_type.index);
-            if (union_obj.layout == .Packed) {
+            const union_obj = ip.loadUnionType(union_type);
+            if (union_obj.flagsPtr(ip).layout == .Packed) {
                 const operand_lval = if (struct_byval == .constant) blk: {
                     const operand_local = try f.allocLocal(inst, struct_ty);
                     try f.writeCValue(writer, operand_local, .Other);
@@ -5505,8 +5512,8 @@ fn airStructFieldVal(f: *Function, inst: Air.Inst.Index) !CValue {
 
                 return local;
             } else {
-                const name = union_obj.fields.keys()[extra.field_index];
-                break :field_name if (union_type.hasTag()) .{
+                const name = union_obj.field_names.get(ip)[extra.field_index];
+                break :field_name if (union_type.hasTag(ip)) .{
                     .payload_identifier = ip.stringToSlice(name),
                 } else .{
                     .identifier = ip.stringToSlice(name),
@@ -6902,14 +6909,14 @@ fn airUnionInit(f: *Function, inst: Air.Inst.Index) !CValue {
 
     const union_ty = f.typeOfIndex(inst);
     const union_obj = mod.typeToUnion(union_ty).?;
-    const field_name = union_obj.fields.keys()[extra.field_index];
+    const field_name = union_obj.field_names.get(ip)[extra.field_index];
     const payload_ty = f.typeOf(extra.init);
     const payload = try f.resolveInst(extra.init);
     try reap(f, inst, &.{extra.init});
 
     const writer = f.object.writer();
     const local = try f.allocLocal(inst, union_ty);
-    if (union_obj.layout == .Packed) {
+    if (union_obj.getLayout(ip) == .Packed) {
         try f.writeCValue(writer, local, .Other);
         try writer.writeAll(" = ");
         try f.writeCValue(writer, payload, .Initializer);
src/codegen/llvm.zig
@@ -2382,7 +2382,7 @@ pub const Object = struct {
                     break :blk fwd_decl;
                 };
 
-                switch (mod.intern_pool.indexToKey(ty.toIntern())) {
+                switch (ip.indexToKey(ty.toIntern())) {
                     .anon_struct_type => |tuple| {
                         var di_fields: std.ArrayListUnmanaged(*llvm.DIType) = .{};
                         defer di_fields.deinit(gpa);
@@ -2401,7 +2401,7 @@ pub const Object = struct {
                             offset = field_offset + field_size;
 
                             const field_name = if (tuple.names.len != 0)
-                                mod.intern_pool.stringToSlice(tuple.names[i])
+                                ip.stringToSlice(tuple.names[i])
                             else
                                 try std.fmt.allocPrintZ(gpa, "{d}", .{i});
                             defer if (tuple.names.len == 0) gpa.free(field_name);
@@ -2491,7 +2491,7 @@ pub const Object = struct {
                     const field_offset = std.mem.alignForward(u64, offset, field_align);
                     offset = field_offset + field_size;
 
-                    const field_name = mod.intern_pool.stringToSlice(fields.keys()[field_and_index.index]);
+                    const field_name = ip.stringToSlice(fields.keys()[field_and_index.index]);
 
                     try di_fields.append(gpa, dib.createMemberType(
                         fwd_decl.toScope(),
@@ -2546,8 +2546,8 @@ pub const Object = struct {
                     break :blk fwd_decl;
                 };
 
-                const union_obj = mod.typeToUnion(ty).?;
-                if (!union_obj.haveFieldTypes() or !ty.hasRuntimeBitsIgnoreComptime(mod)) {
+                const union_type = ip.indexToKey(ty.toIntern()).union_type;
+                if (!union_type.haveFieldTypes(ip) or !ty.hasRuntimeBitsIgnoreComptime(mod)) {
                     const union_di_ty = try o.makeEmptyNamespaceDIType(owner_decl_index);
                     dib.replaceTemporary(fwd_decl, union_di_ty);
                     // The recursive call to `lowerDebugType` via `makeEmptyNamespaceDIType`
@@ -2556,10 +2556,11 @@ pub const Object = struct {
                     return union_di_ty;
                 }
 
-                const layout = ty.unionGetLayout(mod);
+                const union_obj = ip.loadUnionType(union_type);
+                const layout = mod.getUnionLayout(union_obj);
 
                 if (layout.payload_size == 0) {
-                    const tag_di_ty = try o.lowerDebugType(union_obj.tag_ty, .full);
+                    const tag_di_ty = try o.lowerDebugType(union_obj.enum_tag_ty.toType(), .full);
                     const di_fields = [_]*llvm.DIType{tag_di_ty};
                     const full_di_ty = dib.createStructType(
                         compile_unit_scope,
@@ -2586,22 +2587,20 @@ pub const Object = struct {
                 var di_fields: std.ArrayListUnmanaged(*llvm.DIType) = .{};
                 defer di_fields.deinit(gpa);
 
-                try di_fields.ensureUnusedCapacity(gpa, union_obj.fields.count());
+                try di_fields.ensureUnusedCapacity(gpa, union_obj.field_names.len);
 
-                var it = union_obj.fields.iterator();
-                while (it.next()) |kv| {
-                    const field_name = kv.key_ptr.*;
-                    const field = kv.value_ptr.*;
+                for (0..union_obj.field_names.len) |field_index| {
+                    const field_ty = union_obj.field_types.get(ip)[field_index];
+                    if (!field_ty.toType().hasRuntimeBitsIgnoreComptime(mod)) continue;
 
-                    if (!field.ty.hasRuntimeBitsIgnoreComptime(mod)) continue;
-
-                    const field_size = field.ty.abiSize(mod);
-                    const field_align = field.normalAlignment(mod);
+                    const field_size = field_ty.toType().abiSize(mod);
+                    const field_align = mod.unionFieldNormalAlignment(union_obj, @intCast(field_index));
 
-                    const field_di_ty = try o.lowerDebugType(field.ty, .full);
+                    const field_di_ty = try o.lowerDebugType(field_ty.toType(), .full);
+                    const field_name = union_obj.field_names.get(ip)[field_index];
                     di_fields.appendAssumeCapacity(dib.createMemberType(
                         fwd_decl.toScope(),
-                        mod.intern_pool.stringToSlice(field_name),
+                        ip.stringToSlice(field_name),
                         null, // file
                         0, // line
                         field_size * 8, // size in bits
@@ -2659,7 +2658,7 @@ pub const Object = struct {
                     layout.tag_align * 8, // align in bits
                     tag_offset * 8, // offset in bits
                     0, // flags
-                    try o.lowerDebugType(union_obj.tag_ty, .full),
+                    try o.lowerDebugType(union_obj.enum_tag_ty.toType(), .full),
                 );
 
                 const payload_di = dib.createMemberType(
@@ -3078,6 +3077,7 @@ pub const Object = struct {
     fn lowerTypeInner(o: *Object, t: Type) Allocator.Error!Builder.Type {
         const mod = o.module;
         const target = mod.getTarget();
+        const ip = &mod.intern_pool;
         return switch (t.toIntern()) {
             .u0_type, .i0_type => unreachable,
             inline .u1_type,
@@ -3172,7 +3172,7 @@ pub const Object = struct {
             .var_args_param_type,
             .none,
             => unreachable,
-            else => switch (mod.intern_pool.indexToKey(t.toIntern())) {
+            else => switch (ip.indexToKey(t.toIntern())) {
                 .int_type => |int_type| try o.builder.intType(int_type.bits),
                 .ptr_type => |ptr_type| type: {
                     const ptr_ty = try o.builder.ptrType(
@@ -3264,7 +3264,7 @@ pub const Object = struct {
                         return int_ty;
                     }
 
-                    const name = try o.builder.string(mod.intern_pool.stringToSlice(
+                    const name = try o.builder.string(ip.stringToSlice(
                         try struct_obj.getFullyQualifiedName(mod),
                     ));
                     const ty = try o.builder.opaqueType(name);
@@ -3357,40 +3357,40 @@ pub const Object = struct {
                     const gop = try o.type_map.getOrPut(o.gpa, t.toIntern());
                     if (gop.found_existing) return gop.value_ptr.*;
 
-                    const union_obj = mod.unionPtr(union_type.index);
-                    const layout = union_obj.getLayout(mod, union_type.hasTag());
+                    const union_obj = ip.loadUnionType(union_type);
+                    const layout = mod.getUnionLayout(union_obj);
 
-                    if (union_obj.layout == .Packed) {
+                    if (union_obj.flagsPtr(ip).layout == .Packed) {
                         const int_ty = try o.builder.intType(@intCast(t.bitSize(mod)));
                         gop.value_ptr.* = int_ty;
                         return int_ty;
                     }
 
                     if (layout.payload_size == 0) {
-                        const enum_tag_ty = try o.lowerType(union_obj.tag_ty);
+                        const enum_tag_ty = try o.lowerType(union_obj.enum_tag_ty.toType());
                         gop.value_ptr.* = enum_tag_ty;
                         return enum_tag_ty;
                     }
 
-                    const name = try o.builder.string(mod.intern_pool.stringToSlice(
-                        try union_obj.getFullyQualifiedName(mod),
+                    const name = try o.builder.string(ip.stringToSlice(
+                        try mod.declPtr(union_obj.decl).getFullyQualifiedName(mod),
                     ));
                     const ty = try o.builder.opaqueType(name);
                     gop.value_ptr.* = ty; // must be done before any recursive calls
 
-                    const aligned_field = union_obj.fields.values()[layout.most_aligned_field];
-                    const aligned_field_ty = try o.lowerType(aligned_field.ty);
+                    const aligned_field_ty = union_obj.field_types.get(ip)[layout.most_aligned_field].toType();
+                    const aligned_field_llvm_ty = try o.lowerType(aligned_field_ty);
 
                     const payload_ty = ty: {
                         if (layout.most_aligned_field_size == layout.payload_size) {
-                            break :ty aligned_field_ty;
+                            break :ty aligned_field_llvm_ty;
                         }
                         const padding_len = if (layout.tag_size == 0)
                             layout.abi_size - layout.most_aligned_field_size
                         else
                             layout.payload_size - layout.most_aligned_field_size;
                         break :ty try o.builder.structType(.@"packed", &.{
-                            aligned_field_ty,
+                            aligned_field_llvm_ty,
                             try o.builder.arrayType(padding_len, .i8),
                         });
                     };
@@ -3402,7 +3402,7 @@ pub const Object = struct {
                         );
                         return ty;
                     }
-                    const enum_tag_ty = try o.lowerType(union_obj.tag_ty);
+                    const enum_tag_ty = try o.lowerType(union_obj.enum_tag_ty.toType());
 
                     // Put the tag before or after the payload depending on which one's
                     // alignment is greater.
@@ -3430,7 +3430,7 @@ pub const Object = struct {
                 .opaque_type => |opaque_type| {
                     const gop = try o.type_map.getOrPut(o.gpa, t.toIntern());
                     if (!gop.found_existing) {
-                        const name = try o.builder.string(mod.intern_pool.stringToSlice(
+                        const name = try o.builder.string(ip.stringToSlice(
                             try mod.opaqueFullyQualifiedName(opaque_type),
                         ));
                         gop.value_ptr.* = try o.builder.opaqueType(name);
@@ -3551,10 +3551,11 @@ pub const Object = struct {
 
     fn lowerValue(o: *Object, arg_val: InternPool.Index) Error!Builder.Constant {
         const mod = o.module;
+        const ip = &mod.intern_pool;
         const target = mod.getTarget();
 
         var val = arg_val.toValue();
-        const arg_val_key = mod.intern_pool.indexToKey(arg_val);
+        const arg_val_key = ip.indexToKey(arg_val);
         switch (arg_val_key) {
             .runtime_value => |rt| val = rt.val.toValue(),
             else => {},
@@ -3563,7 +3564,7 @@ pub const Object = struct {
             return o.builder.undefConst(try o.lowerType(arg_val_key.typeOf().toType()));
         }
 
-        const val_key = mod.intern_pool.indexToKey(val.toIntern());
+        const val_key = ip.indexToKey(val.toIntern());
         const ty = val_key.typeOf().toType();
         return switch (val_key) {
             .int_type,
@@ -3749,7 +3750,7 @@ pub const Object = struct {
                     fields[0..llvm_ty_fields.len],
                 ), vals[0..llvm_ty_fields.len]);
             },
-            .aggregate => |aggregate| switch (mod.intern_pool.indexToKey(ty.toIntern())) {
+            .aggregate => |aggregate| switch (ip.indexToKey(ty.toIntern())) {
                 .array_type => |array_type| switch (aggregate.storage) {
                     .bytes => |bytes| try o.builder.stringConst(try o.builder.string(bytes)),
                     .elems => |elems| {
@@ -4024,11 +4025,10 @@ pub const Object = struct {
                 if (layout.payload_size == 0) return o.lowerValue(un.tag);
 
                 const union_obj = mod.typeToUnion(ty).?;
-                const field_index = ty.unionTagFieldIndex(un.tag.toValue(), o.module).?;
-                assert(union_obj.haveFieldTypes());
+                const field_index = mod.unionTagFieldIndex(union_obj, un.tag.toValue()).?;
 
-                const field_ty = union_obj.fields.values()[field_index].ty;
-                if (union_obj.layout == .Packed) {
+                const field_ty = union_obj.field_types.get(ip)[field_index].toType();
+                if (union_obj.getLayout(ip) == .Packed) {
                     if (!field_ty.hasRuntimeBits(mod)) return o.builder.intConst(union_ty, 0);
                     const small_int_val = try o.builder.castConst(
                         if (field_ty.isPtrAtRuntime(mod)) .ptrtoint else .bitcast,
@@ -9676,6 +9676,7 @@ pub const FuncGen = struct {
     fn airUnionInit(self: *FuncGen, inst: Air.Inst.Index) !Builder.Value {
         const o = self.dg.object;
         const mod = o.module;
+        const ip = &mod.intern_pool;
         const ty_pl = self.air.instructions.items(.data)[inst].ty_pl;
         const extra = self.air.extraData(Air.UnionInit, ty_pl.payload).data;
         const union_ty = self.typeOfIndex(inst);
@@ -9683,13 +9684,13 @@ pub const FuncGen = struct {
         const layout = union_ty.unionGetLayout(mod);
         const union_obj = mod.typeToUnion(union_ty).?;
 
-        if (union_obj.layout == .Packed) {
+        if (union_obj.getLayout(ip) == .Packed) {
             const big_bits = union_ty.bitSize(mod);
             const int_llvm_ty = try o.builder.intType(@intCast(big_bits));
-            const field = union_obj.fields.values()[extra.field_index];
+            const field_ty = union_obj.field_types.get(ip)[extra.field_index].toType();
             const non_int_val = try self.resolveInst(extra.init);
-            const small_int_ty = try o.builder.intType(@intCast(field.ty.bitSize(mod)));
-            const small_int_val = if (field.ty.isPtrAtRuntime(mod))
+            const small_int_ty = try o.builder.intType(@intCast(field_ty.bitSize(mod)));
+            const small_int_val = if (field_ty.isPtrAtRuntime(mod))
                 try self.wip.cast(.ptrtoint, non_int_val, small_int_ty, "")
             else
                 try self.wip.cast(.bitcast, non_int_val, small_int_ty, "");
@@ -9698,7 +9699,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 union_field_name = union_obj.field_names.get(ip)[extra.field_index];
             const enum_field_index = tag_ty.enumFieldIndex(union_field_name, mod).?;
             const tag_val = try mod.enumValueFieldIndex(tag_ty, enum_field_index);
             const tag_int_val = try tag_val.intFromEnum(tag_ty, mod);
@@ -9719,18 +9720,17 @@ pub const FuncGen = struct {
         const alignment = Builder.Alignment.fromByteUnits(layout.abi_align);
         const result_ptr = try self.buildAlloca(union_llvm_ty, alignment);
         const llvm_payload = try self.resolveInst(extra.init);
-        assert(union_obj.haveFieldTypes());
-        const field = union_obj.fields.values()[extra.field_index];
-        const field_llvm_ty = try o.lowerType(field.ty);
-        const field_size = field.ty.abiSize(mod);
-        const field_align = field.normalAlignment(mod);
+        const field_ty = union_obj.field_types.get(ip)[extra.field_index].toType();
+        const field_llvm_ty = try o.lowerType(field_ty);
+        const field_size = field_ty.abiSize(mod);
+        const field_align = mod.unionFieldNormalAlignment(union_obj, extra.field_index);
         const llvm_usize = try o.lowerType(Type.usize);
         const usize_zero = try o.builder.intValue(llvm_usize, 0);
         const i32_zero = try o.builder.intValue(.i32, 0);
 
         const llvm_union_ty = t: {
             const payload_ty = p: {
-                if (!field.ty.hasRuntimeBitsIgnoreComptime(mod)) {
+                if (!field_ty.hasRuntimeBitsIgnoreComptime(mod)) {
                     const padding_len = layout.payload_size;
                     break :p try o.builder.arrayType(padding_len, .i8);
                 }
@@ -9743,7 +9743,7 @@ pub const FuncGen = struct {
                 });
             };
             if (layout.tag_size == 0) break :t try o.builder.structType(.normal, &.{payload_ty});
-            const tag_ty = try o.lowerType(union_obj.tag_ty);
+            const tag_ty = try o.lowerType(union_obj.enum_tag_ty.toType());
             var fields: [3]Builder.Type = undefined;
             var fields_len: usize = 2;
             if (layout.tag_align >= layout.payload_align) {
@@ -9761,7 +9761,7 @@ pub const FuncGen = struct {
         // Now we follow the layout as expressed above with GEP instructions to set the
         // tag and the payload.
         const field_ptr_ty = try mod.ptrType(.{
-            .child = field.ty.toIntern(),
+            .child = field_ty.toIntern(),
             .flags = .{ .alignment = InternPool.Alignment.fromNonzeroByteUnits(field_align) },
         });
         if (layout.tag_size == 0) {
@@ -9786,9 +9786,9 @@ pub const FuncGen = struct {
             const tag_index = @intFromBool(layout.tag_align < layout.payload_align);
             const indices: [2]Builder.Value = .{ usize_zero, try o.builder.intValue(.i32, tag_index) };
             const field_ptr = try self.wip.gep(.inbounds, llvm_union_ty, result_ptr, &indices, "");
-            const tag_ty = try o.lowerType(union_obj.tag_ty);
+            const tag_ty = try o.lowerType(union_obj.enum_tag_ty.toType());
             const llvm_tag = try o.builder.intValue(tag_ty, tag_int);
-            const tag_alignment = Builder.Alignment.fromByteUnits(union_obj.tag_ty.abiAlignment(mod));
+            const tag_alignment = Builder.Alignment.fromByteUnits(union_obj.enum_tag_ty.toType().abiAlignment(mod));
             _ = try self.wip.store(.normal, llvm_tag, field_ptr, tag_alignment);
         }
 
src/codegen/spirv.zig
@@ -619,9 +619,10 @@ pub const DeclGen = struct {
         fn lower(self: *@This(), ty: Type, arg_val: Value) !void {
             const dg = self.dg;
             const mod = dg.module;
+            const ip = &mod.intern_pool;
 
             var val = arg_val;
-            switch (mod.intern_pool.indexToKey(val.toIntern())) {
+            switch (ip.indexToKey(val.toIntern())) {
                 .runtime_value => |rt| val = rt.val.toValue(),
                 else => {},
             }
@@ -631,7 +632,7 @@ pub const DeclGen = struct {
                 return try self.addUndef(size);
             }
 
-            switch (mod.intern_pool.indexToKey(val.toIntern())) {
+            switch (ip.indexToKey(val.toIntern())) {
                 .int_type,
                 .ptr_type,
                 .array_type,
@@ -770,7 +771,7 @@ pub const DeclGen = struct {
                     try self.addConstBool(payload_val != null);
                     try self.addUndef(padding);
                 },
-                .aggregate => |aggregate| switch (mod.intern_pool.indexToKey(ty.ip_index)) {
+                .aggregate => |aggregate| switch (ip.indexToKey(ty.ip_index)) {
                     .array_type => |array_type| {
                         const elem_ty = array_type.child.toType();
                         switch (aggregate.storage) {
@@ -801,7 +802,7 @@ pub const DeclGen = struct {
                             if (field.is_comptime or !field.ty.hasRuntimeBits(mod)) continue;
 
                             const field_val = switch (aggregate.storage) {
-                                .bytes => |bytes| try mod.intern_pool.get(mod.gpa, .{ .int = .{
+                                .bytes => |bytes| try ip.get(mod.gpa, .{ .int = .{
                                     .ty = field.ty.toIntern(),
                                     .storage = .{ .u64 = bytes[i] },
                                 } }),
@@ -828,13 +829,13 @@ pub const DeclGen = struct {
                         return try self.lower(ty.unionTagTypeSafety(mod).?, un.tag.toValue());
                     }
 
-                    const union_ty = mod.typeToUnion(ty).?;
-                    if (union_ty.layout == .Packed) {
+                    const union_obj = mod.typeToUnion(ty).?;
+                    if (union_obj.getLayout(ip) == .Packed) {
                         return dg.todo("packed union constants", .{});
                     }
 
                     const active_field = ty.unionTagFieldIndex(un.tag.toValue(), dg.module).?;
-                    const active_field_ty = union_ty.fields.values()[active_field].ty;
+                    const active_field_ty = union_obj.field_types.get(ip)[active_field].toType();
 
                     const has_tag = layout.tag_size != 0;
                     const tag_first = layout.tag_align >= layout.payload_align;
@@ -1162,16 +1163,17 @@ pub const DeclGen = struct {
     ///   resulting struct will be *underaligned*.
     fn resolveUnionType(self: *DeclGen, ty: Type, maybe_active_field: ?usize) !CacheRef {
         const mod = self.module;
+        const ip = &mod.intern_pool;
         const layout = ty.unionGetLayout(mod);
-        const union_ty = mod.typeToUnion(ty).?;
+        const union_obj = mod.typeToUnion(ty).?;
 
-        if (union_ty.layout == .Packed) {
+        if (union_obj.getLayout(ip) == .Packed) {
             return self.todo("packed union types", .{});
         }
 
         if (layout.payload_size == 0) {
             // No payload, so represent this as just the tag type.
-            return try self.resolveType(union_ty.tag_ty, .indirect);
+            return try self.resolveType(union_obj.enum_tag_ty.toType(), .indirect);
         }
 
         var member_types = std.BoundedArray(CacheRef, 4){};
@@ -1182,13 +1184,13 @@ pub const DeclGen = struct {
         const u8_ty_ref = try self.intType(.unsigned, 8); // TODO: What if Int8Type is not enabled?
 
         if (has_tag and tag_first) {
-            const tag_ty_ref = try self.resolveType(union_ty.tag_ty, .indirect);
+            const tag_ty_ref = try self.resolveType(union_obj.enum_tag_ty.toType(), .indirect);
             member_types.appendAssumeCapacity(tag_ty_ref);
             member_names.appendAssumeCapacity(try self.spv.resolveString("tag"));
         }
 
         const active_field = maybe_active_field orelse layout.most_aligned_field;
-        const active_field_ty = union_ty.fields.values()[active_field].ty;
+        const active_field_ty = union_obj.field_types.get(ip)[active_field].toType();
 
         const active_field_size = if (active_field_ty.hasRuntimeBitsIgnoreComptime(mod)) blk: {
             const active_payload_ty_ref = try self.resolveType(active_field_ty, .indirect);
@@ -1205,7 +1207,7 @@ pub const DeclGen = struct {
         }
 
         if (has_tag and !tag_first) {
-            const tag_ty_ref = try self.resolveType(union_ty.tag_ty, .indirect);
+            const tag_ty_ref = try self.resolveType(union_obj.enum_tag_ty.toType(), .indirect);
             member_types.appendAssumeCapacity(tag_ty_ref);
             member_names.appendAssumeCapacity(try self.spv.resolveString("tag"));
         }
src/link/Dwarf.zig
@@ -166,6 +166,7 @@ pub const DeclState = struct {
         const dbg_info_buffer = &self.dbg_info;
         const target = mod.getTarget();
         const target_endian = target.cpu.arch.endian();
+        const ip = &mod.intern_pool;
 
         switch (ty.zigTypeTag(mod)) {
             .NoReturn => unreachable,
@@ -321,7 +322,7 @@ pub const DeclState = struct {
                 // DW.AT.byte_size, DW.FORM.udata
                 try leb128.writeULEB128(dbg_info_buffer.writer(), ty.abiSize(mod));
 
-                switch (mod.intern_pool.indexToKey(ty.ip_index)) {
+                switch (ip.indexToKey(ty.ip_index)) {
                     .anon_struct_type => |fields| {
                         // DW.AT.name, DW.FORM.string
                         try dbg_info_buffer.writer().print("{}\x00", .{ty.fmt(mod)});
@@ -357,7 +358,7 @@ pub const DeclState = struct {
                             0..,
                         ) |field_name_ip, field, field_index| {
                             if (!field.ty.hasRuntimeBits(mod)) continue;
-                            const field_name = mod.intern_pool.stringToSlice(field_name_ip);
+                            const field_name = ip.stringToSlice(field_name_ip);
                             // DW.AT.member
                             try dbg_info_buffer.ensureUnusedCapacity(field_name.len + 2);
                             dbg_info_buffer.appendAssumeCapacity(@intFromEnum(AbbrevKind.struct_member));
@@ -388,7 +389,6 @@ pub const DeclState = struct {
                 try ty.print(dbg_info_buffer.writer(), mod);
                 try dbg_info_buffer.append(0);
 
-                const ip = &mod.intern_pool;
                 const enum_type = ip.indexToKey(ty.ip_index).enum_type;
                 for (enum_type.names.get(ip), 0..) |field_name_index, field_i| {
                     const field_name = ip.stringToSlice(field_name_index);
@@ -414,8 +414,8 @@ pub const DeclState = struct {
                 try dbg_info_buffer.append(0);
             },
             .Union => {
-                const layout = ty.unionGetLayout(mod);
                 const union_obj = mod.typeToUnion(ty).?;
+                const layout = mod.getUnionLayout(union_obj);
                 const payload_offset = if (layout.tag_align >= layout.payload_align) layout.tag_size else 0;
                 const tag_offset = if (layout.tag_align >= layout.payload_align) 0 else layout.payload_size;
                 // TODO this is temporary to match current state of unions in Zig - we don't yet have
@@ -457,19 +457,17 @@ pub const DeclState = struct {
                     try dbg_info_buffer.append(0);
                 }
 
-                const fields = ty.unionFields(mod);
-                for (fields.keys()) |field_name| {
-                    const field = fields.get(field_name).?;
-                    if (!field.ty.hasRuntimeBits(mod)) continue;
+                for (union_obj.field_types.get(ip), union_obj.field_names.get(ip)) |field_ty, field_name| {
+                    if (!field_ty.toType().hasRuntimeBits(mod)) continue;
                     // DW.AT.member
                     try dbg_info_buffer.append(@intFromEnum(AbbrevKind.struct_member));
                     // DW.AT.name, DW.FORM.string
-                    try dbg_info_buffer.appendSlice(mod.intern_pool.stringToSlice(field_name));
+                    try dbg_info_buffer.appendSlice(ip.stringToSlice(field_name));
                     try dbg_info_buffer.append(0);
                     // DW.AT.type, DW.FORM.ref4
                     const index = dbg_info_buffer.items.len;
                     try dbg_info_buffer.resize(index + 4);
-                    try self.addTypeRelocGlobal(atom_index, field.ty, @as(u32, @intCast(index)));
+                    try self.addTypeRelocGlobal(atom_index, field_ty.toType(), @intCast(index));
                     // DW.AT.data_member_location, DW.FORM.udata
                     try dbg_info_buffer.append(0);
                 }
@@ -486,7 +484,7 @@ pub const DeclState = struct {
                     // DW.AT.type, DW.FORM.ref4
                     const index = dbg_info_buffer.items.len;
                     try dbg_info_buffer.resize(index + 4);
-                    try self.addTypeRelocGlobal(atom_index, union_obj.tag_ty, @as(u32, @intCast(index)));
+                    try self.addTypeRelocGlobal(atom_index, union_obj.enum_tag_ty.toType(), @intCast(index));
                     // DW.AT.data_member_location, DW.FORM.udata
                     try leb128.writeULEB128(dbg_info_buffer.writer(), tag_offset);
 
src/AstGen.zig
@@ -4696,6 +4696,7 @@ fn unionDeclInner(
 
     const bits_per_field = 4;
     const max_field_size = 5;
+    var any_aligned_fields = false;
     var wip_members = try WipMembers.init(gpa, &astgen.scratch, decl_count, field_count, bits_per_field, max_field_size);
     defer wip_members.deinit();
 
@@ -4733,6 +4734,7 @@ fn unionDeclInner(
         if (have_align) {
             const align_inst = try expr(&block_scope, &block_scope.base, .{ .rl = .{ .ty = .u32_type } }, member.ast.align_expr);
             wip_members.appendToField(@intFromEnum(align_inst));
+            any_aligned_fields = true;
         }
         if (have_value) {
             if (arg_inst == .none) {
@@ -4783,6 +4785,7 @@ fn unionDeclInner(
         .fields_len = field_count,
         .decls_len = decl_count,
         .auto_enum_tag = auto_enum_tok != null,
+        .any_aligned_fields = any_aligned_fields,
     });
 
     wip_members.finishBits(bits_per_field);
@@ -11754,6 +11757,7 @@ const GenZir = struct {
         decls_len: u32,
         layout: std.builtin.Type.ContainerLayout,
         auto_enum_tag: bool,
+        any_aligned_fields: bool,
     }) !void {
         const astgen = gz.astgen;
         const gpa = astgen.gpa;
@@ -11790,6 +11794,7 @@ const GenZir = struct {
                     .name_strategy = gz.anon_name_strategy,
                     .layout = args.layout,
                     .auto_enum_tag = args.auto_enum_tag,
+                    .any_aligned_fields = args.any_aligned_fields,
                 }),
                 .operand = payload_index,
             } },
src/codegen.zig
@@ -185,8 +185,9 @@ pub fn generateSymbol(
     defer tracy.end();
 
     const mod = bin_file.options.module.?;
+    const ip = &mod.intern_pool;
     var typed_value = arg_tv;
-    switch (mod.intern_pool.indexToKey(typed_value.val.toIntern())) {
+    switch (ip.indexToKey(typed_value.val.toIntern())) {
         .runtime_value => |rt| typed_value.val = rt.val.toValue(),
         else => {},
     }
@@ -205,7 +206,7 @@ pub fn generateSymbol(
         return .ok;
     }
 
-    switch (mod.intern_pool.indexToKey(typed_value.val.toIntern())) {
+    switch (ip.indexToKey(typed_value.val.toIntern())) {
         .int_type,
         .ptr_type,
         .array_type,
@@ -385,7 +386,7 @@ pub fn generateSymbol(
                 try code.appendNTimes(0, padding);
             }
         },
-        .aggregate => |aggregate| switch (mod.intern_pool.indexToKey(typed_value.ty.toIntern())) {
+        .aggregate => |aggregate| switch (ip.indexToKey(typed_value.ty.toIntern())) {
             .array_type => |array_type| switch (aggregate.storage) {
                 .bytes => |bytes| try code.appendSlice(bytes),
                 .elems, .repeated_elem => {
@@ -442,7 +443,7 @@ pub fn generateSymbol(
                     if (!field_ty.toType().hasRuntimeBits(mod)) continue;
 
                     const field_val = switch (aggregate.storage) {
-                        .bytes => |bytes| try mod.intern_pool.get(mod.gpa, .{ .int = .{
+                        .bytes => |bytes| try ip.get(mod.gpa, .{ .int = .{
                             .ty = field_ty,
                             .storage = .{ .u64 = bytes[index] },
                         } }),
@@ -484,7 +485,7 @@ pub fn generateSymbol(
                         const field_ty = field.ty;
 
                         const field_val = switch (aggregate.storage) {
-                            .bytes => |bytes| try mod.intern_pool.get(mod.gpa, .{ .int = .{
+                            .bytes => |bytes| try ip.get(mod.gpa, .{ .int = .{
                                 .ty = field_ty.toIntern(),
                                 .storage = .{ .u64 = bytes[index] },
                             } }),
@@ -522,8 +523,8 @@ pub fn generateSymbol(
 
                         if (!field_ty.hasRuntimeBits(mod)) continue;
 
-                        const field_val = switch (mod.intern_pool.indexToKey(typed_value.val.toIntern()).aggregate.storage) {
-                            .bytes => |bytes| try mod.intern_pool.get(mod.gpa, .{ .int = .{
+                        const field_val = switch (ip.indexToKey(typed_value.val.toIntern()).aggregate.storage) {
+                            .bytes => |bytes| try ip.get(mod.gpa, .{ .int = .{
                                 .ty = field_ty.toIntern(),
                                 .storage = .{ .u64 = bytes[field_offset.field] },
                             } }),
@@ -570,10 +571,9 @@ pub fn generateSymbol(
                 }
             }
 
-            const union_ty = mod.typeToUnion(typed_value.ty).?;
+            const union_obj = mod.typeToUnion(typed_value.ty).?;
             const field_index = typed_value.ty.unionTagFieldIndex(un.tag.toValue(), mod).?;
-            assert(union_ty.haveFieldTypes());
-            const field_ty = union_ty.fields.values()[field_index].ty;
+            const field_ty = union_obj.field_types.get(ip)[field_index].toType();
             if (!field_ty.hasRuntimeBits(mod)) {
                 try code.appendNTimes(0xaa, math.cast(usize, layout.payload_size) orelse return error.Overflow);
             } else {
@@ -593,7 +593,7 @@ pub fn generateSymbol(
 
             if (layout.tag_size > 0 and layout.tag_align < layout.payload_align) {
                 switch (try generateSymbol(bin_file, src_loc, .{
-                    .ty = union_ty.tag_ty,
+                    .ty = union_obj.enum_tag_ty.toType(),
                     .val = un.tag.toValue(),
                 }, code, debug_output, reloc_info)) {
                     .ok => {},
src/InternPool.zig
@@ -46,13 +46,6 @@ allocated_structs: std.SegmentedList(Module.Struct, 0) = .{},
 /// When a Struct object is freed from `allocated_structs`, it is pushed into this stack.
 structs_free_list: std.ArrayListUnmanaged(Module.Struct.Index) = .{},
 
-/// Union objects are stored in this data structure because:
-/// * They contain pointers such as the field maps.
-/// * They need to be mutated after creation.
-allocated_unions: std.SegmentedList(Module.Union, 0) = .{},
-/// When a Union object is freed from `allocated_unions`, it is pushed into this stack.
-unions_free_list: std.ArrayListUnmanaged(Module.Union.Index) = .{},
-
 /// Some types such as enums, structs, and unions need to store mappings from field names
 /// to field index, or value to field index. In such cases, they will store the underlying
 /// field names and values directly, relying on one of these maps, stored separately,
@@ -241,7 +234,7 @@ pub const Key = union(enum) {
     /// declaration. It is used for types that have no `struct` keyword in the
     /// source code, and were not created via `@Type`.
     anon_struct_type: AnonStructType,
-    union_type: UnionType,
+    union_type: Key.UnionType,
     opaque_type: OpaqueType,
     enum_type: EnumType,
     func_type: FuncType,
@@ -391,17 +384,72 @@ pub const Key = union(enum) {
         }
     };
 
+    /// Serves two purposes:
+    /// * Being the key in the InternPool hash map, which only requires the `decl` field.
+    /// * Provide the other fields that do not require chasing the enum type.
     pub const UnionType = struct {
-        index: Module.Union.Index,
-        runtime_tag: RuntimeTag,
+        /// The Decl that corresponds to the union itself.
+        decl: Module.Decl.Index,
+        /// The index of the `Tag.TypeUnion` payload. Ignored by `get`,
+        /// populated by `indexToKey`.
+        extra_index: u32,
+        namespace: Module.Namespace.Index,
+        flags: Tag.TypeUnion.Flags,
+        /// The enum that provides the list of field names and values.
+        enum_tag_ty: Index,
+        zir_index: Zir.Inst.Index,
+
+        /// The returned pointer expires with any addition to the `InternPool`.
+        pub fn flagsPtr(self: @This(), ip: *const InternPool) *Tag.TypeUnion.Flags {
+            const flags_field_index = std.meta.fieldIndex(Tag.TypeUnion, "flags").?;
+            return @ptrCast(&ip.extra.items[self.extra_index + flags_field_index]);
+        }
 
-        pub const RuntimeTag = enum { none, safety, tagged };
+        pub fn haveFieldTypes(self: @This(), ip: *const InternPool) bool {
+            return self.flagsPtr(ip).status.haveFieldTypes();
+        }
 
-        pub fn hasTag(self: UnionType) bool {
-            return switch (self.runtime_tag) {
-                .none => false,
-                .tagged, .safety => true,
-            };
+        pub fn hasTag(self: @This(), ip: *const InternPool) bool {
+            return self.flagsPtr(ip).runtime_tag.hasTag();
+        }
+
+        pub fn getLayout(self: @This(), ip: *const InternPool) std.builtin.Type.ContainerLayout {
+            return self.flagsPtr(ip).layout;
+        }
+
+        pub fn haveLayout(self: @This(), ip: *const InternPool) bool {
+            return self.flagsPtr(ip).status.haveLayout();
+        }
+
+        /// Pointer to an enum type which is used for the tag of the union.
+        /// This type is created even for untagged unions, even when the memory
+        /// layout does not store the tag.
+        /// Whether zig chooses this type or the user specifies it, it is stored here.
+        /// This will be set to the null type until status is `have_field_types`.
+        /// This accessor is provided so that the tag type can be mutated, and so that
+        /// when it is mutated, the mutations are observed.
+        /// The returned pointer is invalidated when something is added to the `InternPool`.
+        pub fn tagTypePtr(self: @This(), ip: *const InternPool) *Index {
+            const tag_ty_field_index = std.meta.fieldIndex(Tag.TypeUnion, "tag_ty").?;
+            return @ptrCast(&ip.extra.items[self.extra_index + tag_ty_field_index]);
+        }
+
+        pub fn setFieldTypes(self: @This(), ip: *InternPool, types: []const Index) void {
+            @memcpy((Index.Slice{
+                .start = @intCast(self.extra_index + @typeInfo(Tag.TypeUnion).Struct.fields.len),
+                .len = @intCast(types.len),
+            }).get(ip), types);
+        }
+
+        pub fn setFieldAligns(self: @This(), ip: *InternPool, aligns: []const Alignment) void {
+            if (aligns.len == 0) return;
+            assert(self.flagsPtr(ip).any_aligned_fields);
+            @memcpy((Alignment.Slice{
+                .start = @intCast(
+                    self.extra_index + @typeInfo(Tag.TypeUnion).Struct.fields.len + aligns.len,
+                ),
+                .len = @intCast(aligns.len),
+            }).get(ip), aligns);
         }
     };
 
@@ -833,7 +881,6 @@ pub const Key = union(enum) {
             => |x| Hash.hash(seed, asBytes(&x)),
 
             .int_type => |x| Hash.hash(seed + @intFromEnum(x.signedness), asBytes(&x.bits)),
-            .union_type => |x| Hash.hash(seed + @intFromEnum(x.runtime_tag), asBytes(&x.index)),
 
             .error_union => |x| switch (x.val) {
                 .err_name => |y| Hash.hash(seed + 0, asBytes(&x.ty) ++ asBytes(&y)),
@@ -845,6 +892,7 @@ pub const Key = union(enum) {
             inline .opaque_type,
             .enum_type,
             .variable,
+            .union_type,
             => |x| Hash.hash(seed, asBytes(&x.decl)),
 
             .int => |int| {
@@ -1079,10 +1127,6 @@ pub const Key = union(enum) {
                 const b_info = b.struct_type;
                 return std.meta.eql(a_info, b_info);
             },
-            .union_type => |a_info| {
-                const b_info = b.union_type;
-                return std.meta.eql(a_info, b_info);
-            },
             .un => |a_info| {
                 const b_info = b.un;
                 return std.meta.eql(a_info, b_info);
@@ -1250,6 +1294,10 @@ pub const Key = union(enum) {
                 const b_info = b.enum_type;
                 return a_info.decl == b_info.decl;
             },
+            .union_type => |a_info| {
+                const b_info = b.union_type;
+                return a_info.decl == b_info.decl;
+            },
             .aggregate => |a_info| {
                 const b_info = b.aggregate;
                 if (a_info.ty != b_info.ty) return false;
@@ -1385,6 +1433,158 @@ pub const Key = union(enum) {
     }
 };
 
+// Unlike `Tag.TypeUnion` which is an encoding, and `Key.UnionType` which is a
+// minimal hashmap key, this type is a convenience type that contains info
+// needed by semantic analysis.
+pub const UnionType = struct {
+    /// The Decl that corresponds to the union itself.
+    decl: Module.Decl.Index,
+    /// Represents the declarations inside this union.
+    namespace: Module.Namespace.Index,
+    /// The enum tag type.
+    enum_tag_ty: Index,
+    /// The integer tag type of the enum.
+    int_tag_ty: Index,
+    /// List of field names in declaration order.
+    field_names: NullTerminatedString.Slice,
+    /// List of field types in declaration order.
+    /// These are `none` until `status` is `have_field_types` or `have_layout`.
+    field_types: Index.Slice,
+    /// List of field alignments in declaration order.
+    /// `none` means the ABI alignment of the type.
+    /// If this slice has length 0 it means all elements are `none`.
+    field_aligns: Alignment.Slice,
+    /// Index of the union_decl ZIR instruction.
+    zir_index: Zir.Inst.Index,
+    /// Index into extra array of the `flags` field.
+    flags_index: u32,
+    /// Copied from `enum_tag_ty`.
+    names_map: OptionalMapIndex,
+
+    pub const RuntimeTag = enum(u2) {
+        none,
+        safety,
+        tagged,
+
+        pub fn hasTag(self: RuntimeTag) bool {
+            return switch (self) {
+                .none => false,
+                .tagged, .safety => true,
+            };
+        }
+    };
+
+    pub const RequiresComptime = enum(u2) { no, yes, unknown, wip };
+
+    pub const Status = enum(u3) {
+        none,
+        field_types_wip,
+        have_field_types,
+        layout_wip,
+        have_layout,
+        fully_resolved_wip,
+        /// The types and all its fields have had their layout resolved.
+        /// Even through pointer, which `have_layout` does not ensure.
+        fully_resolved,
+
+        pub fn haveFieldTypes(status: Status) bool {
+            return switch (status) {
+                .none,
+                .field_types_wip,
+                => false,
+                .have_field_types,
+                .layout_wip,
+                .have_layout,
+                .fully_resolved_wip,
+                .fully_resolved,
+                => true,
+            };
+        }
+
+        pub fn haveLayout(status: Status) bool {
+            return switch (status) {
+                .none,
+                .field_types_wip,
+                .have_field_types,
+                .layout_wip,
+                => false,
+                .have_layout,
+                .fully_resolved_wip,
+                .fully_resolved,
+                => true,
+            };
+        }
+    };
+
+    /// The returned pointer expires with any addition to the `InternPool`.
+    pub fn flagsPtr(self: UnionType, ip: *const InternPool) *Tag.TypeUnion.Flags {
+        return @ptrCast(&ip.extra.items[self.flags_index]);
+    }
+
+    /// Look up field index based on field name.
+    pub fn nameIndex(self: UnionType, ip: *const InternPool, name: NullTerminatedString) ?u32 {
+        const map = &ip.maps.items[@intFromEnum(self.names_map.unwrap().?)];
+        const adapter: NullTerminatedString.Adapter = .{ .strings = self.field_names.get(ip) };
+        const field_index = map.getIndexAdapted(name, adapter) orelse return null;
+        return @intCast(field_index);
+    }
+
+    pub fn hasTag(self: UnionType, ip: *const InternPool) bool {
+        return self.flagsPtr(ip).runtime_tag.hasTag();
+    }
+
+    pub fn haveLayout(self: UnionType, ip: *const InternPool) bool {
+        return self.flagsPtr(ip).status.haveLayout();
+    }
+
+    pub fn getLayout(self: UnionType, ip: *const InternPool) std.builtin.Type.ContainerLayout {
+        return self.flagsPtr(ip).layout;
+    }
+
+    pub fn fieldAlign(self: UnionType, ip: *const InternPool, field_index: u32) Alignment {
+        if (self.field_aligns.len == 0) return .none;
+        return self.field_aligns.get(ip)[field_index];
+    }
+
+    /// This does not mutate the field of UnionType.
+    pub fn setZirIndex(self: @This(), ip: *InternPool, new_zir_index: Zir.Inst.Index) void {
+        const flags_field_index = std.meta.fieldIndex(Tag.TypeUnion, "flags").?;
+        const zir_index_field_index = std.meta.fieldIndex(Tag.TypeUnion, "zir_index").?;
+        const ptr: *Zir.Inst.Index =
+            @ptrCast(&ip.extra.items[self.flags_index - flags_field_index + zir_index_field_index]);
+        ptr.* = new_zir_index;
+    }
+};
+
+/// Fetch all the interesting fields of a union type into a convenient data
+/// structure.
+/// This asserts that the union's enum tag type has been resolved.
+pub fn loadUnionType(ip: *InternPool, key: Key.UnionType) UnionType {
+    const type_union = ip.extraDataTrail(Tag.TypeUnion, key.extra_index);
+    const enum_ty = type_union.data.tag_ty;
+    const enum_info = ip.indexToKey(enum_ty).enum_type;
+    const fields_len: u32 = @intCast(enum_info.names.len);
+
+    return .{
+        .decl = type_union.data.decl,
+        .namespace = type_union.data.namespace,
+        .enum_tag_ty = enum_ty,
+        .int_tag_ty = enum_info.tag_ty,
+        .field_names = enum_info.names,
+        .names_map = enum_info.names_map,
+        .field_types = .{
+            .start = type_union.end,
+            .len = fields_len,
+        },
+        .field_aligns = .{
+            .start = type_union.end + fields_len,
+            .len = if (type_union.data.flags.any_aligned_fields) fields_len else 0,
+        },
+        .zir_index = type_union.data.zir_index,
+        .flags_index = key.extra_index + std.meta.fieldIndex(Tag.TypeUnion, "flags").?,
+    };
+}
+
 pub const Item = struct {
     tag: Tag,
     /// The doc comments on the respective Tag explain how to interpret this.
@@ -1618,9 +1818,7 @@ pub const Index = enum(u32) {
         type_struct_ns: struct { data: Module.Namespace.Index },
         type_struct_anon: DataIsExtraIndexOfTypeStructAnon,
         type_tuple_anon: DataIsExtraIndexOfTypeStructAnon,
-        type_union_tagged: struct { data: Module.Union.Index },
-        type_union_untagged: struct { data: Module.Union.Index },
-        type_union_safety: struct { data: Module.Union.Index },
+        type_union: struct { data: *Tag.TypeUnion },
         type_function: struct {
             const @"data.flags.has_comptime_bits" = opaque {};
             const @"data.flags.has_noalias_bits" = opaque {};
@@ -2057,15 +2255,9 @@ pub const Tag = enum(u8) {
     /// An AnonStructType which has only types and values for fields.
     /// data is extra index of `TypeStructAnon`.
     type_tuple_anon,
-    /// A tagged union type.
-    /// `data` is `Module.Union.Index`.
-    type_union_tagged,
-    /// An untagged union type. It also has no safety tag.
-    /// `data` is `Module.Union.Index`.
-    type_union_untagged,
-    /// An untagged union type which has a safety tag.
-    /// `data` is `Module.Union.Index`.
-    type_union_safety,
+    /// A union type.
+    /// `data` is extra index of `TypeUnion`.
+    type_union,
     /// A function body type.
     /// `data` is extra index to `TypeFunction`.
     type_function,
@@ -2273,9 +2465,7 @@ pub const Tag = enum(u8) {
             .type_struct_ns => unreachable,
             .type_struct_anon => TypeStructAnon,
             .type_tuple_anon => TypeStructAnon,
-            .type_union_tagged => unreachable,
-            .type_union_untagged => unreachable,
-            .type_union_safety => unreachable,
+            .type_union => TypeUnion,
             .type_function => TypeFunction,
 
             .undef => unreachable,
@@ -2425,6 +2615,30 @@ pub const Tag = enum(u8) {
             _: u9 = 0,
         };
     };
+
+    /// The number of fields is provided by the `tag_ty` field.
+    /// Trailing:
+    /// 0. field type: Index for each field; declaration order
+    /// 1. field align: Alignment for each field; declaration order
+    pub const TypeUnion = struct {
+        flags: Flags,
+        decl: Module.Decl.Index,
+        namespace: Module.Namespace.Index,
+        /// The enum that provides the list of field names and values.
+        tag_ty: Index,
+        zir_index: Zir.Inst.Index,
+
+        pub const Flags = packed struct(u32) {
+            runtime_tag: UnionType.RuntimeTag,
+            /// If false, the field alignment trailing data is omitted.
+            any_aligned_fields: bool,
+            layout: std.builtin.Type.ContainerLayout,
+            status: UnionType.Status,
+            requires_comptime: UnionType.RequiresComptime,
+            assumed_runtime_bits: bool,
+            _: u21 = 0,
+        };
+    };
 };
 
 /// State that is mutable during semantic analysis. This data is not used for
@@ -2582,6 +2796,21 @@ pub const Alignment = enum(u6) {
         assert(lhs != .none and rhs != .none);
         return std.math.order(@intFromEnum(lhs), @intFromEnum(rhs));
     }
+
+    /// An array of `Alignment` objects existing within the `extra` array.
+    /// This type exists to provide a struct with lifetime that is
+    /// not invalidated when items are added to the `InternPool`.
+    pub const Slice = struct {
+        start: u32,
+        len: u32,
+
+        pub fn get(slice: Slice, ip: *const InternPool) []Alignment {
+            // TODO: implement @ptrCast between slices changing the length
+            //const bytes: []u8 = @ptrCast(ip.extra.items[slice.start..]);
+            const bytes: []u8 = std.mem.sliceAsBytes(ip.extra.items[slice.start..]);
+            return @ptrCast(bytes[0..slice.len]);
+        }
+    };
 };
 
 /// Used for non-sentineled arrays that have length fitting in u32, as well as
@@ -2829,9 +3058,6 @@ pub fn deinit(ip: *InternPool, gpa: Allocator) void {
     ip.structs_free_list.deinit(gpa);
     ip.allocated_structs.deinit(gpa);
 
-    ip.unions_free_list.deinit(gpa);
-    ip.allocated_unions.deinit(gpa);
-
     ip.decls_free_list.deinit(gpa);
     ip.allocated_decls.deinit(gpa);
 
@@ -2953,18 +3179,7 @@ pub fn indexToKey(ip: *const InternPool, index: Index) Key {
             } };
         },
 
-        .type_union_untagged => .{ .union_type = .{
-            .index = @as(Module.Union.Index, @enumFromInt(data)),
-            .runtime_tag = .none,
-        } },
-        .type_union_tagged => .{ .union_type = .{
-            .index = @as(Module.Union.Index, @enumFromInt(data)),
-            .runtime_tag = .tagged,
-        } },
-        .type_union_safety => .{ .union_type = .{
-            .index = @as(Module.Union.Index, @enumFromInt(data)),
-            .runtime_tag = .safety,
-        } },
+        .type_union => .{ .union_type = extraUnionType(ip, data) },
 
         .type_enum_auto => {
             const enum_auto = ip.extraDataTrail(EnumAuto, data);
@@ -3279,9 +3494,7 @@ pub fn indexToKey(ip: *const InternPool, index: Index) Key {
 
                 .type_enum_auto,
                 .type_enum_explicit,
-                .type_union_tagged,
-                .type_union_untagged,
-                .type_union_safety,
+                .type_union,
                 => .{ .empty_enum_value = ty },
 
                 else => unreachable,
@@ -3352,6 +3565,18 @@ fn extraErrorSet(ip: *const InternPool, extra_index: u32) Key.ErrorSetType {
     };
 }
 
+fn extraUnionType(ip: *const InternPool, extra_index: u32) Key.UnionType {
+    const type_union = ip.extraData(Tag.TypeUnion, extra_index);
+    return .{
+        .decl = type_union.decl,
+        .namespace = type_union.namespace,
+        .flags = type_union.flags,
+        .enum_tag_ty = type_union.tag_ty,
+        .zir_index = type_union.zir_index,
+        .extra_index = extra_index,
+    };
+}
+
 fn extraFuncType(ip: *const InternPool, extra_index: u32) Key.FuncType {
     const type_function = ip.extraDataTrail(Tag.TypeFunction, extra_index);
     var index: usize = type_function.end;
@@ -3678,16 +3903,7 @@ pub fn get(ip: *InternPool, gpa: Allocator, key: Key) Allocator.Error!Index {
             return @enumFromInt(ip.items.len - 1);
         },
 
-        .union_type => |union_type| {
-            ip.items.appendAssumeCapacity(.{
-                .tag = switch (union_type.runtime_tag) {
-                    .none => .type_union_untagged,
-                    .safety => .type_union_safety,
-                    .tagged => .type_union_tagged,
-                },
-                .data = @intFromEnum(union_type.index),
-            });
-        },
+        .union_type => unreachable, // use getUnionType() instead
 
         .opaque_type => |opaque_type| {
             ip.items.appendAssumeCapacity(.{
@@ -3791,9 +4007,10 @@ pub fn get(ip: *InternPool, gpa: Allocator, key: Key) Allocator.Error!Index {
                                             assert(ptr.addr == .field);
                                             assert(base_index.index < ip.structPtrUnwrapConst(struct_type.index).?.fields.count());
                                         },
-                                        .union_type => |union_type| {
+                                        .union_type => |union_key| {
+                                            const union_type = ip.loadUnionType(union_key);
                                             assert(ptr.addr == .field);
-                                            assert(base_index.index < ip.unionPtrConst(union_type.index).fields.count());
+                                            assert(base_index.index < union_type.field_names.len);
                                         },
                                         .ptr_type => |slice_type| {
                                             assert(ptr.addr == .field);
@@ -4359,6 +4576,76 @@ pub fn get(ip: *InternPool, gpa: Allocator, key: Key) Allocator.Error!Index {
     return @enumFromInt(ip.items.len - 1);
 }
 
+pub const UnionTypeInit = struct {
+    flags: Tag.TypeUnion.Flags,
+    decl: Module.Decl.Index,
+    namespace: Module.Namespace.Index,
+    zir_index: Zir.Inst.Index,
+    fields_len: u32,
+    enum_tag_ty: Index,
+    /// May have length 0 which leaves the values unset until later.
+    field_types: []const Index,
+    /// May have length 0 which leaves the values unset until later.
+    /// The logic for `any_aligned_fields` is asserted to have been done before
+    /// calling this function.
+    field_aligns: []const Alignment,
+};
+
+pub fn getUnionType(ip: *InternPool, gpa: Allocator, ini: UnionTypeInit) Allocator.Error!Index {
+    const prev_extra_len = ip.extra.items.len;
+    const align_elements_len = if (ini.flags.any_aligned_fields) (ini.fields_len + 3) / 4 else 0;
+    const align_element: u32 = @bitCast([1]u8{@intFromEnum(Alignment.none)} ** 4);
+    try ip.extra.ensureUnusedCapacity(gpa, @typeInfo(Tag.TypeUnion).Struct.fields.len +
+        ini.fields_len + // field types
+        align_elements_len);
+    try ip.items.ensureUnusedCapacity(gpa, 1);
+
+    const union_type_extra_index = ip.addExtraAssumeCapacity(Tag.TypeUnion{
+        .flags = ini.flags,
+        .decl = ini.decl,
+        .namespace = ini.namespace,
+        .tag_ty = ini.enum_tag_ty,
+        .zir_index = ini.zir_index,
+    });
+
+    // field types
+    if (ini.field_types.len > 0) {
+        assert(ini.field_types.len == ini.fields_len);
+        ip.extra.appendSliceAssumeCapacity(@ptrCast(ini.field_types));
+    } else {
+        ip.extra.appendNTimesAssumeCapacity(@intFromEnum(Index.none), ini.fields_len);
+    }
+
+    // field alignments
+    if (ini.flags.any_aligned_fields) {
+        ip.extra.appendNTimesAssumeCapacity(align_element, align_elements_len);
+        if (ini.field_aligns.len > 0) {
+            assert(ini.field_aligns.len == ini.fields_len);
+            @memcpy((Alignment.Slice{
+                .start = @intCast(ip.extra.items.len - align_elements_len),
+                .len = @intCast(ini.field_aligns.len),
+            }).get(ip), ini.field_aligns);
+        }
+    } else {
+        assert(ini.field_aligns.len == 0);
+    }
+
+    const adapter: KeyAdapter = .{ .intern_pool = ip };
+    const gop = try ip.map.getOrPutAdapted(gpa, Key{
+        .union_type = extraUnionType(ip, union_type_extra_index),
+    }, adapter);
+    if (gop.found_existing) {
+        ip.extra.items.len = prev_extra_len;
+        return @enumFromInt(gop.index);
+    }
+
+    ip.items.appendAssumeCapacity(.{
+        .tag = .type_union,
+        .data = union_type_extra_index,
+    });
+    return @enumFromInt(ip.items.len - 1);
+}
+
 /// This is equivalent to `Key.FuncType` but adjusted to have a slice for `param_types`.
 pub const GetFuncTypeKey = struct {
     param_types: []Index,
@@ -5310,6 +5597,7 @@ fn addExtraAssumeCapacity(ip: *InternPool, extra: anytype) u32 {
             Tag.TypeFunction.Flags,
             Tag.TypePointer.PackedOffset,
             Tag.Variable.Flags,
+            Tag.TypeUnion.Flags,
             => @bitCast(@field(extra, field.name)),
 
             else => @compileError("bad field type: " ++ @typeName(field.type)),
@@ -5380,6 +5668,7 @@ fn extraDataTrail(ip: *const InternPool, comptime T: type, index: usize) struct
             Tag.TypePointer.Flags,
             Tag.TypeFunction.Flags,
             Tag.TypePointer.PackedOffset,
+            Tag.TypeUnion.Flags,
             Tag.Variable.Flags,
             FuncAnalysis,
             => @bitCast(int32),
@@ -5893,7 +6182,7 @@ pub fn indexToUnionType(ip: *const InternPool, val: Index) Module.Union.Optional
     assert(val != .none);
     const tags = ip.items.items(.tag);
     switch (tags[@intFromEnum(val)]) {
-        .type_union_tagged, .type_union_untagged, .type_union_safety => {},
+        .type_union => {},
         else => return .none,
     }
     const datas = ip.items.items(.data);
@@ -5946,6 +6235,10 @@ pub fn isEnumType(ip: *const InternPool, ty: Index) bool {
     };
 }
 
+pub fn isUnion(ip: *const InternPool, ty: Index) bool {
+    return ip.indexToKey(ty) == .union_type;
+}
+
 pub fn isFunctionType(ip: *const InternPool, ty: Index) bool {
     return ip.indexToKey(ty) == .func_type;
 }
@@ -6010,13 +6303,11 @@ fn dumpStatsFallible(ip: *const InternPool, arena: Allocator) anyerror!void {
     const limbs_size = 8 * ip.limbs.items.len;
     // TODO: fields size is not taken into account
     const structs_size = ip.allocated_structs.len *
-        (@sizeOf(Module.Struct) + @sizeOf(Module.Namespace) + @sizeOf(Module.Decl));
-    const unions_size = ip.allocated_unions.len *
-        (@sizeOf(Module.Union) + @sizeOf(Module.Namespace) + @sizeOf(Module.Decl));
+        (@sizeOf(Module.Struct) + @sizeOf(Module.Namespace));
+    const decls_size = ip.allocated_decls.len * @sizeOf(Module.Decl);
 
     // TODO: map overhead size is not taken into account
-    const total_size = @sizeOf(InternPool) + items_size + extra_size + limbs_size +
-        structs_size + unions_size;
+    const total_size = @sizeOf(InternPool) + items_size + extra_size + limbs_size + structs_size + decls_size;
 
     std.debug.print(
         \\InternPool size: {d} bytes
@@ -6024,7 +6315,7 @@ fn dumpStatsFallible(ip: *const InternPool, arena: Allocator) anyerror!void {
         \\  {d} extra: {d} bytes
         \\  {d} limbs: {d} bytes
         \\  {d} structs: {d} bytes
-        \\  {d} unions: {d} bytes
+        \\  {d} decls: {d} bytes
         \\
     , .{
         total_size,
@@ -6036,8 +6327,8 @@ fn dumpStatsFallible(ip: *const InternPool, arena: Allocator) anyerror!void {
         limbs_size,
         ip.allocated_structs.len,
         structs_size,
-        ip.allocated_unions.len,
-        unions_size,
+        ip.allocated_decls.len,
+        decls_size,
     });
 
     const tags = ip.items.items(.tag);
@@ -6076,7 +6367,6 @@ fn dumpStatsFallible(ip: *const InternPool, arena: Allocator) anyerror!void {
                 const struct_obj = ip.structPtrConst(struct_index);
                 break :b @sizeOf(Module.Struct) +
                     @sizeOf(Module.Namespace) +
-                    @sizeOf(Module.Decl) +
                     (struct_obj.fields.count() * @sizeOf(Module.Struct.Field));
             },
             .type_struct_ns => @sizeOf(Module.Namespace),
@@ -6089,10 +6379,18 @@ fn dumpStatsFallible(ip: *const InternPool, arena: Allocator) anyerror!void {
                 break :b @sizeOf(TypeStructAnon) + (@sizeOf(u32) * 2 * info.fields_len);
             },
 
-            .type_union_tagged,
-            .type_union_untagged,
-            .type_union_safety,
-            => @sizeOf(Module.Union) + @sizeOf(Module.Namespace) + @sizeOf(Module.Decl),
+            .type_union => b: {
+                const info = ip.extraData(Tag.TypeUnion, data);
+                const enum_info = ip.indexToKey(info.tag_ty).enum_type;
+                const fields_len: u32 = @intCast(enum_info.names.len);
+                const per_field = @sizeOf(u32); // field type
+                // 1 byte per field for alignment, rounded up to the nearest 4 bytes
+                const alignments = if (info.flags.any_aligned_fields)
+                    ((fields_len + 3) / 4) * 4
+                else
+                    0;
+                break :b @sizeOf(Tag.TypeUnion) + (fields_len * per_field) + alignments;
+            },
 
             .type_function => b: {
                 const info = ip.extraData(Tag.TypeFunction, data);
@@ -6161,15 +6459,14 @@ fn dumpStatsFallible(ip: *const InternPool, arena: Allocator) anyerror!void {
             .float_c_longdouble_f80 => @sizeOf(Float80),
             .float_c_longdouble_f128 => @sizeOf(Float128),
             .float_comptime_float => @sizeOf(Float128),
-            .variable => @sizeOf(Tag.Variable) + @sizeOf(Module.Decl),
-            .extern_func => @sizeOf(Tag.ExternFunc) + @sizeOf(Module.Decl),
-            .func_decl => @sizeOf(Tag.FuncDecl) + @sizeOf(Module.Decl),
+            .variable => @sizeOf(Tag.Variable),
+            .extern_func => @sizeOf(Tag.ExternFunc),
+            .func_decl => @sizeOf(Tag.FuncDecl),
             .func_instance => b: {
                 const info = ip.extraData(Tag.FuncInstance, data);
                 const ty = ip.typeOf(info.generic_owner);
                 const params_len = ip.indexToKey(ty).func_type.param_types.len;
-                break :b @sizeOf(Tag.FuncInstance) + @sizeOf(Index) * params_len +
-                    @sizeOf(Module.Decl);
+                break :b @sizeOf(Tag.FuncInstance) + @sizeOf(Index) * params_len;
             },
             .func_coerced => @sizeOf(Tag.FuncCoerced),
             .only_possible_value => 0,
@@ -6230,9 +6527,7 @@ fn dumpAllFallible(ip: *const InternPool) anyerror!void {
             .type_struct_ns,
             .type_struct_anon,
             .type_tuple_anon,
-            .type_union_tagged,
-            .type_union_untagged,
-            .type_union_safety,
+            .type_union,
             .type_function,
             .undef,
             .runtime_value,
@@ -6358,14 +6653,6 @@ pub fn structPtrUnwrapConst(ip: *const InternPool, index: Module.Struct.Optional
     return structPtrConst(ip, index.unwrap() orelse return null);
 }
 
-pub fn unionPtr(ip: *InternPool, index: Module.Union.Index) *Module.Union {
-    return ip.allocated_unions.at(@intFromEnum(index));
-}
-
-pub fn unionPtrConst(ip: *const InternPool, index: Module.Union.Index) *const Module.Union {
-    return ip.allocated_unions.at(@intFromEnum(index));
-}
-
 pub fn declPtr(ip: *InternPool, index: Module.Decl.Index) *Module.Decl {
     return ip.allocated_decls.at(@intFromEnum(index));
 }
@@ -6400,28 +6687,6 @@ pub fn destroyStruct(ip: *InternPool, gpa: Allocator, index: Module.Struct.Index
     };
 }
 
-pub fn createUnion(
-    ip: *InternPool,
-    gpa: Allocator,
-    initialization: Module.Union,
-) Allocator.Error!Module.Union.Index {
-    if (ip.unions_free_list.popOrNull()) |index| {
-        ip.allocated_unions.at(@intFromEnum(index)).* = initialization;
-        return index;
-    }
-    const ptr = try ip.allocated_unions.addOne(gpa);
-    ptr.* = initialization;
-    return @enumFromInt(ip.allocated_unions.len - 1);
-}
-
-pub fn destroyUnion(ip: *InternPool, gpa: Allocator, index: Module.Union.Index) void {
-    ip.unionPtr(index).* = undefined;
-    ip.unions_free_list.append(gpa, index) catch {
-        // In order to keep `destroyUnion` a non-fallible function, we ignore memory
-        // allocation failures here, instead leaking the Union until garbage collection.
-    };
-}
-
 pub fn createDecl(
     ip: *InternPool,
     gpa: Allocator,
@@ -6667,9 +6932,7 @@ pub fn typeOf(ip: *const InternPool, index: Index) Index {
             .type_struct_ns,
             .type_struct_anon,
             .type_tuple_anon,
-            .type_union_tagged,
-            .type_union_untagged,
-            .type_union_safety,
+            .type_union,
             .type_function,
             => .type_type,
 
@@ -7005,10 +7268,7 @@ pub fn zigTypeTagOrPoison(ip: *const InternPool, index: Index) error{GenericPois
             .type_tuple_anon,
             => .Struct,
 
-            .type_union_tagged,
-            .type_union_untagged,
-            .type_union_safety,
-            => .Union,
+            .type_union => .Union,
 
             .type_function => .Fn,
 
src/Module.zig
@@ -96,7 +96,7 @@ intern_pool: InternPool = .{},
 /// Current uses that must be eliminated:
 /// * Struct comptime_args
 /// * Struct optimized_order
-/// * Union fields
+/// * comptime pointer mutation
 /// This memory lives until the Module is destroyed.
 tmp_hack_arena: std.heap.ArenaAllocator,
 
@@ -736,7 +736,7 @@ pub const Decl = struct {
 
     /// If the Decl owns its value and it is a union, return it,
     /// otherwise null.
-    pub fn getOwnedUnion(decl: Decl, mod: *Module) ?*Union {
+    pub fn getOwnedUnion(decl: Decl, mod: *Module) ?InternPool.UnionType {
         if (!decl.owns_tv) return null;
         if (decl.val.ip_index == .none) return null;
         return mod.typeToUnion(decl.val.toType());
@@ -778,7 +778,7 @@ pub const Decl = struct {
             else => switch (mod.intern_pool.indexToKey(decl.val.toIntern())) {
                 .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(),
+                .union_type => |union_type| union_type.namespace.toOptional(),
                 .enum_type => |enum_type| enum_type.namespace,
                 else => .none,
             },
@@ -1064,246 +1064,6 @@ pub const Struct = struct {
     }
 };
 
-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
-    /// layout does not store the tag.
-    /// Whether zig chooses this type or the user specifies it, it is stored here.
-    /// This will be set to the null type until status is `have_field_types`.
-    tag_ty: Type,
-    /// Set of field names in declaration order.
-    fields: Fields,
-    /// Represents the declarations inside this union.
-    namespace: Namespace.Index,
-    /// The Decl that corresponds to the union itself.
-    owner_decl: Decl.Index,
-    /// Index of the union_decl ZIR instruction.
-    zir_index: Zir.Inst.Index,
-
-    layout: std.builtin.Type.ContainerLayout,
-    status: enum {
-        none,
-        field_types_wip,
-        have_field_types,
-        layout_wip,
-        have_layout,
-        fully_resolved_wip,
-        // The types and all its fields have had their layout resolved. Even through pointer,
-        // which `have_layout` does not ensure.
-        fully_resolved,
-    },
-    requires_comptime: PropertyBoolean = .unknown,
-    assumed_runtime_bits: bool = false,
-
-    pub const Index = enum(u32) {
-        _,
-
-        pub fn toOptional(i: Index) OptionalIndex {
-            return @as(OptionalIndex, @enumFromInt(@intFromEnum(i)));
-        }
-    };
-
-    pub const OptionalIndex = enum(u32) {
-        none = std.math.maxInt(u32),
-        _,
-
-        pub fn init(oi: ?Index) OptionalIndex {
-            return @as(OptionalIndex, @enumFromInt(@intFromEnum(oi orelse return .none)));
-        }
-
-        pub fn unwrap(oi: OptionalIndex) ?Index {
-            if (oi == .none) return null;
-            return @as(Index, @enumFromInt(@intFromEnum(oi)));
-        }
-    };
-
-    pub const Field = struct {
-        /// undefined until `status` is `have_field_types` or `have_layout`.
-        ty: Type,
-        /// 0 means the ABI alignment of the type.
-        abi_align: Alignment,
-
-        /// Returns the field alignment, assuming the union is not packed.
-        /// Keep implementation in sync with `Sema.unionFieldAlignment`.
-        /// Prefer to call that function instead of this one during Sema.
-        pub fn normalAlignment(field: Field, mod: *Module) u32 {
-            return @as(u32, @intCast(field.abi_align.toByteUnitsOptional() orelse field.ty.abiAlignment(mod)));
-        }
-    };
-
-    pub const Fields = std.AutoArrayHashMapUnmanaged(InternPool.NullTerminatedString, Field);
-
-    pub fn getFullyQualifiedName(s: *Union, mod: *Module) !InternPool.NullTerminatedString {
-        return mod.declPtr(s.owner_decl).getFullyQualifiedName(mod);
-    }
-
-    pub fn srcLoc(self: Union, 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 haveFieldTypes(u: Union) bool {
-        return switch (u.status) {
-            .none,
-            .field_types_wip,
-            => false,
-            .have_field_types,
-            .layout_wip,
-            .have_layout,
-            .fully_resolved_wip,
-            .fully_resolved,
-            => true,
-        };
-    }
-
-    pub fn hasAllZeroBitFieldTypes(u: Union, mod: *Module) bool {
-        assert(u.haveFieldTypes());
-        for (u.fields.values()) |field| {
-            if (field.ty.hasRuntimeBits(mod)) return false;
-        }
-        return true;
-    }
-
-    pub fn mostAlignedField(u: Union, mod: *Module) u32 {
-        assert(u.haveFieldTypes());
-        var most_alignment: u32 = 0;
-        var most_index: usize = undefined;
-        for (u.fields.values(), 0..) |field, i| {
-            if (!field.ty.hasRuntimeBits(mod)) continue;
-
-            const field_align = field.normalAlignment(mod);
-            if (field_align > most_alignment) {
-                most_alignment = field_align;
-                most_index = i;
-            }
-        }
-        return @as(u32, @intCast(most_index));
-    }
-
-    /// Returns 0 if the union is represented with 0 bits at runtime.
-    pub fn abiAlignment(u: Union, mod: *Module, have_tag: bool) u32 {
-        var max_align: u32 = 0;
-        if (have_tag) max_align = u.tag_ty.abiAlignment(mod);
-        for (u.fields.values()) |field| {
-            if (!field.ty.hasRuntimeBits(mod)) continue;
-
-            const field_align = field.normalAlignment(mod);
-            max_align = @max(max_align, field_align);
-        }
-        return max_align;
-    }
-
-    pub fn abiSize(u: Union, mod: *Module, have_tag: bool) u64 {
-        return u.getLayout(mod, have_tag).abi_size;
-    }
-
-    pub const Layout = struct {
-        abi_size: u64,
-        abi_align: u32,
-        most_aligned_field: u32,
-        most_aligned_field_size: u64,
-        biggest_field: u32,
-        payload_size: u64,
-        payload_align: u32,
-        tag_align: u32,
-        tag_size: u64,
-        padding: u32,
-    };
-
-    pub fn haveLayout(u: Union) bool {
-        return switch (u.status) {
-            .none,
-            .field_types_wip,
-            .have_field_types,
-            .layout_wip,
-            => false,
-            .have_layout,
-            .fully_resolved_wip,
-            .fully_resolved,
-            => true,
-        };
-    }
-
-    pub fn getLayout(u: Union, mod: *Module, have_tag: bool) Layout {
-        assert(u.haveLayout());
-        var most_aligned_field: u32 = undefined;
-        var most_aligned_field_size: u64 = undefined;
-        var biggest_field: u32 = undefined;
-        var payload_size: u64 = 0;
-        var payload_align: u32 = 0;
-        const fields = u.fields.values();
-        for (fields, 0..) |field, i| {
-            if (!field.ty.hasRuntimeBitsIgnoreComptime(mod)) continue;
-
-            const field_align = field.abi_align.toByteUnitsOptional() orelse field.ty.abiAlignment(mod);
-            const field_size = field.ty.abiSize(mod);
-            if (field_size > payload_size) {
-                payload_size = field_size;
-                biggest_field = @as(u32, @intCast(i));
-            }
-            if (field_align > payload_align) {
-                payload_align = @as(u32, @intCast(field_align));
-                most_aligned_field = @as(u32, @intCast(i));
-                most_aligned_field_size = field_size;
-            }
-        }
-        payload_align = @max(payload_align, 1);
-        if (!have_tag or !u.tag_ty.hasRuntimeBits(mod)) {
-            return .{
-                .abi_size = std.mem.alignForward(u64, payload_size, payload_align),
-                .abi_align = payload_align,
-                .most_aligned_field = most_aligned_field,
-                .most_aligned_field_size = most_aligned_field_size,
-                .biggest_field = biggest_field,
-                .payload_size = payload_size,
-                .payload_align = payload_align,
-                .tag_align = 0,
-                .tag_size = 0,
-                .padding = 0,
-            };
-        }
-        // Put the tag before or after the payload depending on which one's
-        // alignment is greater.
-        const tag_size = u.tag_ty.abiSize(mod);
-        const tag_align = @max(1, u.tag_ty.abiAlignment(mod));
-        var size: u64 = 0;
-        var padding: u32 = undefined;
-        if (tag_align >= payload_align) {
-            // {Tag, Payload}
-            size += tag_size;
-            size = std.mem.alignForward(u64, size, payload_align);
-            size += payload_size;
-            const prev_size = size;
-            size = std.mem.alignForward(u64, size, tag_align);
-            padding = @as(u32, @intCast(size - prev_size));
-        } else {
-            // {Payload, Tag}
-            size += payload_size;
-            size = std.mem.alignForward(u64, size, tag_align);
-            size += tag_size;
-            const prev_size = size;
-            size = std.mem.alignForward(u64, size, payload_align);
-            padding = @as(u32, @intCast(size - prev_size));
-        }
-        return .{
-            .abi_size = size,
-            .abi_align = @max(tag_align, payload_align),
-            .most_aligned_field = most_aligned_field,
-            .most_aligned_field_size = most_aligned_field_size,
-            .biggest_field = biggest_field,
-            .payload_size = payload_size,
-            .payload_align = payload_align,
-            .tag_align = tag_align,
-            .tag_size = tag_size,
-            .padding = padding,
-        };
-    }
-};
-
 pub const DeclAdapter = struct {
     mod: *Module,
 
@@ -3182,10 +2942,6 @@ pub fn namespacePtr(mod: *Module, index: Namespace.Index) *Namespace {
     return mod.intern_pool.namespacePtr(index);
 }
 
-pub fn unionPtr(mod: *Module, index: Union.Index) *Union {
-    return mod.intern_pool.unionPtr(index);
-}
-
 pub fn structPtr(mod: *Module, index: Struct.Index) *Struct {
     return mod.intern_pool.structPtr(index);
 }
@@ -3651,11 +3407,11 @@ fn updateZirRefs(mod: *Module, file: *File, old_zir: Zir) !void {
             };
         }
 
-        if (decl.getOwnedUnion(mod)) |union_obj| {
-            union_obj.zir_index = inst_map.get(union_obj.zir_index) orelse {
+        if (decl.getOwnedUnion(mod)) |union_type| {
+            union_type.setZirIndex(ip, inst_map.get(union_type.zir_index) orelse {
                 try file.deleted_decls.append(gpa, decl_index);
                 continue;
-            };
+            });
         }
 
         if (decl.getOwnedFunction(mod)) |func| {
@@ -5550,14 +5306,6 @@ pub fn destroyStruct(mod: *Module, index: Struct.Index) void {
     return mod.intern_pool.destroyStruct(mod.gpa, index);
 }
 
-pub fn createUnion(mod: *Module, initialization: Union) Allocator.Error!Union.Index {
-    return mod.intern_pool.createUnion(mod.gpa, initialization);
-}
-
-pub fn destroyUnion(mod: *Module, index: Union.Index) void {
-    return mod.intern_pool.destroyUnion(mod.gpa, index);
-}
-
 pub fn allocateNewDecl(
     mod: *Module,
     namespace: Namespace.Index,
@@ -6956,10 +6704,14 @@ pub fn typeToStruct(mod: *Module, ty: Type) ?*Struct {
     return mod.structPtr(struct_index);
 }
 
-pub fn typeToUnion(mod: *Module, ty: Type) ?*Union {
+/// This asserts that the union's enum tag type has been resolved.
+pub fn typeToUnion(mod: *Module, ty: Type) ?InternPool.UnionType {
     if (ty.ip_index == .none) return null;
-    const union_index = mod.intern_pool.indexToUnionType(ty.toIntern()).unwrap() orelse return null;
-    return mod.unionPtr(union_index);
+    const ip = &mod.intern_pool;
+    switch (ip.indexToKey(ty.ip_index)) {
+        .union_type => |k| return ip.loadUnionType(k),
+        else => return null,
+    }
 }
 
 pub fn typeToFunc(mod: *Module, ty: Type) ?InternPool.Key.FuncType {
@@ -7045,3 +6797,131 @@ pub fn getParamName(mod: *Module, func_index: InternPool.Index, index: u32) [:0]
         else => unreachable,
     };
 }
+
+pub const UnionLayout = struct {
+    abi_size: u64,
+    abi_align: u32,
+    most_aligned_field: u32,
+    most_aligned_field_size: u64,
+    biggest_field: u32,
+    payload_size: u64,
+    payload_align: u32,
+    tag_align: u32,
+    tag_size: u64,
+    padding: u32,
+};
+
+pub fn getUnionLayout(mod: *Module, u: InternPool.UnionType) UnionLayout {
+    const ip = &mod.intern_pool;
+    assert(u.haveLayout(ip));
+    var most_aligned_field: u32 = undefined;
+    var most_aligned_field_size: u64 = undefined;
+    var biggest_field: u32 = undefined;
+    var payload_size: u64 = 0;
+    var payload_align: u32 = 0;
+    for (u.field_types.get(ip), 0..) |field_ty, i| {
+        if (!field_ty.toType().hasRuntimeBitsIgnoreComptime(mod)) continue;
+
+        const field_align = u.fieldAlign(ip, @intCast(i)).toByteUnitsOptional() orelse
+            field_ty.toType().abiAlignment(mod);
+        const field_size = field_ty.toType().abiSize(mod);
+        if (field_size > payload_size) {
+            payload_size = field_size;
+            biggest_field = @intCast(i);
+        }
+        if (field_align > payload_align) {
+            payload_align = @intCast(field_align);
+            most_aligned_field = @intCast(i);
+            most_aligned_field_size = field_size;
+        }
+    }
+    payload_align = @max(payload_align, 1);
+    const have_tag = u.flagsPtr(ip).runtime_tag.hasTag();
+    if (!have_tag or !u.enum_tag_ty.toType().hasRuntimeBits(mod)) {
+        return .{
+            .abi_size = std.mem.alignForward(u64, payload_size, payload_align),
+            .abi_align = payload_align,
+            .most_aligned_field = most_aligned_field,
+            .most_aligned_field_size = most_aligned_field_size,
+            .biggest_field = biggest_field,
+            .payload_size = payload_size,
+            .payload_align = payload_align,
+            .tag_align = 0,
+            .tag_size = 0,
+            .padding = 0,
+        };
+    }
+    // Put the tag before or after the payload depending on which one's
+    // alignment is greater.
+    const tag_size = u.enum_tag_ty.toType().abiSize(mod);
+    const tag_align = @max(1, u.enum_tag_ty.toType().abiAlignment(mod));
+    var size: u64 = 0;
+    var padding: u32 = undefined;
+    if (tag_align >= payload_align) {
+        // {Tag, Payload}
+        size += tag_size;
+        size = std.mem.alignForward(u64, size, payload_align);
+        size += payload_size;
+        const prev_size = size;
+        size = std.mem.alignForward(u64, size, tag_align);
+        padding = @as(u32, @intCast(size - prev_size));
+    } else {
+        // {Payload, Tag}
+        size += payload_size;
+        size = std.mem.alignForward(u64, size, tag_align);
+        size += tag_size;
+        const prev_size = size;
+        size = std.mem.alignForward(u64, size, payload_align);
+        padding = @as(u32, @intCast(size - prev_size));
+    }
+    return .{
+        .abi_size = size,
+        .abi_align = @max(tag_align, payload_align),
+        .most_aligned_field = most_aligned_field,
+        .most_aligned_field_size = most_aligned_field_size,
+        .biggest_field = biggest_field,
+        .payload_size = payload_size,
+        .payload_align = payload_align,
+        .tag_align = tag_align,
+        .tag_size = tag_size,
+        .padding = padding,
+    };
+}
+
+pub fn unionAbiSize(mod: *Module, u: InternPool.UnionType) u64 {
+    return mod.getUnionLayout(u).abi_size;
+}
+
+/// Returns 0 if the union is represented with 0 bits at runtime.
+/// TODO: this returns alignment in byte units should should be a u64
+pub fn unionAbiAlignment(mod: *Module, u: InternPool.UnionType) u32 {
+    const ip = &mod.intern_pool;
+    const have_tag = u.flagsPtr(ip).runtime_tag.hasTag();
+    var max_align: u32 = 0;
+    if (have_tag) max_align = u.enum_tag_ty.toType().abiAlignment(mod);
+    for (u.field_types.get(ip), 0..) |field_ty, field_index| {
+        if (!field_ty.toType().hasRuntimeBits(mod)) continue;
+
+        const field_align = mod.unionFieldNormalAlignment(u, @intCast(field_index));
+        max_align = @max(max_align, field_align);
+    }
+    return max_align;
+}
+
+/// Returns the field alignment, assuming the union is not packed.
+/// Keep implementation in sync with `Sema.unionFieldAlignment`.
+/// Prefer to call that function instead of this one during Sema.
+/// TODO: this returns alignment in byte units should should be a u64
+pub fn unionFieldNormalAlignment(mod: *Module, u: InternPool.UnionType, field_index: u32) u32 {
+    const ip = &mod.intern_pool;
+    if (u.fieldAlign(ip, field_index).toByteUnitsOptional()) |a| return @intCast(a);
+    const field_ty = u.field_types.get(ip)[field_index].toType();
+    return field_ty.abiAlignment(mod);
+}
+
+pub fn unionTagFieldIndex(mod: *Module, u: InternPool.UnionType, enum_tag: Value) ?u32 {
+    const ip = &mod.intern_pool;
+    assert(ip.typeOf(enum_tag.toIntern()) == u.enum_tag_ty);
+    const enum_type = ip.indexToKey(u.enum_tag_ty).enum_type;
+    return enum_type.tagValueIndex(ip, enum_tag.toIntern());
+}
src/Sema.zig
@@ -3022,18 +3022,18 @@ fn zirEnumDecl(
 
     const mod = sema.mod;
     const gpa = sema.gpa;
-    const small = @as(Zir.Inst.EnumDecl.Small, @bitCast(extended.small));
+    const small: Zir.Inst.EnumDecl.Small = @bitCast(extended.small);
     var extra_index: usize = extended.operand;
 
     const src: LazySrcLoc = if (small.has_src_node) blk: {
-        const node_offset = @as(i32, @bitCast(sema.code.extra[extra_index]));
+        const node_offset: i32 = @bitCast(sema.code.extra[extra_index]);
         extra_index += 1;
         break :blk LazySrcLoc.nodeOffset(node_offset);
     } else sema.src;
     const tag_ty_src: LazySrcLoc = .{ .node_offset_container_tag = src.node_offset.x };
 
     const tag_type_ref = if (small.has_tag_type) blk: {
-        const tag_type_ref = @as(Zir.Inst.Ref, @enumFromInt(sema.code.extra[extra_index]));
+        const tag_type_ref: Zir.Inst.Ref = @enumFromInt(sema.code.extra[extra_index]);
         extra_index += 1;
         break :blk tag_type_ref;
     } else .none;
@@ -3310,7 +3310,11 @@ fn zirUnionDecl(
 
     extra_index += @intFromBool(small.has_tag_type);
     extra_index += @intFromBool(small.has_body_len);
-    extra_index += @intFromBool(small.has_fields_len);
+    const fields_len = if (small.has_fields_len) blk: {
+        const fields_len = sema.code.extra[extra_index];
+        extra_index += 1;
+        break :blk fields_len;
+    } else 0;
 
     const decls_len = if (small.has_decls_len) blk: {
         const decls_len = sema.code.extra[extra_index];
@@ -3338,29 +3342,31 @@ fn zirUnionDecl(
     const new_namespace = mod.namespacePtr(new_namespace_index);
     errdefer mod.destroyNamespace(new_namespace_index);
 
-    const union_index = try mod.createUnion(.{
-        .owner_decl = new_decl_index,
-        .tag_ty = Type.null,
-        .fields = .{},
-        .zir_index = inst,
-        .layout = small.layout,
-        .status = .none,
-        .namespace = new_namespace_index,
-    });
-    errdefer mod.destroyUnion(union_index);
-
     const union_ty = ty: {
-        const ty = try mod.intern_pool.get(gpa, .{ .union_type = .{
-            .index = union_index,
-            .runtime_tag = if (small.has_tag_type or small.auto_enum_tag)
-                .tagged
-            else if (small.layout != .Auto)
-                .none
-            else switch (block.sema.mod.optimizeMode()) {
-                .Debug, .ReleaseSafe => .safety,
-                .ReleaseFast, .ReleaseSmall => .none,
+        const ty = try mod.intern_pool.getUnionType(gpa, .{
+            .flags = .{
+                .layout = small.layout,
+                .status = .none,
+                .runtime_tag = if (small.has_tag_type or small.auto_enum_tag)
+                    .tagged
+                else if (small.layout != .Auto)
+                    .none
+                else switch (block.wantSafety()) {
+                    true => .safety,
+                    false => .none,
+                },
+                .any_aligned_fields = small.any_aligned_fields,
+                .requires_comptime = .unknown,
+                .assumed_runtime_bits = false,
             },
-        } });
+            .decl = new_decl_index,
+            .namespace = new_namespace_index,
+            .zir_index = inst,
+            .fields_len = fields_len,
+            .enum_tag_ty = .none,
+            .field_types = &.{},
+            .field_aligns = &.{},
+        });
         if (sema.builtin_type_target_index != .none) {
             mod.intern_pool.resolveBuiltinType(sema.builtin_type_target_index, ty);
             break :ty sema.builtin_type_target_index;
@@ -4505,8 +4511,7 @@ fn validateUnionInit(
     const field_src: LazySrcLoc = .{ .node_offset_initializer = field_ptr_data.src_node };
     const field_ptr_extra = sema.code.extraData(Zir.Inst.Field, field_ptr_data.payload_index).data;
     const field_name = try mod.intern_pool.getOrPutString(gpa, sema.code.nullTerminatedString(field_ptr_extra.field_name_start));
-    // Validate the field access but ignore the index since we want the tag enum field index.
-    _ = try sema.unionFieldIndex(block, union_ty, field_name, field_src);
+    const field_index = try sema.unionFieldIndex(block, union_ty, field_name, field_src);
     const air_tags = sema.air_instructions.items(.tag);
     const air_datas = sema.air_instructions.items(.data);
     const field_ptr_ref = sema.inst_map.get(field_ptr).?;
@@ -4563,8 +4568,7 @@ fn validateUnionInit(
     }
 
     const tag_ty = union_ty.unionTagTypeHypothetical(mod);
-    const enum_field_index = @as(u32, @intCast(tag_ty.enumFieldIndex(field_name, mod).?));
-    const tag_val = try mod.enumValueFieldIndex(tag_ty, enum_field_index);
+    const tag_val = try mod.enumValueFieldIndex(tag_ty, field_index);
 
     if (init_val) |val| {
         // Our task is to delete all the `field_ptr` and `store` instructions, and insert
@@ -5227,14 +5231,15 @@ fn failWithBadStructFieldAccess(
 fn failWithBadUnionFieldAccess(
     sema: *Sema,
     block: *Block,
-    union_obj: *Module.Union,
+    union_obj: InternPool.UnionType,
     field_src: LazySrcLoc,
     field_name: InternPool.NullTerminatedString,
 ) CompileError {
     const mod = sema.mod;
     const gpa = sema.gpa;
 
-    const fqn = try union_obj.getFullyQualifiedName(mod);
+    const decl = mod.declPtr(union_obj.decl);
+    const fqn = try decl.getFullyQualifiedName(mod);
 
     const msg = msg: {
         const msg = try sema.errMsg(
@@ -5244,7 +5249,7 @@ fn failWithBadUnionFieldAccess(
             .{ field_name.fmt(&mod.intern_pool), fqn.fmt(&mod.intern_pool) },
         );
         errdefer msg.destroy(gpa);
-        try mod.errNoteNonLazy(union_obj.srcLoc(mod), msg, "union declared here", .{});
+        try mod.errNoteNonLazy(decl.srcLoc(mod), msg, "union declared here", .{});
         break :msg msg;
     };
     return sema.failWithOwnedErrorMsg(msg);
@@ -10500,6 +10505,7 @@ const SwitchProngAnalysis = struct {
     ) CompileError!Air.Inst.Ref {
         const sema = spa.sema;
         const mod = sema.mod;
+        const ip = &mod.intern_pool;
 
         const zir_datas = sema.code.instructions.items(.data);
         const switch_node_offset = zir_datas[spa.switch_block_inst].pl_node.src_node;
@@ -10511,9 +10517,9 @@ const SwitchProngAnalysis = struct {
         if (inline_case_capture != .none) {
             const item_val = sema.resolveConstValue(block, .unneeded, inline_case_capture, "") catch unreachable;
             if (operand_ty.zigTypeTag(mod) == .Union) {
-                const field_index = @as(u32, @intCast(operand_ty.unionTagFieldIndex(item_val, mod).?));
+                const field_index: u32 = @intCast(operand_ty.unionTagFieldIndex(item_val, mod).?);
                 const union_obj = mod.typeToUnion(operand_ty).?;
-                const field_ty = union_obj.fields.values()[field_index].ty;
+                const field_ty = union_obj.field_types.get(ip)[field_index].toType();
                 if (capture_byref) {
                     const ptr_field_ty = try mod.ptrType(.{
                         .child = field_ty.toIntern(),
@@ -10535,7 +10541,7 @@ const SwitchProngAnalysis = struct {
                     return block.addStructFieldPtr(spa.operand_ptr, field_index, ptr_field_ty);
                 } else {
                     if (try sema.resolveDefinedValue(block, sema.src, spa.operand)) |union_val| {
-                        const tag_and_val = mod.intern_pool.indexToKey(union_val.toIntern()).un;
+                        const tag_and_val = ip.indexToKey(union_val.toIntern()).un;
                         return Air.internedToRef(tag_and_val.val);
                     }
                     return block.addStructFieldVal(spa.operand, field_index, field_ty);
@@ -10568,14 +10574,14 @@ const SwitchProngAnalysis = struct {
                 const union_obj = mod.typeToUnion(operand_ty).?;
                 const first_item_val = sema.resolveConstValue(block, .unneeded, case_vals[0], "") catch unreachable;
 
-                const first_field_index = @as(u32, @intCast(operand_ty.unionTagFieldIndex(first_item_val, mod).?));
-                const first_field = union_obj.fields.values()[first_field_index];
+                const first_field_index: u32 = mod.unionTagFieldIndex(union_obj, first_item_val).?;
+                const first_field_ty = union_obj.field_types.get(ip)[first_field_index].toType();
 
                 const field_tys = try sema.arena.alloc(Type, case_vals.len);
                 for (case_vals, field_tys) |item, *field_ty| {
                     const item_val = sema.resolveConstValue(block, .unneeded, item, "") catch unreachable;
-                    const field_idx = @as(u32, @intCast(operand_ty.unionTagFieldIndex(item_val, sema.mod).?));
-                    field_ty.* = union_obj.fields.values()[field_idx].ty;
+                    const field_idx = mod.unionTagFieldIndex(union_obj, item_val).?;
+                    field_ty.* = union_obj.field_types.get(ip)[field_idx].toType();
                 }
 
                 // Fast path: if all the operands are the same type already, we don't need to hit
@@ -10682,7 +10688,7 @@ const SwitchProngAnalysis = struct {
 
                 if (try sema.resolveDefinedValue(block, operand_src, spa.operand)) |operand_val| {
                     if (operand_val.isUndef(mod)) return mod.undefRef(capture_ty);
-                    const union_val = mod.intern_pool.indexToKey(operand_val.toIntern()).un;
+                    const union_val = ip.indexToKey(operand_val.toIntern()).un;
                     if (union_val.tag.toValue().isUndef(mod)) return mod.undefRef(capture_ty);
                     const uncoerced = Air.internedToRef(union_val.val);
                     return sema.coerce(block, capture_ty, uncoerced, operand_src);
@@ -10704,7 +10710,7 @@ const SwitchProngAnalysis = struct {
                     }
                     // All fields are in-memory coercible to the resolved type!
                     // Just take the first field and bitcast the result.
-                    const uncoerced = try block.addStructFieldVal(spa.operand, first_field_index, first_field.ty);
+                    const uncoerced = try block.addStructFieldVal(spa.operand, first_field_index, first_field_ty);
                     return block.addBitCast(capture_ty, uncoerced);
                 };
 
@@ -12287,7 +12293,7 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
             for (seen_enum_fields, 0..) |seen_field, index| {
                 if (seen_field != null) continue;
                 const union_obj = mod.typeToUnion(maybe_union_ty).?;
-                const field_ty = union_obj.fields.values()[index].ty;
+                const field_ty = union_obj.field_types.get(ip)[index].toType();
                 if (field_ty.zigTypeTag(mod) != .NoReturn) break true;
             } else false
         else
@@ -12800,9 +12806,8 @@ fn zirHasField(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Ai
                 break :hf struct_obj.fields.contains(field_name);
             },
             .union_type => |union_type| {
-                const union_obj = mod.unionPtr(union_type.index);
-                assert(union_obj.haveFieldTypes());
-                break :hf union_obj.fields.contains(field_name);
+                const union_obj = ip.loadUnionType(union_type);
+                break :hf union_obj.nameIndex(ip, field_name) != null;
             },
             .enum_type => |enum_type| {
                 break :hf enum_type.nameIndex(ip, field_name) != null;
@@ -17271,16 +17276,15 @@ fn zirTypeInfo(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Ai
             };
 
             try sema.resolveTypeLayout(ty); // Getting alignment requires type layout
-            const layout = ty.containerLayout(mod);
+            const union_obj = mod.typeToUnion(ty).?;
+            const layout = union_obj.getLayout(ip);
 
-            const union_fields = ty.unionFields(mod);
-            const union_field_vals = try gpa.alloc(InternPool.Index, union_fields.count());
+            const union_field_vals = try gpa.alloc(InternPool.Index, union_obj.field_names.len);
             defer gpa.free(union_field_vals);
 
             for (union_field_vals, 0..) |*field_val, i| {
-                const field = union_fields.values()[i];
                 // TODO: write something like getCoercedInts to avoid needing to dupe
-                const name = try sema.arena.dupe(u8, ip.stringToSlice(union_fields.keys()[i]));
+                const name = try sema.arena.dupe(u8, ip.stringToSlice(union_obj.field_names.get(ip)[i]));
                 const name_val = v: {
                     var anon_decl = try block.startAnonDecl();
                     defer anon_decl.deinit();
@@ -17304,15 +17308,16 @@ fn zirTypeInfo(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Ai
                 };
 
                 const alignment = switch (layout) {
-                    .Auto, .Extern => try sema.unionFieldAlignment(field),
+                    .Auto, .Extern => try sema.unionFieldAlignment(union_obj, @intCast(i)),
                     .Packed => 0,
                 };
 
+                const field_ty = union_obj.field_types.get(ip)[i];
                 const union_field_fields = .{
                     // name: []const u8,
                     name_val,
                     // type: type,
-                    field.ty.toIntern(),
+                    field_ty,
                     // alignment: comptime_int,
                     (try mod.intValue(Type.comptime_int, alignment)).toIntern(),
                 };
@@ -18929,18 +18934,18 @@ fn unionInit(
     field_src: LazySrcLoc,
 ) CompileError!Air.Inst.Ref {
     const mod = sema.mod;
+    const ip = &mod.intern_pool;
     const field_index = try sema.unionFieldIndex(block, union_ty, field_name, field_src);
-    const field = union_ty.unionFields(mod).values()[field_index];
-    const init = try sema.coerce(block, field.ty, uncasted_init, init_src);
+    const field_ty = mod.typeToUnion(union_ty).?.field_types.get(ip)[field_index].toType();
+    const init = try sema.coerce(block, field_ty, uncasted_init, init_src);
 
     if (try sema.resolveMaybeUndefVal(init)) |init_val| {
         const tag_ty = union_ty.unionTagTypeHypothetical(mod);
-        const enum_field_index = @as(u32, @intCast(tag_ty.enumFieldIndex(field_name, mod).?));
-        const tag_val = try mod.enumValueFieldIndex(tag_ty, enum_field_index);
+        const tag_val = try mod.enumValueFieldIndex(tag_ty, field_index);
         return Air.internedToRef((try mod.intern(.{ .un = .{
             .ty = union_ty.toIntern(),
             .tag = try tag_val.intern(tag_ty, mod),
-            .val = try init_val.intern(field.ty, mod),
+            .val = try init_val.intern(field_ty, mod),
         } })));
     }
 
@@ -18963,6 +18968,7 @@ fn zirStructInit(
     const src = inst_data.src();
 
     const mod = sema.mod;
+    const ip = &mod.intern_pool;
     const first_item = sema.code.extraData(Zir.Inst.StructInit.Item, extra.end).data;
     const first_field_type_data = zir_datas[first_item.field_type].pl_node;
     const first_field_type_extra = sema.code.extraData(Zir.Inst.FieldType, first_field_type_data.payload_index).data;
@@ -18999,7 +19005,7 @@ fn zirStructInit(
             const field_type_data = zir_datas[item.data.field_type].pl_node;
             const field_src: LazySrcLoc = .{ .node_offset_initializer = field_type_data.src_node };
             const field_type_extra = sema.code.extraData(Zir.Inst.FieldType, field_type_data.payload_index).data;
-            const field_name = try mod.intern_pool.getOrPutString(gpa, sema.code.nullTerminatedString(field_type_extra.name_start));
+            const field_name = try ip.getOrPutString(gpa, sema.code.nullTerminatedString(field_type_extra.name_start));
             const field_index = if (resolved_ty.isTuple(mod))
                 try sema.tupleFieldIndex(block, resolved_ty, field_name, field_src)
             else
@@ -19040,19 +19046,18 @@ fn zirStructInit(
         const field_type_data = zir_datas[item.data.field_type].pl_node;
         const field_src: LazySrcLoc = .{ .node_offset_initializer = field_type_data.src_node };
         const field_type_extra = sema.code.extraData(Zir.Inst.FieldType, field_type_data.payload_index).data;
-        const field_name = try mod.intern_pool.getOrPutString(gpa, sema.code.nullTerminatedString(field_type_extra.name_start));
+        const field_name = try ip.getOrPutString(gpa, 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 = @as(u32, @intCast(tag_ty.enumFieldIndex(field_name, mod).?));
-        const tag_val = try mod.enumValueFieldIndex(tag_ty, enum_field_index);
+        const tag_val = try mod.enumValueFieldIndex(tag_ty, field_index);
 
         const init_inst = try sema.resolveInst(item.data.init);
         if (try sema.resolveMaybeUndefVal(init_inst)) |val| {
-            const field = resolved_ty.unionFields(mod).values()[field_index];
+            const field_ty = mod.typeToUnion(resolved_ty).?.field_types.get(ip)[field_index].toType();
             return sema.addConstantMaybeRef(block, resolved_ty, (try mod.intern(.{ .un = .{
                 .ty = resolved_ty.toIntern(),
                 .tag = try tag_val.intern(tag_ty, mod),
-                .val = try val.intern(field.ty, mod),
+                .val = try val.intern(field_ty, mod),
             } })).toValue(), is_ref);
         }
 
@@ -19662,11 +19667,12 @@ fn fieldType(
     ty_src: LazySrcLoc,
 ) CompileError!Air.Inst.Ref {
     const mod = sema.mod;
+    const ip = &mod.intern_pool;
     var cur_ty = aggregate_ty;
     while (true) {
         try sema.resolveTypeFields(cur_ty);
         switch (cur_ty.zigTypeTag(mod)) {
-            .Struct => switch (mod.intern_pool.indexToKey(cur_ty.toIntern())) {
+            .Struct => switch (ip.indexToKey(cur_ty.toIntern())) {
                 .anon_struct_type => |anon_struct| {
                     const field_index = try sema.anonStructFieldIndex(block, cur_ty, field_name, field_src);
                     return Air.internedToRef(anon_struct.types[field_index]);
@@ -19681,14 +19687,15 @@ fn fieldType(
             },
             .Union => {
                 const union_obj = mod.typeToUnion(cur_ty).?;
-                const field = union_obj.fields.get(field_name) orelse
+                const field_index = union_obj.nameIndex(ip, field_name) orelse
                     return sema.failWithBadUnionFieldAccess(block, union_obj, field_src, field_name);
-                return Air.internedToRef(field.ty.toIntern());
+                const field_ty = union_obj.field_types.get(ip)[field_index];
+                return Air.internedToRef(field_ty);
             },
             .Optional => {
                 // Struct/array init through optional requires the child type to not be a pointer.
                 // If the child of .optional is a pointer it'll error on the next loop.
-                cur_ty = mod.intern_pool.indexToKey(cur_ty.toIntern()).opt_type.toType();
+                cur_ty = ip.indexToKey(cur_ty.toIntern()).opt_type.toType();
                 continue;
             },
             .ErrorUnion => {
@@ -20396,68 +20403,16 @@ fn zirReify(
                 return sema.fail(block, src, "reified unions must have no decls", .{});
             }
             const layout = mod.toEnum(std.builtin.Type.ContainerLayout, layout_val);
-
-            // Because these three things each reference each other, `undefined`
-            // placeholders are used before being set after the union type gains an
-            // InternPool index.
-
-            const new_decl_index = try sema.createAnonymousDeclTypeNamed(block, src, .{
-                .ty = Type.noreturn,
-                .val = Value.@"unreachable",
-            }, name_strategy, "union", inst);
-            const new_decl = mod.declPtr(new_decl_index);
-            new_decl.owns_tv = true;
-            errdefer {
-                new_decl.has_tv = false; // namespace and val were destroyed by later errdefers
-                mod.abortAnonDecl(new_decl_index);
-            }
-
-            const new_namespace_index = try mod.createNamespace(.{
-                .parent = block.namespace.toOptional(),
-                .ty = undefined,
-                .file_scope = block.getFileScope(mod),
-            });
-            const new_namespace = mod.namespacePtr(new_namespace_index);
-            errdefer mod.destroyNamespace(new_namespace_index);
-
-            const union_index = try mod.createUnion(.{
-                .owner_decl = new_decl_index,
-                .tag_ty = Type.null,
-                .fields = .{},
-                .zir_index = inst,
-                .layout = layout,
-                .status = .have_field_types,
-                .namespace = new_namespace_index,
-            });
-            const union_obj = mod.unionPtr(union_index);
-            errdefer mod.destroyUnion(union_index);
-
-            const union_ty = try ip.get(gpa, .{ .union_type = .{
-                .index = union_index,
-                .runtime_tag = if (!tag_type_val.isNull(mod))
-                    .tagged
-                else if (layout != .Auto)
-                    .none
-                else switch (mod.optimizeMode()) {
-                    .Debug, .ReleaseSafe => .safety,
-                    .ReleaseFast, .ReleaseSmall => .none,
-                },
-            } });
-            // TODO: figure out InternPool removals for incremental compilation
-            //errdefer ip.remove(union_ty);
-
-            new_decl.ty = Type.type;
-            new_decl.val = union_ty.toValue();
-            new_namespace.ty = union_ty.toType();
+            const fields_len: u32 = @intCast(try sema.usizeCast(block, src, fields_val.sliceLen(mod)));
 
             // Tag type
-            const fields_len = try sema.usizeCast(block, src, fields_val.sliceLen(mod));
             var explicit_tags_seen: []bool = &.{};
             var enum_field_names: []InternPool.NullTerminatedString = &.{};
+            var enum_tag_ty: InternPool.Index = .none;
             if (tag_type_val.optionalValue(mod)) |payload_val| {
-                union_obj.tag_ty = payload_val.toType();
+                enum_tag_ty = payload_val.toType().toIntern();
 
-                const enum_type = switch (ip.indexToKey(union_obj.tag_ty.toIntern())) {
+                const enum_type = switch (ip.indexToKey(enum_tag_ty)) {
                     .enum_type => |x| x,
                     else => return sema.fail(block, src, "Type.Union.tag_type must be an enum type", .{}),
                 };
@@ -20469,7 +20424,13 @@ fn zirReify(
             }
 
             // Fields
-            try union_obj.fields.ensureTotalCapacity(mod.tmp_hack_arena.allocator(), fields_len);
+            var any_aligned_fields: bool = false;
+            var union_fields: std.MultiArrayList(struct {
+                type: InternPool.Index,
+                alignment: InternPool.Alignment,
+            }) = .{};
+            var field_name_table: std.AutoArrayHashMapUnmanaged(InternPool.NullTerminatedString, void) = .{};
+            try field_name_table.ensureTotalCapacity(sema.arena, fields_len);
 
             for (0..fields_len) |i| {
                 const elem_val = try fields_val.elemValue(mod, i);
@@ -20491,15 +20452,15 @@ fn zirReify(
                 }
 
                 if (explicit_tags_seen.len > 0) {
-                    const tag_info = ip.indexToKey(union_obj.tag_ty.toIntern()).enum_type;
+                    const tag_info = ip.indexToKey(enum_tag_ty).enum_type;
                     const enum_index = tag_info.nameIndex(ip, field_name) orelse {
                         const msg = msg: {
                             const msg = try sema.errMsg(block, src, "no field named '{}' in enum '{}'", .{
                                 field_name.fmt(ip),
-                                union_obj.tag_ty.fmt(mod),
+                                enum_tag_ty.toType().fmt(mod),
                             });
                             errdefer msg.destroy(gpa);
-                            try sema.addDeclaredHereNote(msg, union_obj.tag_ty);
+                            try sema.addDeclaredHereNote(msg, enum_tag_ty.toType());
                             break :msg msg;
                         };
                         return sema.failWithOwnedErrorMsg(msg);
@@ -20510,17 +20471,20 @@ fn zirReify(
                     explicit_tags_seen[enum_index] = true;
                 }
 
-                const gop = union_obj.fields.getOrPutAssumeCapacity(field_name);
+                const gop = field_name_table.getOrPutAssumeCapacity(field_name);
                 if (gop.found_existing) {
                     // TODO: better source location
                     return sema.fail(block, src, "duplicate union field {}", .{field_name.fmt(ip)});
                 }
 
                 const field_ty = type_val.toType();
-                gop.value_ptr.* = .{
-                    .ty = field_ty,
-                    .abi_align = Alignment.fromByteUnits((try alignment_val.getUnsignedIntAdvanced(mod, sema)).?),
-                };
+                const field_align = Alignment.fromByteUnits((try alignment_val.getUnsignedIntAdvanced(mod, sema)).?);
+                any_aligned_fields = any_aligned_fields or field_align != .none;
+
+                try union_fields.append(sema.arena, .{
+                    .type = field_ty.toIntern(),
+                    .alignment = field_align,
+                });
 
                 if (field_ty.zigTypeTag(mod) == .Opaque) {
                     const msg = msg: {
@@ -20532,7 +20496,7 @@ fn zirReify(
                     };
                     return sema.failWithOwnedErrorMsg(msg);
                 }
-                if (union_obj.layout == .Extern and !try sema.validateExternType(field_ty, .union_field)) {
+                if (layout == .Extern and !try sema.validateExternType(field_ty, .union_field)) {
                     const msg = msg: {
                         const msg = try sema.errMsg(block, src, "extern unions cannot contain fields of type '{}'", .{field_ty.fmt(mod)});
                         errdefer msg.destroy(gpa);
@@ -20544,7 +20508,7 @@ fn zirReify(
                         break :msg msg;
                     };
                     return sema.failWithOwnedErrorMsg(msg);
-                } else if (union_obj.layout == .Packed and !(validatePackedType(field_ty, mod))) {
+                } else if (layout == .Packed and !(validatePackedType(field_ty, mod))) {
                     const msg = msg: {
                         const msg = try sema.errMsg(block, src, "packed unions cannot contain fields of type '{}'", .{field_ty.fmt(mod)});
                         errdefer msg.destroy(gpa);
@@ -20560,28 +20524,79 @@ fn zirReify(
             }
 
             if (explicit_tags_seen.len > 0) {
-                const tag_info = ip.indexToKey(union_obj.tag_ty.toIntern()).enum_type;
+                const tag_info = ip.indexToKey(enum_tag_ty).enum_type;
                 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 (tag_info.names.get(ip), 0..) |field_name, field_index| {
                             if (explicit_tags_seen[field_index]) continue;
-                            try sema.addFieldErrNote(enum_ty, field_index, msg, "field '{}' missing, declared here", .{
+                            try sema.addFieldErrNote(enum_tag_ty.toType(), field_index, msg, "field '{}' missing, declared here", .{
                                 field_name.fmt(ip),
                             });
                         }
-                        try sema.addDeclaredHereNote(msg, union_obj.tag_ty);
+                        try sema.addDeclaredHereNote(msg, enum_tag_ty.toType());
                         break :msg msg;
                     };
                     return sema.failWithOwnedErrorMsg(msg);
                 }
             } else {
-                union_obj.tag_ty = try sema.generateUnionTagTypeSimple(block, enum_field_names, null);
+                enum_tag_ty = try sema.generateUnionTagTypeSimple(block, enum_field_names, .none);
+            }
+
+            // Because these three things each reference each other, `undefined`
+            // placeholders are used before being set after the union type gains an
+            // InternPool index.
+
+            const new_decl_index = try sema.createAnonymousDeclTypeNamed(block, src, .{
+                .ty = Type.noreturn,
+                .val = Value.@"unreachable",
+            }, name_strategy, "union", inst);
+            const new_decl = mod.declPtr(new_decl_index);
+            new_decl.owns_tv = true;
+            errdefer {
+                new_decl.has_tv = false; // namespace and val were destroyed by later errdefers
+                mod.abortAnonDecl(new_decl_index);
             }
 
+            const new_namespace_index = try mod.createNamespace(.{
+                .parent = block.namespace.toOptional(),
+                .ty = undefined,
+                .file_scope = block.getFileScope(mod),
+            });
+            const new_namespace = mod.namespacePtr(new_namespace_index);
+            errdefer mod.destroyNamespace(new_namespace_index);
+
+            const union_ty = try ip.getUnionType(gpa, .{
+                .decl = new_decl_index,
+                .namespace = new_namespace_index,
+                .enum_tag_ty = enum_tag_ty,
+                .fields_len = fields_len,
+                .zir_index = inst,
+                .flags = .{
+                    .layout = layout,
+                    .status = .have_field_types,
+                    .runtime_tag = if (!tag_type_val.isNull(mod))
+                        .tagged
+                    else if (layout != .Auto)
+                        .none
+                    else switch (block.wantSafety()) {
+                        true => .safety,
+                        false => .none,
+                    },
+                    .any_aligned_fields = any_aligned_fields,
+                    .requires_comptime = .unknown,
+                    .assumed_runtime_bits = false,
+                },
+                .field_types = union_fields.items(.type),
+                .field_aligns = if (any_aligned_fields) union_fields.items(.alignment) else &.{},
+            });
+
+            new_decl.ty = Type.type;
+            new_decl.val = union_ty.toValue();
+            new_namespace.ty = union_ty.toType();
+
             const decl_val = sema.analyzeDeclVal(block, src, new_decl_index);
             try mod.finalizeAnonDecl(new_decl_index);
             return decl_val;
@@ -23341,7 +23356,7 @@ fn zirFieldParentPtr(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileEr
             if (mod.typeToStruct(parent_ty)) |struct_obj| {
                 break :blk struct_obj.fields.values()[field_index].abi_align;
             } else if (mod.typeToUnion(parent_ty)) |union_obj| {
-                break :blk union_obj.fields.values()[field_index].abi_align;
+                break :blk union_obj.fieldAlign(ip, field_index);
             } else {
                 break :blk .none;
             }
@@ -24683,18 +24698,28 @@ fn validateVarType(
     is_extern: bool,
 ) CompileError!void {
     const mod = sema.mod;
-    if (is_extern and !try sema.validateExternType(var_ty, .other)) {
-        const msg = msg: {
-            const msg = try sema.errMsg(block, src, "extern variable cannot have type '{}'", .{var_ty.fmt(mod)});
-            errdefer msg.destroy(sema.gpa);
-            const src_decl = mod.declPtr(block.src_decl);
-            try sema.explainWhyTypeIsNotExtern(msg, src.toSrcLoc(src_decl, mod), var_ty, .other);
-            break :msg msg;
-        };
-        return sema.failWithOwnedErrorMsg(msg);
+    if (is_extern) {
+        if (!try sema.validateExternType(var_ty, .other)) {
+            const msg = msg: {
+                const msg = try sema.errMsg(block, src, "extern variable cannot have type '{}'", .{var_ty.fmt(mod)});
+                errdefer msg.destroy(sema.gpa);
+                const src_decl = mod.declPtr(block.src_decl);
+                try sema.explainWhyTypeIsNotExtern(msg, src.toSrcLoc(src_decl, mod), var_ty, .other);
+                break :msg msg;
+            };
+            return sema.failWithOwnedErrorMsg(msg);
+        }
+    } else {
+        if (var_ty.zigTypeTag(mod) == .Opaque) {
+            return sema.fail(
+                block,
+                src,
+                "non-extern variable with opaque type '{}'",
+                .{var_ty.fmt(mod)},
+            );
+        }
     }
 
-    if (is_extern and var_ty.zigTypeTag(mod) == .Opaque) return;
     if (!try sema.typeRequiresComptime(var_ty)) return;
 
     const msg = msg: {
@@ -24735,6 +24760,7 @@ fn explainWhyTypeIsComptimeInner(
     type_set: *TypeSet,
 ) CompileError!void {
     const mod = sema.mod;
+    const ip = &mod.intern_pool;
     switch (ty.zigTypeTag(mod)) {
         .Bool,
         .Int,
@@ -24820,15 +24846,16 @@ fn explainWhyTypeIsComptimeInner(
             if ((try type_set.getOrPut(sema.gpa, ty.toIntern())).found_existing) return;
 
             if (mod.typeToUnion(ty)) |union_obj| {
-                for (union_obj.fields.values(), 0..) |field, i| {
-                    const field_src_loc = mod.fieldSrcLoc(union_obj.owner_decl, .{
+                for (0..union_obj.field_types.len) |i| {
+                    const field_ty = union_obj.field_types.get(ip)[i].toType();
+                    const field_src_loc = mod.fieldSrcLoc(union_obj.decl, .{
                         .index = i,
                         .range = .type,
                     });
 
-                    if (try sema.typeRequiresComptime(field.ty)) {
+                    if (try sema.typeRequiresComptime(field_ty)) {
                         try mod.errNoteNonLazy(field_src_loc, msg, "union requires comptime because of this field", .{});
-                        try sema.explainWhyTypeIsComptimeInner(msg, field_src_loc, field.ty, type_set);
+                        try sema.explainWhyTypeIsComptimeInner(msg, field_src_loc, field_ty, type_set);
                     }
                 }
             }
@@ -25886,12 +25913,11 @@ fn fieldCallBind(
             },
             .Union => {
                 try sema.resolveTypeFields(concrete_ty);
-                const fields = concrete_ty.unionFields(mod);
-                const field_index_usize = fields.getIndex(field_name) orelse break :find_field;
-                const field_index = @as(u32, @intCast(field_index_usize));
-                const field = fields.values()[field_index];
+                const union_obj = mod.typeToUnion(concrete_ty).?;
+                const field_index = union_obj.nameIndex(ip, field_name) orelse break :find_field;
+                const field_ty = union_obj.field_types.get(ip)[field_index].toType();
 
-                return sema.finishFieldCallBind(block, src, ptr_ty, field.ty, field_index, object_ptr);
+                return sema.finishFieldCallBind(block, src, ptr_ty, field_ty, field_index, object_ptr);
             },
             .Type => {
                 const namespace = try sema.analyzeLoad(block, src, object_ptr, src);
@@ -26378,24 +26404,24 @@ fn unionFieldPtr(
     try sema.resolveTypeFields(union_ty);
     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 field_ty = union_obj.field_types.get(ip)[field_index].toType();
     const ptr_field_ty = try mod.ptrType(.{
-        .child = field.ty.toIntern(),
+        .child = field_ty.toIntern(),
         .flags = .{
             .is_const = union_ptr_info.flags.is_const,
             .is_volatile = union_ptr_info.flags.is_volatile,
             .address_space = union_ptr_info.flags.address_space,
-            .alignment = if (union_obj.layout == .Auto) blk: {
+            .alignment = if (union_obj.getLayout(ip) == .Auto) blk: {
                 const union_align = union_ptr_info.flags.alignment.toByteUnitsOptional() orelse try sema.typeAbiAlignment(union_ty);
-                const field_align = try sema.unionFieldAlignment(field);
+                const field_align = try sema.unionFieldAlignment(union_obj, field_index);
                 break :blk InternPool.Alignment.fromByteUnits(@min(union_align, field_align));
             } else union_ptr_info.flags.alignment,
         },
         .packed_offset = union_ptr_info.packed_offset,
     });
-    const enum_field_index = @as(u32, @intCast(union_obj.tag_ty.enumFieldIndex(field_name, mod).?));
+    const enum_field_index: u32 = @intCast(union_obj.enum_tag_ty.toType().enumFieldIndex(field_name, mod).?);
 
-    if (initializing and field.ty.zigTypeTag(mod) == .NoReturn) {
+    if (initializing and field_ty.zigTypeTag(mod) == .NoReturn) {
         const msg = msg: {
             const msg = try sema.errMsg(block, src, "cannot initialize 'noreturn' field of union", .{});
             errdefer msg.destroy(sema.gpa);
@@ -26410,7 +26436,7 @@ fn unionFieldPtr(
     }
 
     if (try sema.resolveDefinedValue(block, src, union_ptr)) |union_ptr_val| ct: {
-        switch (union_obj.layout) {
+        switch (union_obj.getLayout(ip)) {
             .Auto => if (!initializing) {
                 const union_val = (try sema.pointerDeref(block, src, union_ptr_val, union_ptr_ty)) orelse
                     break :ct;
@@ -26418,12 +26444,12 @@ fn unionFieldPtr(
                     return sema.failWithUseOfUndef(block, src);
                 }
                 const un = ip.indexToKey(union_val.toIntern()).un;
-                const field_tag = try mod.enumValueFieldIndex(union_obj.tag_ty, enum_field_index);
+                const field_tag = try mod.enumValueFieldIndex(union_obj.enum_tag_ty.toType(), enum_field_index);
                 const tag_matches = un.tag == field_tag.toIntern();
                 if (!tag_matches) {
                     const msg = msg: {
-                        const active_index = union_obj.tag_ty.enumTagFieldIndex(un.tag.toValue(), mod).?;
-                        const active_field_name = union_obj.tag_ty.enumFieldName(active_index, mod);
+                        const active_index = union_obj.enum_tag_ty.toType().enumTagFieldIndex(un.tag.toValue(), mod).?;
+                        const active_field_name = union_obj.enum_tag_ty.toType().enumFieldName(active_index, mod);
                         const msg = try sema.errMsg(block, src, "access of union field '{}' while field '{}' is active", .{
                             field_name.fmt(ip),
                             active_field_name.fmt(ip),
@@ -26447,17 +26473,17 @@ fn unionFieldPtr(
     }
 
     try sema.requireRuntimeBlock(block, src, null);
-    if (!initializing and union_obj.layout == .Auto and block.wantSafety() and
-        union_ty.unionTagTypeSafety(mod) != null and union_obj.fields.count() > 1)
+    if (!initializing and union_obj.getLayout(ip) == .Auto and block.wantSafety() and
+        union_ty.unionTagTypeSafety(mod) != null and union_obj.field_names.len > 1)
     {
-        const wanted_tag_val = try mod.enumValueFieldIndex(union_obj.tag_ty, enum_field_index);
+        const wanted_tag_val = try mod.enumValueFieldIndex(union_obj.enum_tag_ty.toType(), enum_field_index);
         const wanted_tag = Air.internedToRef(wanted_tag_val.toIntern());
         // TODO would it be better if get_union_tag supported pointers to unions?
         const union_val = try block.addTyOp(.load, union_ty, union_ptr);
-        const active_tag = try block.addTyOp(.get_union_tag, union_obj.tag_ty, union_val);
+        const active_tag = try block.addTyOp(.get_union_tag, union_obj.enum_tag_ty.toType(), union_val);
         try sema.panicInactiveUnionField(block, src, active_tag, wanted_tag);
     }
-    if (field.ty.zigTypeTag(mod) == .NoReturn) {
+    if (field_ty.zigTypeTag(mod) == .NoReturn) {
         _ = try block.addNoOp(.unreach);
         return Air.Inst.Ref.unreachable_value;
     }
@@ -26480,23 +26506,23 @@ fn unionFieldVal(
     try sema.resolveTypeFields(union_ty);
     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 = @as(u32, @intCast(union_obj.tag_ty.enumFieldIndex(field_name, mod).?));
+    const field_ty = union_obj.field_types.get(ip)[field_index].toType();
+    const enum_field_index: u32 = @intCast(union_obj.enum_tag_ty.toType().enumFieldIndex(field_name, mod).?);
 
     if (try sema.resolveMaybeUndefVal(union_byval)) |union_val| {
-        if (union_val.isUndef(mod)) return mod.undefRef(field.ty);
+        if (union_val.isUndef(mod)) return mod.undefRef(field_ty);
 
         const un = ip.indexToKey(union_val.toIntern()).un;
-        const field_tag = try mod.enumValueFieldIndex(union_obj.tag_ty, enum_field_index);
+        const field_tag = try mod.enumValueFieldIndex(union_obj.enum_tag_ty.toType(), enum_field_index);
         const tag_matches = un.tag == field_tag.toIntern();
-        switch (union_obj.layout) {
+        switch (union_obj.getLayout(ip)) {
             .Auto => {
                 if (tag_matches) {
                     return Air.internedToRef(un.val);
                 } else {
                     const msg = msg: {
-                        const active_index = union_obj.tag_ty.enumTagFieldIndex(un.tag.toValue(), mod).?;
-                        const active_field_name = union_obj.tag_ty.enumFieldName(active_index, mod);
+                        const active_index = union_obj.enum_tag_ty.toType().enumTagFieldIndex(un.tag.toValue(), mod).?;
+                        const active_field_name = union_obj.enum_tag_ty.toType().enumFieldName(active_index, mod);
                         const msg = try sema.errMsg(block, src, "access of union field '{}' while field '{}' is active", .{
                             field_name.fmt(ip), active_field_name.fmt(ip),
                         });
@@ -26512,7 +26538,7 @@ fn unionFieldVal(
                     return Air.internedToRef(un.val);
                 } else {
                     const old_ty = union_ty.unionFieldType(un.tag.toValue(), mod);
-                    if (try sema.bitCastVal(block, src, un.val.toValue(), old_ty, field.ty, 0)) |new_val| {
+                    if (try sema.bitCastVal(block, src, un.val.toValue(), old_ty, field_ty, 0)) |new_val| {
                         return Air.internedToRef(new_val.toIntern());
                     }
                 }
@@ -26521,19 +26547,19 @@ fn unionFieldVal(
     }
 
     try sema.requireRuntimeBlock(block, src, null);
-    if (union_obj.layout == .Auto and block.wantSafety() and
-        union_ty.unionTagTypeSafety(mod) != null and union_obj.fields.count() > 1)
+    if (union_obj.getLayout(ip) == .Auto and block.wantSafety() and
+        union_ty.unionTagTypeSafety(mod) != null and union_obj.field_names.len > 1)
     {
-        const wanted_tag_val = try mod.enumValueFieldIndex(union_obj.tag_ty, enum_field_index);
+        const wanted_tag_val = try mod.enumValueFieldIndex(union_obj.enum_tag_ty.toType(), enum_field_index);
         const wanted_tag = Air.internedToRef(wanted_tag_val.toIntern());
-        const active_tag = try block.addTyOp(.get_union_tag, union_obj.tag_ty, union_byval);
+        const active_tag = try block.addTyOp(.get_union_tag, union_obj.enum_tag_ty.toType(), union_byval);
         try sema.panicInactiveUnionField(block, src, active_tag, wanted_tag);
     }
-    if (field.ty.zigTypeTag(mod) == .NoReturn) {
+    if (field_ty.zigTypeTag(mod) == .NoReturn) {
         _ = try block.addNoOp(.unreach);
         return Air.Inst.Ref.unreachable_value;
     }
-    return block.addStructFieldVal(union_byval, field_index, field.ty);
+    return block.addStructFieldVal(union_byval, field_index, field_ty);
 }
 
 fn elemPtr(
@@ -30048,14 +30074,14 @@ fn coerceEnumToUnion(
         };
 
         const union_obj = mod.typeToUnion(union_ty).?;
-        const field = union_obj.fields.values()[field_index];
-        try sema.resolveTypeFields(field.ty);
-        if (field.ty.zigTypeTag(mod) == .NoReturn) {
+        const field_ty = union_obj.field_types.get(ip)[field_index].toType();
+        try sema.resolveTypeFields(field_ty);
+        if (field_ty.zigTypeTag(mod) == .NoReturn) {
             const msg = msg: {
                 const msg = try sema.errMsg(block, inst_src, "cannot initialize 'noreturn' field of union", .{});
                 errdefer msg.destroy(sema.gpa);
 
-                const field_name = union_obj.fields.keys()[field_index];
+                const field_name = union_obj.field_names.get(ip)[field_index];
                 try sema.addFieldErrNote(union_ty, field_index, msg, "field '{}' declared here", .{
                     field_name.fmt(ip),
                 });
@@ -30064,12 +30090,12 @@ fn coerceEnumToUnion(
             };
             return sema.failWithOwnedErrorMsg(msg);
         }
-        const opv = (try sema.typeHasOnePossibleValue(field.ty)) orelse {
+        const opv = (try sema.typeHasOnePossibleValue(field_ty)) orelse {
             const msg = msg: {
-                const field_name = union_obj.fields.keys()[field_index];
+                const field_name = union_obj.field_names.get(ip)[field_index];
                 const msg = try sema.errMsg(block, inst_src, "coercion from enum '{}' to union '{}' must initialize '{}' field '{}'", .{
                     inst_ty.fmt(sema.mod),  union_ty.fmt(sema.mod),
-                    field.ty.fmt(sema.mod), field_name.fmt(ip),
+                    field_ty.fmt(sema.mod), field_name.fmt(ip),
                 });
                 errdefer msg.destroy(sema.gpa);
 
@@ -30104,8 +30130,8 @@ fn coerceEnumToUnion(
         var msg: ?*Module.ErrorMsg = null;
         errdefer if (msg) |some| some.destroy(sema.gpa);
 
-        for (union_obj.fields.values(), 0..) |field, i| {
-            if (field.ty.zigTypeTag(mod) == .NoReturn) {
+        for (union_obj.field_types.get(ip), 0..) |field_ty, field_index| {
+            if (field_ty.toType().zigTypeTag(mod) == .NoReturn) {
                 const err_msg = msg orelse try sema.errMsg(
                     block,
                     inst_src,
@@ -30114,7 +30140,7 @@ fn coerceEnumToUnion(
                 );
                 msg = err_msg;
 
-                try sema.addFieldErrNote(union_ty, i, err_msg, "'noreturn' field here", .{});
+                try sema.addFieldErrNote(union_ty, field_index, err_msg, "'noreturn' field here", .{});
             }
         }
         if (msg) |some| {
@@ -30138,11 +30164,9 @@ fn coerceEnumToUnion(
         );
         errdefer msg.destroy(sema.gpa);
 
-        var it = union_obj.fields.iterator();
-        var field_index: usize = 0;
-        while (it.next()) |field| : (field_index += 1) {
-            const field_name = field.key_ptr.*;
-            const field_ty = field.value_ptr.ty;
+        for (0..union_obj.field_names.len) |field_index| {
+            const field_name = union_obj.field_names.get(ip)[field_index];
+            const field_ty = union_obj.field_types.get(ip)[field_index].toType();
             if (!(try sema.typeHasRuntimeBits(field_ty))) continue;
             try sema.addFieldErrNote(union_ty, field_index, msg, "field '{}' has type '{}'", .{
                 field_name.fmt(ip),
@@ -30886,6 +30910,9 @@ fn analyzeLoad(
         .Pointer => ptr_ty.childType(mod),
         else => return sema.fail(block, ptr_src, "expected pointer, found '{}'", .{ptr_ty.fmt(sema.mod)}),
     };
+    if (elem_ty.zigTypeTag(mod) == .Opaque) {
+        return sema.fail(block, ptr_src, "cannot load opaque type '{}'", .{elem_ty.fmt(mod)});
+    }
 
     if (try sema.typeHasOnePossibleValue(elem_ty)) |opv| {
         return Air.internedToRef(opv.toIntern());
@@ -33816,7 +33843,7 @@ fn resolveStructLayout(sema: *Sema, ty: Type) CompileError!void {
         }
 
         struct_obj.status = .have_layout;
-        _ = try sema.resolveTypeRequiresComptime(ty);
+        _ = try sema.typeRequiresComptime(ty);
 
         if (struct_obj.assumed_runtime_bits and !(try sema.typeHasRuntimeBits(ty))) {
             const msg = try Module.ErrorMsg.create(
@@ -34030,44 +34057,46 @@ fn checkMemOperand(sema: *Sema, block: *Block, src: LazySrcLoc, ty: Type) !void
 
 fn resolveUnionLayout(sema: *Sema, ty: Type) CompileError!void {
     const mod = sema.mod;
+    const ip = &mod.intern_pool;
     try sema.resolveTypeFields(ty);
     const union_obj = mod.typeToUnion(ty).?;
-    switch (union_obj.status) {
+    switch (union_obj.flagsPtr(ip).status) {
         .none, .have_field_types => {},
         .field_types_wip, .layout_wip => {
             const msg = try Module.ErrorMsg.create(
                 sema.gpa,
-                union_obj.srcLoc(sema.mod),
+                mod.declPtr(union_obj.decl).srcLoc(mod),
                 "union '{}' depends on itself",
-                .{ty.fmt(sema.mod)},
+                .{ty.fmt(mod)},
             );
             return sema.failWithOwnedErrorMsg(msg);
         },
         .have_layout, .fully_resolved_wip, .fully_resolved => return,
     }
-    const prev_status = union_obj.status;
-    errdefer if (union_obj.status == .layout_wip) {
-        union_obj.status = prev_status;
+    const prev_status = union_obj.flagsPtr(ip).status;
+    errdefer if (union_obj.flagsPtr(ip).status == .layout_wip) {
+        union_obj.flagsPtr(ip).status = prev_status;
     };
 
-    union_obj.status = .layout_wip;
-    for (union_obj.fields.values(), 0..) |field, i| {
-        sema.resolveTypeLayout(field.ty) catch |err| switch (err) {
+    union_obj.flagsPtr(ip).status = .layout_wip;
+    for (0..union_obj.field_types.len) |field_index| {
+        const field_ty = union_obj.field_types.get(ip)[field_index].toType();
+        sema.resolveTypeLayout(field_ty) catch |err| switch (err) {
             error.AnalysisFail => {
                 const msg = sema.err orelse return err;
-                try sema.addFieldErrNote(ty, i, msg, "while checking this field", .{});
+                try sema.addFieldErrNote(ty, field_index, msg, "while checking this field", .{});
                 return err;
             },
             else => return err,
         };
     }
-    union_obj.status = .have_layout;
-    _ = try sema.resolveTypeRequiresComptime(ty);
+    union_obj.flagsPtr(ip).status = .have_layout;
+    _ = try sema.typeRequiresComptime(ty);
 
-    if (union_obj.assumed_runtime_bits and !(try sema.typeHasRuntimeBits(ty))) {
+    if (union_obj.flagsPtr(ip).assumed_runtime_bits and !(try sema.typeHasRuntimeBits(ty))) {
         const msg = try Module.ErrorMsg.create(
             sema.gpa,
-            union_obj.srcLoc(sema.mod),
+            mod.declPtr(union_obj.decl).srcLoc(mod),
             "union layout depends on it having runtime bits",
             .{},
         );
@@ -34075,163 +34104,6 @@ fn resolveUnionLayout(sema: *Sema, ty: Type) CompileError!void {
     }
 }
 
-// In case of querying the ABI alignment of this struct, we will ask
-// for hasRuntimeBits() of each field, so we need "requires comptime"
-// to be known already before this function returns.
-pub fn resolveTypeRequiresComptime(sema: *Sema, ty: Type) CompileError!bool {
-    const mod = sema.mod;
-
-    return switch (ty.toIntern()) {
-        .empty_struct_type => false,
-        else => switch (mod.intern_pool.indexToKey(ty.toIntern())) {
-            .int_type => false,
-            .ptr_type => |ptr_type| {
-                const child_ty = ptr_type.child.toType();
-                if (child_ty.zigTypeTag(mod) == .Fn) {
-                    return mod.typeToFunc(child_ty).?.is_generic;
-                } else {
-                    return sema.resolveTypeRequiresComptime(child_ty);
-                }
-            },
-            .anyframe_type => |child| {
-                if (child == .none) return false;
-                return sema.resolveTypeRequiresComptime(child.toType());
-            },
-            .array_type => |array_type| return sema.resolveTypeRequiresComptime(array_type.child.toType()),
-            .vector_type => |vector_type| return sema.resolveTypeRequiresComptime(vector_type.child.toType()),
-            .opt_type => |child| return sema.resolveTypeRequiresComptime(child.toType()),
-            .error_union_type => |error_union_type| return sema.resolveTypeRequiresComptime(error_union_type.payload_type.toType()),
-            .error_set_type, .inferred_error_set_type => false,
-
-            .func_type => true,
-
-            .simple_type => |t| switch (t) {
-                .f16,
-                .f32,
-                .f64,
-                .f80,
-                .f128,
-                .usize,
-                .isize,
-                .c_char,
-                .c_short,
-                .c_ushort,
-                .c_int,
-                .c_uint,
-                .c_long,
-                .c_ulong,
-                .c_longlong,
-                .c_ulonglong,
-                .c_longdouble,
-                .anyopaque,
-                .bool,
-                .void,
-                .anyerror,
-                .adhoc_inferred_error_set,
-                .noreturn,
-                .generic_poison,
-                .atomic_order,
-                .atomic_rmw_op,
-                .calling_convention,
-                .address_space,
-                .float_mode,
-                .reduce_op,
-                .call_modifier,
-                .prefetch_options,
-                .export_options,
-                .extern_options,
-                => false,
-
-                .type,
-                .comptime_int,
-                .comptime_float,
-                .null,
-                .undefined,
-                .enum_literal,
-                .type_info,
-                => true,
-            },
-            .struct_type => |struct_type| {
-                const struct_obj = mod.structPtrUnwrap(struct_type.index) orelse return false;
-                switch (struct_obj.requires_comptime) {
-                    .no, .wip => return false,
-                    .yes => return true,
-                    .unknown => {
-                        var requires_comptime = false;
-                        struct_obj.requires_comptime = .wip;
-                        for (struct_obj.fields.values()) |field| {
-                            if (try sema.resolveTypeRequiresComptime(field.ty)) requires_comptime = true;
-                        }
-                        if (requires_comptime) {
-                            struct_obj.requires_comptime = .yes;
-                        } else {
-                            struct_obj.requires_comptime = .no;
-                        }
-                        return requires_comptime;
-                    },
-                }
-            },
-
-            .anon_struct_type => |tuple| {
-                for (tuple.types, tuple.values) |field_ty, field_val| {
-                    const have_comptime_val = field_val != .none;
-                    if (!have_comptime_val and try sema.resolveTypeRequiresComptime(field_ty.toType())) {
-                        return true;
-                    }
-                }
-                return false;
-            },
-
-            .union_type => |union_type| {
-                const union_obj = mod.unionPtr(union_type.index);
-                switch (union_obj.requires_comptime) {
-                    .no, .wip => return false,
-                    .yes => return true,
-                    .unknown => {
-                        var requires_comptime = false;
-                        union_obj.requires_comptime = .wip;
-                        for (union_obj.fields.values()) |field| {
-                            if (try sema.resolveTypeRequiresComptime(field.ty)) requires_comptime = true;
-                        }
-                        if (requires_comptime) {
-                            union_obj.requires_comptime = .yes;
-                        } else {
-                            union_obj.requires_comptime = .no;
-                        }
-                        return requires_comptime;
-                    },
-                }
-            },
-
-            .opaque_type => false,
-
-            .enum_type => |enum_type| try sema.resolveTypeRequiresComptime(enum_type.tag_ty.toType()),
-
-            // values, not types
-            .undef,
-            .runtime_value,
-            .simple_value,
-            .variable,
-            .extern_func,
-            .func,
-            .int,
-            .err,
-            .error_union,
-            .enum_literal,
-            .enum_tag,
-            .empty_enum_value,
-            .float,
-            .ptr,
-            .opt,
-            .aggregate,
-            .un,
-            // memoization, not types
-            .memoized_call,
-            => unreachable,
-        },
-    };
-}
-
 /// Returns `error.AnalysisFail` if any of the types (recursively) failed to
 /// be resolved.
 pub fn resolveTypeFully(sema: *Sema, ty: Type) CompileError!void {
@@ -34306,11 +34178,12 @@ fn resolveStructFully(sema: *Sema, ty: Type) CompileError!void {
 
 fn resolveUnionFully(sema: *Sema, ty: Type) CompileError!void {
     try sema.resolveUnionLayout(ty);
+    try sema.resolveTypeFields(ty);
 
     const mod = sema.mod;
-    try sema.resolveTypeFields(ty);
+    const ip = &mod.intern_pool;
     const union_obj = mod.typeToUnion(ty).?;
-    switch (union_obj.status) {
+    switch (union_obj.flagsPtr(ip).status) {
         .none, .have_field_types, .field_types_wip, .layout_wip, .have_layout => {},
         .fully_resolved_wip, .fully_resolved => return,
     }
@@ -34319,14 +34192,15 @@ fn resolveUnionFully(sema: *Sema, ty: Type) CompileError!void {
         // After we have resolve union layout we have to go over the fields again to
         // make sure pointer fields get their child types resolved as well.
         // See also similar code for structs.
-        const prev_status = union_obj.status;
-        errdefer union_obj.status = prev_status;
+        const prev_status = union_obj.flagsPtr(ip).status;
+        errdefer union_obj.flagsPtr(ip).status = prev_status;
 
-        union_obj.status = .fully_resolved_wip;
-        for (union_obj.fields.values()) |field| {
-            try sema.resolveTypeFully(field.ty);
+        union_obj.flagsPtr(ip).status = .fully_resolved_wip;
+        for (0..union_obj.field_types.len) |field_index| {
+            const field_ty = union_obj.field_types.get(ip)[field_index].toType();
+            try sema.resolveTypeFully(field_ty);
         }
-        union_obj.status = .fully_resolved;
+        union_obj.flagsPtr(ip).status = .fully_resolved;
     }
 
     // And let's not forget comptime-only status.
@@ -34420,19 +34294,14 @@ pub fn resolveTypeFields(sema: *Sema, ty: Type) CompileError!void {
         else => switch (mod.intern_pool.items.items(.tag)[@intFromEnum(ty.toIntern())]) {
             .type_struct,
             .type_struct_ns,
-            .type_union_tagged,
-            .type_union_untagged,
-            .type_union_safety,
+            .type_union,
             .simple_type,
             => switch (mod.intern_pool.indexToKey(ty.toIntern())) {
                 .struct_type => |struct_type| {
                     const struct_obj = mod.structPtrUnwrap(struct_type.index) orelse return;
                     try sema.resolveTypeFieldsStruct(ty, struct_obj);
                 },
-                .union_type => |union_type| {
-                    const union_obj = mod.unionPtr(union_type.index);
-                    try sema.resolveTypeFieldsUnion(ty, union_obj);
-                },
+                .union_type => |union_type| try sema.resolveTypeFieldsUnion(ty, union_type),
                 .simple_type => |simple_type| try sema.resolveSimpleType(simple_type),
                 else => unreachable,
             },
@@ -34504,27 +34373,30 @@ fn resolveTypeFieldsStruct(
     try semaStructFields(sema.mod, struct_obj);
 }
 
-fn resolveTypeFieldsUnion(sema: *Sema, ty: Type, union_obj: *Module.Union) CompileError!void {
-    switch (sema.mod.declPtr(union_obj.owner_decl).analysis) {
+fn resolveTypeFieldsUnion(sema: *Sema, ty: Type, union_type: InternPool.Key.UnionType) CompileError!void {
+    const mod = sema.mod;
+    const ip = &mod.intern_pool;
+    const owner_decl = mod.declPtr(union_type.decl);
+    switch (owner_decl.analysis) {
         .file_failure,
         .dependency_failure,
         .sema_failure,
         .sema_failure_retryable,
         => {
             sema.owner_decl.analysis = .dependency_failure;
-            sema.owner_decl.generation = sema.mod.generation;
+            sema.owner_decl.generation = mod.generation;
             return error.AnalysisFail;
         },
         else => {},
     }
-    switch (union_obj.status) {
+    switch (union_type.flagsPtr(ip).status) {
         .none => {},
         .field_types_wip => {
             const msg = try Module.ErrorMsg.create(
                 sema.gpa,
-                union_obj.srcLoc(sema.mod),
+                owner_decl.srcLoc(mod),
                 "union '{}' depends on itself",
-                .{ty.fmt(sema.mod)},
+                .{ty.fmt(mod)},
             );
             return sema.failWithOwnedErrorMsg(msg);
         },
@@ -34536,10 +34408,10 @@ fn resolveTypeFieldsUnion(sema: *Sema, ty: Type, union_obj: *Module.Union) Compi
         => return,
     }
 
-    union_obj.status = .field_types_wip;
-    errdefer union_obj.status = .none;
-    try semaUnionFields(sema.mod, union_obj);
-    union_obj.status = .have_field_types;
+    union_type.flagsPtr(ip).status = .field_types_wip;
+    errdefer union_type.flagsPtr(ip).status = .none;
+    try semaUnionFields(mod, sema.arena, union_type);
+    union_type.flagsPtr(ip).status = .have_field_types;
 }
 
 /// Returns a normal error set corresponding to the fully populated inferred
@@ -35027,24 +34899,24 @@ fn semaStructFields(mod: *Module, struct_obj: *Module.Struct) CompileError!void
     struct_obj.have_field_inits = true;
 }
 
-fn semaUnionFields(mod: *Module, union_obj: *Module.Union) CompileError!void {
+fn semaUnionFields(mod: *Module, arena: Allocator, union_type: InternPool.Key.UnionType) CompileError!void {
     const tracy = trace(@src());
     defer tracy.end();
 
     const gpa = mod.gpa;
     const ip = &mod.intern_pool;
-    const decl_index = union_obj.owner_decl;
-    const zir = mod.namespacePtr(union_obj.namespace).file_scope.zir;
-    const extended = zir.instructions.items(.data)[union_obj.zir_index].extended;
+    const decl_index = union_type.decl;
+    const zir = mod.namespacePtr(union_type.namespace).file_scope.zir;
+    const extended = zir.instructions.items(.data)[union_type.zir_index].extended;
     assert(extended.opcode == .union_decl);
-    const small = @as(Zir.Inst.UnionDecl.Small, @bitCast(extended.small));
+    const small: Zir.Inst.UnionDecl.Small = @bitCast(extended.small);
     var extra_index: usize = extended.operand;
 
     const src = LazySrcLoc.nodeOffset(0);
     extra_index += @intFromBool(small.has_src_node);
 
     const tag_type_ref: Zir.Inst.Ref = if (small.has_tag_type) blk: {
-        const ty_ref = @as(Zir.Inst.Ref, @enumFromInt(zir.extra[extra_index]));
+        const ty_ref: Zir.Inst.Ref = @enumFromInt(zir.extra[extra_index]);
         extra_index += 1;
         break :blk ty_ref;
     } else .none;
@@ -35077,16 +34949,13 @@ fn semaUnionFields(mod: *Module, union_obj: *Module.Union) CompileError!void {
 
     const decl = mod.declPtr(decl_index);
 
-    var analysis_arena = std.heap.ArenaAllocator.init(gpa);
-    defer analysis_arena.deinit();
-
     var comptime_mutable_decls = std.ArrayList(Decl.Index).init(gpa);
     defer comptime_mutable_decls.deinit();
 
     var sema: Sema = .{
         .mod = mod,
         .gpa = gpa,
-        .arena = analysis_arena.allocator(),
+        .arena = arena,
         .code = zir,
         .owner_decl = decl,
         .owner_decl_index = decl_index,
@@ -35106,7 +34975,7 @@ fn semaUnionFields(mod: *Module, union_obj: *Module.Union) CompileError!void {
         .parent = null,
         .sema = &sema,
         .src_decl = decl_index,
-        .namespace = union_obj.namespace,
+        .namespace = union_type.namespace,
         .wip_capture_scope = wip_captures.scope,
         .instructions = .{},
         .inlining = null,
@@ -35124,8 +34993,6 @@ fn semaUnionFields(mod: *Module, union_obj: *Module.Union) CompileError!void {
         _ = try ct_decl.internValue(mod);
     }
 
-    try union_obj.fields.ensureTotalCapacity(mod.tmp_hack_arena.allocator(), fields_len);
-
     var int_tag_ty: Type = undefined;
     var enum_field_names: []InternPool.NullTerminatedString = &.{};
     var enum_field_vals: std.AutoArrayHashMapUnmanaged(InternPool.Index, void) = .{};
@@ -35159,10 +35026,10 @@ fn semaUnionFields(mod: *Module, union_obj: *Module.Union) CompileError!void {
             }
         } else {
             // The provided type is the enum tag type.
-            union_obj.tag_ty = provided_ty;
-            const enum_type = switch (ip.indexToKey(union_obj.tag_ty.toIntern())) {
+            union_type.tagTypePtr(ip).* = provided_ty.toIntern();
+            const enum_type = switch (ip.indexToKey(provided_ty.toIntern())) {
                 .enum_type => |x| x,
-                else => return sema.fail(&block_scope, tag_ty_src, "expected enum tag type, found '{}'", .{union_obj.tag_ty.fmt(mod)}),
+                else => return sema.fail(&block_scope, tag_ty_src, "expected enum tag type, found '{}'", .{provided_ty.fmt(mod)}),
             };
             // The fields of the union must match the enum exactly.
             // A flag per field is used to check for missing and extraneous fields.
@@ -35176,6 +35043,15 @@ fn semaUnionFields(mod: *Module, union_obj: *Module.Union) CompileError!void {
         enum_field_names = try sema.arena.alloc(InternPool.NullTerminatedString, fields_len);
     }
 
+    var field_types: std.ArrayListUnmanaged(InternPool.Index) = .{};
+    var field_aligns: std.ArrayListUnmanaged(InternPool.Alignment) = .{};
+    var field_name_table: std.AutoArrayHashMapUnmanaged(InternPool.NullTerminatedString, void) = .{};
+
+    try field_types.ensureTotalCapacityPrecise(sema.arena, fields_len);
+    if (small.any_aligned_fields)
+        try field_aligns.ensureTotalCapacityPrecise(sema.arena, fields_len);
+    try field_name_table.ensureTotalCapacity(sema.arena, fields_len);
+
     const bits_per_field = 4;
     const fields_per_u32 = 32 / bits_per_field;
     const bit_bags_count = std.math.divCeil(usize, fields_len, fields_per_u32) catch unreachable;
@@ -35206,19 +35082,19 @@ fn semaUnionFields(mod: *Module, union_obj: *Module.Union) CompileError!void {
         extra_index += 1;
 
         const field_type_ref: Zir.Inst.Ref = if (has_type) blk: {
-            const field_type_ref = @as(Zir.Inst.Ref, @enumFromInt(zir.extra[extra_index]));
+            const field_type_ref: Zir.Inst.Ref = @enumFromInt(zir.extra[extra_index]);
             extra_index += 1;
             break :blk field_type_ref;
         } else .none;
 
         const align_ref: Zir.Inst.Ref = if (has_align) blk: {
-            const align_ref = @as(Zir.Inst.Ref, @enumFromInt(zir.extra[extra_index]));
+            const align_ref: Zir.Inst.Ref = @enumFromInt(zir.extra[extra_index]);
             extra_index += 1;
             break :blk align_ref;
         } else .none;
 
         const tag_ref: Air.Inst.Ref = if (has_tag) blk: {
-            const tag_ref = @as(Zir.Inst.Ref, @enumFromInt(zir.extra[extra_index]));
+            const tag_ref: Zir.Inst.Ref = @enumFromInt(zir.extra[extra_index]);
             extra_index += 1;
             break :blk try sema.resolveInst(tag_ref);
         } else .none;
@@ -35227,7 +35103,7 @@ fn semaUnionFields(mod: *Module, union_obj: *Module.Union) CompileError!void {
             const enum_tag_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 = mod.fieldSrcLoc(union_obj.owner_decl, .{
+                        const val_src = mod.fieldSrcLoc(union_type.decl, .{
                             .index = field_i,
                             .range = .value,
                         }).lazy;
@@ -35250,8 +35126,8 @@ fn semaUnionFields(mod: *Module, union_obj: *Module.Union) CompileError!void {
             };
             const gop = enum_field_vals.getOrPutAssumeCapacity(enum_tag_val.toIntern());
             if (gop.found_existing) {
-                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 field_src = mod.fieldSrcLoc(union_type.decl, .{ .index = field_i }).lazy;
+                const other_field_src = mod.fieldSrcLoc(union_type.decl, .{ .index = gop.index }).lazy;
                 const msg = msg: {
                     const msg = try sema.errMsg(&block_scope, field_src, "enum tag value {} already taken", .{enum_tag_val.fmtValue(int_tag_ty, mod)});
                     errdefer msg.destroy(gpa);
@@ -35275,7 +35151,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 = mod.fieldSrcLoc(union_obj.owner_decl, .{
+                    const ty_src = mod.fieldSrcLoc(union_type.decl, .{
                         .index = field_i,
                         .range = .type,
                     }).lazy;
@@ -35289,17 +35165,16 @@ fn semaUnionFields(mod: *Module, union_obj: *Module.Union) CompileError!void {
             return error.GenericPoison;
         }
 
-        const gop = union_obj.fields.getOrPutAssumeCapacity(field_name);
+        const gop = field_name_table.getOrPutAssumeCapacity(field_name);
         if (gop.found_existing) {
             const msg = msg: {
-                const field_src = mod.fieldSrcLoc(union_obj.owner_decl, .{ .index = field_i }).lazy;
+                const field_src = mod.fieldSrcLoc(union_type.decl, .{ .index = field_i }).lazy;
                 const msg = try sema.errMsg(&block_scope, field_src, "duplicate union field: '{}'", .{
                     field_name.fmt(ip),
                 });
                 errdefer msg.destroy(gpa);
 
-                const prev_field_index = union_obj.fields.getIndex(field_name).?;
-                const prev_field_src = mod.fieldSrcLoc(union_obj.owner_decl, .{ .index = prev_field_index }).lazy;
+                const prev_field_src = mod.fieldSrcLoc(union_type.decl, .{ .index = gop.index }).lazy;
                 try 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;
@@ -35308,18 +35183,18 @@ fn semaUnionFields(mod: *Module, union_obj: *Module.Union) CompileError!void {
         }
 
         if (explicit_tags_seen.len > 0) {
-            const tag_info = ip.indexToKey(union_obj.tag_ty.toIntern()).enum_type;
+            const tag_info = ip.indexToKey(union_type.tagTypePtr(ip).*).enum_type;
             const enum_index = tag_info.nameIndex(ip, field_name) orelse {
                 const msg = msg: {
-                    const ty_src = mod.fieldSrcLoc(union_obj.owner_decl, .{
+                    const ty_src = mod.fieldSrcLoc(union_type.decl, .{
                         .index = field_i,
                         .range = .type,
                     }).lazy;
                     const msg = try sema.errMsg(&block_scope, ty_src, "no field named '{}' in enum '{}'", .{
-                        field_name.fmt(ip), union_obj.tag_ty.fmt(mod),
+                        field_name.fmt(ip), union_type.tagTypePtr(ip).toType().fmt(mod),
                     });
                     errdefer msg.destroy(sema.gpa);
-                    try sema.addDeclaredHereNote(msg, union_obj.tag_ty);
+                    try sema.addDeclaredHereNote(msg, union_type.tagTypePtr(ip).toType());
                     break :msg msg;
                 };
                 return sema.failWithOwnedErrorMsg(msg);
@@ -35328,11 +35203,29 @@ fn semaUnionFields(mod: *Module, union_obj: *Module.Union) CompileError!void {
             // to create the enum type in the first place.
             assert(!explicit_tags_seen[enum_index]);
             explicit_tags_seen[enum_index] = true;
+
+            // Enforce the enum fields and the union fields being in the same order.
+            if (enum_index != field_i) {
+                const msg = msg: {
+                    const ty_src = mod.fieldSrcLoc(union_type.decl, .{
+                        .index = field_i,
+                        .range = .type,
+                    }).lazy;
+                    const enum_field_src = mod.fieldSrcLoc(tag_info.decl, .{ .index = enum_index }).lazy;
+                    const msg = try sema.errMsg(&block_scope, ty_src, "union field '{}' ordered differently than corresponding enum field", .{
+                        field_name.fmt(ip),
+                    });
+                    errdefer msg.destroy(sema.gpa);
+                    try sema.errNote(&block_scope, enum_field_src, msg, "enum field here", .{});
+                    break :msg msg;
+                };
+                return sema.failWithOwnedErrorMsg(msg);
+            }
         }
 
         if (field_ty.zigTypeTag(mod) == .Opaque) {
             const msg = msg: {
-                const ty_src = mod.fieldSrcLoc(union_obj.owner_decl, .{
+                const ty_src = mod.fieldSrcLoc(union_type.decl, .{
                     .index = field_i,
                     .range = .type,
                 }).lazy;
@@ -35344,9 +35237,12 @@ fn semaUnionFields(mod: *Module, union_obj: *Module.Union) CompileError!void {
             };
             return sema.failWithOwnedErrorMsg(msg);
         }
-        if (union_obj.layout == .Extern and !try sema.validateExternType(field_ty, .union_field)) {
+        const layout = union_type.getLayout(ip);
+        if (layout == .Extern and
+            !try sema.validateExternType(field_ty, .union_field))
+        {
             const msg = msg: {
-                const ty_src = mod.fieldSrcLoc(union_obj.owner_decl, .{
+                const ty_src = mod.fieldSrcLoc(union_type.decl, .{
                     .index = field_i,
                     .range = .type,
                 });
@@ -35359,9 +35255,9 @@ fn semaUnionFields(mod: *Module, union_obj: *Module.Union) CompileError!void {
                 break :msg msg;
             };
             return sema.failWithOwnedErrorMsg(msg);
-        } else if (union_obj.layout == .Packed and !(validatePackedType(field_ty, mod))) {
+        } else if (layout == .Packed and !validatePackedType(field_ty, mod)) {
             const msg = msg: {
-                const ty_src = mod.fieldSrcLoc(union_obj.owner_decl, .{
+                const ty_src = mod.fieldSrcLoc(union_type.decl, .{
                     .index = field_i,
                     .range = .type,
                 });
@@ -35376,51 +35272,55 @@ fn semaUnionFields(mod: *Module, union_obj: *Module.Union) CompileError!void {
             return sema.failWithOwnedErrorMsg(msg);
         }
 
-        gop.value_ptr.* = .{
-            .ty = field_ty,
-            .abi_align = .none,
-        };
+        field_types.appendAssumeCapacity(field_ty.toIntern());
 
-        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 = mod.fieldSrcLoc(union_obj.owner_decl, .{
-                        .index = field_i,
-                        .range = .alignment,
-                    }).lazy;
-                    _ = try sema.resolveAlign(&block_scope, align_src, align_ref);
-                    unreachable;
-                },
-                else => |e| return e,
-            };
+        if (small.any_aligned_fields) {
+            field_aligns.appendAssumeCapacity(if (align_ref != .none)
+                sema.resolveAlign(&block_scope, .unneeded, align_ref) catch |err| switch (err) {
+                    error.NeededSourceLocation => {
+                        const align_src = mod.fieldSrcLoc(union_type.decl, .{
+                            .index = field_i,
+                            .range = .alignment,
+                        }).lazy;
+                        _ = try sema.resolveAlign(&block_scope, align_src, align_ref);
+                        unreachable;
+                    },
+                    else => |e| return e,
+                }
+            else
+                .none);
         } else {
-            gop.value_ptr.abi_align = .none;
+            assert(align_ref == .none);
         }
     }
 
+    union_type.setFieldTypes(ip, field_types.items);
+    union_type.setFieldAligns(ip, field_aligns.items);
+
     if (explicit_tags_seen.len > 0) {
-        const tag_info = ip.indexToKey(union_obj.tag_ty.toIntern()).enum_type;
+        const tag_info = ip.indexToKey(union_type.tagTypePtr(ip).*).enum_type;
         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 (tag_info.names.get(ip), 0..) |field_name, field_index| {
                     if (explicit_tags_seen[field_index]) continue;
-                    try sema.addFieldErrNote(enum_ty, field_index, msg, "field '{}' missing, declared here", .{
+                    try sema.addFieldErrNote(union_type.tagTypePtr(ip).toType(), field_index, msg, "field '{}' missing, declared here", .{
                         field_name.fmt(ip),
                     });
                 }
-                try sema.addDeclaredHereNote(msg, union_obj.tag_ty);
+                try sema.addDeclaredHereNote(msg, union_type.tagTypePtr(ip).toType());
                 break :msg msg;
             };
             return sema.failWithOwnedErrorMsg(msg);
         }
     } else if (enum_field_vals.count() > 0) {
-        union_obj.tag_ty = try sema.generateUnionTagTypeNumbered(&block_scope, enum_field_names, enum_field_vals.keys(), union_obj);
+        const enum_ty = try sema.generateUnionTagTypeNumbered(&block_scope, enum_field_names, enum_field_vals.keys(), mod.declPtr(union_type.decl));
+        union_type.tagTypePtr(ip).* = enum_ty;
     } else {
-        union_obj.tag_ty = try sema.generateUnionTagTypeSimple(&block_scope, enum_field_names, union_obj);
+        const enum_ty = try sema.generateUnionTagTypeSimple(&block_scope, enum_field_names, union_type.decl.toOptional());
+        union_type.tagTypePtr(ip).* = enum_ty;
     }
 }
 
@@ -35434,8 +35334,8 @@ fn generateUnionTagTypeNumbered(
     block: *Block,
     enum_field_names: []const InternPool.NullTerminatedString,
     enum_field_vals: []const InternPool.Index,
-    union_obj: *Module.Union,
-) !Type {
+    decl: *Module.Decl,
+) !InternPool.Index {
     const mod = sema.mod;
     const gpa = sema.gpa;
     const ip = &mod.intern_pool;
@@ -35443,7 +35343,7 @@ fn generateUnionTagTypeNumbered(
     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);
-    const fqn = try union_obj.getFullyQualifiedName(mod);
+    const fqn = try decl.getFullyQualifiedName(mod);
     const name = try ip.getOrPutStringFmt(gpa, "@typeInfo({}).Union.tag_type.?", .{fqn.fmt(ip)});
     try mod.initNewAnonDecl(new_decl_index, src_decl.src_line, .{
         .ty = Type.noreturn,
@@ -35472,30 +35372,30 @@ fn generateUnionTagTypeNumbered(
     new_decl.val = enum_ty.toValue();
 
     try mod.finalizeAnonDecl(new_decl_index);
-    return enum_ty.toType();
+    return enum_ty;
 }
 
 fn generateUnionTagTypeSimple(
     sema: *Sema,
     block: *Block,
     enum_field_names: []const InternPool.NullTerminatedString,
-    maybe_union_obj: ?*Module.Union,
-) !Type {
+    maybe_decl_index: Module.Decl.OptionalIndex,
+) !InternPool.Index {
     const mod = sema.mod;
     const ip = &mod.intern_pool;
     const gpa = sema.gpa;
 
     const new_decl_index = new_decl_index: {
-        const union_obj = maybe_union_obj orelse {
+        const decl_index = maybe_decl_index.unwrap() orelse {
             break :new_decl_index try mod.createAnonymousDecl(block, .{
                 .ty = Type.noreturn,
                 .val = Value.@"unreachable",
             });
         };
+        const fqn = try mod.declPtr(decl_index).getFullyQualifiedName(mod);
         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);
-        const fqn = try union_obj.getFullyQualifiedName(mod);
         const name = try ip.getOrPutStringFmt(gpa, "@typeInfo({}).Union.tag_type.?", .{fqn.fmt(ip)});
         try mod.initNewAnonDecl(new_decl_index, src_decl.src_line, .{
             .ty = Type.noreturn,
@@ -35524,7 +35424,7 @@ fn generateUnionTagTypeSimple(
     new_decl.val = enum_ty.toValue();
 
     try mod.finalizeAnonDecl(new_decl_index);
-    return enum_ty.toType();
+    return enum_ty;
 }
 
 fn getBuiltin(sema: *Sema, name: []const u8) CompileError!Air.Inst.Ref {
@@ -35787,9 +35687,7 @@ pub fn typeHasOnePossibleValue(sema: *Sema, ty: Type) CompileError!?Value {
             .type_struct_ns,
             .type_struct_anon,
             .type_tuple_anon,
-            .type_union_tagged,
-            .type_union_untagged,
-            .type_union_safety,
+            .type_union,
             => switch (ip.indexToKey(ty.toIntern())) {
                 inline .array_type, .vector_type => |seq_type, seq_tag| {
                     const has_sentinel = seq_tag == .array_type and seq_type.sentinel != .none;
@@ -35816,12 +35714,12 @@ pub fn typeHasOnePossibleValue(sema: *Sema, ty: Type) CompileError!?Value {
                                 field_val.* = field.default_val;
                                 continue;
                             }
-                            if (field.ty.eql(ty, sema.mod)) {
+                            if (field.ty.eql(ty, mod)) {
                                 const msg = try Module.ErrorMsg.create(
                                     sema.gpa,
-                                    s.srcLoc(sema.mod),
+                                    s.srcLoc(mod),
                                     "struct '{}' depends on itself",
-                                    .{ty.fmt(sema.mod)},
+                                    .{ty.fmt(mod)},
                                 );
                                 try sema.addFieldErrNote(ty, i, msg, "while checking this field", .{});
                                 return sema.failWithOwnedErrorMsg(msg);
@@ -35862,26 +35760,25 @@ pub fn typeHasOnePossibleValue(sema: *Sema, ty: Type) CompileError!?Value {
 
                 .union_type => |union_type| {
                     try sema.resolveTypeFields(ty);
-                    const union_obj = mod.unionPtr(union_type.index);
-                    const tag_val = (try sema.typeHasOnePossibleValue(union_obj.tag_ty)) orelse
+                    const union_obj = ip.loadUnionType(union_type);
+                    const tag_val = (try sema.typeHasOnePossibleValue(union_obj.enum_tag_ty.toType())) orelse
                         return null;
-                    const fields = union_obj.fields.values();
-                    if (fields.len == 0) {
+                    if (union_obj.field_types.len == 0) {
                         const only = try mod.intern(.{ .empty_enum_value = ty.toIntern() });
                         return only.toValue();
                     }
-                    const only_field = fields[0];
-                    if (only_field.ty.eql(ty, sema.mod)) {
+                    const only_field_ty = union_obj.field_types.get(ip)[0].toType();
+                    if (only_field_ty.eql(ty, mod)) {
                         const msg = try Module.ErrorMsg.create(
                             sema.gpa,
-                            union_obj.srcLoc(sema.mod),
+                            mod.declPtr(union_obj.decl).srcLoc(mod),
                             "union '{}' depends on itself",
-                            .{ty.fmt(sema.mod)},
+                            .{ty.fmt(mod)},
                         );
                         try sema.addFieldErrNote(ty, 0, msg, "while checking this field", .{});
                         return sema.failWithOwnedErrorMsg(msg);
                     }
-                    const val_val = (try sema.typeHasOnePossibleValue(only_field.ty)) orelse
+                    const val_val = (try sema.typeHasOnePossibleValue(only_field_ty)) orelse
                         return null;
                     const only = try mod.intern(.{ .un = .{
                         .ty = ty.toIntern(),
@@ -36225,10 +36122,11 @@ fn typePtrOrOptionalPtrTy(sema: *Sema, ty: Type) !?Type {
 /// elsewhere in value.zig
 pub fn typeRequiresComptime(sema: *Sema, ty: Type) CompileError!bool {
     const mod = sema.mod;
+    const ip = &mod.intern_pool;
     return switch (ty.toIntern()) {
         .empty_struct_type => false,
 
-        else => switch (mod.intern_pool.indexToKey(ty.toIntern())) {
+        else => switch (ip.indexToKey(ty.toIntern())) {
             .int_type => return false,
             .ptr_type => |ptr_type| {
                 const child_ty = ptr_type.child.toType();
@@ -36254,7 +36152,7 @@ pub fn typeRequiresComptime(sema: *Sema, ty: Type) CompileError!bool {
 
             .func_type => true,
 
-            .simple_type => |t| return switch (t) {
+            .simple_type => |t| switch (t) {
                 .f16,
                 .f32,
                 .f64,
@@ -36272,9 +36170,11 @@ pub fn typeRequiresComptime(sema: *Sema, ty: Type) CompileError!bool {
                 .c_longlong,
                 .c_ulonglong,
                 .c_longdouble,
+                .anyopaque,
                 .bool,
                 .void,
                 .anyerror,
+                .adhoc_inferred_error_set,
                 .noreturn,
                 .generic_poison,
                 .atomic_order,
@@ -36287,10 +36187,8 @@ pub fn typeRequiresComptime(sema: *Sema, ty: Type) CompileError!bool {
                 .prefetch_options,
                 .export_options,
                 .extern_options,
-                .adhoc_inferred_error_set,
                 => false,
 
-                .anyopaque,
                 .type,
                 .comptime_int,
                 .comptime_float,
@@ -36335,30 +36233,31 @@ pub fn typeRequiresComptime(sema: *Sema, ty: Type) CompileError!bool {
             },
 
             .union_type => |union_type| {
-                const union_obj = mod.unionPtr(union_type.index);
-                switch (union_obj.requires_comptime) {
+                switch (union_type.flagsPtr(ip).requires_comptime) {
                     .no, .wip => return false,
                     .yes => return true,
                     .unknown => {
-                        if (union_obj.status == .field_types_wip)
+                        if (union_type.flagsPtr(ip).status == .field_types_wip)
                             return false;
 
-                        try sema.resolveTypeFieldsUnion(ty, union_obj);
+                        try sema.resolveTypeFieldsUnion(ty, union_type);
+                        const union_obj = ip.loadUnionType(union_type);
 
-                        union_obj.requires_comptime = .wip;
-                        for (union_obj.fields.values()) |field| {
-                            if (try sema.typeRequiresComptime(field.ty)) {
-                                union_obj.requires_comptime = .yes;
+                        union_obj.flagsPtr(ip).requires_comptime = .wip;
+                        for (0..union_obj.field_types.len) |field_index| {
+                            const field_ty = union_obj.field_types.get(ip)[field_index];
+                            if (try sema.typeRequiresComptime(field_ty.toType())) {
+                                union_obj.flagsPtr(ip).requires_comptime = .yes;
                                 return true;
                             }
                         }
-                        union_obj.requires_comptime = .no;
+                        union_obj.flagsPtr(ip).requires_comptime = .no;
                         return false;
                     },
                 }
             },
 
-            .opaque_type => true,
+            .opaque_type => false,
             .enum_type => |enum_type| try sema.typeRequiresComptime(enum_type.tag_ty.toType()),
 
             // values, not types
@@ -36404,12 +36303,15 @@ fn typeAbiAlignment(sema: *Sema, ty: Type) CompileError!u32 {
 }
 
 /// Not valid to call for packed unions.
-/// Keep implementation in sync with `Module.Union.Field.normalAlignment`.
-fn unionFieldAlignment(sema: *Sema, field: Module.Union.Field) !u32 {
-    return @as(u32, @intCast(if (field.ty.isNoReturn(sema.mod))
-        0
-    else
-        field.abi_align.toByteUnitsOptional() orelse try sema.typeAbiAlignment(field.ty)));
+/// Keep implementation in sync with `Module.unionFieldNormalAlignment`.
+/// TODO: this returns alignment in byte units should should be a u64
+fn unionFieldAlignment(sema: *Sema, u: InternPool.UnionType, field_index: u32) !u32 {
+    const mod = sema.mod;
+    const ip = &mod.intern_pool;
+    if (u.fieldAlign(ip, field_index).toByteUnitsOptional()) |a| return @intCast(a);
+    const field_ty = u.field_types.get(ip)[field_index].toType();
+    if (field_ty.isNoReturn(sema.mod)) return 0;
+    return @intCast(try sema.typeAbiAlignment(field_ty));
 }
 
 /// Keep implementation in sync with `Module.Struct.Field.alignment`.
@@ -36459,11 +36361,12 @@ fn unionFieldIndex(
     field_src: LazySrcLoc,
 ) !u32 {
     const mod = sema.mod;
+    const ip = &mod.intern_pool;
     try sema.resolveTypeFields(union_ty);
     const union_obj = mod.typeToUnion(union_ty).?;
-    const field_index_usize = union_obj.fields.getIndex(field_name) orelse
+    const field_index = union_obj.nameIndex(ip, field_name) orelse
         return sema.failWithBadUnionFieldAccess(block, union_obj, field_src, field_name);
-    return @as(u32, @intCast(field_index_usize));
+    return @intCast(field_index);
 }
 
 fn structFieldIndex(
src/type.zig
@@ -349,8 +349,7 @@ pub const Type = struct {
             },
 
             .union_type => |union_type| {
-                const union_obj = mod.unionPtr(union_type.index);
-                const decl = mod.declPtr(union_obj.owner_decl);
+                const decl = mod.declPtr(union_type.decl);
                 try decl.renderFullyQualifiedName(mod, writer);
             },
             .opaque_type => |opaque_type| {
@@ -462,10 +461,11 @@ pub const Type = struct {
         ignore_comptime_only: bool,
         strat: AbiAlignmentAdvancedStrat,
     ) RuntimeBitsError!bool {
+        const ip = &mod.intern_pool;
         return switch (ty.toIntern()) {
             // False because it is a comptime-only type.
             .empty_struct_type => false,
-            else => switch (mod.intern_pool.indexToKey(ty.toIntern())) {
+            else => switch (ip.indexToKey(ty.toIntern())) {
                 .int_type => |int_type| int_type.bits != 0,
                 .ptr_type => |ptr_type| {
                     // Pointers to zero-bit types still have a runtime address; however, pointers
@@ -595,29 +595,36 @@ pub const Type = struct {
                 },
 
                 .union_type => |union_type| {
-                    const union_obj = mod.unionPtr(union_type.index);
-                    switch (union_type.runtime_tag) {
+                    switch (union_type.flagsPtr(ip).runtime_tag) {
                         .none => {
-                            if (union_obj.status == .field_types_wip) {
+                            if (union_type.flagsPtr(ip).status == .field_types_wip) {
                                 // In this case, we guess that hasRuntimeBits() for this type is true,
                                 // and then later if our guess was incorrect, we emit a compile error.
-                                union_obj.assumed_runtime_bits = true;
+                                union_type.flagsPtr(ip).assumed_runtime_bits = true;
                                 return true;
                             }
                         },
                         .safety, .tagged => {
-                            if (try union_obj.tag_ty.hasRuntimeBitsAdvanced(mod, ignore_comptime_only, strat)) {
+                            const tag_ty = union_type.tagTypePtr(ip).*;
+                            // tag_ty will be `none` if this union's tag type is not resolved yet,
+                            // in which case we want control flow to continue down below.
+                            if (tag_ty != .none and
+                                try tag_ty.toType().hasRuntimeBitsAdvanced(mod, ignore_comptime_only, strat))
+                            {
                                 return true;
                             }
                         },
                     }
                     switch (strat) {
                         .sema => |sema| _ = try sema.resolveTypeFields(ty),
-                        .eager => assert(union_obj.haveFieldTypes()),
-                        .lazy => if (!union_obj.haveFieldTypes()) return error.NeedLazy,
+                        .eager => assert(union_type.flagsPtr(ip).status.haveFieldTypes()),
+                        .lazy => if (!union_type.flagsPtr(ip).status.haveFieldTypes())
+                            return error.NeedLazy,
                     }
-                    for (union_obj.fields.values()) |value| {
-                        if (try value.ty.hasRuntimeBitsAdvanced(mod, ignore_comptime_only, strat))
+                    const union_obj = ip.loadUnionType(union_type);
+                    for (0..union_obj.field_types.len) |field_index| {
+                        const field_ty = union_obj.field_types.get(ip)[field_index].toType();
+                        if (try field_ty.hasRuntimeBitsAdvanced(mod, ignore_comptime_only, strat))
                             return true;
                     } else {
                         return false;
@@ -656,7 +663,8 @@ pub const Type = struct {
     /// readFrom/writeToMemory are supported only for types with a well-
     /// defined memory layout
     pub fn hasWellDefinedLayout(ty: Type, mod: *Module) bool {
-        return switch (mod.intern_pool.indexToKey(ty.toIntern())) {
+        const ip = &mod.intern_pool;
+        return switch (ip.indexToKey(ty.toIntern())) {
             .int_type,
             .vector_type,
             => true,
@@ -728,8 +736,8 @@ pub const Type = struct {
                 };
                 return struct_obj.layout != .Auto;
             },
-            .union_type => |union_type| switch (union_type.runtime_tag) {
-                .none, .safety => mod.unionPtr(union_type.index).layout != .Auto,
+            .union_type => |union_type| switch (union_type.flagsPtr(ip).runtime_tag) {
+                .none, .safety => union_type.flagsPtr(ip).layout != .Auto,
                 .tagged => false,
             },
             .enum_type => |enum_type| switch (enum_type.tag_mode) {
@@ -867,6 +875,7 @@ pub const Type = struct {
         strat: AbiAlignmentAdvancedStrat,
     ) Module.CompileError!AbiAlignmentAdvanced {
         const target = mod.getTarget();
+        const ip = &mod.intern_pool;
 
         const opt_sema = switch (strat) {
             .sema => |sema| sema,
@@ -875,7 +884,7 @@ pub const Type = struct {
 
         switch (ty.toIntern()) {
             .empty_struct_type => return AbiAlignmentAdvanced{ .scalar = 0 },
-            else => switch (mod.intern_pool.indexToKey(ty.toIntern())) {
+            else => switch (ip.indexToKey(ty.toIntern())) {
                 .int_type => |int_type| {
                     if (int_type.bits == 0) return AbiAlignmentAdvanced{ .scalar = 0 };
                     return AbiAlignmentAdvanced{ .scalar = intAbiAlignment(int_type.bits, target) };
@@ -1066,8 +1075,65 @@ pub const Type = struct {
                 },
 
                 .union_type => |union_type| {
-                    const union_obj = mod.unionPtr(union_type.index);
-                    return abiAlignmentAdvancedUnion(ty, mod, strat, union_obj, union_type.hasTag());
+                    if (opt_sema) |sema| {
+                        if (union_type.flagsPtr(ip).status == .field_types_wip) {
+                            // We'll guess "pointer-aligned", if the union has an
+                            // underaligned pointer field then some allocations
+                            // might require explicit alignment.
+                            return AbiAlignmentAdvanced{ .scalar = @divExact(target.ptrBitWidth(), 8) };
+                        }
+                        _ = try sema.resolveTypeFields(ty);
+                    }
+                    if (!union_type.haveFieldTypes(ip)) switch (strat) {
+                        .eager => unreachable, // union layout not resolved
+                        .sema => unreachable, // handled above
+                        .lazy => return .{ .val = (try mod.intern(.{ .int = .{
+                            .ty = .comptime_int_type,
+                            .storage = .{ .lazy_align = ty.toIntern() },
+                        } })).toValue() },
+                    };
+                    const union_obj = ip.loadUnionType(union_type);
+                    if (union_obj.field_names.len == 0) {
+                        if (union_obj.hasTag(ip)) {
+                            return abiAlignmentAdvanced(union_obj.enum_tag_ty.toType(), mod, strat);
+                        } else {
+                            return AbiAlignmentAdvanced{
+                                .scalar = @intFromBool(union_obj.flagsPtr(ip).layout == .Extern),
+                            };
+                        }
+                    }
+
+                    var max_align: u32 = 0;
+                    if (union_obj.hasTag(ip)) max_align = union_obj.enum_tag_ty.toType().abiAlignment(mod);
+                    for (0..union_obj.field_names.len) |field_index| {
+                        const field_ty = union_obj.field_types.get(ip)[field_index].toType();
+                        const field_align = if (union_obj.field_aligns.len == 0)
+                            .none
+                        else
+                            union_obj.field_aligns.get(ip)[field_index];
+                        if (!(field_ty.hasRuntimeBitsAdvanced(mod, false, strat) catch |err| switch (err) {
+                            error.NeedLazy => return .{ .val = (try mod.intern(.{ .int = .{
+                                .ty = .comptime_int_type,
+                                .storage = .{ .lazy_align = ty.toIntern() },
+                            } })).toValue() },
+                            else => |e| return e,
+                        })) continue;
+
+                        const field_align_bytes: u32 = @intCast(field_align.toByteUnitsOptional() orelse
+                            switch (try field_ty.abiAlignmentAdvanced(mod, strat)) {
+                            .scalar => |a| a,
+                            .val => switch (strat) {
+                                .eager => unreachable, // struct layout not resolved
+                                .sema => unreachable, // handled above
+                                .lazy => return .{ .val = (try mod.intern(.{ .int = .{
+                                    .ty = .comptime_int_type,
+                                    .storage = .{ .lazy_align = ty.toIntern() },
+                                } })).toValue() },
+                            },
+                        });
+                        max_align = @max(max_align, field_align_bytes);
+                    }
+                    return AbiAlignmentAdvanced{ .scalar = max_align };
                 },
                 .opaque_type => return AbiAlignmentAdvanced{ .scalar = 1 },
                 .enum_type => |enum_type| return AbiAlignmentAdvanced{ .scalar = enum_type.tag_ty.toType().abiAlignment(mod) },
@@ -1177,71 +1243,6 @@ pub const Type = struct {
         }
     }
 
-    pub fn abiAlignmentAdvancedUnion(
-        ty: Type,
-        mod: *Module,
-        strat: AbiAlignmentAdvancedStrat,
-        union_obj: *Module.Union,
-        have_tag: bool,
-    ) Module.CompileError!AbiAlignmentAdvanced {
-        const opt_sema = switch (strat) {
-            .sema => |sema| sema,
-            else => null,
-        };
-        if (opt_sema) |sema| {
-            if (union_obj.status == .field_types_wip) {
-                // We'll guess "pointer-aligned", if the union has an
-                // underaligned pointer field then some allocations
-                // might require explicit alignment.
-                const target = mod.getTarget();
-                return AbiAlignmentAdvanced{ .scalar = @divExact(target.ptrBitWidth(), 8) };
-            }
-            _ = try sema.resolveTypeFields(ty);
-        }
-        if (!union_obj.haveFieldTypes()) switch (strat) {
-            .eager => unreachable, // union layout not resolved
-            .sema => unreachable, // handled above
-            .lazy => return .{ .val = (try mod.intern(.{ .int = .{
-                .ty = .comptime_int_type,
-                .storage = .{ .lazy_align = ty.toIntern() },
-            } })).toValue() },
-        };
-        if (union_obj.fields.count() == 0) {
-            if (have_tag) {
-                return abiAlignmentAdvanced(union_obj.tag_ty, mod, strat);
-            } else {
-                return AbiAlignmentAdvanced{ .scalar = @intFromBool(union_obj.layout == .Extern) };
-            }
-        }
-
-        var max_align: u32 = 0;
-        if (have_tag) max_align = union_obj.tag_ty.abiAlignment(mod);
-        for (union_obj.fields.values()) |field| {
-            if (!(field.ty.hasRuntimeBitsAdvanced(mod, false, strat) catch |err| switch (err) {
-                error.NeedLazy => return .{ .val = (try mod.intern(.{ .int = .{
-                    .ty = .comptime_int_type,
-                    .storage = .{ .lazy_align = ty.toIntern() },
-                } })).toValue() },
-                else => |e| return e,
-            })) continue;
-
-            const field_align = @as(u32, @intCast(field.abi_align.toByteUnitsOptional() orelse
-                switch (try field.ty.abiAlignmentAdvanced(mod, strat)) {
-                .scalar => |a| a,
-                .val => switch (strat) {
-                    .eager => unreachable, // struct layout not resolved
-                    .sema => unreachable, // handled above
-                    .lazy => return .{ .val = (try mod.intern(.{ .int = .{
-                        .ty = .comptime_int_type,
-                        .storage = .{ .lazy_align = ty.toIntern() },
-                    } })).toValue() },
-                },
-            }));
-            max_align = @max(max_align, field_align);
-        }
-        return AbiAlignmentAdvanced{ .scalar = max_align };
-    }
-
     /// May capture a reference to `ty`.
     pub fn lazyAbiSize(ty: Type, mod: *Module) !Value {
         switch (try ty.abiSizeAdvanced(mod, .lazy)) {
@@ -1273,11 +1274,12 @@ pub const Type = struct {
         strat: AbiAlignmentAdvancedStrat,
     ) Module.CompileError!AbiSizeAdvanced {
         const target = mod.getTarget();
+        const ip = &mod.intern_pool;
 
         switch (ty.toIntern()) {
             .empty_struct_type => return AbiSizeAdvanced{ .scalar = 0 },
 
-            else => switch (mod.intern_pool.indexToKey(ty.toIntern())) {
+            else => switch (ip.indexToKey(ty.toIntern())) {
                 .int_type => |int_type| {
                     if (int_type.bits == 0) return AbiSizeAdvanced{ .scalar = 0 };
                     return AbiSizeAdvanced{ .scalar = intAbiSize(int_type.bits, target) };
@@ -1484,8 +1486,18 @@ pub const Type = struct {
                 },
 
                 .union_type => |union_type| {
-                    const union_obj = mod.unionPtr(union_type.index);
-                    return abiSizeAdvancedUnion(ty, mod, strat, union_obj, union_type.hasTag());
+                    switch (strat) {
+                        .sema => |sema| try sema.resolveTypeLayout(ty),
+                        .lazy => if (!union_type.flagsPtr(ip).status.haveLayout()) return .{
+                            .val = (try mod.intern(.{ .int = .{
+                                .ty = .comptime_int_type,
+                                .storage = .{ .lazy_size = ty.toIntern() },
+                            } })).toValue(),
+                        },
+                        .eager => {},
+                    }
+                    const union_obj = ip.loadUnionType(union_type);
+                    return AbiSizeAdvanced{ .scalar = mod.unionAbiSize(union_obj) };
                 },
                 .opaque_type => unreachable, // no size available
                 .enum_type => |enum_type| return AbiSizeAdvanced{ .scalar = enum_type.tag_ty.toType().abiSize(mod) },
@@ -1515,24 +1527,6 @@ pub const Type = struct {
         }
     }
 
-    pub fn abiSizeAdvancedUnion(
-        ty: Type,
-        mod: *Module,
-        strat: AbiAlignmentAdvancedStrat,
-        union_obj: *Module.Union,
-        have_tag: bool,
-    ) Module.CompileError!AbiSizeAdvanced {
-        switch (strat) {
-            .sema => |sema| try sema.resolveTypeLayout(ty),
-            .lazy => if (!union_obj.haveLayout()) return .{ .val = (try mod.intern(.{ .int = .{
-                .ty = .comptime_int_type,
-                .storage = .{ .lazy_size = ty.toIntern() },
-            } })).toValue() },
-            .eager => {},
-        }
-        return AbiSizeAdvanced{ .scalar = union_obj.abiSize(mod, have_tag) };
-    }
-
     fn abiSizeAdvancedOptional(
         ty: Type,
         mod: *Module,
@@ -1602,10 +1596,11 @@ pub const Type = struct {
         opt_sema: ?*Sema,
     ) Module.CompileError!u64 {
         const target = mod.getTarget();
+        const ip = &mod.intern_pool;
 
         const strat: AbiAlignmentAdvancedStrat = if (opt_sema) |sema| .{ .sema = sema } else .eager;
 
-        switch (mod.intern_pool.indexToKey(ty.toIntern())) {
+        switch (ip.indexToKey(ty.toIntern())) {
             .int_type => |int_type| return int_type.bits,
             .ptr_type => |ptr_type| switch (ptr_type.flags.size) {
                 .Slice => return target.ptrBitWidth() * 2,
@@ -1714,12 +1709,13 @@ pub const Type = struct {
                 if (ty.containerLayout(mod) != .Packed) {
                     return (try ty.abiSizeAdvanced(mod, strat)).scalar * 8;
                 }
-                const union_obj = mod.unionPtr(union_type.index);
-                assert(union_obj.haveFieldTypes());
+                const union_obj = ip.loadUnionType(union_type);
+                assert(union_obj.flagsPtr(ip).status.haveFieldTypes());
 
                 var size: u64 = 0;
-                for (union_obj.fields.values()) |field| {
-                    size = @max(size, try bitSizeAdvanced(field.ty, mod, opt_sema));
+                for (0..union_obj.field_types.len) |field_index| {
+                    const field_ty = union_obj.field_types.get(ip)[field_index];
+                    size = @max(size, try bitSizeAdvanced(field_ty.toType(), mod, opt_sema));
                 }
                 return size;
             },
@@ -1753,33 +1749,24 @@ pub const Type = struct {
     /// Returns true if the type's layout is already resolved and it is safe
     /// to use `abiSize`, `abiAlignment` and `bitSize` on it.
     pub fn layoutIsResolved(ty: Type, mod: *Module) bool {
-        switch (ty.zigTypeTag(mod)) {
-            .Struct => {
-                if (mod.typeToStruct(ty)) |struct_obj| {
+        const ip = &mod.intern_pool;
+        return switch (ip.indexToKey(ty.toIntern())) {
+            .struct_type => |struct_type| {
+                if (mod.structPtrUnwrap(struct_type.index)) |struct_obj| {
                     return struct_obj.haveLayout();
+                } else {
+                    return true;
                 }
-                return true;
-            },
-            .Union => {
-                if (mod.typeToUnion(ty)) |union_obj| {
-                    return union_obj.haveLayout();
-                }
-                return true;
             },
-            .Array => {
-                if (ty.arrayLenIncludingSentinel(mod) == 0) return true;
-                return ty.childType(mod).layoutIsResolved(mod);
-            },
-            .Optional => {
-                const payload_ty = ty.optionalChild(mod);
-                return payload_ty.layoutIsResolved(mod);
-            },
-            .ErrorUnion => {
-                const payload_ty = ty.errorUnionPayload(mod);
-                return payload_ty.layoutIsResolved(mod);
+            .union_type => |union_type| union_type.haveLayout(ip),
+            .array_type => |array_type| {
+                if ((array_type.len + @intFromBool(array_type.sentinel != .none)) == 0) return true;
+                return array_type.child.toType().layoutIsResolved(mod);
             },
-            else => return true,
-        }
+            .opt_type => |child| child.toType().layoutIsResolved(mod),
+            .error_union_type => |k| k.payload_type.toType().layoutIsResolved(mod),
+            else => true,
+        };
     }
 
     pub fn isSinglePointer(ty: Type, mod: *const Module) bool {
@@ -1970,12 +1957,12 @@ pub const Type = struct {
     /// Returns the tag type of a union, if the type is a union and it has a tag type.
     /// Otherwise, returns `null`.
     pub fn unionTagType(ty: Type, mod: *Module) ?Type {
-        return switch (mod.intern_pool.indexToKey(ty.toIntern())) {
-            .union_type => |union_type| switch (union_type.runtime_tag) {
+        const ip = &mod.intern_pool;
+        return switch (ip.indexToKey(ty.toIntern())) {
+            .union_type => |union_type| switch (union_type.flagsPtr(ip).runtime_tag) {
                 .tagged => {
-                    const union_obj = mod.unionPtr(union_type.index);
-                    assert(union_obj.haveFieldTypes());
-                    return union_obj.tag_ty;
+                    assert(union_type.flagsPtr(ip).status.haveFieldTypes());
+                    return union_type.enum_tag_ty.toType();
                 },
                 else => null,
             },
@@ -1986,12 +1973,12 @@ pub const Type = struct {
     /// Same as `unionTagType` but includes safety tag.
     /// Codegen should use this version.
     pub fn unionTagTypeSafety(ty: Type, mod: *Module) ?Type {
-        return switch (mod.intern_pool.indexToKey(ty.toIntern())) {
+        const ip = &mod.intern_pool;
+        return switch (ip.indexToKey(ty.toIntern())) {
             .union_type => |union_type| {
-                if (!union_type.hasTag()) return null;
-                const union_obj = mod.unionPtr(union_type.index);
-                assert(union_obj.haveFieldTypes());
-                return union_obj.tag_ty;
+                if (!union_type.hasTag(ip)) return null;
+                assert(union_type.haveFieldTypes(ip));
+                return union_type.enum_tag_ty.toType();
             },
             else => null,
         };
@@ -2001,52 +1988,46 @@ pub const Type = struct {
     /// not be stored at runtime.
     pub fn unionTagTypeHypothetical(ty: Type, mod: *Module) Type {
         const union_obj = mod.typeToUnion(ty).?;
-        assert(union_obj.haveFieldTypes());
-        return union_obj.tag_ty;
-    }
-
-    pub fn unionFields(ty: Type, mod: *Module) Module.Union.Fields {
-        const union_obj = mod.typeToUnion(ty).?;
-        assert(union_obj.haveFieldTypes());
-        return union_obj.fields;
+        return union_obj.enum_tag_ty.toType();
     }
 
     pub fn unionFieldType(ty: Type, enum_tag: Value, mod: *Module) Type {
+        const ip = &mod.intern_pool;
         const union_obj = mod.typeToUnion(ty).?;
-        const index = ty.unionTagFieldIndex(enum_tag, mod).?;
-        assert(union_obj.haveFieldTypes());
-        return union_obj.fields.values()[index].ty;
+        const index = mod.unionTagFieldIndex(union_obj, enum_tag).?;
+        return union_obj.field_types.get(ip)[index].toType();
     }
 
-    pub fn unionTagFieldIndex(ty: Type, enum_tag: Value, mod: *Module) ?usize {
+    pub fn unionTagFieldIndex(ty: Type, enum_tag: Value, mod: *Module) ?u32 {
         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, mod);
-        return union_obj.fields.getIndex(name);
+        return mod.unionTagFieldIndex(union_obj, enum_tag);
     }
 
     pub fn unionHasAllZeroBitFieldTypes(ty: Type, mod: *Module) bool {
+        const ip = &mod.intern_pool;
         const union_obj = mod.typeToUnion(ty).?;
-        return union_obj.hasAllZeroBitFieldTypes(mod);
+        for (union_obj.field_types.get(ip)) |field_ty| {
+            if (field_ty.toType().hasRuntimeBits(mod)) return false;
+        }
+        return true;
     }
 
-    pub fn unionGetLayout(ty: Type, mod: *Module) Module.Union.Layout {
-        const union_type = mod.intern_pool.indexToKey(ty.toIntern()).union_type;
-        const union_obj = mod.unionPtr(union_type.index);
-        return union_obj.getLayout(mod, union_type.hasTag());
+    pub fn unionGetLayout(ty: Type, mod: *Module) Module.UnionLayout {
+        const ip = &mod.intern_pool;
+        const union_type = ip.indexToKey(ty.toIntern()).union_type;
+        const union_obj = ip.loadUnionType(union_type);
+        return mod.getUnionLayout(union_obj);
     }
 
     pub fn containerLayout(ty: Type, mod: *Module) std.builtin.Type.ContainerLayout {
-        return switch (mod.intern_pool.indexToKey(ty.toIntern())) {
+        const ip = &mod.intern_pool;
+        return switch (ip.indexToKey(ty.toIntern())) {
             .struct_type => |struct_type| {
                 const struct_obj = mod.structPtrUnwrap(struct_type.index) orelse return .Auto;
                 return struct_obj.layout;
             },
             .anon_struct_type => .Auto,
-            .union_type => |union_type| {
-                const union_obj = mod.unionPtr(union_type.index);
-                return union_obj.layout;
-            },
+            .union_type => |union_type| union_type.flagsPtr(ip).layout,
             else => unreachable,
         };
     }
@@ -2570,14 +2551,16 @@ pub const Type = struct {
                 },
 
                 .union_type => |union_type| {
-                    const union_obj = mod.unionPtr(union_type.index);
-                    const tag_val = (try union_obj.tag_ty.onePossibleValue(mod)) orelse return null;
-                    if (union_obj.fields.count() == 0) {
+                    const union_obj = ip.loadUnionType(union_type);
+                    const tag_val = (try union_obj.enum_tag_ty.toType().onePossibleValue(mod)) orelse
+                        return null;
+                    if (union_obj.field_names.len == 0) {
                         const only = try mod.intern(.{ .empty_enum_value = ty.toIntern() });
                         return only.toValue();
                     }
-                    const only_field = union_obj.fields.values()[0];
-                    const val_val = (try only_field.ty.onePossibleValue(mod)) orelse return null;
+                    const only_field_ty = union_obj.field_types.get(ip)[0];
+                    const val_val = (try only_field_ty.toType().onePossibleValue(mod)) orelse
+                        return null;
                     const only = try mod.intern(.{ .un = .{
                         .ty = ty.toIntern(),
                         .tag = tag_val.toIntern(),
@@ -2657,10 +2640,11 @@ pub const Type = struct {
     /// TODO merge these implementations together with the "advanced" pattern seen
     /// elsewhere in this file.
     pub fn comptimeOnly(ty: Type, mod: *Module) bool {
+        const ip = &mod.intern_pool;
         return switch (ty.toIntern()) {
             .empty_struct_type => false,
 
-            else => switch (mod.intern_pool.indexToKey(ty.toIntern())) {
+            else => switch (ip.indexToKey(ty.toIntern())) {
                 .int_type => false,
                 .ptr_type => |ptr_type| {
                     const child_ty = ptr_type.child.toType();
@@ -2704,6 +2688,7 @@ pub const Type = struct {
                     .c_longlong,
                     .c_ulonglong,
                     .c_longdouble,
+                    .anyopaque,
                     .bool,
                     .void,
                     .anyerror,
@@ -2722,7 +2707,6 @@ pub const Type = struct {
                     .extern_options,
                     => false,
 
-                    .anyopaque,
                     .type,
                     .comptime_int,
                     .comptime_float,
@@ -2756,8 +2740,7 @@ pub const Type = struct {
                 },
 
                 .union_type => |union_type| {
-                    const union_obj = mod.unionPtr(union_type.index);
-                    switch (union_obj.requires_comptime) {
+                    switch (union_type.flagsPtr(ip).requires_comptime) {
                         .wip, .unknown => {
                             // Return false to avoid incorrect dependency loops.
                             // This will be handled correctly once merged with
@@ -2769,7 +2752,7 @@ pub const Type = struct {
                     }
                 },
 
-                .opaque_type => true,
+                .opaque_type => false,
 
                 .enum_type => |enum_type| enum_type.tag_ty.toType().comptimeOnly(mod),
 
@@ -2847,7 +2830,7 @@ pub const Type = struct {
         return switch (mod.intern_pool.indexToKey(ty.toIntern())) {
             .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(),
+            .union_type => |union_type| union_type.namespace.toOptional(),
             .enum_type => |enum_type| enum_type.namespace,
 
             else => .none,
@@ -2935,7 +2918,7 @@ pub const Type = struct {
     /// Asserts the type is an enum or a union.
     pub fn intTagType(ty: Type, mod: *Module) Type {
         return switch (mod.intern_pool.indexToKey(ty.toIntern())) {
-            .union_type => |union_type| mod.unionPtr(union_type.index).tag_ty.intTagType(mod),
+            .union_type => |union_type| union_type.enum_tag_ty.toType().intTagType(mod),
             .enum_type => |enum_type| enum_type.tag_ty.toType(),
             else => unreachable,
         };
@@ -3038,15 +3021,16 @@ pub const Type = struct {
 
     /// Supports structs and unions.
     pub fn structFieldType(ty: Type, index: usize, mod: *Module) Type {
-        return switch (mod.intern_pool.indexToKey(ty.toIntern())) {
+        const ip = &mod.intern_pool;
+        return switch (ip.indexToKey(ty.toIntern())) {
             .struct_type => |struct_type| {
                 const struct_obj = mod.structPtrUnwrap(struct_type.index).?;
                 assert(struct_obj.haveFieldTypes());
                 return struct_obj.fields.values()[index].ty;
             },
             .union_type => |union_type| {
-                const union_obj = mod.unionPtr(union_type.index);
-                return union_obj.fields.values()[index].ty;
+                const union_obj = ip.loadUnionType(union_type);
+                return union_obj.field_types.get(ip)[index].toType();
             },
             .anon_struct_type => |anon_struct| anon_struct.types[index].toType(),
             else => unreachable,
@@ -3054,7 +3038,8 @@ pub const Type = struct {
     }
 
     pub fn structFieldAlign(ty: Type, index: usize, mod: *Module) u32 {
-        switch (mod.intern_pool.indexToKey(ty.toIntern())) {
+        const ip = &mod.intern_pool;
+        switch (ip.indexToKey(ty.toIntern())) {
             .struct_type => |struct_type| {
                 const struct_obj = mod.structPtrUnwrap(struct_type.index).?;
                 assert(struct_obj.layout != .Packed);
@@ -3064,8 +3049,8 @@ pub const Type = struct {
                 return anon_struct.types[index].toType().abiAlignment(mod);
             },
             .union_type => |union_type| {
-                const union_obj = mod.unionPtr(union_type.index);
-                return union_obj.fields.values()[index].normalAlignment(mod);
+                const union_obj = ip.loadUnionType(union_type);
+                return mod.unionFieldNormalAlignment(union_obj, @intCast(index));
             },
             else => unreachable,
         }
@@ -3198,7 +3183,8 @@ pub const Type = struct {
 
     /// Supports structs and unions.
     pub fn structFieldOffset(ty: Type, index: usize, mod: *Module) u64 {
-        switch (mod.intern_pool.indexToKey(ty.toIntern())) {
+        const ip = &mod.intern_pool;
+        switch (ip.indexToKey(ty.toIntern())) {
             .struct_type => |struct_type| {
                 const struct_obj = mod.structPtrUnwrap(struct_type.index).?;
                 assert(struct_obj.haveLayout());
@@ -3234,10 +3220,10 @@ pub const Type = struct {
             },
 
             .union_type => |union_type| {
-                if (!union_type.hasTag())
+                if (!union_type.hasTag(ip))
                     return 0;
-                const union_obj = mod.unionPtr(union_type.index);
-                const layout = union_obj.getLayout(mod, true);
+                const union_obj = ip.loadUnionType(union_type);
+                const layout = mod.getUnionLayout(union_obj);
                 if (layout.tag_align >= layout.payload_align) {
                     // {Tag, Payload}
                     return std.mem.alignForward(u64, layout.tag_size, layout.payload_align);
@@ -3262,8 +3248,7 @@ pub const Type = struct {
                 return struct_obj.srcLoc(mod);
             },
             .union_type => |union_type| {
-                const union_obj = mod.unionPtr(union_type.index);
-                return union_obj.srcLoc(mod);
+                return mod.declPtr(union_type.decl).srcLoc(mod);
             },
             .opaque_type => |opaque_type| mod.opaqueSrcLoc(opaque_type),
             .enum_type => |enum_type| mod.declPtr(enum_type.decl).srcLoc(mod),
@@ -3281,10 +3266,7 @@ pub const Type = struct {
                 const struct_obj = mod.structPtrUnwrap(struct_type.index) orelse return null;
                 return struct_obj.owner_decl;
             },
-            .union_type => |union_type| {
-                const union_obj = mod.unionPtr(union_type.index);
-                return union_obj.owner_decl;
-            },
+            .union_type => |union_type| union_type.decl,
             .opaque_type => |opaque_type| opaque_type.decl,
             .enum_type => |enum_type| enum_type.decl,
             else => null,
src/TypedValue.zig
@@ -88,7 +88,7 @@ pub fn print(
                 try writer.writeAll(".{ ");
 
                 try print(.{
-                    .ty = mod.unionPtr(ip.indexToKey(ty.toIntern()).union_type.index).tag_ty,
+                    .ty = ip.indexToKey(ty.toIntern()).union_type.enum_tag_ty.toType(),
                     .val = union_val.tag,
                 }, writer, level - 1, mod);
                 try writer.writeAll(" = ");
@@ -357,7 +357,7 @@ pub fn print(
                                 try writer.print(".{i}", .{field_name.fmt(ip)});
                             },
                             .Union => {
-                                const field_name = container_ty.unionFields(mod).keys()[@as(usize, @intCast(field.index))];
+                                const field_name = mod.typeToUnion(container_ty).?.field_names.get(ip)[@intCast(field.index)];
                                 try writer.print(".{i}", .{field_name.fmt(ip)});
                             },
                             .Pointer => {
src/value.zig
@@ -734,6 +734,7 @@ pub const Value = struct {
         buffer: []u8,
         bit_offset: usize,
     ) error{ ReinterpretDeclRef, OutOfMemory }!void {
+        const ip = &mod.intern_pool;
         const target = mod.getTarget();
         const endian = target.cpu.arch.endian();
         if (val.isUndef(mod)) {
@@ -759,7 +760,7 @@ pub const Value = struct {
                 const bits = ty.intInfo(mod).bits;
                 if (bits == 0) return;
 
-                switch (mod.intern_pool.indexToKey((try val.intFromEnum(ty, mod)).toIntern()).int.storage) {
+                switch (ip.indexToKey((try val.intFromEnum(ty, mod)).toIntern()).int.storage) {
                     inline .u64, .i64 => |int| std.mem.writeVarPackedInt(buffer, bit_offset, bits, int, endian),
                     .big_int => |bigint| bigint.writePackedTwosComplement(buffer, bit_offset, bits, endian),
                     else => unreachable,
@@ -794,7 +795,7 @@ pub const Value = struct {
                 .Packed => {
                     var bits: u16 = 0;
                     const fields = ty.structFields(mod).values();
-                    const storage = mod.intern_pool.indexToKey(val.toIntern()).aggregate.storage;
+                    const storage = ip.indexToKey(val.toIntern()).aggregate.storage;
                     for (fields, 0..) |field, i| {
                         const field_bits = @as(u16, @intCast(field.ty.bitSize(mod)));
                         const field_val = switch (storage) {
@@ -807,16 +808,19 @@ pub const Value = struct {
                     }
                 },
             },
-            .Union => switch (ty.containerLayout(mod)) {
-                .Auto => unreachable, // Sema is supposed to have emitted a compile error already
-                .Extern => unreachable, // Handled in non-packed writeToMemory
-                .Packed => {
-                    const field_index = ty.unionTagFieldIndex(val.unionTag(mod), mod);
-                    const field_type = ty.unionFields(mod).values()[field_index.?].ty;
-                    const field_val = try val.fieldValue(mod, field_index.?);
-
-                    return field_val.writeToPackedMemory(field_type, mod, buffer, bit_offset);
-                },
+            .Union => {
+                const union_obj = mod.typeToUnion(ty).?;
+                switch (union_obj.getLayout(ip)) {
+                    .Auto => unreachable, // Sema is supposed to have emitted a compile error already
+                    .Extern => unreachable, // Handled in non-packed writeToMemory
+                    .Packed => {
+                        const field_index = mod.unionTagFieldIndex(union_obj, val.unionTag(mod)).?;
+                        const field_type = union_obj.field_types.get(ip)[field_index].toType();
+                        const field_val = try val.fieldValue(mod, field_index);
+
+                        return field_val.writeToPackedMemory(field_type, mod, buffer, bit_offset);
+                    },
+                }
             },
             .Pointer => {
                 assert(!ty.isSlice(mod)); // No well defined layout.
src/Zir.zig
@@ -2956,7 +2956,8 @@ pub const Inst = struct {
             ///    true      | true          |  union(enum(T)) { }
             ///    true      | false         |  union(T) { }
             auto_enum_tag: bool,
-            _: u6 = undefined,
+            any_aligned_fields: bool,
+            _: u5 = undefined,
         };
     };
 
test/behavior/union.zig
@@ -1347,31 +1347,6 @@ test "noreturn field in union" {
     try expect(count == 6);
 }
 
-test "union and enum field order doesn't match" {
-    if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
-    if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
-    if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
-    if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest;
-
-    const MyTag = enum(u32) {
-        b = 1337,
-        a = 1666,
-    };
-    const MyUnion = union(MyTag) {
-        a: f32,
-        b: void,
-    };
-    var x: MyUnion = .{ .a = 666 };
-    switch (x) {
-        .a => |my_f32| {
-            try expect(@TypeOf(my_f32) == f32);
-        },
-        .b => unreachable,
-    }
-    x = .b;
-    try expect(x == .b);
-}
-
 test "@unionInit uses tag value instead of field index" {
     if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
     if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
@@ -1383,8 +1358,8 @@ test "@unionInit uses tag value instead of field index" {
         a = 3,
     };
     const U = union(E) {
-        a: usize,
         b: isize,
+        a: usize,
     };
     var i: isize = -1;
     var u = @unionInit(U, "b", i);
test/cases/compile_errors/access_inactive_union_field_comptime.zig
@@ -1,4 +1,4 @@
-const Enum = enum(u32) { a, b };
+const Enum = enum(u32) { b, a };
 const TaggedUnion = union(Enum) {
     b: []const u8,
     a: []const u8,
test/cases/compile_errors/dereference_anyopaque.zig
@@ -45,8 +45,7 @@ pub export fn entry() void {
 // backend=llvm
 //
 // :11:22: error: comparison of 'void' with null
-// :25:51: error: values of type 'anyopaque' must be comptime-known, but operand value is runtime-known
-// :25:51: note: opaque type 'anyopaque' has undefined size
+// :25:51: error: cannot load opaque type 'anyopaque'
 // :25:51: error: values of type 'fn(*anyopaque, usize, u8, usize) ?[*]u8' must be comptime-known, but operand value is runtime-known
 // :25:51: note: use '*const fn(*anyopaque, usize, u8, usize) ?[*]u8' for a function pointer type
 // :25:51: error: values of type 'fn(*anyopaque, []u8, u8, usize, usize) bool' must be comptime-known, but operand value is runtime-known
test/cases/compile_errors/directly_embedding_opaque_type_in_struct_and_union.zig
@@ -15,12 +15,12 @@ export fn b() void {
     _ = bar;
 }
 export fn c() void {
-    const baz = &@as(opaque {}, undefined);
+    const baz = &@as(O, undefined);
     const qux = .{baz.*};
     _ = qux;
 }
 export fn d() void {
-    const baz = &@as(opaque {}, undefined);
+    const baz = &@as(O, undefined);
     const qux = .{ .a = baz.* };
     _ = qux;
 }
@@ -33,7 +33,5 @@ export fn d() void {
 // :1:11: note: opaque declared here
 // :7:10: error: opaque types have unknown size and therefore cannot be directly embedded in unions
 // :1:11: note: opaque declared here
-// :19:18: error: opaque types have unknown size and therefore cannot be directly embedded in structs
-// :18:22: note: opaque declared here
-// :24:23: error: opaque types have unknown size and therefore cannot be directly embedded in structs
-// :23:22: note: opaque declared here
+// :19:22: error: cannot load opaque type 'tmp.O'
+// :24:28: error: cannot load opaque type 'tmp.O'
test/cases/compile_errors/non-const_variables_of_things_that_require_const_variables.zig
@@ -27,6 +27,10 @@ export fn entry7() void {
     _ = f;
 }
 const Opaque = opaque {};
+export fn entry8() void {
+    var e: Opaque = undefined;
+    _ = &e;
+}
 
 // error
 // backend=stage2
@@ -39,7 +43,7 @@ const Opaque = opaque {};
 // :14:9: error: variable of type 'comptime_float' must be const or comptime
 // :14:9: note: to modify this variable at runtime, it must be given an explicit fixed-size number type
 // :18:9: error: variable of type '@TypeOf(null)' must be const or comptime
-// :22:20: error: values of type 'tmp.Opaque' must be comptime-known, but operand value is runtime-known
-// :22:20: note: opaque type 'tmp.Opaque' has undefined size
+// :22:20: error: cannot load opaque type 'tmp.Opaque'
 // :26:9: error: variable of type 'type' must be const or comptime
 // :26:9: note: types are not available at runtime
+// :31:12: error: non-extern variable with opaque type 'tmp.Opaque'