sema: handle big-endian when bitcasting between different-sized union fields

Updated the tests to also run at runtime, and moved them to union.zig
This commit is contained in:
kcbanner 2023-10-02 02:06:37 -04:00
parent d657b6c0e2
commit fb33bc99e1
3 changed files with 247 additions and 179 deletions

View File

@ -27251,7 +27251,7 @@ fn unionFieldVal(
return sema.failWithOwnedErrorMsg(block, msg);
}
},
.Packed, .Extern => {
.Packed, .Extern => |layout| {
if (tag_matches) {
return Air.internedToRef(un.val);
} else {
@ -27260,7 +27260,7 @@ fn unionFieldVal(
else
union_ty.unionFieldType(un.tag.toValue(), mod).?;
if (try sema.bitCastUnionFieldVal(block, src, un.val.toValue(), old_ty, field_ty)) |new_val| {
if (try sema.bitCastUnionFieldVal(block, src, un.val.toValue(), old_ty, field_ty, layout)) |new_val| {
return Air.internedToRef(new_val.toIntern());
}
}
@ -30751,26 +30751,50 @@ fn bitCastUnionFieldVal(
val: Value,
old_ty: Type,
field_ty: Type,
layout: std.builtin.Type.ContainerLayout,
) !?Value {
const mod = sema.mod;
if (old_ty.eql(field_ty, mod)) return val;
const old_size = try sema.usizeCast(block, src, old_ty.abiSize(mod));
const field_size = try sema.usizeCast(block, src, field_ty.abiSize(mod));
const endian = mod.getTarget().cpu.arch.endian();
const buffer = try sema.gpa.alloc(u8, @max(old_size, field_size));
defer sema.gpa.free(buffer);
val.writeToMemory(old_ty, mod, buffer) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
error.ReinterpretDeclRef => return null,
error.IllDefinedMemoryLayout => unreachable, // Sema was supposed to emit a compile error already
error.Unimplemented => return sema.fail(block, src, "TODO: implement writeToMemory for type '{}'", .{old_ty.fmt(mod)}),
// Reading a larger value means we need to reinterpret from undefined bytes.
const offset = switch (layout) {
.Extern => offset: {
if (field_size > old_size) @memset(buffer[old_size..], 0xaa);
val.writeToMemory(old_ty, mod, buffer) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
error.ReinterpretDeclRef => return null,
error.IllDefinedMemoryLayout => unreachable, // Sema was supposed to emit a compile error already
error.Unimplemented => return sema.fail(block, src, "TODO: implement writeToMemory for type '{}'", .{old_ty.fmt(mod)}),
};
break :offset 0;
},
.Packed => offset: {
if (field_size > old_size) {
const min_size = @max(old_size, 1);
switch (endian) {
.Little => @memset(buffer[min_size - 1 ..], 0xaa),
.Big => @memset(buffer[0 .. buffer.len - min_size + 1], 0xaa),
}
}
val.writeToPackedMemory(old_ty, mod, buffer, 0) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
error.ReinterpretDeclRef => return null,
};
break :offset if (endian == .Big) buffer.len - field_size else 0;
},
.Auto => unreachable,
};
// Reading a larger value means we need to reinterpret from undefined bytes
if (field_size > old_size) @memset(buffer[old_size..], 0xaa);
return Value.readFromMemory(field_ty, mod, buffer[0..], sema.arena) catch |err| switch (err) {
return Value.readFromMemory(field_ty, mod, buffer[offset..], sema.arena) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
error.IllDefinedMemoryLayout => unreachable,
error.Unimplemented => return sema.fail(block, src, "TODO: implement readFromMemory for type '{}'", .{field_ty.fmt(mod)}),

View File

@ -455,164 +455,3 @@ test "type pun null pointer-like optional" {
// note that expectEqual hides the bug
try testing.expect(@as(*const ?*i8, @ptrCast(&p)).* == null);
}
test "reinterpret extern union" {
const U = extern union {
foo: u8,
baz: u32 align(8),
bar: u32,
};
comptime {
{
// Undefined initialization
const u = blk: {
var u: U = undefined;
@memset(std.mem.asBytes(&u), 0);
u.bar = 0xbbbbbbbb;
u.foo = 0x2a;
break :blk u;
};
try testing.expectEqual(@as(u8, 0x2a), u.foo);
try testing.expectEqual(@as(u32, 0xbbbbbb2a), u.bar);
try testing.expectEqual(@as(u64, 0x00000000_bbbbbb2a), u.baz);
}
{
// Union initialization
var u: U = .{
.foo = 0x2a,
};
try testing.expectEqual(@as(u8, 0x2a), u.foo);
try testing.expectEqual(@as(u32, 0x2a), u.bar & 0xff);
try testing.expectEqual(@as(u64, 0x2a), u.baz & 0xff);
// Writing to a larger field
u = .{
.baz = 0xbbbbbbbb,
};
try testing.expectEqual(@as(u8, 0xbb), u.foo);
try testing.expectEqual(@as(u32, 0xbbbbbbbb), u.bar);
try testing.expectEqual(@as(u64, 0xbbbbbbbb), u.baz);
// Writing to the same field
u = .{
.baz = 0xcccccccc,
};
try testing.expectEqual(@as(u8, 0xcc), u.foo);
try testing.expectEqual(@as(u32, 0xcccccccc), u.bar);
try testing.expectEqual(@as(u64, 0xcccccccc), u.baz);
// Writing to a smaller field
u = .{
.foo = 0xdd,
};
try testing.expectEqual(@as(u8, 0xdd), u.foo);
try testing.expectEqual(@as(u32, 0xccccccdd), u.bar);
try testing.expectEqual(@as(u64, 0xccccccdd), u.baz);
}
}
}
test "reinterpret packed union" {
{
const U = packed union {
a: u32,
b: u8 align(8),
};
comptime {
var u: U = undefined;
@memset(std.mem.asBytes(&u), 42);
try testing.expect(0x2a2a2a2a == u.a);
try testing.expect(0x2a == u.b);
try testing.expectEqual(@as(u32, 0x2a2a2a2a), u.a);
try testing.expectEqual(0x2a, u.b);
}
}
{
const U = packed union {
a: u7,
b: u1,
};
const S = packed struct {
lsb: U,
msb: U,
};
comptime {
var s: S = undefined;
@memset(std.mem.asBytes(&s), 0x55);
try testing.expectEqual(@as(u7, 0x55), s.lsb.a);
try testing.expectEqual(@as(u1, 1), s.lsb.b);
try testing.expectEqual(@as(u7, 0x2a), s.msb.a);
try testing.expectEqual(@as(u1, 0), s.msb.b);
s.lsb.b = 0;
try testing.expectEqual(@as(u7, 0x54), s.lsb.a);
try testing.expectEqual(@as(u1, 0), s.lsb.b);
s.msb.b = 1;
try testing.expectEqual(@as(u7, 0x2b), s.msb.a);
try testing.expectEqual(@as(u1, 1), s.msb.b);
}
}
{
const U = packed union {
foo: u8,
bar: u29,
baz: u64,
};
comptime {
{
const u = blk: {
var u: U = undefined;
@memset(std.mem.asBytes(&u), 0);
u.baz = 0xbbbbbbbb;
u.foo = 0x2a;
break :blk u;
};
try testing.expectEqual(@as(u8, 0x2a), u.foo);
try testing.expectEqual(@as(u29, 0x1bbbbb2a), u.bar);
try testing.expectEqual(@as(u64, 0x00000000_bbbbbb2a), u.baz);
}
{
// Union initialization
var u: U = .{
.foo = 0x2a,
};
try testing.expectEqual(@as(u8, 0x2a), u.foo);
try testing.expectEqual(@as(u29, 0x2a), u.bar & 0xff);
try testing.expectEqual(@as(u64, 0x2a), u.baz & 0xff);
// Writing to a larger field
u = .{
.baz = 0xbbbbbbbb,
};
try testing.expectEqual(@as(u8, 0xbb), u.foo);
try testing.expectEqual(@as(u29, 0x1bbbbbbb), u.bar);
try testing.expectEqual(@as(u64, 0xbbbbbbbb), u.baz);
// Writing to the same field
u = .{
.baz = 0xcccccccc,
};
try testing.expectEqual(@as(u8, 0xcc), u.foo);
try testing.expectEqual(@as(u29, 0x0ccccccc), u.bar);
try testing.expectEqual(@as(u64, 0xcccccccc), u.baz);
// Writing to a smaller field
u = .{
.foo = 0xdd,
};
try testing.expectEqual(@as(u8, 0xdd), u.foo);
try testing.expectEqual(@as(u29, 0x0cccccdd), u.bar);
try testing.expectEqual(@as(u64, 0xccccccdd), u.baz);
}
}
}
}

View File

@ -1,5 +1,6 @@
const builtin = @import("builtin");
const std = @import("std");
const endian = builtin.cpu.arch.endian();
const expect = std.testing.expect;
const assert = std.debug.assert;
const expectEqual = std.testing.expectEqual;
@ -1660,15 +1661,219 @@ test "union with 128 bit integer" {
}
}
test "memset extern union at comptime" {
test "memset extern union" {
const U = extern union {
foo: u8,
bar: u32,
};
const u = comptime blk: {
var u: U = undefined;
@memset(std.mem.asBytes(&u), 0);
u.foo = 0;
break :blk u;
const S = struct {
fn doTheTest() !void {
var u: U = undefined;
@memset(std.mem.asBytes(&u), 0);
try expectEqual(@as(u8, 0), u.foo);
try expectEqual(@as(u32, 0), u.bar);
}
};
try expect(u.foo == 0);
try comptime S.doTheTest();
try S.doTheTest();
}
test "memset packed union" {
const U = packed union {
a: u32,
b: u8,
};
const S = struct {
fn doTheTest() !void {
var u: U = undefined;
@memset(std.mem.asBytes(&u), 42);
try expectEqual(@as(u32, 0x2a2a2a2a), u.a);
try expectEqual(@as(u8, 0x2a), u.b);
}
};
try comptime S.doTheTest();
if (builtin.cpu.arch.isWasm()) return error.SkipZigTest; // TODO
try S.doTheTest();
}
fn littleToNativeEndian(comptime T: type, v: T) T {
return if (endian == .Little) v else @byteSwap(v);
}
test "reinterpret extern union" {
const U = extern union {
foo: u8,
baz: u32 align(8),
bar: u32,
};
const S = struct {
fn doTheTest() !void {
{
// Undefined initialization
const u = blk: {
var u: U = undefined;
@memset(std.mem.asBytes(&u), 0);
u.bar = 0xbbbbbbbb;
u.foo = 0x2a;
break :blk u;
};
try expectEqual(@as(u8, 0x2a), u.foo);
try expectEqual(littleToNativeEndian(u32, 0xbbbbbb2a), u.bar);
try expectEqual(littleToNativeEndian(u32, 0xbbbbbb2a), u.baz);
}
{
// Union initialization
var u: U = .{
.foo = 0x2a,
};
{
const expected, const mask = switch (endian) {
.Little => .{ 0x2a, 0xff },
.Big => .{ 0x2a000000, 0xff000000 },
};
try expectEqual(@as(u8, 0x2a), u.foo);
try expectEqual(@as(u32, expected), u.bar & mask);
try expectEqual(@as(u32, expected), u.baz & mask);
}
// Writing to a larger field
u.baz = 0xbbbbbbbb;
try expectEqual(@as(u8, 0xbb), u.foo);
try expectEqual(@as(u32, 0xbbbbbbbb), u.bar);
try expectEqual(@as(u32, 0xbbbbbbbb), u.baz);
// Writing to the same field
u.baz = 0xcccccccc;
try expectEqual(@as(u8, 0xcc), u.foo);
try expectEqual(@as(u32, 0xcccccccc), u.bar);
try expectEqual(@as(u32, 0xcccccccc), u.baz);
// Writing to a smaller field
u.foo = 0xdd;
try expectEqual(@as(u8, 0xdd), u.foo);
try expectEqual(littleToNativeEndian(u32, 0xccccccdd), u.bar);
try expectEqual(littleToNativeEndian(u32, 0xccccccdd), u.baz);
}
}
};
try comptime S.doTheTest();
if (builtin.zig_backend == .stage2_llvm) return error.SkipZigTest; // TODO
try S.doTheTest();
}
test "reinterpret packed union" {
const U = packed union {
foo: u8,
bar: u29,
baz: u64,
qux: u12,
};
const S = struct {
fn doTheTest() !void {
{
const u = blk: {
var u: U = undefined;
@memset(std.mem.asBytes(&u), 0);
u.baz = 0xbbbbbbbb;
u.qux = 0xe2a;
break :blk u;
};
try expectEqual(@as(u8, 0x2a), u.foo);
try expectEqual(@as(u12, 0xe2a), u.qux);
// https://github.com/ziglang/zig/issues/17360
if (@inComptime()) {
try expectEqual(@as(u29, 0x1bbbbe2a), u.bar);
try expectEqual(@as(u64, 0xbbbbbe2a), u.baz);
}
}
{
// Union initialization
var u: U = .{
.qux = 0xe2a,
};
try expectEqual(@as(u8, 0x2a), u.foo);
try expectEqual(@as(u12, 0xe2a), u.qux);
try expectEqual(@as(u29, 0xe2a), u.bar & 0xfff);
try expectEqual(@as(u64, 0xe2a), u.baz & 0xfff);
// Writing to a larger field
u.baz = 0xbbbbbbbb;
try expectEqual(@as(u8, 0xbb), u.foo);
try expectEqual(@as(u12, 0xbbb), u.qux);
try expectEqual(@as(u29, 0x1bbbbbbb), u.bar);
try expectEqual(@as(u64, 0xbbbbbbbb), u.baz);
// Writing to the same field
u.baz = 0xcccccccc;
try expectEqual(@as(u8, 0xcc), u.foo);
try expectEqual(@as(u12, 0xccc), u.qux);
try expectEqual(@as(u29, 0x0ccccccc), u.bar);
try expectEqual(@as(u64, 0xcccccccc), u.baz);
// Writing to a smaller field
u.foo = 0xdd;
try expectEqual(@as(u8, 0xdd), u.foo);
try expectEqual(@as(u12, 0xcdd), u.qux);
try expectEqual(@as(u29, 0x0cccccdd), u.bar);
try expectEqual(@as(u64, 0xccccccdd), u.baz);
}
}
};
try comptime S.doTheTest();
if (builtin.zig_backend == .stage2_c) return error.SkipZigTest; // TODO
if (builtin.cpu.arch.isWasm()) return error.SkipZigTest; // TODO
try S.doTheTest();
}
test "reinterpret packed union inside packed struct" {
const U = packed union {
a: u7,
b: u1,
};
const V = packed struct {
lo: U,
hi: U,
};
const S = struct {
fn doTheTest() !void {
var v: V = undefined;
@memset(std.mem.asBytes(&v), 0x55);
try expectEqual(@as(u7, 0x55), v.lo.a);
try expectEqual(@as(u1, 1), v.lo.b);
try expectEqual(@as(u7, 0x2a), v.hi.a);
try expectEqual(@as(u1, 0), v.hi.b);
v.lo.b = 0;
try expectEqual(@as(u7, 0x54), v.lo.a);
try expectEqual(@as(u1, 0), v.lo.b);
v.hi.b = 1;
try expectEqual(@as(u7, 0x2b), v.hi.a);
try expectEqual(@as(u1, 1), v.hi.b);
}
};
try comptime S.doTheTest();
if (builtin.zig_backend == .stage2_c) return error.SkipZigTest; // TODO
try S.doTheTest();
}