zig
zig copied to clipboard
A packed struct member of a struct isn't equal to itself.
Zig Version
0.11.0-dev.900+0fb53bd24
Steps to Reproduce and Observed Behavior
const testing = @import("std").testing;
const DirectionalPad = packed struct {
x: i2 = 0,
y: i2 = 0,
};
const Input = struct {
dir: DirectionalPad = DirectionalPad{},
};
fn testPositive(input: Input) bool {
return input.dir.y == 1;
}
var t: bool = true;
pub fn createInput() Input {
return Input{
.dir = DirectionalPad{ .x = 0, .y = if (t) 1 else 0 },
};
}
test "packed struct member equality" {
const is_positive = testPositive(createInput());
try testing.expectEqual(is_positive, true);
}
It is the most minimal reproduction of this bug I could make. The test fails, however when "printf debugging" input.dir.y is 1, but comparing it with 1 fails.
Expected Behavior
That input.dir.y is 1 and testing equal to 1 results in 'true'.
seems like it doesn't reproduce for me:
$ zig test foo.zig
1/1 test.packed struct member equality... OK
All 1 tests passed.
What OS/Architecture are you on (output of zig env
maybe?)
x86_64-macos.11.6.4...11.6.4-none
I'm able to reproduce it on 0.11.0-dev.980+601ab9a25
/ x86_64-macos.12.6...12.6-none
.
I think this has to do with .y = if (t) 1 else 0 },
, if replaced with .y = 1
it passes.
Well this is interesting. I tested this on 3 machines, all with the given compiler version 0.11.0-dev.900+0fb53bd24
. Here are the results:
- linux-x86_64: PASS (issue does not reproduce)
- macos-AARCH64: PASS (issue does not reproduce)
- macos-x86_64: FAIL (issue DOES reproduce)
So looks like this is specific to macos x86_64!?!
I've minifed the example a bit more and added a couple comments in places that make the issue disappear:
const PackedStruct = packed struct {
y: i7 = 0, // i8 works
};
const Wrap = struct {
pack: PackedStruct,
};
var t = true; // const works
test "packed struct member equality" {
const wrap = Wrap{
.pack = PackedStruct{ .y = if (t) 1 else 0 },
};
try @import("std").testing.expectEqual(true, wrap.pack.y == 1);
}
I think the reason for needing the global var t
in place of just using true
is to force the initialization of the y
field in the packed struct to occur at runtime. This makes me think the issue could be with the codegen for initializing fields of packed structs on macos x86_64 that are less than 8 bits.
I was able to reproduce on my x86_64-linux
machine on 0.11.0-dev.1182+fd0fb26ab
I also managed to reproduce this on godbolt which also seems to use linux.
I was not able to reproduce the minified version of @marler8997 on linux. So we might have two different issues here.
I've run into the same (or at least similar) issue and have been able to reproduce it with the following code, using zig-linux-x86_64-0.11.0-dev.1605+abc9530a8 with target x86_64-linux.5.15...5.15-gnu.2.35
:
const std = @import("std");
const expect = std.testing.expect;
// Using the new tuple type declaration introduced in https://github.com/ziglang/zig/issues/4335
const BoardPosition = packed struct { // <---- Remove `packed` and everything works fine
u3,
u3,
};
/// index (values 0-63) enumerates the 64 squares of an 8x8 board, from top left to bottom right
fn u6toBoardPosition(index: u6) BoardPosition {
const row = @intCast(u3, index / 8);
const column = @intCast(u3, index % 8);
std.debug.print("row:column (raw) = {}:{}\n", .{ index / 8, index % 8 }); // outputs "7:7"
std.debug.print("row:column (cast) = {}:{}\n", .{ row, column }); // outputs "7:7"
return .{ row, column };
}
test "MyTest" {
const pos = u6toBoardPosition(@intCast(u6, 63));
std.debug.print("row:column (packed) = {}:{}\n", pos); // outputs "7:0" on my machine when using the packed struct, otherwise "7:7" as expected.
try expect(pos[0] == 7);
try expect(pos[1] == 7);
}
Output:
$ zig test mytest.zig
Test [1/1] test.MyTest... row:column (raw) = 7:7
row:column (cast) = 7:7
row:column (packed) = 7:0
Test [1/1] test.MyTest... FAIL (TestUnexpectedResult)
/home/user/opt/zig/lib/std/testing.zig:509:14: 0x20c457 in expect (test)
if (!ok) return error.TestUnexpectedResult;
^
/home/user/mytest/src/mytest.zig:26:5: 0x20c5dc in test.MyTest (test)
try expect(pos[1] == 7);
^
0 passed; 0 skipped; 1 failed.
error: the following test command failed with exit code 1:
/home/user/mytest/zig-cache/o/1e7bbe846c2a16d742a3844286427a2f/test
Interestingly, though, @DiskPoppy's code works just fine for me.
I just ran into this bug again, though this time it came in the form of a nasty Heisenbug, meaning that in 9 out of 10 (or sometimes 19 out of 20) test runs the test below yields an error, but every once in a while the test passes.
My apologies for the long code snippet – I tried to condense it further but that didn't work as expected and, given that I already spent hours debugging this, I have now given up and will stay as far away from packed structs as I can.
const std = @import("std");
pub const PieceType = enum {
KING,
QUEEN,
BISHOP,
KNIGHT,
ROOK,
PAWN,
};
pub const Color = enum {
WHITE,
BLACK,
};
pub const Piece = packed struct { // <--- Remove `packed` and the code works as expected.
color: Color,
type: PieceType,
};
pub const Square = struct {
y: u3,
x: u3,
};
pub const Board = struct {
const Self = @This();
array: [8][8]?Piece = .{
.{null} ** 8,
} ** 8,
pub inline fn get(self: Self, square: Square) ?Piece {
return self.array[square.y][square.x];
}
};
pub const ChessPosition = struct {
board: Board = .{},
our_color: Color,
};
pub fn chessPositionFromString(str: *const [64:0]u8) !ChessPosition {
const board = try boardFromString(str);
return ChessPosition{
.board = board,
.our_color = .WHITE,
};
}
pub fn boardFromString(str: *const [64:0]u8) !Board {
var board: Board = .{};
for (str, 0..) |c, index| {
const square = u6toSquare(@intCast(u6, index));
if (c == '.') {
board.array[square.y][square.x] = null;
} else {
board.array[square.y][square.x] = try charToPiece(c);
}
}
return board;
}
pub fn u6toSquare(index: u6) Square {
const row = @intCast(u3, index / 8);
const column = @intCast(u3, index % 8);
return .{ .y = row, .x = column };
}
fn charToPiece(c: u8) !Piece {
return .{
.type = try charToPieceType(c),
.color = if (std.ascii.isUpper(c)) Color.WHITE else Color.BLACK,
};
}
fn charToPieceType(c: u8) !PieceType {
return switch (std.ascii.toLower(c)) {
'p' => PieceType.PAWN,
'k' => PieceType.KING,
'q' => PieceType.QUEEN,
'b' => PieceType.BISHOP,
'n' => PieceType.KNIGHT,
'r' => PieceType.ROOK,
else => error.UnexpectedCharError,
};
}
test {
var initial_pos = try chessPositionFromString(
"rnbqkbnr" ++
"pp..pppp" ++
"..p....." ++
"...pP..." ++
"........" ++
"........" ++
"PPPP.PPP" ++
"RNBQKBNR"
);
const pawn_square = initial_pos.board.get(Square{ .y = 3, .x = 3});
try std.testing.expect(pawn_square.?.type == .PAWN);
try std.testing.expect(pawn_square.?.color == .BLACK);
}
Error (when it occurs):
$ zig test test.zig
Test [1/1] test_0... FAIL (TestUnexpectedResult)
/home/user/opt/zig/lib/std/testing.zig:509:14: 0x20c7c7 in expect (test)
if (!ok) return error.TestUnexpectedResult;
^
/home/user/zig-test/test.zig:107:5: 0x20c8c8 in test_0 (test)
try std.testing.expect(pawn_square.?.type == .PAWN);
^
0 passed; 0 skipped; 1 failed.
error: the following test command failed with exit code 1:
/home/user/zig-test/zig-cache/o/2fb76e24211cebd8013e87fd8c71dd2f/test
Importantly, it was exactly the same binary that ran in every single test run. Could it be that the behavior I'm seeing is related to where the OS happens to allocate the process memory(?) and this then causes, in the code generated for the packed struct, the type of nasty (un-)alignment issue that is discussed here?
$ zig env
{
"zig_exe": "/home/user/opt/zig/zig",
"lib_dir": "opt/zig/lib",
"std_dir": "opt/zig/lib/std",
"global_cache_dir": "/home/user/.cache/zig",
"version": "0.11.0-dev.1836+28364166e",
"target": "x86_64-linux.5.19...5.19-gnu.2.35"
}
$ uname -a
Linux hostname 5.19.0-35-generic #36~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Fri Feb 17 15:17:25 UTC 2 x86_64 x86_64 x86_64 GNU/Linux
$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 22.04.2 LTS
Release: 22.04
Codename: jammy
Note that the test failure depends on the value of uninitialized bits on the stack, so the target-specificity is a red herring.