340 lines
14 KiB
Zig
340 lines
14 KiB
Zig
//! Fixed-width integer operations: the bitwise operators, the shifts and the
|
|
//! rotations, for both modes.
|
|
//!
|
|
//! These eight binary operators and two unary ones used to be implemented twice,
|
|
//! once in `evaluator.zig` over a 64-bit projection and once in `programmer.zig`
|
|
//! over the configured width, and the two disagreed:
|
|
//!
|
|
//! - `>>` was logical in standard mode and arithmetic in programmer mode, so
|
|
//! `-8 >> 1` was 9223372036854775804 in one and -4 in the other.
|
|
//! - A shift distance at or beyond the width wrapped modulo 64 in standard mode
|
|
//! (`1 << 64` was 1) and clamped to `width - 1` in programmer mode (8-bit
|
|
//! `0xFF >>> 20` shifted by 7 and gave 1). Both turned "shift everything out"
|
|
//! into "shift a little".
|
|
//! - Standard mode ignored the configured width for shifts while honouring it for
|
|
//! `~`.
|
|
//!
|
|
//! FR-2.12 promises that every operator means the same thing in both modes, so
|
|
//! there is one implementation, over `Integer` values that carry their own width and
|
|
//! signedness. Standard mode is fixed at 64-bit signed; a different width is what
|
|
//! programmer mode is for (FR-2.3).
|
|
//!
|
|
//! Operands are `Integer`, not bare patterns plus a separate type. Taking them apart
|
|
//! made it possible to hand an operation a pattern from one width and a type from
|
|
//! another, which nothing would have noticed.
|
|
|
|
const std = @import("std");
|
|
const ast = @import("ast.zig");
|
|
const BinaryOp = ast.BinaryOp;
|
|
const Integer = @import("Integer.zig");
|
|
const BitWidth = Integer.BitWidth;
|
|
|
|
/// The one way a fixed-width operation can fail: a shift or rotate distance that is
|
|
/// negative in the operand's type. Everything else about these operators is total.
|
|
pub const Error = error{DomainError};
|
|
|
|
/// The operators this module implements.
|
|
///
|
|
/// A narrower set than `ast.BinaryOp`: the arithmetic operators are rational in
|
|
/// standard mode and wrapping in programmer mode, so they have nothing to share.
|
|
pub const Op = enum {
|
|
bit_and,
|
|
bit_or,
|
|
bit_xor,
|
|
shift_left,
|
|
/// Arithmetic right shift: vacated high bits take the value of the sign bit.
|
|
shift_right,
|
|
/// Logical right shift: vacated high bits are zero.
|
|
shift_right_logical,
|
|
rotate_left,
|
|
rotate_right,
|
|
};
|
|
|
|
/// The `Op` for a `BinaryOp`, or null for the arithmetic operators.
|
|
///
|
|
/// Callers use this at comptime from an `inline else` prong, so an operator added
|
|
/// to `BinaryOp` that belongs here is a compile error rather than a silent fall
|
|
/// through to the wrong tier.
|
|
pub fn fromBinaryOp(op: BinaryOp) ?Op {
|
|
return switch (op) {
|
|
.bit_and => .bit_and,
|
|
.bit_or => .bit_or,
|
|
.bit_xor => .bit_xor,
|
|
.shift_left => .shift_left,
|
|
.shift_right => .shift_right,
|
|
.shift_right_logical => .shift_right_logical,
|
|
.rotate_left => .rotate_left,
|
|
.rotate_right => .rotate_right,
|
|
.add, .sub, .mul, .div, .mod, .pow => null,
|
|
};
|
|
}
|
|
|
|
/// How far a shift moves, once the distance has been checked.
|
|
const Distance = union(enum) {
|
|
/// Shorter than the width, so some bits survive.
|
|
within: u7,
|
|
/// At or beyond the width: every original bit leaves the value.
|
|
past_width,
|
|
};
|
|
|
|
/// Interpret the right operand of a shift as a distance.
|
|
///
|
|
/// A negative distance is a domain error rather than a very large one. Standard
|
|
/// mode used to reduce it modulo 64, so `8 >> -1` quietly became `8 >> 63`.
|
|
fn distance(right: Integer) Error!Distance {
|
|
if (right.isNegative()) return Error.DomainError;
|
|
const value = right.unsignedValue();
|
|
if (value >= right.width.bits()) return .past_width;
|
|
return .{ .within = @intCast(value) };
|
|
}
|
|
|
|
/// Interpret the right operand of a rotation as a distance.
|
|
///
|
|
/// Rotation is cyclic, so a distance beyond the width is reduced rather than
|
|
/// saturated: rotating a 64-bit value by 65 is rotating it by 1.
|
|
fn rotation(right: Integer) Error!u7 {
|
|
if (right.isNegative()) return Error.DomainError;
|
|
return @intCast(right.unsignedValue() % right.width.bits());
|
|
}
|
|
|
|
/// Apply a fixed-width operation to two values of the same integer type.
|
|
///
|
|
/// Both operands must be the same width and signedness; in practice they come from
|
|
/// one evaluation with one configuration. The result takes the left operand's type.
|
|
pub fn apply(op: Op, left_in: Integer, right_in: Integer) Error!Integer {
|
|
std.debug.assert(left_in.sameTypeAs(right_in));
|
|
|
|
const mask = left_in.width.mask();
|
|
const left = left_in.unsignedValue();
|
|
const right = right_in.unsignedValue();
|
|
|
|
const raw: u128 = switch (op) {
|
|
.bit_and => left & right,
|
|
.bit_or => left | right,
|
|
.bit_xor => left ^ right,
|
|
// Bits shifted past the end of the width are discarded, not wrapped:
|
|
// `0b1000 << 1` is 16. Wrapping is what `rol` and `ror` are for.
|
|
.shift_left => switch (try distance(right_in)) {
|
|
.past_width => 0,
|
|
.within => |amt| (left << amt) & mask,
|
|
},
|
|
.shift_right_logical => switch (try distance(right_in)) {
|
|
.past_width => 0,
|
|
.within => |amt| left >> amt,
|
|
},
|
|
.shift_right => blk: {
|
|
const negative = left_in.isNegative();
|
|
switch (try distance(right_in)) {
|
|
// Shifting a negative value all the way out leaves the sign fill,
|
|
// which is every bit set; a non-negative one leaves zero.
|
|
.past_width => break :blk if (negative) mask else 0,
|
|
.within => |amt| {
|
|
if (!negative) break :blk left >> amt;
|
|
// Shift the sign-extended value so the sign bit is the fill.
|
|
const extended = left_in.signedValue();
|
|
break :blk @as(u128, @bitCast(extended >> amt)) & mask;
|
|
},
|
|
}
|
|
},
|
|
.rotate_left => blk: {
|
|
const amt = try rotation(right_in);
|
|
if (amt == 0) break :blk left;
|
|
const anti: u7 = @intCast(left_in.width.bits() - amt);
|
|
break :blk ((left << amt) | (left >> anti)) & mask;
|
|
},
|
|
.rotate_right => blk: {
|
|
const amt = try rotation(right_in);
|
|
if (amt == 0) break :blk left;
|
|
const anti: u7 = @intCast(left_in.width.bits() - amt);
|
|
break :blk ((left >> amt) | (left << anti)) & mask;
|
|
},
|
|
};
|
|
|
|
return left_in.withRaw(raw);
|
|
}
|
|
|
|
/// Bitwise complement within the width.
|
|
pub fn not(value: Integer) Integer {
|
|
return value.withRaw(~value.unsignedValue());
|
|
}
|
|
|
|
/// Two's complement negation within the width.
|
|
pub fn negate(value: Integer) Integer {
|
|
return value.withRaw(~value.unsignedValue() +% 1);
|
|
}
|
|
|
|
// -- Tests --
|
|
|
|
const testing = std.testing;
|
|
|
|
/// An 8-bit signed value, the width most of these cases are easiest to read in.
|
|
fn i8v(raw: u128) Integer {
|
|
return .{ .raw = raw, .width = .bits8 };
|
|
}
|
|
|
|
/// An 8-bit unsigned value.
|
|
fn u8v(raw: u128) Integer {
|
|
return .{ .raw = raw, .width = .bits8, .signedness = .unsigned };
|
|
}
|
|
|
|
/// A 64-bit signed value, which is what standard mode uses.
|
|
fn i64v(raw: u128) Integer {
|
|
return .{ .raw = raw };
|
|
}
|
|
|
|
test "fromBinaryOp: every fixed-width operator maps, no arithmetic one does" {
|
|
// The compiler enforces the total mapping; this pins which side each lands on.
|
|
try testing.expectEqual(Op.shift_right, fromBinaryOp(.shift_right).?);
|
|
try testing.expectEqual(Op.rotate_right, fromBinaryOp(.rotate_right).?);
|
|
for ([_]BinaryOp{ .add, .sub, .mul, .div, .mod, .pow }) |op| {
|
|
try testing.expect(fromBinaryOp(op) == null);
|
|
}
|
|
var fixed_width: usize = 0;
|
|
inline for (@typeInfo(BinaryOp).@"enum".fields) |field| {
|
|
if (fromBinaryOp(@field(BinaryOp, field.name)) != null) fixed_width += 1;
|
|
}
|
|
try testing.expectEqual(@as(usize, @typeInfo(Op).@"enum".fields.len), fixed_width);
|
|
}
|
|
|
|
test "the result carries the operands' type" {
|
|
const result = try apply(.bit_and, u8v(0xFF), u8v(0x0F));
|
|
try testing.expectEqual(Integer.BitWidth.bits8, result.width);
|
|
try testing.expectEqual(Integer.Signedness.unsigned, result.signedness);
|
|
try testing.expectEqual(@as(u128, 0x0F), result.raw);
|
|
}
|
|
|
|
test "arithmetic right shift fills with the sign bit" {
|
|
// -8 in 8 bits is 0b1111_1000; one place right is 0b1111_1100, which is -4.
|
|
const result = try apply(.shift_right, i8v(0b1111_1000), i8v(1));
|
|
try testing.expectEqual(@as(u128, 0b1111_1100), result.raw);
|
|
try testing.expectEqual(@as(i128, -4), result.signedValue());
|
|
}
|
|
|
|
test "logical right shift fills with zeros" {
|
|
// The same bits, shifted the other way: 0b0111_1100 is 124.
|
|
const result = try apply(.shift_right_logical, i8v(0b1111_1000), i8v(1));
|
|
try testing.expectEqual(@as(u128, 124), result.raw);
|
|
}
|
|
|
|
test "the two right shifts agree on non-negative values" {
|
|
var value: u128 = 0;
|
|
while (value < 0x80) : (value += 1) {
|
|
var amt: u128 = 0;
|
|
while (amt < 8) : (amt += 1) {
|
|
const arithmetic = try apply(.shift_right, i8v(value), i8v(amt));
|
|
const logical = try apply(.shift_right_logical, i8v(value), i8v(amt));
|
|
try testing.expectEqual(arithmetic.raw, logical.raw);
|
|
}
|
|
}
|
|
}
|
|
|
|
test "an unsigned value has no sign to extend" {
|
|
// 0xFF is 255 here, not -1, so the arithmetic shift is a zero fill too. The old
|
|
// programmer-mode implementation looked at the top bit regardless of the
|
|
// configured signedness and gave 0xFF.
|
|
const arithmetic = try apply(.shift_right, u8v(0xFF), u8v(1));
|
|
const logical = try apply(.shift_right_logical, u8v(0xFF), u8v(1));
|
|
try testing.expectEqual(@as(u128, 0x7F), arithmetic.raw);
|
|
try testing.expectEqual(@as(u128, 0x7F), logical.raw);
|
|
}
|
|
|
|
test "shifts run to completion instead of wrapping or clamping the distance" {
|
|
// Standard mode reduced the distance modulo the width, so `1 << 64` was 1;
|
|
// programmer mode clamped it to width - 1, so 8-bit `0xFF >>> 20` was 1.
|
|
try testing.expectEqual(@as(u128, 0), (try apply(.shift_left, i64v(1), i64v(64))).raw);
|
|
try testing.expectEqual(@as(u128, 0), (try apply(.shift_left, i64v(1), i64v(1000))).raw);
|
|
try testing.expectEqual(@as(u128, 0), (try apply(.shift_right_logical, i8v(0xFF), i8v(20))).raw);
|
|
try testing.expectEqual(@as(u128, 0), (try apply(.shift_left, i8v(0xFF), i8v(8))).raw);
|
|
|
|
// A negative value shifted all the way out is all sign bits, not zero.
|
|
try testing.expectEqual(@as(u128, 0xFF), (try apply(.shift_right, i8v(0b1111_1000), i8v(8))).raw);
|
|
try testing.expectEqual(@as(u128, 0xFF), (try apply(.shift_right, i8v(0b1111_1000), i8v(100))).raw);
|
|
// A non-negative one is zero.
|
|
try testing.expectEqual(@as(u128, 0), (try apply(.shift_right, i8v(0b0100_0000), i8v(8))).raw);
|
|
}
|
|
|
|
test "shifting by one less than the width still keeps a bit" {
|
|
// The boundary the clamping rule used to hide.
|
|
try testing.expectEqual(@as(u128, 0b1000_0000), (try apply(.shift_left, i8v(1), i8v(7))).raw);
|
|
try testing.expectEqual(@as(u128, 1), (try apply(.shift_right_logical, i8v(0b1000_0000), i8v(7))).raw);
|
|
try testing.expectEqual(@as(u128, 0xFF), (try apply(.shift_right, i8v(0b1000_0000), i8v(7))).raw);
|
|
}
|
|
|
|
test "a negative shift distance is a domain error, not a huge one" {
|
|
const neg_one = i8v(0xFF); // -1 in 8-bit signed
|
|
try testing.expectError(Error.DomainError, apply(.shift_left, i8v(1), neg_one));
|
|
try testing.expectError(Error.DomainError, apply(.shift_right, i8v(1), neg_one));
|
|
try testing.expectError(Error.DomainError, apply(.shift_right_logical, i8v(1), neg_one));
|
|
try testing.expectError(Error.DomainError, apply(.rotate_left, i8v(1), neg_one));
|
|
try testing.expectError(Error.DomainError, apply(.rotate_right, i8v(1), neg_one));
|
|
// The same pattern in an unsigned value is 255, a distance past the width.
|
|
try testing.expectEqual(@as(u128, 0), (try apply(.shift_left, u8v(1), u8v(0xFF))).raw);
|
|
}
|
|
|
|
test "rotation is cyclic and reduces the distance" {
|
|
try testing.expectEqual(
|
|
@as(u128, 0b0000_0011),
|
|
(try apply(.rotate_left, i8v(0b1000_0001), i8v(1))).raw,
|
|
);
|
|
try testing.expectEqual(
|
|
@as(u128, 0b1100_0000),
|
|
(try apply(.rotate_right, i8v(0b1000_0001), i8v(1))).raw,
|
|
);
|
|
// Rotating by the width is the identity, and by width + 1 is by 1.
|
|
try testing.expectEqual(
|
|
@as(u128, 0b1000_0001),
|
|
(try apply(.rotate_left, i8v(0b1000_0001), i8v(8))).raw,
|
|
);
|
|
try testing.expectEqual(
|
|
@as(u128, 0b0000_0011),
|
|
(try apply(.rotate_left, i8v(0b1000_0001), i8v(9))).raw,
|
|
);
|
|
}
|
|
|
|
test "rotate left and rotate right are inverses at every distance and width" {
|
|
for (std.enums.values(BitWidth)) |bw| {
|
|
const value: Integer = .{
|
|
.raw = 0x1234_5678_9ABC_DEF0 & bw.mask(),
|
|
.width = bw,
|
|
.signedness = .unsigned,
|
|
};
|
|
var amt: u128 = 0;
|
|
while (amt < value.width.bits()) : (amt += 1) {
|
|
const there = try apply(.rotate_left, value, value.withRaw(amt));
|
|
const back = try apply(.rotate_right, there, value.withRaw(amt));
|
|
try testing.expectEqual(value.raw, back.raw);
|
|
}
|
|
}
|
|
}
|
|
|
|
test "results stay inside the width, at every width and operator" {
|
|
for (std.enums.values(BitWidth)) |bw| {
|
|
const all_ones: Integer = .{ .raw = bw.mask(), .width = bw };
|
|
inline for (@typeInfo(Op).@"enum".fields) |field| {
|
|
const op = @field(Op, field.name);
|
|
// 1 is a safe distance for the shifts and a legal operand for the rest.
|
|
const result = try apply(op, all_ones, all_ones.withRaw(1));
|
|
try testing.expectEqual(result.raw, result.unsignedValue());
|
|
try testing.expect(result.sameTypeAs(all_ones));
|
|
}
|
|
}
|
|
}
|
|
|
|
test "not and negate stay inside the width and keep the type" {
|
|
try testing.expectEqual(@as(u128, 0xFF), not(i8v(0)).raw);
|
|
try testing.expectEqual(@as(u128, 0), not(i8v(0xFF)).raw);
|
|
try testing.expectEqual(@as(u128, 0xFF), negate(i8v(1)).raw);
|
|
try testing.expectEqual(@as(u128, 1), negate(i8v(0xFF)).raw);
|
|
// Negating the most negative value gives itself back, as two's complement does.
|
|
try testing.expectEqual(@as(u128, 0x80), negate(i8v(0x80)).raw);
|
|
try testing.expect(not(u8v(0)).sameTypeAs(u8v(0)));
|
|
}
|
|
|
|
test "the default value type, which standard mode uses, is 64-bit signed" {
|
|
const minus_eight = i64v(@bitCast(@as(i128, -8) & @as(i128, @bitCast(Integer.BitWidth.bits64.mask()))));
|
|
try testing.expectEqual(Integer.BitWidth.bits64, minus_eight.width);
|
|
try testing.expectEqual(Integer.Signedness.signed, minus_eight.signedness);
|
|
// -8 >> 1 is -4 there, which is the case that used to differ between modes.
|
|
const shifted = try apply(.shift_right, minus_eight, minus_eight.withRaw(1));
|
|
try testing.expectEqual(@as(i128, -4), shifted.signedValue());
|
|
}
|