Commit 8b88274781

Andrew Kelley <andrew@ziglang.org>
2021-10-15 02:44:46
stage2: improved union support
* `Module.Union.getFullyQualifiedName` returns a sentinel-terminated slice so that backends that need null-termination do not need an additional copy. * Module.Union: implement a `getLayout` function which returns information about ABI size and alignment so that the LLVM backend can properly lower union types into llvm types. * Sema: `resolveType` now returns `error.GenericPoison` rather than a Type with tag `generic_poison`. Callsites that want to allow that need to bypass this higher-level function. * Sema: implement coercion of enums and enum literals to unions. * Sema: fix comptime mutation of pointers to unions * LLVM backend: fully implement proper lowering of union types and values according to the union layout, and update the handling of AIR instructions that deal with unions to support union layouts. * LLVM backend: handle `decl_ref_mut` - Maybe this should be unreachable since comptime vars should be changed to be non-mutable when they go out of scope, but it's harmless for the LLVM backend to support lowering the value. * Type: fix `requiresComptime` for optionals, pointers, and some other types. This function is still wrong for structs, unions, and enums.
1 parent ed5a5e2
Changed files (7)
src/codegen/llvm/bindings.zig
@@ -220,6 +220,9 @@ pub const Type = opaque {
         Packed: Bool,
     ) void;
 
+    pub const structGetTypeAtIndex = LLVMStructGetTypeAtIndex;
+    extern fn LLVMStructGetTypeAtIndex(StructTy: *const Type, i: c_uint) *const Type;
+
     pub const getTypeKind = LLVMGetTypeKind;
     extern fn LLVMGetTypeKind(Ty: *const Type) TypeKind;
 };
