zig/test/behavior/union.zig
Andrew Kelley b34f994c0b stage2: type system treats fn ptr and body separately
This commit updates stage2 to enforce the property that the syntax
`fn()void` is a function *body* not a *pointer*. To get a pointer, the
syntax `*const fn()void` is required.

ZIR puts function alignment into the func instruction rather than the
decl because this way it makes it into function types. LLVM backend
respects function alignments.

Struct and Union have methods `fieldSrcLoc` to help look up source
locations of their fields. These trigger full loading, tokenization, and
parsing of source files, so should only be called once it is confirmed
that an error message needs to be printed.

There are some nice new error hints for explaining why a type is
required to be comptime, particularly for structs that contain function
body types.

`Type.requiresComptime` is now moved into Sema because it can fail and
might need to trigger field type resolution. Comptime pointer loading
takes into account types that do not have a well-defined memory layout
and does not try to compute a byte offset for them.

`fn()void` syntax no longer secretly makes a pointer. You get a function
body type, which requires comptime. However a pointer to a function body
can be runtime known (obviously).

Compile errors that report "expected pointer, found ..." are factored
out into convenience functions `checkPtrOperand` and `checkPtrType` and
have a note about function pointers.

Implemented `Value.hash` for functions, enum literals, and undefined values.

stage1 is not updated to this (yet?), so some workarounds and disabled
tests are needed to keep everything working. Should we update stage1 to
these new type semantics? Yes probably because I don't want to add too
much conditional compilation logic in the std lib for the different
backends.
2022-01-24 21:47:53 -07:00

491 lines
11 KiB
Zig

