Potential compiler bug with packed struct and packed unions
Zig Version
0.11.0-dev.1812+26196be34
Steps to Reproduce and Observed Behavior
I've been hacking on an ARMv4t emulator in Zig. While defining the registers of that arch, i found that the tests in this code do not pass.
Specifically, anything that uses the hir structure in the Registers union will fail. The arguments to expectEqual are not comptime resolvable, and fail with:
registers.zig:253:44: error: unable to resolve comptime value
try std.testing.expectEqual(15, r.arm.hir.r15);
If i swap those arguments (which feels wrong)
Test [1/2] test_0... expected 167772160, found 15
Test [1/2] test_0... FAIL (TestExpectedEqual)
Where that random number produced doesn't really seem to change unless i change the struct.
const std = @import("std");
pub fn Banked(comptime T: type) type {
return packed struct {
usr: T,
fiq: T,
svc: T,
abt: T,
irq: T,
und: T,
pub fn reset() @This() {
return .{
.usr = std.mem.zeroes(T),
.fiq = std.mem.zeroes(T),
.svc = std.mem.zeroes(T),
.abt = std.mem.zeroes(T),
.irq = std.mem.zeroes(T),
.und = std.mem.zeroes(T),
};
}
};
}
pub fn FiqBanked(comptime T: type) type {
return packed struct {
usr: T,
fiq: T,
pub fn reset() @This() {
return .{
.usr = std.mem.zeroes(T),
.fiq = std.mem.zeroes(T),
};
}
};
}
/// Program status register
pub const ProgramStatusRegister = packed struct {
pub const Mode = enum(u5) {
usr = 0b00001,
fiq = 0b10001,
irq = 0b01001,
svc = 0b11001,
abt = 0b11101,
und = 0b11011,
sys = 0b11111,
};
pub const Control = packed struct {
/// mode bits
mode: Mode,
/// thumb state
t: u1,
/// fast interupt disable
f: u1,
/// interupt disable
i: u1,
};
test {
try std.testing.expectEqual(@sizeOf(Control), 1);
}
pub const Condition = packed struct {
/// overflow
v: u1,
/// carry, borrow, extend
c: u1,
/// zero
z: u1,
/// Negative or less than
n: u1
};
/// control bits
control: Control,
/// reserved for future use
reserved: u10,
/// condition code flags
condition: Condition
};
pub const ProgramStatus = packed struct {
s: Banked(ProgramStatusRegister),
c: Banked(ProgramStatusRegister),
};
pub const Low = packed struct {
r07: u32,
r06: u32,
r05: u32,
r04: u32,
r03: u32,
r02: u32,
r01: u32,
r00: u32,
};
pub const Arm = packed struct {
pub const Hi = packed struct {
/// Program counter
r15: u32,
r14: Banked(u32),
r13: Banked(u32),
r12: FiqBanked(u32),
r11: FiqBanked(u32),
r10: FiqBanked(u32),
r09: FiqBanked(u32),
r08: FiqBanked(u32),
};
lor: Low,
psr: ProgramStatus,
hir: Hi,
};
pub const Thumb = packed struct {
pub const Hi = packed struct {
/// program counter
r15: u32,
/// stack pointer
r14: Banked(u32),
/// link pointer
r13: Banked(u32),
/// reserved should not be used
reserved: packed struct {
r12: FiqBanked(u32),
r11: FiqBanked(u32),
r10: FiqBanked(u32),
r09: FiqBanked(u32),
r08: FiqBanked(u32),
}
};
lor: Low,
psr: ProgramStatus,
hir: Hi,
};
// .{.mode = .svc, .i = 1, .f = 1, .t = 0}
pub const Registers = packed union {
arm: Arm,
thumb: Thumb,
pub fn reset() @This() {
return .{.arm = .{
.psr = .{
.c = .{
.usr = .{.control = .{.mode = .svc, .i = 1, .f = 1, .t = 0}, .reserved = 0, .condition = .{.c = 0, .z = 0, .n = 0, .v = 0}},
.fiq = .{.control = .{.mode = .svc, .i = 1, .f = 1, .t = 0}, .reserved = 0, .condition = .{.c = 0, .z = 0, .n = 0, .v = 0}},
.abt = .{.control = .{.mode = .svc, .i = 1, .f = 1, .t = 0}, .reserved = 0, .condition = .{.c = 0, .z = 0, .n = 0, .v = 0}},
.irq = .{.control = .{.mode = .svc, .i = 1, .f = 1, .t = 0}, .reserved = 0, .condition = .{.c = 0, .z = 0, .n = 0, .v = 0}},
.und = .{.control = .{.mode = .svc, .i = 1, .f = 1, .t = 0}, .reserved = 0, .condition = .{.c = 0, .z = 0, .n = 0, .v = 0}},
.svc = .{.control = .{.mode = .svc, .i = 1, .f = 1, .t = 0}, .reserved = 0, .condition = .{.c = 0, .z = 0, .n = 0, .v = 0}},
},
.s = .{
.usr = .{.control = .{.mode = .svc, .i = 1, .f = 1, .t = 0}, .reserved = 0, .condition = .{.c = 0, .z = 0, .n = 0, .v = 0}},
.fiq = .{.control = .{.mode = .svc, .i = 1, .f = 1, .t = 0}, .reserved = 0, .condition = .{.c = 0, .z = 0, .n = 0, .v = 0}},
.abt = .{.control = .{.mode = .svc, .i = 1, .f = 1, .t = 0}, .reserved = 0, .condition = .{.c = 0, .z = 0, .n = 0, .v = 0}},
.irq = .{.control = .{.mode = .svc, .i = 1, .f = 1, .t = 0}, .reserved = 0, .condition = .{.c = 0, .z = 0, .n = 0, .v = 0}},
.und = .{.control = .{.mode = .svc, .i = 1, .f = 1, .t = 0}, .reserved = 0, .condition = .{.c = 0, .z = 0, .n = 0, .v = 0}},
.svc = .{.control = .{.mode = .svc, .i = 1, .f = 1, .t = 0}, .reserved = 0, .condition = .{.c = 0, .z = 0, .n = 0, .v = 0}},
},
},
.hir = .{
// PC
.r15 = 0,
.r14 = Banked(u32).reset(),
.r13 = Banked(u32).reset(),
.r12 = FiqBanked(u32).reset(),
.r11 = FiqBanked(u32).reset(),
.r10 = FiqBanked(u32).reset(),
.r09 = FiqBanked(u32).reset(),
.r08 = FiqBanked(u32).reset(),
},
.lor = .{
.r07 = 0,
.r06 = 0,
.r05 = 0,
.r04 = 0,
.r03 = 0,
.r02 = 0,
.r01 = 0,
.r00 = 0,
}
}};
}
};
test {
std.testing.refAllDecls(@This());
// 1 word
try std.testing.expectEqual(@sizeOf(ProgramStatusRegister), 4);
// union access
try std.testing.expectEqual(@sizeOf(Arm), @sizeOf(Thumb));
// 6 banks
try std.testing.expectEqual(@sizeOf(Banked(u32)), 4 * 6);
// 2 banks
try std.testing.expectEqual(@sizeOf(FiqBanked(u32)), 4 * 2);
var r = Registers.reset();
// var r: Registers = undefined;
// r.arm.psr.c.usr = 0
r.arm.lor.r00 = 0;
r.arm.lor.r01 = 1;
r.arm.lor.r02 = 2;
r.arm.lor.r03 = 3;
r.arm.lor.r04 = 4;
r.arm.lor.r05 = 5;
r.arm.lor.r06 = 6;
r.arm.lor.r07 = 7;
// arm low register access
try std.testing.expectEqual(r.arm.lor.r00, 0);
try std.testing.expectEqual(r.arm.lor.r01, 1);
try std.testing.expectEqual(r.arm.lor.r02, 2);
try std.testing.expectEqual(r.arm.lor.r03, 3);
try std.testing.expectEqual(r.arm.lor.r04, 4);
try std.testing.expectEqual(r.arm.lor.r05, 5);
try std.testing.expectEqual(r.arm.lor.r06, 6);
try std.testing.expectEqual(r.arm.lor.r07, 7);
// thumb low register access
try std.testing.expectEqual(r.thumb.lor.r00, 0);
try std.testing.expectEqual(r.thumb.lor.r01, 1);
try std.testing.expectEqual(r.thumb.lor.r02, 2);
try std.testing.expectEqual(r.thumb.lor.r03, 3);
try std.testing.expectEqual(r.thumb.lor.r04, 4);
try std.testing.expectEqual(r.thumb.lor.r05, 5);
try std.testing.expectEqual(r.thumb.lor.r06, 6);
try std.testing.expectEqual(r.thumb.lor.r07, 7);
r.arm.hir.r08.usr = 8;
r.arm.hir.r09.usr = 9;
r.arm.hir.r10.usr = 10;
r.arm.hir.r11.usr = 11;
r.arm.hir.r12.usr = 12;
r.arm.hir.r13.usr = 13;
r.arm.hir.r14.usr = 14;
r.arm.hir.r15 = 15;
try std.testing.expectEqual(15, r.arm.hir.r15);
try std.testing.expectEqual(15, r.thumb.hir.r15);
try std.testing.expectEqual(r.thumb.hir.r14.usr, 14);
try std.testing.expectEqual(r.arm.hir.r14.usr, 14);
try std.testing.expectEqual(r.arm.hir.r13.usr, 13);
try std.testing.expectEqual(r.arm.hir.r12.usr, 12);
try std.testing.expectEqual(r.arm.hir.r11.usr, 11);
try std.testing.expectEqual(r.arm.hir.r10.usr, 10);
try std.testing.expectEqual(r.arm.hir.r09.usr, 9);
try std.testing.expectEqual(r.arm.hir.r08.usr, 8);
}
Expected Behavior
The tests in this module should pass
For good luck, i updated to zig 0.11.0-dev.1975+e17998b39 which is the lastest on the website as of right now and get the same issue.
I've been chatting with a few users in #zig-help on the Discord. The theory so far is that LLVM isn't going to like that massive integer type, which sort of makes sense. Seems like Zig could give at least a warning about it.
Updating all the structs to be extern instead of packed will fix it, at the cost of not being able to represent the ProgramStatusRegister structure directly since you can't pack unaligned types in extern. (IE u10 is invalid there).
Leaving the issue open since it still feels like a bug to me, but i have a fix for now. Feel free to close if deemed a non issue, or covered elsewhere.
maybe related #14706
I just posted in zig-help on the discord about an example involving packed structs/unions and enums:
This is the broken example, made as minimal as possible: Godbolt Removing "packed" from everything makes it work (as do a few other small changes, noted in comments): Godbolt
I can create a new GitHub issue for it if that seems worthwhile.
Here's the code, for convenience. It code should output "READ" but outputs "INSERT" instead.
const std = @import("std");
// If I make these structs have no fields, it works fine.
const ReadRequest = packed struct { key: i32 };
const InsertRequest = packed struct { entry: i32 };
const RequestType = enum {
read,
insert,
};
const RequestUnion = packed union {
read: ReadRequest,
insert: InsertRequest,
};
const Request = packed struct {
active_type: RequestType,
request: RequestUnion, // If I change this to an i32, it works fine.
const Self = @This();
fn initReadRequest(read: ReadRequest) @This() {
std.debug.print("\ninitReadRequest\n", .{});
return Self{
.active_type = RequestType.read,
.request = RequestUnion{ .read = read },
};
}
fn initInsertRequest(insert: InsertRequest) @This() {
return Self{
.active_type = RequestType.insert,
.request = RequestUnion{
.insert = insert,
},
};
}
};
pub fn main() void {
const r = Request.initReadRequest(ReadRequest{.key = 3});
switch (r.active_type) {
.read => std.debug.print("READ\n", .{}),
.insert => std.debug.print("INSERT\n", .{})
}
}
I went ahead and created a new issue, since I felt that'd be easiest.