Skip to documentation
SLOP

tiny.preserves.integer_mod

Reference 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.

No direct callersNo direct callstiny.preservesinteger mod
Static calls · unresolved targets: unknown · external targets: unknown.

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

Definitions4
Public names4
Members5
Version26.7.0
Revisiondaab053ee433