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