|
| 1 | +//===----------------------------------------------------------------------===// |
| 2 | +// |
| 3 | +// This source file is part of the Swift Async Algorithms open source project |
| 4 | +// |
| 5 | +// Copyright (c) 2023 Apple Inc. and the Swift project authors |
| 6 | +// Licensed under Apache License v2.0 with Runtime Library Exception |
| 7 | +// |
| 8 | +// See https://swift.org/LICENSE.txt for license information |
| 9 | +// |
| 10 | +//===----------------------------------------------------------------------===// |
| 11 | +//===----------------------------------------------------------------------===// |
| 12 | +// |
| 13 | +// This source file is part of the SwiftCertificates open source project |
| 14 | +// |
| 15 | +// Copyright (c) 2023 Apple Inc. and the SwiftCertificates project authors |
| 16 | +// Licensed under Apache License v2.0 |
| 17 | +// |
| 18 | +// See LICENSE.txt for license information |
| 19 | +// See CONTRIBUTORS.txt for the list of SwiftCertificates project authors |
| 20 | +// |
| 21 | +// SPDX-License-Identifier: Apache-2.0 |
| 22 | +// |
| 23 | +//===----------------------------------------------------------------------===// |
| 24 | + |
| 25 | +/// ``_TinyArray`` is a ``RandomAccessCollection`` optimised to store zero or one ``Element``. |
| 26 | +/// It supports arbitrary many elements but if only up to one ``Element`` is stored it does **not** allocate separate storage on the heap |
| 27 | +/// and instead stores the ``Element`` inline. |
| 28 | +@usableFromInline |
| 29 | +struct _TinyArray<Element> { |
| 30 | + @usableFromInline |
| 31 | + enum Storage { |
| 32 | + case one(Element) |
| 33 | + case arbitrary([Element]) |
| 34 | + } |
| 35 | + |
| 36 | + @usableFromInline |
| 37 | + var storage: Storage |
| 38 | +} |
| 39 | + |
| 40 | +// MARK: - TinyArray "public" interface |
| 41 | + |
| 42 | +extension _TinyArray: Equatable where Element: Equatable {} |
| 43 | +extension _TinyArray: Hashable where Element: Hashable {} |
| 44 | +extension _TinyArray: Sendable where Element: Sendable {} |
| 45 | + |
| 46 | +extension _TinyArray: RandomAccessCollection { |
| 47 | + @usableFromInline |
| 48 | + typealias Element = Element |
| 49 | + |
| 50 | + @usableFromInline |
| 51 | + typealias Index = Int |
| 52 | + |
| 53 | + @inlinable |
| 54 | + subscript(position: Int) -> Element { |
| 55 | + get { |
| 56 | + self.storage[position] |
| 57 | + } |
| 58 | + set { |
| 59 | + self.storage[position] = newValue |
| 60 | + } |
| 61 | + } |
| 62 | + |
| 63 | + @inlinable |
| 64 | + var startIndex: Int { |
| 65 | + self.storage.startIndex |
| 66 | + } |
| 67 | + |
| 68 | + @inlinable |
| 69 | + var endIndex: Int { |
| 70 | + self.storage.endIndex |
| 71 | + } |
| 72 | +} |
| 73 | + |
| 74 | +extension _TinyArray { |
| 75 | + @inlinable |
| 76 | + init(_ elements: some Sequence<Element>) { |
| 77 | + self.storage = .init(elements) |
| 78 | + } |
| 79 | + |
| 80 | + @inlinable |
| 81 | + init() { |
| 82 | + self.storage = .init() |
| 83 | + } |
| 84 | + |
| 85 | + @inlinable |
| 86 | + mutating func append(_ newElement: Element) { |
| 87 | + self.storage.append(newElement) |
| 88 | + } |
| 89 | + |
| 90 | + @inlinable |
| 91 | + mutating func append(contentsOf newElements: some Sequence<Element>) { |
| 92 | + self.storage.append(contentsOf: newElements) |
| 93 | + } |
| 94 | + |
| 95 | + @discardableResult |
| 96 | + @inlinable |
| 97 | + mutating func remove(at index: Int) -> Element { |
| 98 | + self.storage.remove(at: index) |
| 99 | + } |
| 100 | + |
| 101 | + @inlinable |
| 102 | + mutating func removeAll(where shouldBeRemoved: (Element) throws -> Bool) rethrows { |
| 103 | + try self.storage.removeAll(where: shouldBeRemoved) |
| 104 | + } |
| 105 | + |
| 106 | + @inlinable |
| 107 | + mutating func sort(by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows { |
| 108 | + try self.storage.sort(by: areInIncreasingOrder) |
| 109 | + } |
| 110 | +} |
| 111 | + |
| 112 | +// MARK: - TinyArray.Storage "private" implementation |
| 113 | + |
| 114 | +extension _TinyArray.Storage: Equatable where Element: Equatable { |
| 115 | + @inlinable |
| 116 | + static func == (lhs: Self, rhs: Self) -> Bool { |
| 117 | + switch (lhs, rhs) { |
| 118 | + case (.one(let lhs), .one(let rhs)): |
| 119 | + return lhs == rhs |
| 120 | + case (.arbitrary(let lhs), .arbitrary(let rhs)): |
| 121 | + // we don't use lhs.elementsEqual(rhs) so we can hit the fast path from Array |
| 122 | + // if both arrays share the same underlying storage: https://github.com/apple/swift/blob/b42019005988b2d13398025883e285a81d323efa/stdlib/public/core/Array.swift#L1775 |
| 123 | + return lhs == rhs |
| 124 | + |
| 125 | + case (.one(let element), .arbitrary(let array)), |
| 126 | + (.arbitrary(let array), .one(let element)): |
| 127 | + guard array.count == 1 else { |
| 128 | + return false |
| 129 | + } |
| 130 | + return element == array[0] |
| 131 | + |
| 132 | + } |
| 133 | + } |
| 134 | +} |
| 135 | +extension _TinyArray.Storage: Hashable where Element: Hashable { |
| 136 | + @inlinable |
| 137 | + func hash(into hasher: inout Hasher) { |
| 138 | + // same strategy as Array: https://github.com/apple/swift/blob/b42019005988b2d13398025883e285a81d323efa/stdlib/public/core/Array.swift#L1801 |
| 139 | + hasher.combine(count) |
| 140 | + for element in self { |
| 141 | + hasher.combine(element) |
| 142 | + } |
| 143 | + } |
| 144 | +} |
| 145 | +extension _TinyArray.Storage: Sendable where Element: Sendable {} |
| 146 | + |
| 147 | +extension _TinyArray.Storage: RandomAccessCollection { |
| 148 | + @inlinable |
| 149 | + subscript(position: Int) -> Element { |
| 150 | + get { |
| 151 | + switch self { |
| 152 | + case .one(let element): |
| 153 | + guard position == 0 else { |
| 154 | + fatalError("index \(position) out of bounds") |
| 155 | + } |
| 156 | + return element |
| 157 | + case .arbitrary(let elements): |
| 158 | + return elements[position] |
| 159 | + } |
| 160 | + } |
| 161 | + set { |
| 162 | + switch self { |
| 163 | + case .one: |
| 164 | + guard position == 0 else { |
| 165 | + fatalError("index \(position) out of bounds") |
| 166 | + } |
| 167 | + self = .one(newValue) |
| 168 | + case .arbitrary(var elements): |
| 169 | + elements[position] = newValue |
| 170 | + self = .arbitrary(elements) |
| 171 | + } |
| 172 | + } |
| 173 | + } |
| 174 | + |
| 175 | + @inlinable |
| 176 | + var startIndex: Int { |
| 177 | + 0 |
| 178 | + } |
| 179 | + |
| 180 | + @inlinable |
| 181 | + var endIndex: Int { |
| 182 | + switch self { |
| 183 | + case .one: return 1 |
| 184 | + case .arbitrary(let elements): return elements.endIndex |
| 185 | + } |
| 186 | + } |
| 187 | +} |
| 188 | + |
| 189 | +extension _TinyArray.Storage { |
| 190 | + @inlinable |
| 191 | + init(_ elements: some Sequence<Element>) { |
| 192 | + var iterator = elements.makeIterator() |
| 193 | + guard let firstElement = iterator.next() else { |
| 194 | + self = .arbitrary([]) |
| 195 | + return |
| 196 | + } |
| 197 | + guard let secondElement = iterator.next() else { |
| 198 | + // newElements just contains a single element |
| 199 | + // and we hit the fast path |
| 200 | + self = .one(firstElement) |
| 201 | + return |
| 202 | + } |
| 203 | + |
| 204 | + var elements: [Element] = [] |
| 205 | + elements.reserveCapacity(elements.underestimatedCount) |
| 206 | + elements.append(firstElement) |
| 207 | + elements.append(secondElement) |
| 208 | + while let nextElement = iterator.next() { |
| 209 | + elements.append(nextElement) |
| 210 | + } |
| 211 | + self = .arbitrary(elements) |
| 212 | + } |
| 213 | + |
| 214 | + @inlinable |
| 215 | + init() { |
| 216 | + self = .arbitrary([]) |
| 217 | + } |
| 218 | + |
| 219 | + @inlinable |
| 220 | + mutating func append(_ newElement: Element) { |
| 221 | + self.append(contentsOf: CollectionOfOne(newElement)) |
| 222 | + } |
| 223 | + |
| 224 | + @inlinable |
| 225 | + mutating func append(contentsOf newElements: some Sequence<Element>) { |
| 226 | + switch self { |
| 227 | + case .one(let firstElement): |
| 228 | + var iterator = newElements.makeIterator() |
| 229 | + guard let secondElement = iterator.next() else { |
| 230 | + // newElements is empty, nothing to do |
| 231 | + return |
| 232 | + } |
| 233 | + var elements: [Element] = [] |
| 234 | + elements.reserveCapacity(1 + newElements.underestimatedCount) |
| 235 | + elements.append(firstElement) |
| 236 | + elements.append(secondElement) |
| 237 | + elements.appendRemainingElements(from: &iterator) |
| 238 | + self = .arbitrary(elements) |
| 239 | + |
| 240 | + case .arbitrary(var elements): |
| 241 | + if elements.isEmpty { |
| 242 | + // if `self` is currently empty and `newElements` just contains a single |
| 243 | + // element, we skip allocating an array and set `self` to `.one(firstElement)` |
| 244 | + var iterator = newElements.makeIterator() |
| 245 | + guard let firstElement = iterator.next() else { |
| 246 | + // newElements is empty, nothing to do |
| 247 | + return |
| 248 | + } |
| 249 | + guard let secondElement = iterator.next() else { |
| 250 | + // newElements just contains a single element |
| 251 | + // and we hit the fast path |
| 252 | + self = .one(firstElement) |
| 253 | + return |
| 254 | + } |
| 255 | + elements.reserveCapacity(elements.count + newElements.underestimatedCount) |
| 256 | + elements.append(firstElement) |
| 257 | + elements.append(secondElement) |
| 258 | + elements.appendRemainingElements(from: &iterator) |
| 259 | + self = .arbitrary(elements) |
| 260 | + |
| 261 | + } else { |
| 262 | + elements.append(contentsOf: newElements) |
| 263 | + self = .arbitrary(elements) |
| 264 | + } |
| 265 | + |
| 266 | + } |
| 267 | + } |
| 268 | + |
| 269 | + @discardableResult |
| 270 | + @inlinable |
| 271 | + mutating func remove(at index: Int) -> Element { |
| 272 | + switch self { |
| 273 | + case .one(let oldElement): |
| 274 | + guard index == 0 else { |
| 275 | + fatalError("index \(index) out of bounds") |
| 276 | + } |
| 277 | + self = .arbitrary([]) |
| 278 | + return oldElement |
| 279 | + |
| 280 | + case .arbitrary(var elements): |
| 281 | + defer { |
| 282 | + self = .arbitrary(elements) |
| 283 | + } |
| 284 | + return elements.remove(at: index) |
| 285 | + |
| 286 | + } |
| 287 | + } |
| 288 | + |
| 289 | + @inlinable |
| 290 | + mutating func removeAll(where shouldBeRemoved: (Element) throws -> Bool) rethrows { |
| 291 | + switch self { |
| 292 | + case .one(let oldElement): |
| 293 | + if try shouldBeRemoved(oldElement) { |
| 294 | + self = .arbitrary([]) |
| 295 | + } |
| 296 | + |
| 297 | + case .arbitrary(var elements): |
| 298 | + defer { |
| 299 | + self = .arbitrary(elements) |
| 300 | + } |
| 301 | + return try elements.removeAll(where: shouldBeRemoved) |
| 302 | + |
| 303 | + } |
| 304 | + } |
| 305 | + |
| 306 | + @inlinable |
| 307 | + mutating func sort(by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows { |
| 308 | + switch self { |
| 309 | + case .one: |
| 310 | + // a collection of just one element is always sorted, nothing to do |
| 311 | + break |
| 312 | + case .arbitrary(var elements): |
| 313 | + defer { |
| 314 | + self = .arbitrary(elements) |
| 315 | + } |
| 316 | + |
| 317 | + try elements.sort(by: areInIncreasingOrder) |
| 318 | + } |
| 319 | + } |
| 320 | +} |
| 321 | + |
| 322 | +extension Array { |
| 323 | + @inlinable |
| 324 | + mutating func appendRemainingElements(from iterator: inout some IteratorProtocol<Element>) { |
| 325 | + while let nextElement = iterator.next() { |
| 326 | + append(nextElement) |
| 327 | + } |
| 328 | + } |
| 329 | +} |
0 commit comments