Skip to content

Commit 2330495

Browse files
committed
Sema: allow empty enums and unions
1 parent c3d5428 commit 2330495

13 files changed

+153
-175
lines changed

src/AstGen.zig

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4557,9 +4557,6 @@ fn unionDeclInner(
45574557
wip_members.appendToField(@enumToInt(tag_value));
45584558
}
45594559
}
4560-
if (field_count == 0) {
4561-
return astgen.failNode(node, "union declarations must have at least one tag", .{});
4562-
}
45634560

45644561
if (!block_scope.isEmpty()) {
45654562
_ = try block_scope.addBreak(.break_inline, decl_inst, .void_value);
@@ -4715,12 +4712,6 @@ fn containerDecl(
47154712
.nonexhaustive_node = nonexhaustive_node,
47164713
};
47174714
};
4718-
if (counts.total_fields == 0 and counts.nonexhaustive_node == 0) {
4719-
// One can construct an enum with no tags, and it functions the same as `noreturn`. But
4720-
// this is only useful for generic code; when explicitly using `enum {}` syntax, there
4721-
// must be at least one tag.
4722-
try astgen.appendErrorNode(node, "enum declarations must have at least one tag", .{});
4723-
}
47244715
if (counts.nonexhaustive_node != 0 and container_decl.ast.arg == 0) {
47254716
try astgen.appendErrorNodeNotes(
47264717
node,

src/Sema.zig

Lines changed: 84 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -8979,6 +8979,8 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError
89798979
var seen_union_fields: []?Module.SwitchProngSrc = &.{};
89808980
defer gpa.free(seen_union_fields);
89818981

8982+
var empty_enum = false;
8983+
89828984
const operand_ty = sema.typeOf(operand);
89838985

89848986
var else_error_ty: ?Type = null;
@@ -9012,6 +9014,7 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError
90129014
.Union => unreachable, // handled in zirSwitchCond
90139015
.Enum => {
90149016
var seen_fields = try gpa.alloc(?Module.SwitchProngSrc, operand_ty.enumFieldCount());
9017+
empty_enum = seen_fields.len == 0 and !operand_ty.isNonexhaustiveEnum();
90159018
defer if (!union_originally) gpa.free(seen_fields);
90169019
if (union_originally) seen_union_fields = seen_fields;
90179020
mem.set(?Module.SwitchProngSrc, seen_fields, null);
@@ -9607,6 +9610,9 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError
96079610
}
96089611

96099612
if (scalar_cases_len + multi_cases_len == 0) {
9613+
if (empty_enum) {
9614+
return Air.Inst.Ref.void_value;
9615+
}
96109616
if (special_prong == .none) {
96119617
return sema.fail(block, src, "switch must handle all possibilities", .{});
96129618
}
@@ -16690,43 +16696,39 @@ fn zirReify(sema: *Sema, block: *Block, extended: Zir.Inst.Extended.InstData, in
1669016696

1669116697
// Fields
1669216698
const fields_len = try sema.usizeCast(block, src, fields_val.sliceLen(mod));
16693-
if (fields_len > 0) {
16694-
try enum_obj.fields.ensureTotalCapacity(new_decl_arena_allocator, fields_len);
16695-
try enum_obj.values.ensureTotalCapacityContext(new_decl_arena_allocator, fields_len, .{
16696-
.ty = enum_obj.tag_ty,
16697-
.mod = mod,
16698-
});
16699-
16700-
var i: usize = 0;
16701-
while (i < fields_len) : (i += 1) {
16702-
const elem_val = try fields_val.elemValue(sema.mod, sema.arena, i);
16703-
const field_struct_val = elem_val.castTag(.aggregate).?.data;
16704-
// TODO use reflection instead of magic numbers here
16705-
// name: []const u8
16706-
const name_val = field_struct_val[0];
16707-
// value: comptime_int
16708-
const value_val = field_struct_val[1];
16709-
16710-
const field_name = try name_val.toAllocatedBytes(
16711-
Type.initTag(.const_slice_u8),
16712-
new_decl_arena_allocator,
16713-
sema.mod,
16714-
);
16699+
try enum_obj.fields.ensureTotalCapacity(new_decl_arena_allocator, fields_len);
16700+
try enum_obj.values.ensureTotalCapacityContext(new_decl_arena_allocator, fields_len, .{
16701+
.ty = enum_obj.tag_ty,
16702+
.mod = mod,
16703+
});
1671516704

16716-
const gop = enum_obj.fields.getOrPutAssumeCapacity(field_name);
16717-
if (gop.found_existing) {
16718-
// TODO: better source location
16719-
return sema.fail(block, src, "duplicate enum tag {s}", .{field_name});
16720-
}
16705+
var i: usize = 0;
16706+
while (i < fields_len) : (i += 1) {
16707+
const elem_val = try fields_val.elemValue(sema.mod, sema.arena, i);
16708+
const field_struct_val = elem_val.castTag(.aggregate).?.data;
16709+
// TODO use reflection instead of magic numbers here
16710+
// name: []const u8
16711+
const name_val = field_struct_val[0];
16712+
// value: comptime_int
16713+
const value_val = field_struct_val[1];
16714+
16715+
const field_name = try name_val.toAllocatedBytes(
16716+
Type.initTag(.const_slice_u8),
16717+
new_decl_arena_allocator,
16718+
sema.mod,
16719+
);
1672116720

16722-
const copied_tag_val = try value_val.copy(new_decl_arena_allocator);
16723-
enum_obj.values.putAssumeCapacityNoClobberContext(copied_tag_val, {}, .{
16724-
.ty = enum_obj.tag_ty,
16725-
.mod = mod,
16726-
});
16721+
const gop = enum_obj.fields.getOrPutAssumeCapacity(field_name);
16722+
if (gop.found_existing) {
16723+
// TODO: better source location
16724+
return sema.fail(block, src, "duplicate enum tag {s}", .{field_name});
1672716725
}
16728-
} else {
16729-
return sema.fail(block, src, "enums must have at least one field", .{});
16726+
16727+
const copied_tag_val = try value_val.copy(new_decl_arena_allocator);
16728+
enum_obj.values.putAssumeCapacityNoClobberContext(copied_tag_val, {}, .{
16729+
.ty = enum_obj.tag_ty,
16730+
.mod = mod,
16731+
});
1673016732
}
1673116733

1673216734
try new_decl.finalizeNewArena(&new_decl_arena);
@@ -16851,58 +16853,54 @@ fn zirReify(sema: *Sema, block: *Block, extended: Zir.Inst.Extended.InstData, in
1685116853
}
1685216854

1685316855
// Fields
16854-
if (fields_len > 0) {
16855-
try union_obj.fields.ensureTotalCapacity(new_decl_arena_allocator, fields_len);
16856+
try union_obj.fields.ensureTotalCapacity(new_decl_arena_allocator, fields_len);
1685616857

16857-
var i: usize = 0;
16858-
while (i < fields_len) : (i += 1) {
16859-
const elem_val = try fields_val.elemValue(sema.mod, sema.arena, i);
16860-
const field_struct_val = elem_val.castTag(.aggregate).?.data;
16861-
// TODO use reflection instead of magic numbers here
16862-
// name: []const u8
16863-
const name_val = field_struct_val[0];
16864-
// field_type: type,
16865-
const field_type_val = field_struct_val[1];
16866-
// alignment: comptime_int,
16867-
const alignment_val = field_struct_val[2];
16868-
16869-
const field_name = try name_val.toAllocatedBytes(
16870-
Type.initTag(.const_slice_u8),
16871-
new_decl_arena_allocator,
16872-
sema.mod,
16873-
);
16858+
var i: usize = 0;
16859+
while (i < fields_len) : (i += 1) {
16860+
const elem_val = try fields_val.elemValue(sema.mod, sema.arena, i);
16861+
const field_struct_val = elem_val.castTag(.aggregate).?.data;
16862+
// TODO use reflection instead of magic numbers here
16863+
// name: []const u8
16864+
const name_val = field_struct_val[0];
16865+
// field_type: type,
16866+
const field_type_val = field_struct_val[1];
16867+
// alignment: comptime_int,
16868+
const alignment_val = field_struct_val[2];
1687416869

16875-
if (enum_field_names) |set| {
16876-
set.putAssumeCapacity(field_name, {});
16877-
}
16870+
const field_name = try name_val.toAllocatedBytes(
16871+
Type.initTag(.const_slice_u8),
16872+
new_decl_arena_allocator,
16873+
sema.mod,
16874+
);
1687816875

16879-
if (tag_ty_field_names) |*names| {
16880-
const enum_has_field = names.orderedRemove(field_name);
16881-
if (!enum_has_field) {
16882-
const msg = msg: {
16883-
const msg = try sema.errMsg(block, src, "no field named '{s}' in enum '{}'", .{ field_name, union_obj.tag_ty.fmt(sema.mod) });
16884-
errdefer msg.destroy(sema.gpa);
16885-
try sema.addDeclaredHereNote(msg, union_obj.tag_ty);
16886-
break :msg msg;
16887-
};
16888-
return sema.failWithOwnedErrorMsg(msg);
16889-
}
16890-
}
16876+
if (enum_field_names) |set| {
16877+
set.putAssumeCapacity(field_name, {});
16878+
}
1689116879

16892-
const gop = union_obj.fields.getOrPutAssumeCapacity(field_name);
16893-
if (gop.found_existing) {
16894-
// TODO: better source location
16895-
return sema.fail(block, src, "duplicate union field {s}", .{field_name});
16880+
if (tag_ty_field_names) |*names| {
16881+
const enum_has_field = names.orderedRemove(field_name);
16882+
if (!enum_has_field) {
16883+
const msg = msg: {
16884+
const msg = try sema.errMsg(block, src, "no field named '{s}' in enum '{}'", .{ field_name, union_obj.tag_ty.fmt(sema.mod) });
16885+
errdefer msg.destroy(sema.gpa);
16886+
try sema.addDeclaredHereNote(msg, union_obj.tag_ty);
16887+
break :msg msg;
16888+
};
16889+
return sema.failWithOwnedErrorMsg(msg);
1689616890
}
16891+
}
1689716892

16898-
var buffer: Value.ToTypeBuffer = undefined;
16899-
gop.value_ptr.* = .{
16900-
.ty = try field_type_val.toType(&buffer).copy(new_decl_arena_allocator),
16901-
.abi_align = @intCast(u32, alignment_val.toUnsignedInt(target)),
16902-
};
16893+
const gop = union_obj.fields.getOrPutAssumeCapacity(field_name);
16894+
if (gop.found_existing) {
16895+
// TODO: better source location
16896+
return sema.fail(block, src, "duplicate union field {s}", .{field_name});
1690316897
}
16904-
} else {
16905-
return sema.fail(block, src, "unions must have at least one field", .{});
16898+
16899+
var buffer: Value.ToTypeBuffer = undefined;
16900+
gop.value_ptr.* = .{
16901+
.ty = try field_type_val.toType(&buffer).copy(new_decl_arena_allocator),
16902+
.abi_align = @intCast(u32, alignment_val.toUnsignedInt(target)),
16903+
};
1690616904
}
1690716905

1690816906
if (tag_ty_field_names) |names| {
@@ -28146,10 +28144,6 @@ fn semaUnionFields(mod: *Module, union_obj: *Module.Union) CompileError!void {
2814628144
extra_index = decls_it.extra_index;
2814728145

2814828146
const body = zir.extra[extra_index..][0..body_len];
28149-
if (fields_len == 0) {
28150-
assert(body.len == 0);
28151-
return;
28152-
}
2815328147
extra_index += body.len;
2815428148

2815528149
const decl = mod.declPtr(decl_index);
@@ -28237,6 +28231,10 @@ fn semaUnionFields(mod: *Module, union_obj: *Module.Union) CompileError!void {
2823728231
enum_field_names = &union_obj.tag_ty.castTag(.enum_simple).?.data.fields;
2823828232
}
2823928233

28234+
if (fields_len == 0) {
28235+
return;
28236+
}
28237+
2824028238
const bits_per_field = 4;
2824128239
const fields_per_u32 = 32 / bits_per_field;
2824228240
const bit_bags_count = std.math.divCeil(usize, fields_len, fields_per_u32) catch unreachable;
@@ -28772,7 +28770,9 @@ pub fn typeHasOnePossibleValue(
2877228770
const union_obj = resolved_ty.cast(Type.Payload.Union).?.data;
2877328771
const tag_val = (try sema.typeHasOnePossibleValue(block, src, union_obj.tag_ty)) orelse
2877428772
return null;
28775-
const only_field = union_obj.fields.values()[0];
28773+
const fields = union_obj.fields.values();
28774+
if (fields.len == 0) return Value.initTag(.empty_struct_value);
28775+
const only_field = fields[0];
2877628776
if (only_field.ty.eql(resolved_ty, sema.mod)) {
2877728777
const msg = try Module.ErrorMsg.create(
2877828778
sema.gpa,

src/print_zir.zig

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1443,7 +1443,7 @@ const Writer = struct {
14431443
try self.writeFlag(stream, "autoenum, ", small.auto_enum_tag);
14441444

14451445
if (decls_len == 0) {
1446-
try stream.writeAll("{}, ");
1446+
try stream.writeAll("{}");
14471447
} else {
14481448
const prev_parent_decl_node = self.parent_decl_node;
14491449
if (src_node) |off| self.parent_decl_node = self.relativeToNodeIndex(off);
@@ -1454,15 +1454,20 @@ const Writer = struct {
14541454
extra_index = try self.writeDecls(stream, decls_len, extra_index);
14551455
self.indent -= 2;
14561456
try stream.writeByteNTimes(' ', self.indent);
1457-
try stream.writeAll("}, ");
1457+
try stream.writeAll("}");
14581458
}
14591459

1460-
assert(fields_len != 0);
1461-
14621460
if (tag_type_ref != .none) {
1463-
try self.writeInstRef(stream, tag_type_ref);
14641461
try stream.writeAll(", ");
1462+
try self.writeInstRef(stream, tag_type_ref);
1463+
}
1464+
1465+
if (fields_len == 0) {
1466+
try stream.writeAll("})");
1467+
try self.writeSrcNode(stream, src_node);
1468+
return;
14651469
}
1470+
try stream.writeAll(", ");
14661471

14671472
const body = self.code.extra[extra_index..][0..body_len];
14681473
extra_index += body.len;

src/type.zig

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2488,7 +2488,7 @@ pub const Type = extern union {
24882488
},
24892489
.union_safety_tagged, .union_tagged => {
24902490
const union_obj = ty.cast(Payload.Union).?.data;
2491-
if (try union_obj.tag_ty.hasRuntimeBitsAdvanced(ignore_comptime_only, sema_kit)) {
2491+
if (union_obj.fields.count() > 0 and try union_obj.tag_ty.hasRuntimeBitsAdvanced(ignore_comptime_only, sema_kit)) {
24922492
return true;
24932493
}
24942494
if (sema_kit) |sk| {
@@ -3113,6 +3113,9 @@ pub const Type = extern union {
31133113
.sema_kit => unreachable, // handled above
31143114
.lazy => |arena| return AbiAlignmentAdvanced{ .val = try Value.Tag.lazy_align.create(arena, ty) },
31153115
};
3116+
if (union_obj.fields.count() == 0) {
3117+
return AbiAlignmentAdvanced{ .scalar = @boolToInt(union_obj.layout == .Extern) };
3118+
}
31163119

31173120
var max_align: u32 = 0;
31183121
if (have_tag) max_align = union_obj.tag_ty.abiAlignment(target);

test/behavior.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ test {
167167
if (builtin.zig_backend != .stage1) {
168168
_ = @import("behavior/decltest.zig");
169169
_ = @import("behavior/packed_struct_explicit_backing_int.zig");
170+
_ = @import("behavior/empty_union.zig");
170171
}
171172

172173
if (builtin.os.tag != .wasi) {

test/behavior/empty_union.zig

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
const builtin = @import("builtin");
2+
const std = @import("std");
3+
const expect = std.testing.expect;
4+
5+
test "switch on empty enum" {
6+
if (builtin.zig_backend == .stage2_c) return error.SkipZigTest; // TODO
7+
if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO
8+
9+
const E = enum {};
10+
var e: E = undefined;
11+
switch (e) {}
12+
}
13+
14+
test "switch on empty enum with a specified tag type" {
15+
if (builtin.zig_backend == .stage2_c) return error.SkipZigTest; // TODO
16+
if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO
17+
18+
const E = enum(u8) {};
19+
var e: E = undefined;
20+
switch (e) {}
21+
}
22+
23+
test "switch on empty auto numbered tagged union" {
24+
if (builtin.zig_backend == .stage2_c) return error.SkipZigTest; // TODO
25+
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
26+
if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO
27+
28+
const U = union(enum(u8)) {};
29+
var u: U = undefined;
30+
switch (u) {}
31+
}
32+
33+
test "switch on empty tagged union" {
34+
if (builtin.zig_backend == .stage2_c) return error.SkipZigTest; // TODO
35+
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
36+
if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO
37+
38+
const E = enum {};
39+
const U = union(E) {};
40+
var u: U = undefined;
41+
switch (u) {}
42+
}
43+
44+
test "empty union" {
45+
const U = union {};
46+
try expect(@sizeOf(U) == 0);
47+
try expect(@alignOf(U) == 0);
48+
}
49+
50+
test "empty extern union" {
51+
const U = extern union {};
52+
try expect(@sizeOf(U) == 0);
53+
try expect(@alignOf(U) == 1);
54+
}

test/cases/compile_errors/enum_with_0_fields.zig

Lines changed: 0 additions & 7 deletions
This file was deleted.

test/cases/compile_errors/reify_type_for_exhaustive_enum_with_zero_fields.zig

Lines changed: 0 additions & 18 deletions
This file was deleted.

0 commit comments

Comments
 (0)