|
| 1 | +/* |
| 2 | + This source file is part of the Swift.org open source project |
| 3 | + |
| 4 | + Copyright (c) 2024-2025 Apple Inc. and the Swift project authors |
| 5 | + Licensed under Apache License v2.0 with Runtime Library Exception |
| 6 | + |
| 7 | + See https://swift.org/LICENSE.txt for license information |
| 8 | + See https://swift.org/CONTRIBUTORS.txt for Swift project authors |
| 9 | +*/ |
| 10 | + |
| 11 | +/// A fixed size bit set, used for storing very small amounts of small integer values. |
| 12 | +/// |
| 13 | +/// This type can only store values that are `0 ..< Storage.bitWidth` which makes it _unsuitable_ as a general purpose set-algebra type. |
| 14 | +/// However, in specialized cases where the caller can guarantee that all values are in bounds, this type can offer a memory and performance improvement. |
| 15 | +package struct _FixedSizeBitSet<Storage: FixedWidthInteger & Sendable>: Sendable { |
| 16 | + package typealias Element = Int |
| 17 | + |
| 18 | + package init() {} |
| 19 | + |
| 20 | + @usableFromInline |
| 21 | + private(set) var storage: Storage = 0 |
| 22 | + |
| 23 | + @inlinable |
| 24 | + init(storage: Storage) { |
| 25 | + self.storage = storage |
| 26 | + } |
| 27 | +} |
| 28 | + |
| 29 | +// MARK: Set Algebra |
| 30 | + |
| 31 | +extension _FixedSizeBitSet: SetAlgebra { |
| 32 | + private static func mask(_ number: Int) -> Storage { |
| 33 | + precondition(number < Storage.bitWidth, "Number \(number) is out of bounds (0..<\(Storage.bitWidth))") |
| 34 | + return 1 &<< number |
| 35 | + } |
| 36 | + |
| 37 | + @inlinable |
| 38 | + @discardableResult |
| 39 | + mutating package func insert(_ member: Int) -> (inserted: Bool, memberAfterInsert: Int) { |
| 40 | + let newStorage = storage | _FixedSizeBitSet.mask(member) |
| 41 | + defer { |
| 42 | + storage = newStorage |
| 43 | + } |
| 44 | + return (newStorage != storage, member) |
| 45 | + } |
| 46 | + |
| 47 | + @inlinable |
| 48 | + @discardableResult |
| 49 | + mutating package func remove(_ member: Int) -> Int? { |
| 50 | + let newStorage = storage & ~_FixedSizeBitSet.mask(member) |
| 51 | + defer { |
| 52 | + storage = newStorage |
| 53 | + } |
| 54 | + return newStorage != storage ? member : nil |
| 55 | + } |
| 56 | + |
| 57 | + @inlinable |
| 58 | + @discardableResult |
| 59 | + mutating package func update(with member: Int) -> Int? { |
| 60 | + let (inserted, _) = insert(member) |
| 61 | + return inserted ? nil : member |
| 62 | + } |
| 63 | + |
| 64 | + @inlinable |
| 65 | + package func contains(_ member: Int) -> Bool { |
| 66 | + storage & _FixedSizeBitSet.mask(member) != 0 |
| 67 | + } |
| 68 | + |
| 69 | + @inlinable |
| 70 | + package func isSuperset(of other: Self) -> Bool { |
| 71 | + (storage & other.storage) == other.storage |
| 72 | + } |
| 73 | + |
| 74 | + @inlinable |
| 75 | + package func union(_ other: Self) -> Self { |
| 76 | + .init(storage: storage | other.storage) |
| 77 | + } |
| 78 | + |
| 79 | + @inlinable |
| 80 | + package func intersection(_ other: Self) -> Self { |
| 81 | + .init(storage: storage & other.storage) |
| 82 | + } |
| 83 | + |
| 84 | + @inlinable |
| 85 | + package func symmetricDifference(_ other: Self) -> Self { |
| 86 | + .init(storage: storage ^ other.storage) |
| 87 | + } |
| 88 | + |
| 89 | + @inlinable |
| 90 | + mutating package func formUnion(_ other: Self) { |
| 91 | + storage |= other.storage |
| 92 | + } |
| 93 | + |
| 94 | + @inlinable |
| 95 | + mutating package func formIntersection(_ other: Self) { |
| 96 | + storage &= other.storage |
| 97 | + } |
| 98 | + |
| 99 | + @inlinable |
| 100 | + mutating package func formSymmetricDifference(_ other: Self) { |
| 101 | + storage ^= other.storage |
| 102 | + } |
| 103 | + |
| 104 | + @inlinable |
| 105 | + package var isEmpty: Bool { |
| 106 | + storage == 0 |
| 107 | + } |
| 108 | +} |
| 109 | + |
| 110 | +// MARK: Sequence |
| 111 | + |
| 112 | +extension _FixedSizeBitSet: Sequence { |
| 113 | + @inlinable |
| 114 | + package func makeIterator() -> some IteratorProtocol<Int> { |
| 115 | + _Iterator(set: self) |
| 116 | + } |
| 117 | + |
| 118 | + private struct _Iterator: IteratorProtocol { |
| 119 | + typealias Element = Int |
| 120 | + |
| 121 | + private var storage: Storage |
| 122 | + private var current: Int = -1 |
| 123 | + |
| 124 | + @inlinable |
| 125 | + init(set: _FixedSizeBitSet) { |
| 126 | + self.storage = set.storage |
| 127 | + } |
| 128 | + |
| 129 | + @inlinable |
| 130 | + mutating func next() -> Int? { |
| 131 | + guard storage != 0 else { |
| 132 | + return nil |
| 133 | + } |
| 134 | + // If the set is somewhat sparse, we can find the next element faster by shifting to the next value. |
| 135 | + // This saves needing to do `contains()` checks for all the numbers since the previous element. |
| 136 | + let amountToShift = storage.trailingZeroBitCount &+ 1 |
| 137 | + storage &>>= amountToShift |
| 138 | + |
| 139 | + current &+= amountToShift |
| 140 | + return current |
| 141 | + } |
| 142 | + } |
| 143 | +} |
| 144 | + |
| 145 | +// MARK: Collection |
| 146 | + |
| 147 | +extension _FixedSizeBitSet: Collection { |
| 148 | + // Collection conformance requires an `Index` type, that the collection can advance, and `startIndex` and `endIndex` accessors that follow certain requirements. |
| 149 | + // |
| 150 | + // For this design, as a hidden implementation detail, the `Index` holds the bit offset to the element. |
| 151 | + |
| 152 | + @inlinable |
| 153 | + package subscript(position: Index) -> Int { |
| 154 | + precondition(position.bit < Storage.bitWidth, "Index \(position.bit) out of bounds") |
| 155 | + // Because the index stores the bit offset, which is also the value, we can simply return the value without accessing the storage. |
| 156 | + return Int(position.bit) |
| 157 | + } |
| 158 | + |
| 159 | + package struct Index: Comparable { |
| 160 | + // The bit offset into the storage to the value |
| 161 | + fileprivate var bit: UInt8 |
| 162 | + |
| 163 | + package static func < (lhs: Self, rhs: Self) -> Bool { |
| 164 | + lhs.bit < rhs.bit |
| 165 | + } |
| 166 | + } |
| 167 | + |
| 168 | + @inlinable |
| 169 | + package var startIndex: Index { |
| 170 | + // This is the index (bit offset) to the smallest value in the bit set. |
| 171 | + Index(bit: UInt8(storage.trailingZeroBitCount)) |
| 172 | + } |
| 173 | + |
| 174 | + @inlinable |
| 175 | + package var endIndex: Index { |
| 176 | + // For a valid collection, the end index is required to be _exactly_ one past the last in-bounds index, meaning; `index(after: LAST_IN-BOUNDS_INDEX)` |
| 177 | + // If the collection implementation doesn't satisfy this requirement, it will have an infinitely long `indices` collection. |
| 178 | + // This either results in infinite implementations or hits internal preconditions in other Swift types that that collection has more elements than its `count`. |
| 179 | + |
| 180 | + // See `index(after:)` below for explanation of how the index after is calculated. |
| 181 | + let lastInBoundsBit = UInt8(Storage.bitWidth &- storage.leadingZeroBitCount) |
| 182 | + return Index(bit: lastInBoundsBit &+ UInt8((storage &>> lastInBoundsBit).trailingZeroBitCount)) |
| 183 | + } |
| 184 | + |
| 185 | + @inlinable |
| 186 | + package func index(after currentIndex: Index) -> Index { |
| 187 | + // To advance the index we have to find the next 1 bit _after_ the current bit. |
| 188 | + // For example, consider the following 16 bits, where values are represented from right to left: |
| 189 | + // 0110 0010 0110 0010 |
| 190 | + // |
| 191 | + // To go from the first index to the second index, we need to count the number of 0 bits between it and the next 1 bit. |
| 192 | + // We get this value by shifting the bits by one past the current index: |
| 193 | + // 0110 0010 0110 0010 |
| 194 | + // ╰╴current index |
| 195 | + // 0001 1000 1001 1000 |
| 196 | + // ~~~ 3 trailing zero bits |
| 197 | + // |
| 198 | + // The second index's absolute value is the one past the first index's value plus the number of trailing zero bits in the shifted value. |
| 199 | + // |
| 200 | + // For the third index we repeat the same process, starting by shifting the bits by one past second index: |
| 201 | + // 0110 0010 0110 0010 |
| 202 | + // ╰╴current index |
| 203 | + // 0000 0001 1000 1001 |
| 204 | + // 0 trailing zero bits |
| 205 | + // |
| 206 | + // This time there are no trailing zero bits in the shifted value, so the third index's absolute value is just one past the second index. |
| 207 | + let shift = currentIndex.bit &+ 1 |
| 208 | + return Index(bit: shift &+ UInt8((storage &>> shift).trailingZeroBitCount)) |
| 209 | + } |
| 210 | + |
| 211 | + @inlinable |
| 212 | + package func formIndex(after index: inout Index) { |
| 213 | + // See `index(after:)` above for explanation. |
| 214 | + index.bit &+= 1 |
| 215 | + index.bit &+= UInt8((storage &>> index.bit).trailingZeroBitCount) |
| 216 | + } |
| 217 | + |
| 218 | + @inlinable |
| 219 | + package func distance(from start: Index, to end: Index) -> Int { |
| 220 | + // To compute the distance between two indices we have to find the number of 1 bits from the start index to (but excluding) the end index. |
| 221 | + // For example, consider the following 16 bits, where values are represented from right to left: |
| 222 | + // 0110 0010 0110 0010 |
| 223 | + // end╶╯ ╰╴start |
| 224 | + // |
| 225 | + // To find the distance between the second index and the fourth index, we need to count the number of 0 bits between it and the next 1 bit. |
| 226 | + // We limit the calculation to this range in two steps. |
| 227 | + // |
| 228 | + // First, we mask out all the bits above the end index: |
| 229 | + // end╶╮ ╭╴start |
| 230 | + // 0110 0010 0110 0010 |
| 231 | + // 0000 0011 1111 1111 mask |
| 232 | + // |
| 233 | + // Because collections can have end indices that extend out-of-bounds we need to clamp the mask from a larger integer type to avoid it wrapping around to 0. |
| 234 | + let mask = Storage(clamping: (1 &<< UInt(end.bit)) &- 1) |
| 235 | + var distance = storage & mask |
| 236 | + |
| 237 | + // Then, we shift away all the bits below the start index: |
| 238 | + // end╶╮ ╭╴start |
| 239 | + // 0000 0010 0110 0010 |
| 240 | + // 0000 0000 0000 1001 |
| 241 | + distance &>>= start.bit |
| 242 | + |
| 243 | + // The distance from start to end is the number of 1 bits in this number. |
| 244 | + return distance.nonzeroBitCount |
| 245 | + } |
| 246 | + |
| 247 | + @inlinable |
| 248 | + package var first: Element? { |
| 249 | + isEmpty ? nil : storage.trailingZeroBitCount |
| 250 | + } |
| 251 | + |
| 252 | + @inlinable |
| 253 | + package func min() -> Element? { |
| 254 | + first // The elements are already sorted |
| 255 | + } |
| 256 | + |
| 257 | + @inlinable |
| 258 | + package func sorted() -> [Element] { |
| 259 | + Array(self) // The elements are already sorted |
| 260 | + } |
| 261 | + |
| 262 | + @inlinable |
| 263 | + package var count: Int { |
| 264 | + storage.nonzeroBitCount |
| 265 | + } |
| 266 | +} |
| 267 | + |
| 268 | +// MARK: Hashable |
| 269 | + |
| 270 | +extension _FixedSizeBitSet: Hashable {} |
| 271 | + |
| 272 | +// MARK: Combinations |
| 273 | + |
| 274 | +extension _FixedSizeBitSet { |
| 275 | + /// Returns a list of all possible combinations of the elements in the set, in order of increasing number of elements. |
| 276 | + package func allCombinationsOfValues() -> [Self] { |
| 277 | + // Leverage the fact that bits of an Int represent the possible combinations. |
| 278 | + let smallest = storage.trailingZeroBitCount |
| 279 | + |
| 280 | + var combinations: [Self] = [] |
| 281 | + combinations.reserveCapacity((1 &<< count /*known to be less than Storage.bitWidth */) - 1) |
| 282 | + |
| 283 | + for raw in 1 ... storage &>> smallest { |
| 284 | + let combination = Self(storage: Storage(raw &<< smallest)) |
| 285 | + |
| 286 | + // Filter out any combinations that include columns that are the same for all overloads |
| 287 | + guard self.isSuperset(of: combination) else { continue } |
| 288 | + |
| 289 | + combinations.append(combination) |
| 290 | + } |
| 291 | + // The bits of larger and larger Int values won't be in order of number of bits set, so we sort them. |
| 292 | + return combinations.sorted(by: { $0.count < $1.count }) |
| 293 | + } |
| 294 | +} |
0 commit comments