A novel binary integer abstraction

A novel binary integer abstraction

Hello, everybody! I'm working on a novel binary integer abstraction on GitHub.

I thought I'd share it in case some of you find it interesting. It unifies fixed-width and variable-width representations and brings recovery mechanisms to generic code. It's experimental, and I don't expect it to stabilize anytime soon, but it's still super neat in its current state. It's more or less at the point where I want to build something with it and iterate based on that experience.

I'm 5 months and (128,665 ++, 102,232 --) deep into it.

It features 8-bit to 64-bit integers, a generic un/signed double-width model, and a generic un/signed big-integer type. It also introduces a notion of infinity to ensure invariants like ~(~(x)) == x for all x.

The README.md is a work in progress, but it explains the new concepts.

Also, I have seen @wadetregaskis mention my predecessor project Numberick a few times, and I appreciate the positive commentary. Thanks! Having said that, I don't plan on maintaining the old project since the new abstraction is much more powerful.


Example: constant-foldable generic comparisons...
extension BinaryInteger {
    @inlinable public func compared<Other>(to other: Other) -> Signum where Other: BinaryInteger {
        if  let lhsSize = UX(size: Self.self), let rhsSize = UX(size: Other.self) {
            if  lhsSize < rhsSize {
                return Other(load: self).compared(to: other)
                
            }   else if lhsSize > rhsSize {
                return self.compared(to: Self(load: other))
                
            }   else if Self.isSigned, !Other.isSigned {
                return self .isNegative ? Signum.less : Other(load: self).compared(to: other)
                
            }   else if !Self.isSigned, Other.isSigned {
                return other.isNegative ? Signum.more : self.compared(to: Self(load:  other))
                
            }   else {
                return self.compared(to: Self(load: other))
            }
            
        }   else {
            //  note that the elements must be rebindable one way or the other
            if  Other.elementsCanBeRebound(to: Self.Element.Magnitude.self) {
                return self.withUnsafeBinaryIntegerElements { lhs in
                    (other).withUnsafeBinaryIntegerElements(as: Self.Element.Magnitude.self) { rhs in
                        DataInt.compare(
                            lhs: lhs, lhsIsSigned: Self .isSigned,
                            rhs: rhs, rhsIsSigned: Other.isSigned
                        )
                    }!
                }
                
            }   else {
                Swift.assert(Self.elementsCanBeRebound(to: Other.Element.Magnitude.self))
                return self.withUnsafeBinaryIntegerElements(as: Other.Element.Magnitude.self) { lhs in
                    (other).withUnsafeBinaryIntegerElements { rhs in
                        DataInt.compare(
                            lhs: lhs, lhsIsSigned: Self .isSigned,
                            rhs: rhs, rhsIsSigned: Other.isSigned
                        )
                    }
                }!
            }
        }
    }
}
Example: a fully generic and fully recoverable Fibonacci sequence...
@frozen public struct Fibonacci<Value> where Value: BinaryInteger {
    
    //=------------------------------------------------------------------------=
    // MARK: State
    //=------------------------------------------------------------------------=
    
    @usableFromInline var i: Value
    @usableFromInline var a: Value
    @usableFromInline var b: Value
    
    //=------------------------------------------------------------------------=
    // MARK: Initializers
    //=------------------------------------------------------------------------=
    
    /// Creates the first sequence pair.
    @inlinable public init() {
        self.i = 0
        self.a = 0
        self.b = 1
    }
    
    /// Creates the sequence pair at the given `index`.
    @inlinable public init(_ index: Value) throws {
        if  Bool(index.appendix) {
            throw Value.isSigned ? Failure.indexOutOfBounds : Failure.overflow
        }
        
        self.init()
        
        try index.withUnsafeBinaryIntegerElementsAsBytes {
            let x = $0.body.count(.nondescending(.zero))
            for i in (0 ..< x).reversed() {
                try self.double()
                
                if  $0.body[unchecked: i &>> 3] &>> U8(load: i) & 1 != 0 {
                    try self.increment()
                }
            }
        }
    }
    
    //=------------------------------------------------------------------------=
    // MARK: Accessors
    //=------------------------------------------------------------------------=
    
    /// The sequence `index`.
    @inlinable public var index: Value { 
        self.i
    }
    
    /// The sequence `element` at `index`.
    @inlinable public var element: Value {
        self.a
    }
    
    /// The sequence `element` at `index + 1`.
    @inlinable public var next: Value { 
        self.b
    }
    
