Commit 233049503a

Veikka Tuominen <git@vexu.eu>
2022-08-17 15:10:46
Sema: allow empty enums and unions
1 parent c3d5428
src/AstGen.zig
@@ -4557,9 +4557,6 @@ fn unionDeclInner(
             wip_members.appendToField(@enumToInt(tag_value));
         }
     }
-    if (field_count == 0) {
-        return astgen.failNode(node, "union declarations must have at least one tag", .{});
-    }
 
     if (!block_scope.isEmpty()) {
         _ = try block_scope.addBreak(.break_inline, decl_inst, .void_value);
@@ -4715,12 +4712,6 @@ fn containerDecl(
                     .nonexhaustive_node = nonexhaustive_node,
                 };
             };
-            if (counts.total_fields == 0 and counts.nonexhaustive_node == 0) {
-                // One can construct an enum with no tags, and it functions the same as `noreturn`. But
-                // this is only useful for generic code; when explicitly using `enum {}` syntax, there
-                // must be at least one tag.
-                try astgen.appendErrorNode(node, "enum declarations must have at least one tag", .{});
-            }
             if (counts.nonexhaustive_node != 0 and container_decl.ast.arg == 0) {
                 try astgen.appendErrorNodeNotes(
                     node,
src/print_zir.zig
@@ -1443,7 +1443,7 @@ const Writer = struct {
         try self.writeFlag(stream, "autoenum, ", small.auto_enum_tag);
 
         if (decls_len == 0) {
-            try stream.writeAll("{}, ");
+            try stream.writeAll("{}");
         } else {
             const prev_parent_decl_node = self.parent_decl_node;
             if (src_node) |off| self.parent_decl_node = self.relativeToNodeIndex(off);
@@ -1454,15 +1454,20 @@ const Writer = struct {
             extra_index = try self.writeDecls(stream, decls_len, extra_index);
             self.indent -= 2;
             try stream.writeByteNTimes(' ', self.indent);
-            try stream.writeAll("}, ");
+            try stream.writeAll("}");
         }
 
-        assert(fields_len != 0);
-
         if (tag_type_ref != .none) {
-            try self.writeInstRef(stream, tag_type_ref);
             try stream.writeAll(", ");
+            try self.writeInstRef(stream, tag_type_ref);
+        }
+
+        if (fields_len == 0) {
+            try stream.writeAll("})");
+            try self.writeSrcNode(stream, src_node);
+            return;
         }
+        try stream.writeAll(", ");
 
         const body = self.code.extra[extra_index..][0..body_len];
         extra_index += body.len;
src/Sema.zig
@@ -8979,6 +8979,8 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError
     var seen_union_fields: []?Module.SwitchProngSrc = &.{};
     defer gpa.free(seen_union_fields);
 
+    var empty_enum = false;
+
     const operand_ty = sema.typeOf(operand);
 
     var else_error_ty: ?Type = null;
@@ -9012,6 +9014,7 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError
         .Union => unreachable, // handled in zirSwitchCond
         .Enum => {
             var seen_fields = try gpa.alloc(?Module.SwitchProngSrc, operand_ty.enumFieldCount());
+            empty_enum = seen_fields.len == 0 and !operand_ty.isNonexhaustiveEnum();
             defer if (!union_originally) gpa.free(seen_fields);
             if (union_originally) seen_union_fields = seen_fields;
             mem.set(?Module.SwitchProngSrc, seen_fields, null);
@@ -9607,6 +9610,9 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError
     }
 
     if (scalar_cases_len + multi_cases_len == 0) {
+        if (empty_enum) {
+            return Air.Inst.Ref.void_value;
+        }
         if (special_prong == .none) {
             return sema.fail(block, src, "switch must handle all possibilities", .{});
         }
@@ -16690,43 +16696,39 @@ fn zirReify(sema: *Sema, block: *Block, extended: Zir.Inst.Extended.InstData, in
 
             // Fields
             const fields_len = try sema.usizeCast(block, src, fields_val.sliceLen(mod));
-            if (fields_len > 0) {
-                try enum_obj.fields.ensureTotalCapacity(new_decl_arena_allocator, fields_len);
-                try enum_obj.values.ensureTotalCapacityContext(new_decl_arena_allocator, fields_len, .{
-                    .ty = enum_obj.tag_ty,
-                    .mod = mod,
-                });
-
-                var i: usize = 0;
-                while (i < fields_len) : (i += 1) {
-                    const elem_val = try fields_val.elemValue(sema.mod, sema.arena, i);
-                    const field_struct_val = elem_val.castTag(.aggregate).?.data;
-                    // TODO use reflection instead of magic numbers here
-                    // name: []const u8
-                    const name_val = field_struct_val[0];
-                    // value: comptime_int
-                    const value_val = field_struct_val[1];
-
-                    const field_name = try name_val.toAllocatedBytes(
-                        Type.initTag(.const_slice_u8),
-                        new_decl_arena_allocator,
-                        sema.mod,
-                    );
+            try enum_obj.fields.ensureTotalCapacity(new_decl_arena_allocator, fields_len);
+            try enum_obj.values.ensureTotalCapacityContext(new_decl_arena_allocator, fields_len, .{
+                .ty = enum_obj.tag_ty,
+                .mod = mod,
+            });
 
-                    const gop = enum_obj.fields.getOrPutAssumeCapacity(field_name);
-                    if (gop.found_existing) {
-                        // TODO: better source location
-                        return sema.fail(block, src, "duplicate enum tag {s}", .{field_name});
-                    }
+            var i: usize = 0;
+            while (i < fields_len) : (i += 1) {
+                const elem_val = try fields_val.elemValue(sema.mod, sema.arena, i);
+                const field_struct_val = elem_val.castTag(.aggregate).?.data;
+                // TODO use reflection instead of magic numbers here
+                // name: []const u8
+                const name_val = field_struct_val[0];
+                // value: comptime_int
+                const value_val = field_struct_val[1];
+
+                const field_name = try name_val.toAllocatedBytes(
+                    Type.initTag(.const_slice_u8),
+                    new_decl_arena_allocator,
+                    sema.mod,
+                );
 
-                    const copied_tag_val = try value_val.copy(new_decl_arena_allocator);
-                    enum_obj.values.putAssumeCapacityNoClobberContext(copied_tag_val, {}, .{
-                        .ty = enum_obj.tag_ty,
-                        .mod = mod,
-                    });
+                const gop = enum_obj.fields.getOrPutAssumeCapacity(field_name);
+                if (gop.found_existing) {
+                    // TODO: better source location
+                    return sema.fail(block, src, "duplicate enum tag {s}", .{field_name});
                 }
-            } else {
-                return sema.fail(block, src, "enums must have at least one field", .{});
+
+                const copied_tag_val = try value_val.copy(new_decl_arena_allocator);
+                enum_obj.values.putAssumeCapacityNoClobberContext(copied_tag_val, {}, .{
+                    .ty = enum_obj.tag_ty,
+                    .mod = mod,
+                });
             }
 
             try new_decl.finalizeNewArena(&new_decl_arena);
@@ -16851,58 +16853,54 @@ fn zirReify(sema: *Sema, block: *Block, extended: Zir.Inst.Extended.InstData, in
             }
 
             // Fields
-            if (fields_len > 0) {
-                try union_obj.fields.ensureTotalCapacity(new_decl_arena_allocator, fields_len);
+            try union_obj.fields.ensureTotalCapacity(new_decl_arena_allocator, fields_len);
 
-                var i: usize = 0;
-                while (i < fields_len) : (i += 1) {
-                    const elem_val = try fields_val.elemValue(sema.mod, sema.arena, i);
-                    const field_struct_val = elem_val.castTag(.aggregate).?.data;
-                    // TODO use reflection instead of magic numbers here
-                    // name: []const u8
-                    const name_val = field_struct_val[0];
-                    // field_type: type,
-                    const field_type_val = field_struct_val[1];
-                    // alignment: comptime_int,
-                    const alignment_val = field_struct_val[2];
-
-                    const field_name = try name_val.toAllocatedBytes(
-                        Type.initTag(.const_slice_u8),
-                        new_decl_arena_allocator,
-                        sema.mod,
-                    );
+            var i: usize = 0;
+            while (i < fields_len) : (i += 1) {
+                const elem_val = try fields_val.elemValue(sema.mod, sema.arena, i);
+                const field_struct_val = elem_val.castTag(.aggregate).?.data;
+                // TODO use reflection instead of magic numbers here
+                // name: []const u8
+                const name_val = field_struct_val[0];
+                // field_type: type,
+                const field_type_val = field_struct_val[1];
+                // alignment: comptime_int,
+                const alignment_val = field_struct_val[2];
 
-                    if (enum_field_names) |set| {
-                        set.putAssumeCapacity(field_name, {});
-                    }
+                const field_name = try name_val.toAllocatedBytes(
+                    Type.initTag(.const_slice_u8),
+                    new_decl_arena_allocator,
+                    sema.mod,
+                );
 
-                    if (tag_ty_field_names) |*names| {
-                        const enum_has_field = names.orderedRemove(field_name);
-                        if (!enum_has_field) {
-                            const msg = msg: {
-                                const msg = try sema.errMsg(block, src, "no field named '{s}' in enum '{}'", .{ field_name, union_obj.tag_ty.fmt(sema.mod) });
-                                errdefer msg.destroy(sema.gpa);
-                                try sema.addDeclaredHereNote(msg, union_obj.tag_ty);
-                                break :msg msg;
-                            };
-                            return sema.failWithOwnedErrorMsg(msg);
-                        }
-                    }
+                if (enum_field_names) |set| {
+                    set.putAssumeCapacity(field_name, {});
+                }
 
-                    const gop = union_obj.fields.getOrPutAssumeCapacity(field_name);
-                    if (gop.found_existing) {
-                        // TODO: better source location
-                        return sema.fail(block, src, "duplicate union field {s}", .{field_name});
+                if (tag_ty_field_names) |*names| {
+                    const enum_has_field = names.orderedRemove(field_name);
+                    if (!enum_has_field) {
+                        const msg = msg: {
+                            const msg = try sema.errMsg(block, src, "no field named '{s}' in enum '{}'", .{ field_name, union_obj.tag_ty.fmt(sema.mod) });
+                            errdefer msg.destroy(sema.gpa);
+                            try sema.addDeclaredHereNote(msg, union_obj.tag_ty);
+                            break :msg msg;
+                        };
+                        return sema.failWithOwnedErrorMsg(msg);
                     }
+                }
 
-                    var buffer: Value.ToTypeBuffer = undefined;
-                    gop.value_ptr.* = .{
-                        .ty = try field_type_val.toType(&buffer).copy(new_decl_arena_allocator),
-                        .abi_align = @intCast(u32, alignment_val.toUnsignedInt(target)),
-                    };
+                const gop = union_obj.fields.getOrPutAssumeCapacity(field_name);
+                if (gop.found_existing) {
+                    // TODO: better source location
+                    return sema.fail(block, src, "duplicate union field {s}", .{field_name});
                 }
-            } else {
-                return sema.fail(block, src, "unions must have at least one field", .{});
+
+                var buffer: Value.ToTypeBuffer = undefined;
+                gop.value_ptr.* = .{
+                    .ty = try field_type_val.toType(&buffer).copy(new_decl_arena_allocator),
+                    .abi_align = @intCast(u32, alignment_val.toUnsignedInt(target)),
+                };
             }
 
             if (tag_ty_field_names) |names| {
@@ -28146,10 +28144,6 @@ fn semaUnionFields(mod: *Module, union_obj: *Module.Union) CompileError!void {
     extra_index = decls_it.extra_index;
 
     const body = zir.extra[extra_index..][0..body_len];
-    if (fields_len == 0) {
-        assert(body.len == 0);
-        return;
-    }
     extra_index += body.len;
 
     const decl = mod.declPtr(decl_index);
@@ -28237,6 +28231,10 @@ fn semaUnionFields(mod: *Module, union_obj: *Module.Union) CompileError!void {
         enum_field_names = &union_obj.tag_ty.castTag(.enum_simple).?.data.fields;
     }
 
+    if (fields_len == 0) {
+        return;
+    }
+
     const bits_per_field = 4;
     const fields_per_u32 = 32 / bits_per_field;
     const bit_bags_count = std.math.divCeil(usize, fields_len, fields_per_u32) catch unreachable;
@@ -28772,7 +28770,9 @@ pub fn typeHasOnePossibleValue(
             const union_obj = resolved_ty.cast(Type.Payload.Union).?.data;
             const tag_val = (try sema.typeHasOnePossibleValue(block, src, union_obj.tag_ty)) orelse
                 return null;
-            const only_field = union_obj.fields.values()[0];
+            const fields = union_obj.fields.values();
+            if (fields.len == 0) return Value.initTag(.empty_struct_value);
+            const only_field = fields[0];
             if (only_field.ty.eql(resolved_ty, sema.mod)) {
                 const msg = try Module.ErrorMsg.create(
                     sema.gpa,
src/type.zig
@@ -2488,7 +2488,7 @@ pub const Type = extern union {
             },
             .union_safety_tagged, .union_tagged => {
                 const union_obj = ty.cast(Payload.Union).?.data;
-                if (try union_obj.tag_ty.hasRuntimeBitsAdvanced(ignore_comptime_only, sema_kit)) {
+                if (union_obj.fields.count() > 0 and try union_obj.tag_ty.hasRuntimeBitsAdvanced(ignore_comptime_only, sema_kit)) {
                     return true;
                 }
                 if (sema_kit) |sk| {
@@ -3113,6 +3113,9 @@ pub const Type = extern union {
             .sema_kit => unreachable, // handled above
             .lazy => |arena| return AbiAlignmentAdvanced{ .val = try Value.Tag.lazy_align.create(arena, ty) },
         };
+        if (union_obj.fields.count() == 0) {
+            return AbiAlignmentAdvanced{ .scalar = @boolToInt(union_obj.layout == .Extern) };
+        }
 
         var max_align: u32 = 0;
         if (have_tag) max_align = union_obj.tag_ty.abiAlignment(target);
test/behavior/empty_union.zig
@@ -0,0 +1,54 @@
+const builtin = @import("builtin");
+const std = @import("std");
+const expect = std.testing.expect;
+
+test "switch on empty enum" {
+    if (builtin.zig_backend == .stage2_c) return error.SkipZigTest; // TODO
+    if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO
+
+    const E = enum {};
+    var e: E = undefined;
+    switch (e) {}
+}
+
+test "switch on empty enum with a specified tag type" {
+    if (builtin.zig_backend == .stage2_c) return error.SkipZigTest; // TODO
+    if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO
+
+    const E = enum(u8) {};
+    var e: E = undefined;
+    switch (e) {}
+}
+
+test "switch on empty auto numbered tagged union" {
+    if (builtin.zig_backend == .stage2_c) return error.SkipZigTest; // TODO
+    if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
+    if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO
+
+    const U = union(enum(u8)) {};
+    var u: U = undefined;
+    switch (u) {}
+}
+
+test "switch on empty tagged union" {
+    if (builtin.zig_backend == .stage2_c) return error.SkipZigTest; // TODO
+    if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
+    if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO
+
+    const E = enum {};
+    const U = union(E) {};
+    var u: U = undefined;
+    switch (u) {}
+}
+
+test "empty union" {
+    const U = union {};
+    try expect(@sizeOf(U) == 0);
+    try expect(@alignOf(U) == 0);
+}
+
+test "empty extern union" {
+    const U = extern union {};
+    try expect(@sizeOf(U) == 0);
+    try expect(@alignOf(U) == 1);
+}
test/cases/compile_errors/enum_with_0_fields.zig
@@ -1,7 +0,0 @@
-const Foo = enum {};
-
-// error
-// backend=stage2
-// target=native
-//
-// :1:13: error: enum declarations must have at least one tag
test/cases/compile_errors/reify_type_for_exhaustive_enum_with_zero_fields.zig
@@ -1,18 +0,0 @@
-const Tag = @Type(.{
-    .Enum = .{
-        .layout = .Auto,
-        .tag_type = u1,
-        .fields = &.{},
-        .decls = &.{},
-        .is_exhaustive = true,
-    },
-});
-export fn entry() void {
-    _ = @intToEnum(Tag, 0);
-}
-
-// error
-// backend=stage2
-// target=native
-//
-// :1:13: error: enums must have at least one field
test/cases/compile_errors/reify_type_for_union_with_zero_fields.zig
@@ -1,17 +0,0 @@
-const Untagged = @Type(.{
-    .Union = .{
-        .layout = .Auto,
-        .tag_type = null,
-        .fields = &.{},
-        .decls = &.{},
-    },
-});
-export fn entry() void {
-    _ = Untagged{};
-}
-
-// error
-// backend=stage2
-// target=native
-//
-// :1:18: error: unions must have at least one field
test/cases/compile_errors/union_fields_with_value_assignments.zig
@@ -1,7 +0,0 @@
-const Foo = union {};
-
-// error
-// backend=stage2
-// target=native
-//
-// :1:13: error: union declarations must have at least one tag
test/cases/compile_errors/union_with_0_fields.zig
@@ -1,7 +0,0 @@
-const Foo = union {};
-
-// error
-// backend=stage2
-// target=native
-//
-// :1:13: error: union declarations must have at least one tag
test/cases/compile_errors/using_invalid_types_in_function_call_raises_an_error.zig
@@ -1,11 +0,0 @@
-const MenuEffect = enum {};
-fn func(effect: MenuEffect) void { _ = effect; }
-export fn entry() void {
-    func(MenuEffect.ThisDoesNotExist);
-}
-
-// error
-// backend=stage2
-// target=native
-//
-// :1:20: error: enum declarations must have at least one tag
test/stage2/cbe.zig
@@ -704,15 +704,6 @@ pub fn addCases(ctx: *TestContext) !void {
             ":5:9: error: '_' is used to mark an enum as non-exhaustive and cannot be assigned a value",
         });
 
-        case.addError(
-            \\const E1 = enum {};
-            \\export fn foo() void {
-            \\    _ = E1.a;
-            \\}
-        , &.{
-            ":1:12: error: enum declarations must have at least one tag",
-        });
-
         case.addError(
             \\const E1 = enum { a, b, _ };
             \\export fn foo() void {
test/behavior.zig
@@ -167,6 +167,7 @@ test {
     if (builtin.zig_backend != .stage1) {
         _ = @import("behavior/decltest.zig");
         _ = @import("behavior/packed_struct_explicit_backing_int.zig");
+        _ = @import("behavior/empty_union.zig");
     }
 
     if (builtin.os.tag != .wasi) {