zig icon indicating copy to clipboard operation
zig copied to clipboard

Potential compiler bug with packed struct and packed unions

Open ConnorRigby opened this issue 2 years ago • 3 comments

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

ConnorRigby avatar Mar 15 '23 12:03 ConnorRigby

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.

ConnorRigby avatar Mar 15 '23 13:03 ConnorRigby

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.

ConnorRigby avatar Mar 15 '23 14:03 ConnorRigby

maybe related #14706

nektro avatar Mar 15 '23 17:03 nektro

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", .{})
    }
}

saltzm avatar Jul 27 '23 20:07 saltzm

I went ahead and created a new issue, since I felt that'd be easiest.

saltzm avatar Jul 27 '23 20:07 saltzm