tiny.preserves.integer_mod
Defined in tiny.preserves.
Signed integers of any size, as data values carry them.
API (4)
Types and contracts
Public types and contracts.
DecodeError: The errorfromCanonicalBytesreturns when its bytes are longer than the shortest spelling of their number.RangeError: The error thetoconversions return when the number does not fit the requested type.SignedInteger: A signed integer of any size, stored in the smallest of three representations that holds it.Tier: Names the three ways a number is stored:i128for 128 signed bits,u128for 128 unsigned bits above the signed maximum, andbigfor heap bytes.
Source
Source: lib/preserves/src/integer.zig
zig
//! Signed integers of any size, as data values carry them. An integer value needs equality, a total//! order, a hash and an exact round trip through the bytes the binary syntax writes. Most integers//! fit in a machine word, so a program should pay for heap storage only when a number needs it.//!//! Data can carry integers of any size, and every fixed-width Zig integer, the 128-bit ones//! included, has a largest value. The unsigned 128-bit range reaches one bit past the signed one,//! so a number in that gap fits the unsigned type and exceeds the signed one. One number has many//! byte spellings that differ only by extra leading `0x00` or `0xff` bytes, and byte equality needs//! one spelling per number.//!//! The [Preserves](https://preserves.dev/) data language has signed integers of unbounded size, and//! the package's binary syntax writes each as its shortest big-endian two's-complement bytes and//! rejects any longer spelling.//!//! A number takes one of three representations (*tier*): 128 signed bits when it fits, 128 unsigned//! bits when it is larger than that but still fits, and bytes otherwise. A number too large for 128//! bits is stored as its shortest big-endian two's-complement bytes (*canonical bytes*), the same//! bytes the binary syntax writes. Only that third representation allocates, so freeing and copying//! cost nothing for every other integer. Equality, order and hash assume each number has one//! representation, the smallest that holds it: two values in different representations are unequal.//! The constructors `fromI128`, `fromU128` and `fromCanonicalBytes` keep that rule, and a signed//! integer (`SignedInteger`) written directly through its representation field (`repr`) can break//! it with nothing to detect it. Across representations the order is numeric: negative large//! numbers, then 128-bit signed, then 128-bit unsigned, then positive large numbers.const std = @import("std");const Allocator = std.mem.Allocator;/// The error `fromCanonicalBytes` returns when its bytes are longer than the shortest spelling of/// their number. Code that reads integers from bytes handles this error, as the binary and text/// readers do.pub const DecodeError = error{NonCanonicalInteger};/// The error the `to` conversions return when the number does not fit the requested type. Code that/// reads an integer as a fixed-width Zig integer handles this error when the number is too large or/// has the wrong sign.pub const RangeError = error{OutOfRange};/// Names the three ways a number is stored: `i128` for 128 signed bits, `u128` for 128 unsigned/// bits above the signed maximum, and `big` for heap bytes. Code that switches on how an integer is/// stored reads this, as the text reader's tests do.pub const Tier = enum { i128, u128, big };/// A signed integer of any size, stored in the smallest of three representations that holds it./// Every integer in a value is one of these, so every reader, writer and comparison of integers/// uses it. A value built by `fromCanonicalBytes` or `clone` owns its heap bytes, and `deinit`/// frees them. Equality and order assume the smallest representation, so a value written directly/// through `repr` has to keep it.pub const SignedInteger = struct { /// The number in one of its three representations. A value set here directly has to use the /// smallest representation that holds the number, because nothing checks it. repr: Repr, /// The number stored as 128 signed bits, 128 unsigned bits or heap bytes, tagged by which. Code /// that switches on how an integer is stored reads this. pub const Repr = union(Tier) { /// The number when it fits in 128 signed bits. i128: i128, /// The number when it is above the largest 128-bit signed integer and fits in 128 unsigned /// bits. u128: u128, /// The number's shortest big-endian two's-complement bytes, for a number outside both /// 128-bit ranges. These bytes are at least 17 long. big: []const u8, }; /// Returns zero, stored in 128 signed bits. Code that needs a zero integer calls it. pub fn zero() SignedInteger { return .{ .repr = .{ .i128 = 0 } }; } /// Returns `v`, stored in 128 signed bits. Readers and constructors call it for any integer /// that fits in 128 signed bits. pub fn fromI128(v: i128) SignedInteger { return .{ .repr = .{ .i128 = v } }; } /// Returns `v`, stored in 128 signed bits when it fits there and in 128 unsigned bits /// otherwise. Readers call it for a non-negative integer read as 128 unsigned bits. pub fn fromU128(v: u128) SignedInteger { const limit: u128 = @intCast(std.math.maxInt(i128)); if (v <= limit) { return .{ .repr = .{ .i128 = @intCast(v) } }; } return .{ .repr = .{ .u128 = v } }; } /// Reads a number from its shortest big-endian two's-complement bytes. The binary and text /// readers call it for integers too large for fixed-width parsing. Empty bytes are zero, up to /// 16 bytes give a 128-bit signed number, and 17 bytes of a number that fits 128 unsigned bits /// give a 128-bit unsigned number. Numbers with more bytes keep a copy of the bytes from /// `allocator`, which `deinit` frees. The call returns `error.NonCanonicalInteger` for any /// spelling longer than the shortest. pub fn fromCanonicalBytes(allocator: Allocator, bytes: []const u8) !SignedInteger { if (!isCanonicalBytes(bytes)) return DecodeError.NonCanonicalInteger; if (bytes.len == 0) return zero(); if (bytes.len <= 16) return fromI128(decodeI128Bytes(bytes)); if (bytes.len == 17 and bytes[0] == 0x00 and (bytes[1] & 0x80) != 0) { return .{ .repr = .{ .u128 = decode16BytesAsU128(bytes[1..]) } }; } return .{ .repr = .{ .big = try allocator.dupe(u8, bytes) } }; } /// Frees the heap bytes of a number too large for 128 bits, and does nothing for any other /// number. `Value.deinit` and `freeValueDeep` call it for every integer they free. The value is /// undefined afterward. pub fn deinit(self: *SignedInteger, allocator: Allocator) void { switch (self.repr) { .big => |b| if (b.len != 0) allocator.free(b), else => {}, } self.* = undefined; } /// Returns a copy that owns its storage, copying the heap bytes of a large number with /// `allocator`. `cloneValueDeep` calls it for every integer it copies. A number that fits in /// 128 bits is copied without allocating. pub fn clone(self: SignedInteger, allocator: Allocator) !SignedInteger { return switch (self.repr) { .i128 => |v| fromI128(v), .u128 => |v| .{ .repr = .{ .u128 = v } }, .big => |b| .{ .repr = .{ .big = try allocator.dupe(u8, b) } }, }; } /// Returns a new allocation from `allocator` holding the number's shortest big-endian /// two's-complement bytes, which the caller frees. The binary writer calls it for the bytes of /// each integer it writes. Zero gives an empty slice. pub fn toCanonicalBytes(self: SignedInteger, allocator: Allocator) ![]u8 { return switch (self.repr) { .i128 => |v| try encodeI128(allocator, v), .u128 => |v| try encodeU128(allocator, v), .big => |b| try allocator.dupe(u8, b), }; } /// Returns the number when it is stored in 128 signed bits, and `error.OutOfRange` otherwise. /// Code that needs the number as a Zig `i128` calls it, as the package's tests do. pub fn toI128(self: SignedInteger) RangeError!i128 { return switch (self.repr) { .i128 => |v| v, .u128, .big => RangeError.OutOfRange, }; } /// Returns the number when it is zero or positive and fits in 128 unsigned bits, and /// `error.OutOfRange` otherwise. Code that needs a 128-bit unsigned integer calls it. pub fn toU128(self: SignedInteger) RangeError!u128 { return switch (self.repr) { .i128 => |v| if (v < 0) RangeError.OutOfRange else @intCast(v), .u128 => |v| v, .big => RangeError.OutOfRange, }; } /// Returns the number when it fits in 64 signed bits, and `error.OutOfRange` otherwise. Code /// that reads a field it expects to fit in 64 bits calls it for an `i64`. pub fn toI64(self: SignedInteger) RangeError!i64 { const v = try self.toI128(); if (v < std.math.minInt(i64) or v > std.math.maxInt(i64)) return RangeError.OutOfRange; return @intCast(v); } /// Returns the number clamped to the 64-bit signed range: the largest `i64` for any number /// above it and the smallest for any number below. The JSON writer calls it to print an integer /// too large for 128 bits. The call never fails. pub fn toI64Lossy(self: SignedInteger) i64 { return switch (self.repr) { .i128 => |v| blk: { if (v > std.math.maxInt(i64)) break :blk std.math.maxInt(i64); if (v < std.math.minInt(i64)) break :blk std.math.minInt(i64); break :blk @intCast(v); }, .u128 => std.math.maxInt(i64), .big => |b| if (b.len > 0 and (b[0] & 0x80) != 0) std.math.minInt(i64) else std.math.maxInt(i64), }; } /// Returns the number when it is zero or positive and fits in 64 unsigned bits, and /// `error.OutOfRange` otherwise. Code that reads a count or a size calls it for a `u64`. pub fn toU64(self: SignedInteger) RangeError!u64 { const v = try self.toU128(); if (v > std.math.maxInt(u64)) return RangeError.OutOfRange; return @intCast(v); } /// Returns whether the number is zero. Code that tests for zero calls it. Only a zero stored in /// 128 signed bits counts, so a zero written directly into another representation reads as /// nonzero. pub fn isZero(self: SignedInteger) bool { return switch (self.repr) { .i128 => |v| v == 0, .u128, .big => false, }; } /// Returns whether the number is below zero. Code that needs the sign calls it. pub fn isNegative(self: SignedInteger) bool { return switch (self.repr) { .i128 => |v| v < 0, .u128 => false, .big => |b| b.len > 0 and (b[0] & 0x80) != 0, }; } /// Returns whether two integers are equal. `Value.eql` calls it for two integers. Integers in /// different representations are never equal, so the call is exact only when both use the /// smallest representation that holds their number. pub fn eql(a: SignedInteger, b: SignedInteger) bool { return switch (a.repr) { .i128 => |av| switch (b.repr) { .i128 => |bv| av == bv, else => false, }, .u128 => |av| switch (b.repr) { .u128 => |bv| av == bv, else => false, }, .big => |av| switch (b.repr) { .big => |bv| std.mem.eql(u8, av, bv), else => false, }, }; } /// Returns the numeric order of two integers. `Value.compare` calls it for two integers. Large /// numbers compare by sign, then by byte length, then byte by byte. The order across /// representations is exact only when both integers use the smallest representation that holds /// their number. pub fn order(a: SignedInteger, b: SignedInteger) std.math.Order { return switch (a.repr) { .i128 => |av| switch (b.repr) { .i128 => |bv| std.math.order(av, bv), .u128 => .lt, .big => |bv| if (bigIsNegative(bv)) .gt else .lt, }, .u128 => |av| switch (b.repr) { .i128 => .gt, .u128 => |bv| std.math.order(av, bv), .big => |bv| if (bigIsNegative(bv)) .gt else .lt, }, .big => |av| switch (b.repr) { .i128, .u128 => if (bigIsNegative(av)) .lt else .gt, .big => |bv| compareBig(av, bv), }, }; } /// Returns whether `bytes` are the shortest big-endian two's-complement spelling of their /// number. The binary reader calls it before reading an integer, so a longer spelling is /// rejected. Empty bytes spell zero. A lone `0x00` byte, a leading `0x00` before a byte whose /// top bit is clear, and a leading `0xff` before a byte whose top bit is set are all longer /// than needed. pub fn isCanonicalBytes(bytes: []const u8) bool { if (bytes.len == 0) return true; if (bytes.len == 1) return bytes[0] != 0x00; const first = bytes[0]; const second = bytes[1]; if (first == 0x00 and (second & 0x80) == 0) return false; if (first == 0xff and (second & 0x80) != 0) return false; return true; }};fn bigIsNegative(bytes: []const u8) bool { return bytes.len > 0 and (bytes[0] & 0x80) != 0;}fn compareBig(a: []const u8, b: []const u8) std.math.Order { const a_neg = bigIsNegative(a); const b_neg = bigIsNegative(b); if (a_neg != b_neg) return if (a_neg) .lt else .gt; if (a.len != b.len) { return if (a_neg) std.math.order(b.len, a.len) else std.math.order(a.len, b.len); } return std.mem.order(u8, a, b);}fn decodeI128Bytes(bytes: []const u8) i128 { std.debug.assert(bytes.len > 0); std.debug.assert(bytes.len <= 16); var result: u128 = if ((bytes[0] & 0x80) != 0) ~@as(u128, 0) else 0; for (bytes) |b| { result = (result << 8) | b; } return @bitCast(result);}fn decode16BytesAsU128(bytes: []const u8) u128 { std.debug.assert(bytes.len == 16); var result: u128 = 0; for (bytes) |b| { result = (result << 8) | b; } return result;}fn encodeI128(allocator: Allocator, value: i128) ![]u8 { if (value == 0) return allocator.alloc(u8, 0); var raw: [16]u8 = undefined; var bits: u128 = @bitCast(value); var i: usize = 16; while (i > 0) { i -= 1; raw[i] = @truncate(bits); bits >>= 8; } var start: usize = 0; while (start + 1 < 16) { const first = raw[start]; const second = raw[start + 1]; if (first == 0x00 and (second & 0x80) == 0) { start += 1; continue; } if (first == 0xff and (second & 0x80) != 0) { start += 1; continue; } break; } return allocator.dupe(u8, raw[start..]);}fn encodeU128(allocator: Allocator, value: u128) ![]u8 { std.debug.assert(value > @as(u128, @intCast(std.math.maxInt(i128)))); var raw: [17]u8 = undefined; raw[0] = 0x00; var bits = value; var i: usize = 17; while (i > 1) { i -= 1; raw[i] = @truncate(bits); bits >>= 8; } return allocator.dupe(u8, &raw);}test "SignedInteger round-trip I128 fast path" { const allocator = std.testing.allocator; const cases = [_]i128{ 0, 1, -1, 127, -128, 128, -129, 12345, -12345, std.math.maxInt(i128), std.math.minInt(i128) }; for (cases) |v| { const si = SignedInteger.fromI128(v); try std.testing.expectEqual(Tier.i128, @as(Tier, si.repr)); const bytes = try si.toCanonicalBytes(allocator); defer allocator.free(bytes); var roundtrip = try SignedInteger.fromCanonicalBytes(allocator, bytes); defer roundtrip.deinit(allocator); try std.testing.expect(si.eql(roundtrip)); try std.testing.expectEqual(v, try roundtrip.toI128()); }}test "SignedInteger U128 tier above i128 max" { const allocator = std.testing.allocator; const above: u128 = @as(u128, @intCast(std.math.maxInt(i128))) + 1; const si = SignedInteger.fromU128(above); try std.testing.expectEqual(Tier.u128, @as(Tier, si.repr)); const bytes = try si.toCanonicalBytes(allocator); defer allocator.free(bytes); try std.testing.expectEqual(@as(usize, 17), bytes.len); try std.testing.expectEqual(@as(u8, 0x00), bytes[0]); try std.testing.expect((bytes[1] & 0x80) != 0); var roundtrip = try SignedInteger.fromCanonicalBytes(allocator, bytes); defer roundtrip.deinit(allocator); try std.testing.expectEqual(Tier.u128, @as(Tier, roundtrip.repr)); try std.testing.expectEqual(above, try roundtrip.toU128());}test "SignedInteger big tier holds bytes above u128" { const allocator = std.testing.allocator; var bytes_buf: [24]u8 = undefined; bytes_buf[0] = 0x01; @memset(bytes_buf[1..], 0x00); var si = try SignedInteger.fromCanonicalBytes(allocator, &bytes_buf); defer si.deinit(allocator); try std.testing.expectEqual(Tier.big, @as(Tier, si.repr)); try std.testing.expect(!si.isNegative()); try std.testing.expectError(RangeError.OutOfRange, si.toU128());}test "SignedInteger rejects non-canonical bytes" { const allocator = std.testing.allocator; const non_canonical = [_]u8{ 0x00, 0x00 }; try std.testing.expectError(DecodeError.NonCanonicalInteger, SignedInteger.fromCanonicalBytes(allocator, &non_canonical)); const sign_ext = [_]u8{ 0xff, 0xff }; try std.testing.expectError(DecodeError.NonCanonicalInteger, SignedInteger.fromCanonicalBytes(allocator, &sign_ext));}test "SignedInteger cross-tier ordering" { const neg_i128 = SignedInteger.fromI128(-5); const pos_i128 = SignedInteger.fromI128(5); const u128_val = SignedInteger.fromU128(@as(u128, @intCast(std.math.maxInt(i128))) + 1); try std.testing.expectEqual(std.math.Order.lt, SignedInteger.order(neg_i128, pos_i128)); try std.testing.expectEqual(std.math.Order.lt, SignedInteger.order(pos_i128, u128_val)); try std.testing.expectEqual(std.math.Order.gt, SignedInteger.order(u128_val, neg_i128));}test "SignedInteger.toI64Lossy saturates out-of-range tiers" { try std.testing.expectEqual(@as(i64, 42), SignedInteger.fromI128(42).toI64Lossy()); try std.testing.expectEqual(@as(i64, -42), SignedInteger.fromI128(-42).toI64Lossy()); const huge_pos: i128 = std.math.maxInt(i64) + @as(i128, 1); try std.testing.expectEqual(std.math.maxInt(i64), SignedInteger.fromI128(huge_pos).toI64Lossy()); const huge_neg: i128 = std.math.minInt(i64) - @as(i128, 1); try std.testing.expectEqual(std.math.minInt(i64), SignedInteger.fromI128(huge_neg).toI64Lossy()); const u128_val = SignedInteger.fromU128(@as(u128, @intCast(std.math.maxInt(i128))) + 1); try std.testing.expectEqual(std.math.maxInt(i64), u128_val.toI64Lossy());}Source: lib/preserves/src/root.zig:105
zig
pub const integer_mod = @import("integer.zig");Audit
| Definitions | 4 |
|---|---|
| Public names | 4 |
| Members | 5 |
| Version | 26.7.0 |
| Revision | daab053ee433 |