    //=------------------------------------------------------------------------=
    // MARK: Transformations
    //=------------------------------------------------------------------------=
    
    /// Forms the sequence pair at `index + 1`.
    @inlinable public mutating func increment() throws {
        let ix = try i.plus(1).prune(Failure.overflow)
        let bx = try a.plus(b).prune(Failure.overflow)
        
        self.i = consume ix
        self.a = b
        self.b = consume bx
    }
    
    /// Forms the sequence pair at `index - 1`.
    @inlinable public mutating func decrement() throws {
        let ix = try i.minus(1).veto({ $0.isNegative }).prune(Failure.indexOutOfBounds)
        let ax = try b.minus(a).prune(Failure.overflow)
        
        self.i = consume ix
        self.b = a
        self.a = consume ax
    }
    
    //=------------------------------------------------------------------------=
    // MARK: Transformations
    //=------------------------------------------------------------------------=
    
    /// Forms the sequence pair at `index * 2`.
    @inlinable public mutating func double() throws {
        let ix = try i.times(2).prune(Failure.overflow)
        let ax = try b.times(2).minus(a).times (a).prune(Failure.overflow)
        let bx = try b.squared().plus(a.squared()).prune(Failure.overflow)

        self.i = consume ix
        self.a = consume ax
        self.b = consume bx
    }
    
    /// Forms the sequence pair at `index + x.index`.
    @inlinable public mutating func increment(by x: Self) throws {
        let ix = try i.plus (x.i).prune(Failure.overflow)
        let ax = try a.times(x.b).plus(b.minus(a).times(x.a)).prune(Failure.overflow)
        let bx = try b.times(x.b).plus(       (a).times(x.a)).prune(Failure.overflow)

        self.i = consume ix
        self.a = consume ax
        self.b = consume bx
    }
    
    /// Forms the sequence pair at `index - x.index`.
    @inlinable public mutating func decrement(by x: Self) throws {
        let ix = try i.minus(x.i).veto({ $0.isNegative }).prune(Failure.indexOutOfBounds)
        
        var a0 = b.times(x.a).value
        var a1 = a.times(x.b).value
        var b0 = b.plus(a).times(x.a).value
        var b1 = b.times(x.b).value
        
        if  Bool(x.i.leastSignificantBit) {
            Swift.swap(&a0, &a1)
            Swift.swap(&b0, &b1)
        }
        
        self.i = consume ix
        self.a = a1.minus(a0).value
        self.b = b1.minus(b0).value
    }
    
    //*========================================================================*
    // MARK: * Failure
    //*========================================================================*
    
    public enum Failure: Error {
        
        /// Tried to form a sequence pair that cannot be represented.
        case overflow
        
        /// Tried to form a sequence pair at an index less than zero.
        case indexOutOfBounds
    }
}
6 Likes

Looks nice! I created a PR with some typos fixed

1 Like

Neat! Apologies if I missed it, but I read through the readme and didn't see a direct performance comparison to the stdlib integers. Do you have benchmarks somewhere?

I understand many ops may constant fold, I'm curious how many do in practice. Additionally would a notion of const be helpful for ensuring constant folding anywhere in this library?

1 Like

Neat! Apologies if I missed it, but I read through the readme and didn't see a direct performance comparison to the stdlib integers. Do you have benchmarks somewhere?

I performed a lot of micro benchmarks in Numberick, but there was little payoff and nobody cared until quite recently when somebody noticed that this process had made the fib sequence quite speedy. Still, there has been no real payoff, so I've omitted benchmarking. Instead, I have focused on abstraction, complexity, and recovery in generic code. I would rather build something cool and measure the performance by proxy. I have compared the fib sequence, however, and it's a bit faster than it was in Numberick. So the transition from mutating methods to consuming methods seems to have gone well (Β± the occasional nonzero exit code).

//   MacBook Pro, 13-inch, M1, 2020, -O, code coverage disabled
try! Fibonacci<UXL>( 1_000_000) // 0.04s
try! Fibonacci<UXL>(10_000_000) // 1.65s from 2.30s in Numberick

I understand many ops may constant fold, I'm curious how many do in practice. Additionally would a notion of const be helpful for ensuring constant folding anywhere in this library?

In my experience, constants :tm: almost always fold if you follow the rules to the point that a function call can be specialized. At least, that's my experience looking at Swift.FixedWidthInteger assembly on Godbolt. Again, I don't know if they do in this project, because I have not looked at that level of detail. I suppose a compiler hint would be useful if it could guarantee that they are evaluated at compile time. I'm not sure how such a hint would work with the dynamic any stuff, which I rely on a lot for testing purposes, however.

