I can't figure out what's causing the infinite recursion

Testing out a 4-bit unsigned integer type:

public struct UInt4: FixedWidthInteger, UnsignedInteger {
    public init(integerLiteral value: UInt) {
        self.init(exactly: value)! //self.init(value)
    }

    public static func * (lhs: UInt4, rhs: UInt4) -> UInt4 {
        let result = lhs.multipliedReportingOverflow(by: rhs)
        precondition(!result.overflow)
        return result.partialValue
    }

    public static func *= (lhs: inout UInt4, rhs: UInt4) { lhs = lhs * rhs }

    public static func + (lhs: UInt4, rhs: UInt4) -> UInt4 {
        let result = lhs.addingReportingOverflow(rhs)
        precondition(!result.overflow)
        return result.partialValue
    }

    public static func += (lhs: inout UInt4, rhs: UInt4) { lhs = lhs + rhs }

    public static func - (lhs: UInt4, rhs: UInt4) -> UInt4 {
        let result = lhs.subtractingReportingOverflow(rhs)
        precondition(!result.overflow)
        return result.partialValue
    }

    public static func -= (lhs: inout UInt4, rhs: UInt4) { lhs = lhs - rhs }

    public var hashValue: Int { return value.hashValue }

    public init<T: BinaryFloatingPoint>(_ source: T) {
        fatalError("\(#function) not implemented.")
    }

    public var words: UInt8.Words { return value.words }

    public var trailingZeroBitCount: Int { return Swift.max(value.trailingZeroBitCount, bitWidth) }

    public func quotientAndRemainder(dividingBy rhs: UInt4) -> (quotient: UInt4, remainder: UInt4) {
        return rhs.dividingFullWidth((high: 0, low: self))
    }

    public func signum() -> UInt4 { return UInt4(truncatingIfNeeded: value.signum()) }

    public static func &= (lhs: inout UInt4, rhs: UInt4) { lhs.value &= rhs.value }

    public static func / (lhs: UInt4, rhs: UInt4) -> UInt4 {
        let result = lhs.dividedReportingOverflow(by: rhs)
        precondition(!result.overflow)
        return result.partialValue
    }

    public static func /= (lhs: inout UInt4, rhs: UInt4) { lhs = lhs / rhs }

    public static func % (lhs: UInt4, rhs: UInt4) -> UInt4 {
        let result = lhs.remainderReportingOverflow(dividingBy: rhs)
        precondition(!result.overflow)
        return result.partialValue
    }

    public static func %= (lhs: inout UInt4, rhs: UInt4) { lhs = lhs % rhs }

    public static func ^= (lhs: inout UInt4, rhs: UInt4) { lhs.value ^= rhs.value }

    public static func |= (lhs: inout UInt4, rhs: UInt4) { lhs.value |= rhs.value }

    public init(_truncatingBits value: UInt) {
        self.value = UInt8(truncatingIfNeeded: value) & 0x0F
    }

    public var nonzeroBitCount: Int { return value.nonzeroBitCount }

    public var leadingZeroBitCount: Int { return value.leadingZeroBitCount - 4 }

    public var byteSwapped: UInt4 { return self }

    public func addingReportingOverflow(_ rhs: UInt4) -> (partialValue: UInt4, overflow: Bool) {
        let result = value.addingReportingOverflow(rhs.value)
        assert(!result.overflow)
        return (UInt4(truncatingIfNeeded: result.partialValue), result.partialValue >> 4 != 0)
    }

    public func subtractingReportingOverflow(_ rhs: UInt4) -> (partialValue: UInt4, overflow: Bool) {
        let result = value.subtractingReportingOverflow(rhs.value)
        return (UInt4(truncatingIfNeeded: result.partialValue), result.overflow)
    }

    public func multipliedReportingOverflow(by rhs: UInt4) -> (partialValue: UInt4, overflow: Bool) {
        let result = value.multipliedReportingOverflow(by: rhs.value)
        assert(!result.overflow)
        return (UInt4(truncatingIfNeeded: result.partialValue), result.partialValue >> 4 != 0)
    }

    public func dividedReportingOverflow(by rhs: UInt4) -> (partialValue: UInt4, overflow: Bool) {
        let result = value.dividedReportingOverflow(by: rhs.value)
        return (UInt4(truncatingIfNeeded: result.partialValue), result.overflow)
    }

    public func remainderReportingOverflow(dividingBy rhs: UInt4) -> (partialValue: UInt4, overflow: Bool) {
        let result = value.remainderReportingOverflow(dividingBy: rhs.value)
        return (UInt4(truncatingIfNeeded: result.partialValue), result.overflow)
    }

    public func multipliedFullWidth(by other: UInt4) -> (high: UInt4, low: UInt4) {
        let result = value.multipliedFullWidth(by: other.value)
        assert(result.high == 0)
        return (UInt4(truncatingIfNeeded: result.low >> 4), UInt4(truncatingIfNeeded: result.low))
    }

    public func dividingFullWidth(_ dividend: (high: UInt4, low: UInt4)) -> (quotient: UInt4, remainder: UInt4) {
        let result = value.dividingFullWidth((high: 0, low: dividend.high.value << 4 | dividend.low.value))
        precondition(result.quotient & 0xF0 == 0)
        return (UInt4(truncatingIfNeeded: result.quotient), UInt4(truncatingIfNeeded: result.remainder))
    }

    public static var bitWidth: Int { return 4 }

    public static func &<<= (lhs: inout UInt4, rhs: UInt4) { lhs.value <<= (rhs.value & 0x03) ; lhs.value &= 0x0F }

    public static func &>>= (lhs: inout UInt4, rhs: UInt4) { lhs.value >>= (rhs.value & 0x03) }

    /// The raw value.
    var value: UInt8

}

With this code, first in a playground, now in a CLI project so I can set breakpoints:

print(UInt8.bitWidth, UInt4.bitWidth)
print(UInt4(_truncatingBits: 22))
print(UInt4(6))

The first two lines work. (That second line prints “6” as it should.) But the third line hangs. In the playground version, it was showing that “bitWidth” was being called thousands of times before giving up with bad-access. Now that I can use breakpoints, the third line goes:

  1. Calls the ExpressibleByIntegerLiteral initializer
  2. Which calls whichever to the two BinaryInteger initializers I didn’t comment out.
  3. Calls some initializer within UnsignedInteger (whose code is in assembly).
  4. Which calls my integer-literal initializer again!

So I got infinite recursion.

Help.

There are several issues with your understanding of what’s going on, but it’s too much to unpack here.

For the purposes of explaining the infinite recursion: both of the initializers you contemplate using inside your integer literal initializer are default implementations of generic conversion initializers from UInt that you didn’t write, and both require the ability to use a literal zero.

Default implementations of protocol requirements are only possible because they can build on other protocol requirements that don’t have default implementations. As a general rule, you’re going to have a bad time if you try to implement a protocol requirement by calling a default implementation of another one required by the same protocol. Never attempt to do this unless you control the code on both sides (which you don’t here): even if it works today, it may not work tomorrow.