Giter VIP home page Giter VIP logo

ultimathnum's Introduction

Ultimathnum

Everyone who hears these words of mine and does them is like a wise man who built his house on rock. The rain fell, the flood came, and the winds beat against that house, but it did not collapse because its foundation had been laid on rock. (Matthew 7:24-25)

Table of Contents

In programming, complexity is sand, and simplicity is a rock. The former is a sum of parts. This project unifies abstractions and streamlines models to keep the parts few and similar to each other.

This project is mostly self-contained. It only extends the standard library when necessary. In practice, this means adding conversion to and from new types. Other kinds of functionality either belongs to the new types or is kept private through access control. This ensures interoperability without confusion or accidental dependencies.

The are many ways to abstract and there are pros and cons to all of them. Here, we keep it simple. We use a small set of primitives to derive most things. Our abstractions are purposeful and close to the machine. Likewise, you will not find many mutating methods. Instead, we opt for consuming methods. This cuts the project in half, compared to previous iterations of it.

If simplicity is a rock, then recovery is the capstone. Gone are they days of unavoidable traps because there now exists a recoverable alternative for every safe operation. Overflow, underflow, whatever—the framework will lazily prompt you to handle it. Furthermore, the abstractions and recovery mechanisms presented to you are ever-present in generic code as well.

Nomenclature

Binary integers are dead.
Long live binary integers!

This project presents a novel binary integer abstraction that unifies the different sizes. It views all binary integers as infinite bit sequences with various modes of operation. It extends the maximum unsigned value to infinity and lets you recover from failure through intuitive, ergonomic, and generic error propagation mechanisms.

(~0 as IXL) // -1
(~1 as IXL) // -1 - 1
(~0 as UXL) //  ∞
(~1 as UXL) //  ∞ - 1

Some definitions have been updated to fit the new binary integer format. What was once called the sign bit is now the appendix bit. This means a so-called signed right shift treats an infinite unsigned integer like a negative signed integer, for example.

(~2 as UXL) >> 1 == (~1 as UXL)
(~1 as UXL) >> 1 == (~0 as UXL)
(~0 as UXL) >> 1 == (~0 as UXL)
( 0 as UXL) >> 1 == ( 0 as UXL)
( 1 as UXL) >> 1 == ( 0 as UXL)
( 2 as UXL) >> 1 == ( 1 as UXL)

In practice, an infinite integer is represented by its least significant body elements, followed by an infinite repetition of its appendix bit. All signed integers and all infinite integers store this bit in memory, but unsigned systems integers return zero through the type system. This makes all systems integers compatible with the un/signed two's complement format.

~(~(x)) == x for all x
┌────────────────────┐
│ some BinaryInteger │
├──────┬─────────────┤
│ body │ appendix... │
└──────┴─────────────┘

A binary integer must provide contiguous access to its endianness-sensitive body, which can be viewed through a data integer. Such view types keep track of memory alignments and may downsize their element type through reinterpretation.

Read Read & Write
DataInt<Element> MutableDataInt<Element>.Body
DataInt<Element>.Body MutableDataInt<Element>.Body

All binary integers agree on this format, which lets us perform arbitrary conversions with a fixed set of protocol witnesses. Compare this to the square matrix of doom that is formed by generic protocol requirements. In theory, a binary integer needs two initializers. One for aligned loads from compatible sources and one for unaligned loads from unknown sources. In practice, this depends on whether the optimizer can turn them into appropriate instructions.

init(load source: DataInt<U8>)                // unknown
init(load source: DataInt<Element.Magnitude>) // aligned
Here are the other conversion requirements...
@inlinable init(load source: consuming  UX.Signitude)
@inlinable init(load source: consuming  UX.Magnitude)
@inlinable borrowing func load(as type: UX.BitPattern.Type) -> UX.BitPattern

@inlinable init(load source: consuming  Element.Signitude)
@inlinable init(load source: consuming  Element.Magnitude)
@inlinable borrowing func load(as type: Element.BitPattern.Type) -> Element.BitPattern

Keep it simple.

You may realize that the infinite bit pattern definition implies a static size for all binary integers. Indeed, you can compare their sizes through metadata alone. Still, not all binary integers are trivial. A systems integer is represented in memory by its bit pattern. Additionally, a systems integer's size is a power of two in 8 through IX.max.

Size Signed Unsigned
pointer IX UX
8-bit I8 U8
16-bit I16 U16
32-bit I32 U32
64-bit I64 U64

Systems integers are intentionally simple so that the things you build with them may be simple. The only protocol requirements are multiplication and division algorithms for working with full precision in generic code, and byte swapping for efficient endianness conversions.

Trust me, I know what I'm doing...

Type Guarantee
Divisor x ≠ 0
Finite x ∈ ℤ
Natural x ∈ ℕ
Shift x ∈ 0 up to T.size

Once you start using primitive types to form more complex types, you notice that some behaviors compose better than others. A trusted input delegates precondition checks to the programmer so that complex types can be built with less overhead. The type system will ask you to accept or reject such inputs. Your validation strategy will either make your code safer or easier to audit.