2 Likes

Good performance is something people rarely comment on, even though it matters to them. And with relatively uncommon domains, like arbitrary-precision integers, people don't have a good intuition for what the performance should be, so they might just accept whatever they get.

I think Numberick may have also simply suffered from being ranked lower in searches than BigInt, in part down to simple things like the name not being as self-explanatory.

Also, your readme on Numberick does say that it's basically abandoned, so that's going to discourage people from using it. :slightly_smiling_face:

3 Likes

My point is that benchmarking is an unpaid time sink. I'd need food, not GitHub stars.

2 Likes

Here's a cute measurement for those of you who like benchmarks:

MacBook Pro, 13-inch, M1, 2020, -O, code coverage disabled.
let fib1e6 = try! Fibonacci<UXL>(1_000_000)

let fib1e6r10 = fib1e6.element.description(as:     .decimal) // 0.924s (208988 digits)
let fib1e6r16 = fib1e6.element.description(as: .hexadecimal) // 0.002s (173561 digits)

try! UXL(fib1e6r10, as:     .decimal) // 0.040s (208988 digits)
try! UXL(fib1e6r16, as: .hexadecimal) // 0.002s (173561 digits)

This example is on the asymptotic side of things, and there might be better
algorithms for silly numbers, but silly examples are more fun. It also shows
that hexadecimal is king.

The conversion model uses the same non-generic and non-inlinable algorithms for all binary integers. So it's really optimized for size rather than performance, but it's still quite decentβ€”as you can see.

The encoding strategy is more or less the algorithm I wrote for the Foundation project but generalized over the various radices. I would have proposed an arbitrary-precision decoding strategy as well if Swift.BinaryInteger did not make it so difficult. The problem is that it lacks recovery options (1) or a bitwise initializer (2). It is still possible to implement generic arbitrary-precision decoding, but it requires a Swift.BigInt model because of (1) and (2).

2 Likes

So it turns out that using StaticBigInt made DoubleInt<T> super slow. I suppose this is one such case where it would be nice to have compile-time constants. Tangentially, StaticBigInt does not provide access to a buffer pointer, unlike its StaticString sibling. While I'm unfamiliar with its built-in model, I think it would be appropriate to provide a buffer-pointer-view-thing-y if StaticBigInt happens to be represented as binary integer words in memory.

Also, I've made things even more generic and fixed like a gazillion bugs. I believe even the comparison example is incorrect, but I can't edit it. It's like embarrassment etched in stone.

The original pitch had a buffer pointer, but it was removed for various reasons.


Future directions of StaticBigInt:

I wanted to fuzz something, so I've added a new module and some randomness features:

.product(name: "Ultimathnum",  package: "Ultimathnum"), // umbrella
.product(name: "CoreKit",      package: "Ultimathnum"),
.product(name: "DoubleIntKit", package: "Ultimathnum"),
.product(name: "FibonacciKit", package: "Ultimathnum"),
.product(name: "InfiniIntKit", package: "Ultimathnum"),
.product(name: "RandomIntKit", package: "Ultimathnum"), // oh, so shiny!
.product(name: "StdlibIntKit", package: "Ultimathnum"),

Random number generation is fully generic over BinaryInteger, so it also works with signed and unsigned arbitrary-precision integers:

extension Ultimathnum.BinaryInteger {

    /// Generates a random value in the given `range` from the given source of `randomness`.
    ///
    /// - Requires: The `range` must be finite.
    ///
    public static func random(in range: ClosedRange<Self>, using randomness: inout some Randomness) -> Self { ... }
         
    /// Generates a random value in the given `range` from the given source of `randomness`.
    ///
    /// - Requires: The `range` must be finite.
    ///
    public static func random(in range: Range<Self>, using randomness: inout some Randomness) -> Optional<Self> { ... }
         
