This code isn't running into problems with what you think it is. When you write let x = UInt4(6), it's calling words 10 times before failing with EXC_BAD_ACCESS, which is not what happens with integer overflow.
The entirety of the default implementation for that initializer is:
public init<T : BinaryInteger>(_ source: T) {
if T.isSigned {
_precondition(source >= (0 as T), "Negative value is not representable")
}
if source.bitWidth >= Self.bitWidth {
_precondition(source <= Self.max, "Not enough bits to represent a signed value")
}
self.init(truncatingIfNeeded: source)
}
You can try to input each expression by hand into your playground to see what stops working. It's not self.init(truncatingIfNeeded: source), which works just fine. Instead, you get the same runtime error when you write:
6 <= UInt4.max
So as you'll see, the problem is that you never implemented conformance to Comparable. This is essentially the same issue we discussed in the other thread.
The compiler has trouble with compile-time errors when a more general method (in this case, heterogeneous comparison) is implemented in terms of a more specific requirement (in this case, homogeneous comparison) that the user hasn't implemented. There are diagnostics improvements to be made on that front (as I discussed earlier in that thread). However, I've also mentioned a few times how this is avoidable on your part:
Fundamentally, you mustn't attempt implementation of a complex protocol hierarchy in one go, as you've done here. Conform your type in separate extensions to each protocol, one at at time, starting from the top of the hierarchy (Equatable). Make sure that each conformance stands on its own without using default implementations from more refined protocols. Had you done so, the compiler would have warned you about missing Comparable methods before you attempted to conform to BinaryInteger. It would also have spared you from several other issues; I'll touch on a few below.
Because there's no separation of protocol conformances and pervasive use of more refined protocol implementations to implement less refined protocol requirements, you've got a web of redundant calls. Let's take the first implementation:
init(integerLiteral value: UInt8) {
self.init(_truncatingBits: numericCast(value))
}
This method takes a value of type UInt8, then calls numericCast(_:) to convert to type UInt. Then, it calls UInt4.init(_truncatingBits), which you wrote as follows:
init(_truncatingBits value: UInt) {
self.value = UInt8(truncatingIfNeeded: value) & 0x0F
}
So, the method actually calls UInt8.init(truncatingIfNeeded:) to convert that value of type UInt back to a value of type UInt8. UInt8.init(truncatingIfNeeded:) is a standard library implementation that in turn calls UInt8.init(_truncatingBits:). This is all to convert a value of type UInt8 to UInt and back to UInt8!
Let's look at the next member:
static func * (lhs: UInt4, rhs: UInt4) -> UInt4 {
let result = lhs.multipliedReportingOverflow(by: rhs)
precondition(!result.overflow)
return result.partialValue
}
You're implementing a requirement of Numeric (*) by calling an implementation that's a requirement of the more refined protocol FixedWidthInteger (multipliedReportingOverflow). This is not advisable; you do happen to control both implementations, but even in that scenario, if you happen to call a default implementation in multipliedReportingOverflow without realizing it, you're liable to cause infinite recursion. By contrast, if you call a requirement of Numeric from a requirement of FixedWidthInteger, you can be assured that there won't be infinite recursion if you built up Numeric conformance first.
Let's look at another:
func signum() -> UInt4 { return UInt4(truncatingIfNeeded: value.signum()) }
Here, you're implementing signum, a requirement of BinaryInteger, by calling init(truncatingIfNeeded:) a default implementation of another requirement of BinaryInteger. Again, you're liable to cause an infinite recursion, because that default implementation rightly assumes that it can call signum at will. Even if this implementation works today, it's not guaranteed to work tomorrow. Do not do this unless you're also providing an implementation of init(truncatingIfNeeded:) that you control.
I hope I've illustrated how you can safely go about implementing a type with conformance to a complex hierarchy of protocols. This will spare you the repeated troubles that you're running into.