init(_:)         // error: traps
init(_:prune:)   // error: throws
init(exactly:)   // error: nil
init(unchecked:) // error: unchecked

We all know that dynamic validation comes at a price. The validation tax tends to be small in everyday code, but it can be noticeable when working with primitives. That is why some operations have unchecked alternatives. In this project, we validate unchecked operations in debug mode only. You may realize that it is sometimes correct to ignore errors completely. It is, therefore, best to view unchecked operations as semantic markers in performance-sensitive places.

U8.zero.decremented().error       // true, because the result is not representable
U8.zero.decremented().value       // 255,  this is the truncated bit pattern of -1
U8.zero.decremented().unchecked() // 255,  with precondition failure in debug mode

It doesn't matter how many times you fall.
It matters how many times you get back up.

Ergonomic error handling is one of the cornerstones of this project and a lot of effort has gone into ensuring that there's always a path to redemption. The Fallible<Value> wrapper plays an important part the recovery story. Use it to handle errors when you want and how you want. Here's a real example from the generic Fibonacci<Value> sequence:

/// 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
}

Note that each operation propagates failure by setting Fallible<Value>'s error indicator, which is lazily checked in the throwing prune(_:) method. Alternatively, you may use optional(_:), result(_:) unwrap(_:), assert(_:) or plain value and error to validate your results. Also, note that each operation accepts both Value and Fallible<Value> inputs to make error handling as seamless as possible. Alternatively, you may use any of the common arithmetic operators for trapping or wrapping results:

static func  +(lhs: consuming Self, borrowing Self) -> Self // trapping
static func &+(lhs: consuming Self, borrowing Self) -> Self // wrapping

We don't know where it comes from, only that it exists.

You may create integers out of thin air with RootInt, a wrapper around Swift.StaticBigInt. It comes with some additional bells and whistles. You may, for example, query whether a generic type can represent an integer literal or load the extended bit pattern that fits.

static func exactly(_ source: RootInt) -> Fallible<Self>

Each data integer operates on the contiguous in-memory representation of binary integers without taking ownership of them. The [Mutable]DataInt.Body type is fundamentally a buffer pointer. The [Mutable]DataInt type extends the bit pattern of its body with a repeating appendix bit. You may perform various buffer and arithmetic operations on these types, but remember that their operations are finite, unsigned, and unchecked by default. Additionally, a data integer subsequence is usually represented by a rebased instance of the same type.

The data integer types let you downsize binary integer elements by reinterpretation. This versatile power stems from strict systems integer layout requirements. You may not upsize elements in this way, however, because the memory alignment of a smaller systems integer may not be compatible with a larger systems integer. Instead, you may use DataInt<U8> to load elements of any size. It performs an unaligned load when possible and handles the case where the load would read past the end. All binary integers can form a DataInt<U8> view since a byte is the smallest possible systems integer type.

At some point, you'll want to convert your binary integers to a human-readable format. When that happens, the description(as:) and init(_:as:) methods let you perform the common radix conversions via TextInt. The latter uses a fixed number of non-generic and non-inlinable algorithms, which are shared by all binary integers. This is an intentional size-over-performance optimization.

try! TextInt(radix:  12, letters: .uppercase)
try! IXL("123", as: .decimal).description(as: .hexadecimal) //  7b
try! IXL("123", as: .hexadecimal).description(as: .decimal) // 291

You may realize that the introduction of infinite values necessitates changes to the integer description format. The new format adds the # and & markers. The former is a spacer (cf. +) whereas the latter represents bitwise negation. In other words, +&123 translates to ∞ minus 123.

The case-insensitive base 36 regex.
let regex: Regex = #/^(\+|-)?(#|&)?([0-9A-Za-z]+)$/#

While this model prioritizes size, its operations are still fast enough for most purposes. The 210k-digit measurement illustrates this point. Keep in mind that hexadecimal radix conversions are linear operations, whereas decimal conversions are superlinear but instant for numbers intended to be read by humans.

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)
TextInt optimizes base 2, 4, and 16 conversions (but not 8 or 32).

The BitCastable<BitPattern> protocol lets you perform type-safe bit casts in bulk. This is especially pertinent to binary integers since the abstraction is two representations bridged by a bit pattern transformation. You perform type-safe bit-casts by calling init(raw:).

@inlinable public func distance<Distance>(
    to other: Self,
    as type: Distance.Type = Distance.self
)   -> Fallible<Distance> where Distance: SignedInteger {
    
    if  Self.size < Distance.size {
        return Distance(load: other).minus(Distance(load: self))
    
    }   else if Self.isSigned {
        return other.minus(self).map(Distance.exactly)
        
    }   else {
        let distance = Fallible<Signitude>(raw: other.minus(self))
        let superoverflow = distance.value.isNegative != distance.error
        return Distance.exactly(distance.value).veto(superoverflow)
    }
}

The above example shows a generic Strideable/distance(from:to:) esque method. In the narrowing unsigned case you find that the difference is reinterpreted as a same-size signed integer type via the init(raw:) bulk operation. Note that such type relationships are generically available to all binary integers. Also, note that this method is both fully generic and fully recoverable. The init(load:) method is similar, but it returns the bit pattern that fits.