    /// Generates random bits through the given `index`.
    ///
    /// Signed integers are extended by the most significant bit whereas unsigned
    /// integers are extended by zero. You may bit-cast a different type to adopt
    /// its behavior.
    ///
    ///                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    ///                β”‚ index: 0 β”‚ index: 1 β”‚ index: 2 β”‚
    ///     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
    ///     β”‚   Signed β”‚ -1 ... 0 β”‚ -2 ... 1 β”‚ -4 ... 3 β”‚
    ///     β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
    ///     β”‚ Unsigned β”‚  0 ... 1 β”‚  0 ... 3 β”‚  0 ... 7 β”‚
    ///     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
    ///
    /// - Note: The result is always finite.
    ///
    /// - Requires: The request must not exceed the entropy limit.
    ///
    public static func random(through index: Shift<Magnitude>, using randomness: inout some Randomness) -> Self { ... }
}

Arbitrary integers and systems integers derive different algorithms, which is possible because I've hoisted 1-by-1-as-2 multiplication to BinaryInteger:

U32.max.times(U32.max) // value: 1, error: true
UXL.max.times(UXL.max) // value: 1, error: true

U32.max.multiplication(U32.max) // low: 1, high: ~1
UXL.max.multiplication(UXL.max) // low: 1, high: ~1

It turns out that computing the high part of multiplication is trivial in both the signed and the unsigned case, making the latter even more infinite! Lol.

I wrote some division-by-magic-constant-multiplication for my unified text decoding and encoding model, which made the earlier decimal encoding case more than three times faster. I've added 1-by-1 and 2-by-1 magic constant finders.

Note that the 2-by-1 model is responsible for the performance improvement. I first tried reducing the chunking size to 32 bits and performing the 2-by-1 division using a 64-bit version of the 1-by-1 model, but that only improved it by ~20%.

How does it work?

You can add a rounding error to a rational number and get the same quotient, provided that the rounding error is sufficiently insignificant:

       a            a               a            1 
floor( - ) = floor( - + e ) for all - if 0 ≀ e < -
       b            b               b            b

Dividing is expensive and complicated, so we could instead replace it with an unsigned right shift and a sufficiently close approximation of c = (2^k)/b:

       a            2^k    a  
floor( - ) = floor( --- * --- ) = ( c * a ) >> k
       b             b    2^k 

We can always find a good same-size approximation if we allow ourselves to round the multiplier either up or down. We must compensate the latter case with an increment, turning our division algorithm into a multiplication, addition, and shift. The bounds are thus given by the following, where x is the multiplier's rounding error:

The rounding-up case (the increment is zero)
a   2^k + x   a + 0   a   1              
- ≀ ------- * ----- < - + - when 0 ≀ x ≀ 2^(k - size)
b      b       2^k    b   b              
The rounding-down case (the increment is equal to the multiplier)
a   2^k - x   a + 1   a   1
- ≀ ------- * ----- < - + - when 0 < x ≀ 2^(k - size)
b      b       2^k    b   b

At least one case is guaranteed to have a good approximation when k - size β‰₯ log2(b). This mul-add-shr operation also works for power-of-two divisors if we contrive a downward rounding error of 1. It's magic :sparkles:

1 Like

Ultimathnum 0.9.0

Shiny new things and better old things.

  • #84 Improve: arbitrary integer multiplication
  • #83 Add: BinaryInteger/factorial()
  • #82 A more number-y Signum
  • #81 Improve: TextInt
  • #80 Add: BinaryInteger/negate()
  • #79 Hoist GCD, XGCD, and remove unsigned constraint
  • #76 Add: BinaryInteger/isqrt()
  • #75 Generic two-way comparisons
  • #74 Add: Interoperable
  • #73 Using Randomness in Stdlib methods
  • #72 Generic exponent in BinaryInteger/power(_:)
  • #71 Free multiplication in BinaryInteger/power(_:)
  • #70 Add: Division/exactly()
  • #66 Miscellaneous 0.9.0 stuff
  • #64 Divider21<T>
  • #63 Division by constant in TextInt
  • #62 Protocol: Guarantee

Highlight: arbitrary integers go brrr

MacBook Pro, 13-inch, M1, 2020, -O, code coverage disabled.
try Fibonacci<UXL>( 1_000_000) // 0.02s (was 0.04s)
try Fibonacci<UXL>(10_000_000) // 0.50s (was 1.65s)

Arbitrary base case multiplication performed poorly due to poor performance of:

MutableDataInt.Body/incrementSubSequence(by:times:plus:)

A quick fix made my arbitrary-multiplication-related benchmarks run ~3x faster.

Highlight: the Interoperable protocol

I had some issues interoperating with ordinary Swift code, so I added this protocol:

public protocol Interoperable {
    
    associatedtype Stdlib
    
    init(_ stdlib: consuming Stdlib)
        
