Commit 7b72fc6bbc

Cody Tapscott <topolarity@tapscott.me>
2022-02-13 19:54:37
Add `abi_size` parameter to read/writeTwosComplement
Big-int functions were updated to respect the provided abi_size, rather than inferring a potentially incorrect abi_size implicitly. In combination with the convention that any required padding bits are added on the MSB end, this means that exotic integers can potentially have a well-defined memory layout.
1 parent eeb043f
Changed files (3)
lib
std
src
lib/std/math/big/int.zig
@@ -1624,18 +1624,16 @@ pub const Mutable = struct {
     }
 
     /// Read the value of `x` from `buffer`
-    /// Asserts that `buffer` and `bit_count` are large enough to store the value.
+    /// Asserts that `buffer`, `abi_size`, and `bit_count` are large enough to store the value.
     ///
-    /// For integers with a well-defined layout (e.g. all power-of-two integers), this function
-    /// reads from `buffer` as if it were the contents of @ptrCast([]const u8, &x), where the 
-    /// slice length is taken to be @sizeOf(std.meta.Int(signedness, <bit_count>))
-    ///
-    /// For integers with a non-well-defined layout, `buffer` must have been created by 
-    /// writeTwosComplement.
+    /// The contents of `buffer` are interpreted as if they were the contents of 
+    /// @ptrCast(*[abi_size]const u8, &x). Byte ordering is determined by `endian` 
+    /// and any required padding bits are expected on the MSB end.
     pub fn readTwosComplement(
         x: *Mutable,
         buffer: []const u8,
         bit_count: usize,
+        abi_size: usize,
         endian: Endian,
         signedness: Signedness,
     ) void {
@@ -1646,20 +1644,18 @@ pub const Mutable = struct {
             return;
         }
 
-        // byte_count is the total amount of bytes to read from buffer
-        var byte_count = @sizeOf(Limb) * (bit_count / @bitSizeOf(Limb));
-        if (bit_count % @bitSizeOf(Limb) != 0) { // Round up to a power-of-two integer <= Limb
-            byte_count += (std.math.ceilPowerOfTwoAssert(usize, bit_count % @bitSizeOf(Limb)) + 7) / 8;
-        }
-
-        const limb_count = calcTwosCompLimbCount(8 * byte_count);
+        // byte_count is our total read size: it cannot exceed abi_size,
+        // but may be less as long as it includes the required bits
+        const limb_count = calcTwosCompLimbCount(bit_count);
+        const byte_count = std.math.min(abi_size, @sizeOf(Limb) * limb_count);
+        assert(8 * byte_count >= bit_count);
 
         // Check whether the input is negative
         var positive = true;
         if (signedness == .signed) {
             var last_byte = switch (endian) {
                 .Little => ((bit_count + 7) / 8) - 1,
-                .Big => byte_count - ((bit_count + 7) / 8),
+                .Big => abi_size - ((bit_count + 7) / 8),
             };
 
             const sign_bit = @as(u8, 1) << @intCast(u3, (bit_count - 1) % 8);
@@ -1672,7 +1668,7 @@ pub const Mutable = struct {
         while (limb_index < bit_count / @bitSizeOf(Limb)) : (limb_index += 1) {
             var buf_index = switch (endian) {
                 .Little => @sizeOf(Limb) * limb_index,
-                .Big => byte_count - (limb_index + 1) * @sizeOf(Limb),
+                .Big => abi_size - (limb_index + 1) * @sizeOf(Limb),
             };
 
             const limb_buf = @ptrCast(*const [@sizeOf(Limb)]u8, buffer[buf_index..]);
@@ -1683,32 +1679,34 @@ pub const Mutable = struct {
             x.limbs[limb_index] = limb;
         }
 
-        // Copy any remaining bytes, using the nearest power-of-two integer that is large enough
-        const bits_left = @intCast(Log2Limb, bit_count % @bitSizeOf(Limb));
-        if (bits_left != 0) {
-            const bytes_read = limb_index * @sizeOf(Limb);
-            const bytes_left = byte_count - bytes_read;
-            var buffer_left = switch (endian) {
-                .Little => buffer[bytes_read..],
-                .Big => buffer[0..],
-            };
+        // Copy the remaining N bytes (N <= @sizeOf(Limb))
+        var bytes_read = limb_index * @sizeOf(Limb);
+        if (bytes_read != byte_count) {
+            var limb: Limb = 0;
 
-            var limb = @intCast(Limb, blk: {
-                // zig fmt: off
-                if (bytes_left ==  1) break :blk mem.readInt(  u8, buffer_left[0.. 1], endian);
-                if (bytes_left ==  2) break :blk mem.readInt( u16, buffer_left[0.. 2], endian);
-                if (bytes_left ==  4) break :blk mem.readInt( u32, buffer_left[0.. 4], endian);
-                if (bytes_left ==  8) break :blk mem.readInt( u64, buffer_left[0.. 8], endian);
-                if (bytes_left == 16) break :blk mem.readInt(u128, buffer_left[0..16], endian);
-                // zig fmt: on
-                unreachable;
-            });
+            while (bytes_read != byte_count) {
+                const read_size = std.math.floorPowerOfTwo(usize, byte_count - bytes_read);
+                var int_buffer = switch (endian) {
+                    .Little => buffer[bytes_read..],
+                    .Big => buffer[(abi_size - bytes_read - read_size)..],
+                };
+                limb |= @intCast(Limb, switch (read_size) {
+                    1 => mem.readInt(u8, int_buffer[0..1], endian),
+                    2 => mem.readInt(u16, int_buffer[0..2], endian),
+                    4 => mem.readInt(u32, int_buffer[0..4], endian),
+                    8 => mem.readInt(u64, int_buffer[0..8], endian),
+                    16 => mem.readInt(u128, int_buffer[0..16], endian),
+                    else => unreachable,
+                }) << @intCast(Log2Limb, 8 * (bytes_read % @sizeOf(Limb)));
+                bytes_read += read_size;
+            }
 
             // 2's complement (bitwise not, then add carry bit)
             if (!positive) _ = @addWithOverflow(Limb, ~limb, carry, &limb);
 
             // Mask off any unused bits
-            const mask = (@as(Limb, 1) << bits_left) -% 1; // 0b0..01..1 with (bits_left) trailing ones
+            const valid_bits = @intCast(Log2Limb, bit_count % @bitSizeOf(Limb));
+            const mask = (@as(Limb, 1) << valid_bits) -% 1; // 0b0..01..1 with (valid_bits_in_limb) trailing ones
             limb &= mask;
 
             x.limbs[limb_count - 1] = limb;
@@ -2076,21 +2074,16 @@ pub const Const = struct {
     }
 
     /// Write the value of `x` into `buffer`
-    /// Asserts that `buffer` and `bit_count` are large enough to store the value.
+    /// Asserts that `buffer`, `abi_size`, and `bit_count` are large enough to store the value.
     ///
-    /// For integers with a well-defined layout (e.g. all power-of-two integers), this function
-    /// can be thought of as writing to `buffer` the contents of @ptrCast([]const u8, &x), 
-    /// where the slice length is taken to be @sizeOf(std.meta.Int(_,<bit_count>))
-    ///
-    /// For integers with a non-well-defined layout, the only requirement is that readTwosComplement
-    /// on the same buffer creates an equivalent big integer.
-    pub fn writeTwosComplement(x: Const, buffer: []u8, bit_count: usize, endian: Endian) void {
-        if (bit_count == 0) return;
+    /// `buffer` is filled so that its contents match what would be observed via
+    /// @ptrCast(*[abi_size]const u8, &x). Byte ordering is determined by `endian`,
+    /// and any required padding bits are added on the MSB end.
+    pub fn writeTwosComplement(x: Const, buffer: []u8, bit_count: usize, abi_size: usize, endian: Endian) void {
 
-        var byte_count = @sizeOf(Limb) * (bit_count / @bitSizeOf(Limb));
-        if (bit_count % @bitSizeOf(Limb) != 0) {
-            byte_count += (std.math.ceilPowerOfTwoAssert(usize, bit_count % @bitSizeOf(Limb)) + 7) / 8;
-        }
+        // byte_count is our total write size
+        const byte_count = abi_size;
+        assert(8 * byte_count >= bit_count);
         assert(buffer.len >= byte_count);
         assert(x.fitsInTwosComp(if (x.positive) .unsigned else .signed, bit_count));
 
@@ -2100,7 +2093,7 @@ pub const Const = struct {
         while (limb_index < byte_count / @sizeOf(Limb)) : (limb_index += 1) {
             var buf_index = switch (endian) {
                 .Little => @sizeOf(Limb) * limb_index,
-                .Big => byte_count - (limb_index + 1) * @sizeOf(Limb),
+                .Big => abi_size - (limb_index + 1) * @sizeOf(Limb),
             };
 
             var limb: Limb = if (limb_index < x.limbs.len) x.limbs[limb_index] else 0;
@@ -2111,32 +2104,36 @@ pub const Const = struct {
             mem.writeInt(Limb, limb_buf, limb, endian);
         }
 
-        // Copy any remaining bytes
-        if (byte_count % @sizeOf(Limb) != 0) {
-            const bytes_read = limb_index * @sizeOf(Limb);
-            const bytes_left = byte_count - bytes_read;
-            var buffer_left = switch (endian) {
-                .Little => buffer[bytes_read..],
-                .Big => buffer[0..],
-            };
-
+        // Copy the remaining N bytes (N < @sizeOf(Limb))
+        var bytes_written = limb_index * @sizeOf(Limb);
+        if (bytes_written != byte_count) {
             var limb: Limb = if (limb_index < x.limbs.len) x.limbs[limb_index] else 0;
             // 2's complement (bitwise not, then add carry bit)
             if (!x.positive) _ = @addWithOverflow(Limb, ~limb, carry, &limb);
 
-            if (bytes_left == 1) {
-                mem.writeInt(u8, buffer_left[0..1], @truncate(u8, limb), endian);
-            } else if (@sizeOf(Limb) > 1 and bytes_left == 2) {
-                mem.writeInt(u16, buffer_left[0..2], @truncate(u16, limb), endian);
-            } else if (@sizeOf(Limb) > 2 and bytes_left == 4) {
-                mem.writeInt(u32, buffer_left[0..4], @truncate(u32, limb), endian);
-            } else if (@sizeOf(Limb) > 4 and bytes_left == 8) {
-                mem.writeInt(u64, buffer_left[0..8], @truncate(u64, limb), endian);
-            } else if (@sizeOf(Limb) > 8 and bytes_left == 16) {
-                mem.writeInt(u128, buffer_left[0..16], @truncate(u128, limb), endian);
-            } else if (@sizeOf(Limb) > 16) {
-                @compileError("@sizeOf(Limb) exceeded supported range");
-            } else unreachable;
+            while (bytes_written != byte_count) {
+                const write_size = std.math.floorPowerOfTwo(usize, byte_count - bytes_written);
+                var int_buffer = switch (endian) {
+                    .Little => buffer[bytes_written..],
+                    .Big => buffer[(abi_size - bytes_written - write_size)..],
+                };
+
+                if (write_size == 1) {
+                    mem.writeInt(u8, int_buffer[0..1], @truncate(u8, limb), endian);
+                } else if (@sizeOf(Limb) >= 2 and write_size == 2) {
+                    mem.writeInt(u16, int_buffer[0..2], @truncate(u16, limb), endian);
+                } else if (@sizeOf(Limb) >= 4 and write_size == 4) {
+                    mem.writeInt(u32, int_buffer[0..4], @truncate(u32, limb), endian);
+                } else if (@sizeOf(Limb) >= 8 and write_size == 8) {
+                    mem.writeInt(u64, int_buffer[0..8], @truncate(u64, limb), endian);
+                } else if (@sizeOf(Limb) >= 16 and write_size == 16) {
+                    mem.writeInt(u128, int_buffer[0..16], @truncate(u128, limb), endian);
+                } else if (@sizeOf(Limb) >= 32) {
+                    @compileError("@sizeOf(Limb) exceeded supported range");
+                } else unreachable;
+                limb >>= @intCast(Log2Limb, 8 * write_size);
+                bytes_written += write_size;
+            }
         }
     }
 
lib/std/math/big/int_test.zig
@@ -2498,16 +2498,103 @@ test "big int conversion read/write twos complement" {
     defer testing.allocator.free(buffer1);
 
     const endians = [_]std.builtin.Endian{ .Little, .Big };
+    const abi_size = 64;
 
     for (endians) |endian| {
         // Writing to buffer and back should not change anything
-        a.toConst().writeTwosComplement(buffer1, 493, endian);
-        m.readTwosComplement(buffer1, 493, endian, .unsigned);
+        a.toConst().writeTwosComplement(buffer1, 493, abi_size, endian);
+        m.readTwosComplement(buffer1, 493, abi_size, endian, .unsigned);
         try testing.expect(m.toConst().order(a.toConst()) == .eq);
 
         // Equivalent to @bitCast(i493, @as(u493, intMax(u493))
-        a.toConst().writeTwosComplement(buffer1, 493, endian);
-        m.readTwosComplement(buffer1, 493, endian, .signed);
+        a.toConst().writeTwosComplement(buffer1, 493, abi_size, endian);
+        m.readTwosComplement(buffer1, 493, abi_size, endian, .signed);
         try testing.expect(m.toConst().orderAgainstScalar(-1) == .eq);
     }
 }
+
+test "big int conversion read twos complement with padding" {
+    var a = try Managed.initSet(testing.allocator, 0x01_02030405_06070809_0a0b0c0d);
+    defer a.deinit();
+
+    var buffer1 = try testing.allocator.alloc(u8, 16);
+    defer testing.allocator.free(buffer1);
+    @memset(buffer1.ptr, 0xaa, buffer1.len);
+
+    // writeTwosComplement:
+    // (1) should not write beyond buffer[0..abi_size]
+    // (2) should correctly order bytes based on the provided endianness
+    // (3) should sign-extend any bits from bit_count to 8 * abi_size
+
+    var bit_count: usize = 12 * 8 + 1;
+    a.toConst().writeTwosComplement(buffer1, bit_count, 13, .Little);
+    try testing.expect(std.mem.eql(u8, buffer1, &[_]u8{ 0xd, 0xc, 0xb, 0xa, 0x9, 0x8, 0x7, 0x6, 0x5, 0x4, 0x3, 0x2, 0x1, 0xaa, 0xaa, 0xaa }));
+    a.toConst().writeTwosComplement(buffer1, bit_count, 13, .Big);
+    try testing.expect(std.mem.eql(u8, buffer1, &[_]u8{ 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc, 0xd, 0xaa, 0xaa, 0xaa }));
+    a.toConst().writeTwosComplement(buffer1, bit_count, 16, .Little);
+    try testing.expect(std.mem.eql(u8, buffer1, &[_]u8{ 0xd, 0xc, 0xb, 0xa, 0x9, 0x8, 0x7, 0x6, 0x5, 0x4, 0x3, 0x2, 0x1, 0x0, 0x0, 0x0 }));
+    a.toConst().writeTwosComplement(buffer1, bit_count, 16, .Big);
+    try testing.expect(std.mem.eql(u8, buffer1, &[_]u8{ 0x0, 0x0, 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc, 0xd }));
+
+    @memset(buffer1.ptr, 0xaa, buffer1.len);
+    try a.set(-0x01_02030405_06070809_0a0b0c0d);
+    bit_count = 12 * 8 + 2;
+
+    a.toConst().writeTwosComplement(buffer1, bit_count, 13, .Little);
+    try testing.expect(std.mem.eql(u8, buffer1, &[_]u8{ 0xf3, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xaa, 0xaa, 0xaa }));
+    a.toConst().writeTwosComplement(buffer1, bit_count, 13, .Big);
+    try testing.expect(std.mem.eql(u8, buffer1, &[_]u8{ 0xfe, 0xfd, 0xfc, 0xfb, 0xfa, 0xf9, 0xf8, 0xf7, 0xf6, 0xf5, 0xf4, 0xf3, 0xf3, 0xaa, 0xaa, 0xaa }));
+    a.toConst().writeTwosComplement(buffer1, bit_count, 16, .Little);
+    try testing.expect(std.mem.eql(u8, buffer1, &[_]u8{ 0xf3, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff, 0xff, 0xff }));
+    a.toConst().writeTwosComplement(buffer1, bit_count, 16, .Big);
+    try testing.expect(std.mem.eql(u8, buffer1, &[_]u8{ 0xff, 0xff, 0xff, 0xfe, 0xfd, 0xfc, 0xfb, 0xfa, 0xf9, 0xf8, 0xf7, 0xf6, 0xf5, 0xf4, 0xf3, 0xf3 }));
+}
+
+test "big int conversion write twos complement with padding" {
+    var a = try Managed.initSet(testing.allocator, 0x01_ffffffff_ffffffff_ffffffff);
+    defer a.deinit();
+
+    var m = a.toMutable();
+
+    // readTwosComplement:
+    // (1) should not read beyond buffer[0..abi_size]
+    // (2) should correctly interpret bytes based on the provided endianness
+    // (3) should ignore any bits from bit_count to 8 * abi_size
+
+    var bit_count: usize = 12 * 8 + 1;
+    var buffer: []const u8 = undefined;
+
+    buffer = &[_]u8{ 0xd, 0xc, 0xb, 0xa, 0x9, 0x8, 0x7, 0x6, 0x5, 0x4, 0x3, 0x2, 0xb };
+    m.readTwosComplement(buffer, bit_count, 13, .Little, .unsigned);
+    try testing.expect(m.toConst().orderAgainstScalar(0x01_02030405_06070809_0a0b0c0d) == .eq);
+
+    buffer = &[_]u8{ 0xb, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc, 0xd };
+    m.readTwosComplement(buffer, bit_count, 13, .Big, .unsigned);
+    try testing.expect(m.toConst().orderAgainstScalar(0x01_02030405_06070809_0a0b0c0d) == .eq);
+
+    buffer = &[_]u8{ 0xd, 0xc, 0xb, 0xa, 0x9, 0x8, 0x7, 0x6, 0x5, 0x4, 0x3, 0x2, 0xab, 0xaa, 0xaa, 0xaa };
+    m.readTwosComplement(buffer, bit_count, 16, .Little, .unsigned);
+    try testing.expect(m.toConst().orderAgainstScalar(0x01_02030405_06070809_0a0b0c0d) == .eq);
+
+    buffer = &[_]u8{ 0xaa, 0xaa, 0xaa, 0xab, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc, 0xd };
+    m.readTwosComplement(buffer, bit_count, 16, .Big, .unsigned);
+    try testing.expect(m.toConst().orderAgainstScalar(0x01_02030405_06070809_0a0b0c0d) == .eq);
+
+    bit_count = 12 * 8 + 2;
+
+    buffer = &[_]u8{ 0xf3, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0x02 };
+    m.readTwosComplement(buffer, bit_count, 13, .Little, .signed);
+    try testing.expect(m.toConst().orderAgainstScalar(-0x01_02030405_06070809_0a0b0c0d) == .eq);
+
+    buffer = &[_]u8{ 0x02, 0xfd, 0xfc, 0xfb, 0xfa, 0xf9, 0xf8, 0xf7, 0xf6, 0xf5, 0xf4, 0xf3, 0xf3 };
+    m.readTwosComplement(buffer, bit_count, 13, .Big, .signed);
+    try testing.expect(m.toConst().orderAgainstScalar(-0x01_02030405_06070809_0a0b0c0d) == .eq);
+
+    buffer = &[_]u8{ 0xf3, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0x02, 0xaa, 0xaa, 0xaa };
+    m.readTwosComplement(buffer, bit_count, 16, .Little, .signed);
+    try testing.expect(m.toConst().orderAgainstScalar(-0x01_02030405_06070809_0a0b0c0d) == .eq);
+
+    buffer = &[_]u8{ 0xaa, 0xaa, 0xaa, 0x02, 0xfd, 0xfc, 0xfb, 0xfa, 0xf9, 0xf8, 0xf7, 0xf6, 0xf5, 0xf4, 0xf3, 0xf3 };
+    m.readTwosComplement(buffer, bit_count, 16, .Big, .signed);
+    try testing.expect(m.toConst().orderAgainstScalar(-0x01_02030405_06070809_0a0b0c0d) == .eq);
+}
src/value.zig
@@ -1046,7 +1046,8 @@ pub const Value = extern union {
                 var bigint_buffer: BigIntSpace = undefined;
                 const bigint = val.toBigInt(&bigint_buffer);
                 const bits = ty.intInfo(target).bits;
-                bigint.writeTwosComplement(buffer, bits, target.cpu.arch.endian());
+                const abi_size = ty.abiSize(target);
+                bigint.writeTwosComplement(buffer, bits, abi_size, target.cpu.arch.endian());
             },
             .Enum => {
                 var enum_buffer: Payload.U64 = undefined;
@@ -1054,7 +1055,8 @@ pub const Value = extern union {
                 var bigint_buffer: BigIntSpace = undefined;
                 const bigint = int_val.toBigInt(&bigint_buffer);
                 const bits = ty.intInfo(target).bits;
-                bigint.writeTwosComplement(buffer, bits, target.cpu.arch.endian());
+                const abi_size = ty.abiSize(target);
+                bigint.writeTwosComplement(buffer, bits, abi_size, target.cpu.arch.endian());
             },
             .Float => switch (ty.floatBits(target)) {
                 16 => return floatWriteToMemory(f16, val.toFloat(f16), target, buffer),
@@ -1096,8 +1098,9 @@ pub const Value = extern union {
                 const Limb = std.math.big.Limb;
                 const limb_count = (buffer.len + @sizeOf(Limb) - 1) / @sizeOf(Limb);
                 const limbs_buffer = try arena.alloc(Limb, limb_count);
+                const abi_size = ty.abiSize(target);
                 var bigint = BigIntMutable.init(limbs_buffer, 0);
-                bigint.readTwosComplement(buffer, int_info.bits, endian, int_info.signedness);
+                bigint.readTwosComplement(buffer, int_info.bits, abi_size, endian, int_info.signedness);
                 return fromBigInt(arena, bigint.toConst());
             },
             .Float => switch (ty.floatBits(target)) {