Please roll a D20 arcana check.

An arbitrary binary integer's bit pattern extends infinitely, yet its bit pattern has an end. Count<IX> is a pointer-bit model that can count the bits of any binary integer stored in memory. It does this by reinterpreting the last bit as logarithmically infinite.

min ..< msb: [0,  IX.max + 0]
msb ... max: [∞ - IX.max,  ∞] ≤ log2(UXL.max + 1)

All binary integer types and all data integer types let you perform bit-counting operations; the BitCountable protocol unifies them. Their common protocol requires methods like size() and ascending(_:) then derives methods like entropy() and nonascending(_:) for them.

Types that let you perform bitwise logic, such as AND, OR, XOR, and NOT, conform to BitOperable.  It powers logic gates in generic code, and all binary integer types conform to it. Arbitrary unsigned  integers also perform these operations losslessly thanks to the notion of infinite binary integers.

static prefix func ~(instance: consuming Self) -> Self
static func &(lhs: consuming Self, rhs: borrowing Self) -> Self
static func |(lhs: consuming Self, rhs: borrowing Self) -> Self
static func ^(lhs: consuming Self, rhs: borrowing Self) -> Self

This project introduces various additional types. Some are more important than other, so here's a rundown of the three most prominent ones: Bit, Sign and Signum. Bit and Sign are a lot like Bool, but with bitwise operations and no short-circuits. Signum is most notably the return type of the compared(to:) methods.

Type Values
Bit 0, 1
Sign +, -
Signum -1, 0, 1

Veni, vidi, vici. I came, I saw, I conquered.

 DoubleInt<I128>          DoubleInt<U128>
┌───────────────────────┐┌───────────────────────┐
│ I256                  ││ U256                  │
├───────────┬───────────┤├───────────┬───────────┤
│ U128      │ I128      ││ U128      │ U128      │
├─────┬─────┼─────┬─────┤├─────┬─────┼─────┬─────┤
│ U64 │ U64 │ U64 │ I64 ││ U64 │ U64 │ U64 │ U64 │
└─────┴─────┴─────┴─────┘└─────┴─────┴─────┴─────┘

The DoubleInt<Base> model lets you create software-defined systems integers larger than the ones supported by your hardware. Like all systems integers, it is stored without indirection. This makes it superior to arbitrary precision models in situations where the doubled precision is enough. It also has a niche on the stack when heap allocations are prohibitive.

Given that systems integers need some 2x width operations, it is both easy and tempting to accept and return DoubleInt<Base> instances. Swift's generic type system even allows it! The problem, however, is that it makes the model recursive and reliant on unspecialized runtime features. While runtime generics are awesome and overpowered in some cases, they're not appropriate for primitives. This is why the core module features Doublet<Base> instead. Here's a brief illustration of how that stops the recusion:

DoubleInt<Base> -> Doublet  <DoubleInt<Base>> // Doublet cannot upsize itself
DoubleInt<Base> -> DoubleInt<DoubleInt<Base>> -> ............................

Author: Slaps roof of car. This baby can fit infinity!

 InfiniInt<IX>            InfiniInt<UX>
┌───────────────────────┐┌───────────────────────┐
│ IXL                   ││ UXL                   │
├─────────────────┬─────┤├─────────────────┬─────┤
│ UX............. │ Bit ││ UX............. │ Bit │
└─────────────────┴─────┘└─────────────────┴─────┘

It may or may not sound intuitive at first, but this infinite binary integer has a fixed size too. It is important to remember this when you consider the recovery modes of its arithmetic operations. It overflows and underflows just like the systems integers you know and love.

The main difference between a systems integer and an infinite integer is that the appendix bit is the only addressable infinity bit, which means you cannot cut infinity in half. Likewise, the log2(max+1) size must be promoted. To work with infinite numerical values, you must track where your values come from. Keep in mind that recovery from failure is the main purpose of infinity.

![NOTE] The introduction of infinity is what permits ~(~(x)) == x for all x.

Addition and subtraction at and around infinity just works.

UXL.min.decremented() // value: max, error: true
UXL.max.incremented() // value: min, error: true

Multiplication is also unchanged. All of the complicated stuff forms at one bit past the appendix bit. Imagine a really big integer and a product of twice that size with truncating behavior. It just works.

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

So, this is where things get tricky. Wait, no, it still just works in most cases. Since you know finite-by-finite division, I'm sure you intuit that finite-by-infinite division is trivial and that infinite-by-infinite division is at most one subtraction. The only weird case is infinite-by-finite division because the proper computation requires infinite memory. So, in this case, we just let the algorithm run and mark it as an error. This yields results similar to signed division.

dividend == divisor &* quotient &+ remainder // for all binary integers
           Ultimathnum.BinaryInteger                  Swift.BinaryInteger
           ┌───────────┬───────────┐            ┌───────────┬───────────┐
           │  Systems  │ Arbitrary |            │  Systems  │ Arbitrary |
