tally/engine/src/Integer.zig

179 lines
6.6 KiB
Zig

//! 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(),
);
}