    consuming func stdlib() -> Stdlib
    
}

Types conforming to Interoperable have a standard-library-compatible representation determined by their associated Stdlib type. I32 yields Int32, and U64 yields UInt64, for example. Interoperable requires bidirectional consuming conversions: init(_:) and stdlib(). It then derives mutating read and modify accessors available through stdlib:

var randomness = RandomInt() // from RandomIntKit
let random = Bool.random(using: &randomness.stdlib)
1 Like

Highlight: type-agnostic binary integer hashes

Binary integers with equal valuesβ€”regardless of the underlying typeβ€”now produce equal hashes on the main branch. Swift proper does something similar, sometimes. Specifically, Swift either extends or truncates integers to 64 bits when it converts them to AnyHashable so that integers up to the given size have this behavior. Going by the source code comments, this is to preserve the behavior of NSNumber (although I find it elegant on its own).

extension Ultimathnum.BinaryInteger {

    /// Hashes the normalized 8-bit data integer representation of `self`.
    ///
    /// ```swift
    /// #expect(random.hashValue == IXL(load: random).hashValue)
    /// #expect(random.hashValue == UXL(load: random).hashValue)
    /// ```
    ///
    @inlinable public func hash(into hasher: inout Swift.Hasher) {
        self.withUnsafeBinaryIntegerElements(as: U8.self) {
            let normalized: DataInt<U8> = $0.normalized()
            hasher.combine(bytes: normalized.body.bytes())
            hasher.combine(normalized.appendix)
        }
    }
}

The obvious next step is performing a similar AnyHashable conversion while preserving equality for all binary integer types and sizes. Using the new abstraction in combination with Swift's existential any makes this type-erasure trivial. It's only a few lines of code, but protocol _AnyHashableBox is internal, so external developers can't use it. Sad; the end.

I did not #expect this AnyHashable

I fell into the AnyHashable rabbit hole while adopting swift-testing. In this case, the #expect macro performs a rather surprising transformation from Swift.Int to AnyHashable:

@Test func what() {

    // Expectation failed: (U32.zero β†’ 0) == (0 * 0 β†’ 0)
    //
    // - U32.zero : 0
    //   U32
    //   - base : 0
    //     UInt32
    //
    // - 0 * 0 : 0
    //   AnyHashable
    //   - value : 0
    //     Int
    //
    #expect(U32.zero == 0 * 0)
}