const builtin = @import("builtin");
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 "init union with runtime value" {
var foo: Foo = undefined;
setFloat(&foo, 12.34);
try expect(foo.float == 12.34);
setInt(&foo, 42);
try expect(foo.int == 42);
}
fn setFloat(foo: *Foo, x: f64) void {
foo.* = Foo{ .float = x };
}
fn setInt(foo: *Foo, x: i32) void {
foo.* = Foo{ .int = x };
}
test "comptime union field access" {
comptime {
var foo = Foo{ .int = 0 };
try expect(foo.int == 0);
foo = Foo{ .float = 42.42 };
try expect(foo.float == 42.42);
}
}
const FooExtern = extern union {
float: f64,
int: i32,
};
test "basic extern unions" {
var foo = FooExtern{ .int = 1 };
try expect(foo.int == 1);
foo.float = 12.34;
try expect(foo.float == 12.34);
}
const ExternPtrOrInt = extern union {
ptr: *u8,
int: u64,
};
test "extern union size" {
comptime try expect(@sizeOf(ExternPtrOrInt) == 8);
}
test "0-sized extern union definition" {
const U = extern union {
a: void,
const f = 1;
};
try expect(U.f == 1);
}
const Value = union(enum) {
Int: u64,
Array: [9]u8,
};
const Agg = struct {
val1: Value,
val2: Value,
};
const v1 = Value{ .Int = 1234 };
const v2 = Value{ .Array = [_]u8{3} ** 9 };
const err = @as(anyerror!Agg, Agg{
.val1 = v1,
.val2 = v2,
});
const array = [_]Value{ v1, v2, v1, v2 };
test "unions embedded in aggregate types" {
switch (array[1]) {
Value.Array => |arr| try expect(arr[4] == 3),
else => unreachable,
}
switch ((err catch unreachable).val1) {
Value.Int => |x| try expect(x == 1234),
else => unreachable,
}
}
test "access a member of tagged union with conflicting enum tag name" {
const Bar = union(enum) {
A: A,
B: B,
const A = u8;
const B = void;
};
comptime try expect(Bar.A == u8);
}
test "constant tagged union with payload" {
var empty = TaggedUnionWithPayload{ .Empty = {} };
var full = TaggedUnionWithPayload{ .Full = 13 };
shouldBeEmpty(empty);
shouldBeNotEmpty(full);
}
fn shouldBeEmpty(x: TaggedUnionWithPayload) void {
switch (x) {
TaggedUnionWithPayload.Empty => {},
else => unreachable,
}
}
fn shouldBeNotEmpty(x: TaggedUnionWithPayload) void {
switch (x) {
TaggedUnionWithPayload.Empty => unreachable,
else => {},
}
}
const TaggedUnionWithPayload = union(enum) {
Empty: void,
Full: i32,
};
test "union alignment" {
comptime {
try expect(@alignOf(AlignTestTaggedUnion) >= @alignOf([9]u8));
try expect(@alignOf(AlignTestTaggedUnion) >= @alignOf(u64));
}
}
const AlignTestTaggedUnion = union(enum) {
A: [9]u8,
B: u64,
};
const Letter = enum { A, B, C };
const Payload = union(Letter) {
A: i32,
B: f64,
C: bool,
};
test "union with specified enum tag" {
try doTest();
comptime try doTest();
}
test "packed union generates correctly aligned LLVM type" {
if (builtin.zig_backend == .stage1) return error.SkipZigTest;
const U = packed union {
f1: *const fn () error{TestUnexpectedResult}!void,
f2: u32,
};
var foo = [_]U{
U{ .f1 = doTest },
U{ .f2 = 0 },
};
try foo[0].f1();
}
fn doTest() error{TestUnexpectedResult}!void {
try expect((try bar(Payload{ .A = 1234 })) == -10);
}
fn bar(value: Payload) error{TestUnexpectedResult}!i32 {
try expect(@as(Letter, value) == Letter.A);
return switch (value) {
Payload.A => |x| return x - 1244,
Payload.B => |x| if (x == 12.34) @as(i32, 20) else 21,
Payload.C => |x| if (x) @as(i32, 30) else 31,
};
}
fn testComparison() !void {
var x = Payload{ .A = 42 };
try expect(x == .A);
try expect(x != .B);
try expect(x != .C);
try expect((x == .B) == false);
try expect((x == .C) == false);
try expect((x != .A) == false);
}
test "comparison between union and enum literal" {
try testComparison();
comptime try testComparison();
}
const TheTag = enum { A, B, C };
const TheUnion = union(TheTag) {
A: i32,
B: i32,
C: i32,
};
test "cast union to tag type of union" {
try testCastUnionToTag();
comptime try testCastUnionToTag();
}
fn testCastUnionToTag() !void {
var u = TheUnion{ .B = 1234 };
try expect(@as(TheTag, u) == TheTag.B);
}
test "union field access gives the enum values" {
try expect(TheUnion.A == TheTag.A);
try expect(TheUnion.B == TheTag.B);
try expect(TheUnion.C == TheTag.C);
}
test "cast tag type of union to union" {
var x: Value2 = Letter2.B;
try expect(@as(Letter2, x) == Letter2.B);
}
const Letter2 = enum { A, B, C };
const Value2 = union(Letter2) {
A: i32,
B,
C,
};
test "implicit cast union to its tag type" {
var x: Value2 = Letter2.B;
try expect(x == Letter2.B);
try giveMeLetterB(x);
}
fn giveMeLetterB(x: Letter2) !void {
try expect(x == Value2.B);
}
// TODO it looks like this test intended to test packed unions, but this is not a packed
// union. go through git history and find out what happened.
pub const PackThis = union(enum) {
Invalid: bool,
StringLiteral: u2,
};
test "constant packed union" {
try testConstPackedUnion(&[_]PackThis{PackThis{ .StringLiteral = 1 }});
}
fn testConstPackedUnion(expected_tokens: []const PackThis) !void {
try expect(expected_tokens[0].StringLiteral == 1);
}
const MultipleChoice = union(enum(u32)) {
A = 20,
B = 40,
C = 60,
D = 1000,
};
test "simple union(enum(u32))" {
var x = MultipleChoice.C;
try expect(x == MultipleChoice.C);
try expect(@enumToInt(@as(Tag(MultipleChoice), x)) == 60);
}
const PackedPtrOrInt = packed union {
ptr: *u8,
int: u64,
};
test "packed union size" {
comptime try expect(@sizeOf(PackedPtrOrInt) == 8);
}
const ZeroBits = union {
OnlyField: void,
};
test "union with only 1 field which is void should be zero bits" {
comptime try expect(@sizeOf(ZeroBits) == 0);
}
test "tagged union initialization with runtime void" {
try expect(testTaggedUnionInit({}));
}
const TaggedUnionWithAVoid = union(enum) {
A,
B: i32,
};
fn testTaggedUnionInit(x: anytype) bool {
const y = TaggedUnionWithAVoid{ .A = x };
return @as(Tag(TaggedUnionWithAVoid), y) == TaggedUnionWithAVoid.A;
}
pub const UnionEnumNoPayloads = union(enum) { A, B };
test "tagged union with no payloads" {
const a = UnionEnumNoPayloads{ .B = {} };
switch (a) {
Tag(UnionEnumNoPayloads).A => @panic("wrong"),
Tag(UnionEnumNoPayloads).B => {},
}
}
test "union with only 1 field casted to its enum type" {
const Literal = union(enum) {
Number: f64,
Bool: bool,
};
const Expr = union(enum) {
Literal: Literal,
};
var e = Expr{ .Literal = Literal{ .Bool = true } };
const ExprTag = Tag(Expr);
comptime try expect(Tag(ExprTag) == u0);
var t = @as(ExprTag, e);
try expect(t == Expr.Literal);
}
test "union with one member defaults to u0 tag type" {
const U0 = union(enum) {
X: u32,
};
comptime try expect(Tag(Tag(U0)) == u0);
}
const Foo1 = union(enum) {
f: struct {
x: usize,
},
};
var glbl: Foo1 = undefined;
test "global union with single field is correctly initialized" {
glbl = Foo1{
.f = @typeInfo(Foo1).Union.fields[0].field_type{ .x = 123 },
};
try expect(glbl.f.x == 123);
}
pub const FooUnion = union(enum) {
U0: usize,
U1: u8,
};
var glbl_array: [2]FooUnion = undefined;
test "initialize global array of union" {
glbl_array[1] = FooUnion{ .U1 = 2 };
glbl_array[0] = FooUnion{ .U0 = 1 };
try expect(glbl_array[0].U0 == 1);
try expect(glbl_array[1].U1 == 2);
}
test "update the tag value for zero-sized unions" {
const S = union(enum) {
U0: void,
U1: void,
};
var x = S{ .U0 = {} };
try expect(x == .U0);
x = S{ .U1 = {} };
try expect(x == .U1);
}
test "union initializer generates padding only if needed" {
const U = union(enum) {
A: u24,
};
var v = U{ .A = 532 };
try expect(v.A == 532);
}
test "runtime tag name with single field" {
const U = union(enum) {
A: i32,
};
var v = U{ .A = 42 };
try expect(std.mem.eql(u8, @tagName(v), "A"));
}
test "method call on an empty union" {
const S = struct {
const MyUnion = union(MyUnionTag) {
pub const MyUnionTag = enum { X1, X2 };
X1: [0]u8,
X2: [0]u8,
pub fn useIt(self: *@This()) bool {
_ = self;
return true;
}
};
fn doTheTest() !void {
var u = MyUnion{ .X1 = [0]u8{} };
try expect(u.useIt());
}
};
try S.doTheTest();
comptime try S.doTheTest();
}
const Point = struct {
x: u64,
y: u64,
};
const TaggedFoo = union(enum) {
One: i32,
Two: Point,
Three: void,
};
const FooNoVoid = union(enum) {
One: i32,
Two: Point,
};
const Baz = enum { A, B, C, D };
test "tagged union type" {
const foo1 = TaggedFoo{ .One = 13 };
const foo2 = TaggedFoo{
.Two = Point{
.x = 1234,
.y = 5678,
},
};
try expect(foo1.One == 13);
try expect(foo2.Two.x == 1234 and foo2.Two.y == 5678);
const baz = Baz.B;
try expect(baz == Baz.B);
try expect(@typeInfo(TaggedFoo).Union.fields.len == 3);
try expect(@typeInfo(Baz).Enum.fields.len == 4);
try expect(@sizeOf(TaggedFoo) == @sizeOf(FooNoVoid));
try expect(@sizeOf(Baz) == 1);
}
test "tagged union as return value" {
switch (returnAnInt(13)) {
TaggedFoo.One => |value| try expect(value == 13),
else => unreachable,
}
}
fn returnAnInt(x: i32) TaggedFoo {
return TaggedFoo{ .One = x };
}
test "tagged union with all void fields but a meaningful tag" {
const S = struct {
const B = union(enum) {
c: C,
None,
};
const A = struct {
b: B,
};
const C = struct {};
fn doTheTest() !void {
var a: A = A{ .b = B{ .c = C{} } };
try expect(@as(Tag(B), a.b) == Tag(B).c);
a = A{ .b = B.None };
try expect(@as(Tag(B), a.b) == Tag(B).None);
}
};
try S.doTheTest();
// TODO enable the test at comptime too
//comptime try S.doTheTest();
}