All types simple or complex could implicitly conform to some built-in "ValueEnumerable" protocol:
extension UInt8: ValueEnumerable { // implicit
static var possibleValueCount: Int { 256 }
}
enum ABC { case a, b c }
extension ABC: ValueEnumerable { // implicit
static var possibleValueCount: Int { 3 }
}
enum TwoTier { case abc(ABC), d }
extension TwoTier: ValueEnumerable { // implicit
static var possibleValueCount: Int { ABC.possibleValueCount + 1 }
}
typealias Pair = (ABC, UInt8)
// ditto for `struct Pair { var abc: ABC, uint8: UInt8 }`
extension Pair: ValueEnumerable {
static var possibleValueCount: Int { ABC.possibleValueCount * UInt8.possibleValueCount }
}
extension Optional: ValueEnumerable { // implicit, don't use for references
static var possibleValueCount: Int { 1 + Wrapped.possibleValueCount }
}
Similar to RawRepresentable ValueEnumerable might have an initialiser to go from "index" to the value:
protocol ValueEnumerable {
static var possibleValueCount: Int { get }
init(valueIndex: Int) -> Self
}
From there any interesting party (like Array) could determine the appropriate number of bits to store
Int(ceil(log2(TheType.possibleValueCount)))
References could be checked with special logic around the tagged pointers.
Or similar to what you said:
protocol BitRepresentable {
var bitCount: Int? { get }
init(_ bits: UnsafePointer<UInt8>, bitCount: Int)
func toBits(_ bits: UnsafePointer<UInt8>, bitCount: Int)
}