┌──────────┼───────────┤───────────┤ ┌──────────┼───────────┤───────────┤
│   Signed │     X     │     X     │ │   Signed │     X     │     X     │
├──────────┼───────────┤───────────┤ ├──────────┼───────────┤───────────┤
│ Unsigned │     X     │     X     │ │ Unsigned │     X     │           │
└──────────┴───────────┴───────────┘ └──────────┴───────────┴───────────┘

StdlibInt is super simple. It wraps InfiniInt<IX> and conforms to Swift.BinaryInteger by forwarding all relevant function calls. The latter is approximately a trapping Ultimathnum.FiniteInteger plus/minus some miscellaneous stuff. StdlibInt is also its own magnitude type since Swift.BinaryInteger does not support the notion of infinity.

Question: How do you test silly numbers?
Answer: With silly number generators, of course!

try! Fibonacci<UXL>(0) // (index: 0, element: 0, next: 1)
try! Fibonacci<UXL>(1) // (index: 1, element: 1, next: 1)
try! Fibonacci<UXL>(2) // (index: 2, element: 1, next: 2)
try! Fibonacci<UXL>(3) // (index: 3, element: 2, next: 3)
try! Fibonacci<UXL>(4) // (index: 4, element: 3, next: 5)
try! Fibonacci<UXL>(5) // (index: 5, element: 5, next: 8)

It uses a fast double-and-add algorithm:

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

But you can also step through it manually:

mutating func double()       throws // index * 2
mutating func increment()    throws // index + 1
mutating func decrement()    throws // index - 1
mutating func increment(by:) throws // index + x.index
mutating func decrement(by:) throws // index - x.index

You may have noticed that you can pick any two adjacent elements and express the sequence in terms of those elements. This observation allows us to climb up and down the index ladder. The idea is super simple:

f(x + 1 + 0) == f(x) * 0000 + f(x + 1) * 00000001
f(x + 1 + 1) == f(x) * 0001 + f(x + 1) * 00000001
f(x + 1 + 2) == f(x) * 0001 + f(x + 1) * 00000002
f(x + 1 + 3) == f(x) * 0002 + f(x + 1) * 00000003
f(x + 1 + 4) == f(x) * 0003 + f(x + 1) * 00000005
f(x + 1 + 5) == f(x) * 0005 + f(x + 1) * 00000008
-------------------------------------------------
f(x + 1 + y) == f(x) * f(y) + f(x + 1) * f(y + 1)

Going the other direction is a bit more complicated, but not much:

f(x - 0) == + f(x) * 00000001 - f(x + 1) * 0000
f(x - 1) == - f(x) * 00000001 + f(x + 1) * 0001
f(x - 2) == + f(x) * 00000002 - f(x + 1) * 0001
f(x - 3) == - f(x) * 00000003 + f(x + 1) * 0002
f(x - 4) == + f(x) * 00000005 - f(x + 1) * 0003
f(x - 5) == - f(x) * 00000008 + f(x + 1) * 0005
-----------------------------------------------
f(x - y) == ± f(x) * f(y + 1) ± f(x + 1) * f(y)

We have some cute algorithms and a generic sequence. Let's combine them and unit-test our models! We can rearrange the sequence addition formula in such a way that we call all basic arithmetic operations. Note that we don't need to know the inputs and outputs ahead of time because it's an equation. Neat!

f(x) * f(y) == (f(x+y+1) / f(x+1) - f(y+1)) * f(x+1) + f(x+y+1) % f(x+1)

Major version zero (0.y.z) is for initial development.
Anything MAY change at any time.
The public API SHOULD NOT be considered stable.

Add this package to your list of package dependencies.

.package(url: "https://github.com/oscbyspro/Ultimathnum.git", exact: "x.y.z"),

Choose target dependencies from this list of 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: "StdlibIntKit", package: "Ultimathnum"),

ultimathnum's People

Contributors

oscbyspro avatar

Stargazers

Melvin Gundlach avatar  avatar Regina Denice Iverson avatar Hooman Mehr avatar

Watchers

Lucian avatar  avatar  avatar  avatar

Forkers

sajjon

ultimathnum's Issues

GCD and XGCD algorithms

The (extended) Euclidean algorithm is almost always useful but I might limit it to unsigned inputs and mixed outputs:

public let divisor:        T.Magnitude // unsigned
public let lhsCoefficient: T.Signitude //   signed
public let rhsCoefficient: T.Signitude //   signed

This is because the unsigned algorithm is a bit simpler and more elegant, and I haven't yet found a real use for extending signed inputs. I also don't think that the derived quotients are all that useful but I might keep them around, dunno. Edit: Oh, wait, I remember testing it and thinking the quotients were annoying because they overflow for some inputs. Hm. That might have been for signed inputs. I suppose I'll have to test it again.

DataInt.Body/isEmpty

Compare DataInt/isZero and DataInt/isNormal. It reads well and saves 5 characters compared to Body/count.isZero.

DataInt/isCompact

Asking whether the last bit in the body extends the bit pattern is another useful query. It is not complicated, but it is quite wordy so it might deserves a name. It's currently simpler to go through DataInt/entropy(), which is obviously a roundabout way of doing it.

