Sema: allow empty enums and unions

This commit is contained in:
Veikka Tuominen 2022-08-17 16:10:46 +03:00
parent c3d5428cba
commit 233049503a
13 changed files with 156 additions and 178 deletions

View File

@ -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,

View File

@ -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,

View File

@ -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;

View File

@ -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);

View File

@ -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) {

View File

@ -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);
}

View File

@ -1,7 +0,0 @@
const Foo = enum {};
// error
// backend=stage2
// target=native
//
// :1:13: error: enum declarations must have at least one tag

View File

@ -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

View File

@ -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

View File

@ -1,7 +0,0 @@
const Foo = union {};
// error
// backend=stage2
// target=native
//
// :1:13: error: union declarations must have at least one tag

View File

@ -1,7 +0,0 @@
const Foo = union {};
// error
// backend=stage2
// target=native
//
// :1:13: error: union declarations must have at least one tag

View File

@ -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

View File

@ -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 {