Commit c0aa4a1a42

Andrew Kelley <andrew@ziglang.org>
2021-09-28 04:48:42
stage2: implement basic unions
* AIR instructions struct_field_ptr and related functions now are also emitted by the frontend for unions. Backends must inspect the type of the pointer operand to lower the instructions correctly. - These will be renamed to `agg_field_ptr` (short for "aggregate") in the future. * Introduce the new `set_union_tag` AIR instruction. * Introduce `Module.EnumNumbered` and associated `Type` methods. This is for enums which have no decls, but do have the possibility of overriding the integer tag type and tag values. * Sema: Implement support for union tag types in both the auto-generated and explicitly-provided cases, as well as explicitly provided enum tag values in union declarations. * LLVM backend: implement lowering union types, union field pointer instructions, and the new `set_union_tag` instruction.
1 parent 25266d0
src/codegen/c.zig
@@ -955,6 +955,7 @@ fn genBody(f: *Function, body: []const Air.Inst.Index) error{ AnalysisFail, OutO
             .atomic_load      => try airAtomicLoad(f, inst),
             .memset           => try airMemset(f, inst),
             .memcpy           => try airMemcpy(f, inst),
+            .set_union_tag    => try airSetUnionTag(f, inst),
 
             .int_to_float,
             .float_to_int,
@@ -2080,6 +2081,21 @@ fn airMemcpy(f: *Function, inst: Air.Inst.Index) !CValue {
     return CValue.none;
 }
 
+fn airSetUnionTag(f: *Function, inst: Air.Inst.Index) !CValue {
+    const bin_op = f.air.instructions.items(.data)[inst].bin_op;
+    const union_ptr = try f.resolveInst(bin_op.lhs);
+    const new_tag = try f.resolveInst(bin_op.rhs);
+    const writer = f.object.writer();
+
+    try writer.writeAll("*");
+    try f.writeCValue(writer, union_ptr);
+    try writer.writeAll(" = ");
+    try f.writeCValue(writer, new_tag);
+    try writer.writeAll(";\n");
+
+    return CValue.none;
+}
+
 fn toMemoryOrder(order: std.builtin.AtomicOrder) [:0]const u8 {
     return switch (order) {
         .Unordered => "memory_order_relaxed",
src/codegen/llvm.zig
@@ -735,7 +735,7 @@ pub const DeclGen = struct {
             },
             .Enum => {
                 var buffer: Type.Payload.Bits = undefined;
-                const int_ty = t.enumTagType(&buffer);
+                const int_ty = t.intTagType(&buffer);
                 const bit_count = int_ty.intInfo(self.module.getTarget()).bits;
                 return self.context.intType(bit_count);
             },
@@ -812,6 +812,29 @@ pub const DeclGen = struct {
                     .False,
                 );
             },
+            .Union => {
+                const union_obj = t.castTag(.@"union").?.data;
+                assert(union_obj.haveFieldTypes());
+
+                const enum_tag_ty = union_obj.tag_ty;
+                const enum_tag_llvm_ty = try self.llvmType(enum_tag_ty);
+
+                if (union_obj.onlyTagHasCodegenBits()) {
+                    return enum_tag_llvm_ty;
+                }
+
+                const target = self.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 self.llvmType(most_aligned_field.ty),
+                    enum_tag_llvm_ty,
+                };
+                return self.context.structType(&llvm_fields, llvm_fields.len, .False);
+            },
             .Fn => {
                 const ret_ty = try self.llvmType(t.fnReturnType());
                 const params_len = t.fnParamLen();
@@ -840,7 +863,6 @@ pub const DeclGen = struct {
 
             .BoundFn => @panic("TODO remove BoundFn from the language"),
 
-            .Union,
             .Opaque,
             .Frame,
             .AnyFrame,
@@ -1131,7 +1153,7 @@ pub const DeclGen = struct {
         var buffer: Type.Payload.Bits = undefined;
         const int_ty = switch (ty.zigTypeTag()) {
             .Int => ty,
-            .Enum => ty.enumTagType(&buffer),
+            .Enum => ty.intTagType(&buffer),
             .Float => {
                 if (!is_rmw_xchg) return null;
                 return dg.context.intType(@intCast(c_uint, ty.abiSize(target) * 8));
@@ -1281,6 +1303,7 @@ pub const FuncGen = struct {
                 .atomic_load    => try self.airAtomicLoad(inst),
                 .memset         => try self.airMemset(inst),
                 .memcpy         => try self.airMemcpy(inst),
+                .set_union_tag  => try self.airSetUnionTag(inst),
 
                 .atomic_store_unordered => try self.airAtomicStore(inst, .Unordered),
                 .atomic_store_monotonic => try self.airAtomicStore(inst, .Monotonic),
@@ -1381,7 +1404,7 @@ pub const FuncGen = struct {
         const int_ty = switch (operand_ty.zigTypeTag()) {
             .Enum => blk: {
                 var buffer: Type.Payload.Bits = undefined;
-                const int_ty = operand_ty.enumTagType(&buffer);
+                const int_ty = operand_ty.intTagType(&buffer);
                 break :blk int_ty;
             },
             .Int, .Bool, .Pointer, .ErrorSet => operand_ty,
@@ -1660,8 +1683,9 @@ pub const FuncGen = struct {
         const ty_pl = self.air.instructions.items(.data)[inst].ty_pl;
         const struct_field = self.air.extraData(Air.StructField, ty_pl.payload).data;
         const struct_ptr = try self.resolveInst(struct_field.struct_operand);
+        const struct_ptr_ty = self.air.typeOf(struct_field.struct_operand);
         const field_index = @intCast(c_uint, struct_field.field_index);
-        return self.builder.buildStructGEP(struct_ptr, field_index, "");
+        return self.fieldPtr(inst, struct_ptr, struct_ptr_ty, field_index);
     }
 
     fn airStructFieldPtrIndex(self: *FuncGen, inst: Air.Inst.Index, field_index: c_uint) !?*const llvm.Value {
@@ -1670,7 +1694,8 @@ pub const FuncGen = struct {
 
         const ty_op = self.air.instructions.items(.data)[inst].ty_op;
         const struct_ptr = try self.resolveInst(ty_op.operand);
-        return self.builder.buildStructGEP(struct_ptr, field_index, "");
+        const struct_ptr_ty = self.air.typeOf(ty_op.operand);
+        return self.fieldPtr(inst, struct_ptr, struct_ptr_ty, field_index);
     }
 
     fn airStructFieldVal(self: *FuncGen, inst: Air.Inst.Index) !?*const llvm.Value {
@@ -2521,6 +2546,49 @@ pub const FuncGen = struct {
         return null;
     }
 
+    fn airSetUnionTag(self: *FuncGen, inst: Air.Inst.Index) !?*const llvm.Value {
+        const bin_op = self.air.instructions.items(.data)[inst].bin_op;
+        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, "");
+
+        _ = self.builder.buildStore(new_tag, tag_field_ptr);
+        return null;
+    }
+
+    fn fieldPtr(
+        self: *FuncGen,
+        inst: Air.Inst.Index,
+        struct_ptr: *const llvm.Value,
+        struct_ptr_ty: Type,
+        field_index: c_uint,
+    ) !?*const llvm.Value {
+        const struct_ty = struct_ptr_ty.childType();
+        switch (struct_ty.zigTypeTag()) {
+            .Struct => return self.builder.buildStructGEP(struct_ptr, field_index, ""),
+            .Union => return self.unionFieldPtr(inst, struct_ptr, struct_ty, field_index),
+            else => unreachable,
+        }
+    }
+
+    fn unionFieldPtr(
+        self: *FuncGen,
+        inst: Air.Inst.Index,
+        union_ptr: *const llvm.Value,
+        union_ty: Type,
+        field_index: c_uint,
+    ) !?*const llvm.Value {
+        const union_obj = union_ty.cast(Type.Payload.Union).?.data;
+        const field = &union_obj.fields.values()[field_index];
+        const result_llvm_ty = try self.dg.llvmType(self.air.typeOfIndex(inst));
+        if (!field.ty.hasCodeGenBits()) {
+            return null;
+        }
+        const union_field_ptr = self.builder.buildStructGEP(union_ptr, 0, "");
+        return self.builder.buildBitCast(union_field_ptr, result_llvm_ty, "");
+    }
+
     fn getIntrinsic(self: *FuncGen, name: []const u8) *const llvm.Value {
         const id = llvm.lookupIntrinsicID(name.ptr, name.len);
         assert(id != 0);
src/Air.zig
@@ -270,19 +270,26 @@ pub const Inst = struct {
         /// wrap from E to E!T
         /// Uses the `ty_op` field.
         wrap_errunion_err,
-        /// Given a pointer to a struct and a field index, returns a pointer to the field.
+        /// Given a pointer to a struct or union and a field index, returns a pointer to the field.
         /// Uses the `ty_pl` field, payload is `StructField`.
+        /// TODO rename to `agg_field_ptr`.
         struct_field_ptr,
-        /// Given a pointer to a struct, returns a pointer to the field.
+        /// Given a pointer to a struct or union, returns a pointer to the field.
         /// The field index is the number at the end of the name.
         /// Uses `ty_op` field.
+        /// TODO rename to `agg_field_ptr_index_X`
         struct_field_ptr_index_0,
         struct_field_ptr_index_1,
         struct_field_ptr_index_2,
         struct_field_ptr_index_3,
-        /// Given a byval struct and a field index, returns the field byval.
+        /// Given a byval struct or union and a field index, returns the field byval.
         /// Uses the `ty_pl` field, payload is `StructField`.
+        /// TODO rename to `agg_field_val`
         struct_field_val,
+        /// Given a pointer to a tagged union, set its tag to the provided value.
+        /// Result type is always void.
+        /// Uses the `bin_op` field. LHS is union pointer, RHS is new tag value.
+        set_union_tag,
         /// Given a slice value, return the length.
         /// Result type is always usize.
         /// Uses the `ty_op` field.
@@ -643,6 +650,7 @@ pub fn typeOfIndex(air: Air, inst: Air.Inst.Index) Type {
         .atomic_store_seq_cst,
         .memset,
         .memcpy,
+        .set_union_tag,
         => return Type.initTag(.void),
 
         .ptrtoint,
src/codegen.zig
@@ -889,6 +889,7 @@ fn Function(comptime arch: std.Target.Cpu.Arch) type {
                     .atomic_load     => try self.airAtomicLoad(inst),
                     .memcpy          => try self.airMemcpy(inst),
                     .memset          => try self.airMemset(inst),
+                    .set_union_tag   => try self.airSetUnionTag(inst),
 
                     .atomic_store_unordered => try self.airAtomicStore(inst, .Unordered),
                     .atomic_store_monotonic => try self.airAtomicStore(inst, .Monotonic),
@@ -1543,6 +1544,14 @@ fn Function(comptime arch: std.Target.Cpu.Arch) type {
             return self.finishAir(inst, result, .{ bin_op.lhs, bin_op.rhs, .none });
         }
 
+        fn airSetUnionTag(self: *Self, inst: Air.Inst.Index) !void {
+            const bin_op = self.air.instructions.items(.data)[inst].bin_op;
+            const result: MCValue = switch (arch) {
+                else => return self.fail("TODO implement airSetUnionTag for {}", .{self.target.cpu.arch}),
+            };
+            return self.finishAir(inst, result, .{ bin_op.lhs, bin_op.rhs, .none });
+        }
+
         fn reuseOperand(self: *Self, inst: Air.Inst.Index, operand: Air.Inst.Ref, op_index: Liveness.OperandInt, mcv: MCValue) bool {
             if (!self.liveness.operandDies(inst, op_index))
                 return false;
src/Liveness.zig
@@ -256,6 +256,7 @@ fn analyzeInst(
         .atomic_store_monotonic,
         .atomic_store_release,
         .atomic_store_seq_cst,
+        .set_union_tag,
         => {
             const o = inst_datas[inst].bin_op;
             return trackOperands(a, new_set, inst, main_tomb, .{ o.lhs, o.rhs, .none });
src/Module.zig
@@ -859,6 +859,36 @@ pub const EnumSimple = struct {
     }
 };
 
+/// Represents the data that an enum declaration provides, when there are no
+/// declarations. However an integer tag type is provided, and the enum tag values
+/// are explicitly provided.
+pub const EnumNumbered = struct {
+    /// The Decl that corresponds to the enum itself.
+    owner_decl: *Decl,
+    /// An integer type which is used for the numerical value of the enum.
+    /// Whether zig chooses this type or the user specifies it, it is stored here.
+    tag_ty: Type,
+    /// Set of field names in declaration order.
+    fields: NameMap,
+    /// Maps integer tag value to field index.
+    /// Entries are in declaration order, same as `fields`.
+    /// If this hash map is empty, it means the enum tags are auto-numbered.
+    values: ValueMap,
+    /// Offset from `owner_decl`, points to the enum decl AST node.
+    node_offset: i32,
+
+    pub const NameMap = EnumFull.NameMap;
+    pub const ValueMap = EnumFull.ValueMap;
+
+    pub fn srcLoc(self: EnumNumbered) SrcLoc {
+        return .{
+            .file_scope = self.owner_decl.getFileScope(),
+            .parent_decl_node = self.owner_decl.src_node,
+            .lazy = .{ .node_offset = self.node_offset },
+        };
+    }
+};
+
 /// Represents the data that an enum declaration provides, when there is
 /// at least one tag value explicitly specified, or at least one declaration.
 pub const EnumFull = struct {
@@ -868,16 +898,17 @@ pub const EnumFull = struct {
     /// Whether zig chooses this type or the user specifies it, it is stored here.
     tag_ty: Type,
     /// Set of field names in declaration order.
-    fields: std.StringArrayHashMapUnmanaged(void),
+    fields: NameMap,
     /// Maps integer tag value to field index.
     /// Entries are in declaration order, same as `fields`.
     /// If this hash map is empty, it means the enum tags are auto-numbered.
     values: ValueMap,
-    /// Represents the declarations inside this struct.
+    /// Represents the declarations inside this enum.
     namespace: Scope.Namespace,
     /// Offset from `owner_decl`, points to the enum decl AST node.
     node_offset: i32,
 
+    pub const NameMap = std.StringArrayHashMapUnmanaged(void);
     pub const ValueMap = std.ArrayHashMapUnmanaged(Value, void, Value.ArrayHashContext, false);
 
     pub fn srcLoc(self: EnumFull) SrcLoc {
@@ -933,6 +964,44 @@ pub const Union = struct {
             .lazy = .{ .node_offset = self.node_offset },
         };
     }
+
+    pub fn haveFieldTypes(u: Union) bool {
+        return switch (u.status) {
+            .none,
+            .field_types_wip,
+            => false,
+            .have_field_types,
+            .layout_wip,
+            .have_layout,
+            => true,
+        };
+    }
+
+    pub fn onlyTagHasCodegenBits(u: Union) bool {
+        assert(u.haveFieldTypes());
+        for (u.fields.values()) |field| {
+            if (field.ty.hasCodeGenBits()) return false;
+        }
+        return true;
+    }
+
+    pub fn mostAlignedField(u: Union, target: Target) u32 {
+        assert(u.haveFieldTypes());
+        var most_alignment: u64 = 0;
+        var most_index: usize = undefined;
+        for (u.fields.values()) |field, i| {
+            if (!field.ty.hasCodeGenBits()) continue;
+            const field_align = if (field.abi_align.tag() == .abi_align_default)
+                field.ty.abiAlignment(target)
+            else
+                field.abi_align.toUnsignedInt();
+            if (field_align > most_alignment) {
+                most_alignment = field_align;
+                most_index = i;
+            }
+        }
+        return @intCast(u32, most_index);
+    }
 };
 
 /// Some Fn struct memory is owned by the Decl's TypedValue.Managed arena allocator.
@@ -1543,6 +1612,40 @@ pub const Scope = struct {
             });
         }
 
+        pub fn addStructFieldPtr(
+            block: *Block,
+            struct_ptr: Air.Inst.Ref,
+            field_index: u32,
+            ptr_field_ty: Type,
+        ) !Air.Inst.Ref {
+            const ty = try block.sema.addType(ptr_field_ty);
+            const tag: Air.Inst.Tag = switch (field_index) {
+                0 => .struct_field_ptr_index_0,
+                1 => .struct_field_ptr_index_1,
+                2 => .struct_field_ptr_index_2,
+                3 => .struct_field_ptr_index_3,
+                else => {
+                    return block.addInst(.{
+                        .tag = .struct_field_ptr,
+                        .data = .{ .ty_pl = .{
+                            .ty = ty,
+                            .payload = try block.sema.addExtra(Air.StructField{
+                                .struct_operand = struct_ptr,
+                                .field_index = @intCast(u32, field_index),
+                            }),
+                        } },
+                    });
+                },
+            };
+            return block.addInst(.{
+                .tag = tag,
+                .data = .{ .ty_op = .{
+                    .ty = ty,
+                    .operand = struct_ptr,
+                } },
+            });
+        }
+
         pub fn addInst(block: *Block, inst: Air.Inst) error{OutOfMemory}!Air.Inst.Ref {
             return Air.indexToRef(try block.addInstAsIndex(inst));
         }
src/print_air.zig
@@ -130,6 +130,7 @@ const Writer = struct {
             .ptr_ptr_elem_val,
             .shl,
             .shr,
+            .set_union_tag,
             => try w.writeBinOp(s, inst),
 
             .is_null,
src/Sema.zig
@@ -1625,7 +1625,7 @@ fn zirAllocMut(sema: *Sema, block: *Scope.Block, inst: Zir.Inst.Index) CompileEr
     if (block.is_comptime) {
         return sema.analyzeComptimeAlloc(block, var_type);
     }
-    try sema.validateVarType(block, ty_src, var_type);
+    try sema.validateVarType(block, ty_src, var_type, false);
     const ptr_type = try Type.ptr(sema.arena, .{
         .pointee_type = var_type,
         .@"addrspace" = target_util.defaultAddressSpace(sema.mod.getTarget(), .local),
@@ -1711,7 +1711,7 @@ fn zirResolveInferredAlloc(sema: *Sema, block: *Scope.Block, inst: Zir.Inst.Inde
         const peer_inst_list = inferred_alloc.data.stored_inst_list.items;
         const final_elem_ty = try sema.resolvePeerTypes(block, ty_src, peer_inst_list, .none);
         if (var_is_mut) {
-            try sema.validateVarType(block, ty_src, final_elem_ty);
+            try sema.validateVarType(block, ty_src, final_elem_ty, false);
         }
         // Change it to a normal alloc.
         const final_ptr_ty = try Type.ptr(sema.arena, .{
@@ -1730,19 +1730,82 @@ fn zirValidateStructInitPtr(sema: *Sema, block: *Scope.Block, inst: Zir.Inst.Ind
     const tracy = trace(@src());
     defer tracy.end();
 
-    const gpa = sema.gpa;
-    const mod = sema.mod;
     const validate_inst = sema.code.instructions.items(.data)[inst].pl_node;
-    const struct_init_src = validate_inst.src();
+    const init_src = validate_inst.src();
     const validate_extra = sema.code.extraData(Zir.Inst.Block, validate_inst.payload_index);
     const instrs = sema.code.extra[validate_extra.end..][0..validate_extra.data.body_len];
+    const field_ptr_data = sema.code.instructions.items(.data)[instrs[0]].pl_node;
+    const field_ptr_extra = sema.code.extraData(Zir.Inst.Field, field_ptr_data.payload_index).data;
+    const object_ptr = sema.resolveInst(field_ptr_extra.lhs);
+    const agg_ty = sema.typeOf(object_ptr).elemType();
+    switch (agg_ty.zigTypeTag()) {
+        .Struct => return sema.validateStructInitPtr(
+            block,
+            agg_ty.castTag(.@"struct").?.data,
+            init_src,
+            instrs,
+        ),
+        .Union => return sema.validateUnionInitPtr(
+            block,
+            agg_ty.cast(Type.Payload.Union).?.data,
+            init_src,
+            instrs,
+            object_ptr,
+        ),
+        else => unreachable,
+    }
+}
 
-    const struct_obj: *Module.Struct = s: {
-        const field_ptr_data = sema.code.instructions.items(.data)[instrs[0]].pl_node;
-        const field_ptr_extra = sema.code.extraData(Zir.Inst.Field, field_ptr_data.payload_index).data;
-        const object_ptr = sema.resolveInst(field_ptr_extra.lhs);
-        break :s sema.typeOf(object_ptr).elemType().castTag(.@"struct").?.data;
-    };
+fn validateUnionInitPtr(
+    sema: *Sema,
+    block: *Scope.Block,
+    union_obj: *Module.Union,
+    init_src: LazySrcLoc,
+    instrs: []const Zir.Inst.Index,
+    union_ptr: Air.Inst.Ref,
+) CompileError!void {
+    const mod = sema.mod;
+
+    if (instrs.len != 1) {
+        // TODO add note for other field
+        // TODO add note for union declared here
+        return mod.fail(&block.base, init_src, "only one union field can be active at once", .{});
+    }
+
+    const field_ptr = instrs[0];
+    const field_ptr_data = sema.code.instructions.items(.data)[field_ptr].pl_node;
+    const field_src: LazySrcLoc = .{ .node_offset_back2tok = field_ptr_data.src_node };
+    const field_ptr_extra = sema.code.extraData(Zir.Inst.Field, field_ptr_data.payload_index).data;
+    const field_name = sema.code.nullTerminatedString(field_ptr_extra.field_name_start);
+    const field_index_big = union_obj.fields.getIndex(field_name) orelse
+        return sema.failWithBadUnionFieldAccess(block, union_obj, field_src, field_name);
+    const field_index = @intCast(u32, field_index_big);
+
+    // TODO here we need to go back and see if we need to convert the union
+    // to a comptime-known value. This will involve editing the AIR code we have
+    // generated so far - in particular deleting some runtime pointer bitcast
+    // instructions which are not actually needed if the initialization expression
+    // ends up being comptime-known.
+
+    // Otherwise, we set the new union tag now.
+    const new_tag = try sema.addConstant(
+        union_obj.tag_ty,
+        try Value.Tag.enum_field_index.create(sema.arena, field_index),
+    );
+
+    try sema.requireRuntimeBlock(block, init_src);
+    _ = try block.addBinOp(.set_union_tag, union_ptr, new_tag);
+}
+
+fn validateStructInitPtr(
+    sema: *Sema,
+    block: *Scope.Block,
+    struct_obj: *Module.Struct,
+    init_src: LazySrcLoc,
+    instrs: []const Zir.Inst.Index,
+) CompileError!void {
+    const gpa = sema.gpa;
+    const mod = sema.mod;
 
     // Maps field index to field_ptr index of where it was already initialized.
     const found_fields = try gpa.alloc(Zir.Inst.Index, struct_obj.fields.count());
@@ -1781,9 +1844,9 @@ fn zirValidateStructInitPtr(sema: *Sema, block: *Scope.Block, inst: Zir.Inst.Ind
         const template = "missing struct field: {s}";
         const args = .{field_name};
         if (root_msg) |msg| {
-            try mod.errNote(&block.base, struct_init_src, msg, template, args);
+            try mod.errNote(&block.base, init_src, msg, template, args);
         } else {
-            root_msg = try mod.errMsg(&block.base, struct_init_src, template, args);
+            root_msg = try mod.errMsg(&block.base, init_src, template, args);
         }
     }
     if (root_msg) |msg| {
@@ -8037,7 +8100,7 @@ fn checkAtomicOperandType(
     const max_atomic_bits = target_util.largestAtomicBits(target);
     const int_ty = switch (ty.zigTypeTag()) {
         .Int => ty,
-        .Enum => ty.enumTagType(&buffer),
+        .Enum => ty.intTagType(&buffer),
         .Float => {
             const bit_count = ty.floatBits(target);
             if (bit_count > max_atomic_bits) {
@@ -8621,11 +8684,7 @@ fn zirVarExtended(
             return sema.failWithNeededComptime(block, init_src);
     } else Value.initTag(.unreachable_value);
 
-    if (!var_ty.isValidVarType(small.is_extern)) {
-        return sema.mod.fail(&block.base, mut_src, "variable of type '{}' must be const", .{
-            var_ty,
-        });
-    }
+    try sema.validateVarType(block, mut_src, var_ty, small.is_extern);
 
     if (lib_name != null) {
         // Look at the sema code for functions which has this logic, it just needs to
@@ -8810,9 +8869,54 @@ fn requireIntegerType(sema: *Sema, block: *Scope.Block, src: LazySrcLoc, ty: Typ
     }
 }
 
-fn validateVarType(sema: *Sema, block: *Scope.Block, src: LazySrcLoc, ty: Type) !void {
-    if (!ty.isValidVarType(false)) {
-        return sema.mod.fail(&block.base, src, "variable of type '{}' must be const or comptime", .{ty});
+/// Emit a compile error if type cannot be used for a runtime variable.
+fn validateVarType(
+    sema: *Sema,
+    block: *Scope.Block,
+    src: LazySrcLoc,
+    var_ty: Type,
+    is_extern: bool,
+) CompileError!void {
+    var ty = var_ty;
+    const ok: bool = while (true) switch (ty.zigTypeTag()) {
+        .Bool,
+        .Int,
+        .Float,
+        .ErrorSet,
+        .Enum,
+        .Frame,
+        .AnyFrame,
+        => break true,
+
+        .BoundFn,
+        .ComptimeFloat,
+        .ComptimeInt,
+        .EnumLiteral,
+        .NoReturn,
+        .Type,
+        .Void,
+        .Undefined,
+        .Null,
+        => break false,
+
+        .Opaque => break is_extern,
+
+        .Optional => {
+            var buf: Type.Payload.ElemType = undefined;
+            const child_ty = ty.optionalChild(&buf);
+            return validateVarType(sema, block, src, child_ty, is_extern);
+        },
+        .Pointer, .Array, .Vector => ty = ty.elemType(),
+        .ErrorUnion => ty = ty.errorUnionPayload(),
+
+        .Fn => @panic("TODO fn validateVarType"),
+        .Struct, .Union => {
+            const resolved_ty = try sema.resolveTypeFields(block, src, ty);
+            break !resolved_ty.requiresComptime();
+        },
+    } else unreachable; // TODO should not need else unreachable
+    if (!ok) {
+        return sema.mod.fail(&block.base, src, "variable of type '{}' must be const or comptime", .{var_ty});
     }
 }
 
@@ -9393,8 +9497,9 @@ fn structFieldPtr(
     const struct_ty = try sema.resolveTypeFields(block, src, unresolved_struct_ty);
     const struct_obj = struct_ty.castTag(.@"struct").?.data;
 
-    const field_index = struct_obj.fields.getIndex(field_name) orelse
+    const field_index_big = struct_obj.fields.getIndex(field_name) orelse
         return sema.failWithBadFieldAccess(block, struct_obj, field_name_src, field_name);
+    const field_index = @intCast(u32, field_index_big);
     const field = struct_obj.fields.values()[field_index];
     const ptr_field_ty = try Type.ptr(arena, .{
         .pointee_type = field.ty,
@@ -9413,31 +9518,7 @@ fn structFieldPtr(
     }
 
     try sema.requireRuntimeBlock(block, src);
-    const tag: Air.Inst.Tag = switch (field_index) {
-        0 => .struct_field_ptr_index_0,
-        1 => .struct_field_ptr_index_1,
-        2 => .struct_field_ptr_index_2,
-        3 => .struct_field_ptr_index_3,
-        else => {
-            return block.addInst(.{
-                .tag = .struct_field_ptr,
-                .data = .{ .ty_pl = .{
-                    .ty = try sema.addType(ptr_field_ty),
-                    .payload = try sema.addExtra(Air.StructField{
-                        .struct_operand = struct_ptr,
-                        .field_index = @intCast(u32, field_index),
-                    }),
-                } },
-            });
-        },
-    };
-    return block.addInst(.{
-        .tag = tag,
-        .data = .{ .ty_op = .{
-            .ty = try sema.addType(ptr_field_ty),
-            .operand = struct_ptr,
-        } },
-    });
+    return block.addStructFieldPtr(struct_ptr, field_index, ptr_field_ty);
 }
 
 fn structFieldVal(
@@ -9487,7 +9568,6 @@ fn unionFieldPtr(
     field_name_src: LazySrcLoc,
     unresolved_union_ty: Type,
 ) CompileError!Air.Inst.Ref {
-    const mod = sema.mod;
     const arena = sema.arena;
     assert(unresolved_union_ty.zigTypeTag() == .Union);
 
@@ -9495,8 +9575,9 @@ fn unionFieldPtr(
     const union_ty = try sema.resolveTypeFields(block, src, unresolved_union_ty);
     const union_obj = union_ty.cast(Type.Payload.Union).?.data;
 
-    const field_index = union_obj.fields.getIndex(field_name) orelse
+    const field_index_big = union_obj.fields.getIndex(field_name) orelse
         return sema.failWithBadUnionFieldAccess(block, union_obj, field_name_src, field_name);
+    const field_index = @intCast(u32, field_index_big);
 
     const field = union_obj.fields.values()[field_index];
     const ptr_field_ty = try Type.ptr(arena, .{
@@ -9517,7 +9598,7 @@ fn unionFieldPtr(
     }
 
     try sema.requireRuntimeBlock(block, src);
-    return mod.fail(&block.base, src, "TODO implement runtime union field access", .{});
+    return block.addStructFieldPtr(union_ptr, field_index, ptr_field_ty);
 }
 
 fn unionFieldVal(
@@ -11160,6 +11241,28 @@ fn analyzeUnionFields(
     if (body.len != 0) {
         _ = try sema.analyzeBody(block, body);
     }
+    var int_tag_ty: Type = undefined;
+    var enum_field_names: ?*Module.EnumNumbered.NameMap = null;
+    var enum_value_map: ?*Module.EnumNumbered.ValueMap = null;
+    if (tag_type_ref != .none) {
+        const provided_ty = try sema.resolveType(block, src, tag_type_ref);
+        if (small.auto_enum_tag) {
+            // The provided type is an integer type and we must construct the enum tag type here.
+            int_tag_ty = provided_ty;
+            union_obj.tag_ty = try sema.generateUnionTagTypeNumbered(block, fields_len, provided_ty);
+            enum_field_names = &union_obj.tag_ty.castTag(.enum_numbered).?.data.fields;
+            enum_value_map = &union_obj.tag_ty.castTag(.enum_numbered).?.data.values;
+        } else {
+            // The provided type is the enum tag type.
+            union_obj.tag_ty = provided_ty;
+        }
+    } else {
+        // If auto_enum_tag is false, this is an untagged union. However, for semantic analysis
+        // purposes, we still auto-generate an enum tag type the same way. That the union is
+        // untagged is represented by the Type tag (union vs union_tagged).
+        union_obj.tag_ty = try sema.generateUnionTagTypeSimple(block, fields_len);
+        enum_field_names = &union_obj.tag_ty.castTag(.enum_simple).?.data.fields;
+    }
 
     const bits_per_field = 4;
     const fields_per_u32 = 32 / bits_per_field;
@@ -11198,12 +11301,25 @@ fn analyzeUnionFields(
             break :blk align_ref;
         } else .none;
 
-        if (has_tag) {
+        const tag_ref: Zir.Inst.Ref = if (has_tag) blk: {
+            const tag_ref = @intToEnum(Zir.Inst.Ref, zir.extra[extra_index]);
             extra_index += 1;
+            break :blk tag_ref;
+        } else .none;
+
+        if (enum_value_map) |map| {
+            const tag_src = src; // TODO better source location
+            const coerced = try sema.coerce(block, int_tag_ty, tag_ref, tag_src);
+            const val = try sema.resolveConstValue(block, tag_src, coerced);
+            map.putAssumeCapacityContext(val, {}, .{ .ty = int_tag_ty });
         }
 
         // This string needs to outlive the ZIR code.
         const field_name = try decl_arena.allocator.dupe(u8, field_name_zir);
+        if (enum_field_names) |set| {
+            set.putAssumeCapacity(field_name, {});
+        }
+
         const field_ty: Type = if (field_type_ref == .none)
             Type.initTag(.void)
         else
@@ -11225,11 +11341,84 @@ fn analyzeUnionFields(
             // But only resolve the source location if we need to emit a compile error.
             const abi_align_val = (try sema.resolveInstConst(block, src, align_ref)).val;
             gop.value_ptr.abi_align = try abi_align_val.copy(&decl_arena.allocator);
+        } else {
+            gop.value_ptr.abi_align = Value.initTag(.abi_align_default);
         }
     }
+}
+
+fn generateUnionTagTypeNumbered(
+    sema: *Sema,
+    block: *Scope.Block,
+    fields_len: u32,
+    int_ty: Type,
+) !Type {
+    const mod = sema.mod;
+
+    var new_decl_arena = std.heap.ArenaAllocator.init(sema.gpa);
+    errdefer new_decl_arena.deinit();
 
-    // TODO resolve the union tag_type_ref
-    _ = tag_type_ref;
+    const enum_obj = try new_decl_arena.allocator.create(Module.EnumNumbered);
+    const enum_ty_payload = try new_decl_arena.allocator.create(Type.Payload.EnumNumbered);
+    enum_ty_payload.* = .{
+        .base = .{ .tag = .enum_numbered },
+        .data = enum_obj,
+    };
+    const enum_ty = Type.initPayload(&enum_ty_payload.base);
+    const enum_val = try Value.Tag.ty.create(&new_decl_arena.allocator, enum_ty);
+    // TODO better type name
+    const new_decl = try mod.createAnonymousDecl(&block.base, .{
+        .ty = Type.initTag(.type),
+        .val = enum_val,
+    });
+    new_decl.owns_tv = true;
+    errdefer sema.mod.deleteAnonDecl(&block.base, new_decl);
+
+    enum_obj.* = .{
+        .owner_decl = new_decl,
+        .tag_ty = int_ty,
+        .fields = .{},
+        .values = .{},
+        .node_offset = 0,
+    };
+    // Here we pre-allocate the maps using the decl arena.
+    try enum_obj.fields.ensureTotalCapacity(&new_decl_arena.allocator, fields_len);
+    try enum_obj.values.ensureTotalCapacityContext(&new_decl_arena.allocator, fields_len, .{ .ty = int_ty });
+    try new_decl.finalizeNewArena(&new_decl_arena);
+    return enum_ty;
+}
+
+fn generateUnionTagTypeSimple(sema: *Sema, block: *Scope.Block, fields_len: u32) !Type {
+    const mod = sema.mod;
+
+    var new_decl_arena = std.heap.ArenaAllocator.init(sema.gpa);
+    errdefer new_decl_arena.deinit();
+
+    const enum_obj = try new_decl_arena.allocator.create(Module.EnumSimple);
+    const enum_ty_payload = try new_decl_arena.allocator.create(Type.Payload.EnumSimple);
+    enum_ty_payload.* = .{
+        .base = .{ .tag = .enum_simple },
+        .data = enum_obj,
+    };
+    const enum_ty = Type.initPayload(&enum_ty_payload.base);
+    const enum_val = try Value.Tag.ty.create(&new_decl_arena.allocator, enum_ty);
+    // TODO better type name
+    const new_decl = try mod.createAnonymousDecl(&block.base, .{
+        .ty = Type.initTag(.type),
+        .val = enum_val,
+    });
+    new_decl.owns_tv = true;
+    errdefer sema.mod.deleteAnonDecl(&block.base, new_decl);
+
+    enum_obj.* = .{
+        .owner_decl = new_decl,
+        .fields = .{},
+        .node_offset = 0,
+    };
+    // Here we pre-allocate the maps using the decl arena.
+    try enum_obj.fields.ensureTotalCapacity(&new_decl_arena.allocator, fields_len);
+    try new_decl.finalizeNewArena(&new_decl_arena);
+    return enum_ty;
 }
 
 fn getBuiltin(
@@ -11367,11 +11556,28 @@ fn typeHasOnePossibleValue(
             }
             return Value.initTag(.empty_struct_value);
         },
+        .enum_numbered => {
+            const resolved_ty = try sema.resolveTypeFields(block, src, ty);
+            const enum_obj = resolved_ty.castTag(.enum_numbered).?.data;
+            if (enum_obj.fields.count() == 1) {
+                if (enum_obj.values.count() == 0) {
+                    return Value.initTag(.zero); // auto-numbered
+                } else {
+                    return enum_obj.values.keys()[0];
+                }
+            } else {
+                return null;
+            }
+        },
         .enum_full => {
             const resolved_ty = try sema.resolveTypeFields(block, src, ty);
-            const enum_full = resolved_ty.castTag(.enum_full).?.data;
-            if (enum_full.fields.count() == 1) {
-                return enum_full.values.keys()[0];
+            const enum_obj = resolved_ty.castTag(.enum_full).?.data;
+            if (enum_obj.fields.count() == 1) {
+                if (enum_obj.values.count() == 0) {
+                    return Value.initTag(.zero); // auto-numbered
+                } else {
+                    return enum_obj.values.keys()[0];
+                }
             } else {
                 return null;
             }
src/type.zig
@@ -124,6 +124,7 @@ pub const Type = extern union {
             .enum_full,
             .enum_nonexhaustive,
             .enum_simple,
+            .enum_numbered,
             .atomic_order,
             .atomic_rmw_op,
             .calling_convention,
@@ -874,6 +875,7 @@ pub const Type = extern union {
             .@"struct" => return self.copyPayloadShallow(allocator, Payload.Struct),
             .@"union", .union_tagged => return self.copyPayloadShallow(allocator, Payload.Union),
             .enum_simple => return self.copyPayloadShallow(allocator, Payload.EnumSimple),
+            .enum_numbered => return self.copyPayloadShallow(allocator, Payload.EnumNumbered),
             .enum_full, .enum_nonexhaustive => return self.copyPayloadShallow(allocator, Payload.EnumFull),
             .@"opaque" => return self.copyPayloadShallow(allocator, Payload.Opaque),
         }
@@ -958,6 +960,10 @@ pub const Type = extern union {
                     const enum_simple = ty.castTag(.enum_simple).?.data;
                     return enum_simple.owner_decl.renderFullyQualifiedName(writer);
                 },
+                .enum_numbered => {
+                    const enum_numbered = ty.castTag(.enum_numbered).?.data;
+                    return enum_numbered.owner_decl.renderFullyQualifiedName(writer);
+                },
                 .@"opaque" => {
                     // TODO use declaration name
                     return writer.writeAll("opaque {}");
@@ -1268,6 +1274,7 @@ pub const Type = extern union {
             .@"union",
             .union_tagged,
             .enum_simple,
+            .enum_numbered,
             .enum_full,
             .enum_nonexhaustive,
             => false, // TODO some of these should be `true` depending on their child types
@@ -1421,7 +1428,7 @@ pub const Type = extern union {
                 const enum_simple = self.castTag(.enum_simple).?.data;
                 return enum_simple.fields.count() >= 2;
             },
-            .enum_nonexhaustive => {
+            .enum_numbered, .enum_nonexhaustive => {
                 var buffer: Payload.Bits = undefined;
                 const int_tag_ty = self.intTagType(&buffer);
                 return int_tag_ty.hasCodeGenBits();
@@ -1682,7 +1689,7 @@ pub const Type = extern union {
                 assert(biggest != 0);
                 return biggest;
             },
-            .enum_full, .enum_nonexhaustive, .enum_simple => {
+            .enum_full, .enum_nonexhaustive, .enum_simple, .enum_numbered => {
                 var buffer: Payload.Bits = undefined;
                 const int_tag_ty = self.intTagType(&buffer);
                 return int_tag_ty.abiAlignment(target);
@@ -1781,7 +1788,7 @@ pub const Type = extern union {
                 }
                 return size;
             },
-            .enum_simple, .enum_full, .enum_nonexhaustive => {
+            .enum_simple, .enum_full, .enum_nonexhaustive, .enum_numbered => {
                 var buffer: Payload.Bits = undefined;
                 const int_tag_ty = self.intTagType(&buffer);
                 return int_tag_ty.abiSize(target);
@@ -1948,7 +1955,7 @@ pub const Type = extern union {
             .@"struct" => {
                 @panic("TODO bitSize struct");
             },
-            .enum_simple, .enum_full, .enum_nonexhaustive => {
+            .enum_simple, .enum_full, .enum_nonexhaustive, .enum_numbered => {
                 var buffer: Payload.Bits = undefined;
                 const int_tag_ty = self.intTagType(&buffer);
                 return int_tag_ty.bitSize(target);
@@ -2094,23 +2101,6 @@ pub const Type = extern union {
         };
     }
 
-    /// Asserts the type is an enum.
-    pub fn intTagType(self: Type, buffer: *Payload.Bits) Type {
-        switch (self.tag()) {
-            .enum_full, .enum_nonexhaustive => return self.cast(Payload.EnumFull).?.data.tag_ty,
-            .enum_simple => {
-                const enum_simple = self.castTag(.enum_simple).?.data;
-                const bits = std.math.log2_int_ceil(usize, enum_simple.fields.count());
-                buffer.* = .{
-                    .base = .{ .tag = .int_unsigned },
-                    .data = bits,
-                };
-                return Type.initPayload(&buffer.base);
-            },
-            else => unreachable,
-        }
-    }
-
     pub fn isSinglePointer(self: Type) bool {
         return switch (self.tag()) {
             .single_const_pointer,
@@ -2363,48 +2353,6 @@ pub const Type = extern union {
         }
     }
 
-    /// Returns if type can be used for a runtime variable
-    pub fn isValidVarType(self: Type, is_extern: bool) bool {
-        var ty = self;
-        while (true) switch (ty.zigTypeTag()) {
-            .Bool,
-            .Int,
-            .Float,
-            .ErrorSet,
-            .Enum,
-            .Frame,
-            .AnyFrame,
-            => return true,
-
-            .Opaque => return is_extern,
-            .BoundFn,
-            .ComptimeFloat,
-            .ComptimeInt,
-            .EnumLiteral,
-            .NoReturn,
-            .Type,
-            .Void,
-            .Undefined,
-            .Null,
-            => return false,
-
-            .Optional => {
-                var buf: Payload.ElemType = undefined;
-                return ty.optionalChild(&buf).isValidVarType(is_extern);
-            },
-            .Pointer, .Array, .Vector => ty = ty.elemType(),
-            .ErrorUnion => ty = ty.errorUnionPayload(),
-
-            .Fn => @panic("TODO fn isValidVarType"),
-            .Struct => {
-                // TODO this is not always correct; introduce lazy value mechanism
-                // and here we need to force a resolve of "type requires comptime".
-                return true;
-            },
-            .Union => @panic("TODO union isValidVarType"),
-        };
-    }
-
     pub fn childType(ty: Type) Type {
         return switch (ty.tag()) {
             .vector => ty.castTag(.vector).?.data.elem_type,
@@ -2530,6 +2478,15 @@ pub const Type = extern union {
         }
     }
 
+    /// 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) ?Type {
+        return switch (ty.tag()) {
+            .union_tagged => ty.castTag(.union_tagged).?.data.tag_ty,
+            else => null,
+        };
+    }
+
     /// Asserts that the type is an error union.
     pub fn errorUnionPayload(self: Type) Type {
         return switch (self.tag()) {
@@ -3000,6 +2957,7 @@ pub const Type = extern union {
                 }
             },
             .enum_nonexhaustive => ty = ty.castTag(.enum_nonexhaustive).?.data.tag_ty,
+            .enum_numbered => ty = ty.castTag(.enum_numbered).?.data.tag_ty,
             .@"union" => {
                 return null; // TODO
             },
@@ -3114,31 +3072,21 @@ pub const Type = extern union {
         }
     }
 
-    /// Returns the integer tag type of the enum.
-    pub fn enumTagType(ty: Type, buffer: *Payload.Bits) Type {
-        switch (ty.tag()) {
-            .enum_full, .enum_nonexhaustive => {
-                const enum_full = ty.cast(Payload.EnumFull).?.data;
-                return enum_full.tag_ty;
-            },
+    /// Asserts the type is an enum or a union.
+    /// TODO support unions
+    pub fn intTagType(self: Type, buffer: *Payload.Bits) Type {
+        switch (self.tag()) {
+            .enum_full, .enum_nonexhaustive => return self.cast(Payload.EnumFull).?.data.tag_ty,
+            .enum_numbered => return self.castTag(.enum_numbered).?.data.tag_ty,
             .enum_simple => {
-                const enum_simple = ty.castTag(.enum_simple).?.data;
+                const enum_simple = self.castTag(.enum_simple).?.data;
+                const bits = std.math.log2_int_ceil(usize, enum_simple.fields.count());
                 buffer.* = .{
                     .base = .{ .tag = .int_unsigned },
-                    .data = std.math.log2_int_ceil(usize, enum_simple.fields.count()),
+                    .data = bits,
                 };
                 return Type.initPayload(&buffer.base);
             },
-            .atomic_order,
-            .atomic_rmw_op,
-            .calling_convention,
-            .float_mode,
-            .reduce_op,
-            .call_options,
-            .export_options,
-            .extern_options,
-            => @panic("TODO resolve std.builtin types"),
-
             else => unreachable,
         }
     }
@@ -3156,10 +3104,8 @@ pub const Type = extern union {
                 const enum_full = ty.cast(Payload.EnumFull).?.data;
                 return enum_full.fields.count();
             },
-            .enum_simple => {
-                const enum_simple = ty.castTag(.enum_simple).?.data;
-                return enum_simple.fields.count();
-            },
+            .enum_simple => return ty.castTag(.enum_simple).?.data.fields.count(),
+            .enum_numbered => return ty.castTag(.enum_numbered).?.data.fields.count(),
             .atomic_order,
             .atomic_rmw_op,
             .calling_convention,
@@ -3185,6 +3131,10 @@ pub const Type = extern union {
                 const enum_simple = ty.castTag(.enum_simple).?.data;
                 return enum_simple.fields.keys()[field_index];
             },
+            .enum_numbered => {
+                const enum_numbered = ty.castTag(.enum_numbered).?.data;
+                return enum_numbered.fields.keys()[field_index];
+            },
             .atomic_order,
             .atomic_rmw_op,
             .calling_convention,
@@ -3209,6 +3159,10 @@ pub const Type = extern union {
                 const enum_simple = ty.castTag(.enum_simple).?.data;
                 return enum_simple.fields.getIndex(field_name);
             },
+            .enum_numbered => {
+                const enum_numbered = ty.castTag(.enum_numbered).?.data;
+                return enum_numbered.fields.getIndex(field_name);
+            },
             .atomic_order,
             .atomic_rmw_op,
             .calling_convention,
@@ -3252,6 +3206,15 @@ pub const Type = extern union {
                     return enum_full.values.getIndexContext(enum_tag, .{ .ty = tag_ty });
                 }
             },
+            .enum_numbered => {
+                const enum_obj = ty.castTag(.enum_numbered).?.data;
+                const tag_ty = enum_obj.tag_ty;
+                if (enum_obj.values.count() == 0) {
+                    return S.fieldWithRange(tag_ty, enum_tag, enum_obj.fields.count());
+                } else {
+                    return enum_obj.values.getIndexContext(enum_tag, .{ .ty = tag_ty });
+                }
+            },
             .enum_simple => {
                 const enum_simple = ty.castTag(.enum_simple).?.data;
                 const fields_len = enum_simple.fields.count();
@@ -3303,6 +3266,7 @@ pub const Type = extern union {
                 const enum_full = ty.cast(Payload.EnumFull).?.data;
                 return enum_full.srcLoc();
             },
+            .enum_numbered => return ty.castTag(.enum_numbered).?.data.srcLoc(),
             .enum_simple => {
                 const enum_simple = ty.castTag(.enum_simple).?.data;
                 return enum_simple.srcLoc();
@@ -3340,6 +3304,7 @@ pub const Type = extern union {
                 const enum_full = ty.cast(Payload.EnumFull).?.data;
                 return enum_full.owner_decl;
             },
+            .enum_numbered => return ty.castTag(.enum_numbered).?.data.owner_decl,
             .enum_simple => {
                 const enum_simple = ty.castTag(.enum_simple).?.data;
                 return enum_simple.owner_decl;
@@ -3397,6 +3362,15 @@ pub const Type = extern union {
                     return enum_full.values.containsContext(int, .{ .ty = tag_ty });
                 }
             },
+            .enum_numbered => {
+                const enum_obj = ty.castTag(.enum_numbered).?.data;
+                const tag_ty = enum_obj.tag_ty;
+                if (enum_obj.values.count() == 0) {
+                    return S.intInRange(tag_ty, int, enum_obj.fields.count());
+                } else {
+                    return enum_obj.values.containsContext(int, .{ .ty = tag_ty });
+                }
+            },
             .enum_simple => {
                 const enum_simple = ty.castTag(.enum_simple).?.data;
                 const fields_len = enum_simple.fields.count();
@@ -3534,6 +3508,7 @@ pub const Type = extern union {
         @"union",
         union_tagged,
         enum_simple,
+        enum_numbered,
         enum_full,
         enum_nonexhaustive,
 
@@ -3642,6 +3617,7 @@ pub const Type = extern union {
                 .@"union", .union_tagged => Payload.Union,
                 .enum_full, .enum_nonexhaustive => Payload.EnumFull,
                 .enum_simple => Payload.EnumSimple,
+                .enum_numbered => Payload.EnumNumbered,
                 .empty_struct => Payload.ContainerScope,
             };
         }
@@ -3818,6 +3794,11 @@ pub const Type = extern union {
             base: Payload = .{ .tag = .enum_simple },
             data: *Module.EnumSimple,
         };
+
+        pub const EnumNumbered = struct {
+            base: Payload = .{ .tag = .enum_numbered },
+            data: *Module.EnumNumbered,
+        };
     };
 
     pub fn ptr(arena: *Allocator, d: Payload.Pointer.Data) !Type {
@@ -3850,6 +3831,23 @@ pub const Type = extern union {
         };
         return Type.initPayload(&type_payload.base);
     }
+
+    pub fn smallestUnsignedInt(arena: *Allocator, max: u64) !Type {
+        const bits = bits: {
+            if (max == 0) break :bits 0;
+            const base = std.math.log2(max);
+            const upper = (@as(u64, 1) << base) - 1;
+            break :bits base + @boolToInt(upper < max);
+        };
+        return switch (bits) {
+            1 => initTag(.u1),
+            8 => initTag(.u8),
+            16 => initTag(.u16),
+            32 => initTag(.u32),
+            64 => initTag(.u64),
+            else => return Tag.int_unsigned.create(arena, bits),
+        };
+    }
 };
 
 pub const CType = enum {
test/behavior/union.zig
@@ -2,3 +2,15 @@ const std = @import("std");
 const expect = std.testing.expect;
 const expectEqual = std.testing.expectEqual;
 const Tag = std.meta.Tag;
+
+const Foo = union {
+    float: f64,
+    int: i32,
+};
+
+test "basic unions" {
+    var foo = Foo{ .int = 1 };
+    try expect(foo.int == 1);
+    foo = Foo{ .float = 12.34 };
+    try expect(foo.float == 12.34);
+}
test/behavior/union_stage1.zig
@@ -39,13 +39,6 @@ const Foo = union {
     int: i32,
 };
 
-test "basic unions" {
-    var foo = Foo{ .int = 1 };
-    try expect(foo.int == 1);
-    foo = Foo{ .float = 12.34 };
-    try expect(foo.float == 12.34);
-}
-
 test "comptime union field access" {
     comptime {
         var foo = Foo{ .int = 0 };