InfiniInt<T> small-storage optimization

InfiniInt<T> stores an array at the moment. I've occasionally thought about storing an enum that contains an array or one element. After more deliberation, I believe a tagged pointer (#43) is a better optimization. The latter ensures that the inline model fits in a register and there are still ptr - 1 bits to work with.

Appendix bit as pointer tag?

I always feel silly when I let a single bit occupy a whole register. I could, perhaps, use an unused bit in DataInt's base address instead. Alternatively, I could borrow the last bit in DataInt's count property. So, there are at least two better options.

Make DoubleInt great again!

So I wanted to see the difference (#31) makes on DoubleInt, and the difference is great. Then I compared it against my predecessor project and that one still performs divisions faster. Sigh. I haven't yet gotten around to optimizing the new abstraction, although I suppose (#31) is a step in the right direction.


Edit: Ultimathnum.U256 is faster if I set its integer literal to Int or Element.


final class Benchmarks: XCTestCase {

    /// ###### 2024-07-05 (MacBook Pro, 13-inch, M1, 2020):
    ///
    /// - `0.36 seconds`
    ///
    func testNumberick256() {
        let fib369: UInt256 = "58472848379039952684853851736901133239741266891456844557261755914039063645794"
        let fib370: UInt256 = "94611056096305838013295371573764256526437182762229865607320618320601813254535"
                
        for _ in 0 ..< 369 * 100 {
            var lhs = blackHoleIdentity(fib369)
            var rhs = blackHoleIdentity(fib370)
            var counter = 0
            
            dividing: while !rhs.isZero {
                counter += 1
                (lhs, rhs) = (rhs, lhs.remainderReportingOverflow(dividingBy: rhs).partialValue)
            }
            
            precondition(lhs == 1)
            precondition(rhs == 0)
            precondition(counter == 369)
        }
    }
    
    /// ###### 2024-07-05 (MacBook Pro, 13-inch, M1, 2020):
    ///
    /// - `1.67 seconds`
    /// - `0.88 seconds` with pointer-bit shifts
    /// - `0.31 seconds` with pointer-bit shifts and literals
    ///
    func testUltimathnum256() {
        let fib369 = U256("58472848379039952684853851736901133239741266891456844557261755914039063645794")!
        let fib370 = U256("94611056096305838013295371573764256526437182762229865607320618320601813254535")!
        
        for _ in 0 ..< 369 * 100 {
            var lhs = blackHoleIdentity(fib369)
            var rhs = blackHoleIdentity(fib370)
            var counter = 0
            
            dividing: while !rhs.isZero {
                counter += 1
                (lhs, rhs) = (rhs, lhs.remainder(Divisor(unchecked: rhs)))
            }
            
            precondition(lhs == 1)
            precondition(rhs == 0)
            precondition(counter == 369)
        }
    }
}

Pointer-bit binary integer bit counts

All binary integer bit counts fit in a signed machine per protocol. This also means that Shift<T> could be made to fit in a signed machine word. Larger types are wasteful and may require more complicated validation. Arbitrary shifts are also naturally based on major/minor machine word distances. As such, it might make sense to rework how sizes and shifts are represented in memory. A size model would still need the ability to represent infinity, but it is simple enough to reinterpret negative values as infinite. Perhaps something like the following:

@frozen public struct Count: BitCastable, Comparable {
    
    @usableFromInline let base: IX
    
    @inlinable public init(raw source: UX.BitPattern) {
        self.base = IX(raw: source)
    }
    
    @inlinable public func load(as type: IX.BitPattern.Type) -> IX.BitPattern {
        self.base.load(as: IX.BitPattern.self)
    }
    
    @inlinable public var isInfinite: Bool {
        Bool(self.base.appendix)
    }

    @inlinable public func natural() -> Optional<IX> {
        if  self.isInfinite {
            return nil
            
        }   else {
            return self.base
        }
    }
    
    @inlinable public static func ==(lhs: Self, rhs: Self) -> Bool {
        lhs.base == rhs.base
    }
    
    @inlinable public static func < (lhs: Self, rhs: Self) -> Bool {
        UX(raw: lhs.base) < UX(raw: rhs.base)
    }
}

Comparisons are important, but I'm not sure I want to include arithmetic. I suspect it's easier to just bit cast relative values.

Order and Signedness enums (no protocols)

The Endianness or Signedness types are just constant Bool(s) at this point. The methods that used them are either inlineable, too complicated for a dynamic comparison to matter, or designated slow paths. There is some generic stuff you can do with BinaryInteger.Mode, but they are no longer relevant. I still prefer named cases over Bool(s) so I might turn them into enums, we will see.

Flush shifts greater than IX.max

This makes it easier to implement generic shift operations (#32). A) Downshifts already always do this due do binary integer body in-memory limits. B) The alternative upshifting behavior is either trapping or failing in some other way. I don't think upshifts must pretend to be multiplication operations when the equivalent product would run out of address space. C) Nobody use these paths so its all esoteric fluff.

Arithmetic in Karatsuba algorithm should be discard() not unchecked()

I'm writing more multiplication tests for the various data integer algorithms, and it became apparent that at least some of the arithmetic inside the Karatsuba algorithm should be allowed to overflow. In particular, the unchecked() debug assertion was triggered by [~0] x [~0], which usually does not take the Karatsuba path given the 16-element threshold. Tangentially, I kind of thought there would be a lower limit to the algorithm, but it just works if you take away the debug overflow assertions.

Try compact InfiniInt<T> storage model

As noted by the (#3) and (#6) edge cases, I suspect that it might be simpler to store InfiniInt<T>'s appendix bit as the last bit in its body rather than as a separate extension bit. Maybe it's better, maybe it's worse. In either case, it's something I want to try.

DataInteger[Body] protocols

I could write some ergonomic improvements with generic X from [Mutable]X getters:

public protocol DataIntegerBody {
    
    associatedtype Element: SystemsInteger & UnsignedInteger
    
    @inlinable func immutable() -> DataInt<Element>.Body
}

extension DataIntegerBody {

    @inlinable public func compared(to other: some DataIntegerBody) -> Signum {
        DataInt.compare(
            lhs: DataInt(self .immutable()), mode: .unsigned,
            rhs: DataInt(other.immutable()), mode: .unsigned
        )
    }
}

Rework binary integer shift requirements

  1. Move Shift<T> trusted inputs to named methods like upshift(_:) and downshift(_:).
  2. Derive the smart-shift operations and remove the smart-shift requirements (<< and >>).

Associated T or Fallible<T> return types

Here's a shower thought about something I tried at some point but did not commit to. You can replace the Fallible<T> return type with T for specific operations of specific types by sprinkling a bunch of associated types on top of the BinaryInteger protocol. One benefit is that you'd never have to unwrap signed arbitrary arithmetic, or unsigned systems division, for example. It would also turn some error propagation cases into explicit no-ops for some types. Imagine something like this:

protocol Recoverable {
    
    associatedtype Payload
    
    var error: Bool { get }
    
    var value: Payload { get }
}

protocol BinaryInteger: Recoverable where Payload == Self {
    
    associatedtype Addition: Recoverable where Addition.Payload == Self
    
    func plus(_ other: Self) -> Addition
}

New BitCountable protocol

The new pointer-bit bit-counting approach (#31) (#33) unifies the bit-counting methods of binary integers and data integers. I have, therefore, written a common BitCountable protocol. I'm using it for testing purposes at the moment, but it might come in handy elsewhere too. It should allow me deduplicate the inverses, at least.

Protocol: ArbitraryInteger

This protocol makes it easier to reason about the following section of the grid:

Systems Arbitrary
Signed X
Unsigned X

BinaryInteger.init(clamping:mode:) - DataInt

I'm working on (#23) and it seems like I'd want a DataInt version of init(clamping:) to implement StdlibInt.init(clamping: Swift.BinaryInteger). I'm not sure it's necessary, but I would at least have to repeat the logic otherwise and that kind of defeats the point of it. Edit: Never mind, I don't think I need it.

Open to work

🐺 Your favorite underdog is open to work.
🏋️ I will work for food, but not GitHub stars.

Use (low:high:) and remove (high:low:)

I want a canonical tuple format so that calling Doublet/components() instead of Doublet/ascending() and Doublet/descending() makes sense. All other objects destructure their contents by calling components(), and I want this to be consistent across the board. A recent change also made the 2nd and 3rd method easy to conflate with BinaryInteger/ascending(Bit) and BinaryInteger/descending(Bit). This project always view binary integers in ascending order, so (low:high:) makes the most sense to keep.

Negative and infinite small-storage multiplication edge case

I wrote some more multiplication tests and found this bug. I'll fix it tomorrow. It's 22:40.

Test().same((~InfiniInt<I8>(U8.max)).times(256), Fallible(~65535, error: false)) // value: 0 ⚠️
Test().same((~InfiniInt<U8>(U8.max)).times(256), Fallible(~65535, error: true )) // value: 0 ⚠️
Test().same((~InfiniInt<IX>(U8.max)).times(256), Fallible(~65535, error: false))
Test().same((~InfiniInt<UX>(U8.max)).times(256), Fallible(~65535, error: true ))

It's in the small storage fast path, where the body in (body: 0, appendix: 1) zeros the product.

Generic shift operations

I'll add generic shift operations with appropriate paths to the Shift<T> base implementation:

static func  <<=(instance: inout     Self, distance: some BinaryInteger)
static func  << (instance: consuming Self, distance: some BinaryInteger) -> Self 
static func  >>=(instance: inout     Self, distance: some BinaryInteger)
static func  >> (instance: consuming Self, distance: some BinaryInteger) -> Self

static func &<<=(instance: inout     Self, distance: some BinaryInteger)
static func &<< (instance: consuming Self, distance: some BinaryInteger) -> Self 
static func &>>=(instance: inout     Self, distance: some BinaryInteger)
static func &>> (instance: consuming Self, distance: some BinaryInteger) -> Self

Req. big integer elements be normalized

Idea: generic algorithms would never need to perform conservative normalization checks or transformations in big integer branches if big integers are required to offer normalized elements. InfiniInt<T> already does this because it would be silly to do otherwise, but generic algorithms cannot yet assume it.

Bad negative versus infinite comparison

I wrote some more comparison tests and found this bad negative-versus-infinite behavior:

Test().comparison(IXL(-1), UXL(~0), -1 as Signum) // OK
Test().comparison(IXL(-1), UXL(~1), -1 as Signum) // :(
Test().comparison(IXL(-1), UXL(~2), -1 as Signum) // :(
Test().comparison(IXL(-1), UXL(~3), -1 as Signum) // :(

Test().comparison(IXL(-2), UXL(~0), -1 as Signum) // OK
Test().comparison(IXL(-2), UXL(~1), -1 as Signum) // OK
Test().comparison(IXL(-2), UXL(~2), -1 as Signum) // :(
Test().comparison(IXL(-2), UXL(~3), -1 as Signum) // :(

Negative and infinite large-storage multiplication edge case

I found a multiplication bug similar to the one in (#3). It's for a similar reason, but when the storage is large this time. If both inputs have bodies filled with 0s and an appendix of 1, then the product should be the combined number of 0s followed by a single 1. This is the only case that does not fit in the combined body allocation.

Test().multiplication(InfiniInt<I8>(repeating: 1) << 16, InfiniInt<I8>(repeating: 1) << 16, Fallible(1 << 32, error: false)) // value: 0 ⚠️
Test().multiplication(InfiniInt<U8>(repeating: 1) << 16, InfiniInt<U8>(repeating: 1) << 16, Fallible(1 << 32, error: true )) // value: 0 ⚠️

I'll be busy for a few days but it should be easy to fix.

Also, I bet my life would be easier if I stored the appendix bit as the last bit in the body instead.

Bad signed vs unsigned systems integer comparisons

Generics are great, but they naturally add lots of edge cases. Sigh.

Test().comparison(-1 as I32, 0 as U8,  -1 as Signum) // OK
Test().comparison(-1 as I32, 0 as U16, -1 as Signum) // OK
Test().comparison(-1 as I32, 0 as U32, -1 as Signum) // OK
Test().comparison(-1 as I32, 0 as U64, -1 as Signum) // :(

Randomness

I want to support randomness at some point, but I don't want to encode randomness into the protocol hierarchy. I suspect I'll just need a generic way of requesting uninitialized memory up to a limit or the size of the given type.

Some big vs infinite ideas

It turns out that (#31) offers some big performance improvements, but the proposed size model has some complexity and ergonomic problems. The main problem, really, is that an infinite pointer-bit size cannot be modeled as an unsigned binary integer because it has too many edges (i.e. min-min, min-max, max-min, max-max). I am, therefore, pondering an alternative. I think it would be possible to preserve much of the current logic by setting the maximum binary integer and data integer size to UX.max + 1. In this case, a binary integer provides a size mask that the size is derived from:

protocol BinaryInteger {
    static var mask: UX { get }
}

extension SystemsInteger {
    static var size: UX { 
        Self.mask.incremented().unchecked()
    }
}

extension BinaryInteger {
    init?(size: (some BinaryInteger).Type) { ... } 
}

In this case, a size just an unsigned systems integer. Any current if !T.size.isInfinite { ... } would be replaced by if let size = UX(size: T.self) { ... }. Alternatively, one may add a similar but more aptly named getter.

Non-allocating binary integer validation

I noticed that I ought to have a non-allocating binary integer validation algorithm while working on (#21). The failing bit pattern of BinaryInteger.exactly(_:) is immediately discarded in init(clamping:), so it would be better to use something like BinaryInteger.validate(_:).

Edit: I suppose I don't actually have to use BinaryInteger.exactly(_:) in init(clamping:).

Simpler bit counting methods

Having used the count(Bit.Selection) methods for a while now, I think I would prefer something simpler.

| current                  | perhaps this is a better idea |
|--------------------------|-------------------------------|
| size()                   |                        size() |
| count(.entropy)          |                     entropy() |
|--------------------------|-------------------------------|
| count(            1 )    |                      count(1) |
| count( .ascending(1))    |                  ascending(1) |
| count(.descending(1))    |                 descending(1) |
|--------------------------|-------------------------------|
| count( .nonascending(1)) |               nonascending(1) | or size() -  ascending(1)
| count(.nondescending(1)) |              nondescending(1) | or size() - descending(1)
|--------------------------|-------------------------------|
| count(   .appendix)      |          descending(appendix) |
| count(.nonappendix)      |       nondescending(appendix) | or size() - descending(appendix)

DataInt/isNormal

I might as well add a normalization check since I'm on Bool train (#12) already. It is common to normalize data integer types, and it's about as common to assert that data integers are normalized. The former is a core data integer method, so the latter should probably be one too.

extension [Mutable]DataInt[.Body] {

    /// Indicates whether the `body` is free of `appendix` extensions.
    @inlinable public var isNormal: Bool { ... }
}

FiniteInteger(s)

I'm trying to figure out the best way to recover from infinite arguments passed to the greatest common divisor BinaryInteger/euclidean(_:) methods. I then realized that I was missing a finite integer protocol representing the union of systems integer types and arbitrary signed integer types. I'm not sure that is the most appropriate solution to this problem, but it may solve other problems.

Rework lsb/msb stuff

Before

  1. All binary integers can return Self/leastSignificantBit.
  2. All systems integers can return Self.lsb.
  3. All systems integers can return Self.msb.

After

  1. All binary integers can return Self/lsb.
  2. All binary integers can return Self/msb.
  3. All binary integers can return Self.lsb.
  4. All systems integers can return Self.msb.

Finite<T> and Natural<T> trusted inputs

I have settled on hoisting the GCD and XGCD preconditions (#9) via the Trusted Input ™️ pattern to enable recovery. As an example, the XGCD algorithms should extend all unsigned integers and they only fail when the arguments are infinite. I cannot limit these algorithms to a finite integer types because that makes them unavailable to InfiniInt<T>. So, instead, I'll just require that the inputs are of the Finite<T> type:

extension BinaryInteger {
    @inlinable public static func euclidean(
        _ lhs: consuming Finite<Self>,
        _ rhs: consuming Finite<Self>
    ) -> Magnitude
    
    @inlinable public static func euclidean1(
	_ lhs: consuming Finite<Self>,
	_ rhs: consuming Finite<Self>
    ) -> XGCD1 where Self: UnsignedInteger
    
    @inlinable public static func euclidean2(
	_ lhs: consuming Finite<Self>,
	_ rhs: consuming Finite<Self>
    ) -> XGCD2 where Self: UnsignedInteger
}

Some convenience methods can then be written as finite integer extensions:

extension FiniteInteger {
    @inlinable public consuming func euclidean(_ other: consuming Self) -> Magnitude {
        Self.euclidean (Finite(unchecked: self), Finite(unchecked: other))
    }

    @inlinable public consuming func euclidean1(_ other: consuming Self) -> XGCD1 where Self: UnsignedInteger {
        Self.euclidean1(Finite(unchecked: self), Finite(unchecked: other))
    }

    @inlinable public consuming func euclidean2(_ other: consuming Self) -> XGCD2 where Self: UnsignedInteger {
        Self.euclidean2(Finite(unchecked: self), Finite(unchecked: other))
    }
}

The current use case does not need Natural<T> but I imagine that it is of similar importance.

No masking shift requirements

It appears that the compiler is smart enough to remove redundant bit masking operations, so there's no need to require masking shifts. The following examples generate the same machine code. Whatever effect this has on DoubleInt will vanish when its shifts are made pointer-bit (#31):

func shiftA(x: Int, shift: Int) -> Int {
    x &<< (shift)
}

func shiftB(x: Int, shift: Int) -> Int {
    x &<< (shift & (Int.bitWidth - 1))
}

StdlibIntKit

I want to test some ordinary Swift.BinaryInteger stuff. A StdlibInt<T> type would enable it. Swift.BinaryInteger is approximately a trapping Ultimathnum.FiniteInteger, so it should be simple to implement. The only thing I haven't covered yet is binary floating-point conversions, which I'll get to eventually.

BinaryInteger/isZero and DataInt/isZero

I've changed my mind about BinaryInteger/isZero. It's common enough to deserves a seat among isInfinite and isNegative. Even integers with unknown signedness can implement it, so DataInt/isZero makes sense too. A binary integer extension can also ensure that big integers perform the operation in place, which is preferred for performance reasons.

extension BinaryInteger {

    /// Indicates whether this value is equal to zero.
    ///
    /// - Note: Big integers evaluate it in place, cf. `compared(to: 0)`.
    ///
    @inlinable public var isZero: Bool {
        if !Self.size.isInfinite {
            return self == Self.zero
            
        }   else {
            return self.withUnsafeBinaryIntegerElements {
                $0.isZero
            }
        }
    }
}

No arbitrary binary integer token conversions

The BinaryInteger protocol requires 3 pointer-bit conversions but it seems possible to at least move them to SystemsInteger. Most arbitrary binary integer calls are contrived for testing purposes and the InfiniInt<T> methods can be derived if anybody wants them. Ideally, I'd work solely with Element types but this is a start.

Optional DataInt.Body elements

I've changed my mind about DataInt.Body/subscript(exactly:), etc. In particular, optional DataInt.Body/first and DataInt.Body/last elements help fight verbosity. Perhaps I can do without (#28), if the expression is ergonomic enough.

Fibonacci<T> indices less than zero

I might want to extend the fib sequence to negative indices as well so that I can use it to test operations with an appendix of one instead of it always being zero in the finite and nonnegative range.

..., −8, 5, −3, 2, −1, 1, 0, 1, 1, 2, 3, 5, 8, ...

Super generic init(clamping:)

EdgyInteger is an unusual constraint so it might be nice to add init(clamping:):

Self Source Result Status
EdgyInteger BinaryInteger Self X
BinaryInteger FiniteInteger Self
BinaryInteger BinaryInteger Optional<Self>

I have only thought about if briefly, but the above cases should be derivable.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.