It isn't obvious what it does after converting 0 * 0 to AnyHashable. I initially thought I would fix it (the hashing, not the conversion) by forwarding the standard library implementation of Hashable/hash(into:) but that's not enough. Instead, you must conform to the Swift._HasCustomAnyHashableRepresentation protocol and implement its _toCustomAnyHashable() requirement. In any case, I found out that the standard library approach only works up to 64 bits, so I dropped it in (#101).

Ultimathnum 0.11.0 & 0.10.0

It's been a while since my last post, but this project never sleeps.

Highlight: using swift-testing

I've transitioned all unit tests (100+ files) from XCTest to Testing. I've also unified most binary integer tests and randomized their inputs. I especially appreciate how parameterized tests automatically print each argument description since that is enough to recreate a seeded random number generator. Generic error handling at all levels of abstraction in this project lets us use this shape for most tests:

@Test(arguments: types, fuzzers) 
func invariant(
    type: any BinaryInteger.Type, randomness: consuming FuzzerInt
)   throws {
    
    try  whereIs(type)
    func whereIs<T>(_ type: T.Type) throws where T: BinaryInteger {
        try #require(...)
        try #require(...)
        try #require(...)
    }
}

Highlight: a scalable redemption arc

Implicit error propagation did not scale as well as I had hoped, so I've replaced it with more powerful error-handling utilities. In particular, the following transformation has become my new favorite tool:

extension Fallible {
    
    /// Returns the `value` by setting the `remote` indicator on `error`.
    ///
    /// ```swift
    /// var value = U8(3)
    /// var error = false
    ///
    /// value = value.decremented().sink(&error) // value:   2, error: false
    /// value = value.decremented().sink(&error) // value:   1, error: false
    /// value = value.decremented().sink(&error) // value:   0, error: false
    /// value = value.decremented().sink(&error) // value: 255, error: true
    /// ```
    ///
    @inlinable public consuming func sink(_ remote: inout Bool) -> Value
}

Highlight: overhaul of Fibonacci<T> (on main)

I took a moment of respite to smell the flowers and enjoy the silly things. I've extended the Fibonacci<T> sequence to negative indices and reworked the error handling process. Generic binary integers now return the most appropriate value in T, Fallible<T>, and Optional<Fallible<T>>. You may read lossy as truncating.

extension Ultimathnum.BinaryInteger {

    /// Returns the `Fibonacci<Self>` sequence element 
    /// at `index` and an `error` indicator, or `nil`.
    ///
    /// - Note: It ignores `major` element `error` indicators.
    ///
    /// ### Fibonacci
    ///
    /// - Note: The `error` is set if the operation is `lossy`.
    ///
    /// - Note: It produces `nil` if the `index` is `infinite`.
    ///
    @inlinable public static func fibonacci(
        _ index: consuming Self
    )   -> Optional<Fallible<Self>>
    
    @inlinable public static func fibonacci(
        _ index: consuming Self
    )   -> Fallible<Self> where Self: FiniteInteger
    
    @inlinable public static func fibonacci(
        _ index: consuming Self
    )   -> Self where Self: ArbitraryInteger & SignedInteger
}

Future direction: making StdlibInt generic

This project provides a big Swift.BinaryInteger wrapper around InfiniInt<IX>. In the next release, I want to make it generic to interoperate with Swift.FixedWidthInteger using DoubleInt<T> or a future VectorInt<T> perhaps. I started with a concrete InfiniInt<IX> wrapper because I didn't yet have the facilities to implement generic binary-integer-from-binary-floating-point conversions, but now I do (0.10.0). In any case, that should be exciting.

3 Likes

Ultimathnum 0.12.0

Highlight: the grateful guest

This project builds custom abstractions on top of Swift's powerful type system. It now extends its functionality to Swift-proper through various interoperability modules in an act of gratitude. Importing these modules gives you access types conforming to the appropriate standard library protocols. Here's a list of all available package products:

.product(name: "Ultimathnum",  package: "Ultimathnum"), // umbrella
.product(name: "CoreKit",      package: "Ultimathnum"),
.product(name: "DoubleIntKit", package: "Ultimathnum"),
.product(name: "FibonacciKit", package: "Ultimathnum"),
.product(name: "InfiniIntKit", package: "Ultimathnum"),
.product(name: "RandomIntKit", package: "Ultimathnum"),

.product(name: "Ultimathiop",  package: "Ultimathnum"), // umbrella
.product(name: "CoreIop",      package: "Ultimathnum"),
.product(name: "DoubleIntIop", package: "Ultimathnum"),
.product(name: "InfiniIntIop", package: "Ultimathnum"),
.product(name: "RandomIntIop", package: "Ultimathnum"),

Some models, like the core binary integer types and the system random number generator, interoperate naturally with Swift. In that case, the various stdlib() functions will return the appropriate standard library types. As such, the interoperability features of the Interoperable protocol do not introduce any unnecessary monomorphization.

Highlight: T.Stdlib not StdlibInt<T>

Remember how I wanted to make StdlibInt generic? Adding concrete DoubleInt.Stdlib and InfiniInt.Stdlib models is a much better solution. It turns out that signed arbitrary Swift.BinaryInteger(s) are well-behaved, whereas unsigned arbitrary Swift.BinaryInteger(s) are foot guns (in my most sincere opinion). So, to keep our feet safe, we must ensure that Magnitude == Self in the arbitrary binary integer case.

Here's an exercise for the reader: install your favorite BigUInt package and check whether the following expressions produce numerically nonsensical results. If you're curious, consider investigating how this project solves it.

1. ~BigUInt(0) // 0 is silly
2.  BigUInt(truncatingIfNeeded: -1) // 18446744073709551615 is silly

Future direction: more interoperability stuff

I want to support the new and shiny 128-bit integers at some point, but Swift.Int128 and Swift.UInt128 currently fail some tests relating to recoverable division by zero. If somebody on the Swift team wants to put on a cape and be the hero who reviews my pull request fixing this issue, that would be awesome (Fix recoverable [U]Int128 division-by-zero by oscbyspro Β· Pull Request #77854 Β· swiftlang/swift Β· GitHub).

P.S. @stephentyrone donned the cape in between writing and posting.

2 Likes

If you have any interesting ideas or features you'd want to see, I'd love to hear them! I'm a bit busy starving, but I'm otherwise open to suggestions. Also, are there any benchmarks or comparisons you'd be interested in? I haven't started any serious optimization efforts yet, but I think the project is approaching a state where that might be fun.