From 233049503a41ac4c6895f7126cda63349606b8f8 Mon Sep 17 00:00:00 2001 From: Veikka Tuominen Date: Wed, 17 Aug 2022 16:10:46 +0300 Subject: [PATCH] Sema: allow empty enums and unions --- src/AstGen.zig | 9 - src/Sema.zig | 174 +++++++++--------- src/print_zir.zig | 15 +- src/type.zig | 5 +- test/behavior.zig | 1 + test/behavior/empty_union.zig | 54 ++++++ .../compile_errors/enum_with_0_fields.zig | 7 - ...e_for_exhaustive_enum_with_zero_fields.zig | 18 -- .../reify_type_for_union_with_zero_fields.zig | 17 -- .../union_fields_with_value_assignments.zig | 7 - .../compile_errors/union_with_0_fields.zig | 7 - ...types_in_function_call_raises_an_error.zig | 11 -- test/stage2/cbe.zig | 9 - 13 files changed, 156 insertions(+), 178 deletions(-) create mode 100644 test/behavior/empty_union.zig delete mode 100644 test/cases/compile_errors/enum_with_0_fields.zig delete mode 100644 test/cases/compile_errors/reify_type_for_exhaustive_enum_with_zero_fields.zig delete mode 100644 test/cases/compile_errors/reify_type_for_union_with_zero_fields.zig delete mode 100644 test/cases/compile_errors/union_fields_with_value_assignments.zig delete mode 100644 test/cases/compile_errors/union_with_0_fields.zig delete mode 100644 test/cases/compile_errors/using_invalid_types_in_function_call_raises_an_error.zig diff --git a/src/AstGen.zig b/src/AstGen.zig index 2fcd8cd994..ee1dbeffa4 100644 --- a/src/AstGen.zig +++ b/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, diff --git a/src/Sema.zig b/src/Sema.zig index 1aebe8e98e..1a0ded4a00 100644 --- a/src/Sema.zig +++ b/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, .{ + 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, + ); + + 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}); + } + + 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, }); - - 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 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}); - } - - 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, - }); - } - } else { - return sema.fail(block, src, "enums must have at least one field", .{}); } 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]; + 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, - ); + const field_name = try name_val.toAllocatedBytes( + Type.initTag(.const_slice_u8), + new_decl_arena_allocator, + sema.mod, + ); - if (enum_field_names) |set| { - set.putAssumeCapacity(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); - } - } - - 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}); - } - - 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 (enum_field_names) |set| { + set.putAssumeCapacity(field_name, {}); } - } else { - return sema.fail(block, src, "unions must have at least one field", .{}); + + 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); + } + } + + 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}); + } + + 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, diff --git a/src/print_zir.zig b/src/print_zir.zig index 4bc96c4259..579a7970b7 100644 --- a/src/print_zir.zig +++ b/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,16 +1454,21 @@ 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; diff --git a/src/type.zig b/src/type.zig index 582ea230ef..55847b8add 100644 --- a/src/type.zig +++ b/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); diff --git a/test/behavior.zig b/test/behavior.zig index 40f8ca0fb3..ba8379cd72 100644 --- a/test/behavior.zig +++ b/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) { diff --git a/test/behavior/empty_union.zig b/test/behavior/empty_union.zig new file mode 100644 index 0000000000..051e464b72 --- /dev/null +++ b/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); +} diff --git a/test/cases/compile_errors/enum_with_0_fields.zig b/test/cases/compile_errors/enum_with_0_fields.zig deleted file mode 100644 index f34065b69d..0000000000 --- a/test/cases/compile_errors/enum_with_0_fields.zig +++ /dev/null @@ -1,7 +0,0 @@ -const Foo = enum {}; - -// error -// backend=stage2 -// target=native -// -// :1:13: error: enum declarations must have at least one tag diff --git a/test/cases/compile_errors/reify_type_for_exhaustive_enum_with_zero_fields.zig b/test/cases/compile_errors/reify_type_for_exhaustive_enum_with_zero_fields.zig deleted file mode 100644 index 44876e938a..0000000000 --- a/test/cases/compile_errors/reify_type_for_exhaustive_enum_with_zero_fields.zig +++ /dev/null @@ -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 diff --git a/test/cases/compile_errors/reify_type_for_union_with_zero_fields.zig b/test/cases/compile_errors/reify_type_for_union_with_zero_fields.zig deleted file mode 100644 index 0b4f395c81..0000000000 --- a/test/cases/compile_errors/reify_type_for_union_with_zero_fields.zig +++ /dev/null @@ -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 diff --git a/test/cases/compile_errors/union_fields_with_value_assignments.zig b/test/cases/compile_errors/union_fields_with_value_assignments.zig deleted file mode 100644 index 2121568dd2..0000000000 --- a/test/cases/compile_errors/union_fields_with_value_assignments.zig +++ /dev/null @@ -1,7 +0,0 @@ -const Foo = union {}; - -// error -// backend=stage2 -// target=native -// -// :1:13: error: union declarations must have at least one tag diff --git a/test/cases/compile_errors/union_with_0_fields.zig b/test/cases/compile_errors/union_with_0_fields.zig deleted file mode 100644 index 2121568dd2..0000000000 --- a/test/cases/compile_errors/union_with_0_fields.zig +++ /dev/null @@ -1,7 +0,0 @@ -const Foo = union {}; - -// error -// backend=stage2 -// target=native -// -// :1:13: error: union declarations must have at least one tag diff --git a/test/cases/compile_errors/using_invalid_types_in_function_call_raises_an_error.zig b/test/cases/compile_errors/using_invalid_types_in_function_call_raises_an_error.zig deleted file mode 100644 index ee6d1b8b7c..0000000000 --- a/test/cases/compile_errors/using_invalid_types_in_function_call_raises_an_error.zig +++ /dev/null @@ -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 diff --git a/test/stage2/cbe.zig b/test/stage2/cbe.zig index 644cba74c1..321de1e3f6 100644 --- a/test/stage2/cbe.zig +++ b/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 {