//! A fixed-width integer: a two's complement bit pattern plus the type that says //! how to read it. //! //! Programmer mode computes on patterns of a chosen width rather than on the exact //! rationals `number.zig` provides, and standard mode drops into the same //! representation for the bitwise operators (FR-2.12). This file is that //! representation; `bitwise.zig` is the operations on it. //! //! There used to be three shapes of the same idea: `Integer{raw, bit_width, //! signedness}`, `ProgrammerConfig{bit_width, signedness, display_endian}`, and a //! `Domain{bit_width, signedness}` inside `bitwise.zig`. `IntType` is the one //! concept each was carrying a copy of: a value pairs it with bits, and a //! configuration pairs it with a display preference. const std = @import("std"); const Integer = @This(); /// The bit pattern. May carry bits above the width; every reader masks. raw: u128, /// How to read `raw`. int_type: IntType, pub fn init(raw: u128, int_type: IntType) Integer { return .{ .raw = raw, .int_type = int_type }; } /// Apply the width mask, truncating to the configured width. pub fn masked(self: Integer) u128 { return self.raw & self.int_type.mask(); } /// Interpret as a signed value, sign-extended from the width. pub fn signedValue(self: Integer) i128 { return self.int_type.signExtend(self.raw); } /// Interpret as an unsigned value, which is just the mask. pub fn unsignedValue(self: Integer) u128 { return self.masked(); } /// A fixed-width integer type: how wide, and whether the top bit is a sign. /// /// Everything that interprets a pattern needs exactly this pair, which is why it /// was being reassembled in three places. The default is 64-bit signed, which is /// also standard mode's fixed type. pub const IntType = struct { width: BitWidth = .bits64, signedness: Signedness = .signed, pub fn mask(self: IntType) u128 { return self.width.mask(); } pub fn bits(self: IntType) u8 { return self.width.bits(); } /// The top bit's position, whether or not this type treats it as a sign. pub fn topBit(self: IntType) u128 { return @as(u128, 1) << @intCast(self.bits() - 1); } /// True when the pattern denotes a negative number in this type. An unsigned /// type has no negative values, so `>>` is a zero fill there and a large shift /// distance is large rather than negative. pub fn isNegative(self: IntType, value: u128) bool { return self.signedness == .signed and (value & self.mask()) & self.topBit() != 0; } /// Sign-extend the pattern to a full i128. pub fn signExtend(self: IntType, value: u128) i128 { const m = value & self.mask(); if (self.isNegative(m)) return @bitCast(m | ~self.mask()); return @intCast(m); } }; /// Configurable integer bit width. The tag is the bit count. pub const BitWidth = enum(u8) { bits8 = 8, bits16 = 16, bits32 = 32, bits64 = 64, bits128 = 128, /// All bits set within the width. Shifts a full mask down, because /// `(1 << bits) - 1` overflows at 128. pub fn mask(self: BitWidth) u128 { const shift: u7 = @intCast(128 - @as(u16, self.bits())); return @as(u128, std.math.maxInt(u128)) >> shift; } /// Returns the number of bits as a plain integer. pub fn bits(self: BitWidth) u8 { return @intFromEnum(self); } }; /// Whether the top bit of a pattern is a sign. pub const Signedness = enum { signed, unsigned, }; // -- Tests -- const testing = std.testing; test "BitWidth.mask" { try testing.expectEqual(@as(u128, 0xFF), BitWidth.bits8.mask()); try testing.expectEqual(@as(u128, 0xFFFF), BitWidth.bits16.mask()); try testing.expectEqual(@as(u128, 0xFFFF_FFFF), BitWidth.bits32.mask()); try testing.expectEqual(@as(u128, 0xFFFF_FFFF_FFFF_FFFF), BitWidth.bits64.mask()); try testing.expectEqual(@as(u128, 0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF), BitWidth.bits128.mask()); } test "BitWidth.mask: exactly `bits` low bits are set, at every width" { for (std.enums.values(BitWidth)) |bw| { try testing.expectEqual(@as(u8, bw.bits()), @popCount(bw.mask())); try testing.expectEqual(@as(u128, 1), bw.mask() & 1); } } test "IntType: signExtend reads the top bit only when the type is signed" { const i8_type: IntType = .{ .width = .bits8, .signedness = .signed }; const u8_type: IntType = .{ .width = .bits8, .signedness = .unsigned }; try testing.expectEqual(@as(i128, -1), i8_type.signExtend(0xFF)); try testing.expectEqual(@as(i128, 255), u8_type.signExtend(0xFF)); try testing.expectEqual(@as(i128, -128), i8_type.signExtend(0x80)); try testing.expectEqual(@as(i128, 127), i8_type.signExtend(0x7F)); try testing.expect(i8_type.isNegative(0x80)); try testing.expect(!u8_type.isNegative(0x80)); // A 128-bit type has no bits above the width to fill. const i128_type: IntType = .{ .width = .bits128, .signedness = .signed }; try testing.expectEqual(@as(i128, -1), i128_type.signExtend(i128_type.mask())); } test "IntType: the default is 64-bit signed, which standard mode uses" { const default: IntType = .{}; try testing.expectEqual(BitWidth.bits64, default.width); try testing.expectEqual(Signedness.signed, default.signedness); try testing.expectEqual(@as(i128, -1), default.signExtend(default.mask())); } test "bits above the width never reach an interpretation" { const i8_type: IntType = .{ .width = .bits8 }; // The high bits are noise from a wider computation. try testing.expectEqual(@as(i128, -1), i8_type.signExtend(0xDEAD_00FF)); try testing.expectEqual(@as(u128, 0xFF), init(0xDEAD_00FF, i8_type).unsignedValue()); } test "signedValue" { const i8_type: IntType = .{ .width = .bits8 }; try testing.expectEqual(@as(i128, -1), init(0xFF, i8_type).signedValue()); try testing.expectEqual(@as(i128, 127), init(0x7F, i8_type).signedValue()); try testing.expectEqual(@as(i128, -128), init(0x80, i8_type).signedValue()); const i32_type: IntType = .{ .width = .bits32 }; try testing.expectEqual(@as(i128, -1), init(0xFFFF_FFFF, i32_type).signedValue()); } test "unsignedValue masks correctly" { const u8_type: IntType = .{ .width = .bits8, .signedness = .unsigned }; try testing.expectEqual(@as(u128, 0xFF), init(0x1FF, u8_type).unsignedValue()); } test "the same bits read two ways, which is why the type travels with the value" { const bits: u128 = 0xFF; try testing.expectEqual(@as(i128, -1), init(bits, .{ .width = .bits8 }).signedValue()); try testing.expectEqual( @as(i128, 255), init(bits, .{ .width = .bits8, .signedness = .unsigned }).signedValue(), ); }