src/codegen/llvm.zig
@@ -848,27 +848,79 @@ pub const DeclGen = struct {
                 return llvm_struct_ty;
             },
             .Union => {
-                const union_obj = t.castTag(.@"union").?.data;
-                assert(union_obj.haveFieldTypes());
+                const gop = try dg.object.type_map.getOrPut(gpa, t);
+                if (gop.found_existing) return gop.value_ptr.*;
 
-                const enum_tag_ty = union_obj.tag_ty;
-                const enum_tag_llvm_ty = try dg.llvmType(enum_tag_ty);
+                // The Type memory is ephemeral; since we want to store a longer-lived
+                // reference, we need to copy it here.
+                gop.key_ptr.* = try t.copy(&dg.object.type_map_arena.allocator);
+
+                const union_obj = t.cast(Type.Payload.Union).?.data;
+                const target = dg.module.getTarget();
+                if (t.unionTagType()) |enum_tag_ty| {
+                    const enum_tag_llvm_ty = try dg.llvmType(enum_tag_ty);
+                    const layout = union_obj.getLayout(target, true);
+
+                    if (layout.payload_size == 0) {
+                        gop.value_ptr.* = enum_tag_llvm_ty;
+                        return enum_tag_llvm_ty;
+                    }
+
+                    const name = try union_obj.getFullyQualifiedName(gpa);
+                    defer gpa.free(name);
+
+                    const llvm_union_ty = dg.context.structCreateNamed(name);
+                    gop.value_ptr.* = llvm_union_ty; // must be done before any recursive calls
+
+                    const aligned_field = union_obj.fields.values()[layout.most_aligned_field];
+                    const llvm_aligned_field_ty = try dg.llvmType(aligned_field.ty);
+
+                    const llvm_payload_ty = t: {
+                        if (layout.most_aligned_field_size == layout.payload_size) {
+                            break :t llvm_aligned_field_ty;
+                        }
+                        const padding_len = @intCast(c_uint, layout.payload_size - layout.most_aligned_field_size);
+                        const fields: [2]*const llvm.Type = .{
+                            llvm_aligned_field_ty,
+                            dg.context.intType(8).arrayType(padding_len),
+                        };
+                        break :t dg.context.structType(&fields, fields.len, .False);
+                    };
+
+                    if (layout.tag_size == 0) {
+                        var llvm_fields: [1]*const llvm.Type = .{llvm_payload_ty};
+                        llvm_union_ty.structSetBody(&llvm_fields, llvm_fields.len, .False);
+                        return llvm_union_ty;
+                    }
 
-                if (union_obj.onlyTagHasCodegenBits()) {
-                    return enum_tag_llvm_ty;
+                    // Put the tag before or after the payload depending on which one's
+                    // alignment is greater.
+                    var llvm_fields: [2]*const llvm.Type = undefined;
+                    if (layout.tag_align >= layout.payload_align) {
+                        llvm_fields[0] = enum_tag_llvm_ty;
+                        llvm_fields[1] = llvm_payload_ty;
+                    } else {
+                        llvm_fields[0] = llvm_payload_ty;
+                        llvm_fields[1] = enum_tag_llvm_ty;
+                    }
+                    llvm_union_ty.structSetBody(&llvm_fields, llvm_fields.len, .False);
+                    return llvm_union_ty;
                 }
+                // Untagged union
+                const layout = union_obj.getLayout(target, false);
 
-                const target = dg.module.getTarget();
-                const most_aligned_field_index = union_obj.mostAlignedField(target);
-                const most_aligned_field = union_obj.fields.values()[most_aligned_field_index];
-                // TODO handle when the most aligned field is different than the
-                // biggest sized field.
-
-                const llvm_fields = [_]*const llvm.Type{
-                    try dg.llvmType(most_aligned_field.ty),
-                    enum_tag_llvm_ty,
-                };
-                return dg.context.structType(&llvm_fields, llvm_fields.len, .False);
+                const name = try union_obj.getFullyQualifiedName(gpa);
+                defer gpa.free(name);
+
+                const llvm_union_ty = dg.context.structCreateNamed(name);
+                gop.value_ptr.* = llvm_union_ty; // must be done before any recursive calls
+
+                const big_field = union_obj.fields.values()[layout.biggest_field];
+                const llvm_big_field_ty = try dg.llvmType(big_field.ty);
+
+                var llvm_fields: [1]*const llvm.Type = .{llvm_big_field_ty};
+                llvm_union_ty.structSetBody(&llvm_fields, llvm_fields.len, .False);
+                return llvm_union_ty;
             },
             .Fn => {
                 const fn_info = t.fnInfo();
@@ -983,36 +1035,8 @@ pub const DeclGen = struct {
                 return int.constBitCast(llvm_ty);
             },
             .Pointer => switch (tv.val.tag()) {
-                .decl_ref => {
-                    if (tv.ty.isSlice()) {
-                        var buf: Type.SlicePtrFieldTypeBuffer = undefined;
-                        const ptr_ty = tv.ty.slicePtrFieldType(&buf);
-                        var slice_len: Value.Payload.U64 = .{
-                            .base = .{ .tag = .int_u64 },
-                            .data = tv.val.sliceLen(),
-                        };
-                        const fields: [2]*const llvm.Value = .{
-                            try self.genTypedValue(.{
-                                .ty = ptr_ty,
-                                .val = tv.val,
-                            }),
-                            try self.genTypedValue(.{
-                                .ty = Type.initTag(.usize),
-                                .val = Value.initPayload(&slice_len.base),
-                            }),
-                        };
-                        return self.context.constStruct(&fields, fields.len, .False);
-                    } else {
-                        const decl = tv.val.castTag(.decl_ref).?.data;
-                        decl.alive = true;
-                        const llvm_type = try self.llvmType(tv.ty);
-                        const llvm_val = if (decl.ty.zigTypeTag() == .Fn)
-                            try self.resolveLlvmFunction(decl)
-                        else
-                            try self.resolveGlobalDecl(decl);
-                        return llvm_val.constBitCast(llvm_type);
-                    }
-                },
+                .decl_ref_mut => return lowerDeclRefValue(self, tv, tv.val.castTag(.decl_ref_mut).?.data.decl),
+                .decl_ref => return lowerDeclRefValue(self, tv, tv.val.castTag(.decl_ref).?.data),
                 .variable => {
                     const decl = tv.val.castTag(.variable).?.data.owner_decl;
                     decl.alive = true;
@@ -1192,6 +1216,49 @@ pub const DeclGen = struct {
                     @intCast(c_uint, llvm_fields.items.len),
                 );
             },
+            .Union => {
+                const llvm_union_ty = try self.llvmType(tv.ty);
+                const tag_and_val = tv.val.castTag(.@"union").?.data;
+
+                const target = self.module.getTarget();
+                const layout = tv.ty.unionGetLayout(target);
+
+                if (layout.payload_size == 0) {
+                    return genTypedValue(self, .{ .ty = tv.ty.unionTagType().?, .val = tag_and_val.tag });
+                }
+                const field_ty = tv.ty.unionFieldType(tag_and_val.tag);
+                const payload = p: {
+                    const field = try genTypedValue(self, .{ .ty = field_ty, .val = tag_and_val.val });
+                    const field_size = field_ty.abiSize(target);
+                    if (field_size == layout.payload_size) {
+                        break :p field;
+                    }
+                    const padding_len = @intCast(c_uint, layout.payload_size - field_size);
+                    const fields: [2]*const llvm.Value = .{
+                        field, self.context.intType(8).arrayType(padding_len).getUndef(),
+                    };
+                    break :p self.context.constStruct(&fields, fields.len, .False);
+                };
+                if (layout.tag_size == 0) {
+                    const llvm_payload_ty = llvm_union_ty.structGetTypeAtIndex(0);
+                    const fields: [1]*const llvm.Value = .{payload.constBitCast(llvm_payload_ty)};
+                    return llvm_union_ty.constNamedStruct(&fields, fields.len);
+                }
+                const llvm_tag_value = try genTypedValue(self, .{
+                    .ty = tv.ty.unionTagType().?,
+                    .val = tag_and_val.tag,
+                });
+                var fields: [2]*const llvm.Value = undefined;
+                if (layout.tag_align >= layout.payload_align) {
+                    fields[0] = llvm_tag_value;
+                    fields[1] = payload.constBitCast(llvm_union_ty.structGetTypeAtIndex(1));
+                } else {
+                    fields[0] = payload.constBitCast(llvm_union_ty.structGetTypeAtIndex(0));
+                    fields[1] = llvm_tag_value;
+                }
+                return llvm_union_ty.constNamedStruct(&fields, fields.len);
+            },
+
             .ComptimeInt => unreachable,
             .ComptimeFloat => unreachable,
             .Type => unreachable,
@@ -1203,7 +1270,6 @@ pub const DeclGen = struct {
             .BoundFn => unreachable,
             .Opaque => unreachable,
 
-            .Union,
             .Frame,
             .AnyFrame,
             .Vector,
@@ -1211,6 +1277,40 @@ pub const DeclGen = struct {
         }
     }
 
+    fn lowerDeclRefValue(
+        self: *DeclGen,
+        tv: TypedValue,
+        decl: *Module.Decl,
+    ) Error!*const llvm.Value {
+        if (tv.ty.isSlice()) {
+            var buf: Type.SlicePtrFieldTypeBuffer = undefined;
+            const ptr_ty = tv.ty.slicePtrFieldType(&buf);
+            var slice_len: Value.Payload.U64 = .{
+                .base = .{ .tag = .int_u64 },
+                .data = tv.val.sliceLen(),
+            };
+            const fields: [2]*const llvm.Value = .{
+                try self.genTypedValue(.{
+                    .ty = ptr_ty,
+                    .val = tv.val,
+                }),
+                try self.genTypedValue(.{
+                    .ty = Type.initTag(.usize),
+                    .val = Value.initPayload(&slice_len.base),
+                }),
+            };
+            return self.context.constStruct(&fields, fields.len, .False);
+        }
+
+        decl.alive = true;
+        const llvm_type = try self.llvmType(tv.ty);
+        const llvm_val = if (decl.ty.zigTypeTag() == .Fn)
+            try self.resolveLlvmFunction(decl)
+        else
+            try self.resolveGlobalDecl(decl);
+        return llvm_val.constBitCast(llvm_type);
+    }
+
     fn addAttr(dg: DeclGen, val: *const llvm.Value, index: llvm.AttributeIndex, name: []const u8) void {
         return dg.addAttrInt(val, index, name, 0);
     }
@@ -2917,25 +3017,45 @@ pub const FuncGen = struct {
 
     fn airSetUnionTag(self: *FuncGen, inst: Air.Inst.Index) !?*const llvm.Value {
         const bin_op = self.air.instructions.items(.data)[inst].bin_op;
+        const un_ty = self.air.typeOf(bin_op.lhs).childType();
+        const target = self.dg.module.getTarget();
+        const layout = un_ty.unionGetLayout(target);
+        if (layout.tag_size == 0) return null;
         const union_ptr = try self.resolveInst(bin_op.lhs);
-        // TODO handle when onlyTagHasCodegenBits() == true
         const new_tag = try self.resolveInst(bin_op.rhs);
-        const tag_field_ptr = self.builder.buildStructGEP(union_ptr, 1, "");
-
+        if (layout.payload_size == 0) {
+            _ = self.builder.buildStore(new_tag, union_ptr);
+            return null;
+        }
+        const tag_index = @boolToInt(layout.tag_align < layout.payload_align);
+        const tag_field_ptr = self.builder.buildStructGEP(union_ptr, tag_index, "");
         _ = self.builder.buildStore(new_tag, tag_field_ptr);
         return null;
     }
 
     fn airGetUnionTag(self: *FuncGen, inst: Air.Inst.Index) !?*const llvm.Value {
-        if (self.liveness.isUnused(inst))
-            return null;
+        if (self.liveness.isUnused(inst)) return null;
 
         const ty_op = self.air.instructions.items(.data)[inst].ty_op;
         const un_ty = self.air.typeOf(ty_op.operand);
-        const un = try self.resolveInst(ty_op.operand);
-
-        _ = un_ty; // TODO handle when onlyTagHasCodegenBits() == true and other union forms
-        return self.builder.buildExtractValue(un, 1, "");
+        const target = self.dg.module.getTarget();
+        const layout = un_ty.unionGetLayout(target);
+        if (layout.tag_size == 0) return null;
+        const union_handle = try self.resolveInst(ty_op.operand);
+        if (isByRef(un_ty)) {
+            if (layout.payload_size == 0) {
+                return self.builder.buildLoad(union_handle, "");
+            }
+            const tag_index = @boolToInt(layout.tag_align < layout.payload_align);
+            const tag_field_ptr = self.builder.buildStructGEP(union_handle, tag_index, "");
+            return self.builder.buildLoad(tag_field_ptr, "");
+        } else {
+            if (layout.payload_size == 0) {
+                return union_handle;
+            }
+            const tag_index = @boolToInt(layout.tag_align < layout.payload_align);
+            return self.builder.buildExtractValue(union_handle, tag_index, "");
+        }
     }
 
     fn airClzCtz(self: *FuncGen, inst: Air.Inst.Index, prefix: [*:0]const u8) !?*const llvm.Value {
@@ -3004,7 +3124,10 @@ pub const FuncGen = struct {
         if (!field.ty.hasCodeGenBits()) {
             return null;
         }
-        const union_field_ptr = self.builder.buildStructGEP(union_ptr, 0, "");
+        const target = self.dg.module.getTarget();
+        const layout = union_ty.unionGetLayout(target);
+        const payload_index = @boolToInt(layout.tag_align >= layout.payload_align);
+        const union_field_ptr = self.builder.buildStructGEP(union_ptr, payload_index, "");
         return self.builder.buildBitCast(union_field_ptr, result_llvm_ty, "");
     }
 
src/Module.zig
@@ -964,7 +964,7 @@ pub const Union = struct {
 
     pub const Fields = std.StringArrayHashMapUnmanaged(Field);
 
-    pub fn getFullyQualifiedName(s: *Union, gpa: *Allocator) ![]u8 {
+    pub fn getFullyQualifiedName(s: *Union, gpa: *Allocator) ![:0]u8 {
         return s.owner_decl.getFullyQualifiedName(gpa);
     }
 
@@ -988,7 +988,7 @@ pub const Union = struct {
         };
     }
 
-    pub fn onlyTagHasCodegenBits(u: Union) bool {
+    pub fn hasAllZeroBitFieldTypes(u: Union) bool {
         assert(u.haveFieldTypes());
         for (u.fields.values()) |field| {
             if (field.ty.hasCodeGenBits()) return false;
@@ -1038,13 +1038,32 @@ pub const Union = struct {
     }
 
     pub fn abiSize(u: Union, target: Target, have_tag: bool) u64 {
-        assert(u.haveFieldTypes());
+        return u.getLayout(target, 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,
+    };
+
+    pub fn getLayout(u: Union, target: Target, have_tag: bool) Layout {
+        assert(u.status == .have_layout);
         const is_packed = u.layout == .Packed;
         if (is_packed) @panic("TODO packed unions");
 
+        var most_aligned_field: usize = undefined;
+        var most_aligned_field_size: u64 = undefined;
+        var biggest_field: usize = undefined;
         var payload_size: u64 = 0;
         var payload_align: u32 = 0;
-        for (u.fields.values()) |field| {
+        for (u.fields.values()) |field, i| {
             if (!field.ty.hasCodeGenBits()) continue;
 
             const field_align = a: {
@@ -1054,12 +1073,28 @@ pub const Union = struct {
                     break :a @intCast(u32, field.abi_align.toUnsignedInt());
                 }
             };
-            payload_size = @maximum(payload_size, field.ty.abiSize(target));
-            payload_align = @maximum(payload_align, field_align);
-        }
-        if (!have_tag) {
-            return std.mem.alignForwardGeneric(u64, payload_size, payload_align);
+            const field_size = field.ty.abiSize(target);
+            if (field_size > payload_size) {
+                payload_size = field_size;
+                biggest_field = i;
+            }
+            if (field_align > payload_align) {
+                payload_align = field_align;
+                most_aligned_field = i;
+                most_aligned_field_size = field_size;
+            }
         }
+        if (!have_tag) return .{
+            .abi_size = std.mem.alignForwardGeneric(u64, payload_size, payload_align),
+            .abi_align = payload_align,
+            .most_aligned_field = @intCast(u32, most_aligned_field),
+            .most_aligned_field_size = most_aligned_field_size,
+            .biggest_field = @intCast(u32, biggest_field),
+            .payload_size = payload_size,
+            .payload_align = payload_align,
+            .tag_align = 0,
+            .tag_size = 0,
+        };
         // Put the tag before or after the payload depending on which one's
         // alignment is greater.
         const tag_size = u.tag_ty.abiSize(target);
@@ -1078,7 +1113,17 @@ pub const Union = struct {
             size += tag_size;
             size = std.mem.alignForwardGeneric(u64, size, payload_align);
         }
-        return size;
+        return .{
+            .abi_size = size,
+            .abi_align = @maximum(tag_align, payload_align),
+            .most_aligned_field = @intCast(u32, most_aligned_field),
+            .most_aligned_field_size = most_aligned_field_size,
+            .biggest_field = @intCast(u32, biggest_field),
+            .payload_size = payload_size,
+            .payload_align = payload_align,
+            .tag_align = tag_align,
+            .tag_size = tag_size,
+        };
     }
 };
 
src/Sema.zig
@@ -1026,7 +1026,9 @@ fn resolveConstString(
 
 pub fn resolveType(sema: *Sema, block: *Block, src: LazySrcLoc, zir_ref: Zir.Inst.Ref) !Type {
     const air_inst = sema.resolveInst(zir_ref);
-    return sema.analyzeAsType(block, src, air_inst);
+    const ty = try sema.analyzeAsType(block, src, air_inst);
+    if (ty.tag() == .generic_poison) return error.GenericPoison;
+    return ty;
 }
 
 fn analyzeAsType(
@@ -1284,10 +1286,10 @@ fn resolveInt(
     block: *Block,
     src: LazySrcLoc,
     zir_ref: Zir.Inst.Ref,
-    dest_type: Type,
+    dest_ty: Type,
 ) !u64 {
     const air_inst = sema.resolveInst(zir_ref);
-    const coerced = try sema.coerce(block, dest_type, air_inst, src);
+    const coerced = try sema.coerce(block, dest_ty, air_inst, src);
     const val = try sema.resolveConstValue(block, src, coerced);
 
     return val.toUnsignedInt();
@@ -2403,6 +2405,19 @@ fn failWithBadUnionFieldAccess(
     return sema.failWithOwnedErrorMsg(msg);
 }
 
+fn addDeclaredHereNote(sema: *Sema, parent: *Module.ErrorMsg, decl_ty: Type) !void {
+    const src_loc = decl_ty.declSrcLocOrNull() orelse return;
+    const category = switch (decl_ty.zigTypeTag()) {
+        .Union => "union",
+        .Struct => "struct",
+        .Enum => "enum",
+        .Opaque => "opaque",
+        .ErrorSet => "error set",
+        else => unreachable,
+    };
+    try sema.mod.errNoteNonLazy(src_loc, parent, "{s} declared here", .{category});
+}
+
 fn zirStoreToBlockPtr(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!void {
     const tracy = trace(@src());
     defer tracy.end();
@@ -5059,9 +5074,9 @@ fn analyzeAs(
     zir_dest_type: Zir.Inst.Ref,
     zir_operand: Zir.Inst.Ref,
 ) CompileError!Air.Inst.Ref {
-    const dest_type = try sema.resolveType(block, src, zir_dest_type);
+    const dest_ty = try sema.resolveType(block, src, zir_dest_type);
     const operand = sema.resolveInst(zir_operand);
-    return sema.coerce(block, dest_type, operand, src);
+    return sema.coerce(block, dest_ty, operand, src);
 }
 
 fn zirPtrToInt(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air.Inst.Ref {
@@ -5175,21 +5190,21 @@ fn zirIntCast(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air
     const operand_src: LazySrcLoc = .{ .node_offset_builtin_call_arg1 = inst_data.src_node };
     const extra = sema.code.extraData(Zir.Inst.Bin, inst_data.payload_index).data;
 
-    const dest_type = try sema.resolveType(block, dest_ty_src, extra.lhs);
+    const dest_ty = try sema.resolveType(block, dest_ty_src, extra.lhs);
     const operand = sema.resolveInst(extra.rhs);
 
-    const dest_is_comptime_int = try sema.checkIntType(block, dest_ty_src, dest_type);
+    const dest_is_comptime_int = try sema.checkIntType(block, dest_ty_src, dest_ty);
     _ = try sema.checkIntType(block, operand_src, sema.typeOf(operand));
 
     if (try sema.isComptimeKnown(block, operand_src, operand)) {
-        return sema.coerce(block, dest_type, operand, operand_src);
+        return sema.coerce(block, dest_ty, operand, operand_src);
     } else if (dest_is_comptime_int) {
         return sema.fail(block, src, "unable to cast runtime value to 'comptime_int'", .{});
     }
 
     try sema.requireRuntimeBlock(block, operand_src);
     // TODO insert safety check to make sure the value fits in the dest type
-    return block.addTyOp(.intcast, dest_type, operand);
+    return block.addTyOp(.intcast, dest_ty, operand);
 }
 
 fn zirBitcast(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air.Inst.Ref {
@@ -5201,9 +5216,9 @@ fn zirBitcast(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air
     const operand_src: LazySrcLoc = .{ .node_offset_builtin_call_arg1 = inst_data.src_node };
     const extra = sema.code.extraData(Zir.Inst.Bin, inst_data.payload_index).data;
 
-    const dest_type = try sema.resolveType(block, dest_ty_src, extra.lhs);
+    const dest_ty = try sema.resolveType(block, dest_ty_src, extra.lhs);
     const operand = sema.resolveInst(extra.rhs);
-    return sema.bitCast(block, dest_type, operand, operand_src);
+    return sema.bitCast(block, dest_ty, operand, operand_src);
 }
 
 fn zirFloatCast(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air.Inst.Ref {
@@ -5216,17 +5231,17 @@ fn zirFloatCast(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!A
     const operand_src: LazySrcLoc = .{ .node_offset_builtin_call_arg1 = inst_data.src_node };
     const extra = sema.code.extraData(Zir.Inst.Bin, inst_data.payload_index).data;
 
-    const dest_type = try sema.resolveType(block, dest_ty_src, extra.lhs);
+    const dest_ty = try sema.resolveType(block, dest_ty_src, extra.lhs);
     const operand = sema.resolveInst(extra.rhs);
 
-    const dest_is_comptime_float = switch (dest_type.zigTypeTag()) {
+    const dest_is_comptime_float = switch (dest_ty.zigTypeTag()) {
         .ComptimeFloat => true,
         .Float => false,
         else => return sema.fail(
             block,
             dest_ty_src,
             "expected float type, found '{}'",
-            .{dest_type},
+            .{dest_ty},
         ),
     };
 
@@ -5242,19 +5257,19 @@ fn zirFloatCast(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!A
     }
 
     if (try sema.isComptimeKnown(block, operand_src, operand)) {
-        return sema.coerce(block, dest_type, operand, operand_src);
+        return sema.coerce(block, dest_ty, operand, operand_src);
     }
     if (dest_is_comptime_float) {
         return sema.fail(block, src, "unable to cast runtime value to 'comptime_float'", .{});
     }
     const target = sema.mod.getTarget();
     const src_bits = operand_ty.floatBits(target);
-    const dst_bits = dest_type.floatBits(target);
+    const dst_bits = dest_ty.floatBits(target);
     if (dst_bits >= src_bits) {
-        return sema.coerce(block, dest_type, operand, operand_src);
+        return sema.coerce(block, dest_ty, operand, operand_src);
     }
     try sema.requireRuntimeBlock(block, operand_src);
-    return block.addTyOp(.fptrunc, dest_type, operand);
+    return block.addTyOp(.fptrunc, dest_ty, operand);
 }
 
 fn zirElemVal(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air.Inst.Ref {
@@ -11265,60 +11280,60 @@ fn elemPtrArray(
 fn coerce(
     sema: *Sema,
     block: *Block,
-    dest_type_unresolved: Type,
+    dest_ty_unresolved: Type,
     inst: Air.Inst.Ref,
     inst_src: LazySrcLoc,
 ) CompileError!Air.Inst.Ref {
-    switch (dest_type_unresolved.tag()) {
+    switch (dest_ty_unresolved.tag()) {
         .var_args_param => return sema.coerceVarArgParam(block, inst, inst_src),
         .generic_poison => return inst,
         else => {},
     }
-    const dest_type_src = inst_src; // TODO better source location
-    const dest_type = try sema.resolveTypeFields(block, dest_type_src, dest_type_unresolved);
+    const dest_ty_src = inst_src; // TODO better source location
+    const dest_ty = try sema.resolveTypeFields(block, dest_ty_src, dest_ty_unresolved);
 
     const inst_ty = sema.typeOf(inst);
     // If the types are the same, we can return the operand.
-    if (dest_type.eql(inst_ty))
+    if (dest_ty.eql(inst_ty))
         return inst;
 
     const arena = sema.arena;
     const target = sema.mod.getTarget();
 
-    const in_memory_result = coerceInMemoryAllowed(dest_type, inst_ty, false, target);
+    const in_memory_result = coerceInMemoryAllowed(dest_ty, inst_ty, false, target);
     if (in_memory_result == .ok) {
         if (try sema.resolveMaybeUndefVal(block, inst_src, inst)) |val| {
             // Keep the comptime Value representation; take the new type.
-            return sema.addConstant(dest_type, val);
+            return sema.addConstant(dest_ty, val);
         }
         try sema.requireRuntimeBlock(block, inst_src);
-        return block.addTyOp(.bitcast, dest_type, inst);
+        return block.addTyOp(.bitcast, dest_ty, inst);
     }
 
     // undefined to anything
     if (try sema.resolveMaybeUndefVal(block, inst_src, inst)) |val| {
         if (val.isUndef() or inst_ty.zigTypeTag() == .Undefined) {
-            return sema.addConstant(dest_type, val);
+            return sema.addConstant(dest_ty, val);
         }
     }
     assert(inst_ty.zigTypeTag() != .Undefined);
 
     // comptime known number to other number
-    if (try sema.coerceNum(block, dest_type, inst, inst_src)) |some|
+    if (try sema.coerceNum(block, dest_ty, inst, inst_src)) |some|
         return some;
 
-    switch (dest_type.zigTypeTag()) {
+    switch (dest_ty.zigTypeTag()) {
         .Optional => {
             // null to ?T
             if (inst_ty.zigTypeTag() == .Null) {
-                return sema.addConstant(dest_type, Value.initTag(.null_value));
+                return sema.addConstant(dest_ty, Value.initTag(.null_value));
             }
 
             // T to ?T
             var buf: Type.Payload.ElemType = undefined;
-            const child_type = dest_type.optionalChild(&buf);
+            const child_type = dest_ty.optionalChild(&buf);
             const intermediate = try sema.coerce(block, child_type, inst, inst_src);
-            return sema.wrapOptional(block, dest_type, intermediate, inst_src);
+            return sema.wrapOptional(block, dest_ty, intermediate, inst_src);
         },
         .Pointer => {
             // Function body to function pointer.
@@ -11326,7 +11341,7 @@ fn coerce(
                 const fn_val = try sema.resolveConstValue(block, inst_src, inst);
                 const fn_decl = fn_val.castTag(.function).?.data.owner_decl;
                 const inst_as_ptr = try sema.analyzeDeclRef(fn_decl);
-                return sema.coerce(block, dest_type, inst_as_ptr, inst_src);
+                return sema.coerce(block, dest_ty, inst_as_ptr, inst_src);
             }
 
             // Coercions where the source is a single pointer to an array.
@@ -11335,38 +11350,38 @@ fn coerce(
                 const array_type = inst_ty.elemType();
                 if (array_type.zigTypeTag() != .Array) break :src_array_ptr;
                 const array_elem_type = array_type.elemType();
-                const dest_is_mut = !dest_type.isConstPtr();
+                const dest_is_mut = !dest_ty.isConstPtr();
                 if (inst_ty.isConstPtr() and dest_is_mut) break :src_array_ptr;
-                if (inst_ty.isVolatilePtr() and !dest_type.isVolatilePtr()) break :src_array_ptr;
-                if (inst_ty.ptrAddressSpace() != dest_type.ptrAddressSpace()) break :src_array_ptr;
+                if (inst_ty.isVolatilePtr() and !dest_ty.isVolatilePtr()) break :src_array_ptr;
+                if (inst_ty.ptrAddressSpace() != dest_ty.ptrAddressSpace()) break :src_array_ptr;
 
-                const dst_elem_type = dest_type.elemType();
+                const dst_elem_type = dest_ty.elemType();
                 switch (coerceInMemoryAllowed(dst_elem_type, array_elem_type, dest_is_mut, target)) {
                     .ok => {},
                     .no_match => break :src_array_ptr,
                 }
 
-                switch (dest_type.ptrSize()) {
+                switch (dest_ty.ptrSize()) {
                     .Slice => {
                         // *[N]T to []T
-                        return sema.coerceArrayPtrToSlice(block, dest_type, inst, inst_src);
+                        return sema.coerceArrayPtrToSlice(block, dest_ty, inst, inst_src);
                     },
                     .C => {
                         // *[N]T to [*c]T
-                        return sema.coerceArrayPtrToMany(block, dest_type, inst, inst_src);
+                        return sema.coerceArrayPtrToMany(block, dest_ty, inst, inst_src);
                     },
                     .Many => {
                         // *[N]T to [*]T
                         // *[N:s]T to [*:s]T
                         // *[N:s]T to [*]T
-                        if (dest_type.sentinel()) |dst_sentinel| {
+                        if (dest_ty.sentinel()) |dst_sentinel| {
                             if (array_type.sentinel()) |src_sentinel| {
                                 if (src_sentinel.eql(dst_sentinel, dst_elem_type)) {
-                                    return sema.coerceArrayPtrToMany(block, dest_type, inst, inst_src);
+                                    return sema.coerceArrayPtrToMany(block, dest_ty, inst, inst_src);
                                 }
                             }
                         } else {
-                            return sema.coerceArrayPtrToMany(block, dest_type, inst, inst_src);
+                            return sema.coerceArrayPtrToMany(block, dest_ty, inst, inst_src);
                         }
                     },
                     .One => {},
@@ -11378,14 +11393,14 @@ fn coerce(
             if (inst_ty.zigTypeTag() == .Int) {
                 assert(!(try sema.isComptimeKnown(block, inst_src, inst))); // handled above
 
-                const dst_info = dest_type.intInfo(target);
+                const dst_info = dest_ty.intInfo(target);
                 const src_info = inst_ty.intInfo(target);
                 if ((src_info.signedness == dst_info.signedness and dst_info.bits >= src_info.bits) or
                     // small enough unsigned ints can get casted to large enough signed ints
                     (dst_info.signedness == .signed and dst_info.bits > src_info.bits))
                 {
                     try sema.requireRuntimeBlock(block, inst_src);
-                    return block.addTyOp(.intcast, dest_type, inst);
+                    return block.addTyOp(.intcast, dest_ty, inst);
                 }
             }
         },
@@ -11395,10 +11410,10 @@ fn coerce(
                 assert(!(try sema.isComptimeKnown(block, inst_src, inst))); // handled above
 
                 const src_bits = inst_ty.floatBits(target);
-                const dst_bits = dest_type.floatBits(target);
+                const dst_bits = dest_ty.floatBits(target);
                 if (dst_bits >= src_bits) {
                     try sema.requireRuntimeBlock(block, inst_src);
-                    return block.addTyOp(.fpext, dest_type, inst);
+                    return block.addTyOp(.fpext, dest_ty, inst);
                 }
             }
         },
@@ -11407,7 +11422,7 @@ fn coerce(
                 // enum literal to enum
                 const val = try sema.resolveConstValue(block, inst_src, inst);
                 const bytes = val.castTag(.enum_literal).?.data;
-                const resolved_dest_type = try sema.resolveTypeFields(block, inst_src, dest_type);
+                const resolved_dest_type = try sema.resolveTypeFields(block, inst_src, dest_ty);
                 const field_index = resolved_dest_type.enumFieldIndex(bytes) orelse {
                     const msg = msg: {
                         const msg = try sema.errMsg(
@@ -11435,20 +11450,24 @@ fn coerce(
             .Union => blk: {
                 // union to its own tag type
                 const union_tag_ty = inst_ty.unionTagType() orelse break :blk;
-                if (union_tag_ty.eql(dest_type)) {
-                    return sema.unionToTag(block, dest_type, inst, inst_src);
+                if (union_tag_ty.eql(dest_ty)) {
+                    return sema.unionToTag(block, dest_ty, inst, inst_src);
                 }
             },
             else => {},
         },
         .ErrorUnion => {
             // T to E!T or E to E!T
-            return sema.wrapErrorUnion(block, dest_type, inst, inst_src);
+            return sema.wrapErrorUnion(block, dest_ty, inst, inst_src);
+        },
+        .Union => switch (inst_ty.zigTypeTag()) {
+            .Enum, .EnumLiteral => return sema.coerceEnumToUnion(block, dest_ty, dest_ty_src, inst, inst_src),
+            else => {},
         },
         else => {},
     }
 
-    return sema.fail(block, inst_src, "expected {}, found {}", .{ dest_type, inst_ty });
+    return sema.fail(block, inst_src, "expected {}, found {}", .{ dest_ty, inst_ty });
 }
 
 const InMemoryCoercionResult = enum {
@@ -11467,14 +11486,14 @@ const InMemoryCoercionResult = enum {
 /// * sentinel-terminated pointers can coerce into `[*]`
 /// TODO improve this function to report recursive compile errors like it does in stage1.
 /// look at the function types_match_const_cast_only
-fn coerceInMemoryAllowed(dest_type: Type, src_type: Type, dest_is_mut: bool, target: std.Target) InMemoryCoercionResult {
-    if (dest_type.eql(src_type))
+fn coerceInMemoryAllowed(dest_ty: Type, src_type: Type, dest_is_mut: bool, target: std.Target) InMemoryCoercionResult {
+    if (dest_ty.eql(src_type))
         return .ok;
 
-    if (dest_type.zigTypeTag() == .Pointer and
+    if (dest_ty.zigTypeTag() == .Pointer and
         src_type.zigTypeTag() == .Pointer)
     {
-        const dest_info = dest_type.ptrInfo().data;
+        const dest_info = dest_ty.ptrInfo().data;
         const src_info = src_type.ptrInfo().data;
 
         const child = coerceInMemoryAllowed(dest_info.pointee_type, src_info.pointee_type, dest_info.mutable, target);
@@ -11514,7 +11533,7 @@ fn coerceInMemoryAllowed(dest_type: Type, src_type: Type, dest_is_mut: bool, tar
             return .no_match;
         }
 
-        if (dest_type.hasCodeGenBits() != src_type.hasCodeGenBits()) {
+        if (dest_ty.hasCodeGenBits() != src_type.hasCodeGenBits()) {
             return .no_match;
         }
 
@@ -11532,7 +11551,7 @@ fn coerceInMemoryAllowed(dest_type: Type, src_type: Type, dest_is_mut: bool, tar
             !dest_info.pointee_type.eql(src_info.pointee_type))
         {
             const src_align = src_type.ptrAlignment(target);
-            const dest_align = dest_type.ptrAlignment(target);
+            const dest_align = dest_ty.ptrAlignment(target);
 
             if (dest_align > src_align) {
                 return .no_match;
@@ -11550,14 +11569,14 @@ fn coerceInMemoryAllowed(dest_type: Type, src_type: Type, dest_is_mut: bool, tar
 fn coerceNum(
     sema: *Sema,
     block: *Block,
-    dest_type: Type,
+    dest_ty: Type,
     inst: Air.Inst.Ref,
     inst_src: LazySrcLoc,
 ) CompileError!?Air.Inst.Ref {
     const val = (try sema.resolveDefinedValue(block, inst_src, inst)) orelse return null;
     const inst_ty = sema.typeOf(inst);
     const src_zig_tag = inst_ty.zigTypeTag();
-    const dst_zig_tag = dest_type.zigTypeTag();
+    const dst_zig_tag = dest_ty.zigTypeTag();
 
     const target = sema.mod.getTarget();
 
@@ -11565,37 +11584,37 @@ fn coerceNum(
         .ComptimeInt, .Int => switch (src_zig_tag) {
             .Float, .ComptimeFloat => {
                 if (val.floatHasFraction()) {
-                    return sema.fail(block, inst_src, "fractional component prevents float value {} from coercion to type '{}'", .{ val, dest_type });
+                    return sema.fail(block, inst_src, "fractional component prevents float value {} from coercion to type '{}'", .{ val, dest_ty });
                 }
                 return sema.fail(block, inst_src, "TODO float to int", .{});
             },
             .Int, .ComptimeInt => {
-                if (!val.intFitsInType(dest_type, target)) {
-                    return sema.fail(block, inst_src, "type {} cannot represent integer value {}", .{ dest_type, val });
+                if (!val.intFitsInType(dest_ty, target)) {
+                    return sema.fail(block, inst_src, "type {} cannot represent integer value {}", .{ dest_ty, val });
                 }
-                return try sema.addConstant(dest_type, val);
+                return try sema.addConstant(dest_ty, val);
             },
             else => {},
         },
         .ComptimeFloat, .Float => switch (src_zig_tag) {
             .ComptimeFloat => {
-                const result_val = try val.floatCast(sema.arena, dest_type);
-                return try sema.addConstant(dest_type, result_val);
+                const result_val = try val.floatCast(sema.arena, dest_ty);
+                return try sema.addConstant(dest_ty, result_val);
             },
             .Float => {
-                const result_val = try val.floatCast(sema.arena, dest_type);
-                if (!val.eql(result_val, dest_type)) {
+                const result_val = try val.floatCast(sema.arena, dest_ty);
+                if (!val.eql(result_val, dest_ty)) {
                     return sema.fail(
                         block,
                         inst_src,
                         "type {} cannot represent float value {}",
-                        .{ dest_type, val },
+                        .{ dest_ty, val },
                     );
                 }
-                return try sema.addConstant(dest_type, result_val);
+                return try sema.addConstant(dest_ty, result_val);
             },
             .Int, .ComptimeInt => {
-                const result_val = try val.intToFloat(sema.arena, dest_type, target);
+                const result_val = try val.intToFloat(sema.arena, dest_ty, target);
                 // TODO implement this compile error
                 //const int_again_val = try result_val.floatToInt(sema.arena, inst_ty);
                 //if (!int_again_val.eql(val, inst_ty)) {
@@ -11603,10 +11622,10 @@ fn coerceNum(
                 //        block,
                 //        inst_src,
                 //        "type {} cannot represent integer value {}",
-                //        .{ dest_type, val },
+                //        .{ dest_ty, val },
                 //    );
                 //}
-                return try sema.addConstant(dest_type, result_val);
+                return try sema.addConstant(dest_ty, result_val);
             },
             else => {},
         },
@@ -11816,31 +11835,66 @@ fn beginComptimePtrMutation(
         .field_ptr => {
             const field_ptr = ptr_val.castTag(.field_ptr).?.data;
             var parent = try beginComptimePtrMutation(sema, block, src, field_ptr.container_ptr);
-            const field_ty = parent.ty.structFieldType(field_ptr.field_index);
+            const field_index = @intCast(u32, field_ptr.field_index);
+            const field_ty = parent.ty.structFieldType(field_index);
             switch (parent.val.tag()) {
                 .undef => {
-                    // A struct has been initialized to undefined at comptime and now we
+                    // A struct or union has been initialized to undefined at comptime and now we
                     // are for the first time setting a field. We must change the representation
-                    // of the struct from `undef` to `struct`.
+                    // of the struct/union from `undef` to `struct`/`union`.
                     const arena = parent.beginArena(sema.gpa);
                     defer parent.finishArena();
 
-                    const fields = try arena.alloc(Value, parent.ty.structFieldCount());
-                    mem.set(Value, fields, Value.undef);
+                    switch (parent.ty.zigTypeTag()) {
+                        .Struct => {
+                            const fields = try arena.alloc(Value, parent.ty.structFieldCount());
+                            mem.set(Value, fields, Value.undef);
 
-                    parent.val.* = try Value.Tag.@"struct".create(arena, fields);
+                            parent.val.* = try Value.Tag.@"struct".create(arena, fields);
 
-                    return ComptimePtrMutationKit{
-                        .decl_ref_mut = parent.decl_ref_mut,
-                        .val = &fields[field_ptr.field_index],
-                        .ty = field_ty,
-                    };
+                            return ComptimePtrMutationKit{
+                                .decl_ref_mut = parent.decl_ref_mut,
+                                .val = &fields[field_index],
+                                .ty = field_ty,
+                            };
+                        },
+                        .Union => {
+                            const payload = try arena.create(Value.Payload.Union);
+                            payload.* = .{ .data = .{
+                                .tag = try Value.Tag.enum_field_index.create(arena, field_index),
+                                .val = Value.undef,
+                            } };
+
+                            parent.val.* = Value.initPayload(&payload.base);
+
+                            return ComptimePtrMutationKit{
+                                .decl_ref_mut = parent.decl_ref_mut,
+                                .val = &payload.data.val,
+                                .ty = field_ty,
+                            };
+                        },
+                        else => unreachable,
+                    }
                 },
                 .@"struct" => return ComptimePtrMutationKit{
                     .decl_ref_mut = parent.decl_ref_mut,
-                    .val = &parent.val.castTag(.@"struct").?.data[field_ptr.field_index],
+                    .val = &parent.val.castTag(.@"struct").?.data[field_index],
                     .ty = field_ty,
                 },
+                .@"union" => {
+                    // We need to set the active field of the union.
+                    const arena = parent.beginArena(sema.gpa);
+                    defer parent.finishArena();
+
+                    const payload = &parent.val.castTag(.@"union").?.data;
+                    payload.tag = try Value.Tag.enum_field_index.create(arena, field_index);
+
+                    return ComptimePtrMutationKit{
+                        .decl_ref_mut = parent.decl_ref_mut,
+                        .val = &payload.val,
+                        .ty = field_ty,
+                    };
+                },
 
                 else => unreachable,
             }
@@ -11855,7 +11909,7 @@ fn beginComptimePtrMutation(
 fn bitCast(
     sema: *Sema,
     block: *Block,
-    dest_type: Type,
+    dest_ty: Type,
     inst: Air.Inst.Ref,
     inst_src: LazySrcLoc,
 ) CompileError!Air.Inst.Ref {
@@ -11863,41 +11917,132 @@ fn bitCast(
     if (try sema.resolveMaybeUndefVal(block, inst_src, inst)) |val| {
         const target = sema.mod.getTarget();
         const old_ty = sema.typeOf(inst);
-        const result_val = try val.bitCast(old_ty, dest_type, target, sema.gpa, sema.arena);
-        return sema.addConstant(dest_type, result_val);
+        const result_val = try val.bitCast(old_ty, dest_ty, target, sema.gpa, sema.arena);
+        return sema.addConstant(dest_ty, result_val);
     }
     try sema.requireRuntimeBlock(block, inst_src);
-    return block.addTyOp(.bitcast, dest_type, inst);
+    return block.addTyOp(.bitcast, dest_ty, inst);
 }
 
 fn coerceArrayPtrToSlice(
     sema: *Sema,
     block: *Block,
-    dest_type: Type,
+    dest_ty: Type,
     inst: Air.Inst.Ref,
     inst_src: LazySrcLoc,
 ) CompileError!Air.Inst.Ref {
     if (try sema.resolveDefinedValue(block, inst_src, inst)) |val| {
         // The comptime Value representation is compatible with both types.
-        return sema.addConstant(dest_type, val);
+        return sema.addConstant(dest_ty, val);
     }
     try sema.requireRuntimeBlock(block, inst_src);
-    return block.addTyOp(.array_to_slice, dest_type, inst);
+    return block.addTyOp(.array_to_slice, dest_ty, inst);
 }
 
 fn coerceArrayPtrToMany(
     sema: *Sema,
     block: *Block,
-    dest_type: Type,
+    dest_ty: Type,
     inst: Air.Inst.Ref,
     inst_src: LazySrcLoc,
 ) !Air.Inst.Ref {
     if (try sema.resolveDefinedValue(block, inst_src, inst)) |val| {
         // The comptime Value representation is compatible with both types.
-        return sema.addConstant(dest_type, val);
+        return sema.addConstant(dest_ty, val);
     }
     try sema.requireRuntimeBlock(block, inst_src);
-    return sema.bitCast(block, dest_type, inst, inst_src);
+    return sema.bitCast(block, dest_ty, inst, inst_src);
+}
+
+fn coerceEnumToUnion(
+    sema: *Sema,
+    block: *Block,
+    union_ty: Type,
+    union_ty_src: LazySrcLoc,
+    inst: Air.Inst.Ref,
+    inst_src: LazySrcLoc,
+) !Air.Inst.Ref {
+    const inst_ty = sema.typeOf(inst);
+
+    const tag_ty = union_ty.unionTagType() orelse {
+        const msg = msg: {
+            const msg = try sema.errMsg(block, inst_src, "expected {}, found {}", .{
+                union_ty, inst_ty,
+            });
+            errdefer msg.destroy(sema.gpa);
+            try sema.errNote(block, union_ty_src, msg, "cannot coerce enum to untagged union", .{});
+            try sema.addDeclaredHereNote(msg, union_ty);
+            break :msg msg;
+        };
+        return sema.failWithOwnedErrorMsg(msg);
+    };
+
+    const enum_tag = try sema.coerce(block, tag_ty, inst, inst_src);
+    if (try sema.resolveDefinedValue(block, inst_src, enum_tag)) |val| {
+        const union_obj = union_ty.cast(Type.Payload.Union).?.data;
+        const field_index = union_obj.tag_ty.enumTagFieldIndex(val) orelse {
+            const msg = msg: {
+                const msg = try sema.errMsg(block, inst_src, "union {} has no tag with value {}", .{
+                    union_ty, val,
+                });
+                errdefer msg.destroy(sema.gpa);
+                try sema.addDeclaredHereNote(msg, union_ty);
+                break :msg msg;
+            };
+            return sema.failWithOwnedErrorMsg(msg);
+        };
+        const field = union_obj.fields.values()[field_index];
+        const field_ty = try sema.resolveTypeFields(block, inst_src, field.ty);
+        const opv = (try sema.typeHasOnePossibleValue(block, inst_src, field_ty)) orelse {
+            // TODO resolve the field names and include in the error message,
+            // also instead of 'union declared here' make it 'field "foo" declared here'.
+            const msg = msg: {
+                const msg = try sema.errMsg(block, inst_src, "coercion to union {} must initialize {} field", .{
+                    union_ty, field_ty,
+                });
+                errdefer msg.destroy(sema.gpa);
+                try sema.addDeclaredHereNote(msg, union_ty);
+                break :msg msg;
+            };
+            return sema.failWithOwnedErrorMsg(msg);
+        };
+
+        return sema.addConstant(union_ty, try Value.Tag.@"union".create(sema.arena, .{
+            .tag = val,
+            .val = opv,
+        }));
+    }
+
+    try sema.requireRuntimeBlock(block, inst_src);
+
+    if (tag_ty.isNonexhaustiveEnum()) {
+        const msg = msg: {
+            const msg = try sema.errMsg(block, inst_src, "runtime coercion to union {} from non-exhaustive enum", .{
+                union_ty,
+            });
+            errdefer msg.destroy(sema.gpa);
+            try sema.addDeclaredHereNote(msg, tag_ty);
+            break :msg msg;
+        };
+        return sema.failWithOwnedErrorMsg(msg);
+    }
+
+    // If the union has all fields 0 bits, the union value is just the enum value.
+    if (union_ty.unionHasAllZeroBitFieldTypes()) {
+        return block.addTyOp(.bitcast, union_ty, enum_tag);
+    }
+
+    // TODO resolve the field names and add a hint that says "field 'foo' has type 'bar'"
+    // instead of the "union declared here" hint
+    const msg = msg: {
+        const msg = try sema.errMsg(block, inst_src, "runtime coercion to union {} which has non-void fields", .{
+            union_ty,
+        });
+        errdefer msg.destroy(sema.gpa);
+        try sema.addDeclaredHereNote(msg, union_ty);
+        break :msg msg;
+    };
+    return sema.failWithOwnedErrorMsg(msg);
 }
 
 fn analyzeDeclVal(
@@ -12223,7 +12368,7 @@ fn cmpNumeric(
     const target = sema.mod.getTarget();
     if (lhs_is_float and rhs_is_float) {
         // Implicit cast the smaller one to the larger one.
-        const dest_type = x: {
+        const dest_ty = x: {
             if (lhs_ty_tag == .ComptimeFloat) {
                 break :x rhs_ty;
             } else if (rhs_ty_tag == .ComptimeFloat) {
@@ -12235,8 +12380,8 @@ fn cmpNumeric(
                 break :x rhs_ty;
             }
         };
-        const casted_lhs = try sema.coerce(block, dest_type, lhs, lhs_src);
-        const casted_rhs = try sema.coerce(block, dest_type, rhs, rhs_src);
+        const casted_lhs = try sema.coerce(block, dest_ty, lhs, lhs_src);
+        const casted_rhs = try sema.coerce(block, dest_ty, rhs, rhs_src);
         return block.addBinOp(Air.Inst.Tag.fromCmpOp(op), casted_lhs, casted_rhs);
     }
     // For mixed unsigned integer sizes, implicit cast both operands to the larger integer.
@@ -12327,7 +12472,7 @@ fn cmpNumeric(
         rhs_bits = int_info.bits + @boolToInt(int_info.signedness == .unsigned and dest_int_is_signed);
     }
 
-    const dest_type = if (dest_float_type) |ft| ft else blk: {
+    const dest_ty = if (dest_float_type) |ft| ft else blk: {
         const max_bits = std.math.max(lhs_bits, rhs_bits);
         const casted_bits = std.math.cast(u16, max_bits) catch |err| switch (err) {
             error.Overflow => return sema.fail(block, src, "{d} exceeds maximum integer bit count", .{max_bits}),
@@ -12335,8 +12480,8 @@ fn cmpNumeric(
         const signedness: std.builtin.Signedness = if (dest_int_is_signed) .signed else .unsigned;
         break :blk try Module.makeIntType(sema.arena, signedness, casted_bits);
     };
-    const casted_lhs = try sema.coerce(block, dest_type, lhs, lhs_src);
-    const casted_rhs = try sema.coerce(block, dest_type, rhs, rhs_src);
+    const casted_lhs = try sema.coerce(block, dest_ty, lhs, lhs_src);
+    const casted_rhs = try sema.coerce(block, dest_ty, rhs, rhs_src);
 
     return block.addBinOp(Air.Inst.Tag.fromCmpOp(op), casted_lhs, casted_rhs);
 }
@@ -12344,32 +12489,32 @@ fn cmpNumeric(
 fn wrapOptional(
     sema: *Sema,
     block: *Block,
-    dest_type: Type,
+    dest_ty: Type,
     inst: Air.Inst.Ref,
     inst_src: LazySrcLoc,
 ) !Air.Inst.Ref {
     if (try sema.resolveMaybeUndefVal(block, inst_src, inst)) |val| {
-        return sema.addConstant(dest_type, try Value.Tag.opt_payload.create(sema.arena, val));
+        return sema.addConstant(dest_ty, try Value.Tag.opt_payload.create(sema.arena, val));
     }
 
     try sema.requireRuntimeBlock(block, inst_src);
-    return block.addTyOp(.wrap_optional, dest_type, inst);
+    return block.addTyOp(.wrap_optional, dest_ty, inst);
 }
 
 fn wrapErrorUnion(
     sema: *Sema,
     block: *Block,
-    dest_type: Type,
+    dest_ty: Type,
     inst: Air.Inst.Ref,
     inst_src: LazySrcLoc,
 ) !Air.Inst.Ref {
     const inst_ty = sema.typeOf(inst);
-    const dest_err_set_ty = dest_type.errorUnionSet();
-    const dest_payload_ty = dest_type.errorUnionPayload();
+    const dest_err_set_ty = dest_ty.errorUnionSet();
+    const dest_payload_ty = dest_ty.errorUnionPayload();
     if (try sema.resolveMaybeUndefVal(block, inst_src, inst)) |val| {
         if (inst_ty.zigTypeTag() != .ErrorSet) {
             _ = try sema.coerce(block, dest_payload_ty, inst, inst_src);
-            return sema.addConstant(dest_type, try Value.Tag.eu_payload.create(sema.arena, val));
+            return sema.addConstant(dest_ty, try Value.Tag.eu_payload.create(sema.arena, val));
         }
         switch (dest_err_set_ty.tag()) {
             .anyerror => {},
@@ -12417,7 +12562,7 @@ fn wrapErrorUnion(
             },
             else => unreachable,
         }
-        return sema.addConstant(dest_type, val);
+        return sema.addConstant(dest_ty, val);
     }
 
     try sema.requireRuntimeBlock(block, inst_src);
@@ -12425,25 +12570,25 @@ fn wrapErrorUnion(
     // we are coercing from E to E!T
     if (inst_ty.zigTypeTag() == .ErrorSet) {
         var coerced = try sema.coerce(block, dest_err_set_ty, inst, inst_src);
-        return block.addTyOp(.wrap_errunion_err, dest_type, coerced);
+        return block.addTyOp(.wrap_errunion_err, dest_ty, coerced);
     } else {
         var coerced = try sema.coerce(block, dest_payload_ty, inst, inst_src);
-        return block.addTyOp(.wrap_errunion_payload, dest_type, coerced);
+        return block.addTyOp(.wrap_errunion_payload, dest_ty, coerced);
     }
 }
 
 fn unionToTag(
     sema: *Sema,
     block: *Block,
-    dest_type: Type,
+    dest_ty: Type,
     un: Air.Inst.Ref,
     un_src: LazySrcLoc,
 ) !Air.Inst.Ref {
     if (try sema.resolveMaybeUndefVal(block, un_src, un)) |un_val| {
-        return sema.addConstant(dest_type, un_val.unionTag());
+        return sema.addConstant(dest_ty, un_val.unionTag());
     }
     try sema.requireRuntimeBlock(block, un_src);
-    return block.addTyOp(.get_union_tag, dest_type, un);
+    return block.addTyOp(.get_union_tag, dest_ty, un);
 }
 
 fn resolvePeerTypes(
src/type.zig
@@ -1238,7 +1238,6 @@ pub const Type = extern union {
             .fn_void_no_args,
             .fn_naked_noreturn_no_args,
             .fn_ccc_void_no_args,
-            .single_const_pointer_to_comptime_int,
             .const_slice_u8,
             .anyerror_void_error_union,
             .empty_struct_literal,
@@ -1249,8 +1248,14 @@ pub const Type = extern union {
             .error_set_inferred,
             .@"opaque",
             .generic_poison,
+            .array_u8,
+            .array_u8_sentinel_0,
+            .int_signed,
+            .int_unsigned,
+            .enum_simple,
             => false,
 
+            .single_const_pointer_to_comptime_int,
             .type,
             .comptime_int,
             .comptime_float,
@@ -1263,8 +1268,6 @@ pub const Type = extern union {
             .inferred_alloc_const => unreachable,
             .bound_fn => unreachable,
 
-            .array_u8,
-            .array_u8_sentinel_0,
             .array,
             .array_sentinel,
             .vector,
@@ -1277,17 +1280,21 @@ pub const Type = extern union {
             .c_mut_pointer,
             .const_slice,
             .mut_slice,
-            .int_signed,
-            .int_unsigned,
+            => return requiresComptime(childType(ty)),
+
             .optional,
             .optional_single_mut_pointer,
             .optional_single_const_pointer,
+            => {
+                var buf: Payload.ElemType = undefined;
+                return requiresComptime(optionalChild(ty, &buf));
+            },
+
             .error_union,
             .anyframe_T,
             .@"struct",
             .@"union",
             .union_tagged,
-            .enum_simple,
             .enum_numbered,
             .enum_full,
             .enum_nonexhaustive,
@@ -2568,6 +2575,24 @@ pub const Type = extern union {
         return union_obj.fields.values()[index].ty;
     }
 
+    pub fn unionHasAllZeroBitFieldTypes(ty: Type) bool {
+        return ty.cast(Payload.Union).?.data.hasAllZeroBitFieldTypes();
+    }
+
+    pub fn unionGetLayout(ty: Type, target: Target) Module.Union.Layout {
+        switch (ty.tag()) {
+            .@"union" => {
+                const union_obj = ty.castTag(.@"union").?.data;
+                return union_obj.getLayout(target, false);
+            },
+            .union_tagged => {
+                const union_obj = ty.castTag(.union_tagged).?.data;
+                return union_obj.getLayout(target, true);
+            },
+            else => unreachable,
+        }
+    }
+
     /// Asserts that the type is an error union.
     pub fn errorUnionPayload(self: Type) Type {
         return switch (self.tag()) {
@@ -3361,17 +3386,26 @@ pub const Type = extern union {
         }
     }
 
+    /// Supports structs and unions.
     pub fn structFieldType(ty: Type, index: usize) Type {
         switch (ty.tag()) {
             .@"struct" => {
                 const struct_obj = ty.castTag(.@"struct").?.data;
                 return struct_obj.fields.values()[index].ty;
             },
+            .@"union", .union_tagged => {
+                const union_obj = ty.cast(Payload.Union).?.data;
+                return union_obj.fields.values()[index].ty;
+            },
             else => unreachable,
         }
     }
 
     pub fn declSrcLoc(ty: Type) Module.SrcLoc {
+        return declSrcLocOrNull(ty).?;
+    }
+
+    pub fn declSrcLocOrNull(ty: Type) ?Module.SrcLoc {
         switch (ty.tag()) {
             .enum_full, .enum_nonexhaustive => {
                 const enum_full = ty.cast(Payload.EnumFull).?.data;
@@ -3404,8 +3438,9 @@ pub const Type = extern union {
             .export_options,
             .extern_options,
             .type_info,
-            => @panic("TODO resolve std.builtin types"),
-            else => unreachable,
+            => unreachable, // needed to call resolveTypeFields first
+
+            else => return null,
         }
     }
 
test/behavior/union.zig
@@ -32,3 +32,42 @@ fn setFloat(foo: *Foo, x: f64) void {
 fn setInt(foo: *Foo, x: i32) void {
     foo.* = Foo{ .int = x };
 }
+
+test "comptime union field access" {
+    comptime {
+        var foo = Foo{ .int = 0 };
+        try expect(foo.int == 0);
+
+        foo = Foo{ .float = 42.42 };
+        try expect(foo.float == 42.42);
+    }
+}
+
+const FooExtern = extern union {
+    float: f64,
+    int: i32,
+};
+
+test "basic extern unions" {
+    var foo = FooExtern{ .int = 1 };
+    try expect(foo.int == 1);
+    foo.float = 12.34;
+    try expect(foo.float == 12.34);
+}
+
+const ExternPtrOrInt = extern union {
+    ptr: *u8,
+    int: u64,
+};
+test "extern union size" {
+    comptime try expect(@sizeOf(ExternPtrOrInt) == 8);
+}
+
+test "0-sized extern union definition" {
+    const U = extern union {
+        a: void,
+        const f = 1;
+    };
+
+    try expect(U.f == 1);
+}
test/behavior/union_stage1.zig
@@ -34,33 +34,6 @@ test "unions embedded in aggregate types" {
     }
 }
 
-const Foo = union {
-    float: f64,
-    int: i32,
-};
-
-test "comptime union field access" {
-    comptime {
-        var foo = Foo{ .int = 0 };
-        try expect(foo.int == 0);
-
-        foo = Foo{ .float = 42.42 };
-        try expect(foo.float == 42.42);
-    }
-}
-
-const FooExtern = extern union {
-    float: f64,
-    int: i32,
-};
-
-test "basic extern unions" {
-    var foo = FooExtern{ .int = 1 };
-    try expect(foo.int == 1);
-    foo.float = 12.34;
-    try expect(foo.float == 12.34);
-}
-
 const Letter = enum { A, B, C };
 const Payload = union(Letter) {
     A: i32,
@@ -131,19 +104,11 @@ fn testEnumWithSpecifiedAndUnspecifiedTagValues(x: MultipleChoice2) !void {
     });
 }
 
-const ExternPtrOrInt = extern union {
-    ptr: *u8,
-    int: u64,
-};
-test "extern union size" {
-    comptime try expect(@sizeOf(ExternPtrOrInt) == 8);
-}
-
 const PackedPtrOrInt = packed union {
     ptr: *u8,
     int: u64,
 };
-test "extern union size" {
+test "packed union size" {
     comptime try expect(@sizeOf(PackedPtrOrInt) == 8);
 }
 
@@ -576,15 +541,6 @@ test "function call result coerces from tagged union to the tag" {
     comptime try S.doTheTest();
 }
 
-test "0-sized extern union definition" {
-    const U = extern union {
-        a: void,
-        const f = 1;
-    };
-
-    try expect(U.f == 1);
-}
-
 test "union initializer generates padding only if needed" {
     const U = union(enum) {
         A: u24,
@@ -769,6 +725,7 @@ test "union enum type gets a separate scope" {
 
     try S.doTheTest();
 }
+
 test "anytype union field: issue #9233" {
     const Quux = union(enum) { bar: anytype };
     _ = Quux;
@@ -845,7 +802,7 @@ const TaggedUnionWithPayload = union(enum) {
     Full: i32,
 };
 
-test "enum alignment" {
+test "union alignment" {
     comptime {
         try expect(@alignOf(AlignTestTaggedUnion) >= @alignOf([9]u8));
         try expect(@alignOf(AlignTestTaggedUnion) >= @alignOf(u64));