How is `StaticBigInt` intended to be used?

i’ve got a value type, let’s call it UInt8x20, that i am using as storage for a 160-bit SHA1 git hash. the UInt8x20 stores the hash in big-endian order.

now, i am trying to add an ExpressibleByIntegerLiteral conformance for this git hash type. after shooting myself in the foot several times, i finally got to this working implementation:

extension GitRevision:ExpressibleByIntegerLiteral
{
    @inlinable public
    init(integerLiteral:StaticBigInt)
    {
        precondition(integerLiteral.signum() >= 0,
            "revision literal cannot be negative")
        precondition(integerLiteral.bitWidth <= 161,
            "revision literal overflows UInt8x20")

        var hash:UInt8x20 = .init()
        var byte:Int = hash.endIndex
        var word:Int = 0
        while byte != hash.startIndex
        {
            withUnsafeBytes(of: integerLiteral[word].bigEndian)
            {
                for value:UInt8 in $0.reversed()
                {
                    byte = hash.index(before: byte)
                    hash[byte] = value

                    if  byte == hash.startIndex
                    {
                        break
                    }
                }
            }
            word += 1
        }
        self.init(hash: hash)
    }
}

but this feels like a really weird implementation:

  • i’m iterating byte-by-byte, while StaticBigInt wants to be iterated by word (UInt),

  • i’m not using an integral number of StaticBigInt words (on a 64-bit system, i’m using 2.5 words),

  • i’m initializing the memory from back to front, and

  • i’m checking the byte index bounds twice on exit.

am i holding StaticBigInt wrong?

If you want to initialize front to back, maybe you can do something like:

func bigEndianBytes(of integer: StaticBigInt, count: Int) -> [UInt8] {
    [UInt8](unsafeUninitializedCapacity: count) { bytes, byteIndex in
        precondition(count >= 0)
        precondition(integer.signum() >= 0)
        precondition(integer.bitWidth <= 1 + 8 * count)
        
        let remainder: Int = (count &  (MemoryLayout<UInt>.size &- 1))
        let remainderIndex = (count &>> MemoryLayout<UInt>.size.trailingZeroBitCount)
        
        withUnsafeBytes(of: integer[remainderIndex].bigEndian) {
            byteIndex = bytes.initialize(fromContentsOf: $0.suffix(remainder))
        }
        
        for index in (0 ..< remainderIndex).reversed() {
            withUnsafeBytes(of: integer[index].bigEndian) {
                byteIndex = bytes.suffix(from: byteIndex).initialize(fromContentsOf: $0)
            }
        }
    }
}

print(bigEndianBytes(of: 0x0102030405060708090A0B0C0D0E0F1011121314, count: 20))

The StaticBigInt interaction I've settled on is chunking it with a generic ChunkedInt<Base, Element> collection, after turning StaticBigInt into a RandomAccessCollection<UInt>. I think this approach works well because chunking is useful beyond StaticBigInt, and many binary-integer-like things "just work" when StaticBigInt is a random access collection.