diff --git a/Benchmarks/Package.swift b/Benchmarks/Package.swift index 1dbf75c04..83a700b6a 100644 --- a/Benchmarks/Package.swift +++ b/Benchmarks/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.3 +// swift-tools-version:5.5 //===----------------------------------------------------------------------===// // // This source file is part of the Swift Collections open source project @@ -16,6 +16,7 @@ let package = Package( name: "swift-collections.Benchmarks", products: [ .executable(name: "benchmark", targets: ["benchmark"]), + .executable(name: "memory-benchmark", targets: ["memory-benchmark"]), ], dependencies: [ .package(name: "swift-collections", path: ".."), @@ -36,13 +37,20 @@ let package = Package( .target( name: "CppBenchmarks" ), - .target( + .executableTarget( name: "benchmark", dependencies: [ "Benchmarks", ], path: "Sources/benchmark-tool" ), + .executableTarget( + name: "memory-benchmark", + dependencies: [ + .product(name: "Collections", package: "swift-collections"), + .product(name: "CollectionsBenchmark", package: "swift-collections-benchmark"), + ] + ), ], - cxxLanguageStandard: .cxx1z + cxxLanguageStandard: .cxx17 ) diff --git a/Benchmarks/Sources/Benchmarks/PersistentDictionaryBenchmarks.swift b/Benchmarks/Sources/Benchmarks/PersistentDictionaryBenchmarks.swift index 61cd1d214..8e09cf5cd 100644 --- a/Benchmarks/Sources/Benchmarks/PersistentDictionaryBenchmarks.swift +++ b/Benchmarks/Sources/Benchmarks/PersistentDictionaryBenchmarks.swift @@ -72,6 +72,25 @@ extension Benchmark { } } + self.add( + title: "PersistentDictionary striding, 10 steps", + input: [Int].self + ) { input in + let d = PersistentDictionary( + uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + let steps = stride(from: 0, through: 10 * d.count, by: d.count) + .map { $0 / 10 } + return { timer in + var i = d.startIndex + for j in 1 ..< steps.count { + let distance = steps[j] - steps[j - 1] + i = identity(d.index(i, offsetBy: distance)) + } + precondition(i == d.endIndex) + blackHole(i) + } + } + self.add( title: "PersistentDictionary indexing subscript", input: ([Int], [Int]).self diff --git a/Benchmarks/Sources/memory-benchmark/DictionaryStatistics.swift b/Benchmarks/Sources/memory-benchmark/DictionaryStatistics.swift new file mode 100644 index 000000000..85cd90335 --- /dev/null +++ b/Benchmarks/Sources/memory-benchmark/DictionaryStatistics.swift @@ -0,0 +1,62 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +struct DictionaryStatistics { + /// The sum of all storage within the hash table that is available for + /// item storage, measured in bytes. This does account for the maximum + /// load factor. + var capacityBytes: Int = 0 + + /// The number of bytes of storage currently used for storing items. + var itemBytes: Int = 0 + + /// The number of bytes currently available in storage for storing items. + var freeBytes: Int = 0 + + /// An estimate of the actual memory occupied by this hash table. + /// This includes not only storage space available for items, + /// but also the memory taken up by the object header and the hash table + /// occupation bitmap. + var grossBytes: Int = 0 + + /// An estimate of how efficiently this data structure manages memory. + /// This is a value between 0 and 1 -- the ratio between how much space + /// the actual stored data occupies and the overall number of bytes allocated + /// for the entire data structure. (`itemBytes / grossBytes`) + var memoryEfficiency: Double { + guard grossBytes > 0 else { return 1 } + return Double(itemBytes) / Double(grossBytes) + } +} + +extension Dictionary { + var statistics: DictionaryStatistics { + // Note: This logic is based on the Dictionary ABI. It may be off by a few + // bytes due to not accounting for padding bytes between storage regions. + // The gross bytes reported also do not include extra memory that was + // allocated by malloc but not actually used for Dictionary storage. + var stats = DictionaryStatistics() + let keyStride = MemoryLayout.stride + let valueStride = MemoryLayout.stride + stats.capacityBytes = self.capacity * (keyStride + valueStride) + stats.itemBytes = self.count * (keyStride + valueStride) + stats.freeBytes = stats.capacityBytes - stats.itemBytes + + let bucketCount = self.capacity._roundUpToPowerOfTwo() + let bitmapBitcount = (bucketCount + UInt.bitWidth - 1) + + let objectHeaderBits = 2 * Int.bitWidth + let ivarBits = 5 * Int.bitWidth + 64 + stats.grossBytes = (objectHeaderBits + ivarBits + bitmapBitcount) / 8 + stats.grossBytes += bucketCount * keyStride + bucketCount * valueStride + return stats + } +} diff --git a/Benchmarks/Sources/memory-benchmark/MemoryBenchmarks.swift b/Benchmarks/Sources/memory-benchmark/MemoryBenchmarks.swift new file mode 100644 index 000000000..b42df2ae8 --- /dev/null +++ b/Benchmarks/Sources/memory-benchmark/MemoryBenchmarks.swift @@ -0,0 +1,80 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import ArgumentParser +import CollectionsBenchmark +import Collections + +@main +struct MemoryBenchmarks: ParsableCommand { + static var configuration: CommandConfiguration { + CommandConfiguration( + commandName: "memory-statistics", + abstract: "A utility for running memory benchmarks for collection types.") + } + + @OptionGroup + var sizes: Benchmark.Options.SizeSelection + + mutating func run() throws { + let sizes = try self.sizes.resolveSizes() + + var i = 0 + + var d: Dictionary = [:] + var pd: PersistentDictionary = [:] + + print(""" + Size,"Dictionary",\ + "PersistentDictionary",\ + "average node size",\ + "average item depth" + """) + + var sumd: Double = 0 + var sump: Double = 0 + for size in sizes { + while i < size.rawValue { + let key = "key \(i)" + let value = "value \(i)" + d[key] = value + pd[key] = value + i += 1 + } + + let dstats = d.statistics + let pstats = pd._statistics + print(""" + \(size.rawValue),\ + \(dstats.memoryEfficiency),\ + \(pstats.memoryEfficiency),\ + \(pstats.averageNodeSize),\ + \(pstats.averageItemDepth) + """) + sumd += dstats.memoryEfficiency + sump += pstats.memoryEfficiency + } + + let pstats = pd._statistics + complain(""" + Averages: + Dictionary: \(sumd / Double(sizes.count)) + PersistentDictionary: \(sump / Double(sizes.count)) + + PersistentDictionary at 1M items: + average node size: \(pstats.averageNodeSize) + average item depth: \(pstats.averageItemDepth) + average lookup chain length: \(pstats.averageLookupChainLength) + """) + } +} + + diff --git a/Sources/BitCollections/BitSet/BitSet+Extras.swift b/Sources/BitCollections/BitSet/BitSet+Extras.swift index 4ac8f8e04..3333cbd33 100644 --- a/Sources/BitCollections/BitSet/BitSet+Extras.swift +++ b/Sources/BitCollections/BitSet/BitSet+Extras.swift @@ -9,6 +9,10 @@ // //===----------------------------------------------------------------------===// +import _CollectionsUtilities + +extension BitSet: _FastMembershipCheckable {} + extension BitSet { /// Creates a new empty bit set with enough storage capacity to store values /// up to the given maximum value without reallocating storage. diff --git a/Sources/OrderedCollections/OrderedSet/OrderedSet+Extras.swift b/Sources/OrderedCollections/OrderedSet/OrderedSet+Extras.swift new file mode 100644 index 000000000..4efd0309b --- /dev/null +++ b/Sources/OrderedCollections/OrderedSet/OrderedSet+Extras.swift @@ -0,0 +1,14 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import _CollectionsUtilities + +extension OrderedSet: _FastMembershipCheckable {} diff --git a/Sources/PersistentCollections/Node/_AncestorSlots.swift b/Sources/PersistentCollections/Node/_AncestorSlots.swift index 47f14ec18..54d12375f 100644 --- a/Sources/PersistentCollections/Node/_AncestorSlots.swift +++ b/Sources/PersistentCollections/Node/_AncestorSlots.swift @@ -72,6 +72,13 @@ extension _AncestorSlots { path &= ~(_Bucket.bitMask &<< level.shift) } + /// Clear all slots at or below the specified level, by setting them to zero. + @inlinable + internal mutating func clear(atOrBelow level: _Level) { + guard level.shift < UInt.bitWidth else { return } + path &= ~(UInt.max &<< level.shift) + } + /// Truncate this path to the specified level. /// Slots at or beyond the specified level are cleared. @inlinable diff --git a/Sources/PersistentCollections/Node/_Bitmap.swift b/Sources/PersistentCollections/Node/_Bitmap.swift index 7f3267a8f..7cfdce1b6 100644 --- a/Sources/PersistentCollections/Node/_Bitmap.swift +++ b/Sources/PersistentCollections/Node/_Bitmap.swift @@ -83,6 +83,20 @@ extension _Bitmap { @inlinable @inline(__always) internal var isEmpty: Bool { _value == 0 } + + @inlinable @inline(__always) + internal var first: _Bucket? { + guard !isEmpty else { return nil } + return _Bucket( + _value: UInt8(truncatingIfNeeded: _value.trailingZeroBitCount)) + } + + @inlinable @inline(__always) + internal mutating func popFirst() -> _Bucket? { + guard let bucket = first else { return nil } + _value &= _value &- 1 // Clear lowest nonzero bit. + return bucket + } } extension _Bitmap { @@ -147,23 +161,40 @@ extension _Bitmap { } } -extension _Bitmap: Sequence, IteratorProtocol { +extension _Bitmap: Sequence { @usableFromInline - internal typealias Element = _Bucket + internal typealias Element = (bucket: _Bucket, slot: _Slot) - @inlinable - internal var underestimatedCount: Int { count } + @usableFromInline + @frozen + internal struct Iterator: IteratorProtocol { + @usableFromInline + internal var bitmap: _Bitmap + + @usableFromInline + internal var slot: _Slot + + @inlinable + internal init(_ bitmap: _Bitmap) { + self.bitmap = bitmap + self.slot = .zero + } + + /// Return the index of the lowest set bit in this word, + /// and also destructively clear it. + @inlinable + internal mutating func next() -> Element? { + guard let bucket = bitmap.popFirst() else { return nil } + defer { slot = slot.next() } + return (bucket, slot) + } + } @inlinable - internal func makeIterator() -> _Bitmap { self } + internal var underestimatedCount: Int { count } - /// Return the index of the lowest set bit in this word, - /// and also destructively clear it. @inlinable - internal mutating func next() -> _Bucket? { - guard _value != 0 else { return nil } - let bucket = _Bucket(UInt(bitPattern: _value.trailingZeroBitCount)) - _value &= _value &- 1 // Clear lowest nonzero bit. - return bucket + internal func makeIterator() -> Iterator { + Iterator(self) } } diff --git a/Sources/PersistentCollections/Node/_Hash.swift b/Sources/PersistentCollections/Node/_Hash.swift index ed45c79d1..43bf0818b 100644 --- a/Sources/PersistentCollections/Node/_Hash.swift +++ b/Sources/PersistentCollections/Node/_Hash.swift @@ -69,6 +69,11 @@ extension _Hash { } extension _Hash { + @inlinable + internal static var emptyPrefix: _Hash { + _Hash(_value: 0) + } + @inlinable internal func appending(_ bucket: _Bucket, at level: _Level) -> Self { assert(value >> level.shift == 0) diff --git a/Sources/PersistentCollections/Node/_HashTreeStatistics.swift b/Sources/PersistentCollections/Node/_HashTreeStatistics.swift index 011e65bae..1161414db 100644 --- a/Sources/PersistentCollections/Node/_HashTreeStatistics.swift +++ b/Sources/PersistentCollections/Node/_HashTreeStatistics.swift @@ -9,4 +9,122 @@ // //===----------------------------------------------------------------------===// -import Foundation +public struct _HashTreeStatistics { + /// The number of nodes in the tree. + public internal(set) var nodeCount: Int = 0 + + /// The number of collision nodes in the tree. + public internal(set) var collisionNodeCount: Int = 0 + + /// The number of elements within this tree. + public internal(set) var itemCount: Int = 0 + + /// The number of elements whose keys have colliding hashes in the tree. + public internal(set) var collisionCount: Int = 0 + + /// The number of key comparisons that need to be done due to hash collisions + /// when finding every key in the tree. + public internal(set) var _collisionChainCount: Int = 0 + + /// The maximum depth of the tree. + public internal(set) var maxItemDepth: Int = 0 + + internal var _sumItemDepth: Int = 0 + + /// The sum of all storage within the tree that is available for item storage, + /// measured in bytes. (This is storage is shared between actual + /// items and child references. Depending on alignment issues, not all of + /// this may be actually usable.) + public internal(set) var capacityBytes: Int = 0 + + /// The number of bytes of storage currently used for storing items. + public internal(set) var itemBytes: Int = 0 + + /// The number of bytes of storage currently used for storing child + /// references. + public internal(set) var childBytes: Int = 0 + + /// The number of bytes currently available for storage, summed over all + /// nodes in the tree. + public internal(set) var freeBytes: Int = 0 + + /// An estimate of the actual memory occupied by this tree. This includes + /// not only storage space for items & children, but also the memory taken up + /// by node headers and Swift's object headers. + public internal(set) var grossBytes: Int = 0 + + /// The average level of an item within this tree. + public var averageItemDepth: Double { + guard nodeCount > 0 else { return 0 } + return Double(_sumItemDepth) / Double(itemCount) + } + /// An estimate of how efficiently this data structure manages memory. + /// This is a value between 0 and 1 -- the ratio between how much space + /// the actual stored data occupies and the overall number of bytes allocated + /// for the entire data structure. (`itemBytes / grossBytes`) + public var memoryEfficiency: Double { + guard grossBytes > 0 else { return 1 } + return Double(itemBytes) / Double(grossBytes) + } + + public var averageNodeSize: Double { + guard nodeCount > 0 else { return 0 } + return Double(capacityBytes) / Double(nodeCount) + } + + /// The average number of keys that need to be compared within the tree + /// to find a member item. This is exactly 1 unless the tree contains hash + /// collisions. + public var averageLookupChainLength: Double { + guard itemCount > 0 else { return 1 } + return Double(itemCount + _collisionChainCount) / Double(itemCount) + } + + internal init() { + // Nothing to do + } +} + + +extension _Node { + internal func gatherStatistics( + _ level: _Level, _ stats: inout _HashTreeStatistics + ) { + // The empty singleton does not count as a node and occupies no space. + if self.raw.storage === _emptySingleton { return } + + read { + stats.nodeCount += 1 + stats.itemCount += $0.itemCount + + if isCollisionNode { + stats.collisionNodeCount += 1 + stats.collisionCount += $0.itemCount + stats._collisionChainCount += $0.itemCount * ($0.itemCount - 1) / 2 + } + + let keyStride = MemoryLayout.stride + let valueStride = MemoryLayout.stride + + stats.maxItemDepth = Swift.max(stats.maxItemDepth, level.depth) + stats._sumItemDepth += (level.depth + 1) * $0.itemCount + stats.capacityBytes += $0.byteCapacity + stats.freeBytes += $0.bytesFree + stats.itemBytes += $0.itemCount * (keyStride + valueStride) + stats.childBytes += $0.childCount * MemoryLayout<_RawNode>.stride + + let objectHeaderSize = 2 * MemoryLayout.stride + + // Note: for simplicity, we assume that there is no padding between + // the object header and the storage header. + let start = _getUnsafePointerToStoredProperties(self.raw.storage) + let capacity = self.raw.storage.capacity + let end = $0._memory + capacity * MemoryLayout<_RawNode>.stride + stats.grossBytes += objectHeaderSize + (end - start) + + for child in $0.children { + child.gatherStatistics(level.descend(), &stats) + } + } + } +} diff --git a/Sources/PersistentCollections/Node/_Level.swift b/Sources/PersistentCollections/Node/_Level.swift index ecc6c568b..afda20c22 100644 --- a/Sources/PersistentCollections/Node/_Level.swift +++ b/Sources/PersistentCollections/Node/_Level.swift @@ -68,6 +68,11 @@ extension _Level { @inlinable @inline(__always) internal var isAtBottom: Bool { _shift >= UInt.bitWidth } + @inlinable @inline(__always) + internal var depth: Int { + (Int(bitPattern: shift) + _Bitmap.bitWidth - 1) / _Bitmap.bitWidth + } + @inlinable @inline(__always) internal func descend() -> _Level { // FIXME: Consider returning nil when we run out of bits diff --git a/Sources/PersistentCollections/Node/_Node+Builder.swift b/Sources/PersistentCollections/Node/_Node+Builder.swift new file mode 100644 index 000000000..7d3d4972c --- /dev/null +++ b/Sources/PersistentCollections/Node/_Node+Builder.swift @@ -0,0 +1,138 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +extension _Node { + @usableFromInline + @frozen + internal enum Builder { + case empty + case item(Element, _Hash) + case node(_Node, _Hash) + + @inlinable + internal init(_ level: _Level, _ node: _Node, _ hashPrefix: _Hash) { + if node.count == 0 { + self = .empty + } else if node.hasSingletonItem { + let item = node.read { $0[item: .zero] } + self = .item(item, hashPrefix) + } else { + self = .node(node, hashPrefix) + } + } + + @inlinable + internal func finalize(_ level: _Level) -> _Node { + assert(level.isAtRoot) + switch self { + case .empty: + return _Node(storage: _emptySingleton, count: 0) + case .item(let item, let h): + return _Node._regularNode(item, h[level]) + case .node(let node, _): + return node + } + } + + @inlinable + internal mutating func addNewCollision(_ newItem: Element, _ hash: _Hash) { + switch self { + case .empty: + self = .item(newItem, hash) + case .item(let oldItem, let h): + assert(hash == h) + let node = _Node._collisionNode(hash, oldItem, newItem) + self = .node(node, hash) + case .node(var node, let h): + self = .empty + assert(node.isCollisionNode) + assert(hash == h) + assert(hash == node.collisionHash) + node.ensureUniqueAndAppendCollision(isUnique: true, newItem) + self = .node(node, h) + } + } + + @inlinable + internal mutating func addNewItem( + _ level: _Level, _ newItem: Element, _ hashPrefix: _Hash + ) { + switch self { + case .empty: + self = .item(newItem, hashPrefix) + case .item(let oldItem, let oldHash): + let bucket1 = oldHash[level] + let bucket2 = hashPrefix[level] + assert(bucket1 != bucket2) + assert(oldHash.isEqual(to: hashPrefix, upTo: level)) + let node = _Node._regularNode(oldItem, bucket1, newItem, bucket2) + self = .node(node, hashPrefix) + case .node(var node, let nodeHash): + self = .empty + assert(!node.isCollisionNode) + assert(nodeHash.isEqual(to: hashPrefix, upTo: level)) + let bucket = hashPrefix[level] + node.ensureUniqueAndInsertItem(isUnique: true, newItem, bucket) + self = .node(node, nodeHash) + } + } + + @inlinable + internal mutating func addNewChildBranch( + _ level: _Level, _ branch: Builder + ) { + switch (self, branch) { + case (_, .empty): + break + case (.empty, .item): + self = branch + case (.empty, .node(let child, let childHash)): + if child.isCollisionNode { + // Compression + assert(!level.isAtBottom) + self = branch + } else { + let node = _Node._regularNode(child, childHash[level]) + self = .node(node, childHash) + } + case let (.item(li, lh), .item(ri, rh)): + let node = _Node._regularNode(li, lh[level], ri, rh[level]) + self = .node(node, lh) + case let (.item(item, itemHash), .node(child, childHash)): + assert(itemHash.isEqual(to: childHash, upTo: level)) + let node = _Node._regularNode( + item, itemHash[level], + child, childHash[level]) + self = .node(node, childHash) + case (.node(var node, let nodeHash), .item(let item, let itemHash)): + if node.isCollisionNode { + // Expansion + assert(!level.isAtBottom) + node = _Node._regularNode(node, nodeHash[level]) + } + assert(!node.isCollisionNode) + assert(nodeHash.isEqual(to: itemHash, upTo: level)) + node.ensureUniqueAndInsertItem(isUnique: true, item, itemHash[level]) + self = .node(node, nodeHash) + case (.node(var node, let nodeHash), .node(let child, let childHash)): + if node.isCollisionNode { + // Expansion + assert(!level.isAtBottom) + node = _Node._regularNode(node, nodeHash[level]) + } + assert(nodeHash.isEqual(to: childHash, upTo: level)) + node.ensureUnique(isUnique: true, withFreeSpace: _Node.spaceForNewChild) + node.insertChild(child, childHash[level]) + self = .node(node, nodeHash) + } + } + } +} diff --git a/Sources/PersistentCollections/Node/_Node+Debugging.swift b/Sources/PersistentCollections/Node/_Node+Debugging.swift index e7821dba3..3b0dd1c7f 100644 --- a/Sources/PersistentCollections/Node/_Node+Debugging.swift +++ b/Sources/PersistentCollections/Node/_Node+Debugging.swift @@ -67,6 +67,7 @@ extension _Node.UnsafeHandle { print(""" \(firstPrefix)\(isCollisionNode ? "CollisionNode" : "Node")(\ at: \(_addressString(for: _header)), \ + \(isCollisionNode ? "hash: \(collisionHash), " : "")\ \(extra)\ byteCapacity: \(byteCapacity), \ freeBytes: \(bytesFree)) diff --git a/Sources/PersistentCollections/Node/_Node+Initializers.swift b/Sources/PersistentCollections/Node/_Node+Initializers.swift index 0d8d99ea6..fc7cdfd2b 100644 --- a/Sources/PersistentCollections/Node/_Node+Initializers.swift +++ b/Sources/PersistentCollections/Node/_Node+Initializers.swift @@ -12,83 +12,132 @@ extension _Node { @inlinable internal static func _collisionNode( - _ item1: Element, + _ hash: _Hash, + _ item1: __owned Element, + _ item2: __owned Element + ) -> _Node { + let node = _Node.allocateCollision(count: 2, hash) { items in + items.initializeElement(at: 1, to: item1) + items.initializeElement(at: 0, to: item2) + }.node + node._invariantCheck() + return node + } + + @inlinable + internal static func _collisionNode( + _ hash: _Hash, + _ item1: __owned Element, _ inserter2: (UnsafeMutablePointer) -> Void ) -> _Node { - var node = _Node(storage: Storage.allocate(itemCapacity: 2), count: 2) - node.update { - $0.collisionCount = 2 - let byteCount = 2 * MemoryLayout.stride - assert($0.bytesFree >= byteCount) - $0.bytesFree &-= byteCount - let items = $0.reverseItems + let node = _Node.allocateCollision(count: 2, hash) { items in items.initializeElement(at: 1, to: item1) inserter2(items.baseAddress.unsafelyUnwrapped) - } + }.node node._invariantCheck() return node } @inlinable internal static func _regularNode( - _ item1: Element, + _ item: __owned Element, + _ bucket: _Bucket + ) -> _Node { + let r = _Node.allocate( + itemMap: _Bitmap(bucket), + childMap: .empty, + count: 1 + ) { children, items in + assert(items.count == 1 && children.count == 0) + items.initializeElement(at: 0, to: item) + } + r.node._invariantCheck() + return r.node + } + + @inlinable + internal static func _regularNode( + _ item1: __owned Element, + _ bucket1: _Bucket, + _ item2: __owned Element, + _ bucket2: _Bucket + ) -> _Node { + _regularNode( + item1, bucket1, + { $0.initialize(to: item2) }, bucket2).node + } + + @inlinable + internal static func _regularNode( + _ item1: __owned Element, _ bucket1: _Bucket, _ inserter2: (UnsafeMutablePointer) -> Void, _ bucket2: _Bucket ) -> (node: _Node, slot1: _Slot, slot2: _Slot) { assert(bucket1 != bucket2) - var node = _Node(storage: Storage.allocate(itemCapacity: 2), count: 2) - let (slot1, slot2) = node.update { - $0.itemMap.insert(bucket1) - $0.itemMap.insert(bucket2) - $0.bytesFree &-= 2 * MemoryLayout.stride + let r = _Node.allocate( + itemMap: _Bitmap(bucket1, bucket2), + childMap: .empty, + count: 2 + ) { children, items in + assert(items.count == 2 && children.count == 0) let i1 = bucket1 < bucket2 ? 1 : 0 let i2 = 1 &- i1 - let items = $0.reverseItems items.initializeElement(at: i1, to: item1) inserter2(items.baseAddress.unsafelyUnwrapped + i2) return (_Slot(i2), _Slot(i1)) // Note: swapped } - node._invariantCheck() - return (node, slot1, slot2) + r.node._invariantCheck() + return (r.node, r.result.0, r.result.1) } @inlinable internal static func _regularNode( - _ child: _Node, _ bucket: _Bucket + _ child: __owned _Node, _ bucket: _Bucket ) -> _Node { - var node = _Node( - storage: Storage.allocate(childCapacity: 1), - count: child.count) - node.update { - $0.childMap.insert(bucket) - $0.bytesFree &-= MemoryLayout<_Node>.stride - $0.childPtr(at: .zero).initialize(to: child) + let r = _Node.allocate( + itemMap: .empty, + childMap: _Bitmap(bucket), + count: child.count + ) { children, items in + assert(items.count == 0 && children.count == 1) + children.initializeElement(at: 0, to: child) } - node._invariantCheck() - return node + r.node._invariantCheck() + return r.node + } + + @inlinable + internal static func _regularNode( + _ item: __owned Element, + _ itemBucket: _Bucket, + _ child: __owned _Node, + _ childBucket: _Bucket + ) -> _Node { + _regularNode( + { $0.initialize(to: item) }, itemBucket, + child, childBucket) } @inlinable internal static func _regularNode( _ inserter: (UnsafeMutablePointer) -> Void, _ itemBucket: _Bucket, - _ child: _Node, + _ child: __owned _Node, _ childBucket: _Bucket ) -> _Node { assert(itemBucket != childBucket) - var node = _Node( - storage: Storage.allocate(itemCapacity: 1, childCapacity: 1), - count: child.count &+ 1) - node.update { - $0.itemMap.insert(itemBucket) - $0.childMap.insert(childBucket) - $0.bytesFree &-= MemoryLayout.stride + MemoryLayout<_Node>.stride - inserter($0.itemPtr(at: .zero)) - $0.childPtr(at: .zero).initialize(to: child) + let r = _Node.allocate( + itemMap: _Bitmap(itemBucket), + childMap: _Bitmap(childBucket), + count: child.count &+ 1 + ) { children, items in + assert(items.count == 1 && children.count == 1) + inserter(items.baseAddress.unsafelyUnwrapped) + children.initializeElement(at: 0, to: child) } - node._invariantCheck() - return node + r.node._invariantCheck() + return r.node } } @@ -96,13 +145,13 @@ extension _Node { @inlinable internal static func build( level: _Level, - item1: Element, + item1: __owned Element, _ hash1: _Hash, item2 inserter2: (UnsafeMutablePointer) -> Void, _ hash2: _Hash ) -> (top: _Node, leaf: _UnmanagedNode, slot1: _Slot, slot2: _Slot) { if hash1 == hash2 { - let top = _collisionNode(item1, inserter2) + let top = _collisionNode(hash1, item1, inserter2) return (top, top.unmanaged, _Slot(0), _Slot(1)) } let r = _build( @@ -113,7 +162,7 @@ extension _Node { @inlinable internal static func _build( level: _Level, - item1: Element, + item1: __owned Element, _ hash1: _Hash, item2 inserter2: (UnsafeMutablePointer) -> Void, _ hash2: _Hash @@ -137,7 +186,7 @@ extension _Node { level: _Level, item1 inserter1: (UnsafeMutablePointer) -> Void, _ hash1: _Hash, - child2: _Node, + child2: __owned _Node, _ hash2: _Hash ) -> (top: _Node, leaf: _UnmanagedNode, slot1: _Slot, slot2: _Slot) { assert(child2.isCollisionNode) diff --git a/Sources/PersistentCollections/Node/_Node+Invariants.swift b/Sources/PersistentCollections/Node/_Node+Invariants.swift index c9b18e5d8..af02f666f 100644 --- a/Sources/PersistentCollections/Node/_Node+Invariants.swift +++ b/Sources/PersistentCollections/Node/_Node+Invariants.swift @@ -34,8 +34,19 @@ extension _Node { raw.storage.header._invariantCheck() read { let itemBytes = $0.itemCount * MemoryLayout.stride - let childBytes = $0.childCount * MemoryLayout<_Node>.stride - assert(itemBytes + $0.bytesFree + childBytes == $0.byteCapacity) + + if $0.isCollisionNode { + let hashBytes = MemoryLayout<_Hash>.stride + assert($0.itemCount >= 1) + assert($0.childCount == 0) + assert(itemBytes + $0.bytesFree + hashBytes == $0.byteCapacity) + + assert($0.collisionHash == _Hash($0[item: .zero].key)) + } else { + let childBytes = $0.childCount * MemoryLayout<_Node>.stride + assert(itemBytes + $0.bytesFree + childBytes == $0.byteCapacity) + } + let actualCount = $0.children.reduce($0.itemCount, { $0 + $1.count }) assert(actualCount == self.count) } @@ -55,12 +66,11 @@ extension _Node { if $0.isCollisionNode { precondition(count == $0.itemCount) precondition(count > 0) - let key = $0.collisionHash - let hash = _Hash(key) + let hash = $0.collisionHash precondition( hash.isEqual(to: path, upTo: level), - "Misplaced colliding key '\(key)': \(path) isn't a prefix of \(hash)") - for item in $0.reverseItems.dropFirst() { + "Misplaced collision node: \(path) isn't a prefix of \(hash)") + for item in $0.reverseItems { precondition(_Hash(item.key) == hash) } } diff --git a/Sources/PersistentCollections/Node/_Node+Lookups.swift b/Sources/PersistentCollections/Node/_Node+Lookups.swift index c3c42445c..7185509b4 100644 --- a/Sources/PersistentCollections/Node/_Node+Lookups.swift +++ b/Sources/PersistentCollections/Node/_Node+Lookups.swift @@ -46,16 +46,15 @@ extension _Node.UnsafeHandle { @inlinable @inline(never) internal func _findInCollision( _ level: _Level, _ key: Key, _ hash: _Hash - ) -> (code: Int, slot: _Slot, expansionHash: _Hash) { + ) -> (code: Int, slot: _Slot) { assert(isCollisionNode) if !level.isAtBottom { - let h = self.collisionHash - if h != hash { return (2, .zero, h) } + if hash != self.collisionHash { return (2, .zero) } } // Note: this searches the items in reverse insertion order. guard let slot = reverseItems.firstIndex(where: { $0.key == key }) - else { return (1, self.itemsEndSlot, _Hash(_value: 0)) } - return (0, _Slot(itemCount &- 1 &- slot), _Hash(_value: 0)) + else { return (1, self.itemsEndSlot) } + return (0, _Slot(itemCount &- 1 &- slot)) } } @@ -77,15 +76,24 @@ internal enum _FindResult { /// If the current node is a collision node, then the bucket value is /// set to `_Bucket.invalid`. case found(_Bucket, _Slot) + /// The item we're looking for is not currently inside the subtree rooted at /// this node. /// /// If we wanted to insert it, then its correct slot is within this node /// at the specified bucket / item slot. (Which is currently empty.) /// - /// If the current node is a collision node, then the bucket value is - /// set to `_Bucket.invalid`. - case notFound(_Bucket, _Slot) + /// When the node is a collision node, the `insertCollision` case is returned + /// instead of this one. + case insert(_Bucket, _Slot) + + /// The item we're looking for is not currently inside the subtree rooted at + /// this collision node. + /// + /// If we wanted to insert it, then it needs to be appended to the items + /// buffer. + case appendCollision + /// The item we're looking for is not currently inside the subtree rooted at /// this node. /// @@ -95,7 +103,8 @@ internal enum _FindResult { /// it with a new child node. /// /// (This case is never returned if the current node is a collision node.) - case newCollision(_Bucket, _Slot) + case spawnChild(_Bucket, _Slot) + /// The item we're looking for is not in this subtree. /// /// However, the item doesn't belong in this subtree at all. This is an @@ -108,12 +117,9 @@ internal enum _FindResult { /// node further down the tree. (This undoes the compression by expanding /// the collision node's path, hence the name of the enum case.) /// - /// The payload of the expansion case is the shared hash value of all items - /// inside the current (collision) node -- this is needed to sort this node - /// into the proper bucket in any newly created parents. - /// /// (This case is never returned if the current node is a regular node.) - case expansion(_Hash) + case expansion + /// The item we're looking for is not directly stored in this node, but it /// might be somewhere in the subtree rooted at the child at the given /// bucket & slot. @@ -124,17 +130,17 @@ internal enum _FindResult { extension _Node { @inlinable - internal func find( - _ level: _Level, _ key: Key, _ hash: _Hash, forInsert: Bool + internal func findForInsertion( + _ level: _Level, _ key: Key, _ hash: _Hash ) -> _FindResult { - read { $0.find(level, key, hash, forInsert: forInsert) } + read { $0.findForInsertion(level, key, hash) } } } extension _Node.UnsafeHandle { @inlinable - internal func find( - _ level: _Level, _ key: Key, _ hash: _Hash, forInsert: Bool + internal func findForInsertion( + _ level: _Level, _ key: Key, _ hash: _Hash ) -> _FindResult { guard !isCollisionNode else { let r = _findInCollision(level, key, hash) @@ -142,10 +148,10 @@ extension _Node.UnsafeHandle { return .found(.invalid, r.slot) } if r.code == 1 { - return .notFound(.invalid, self.itemsEndSlot) + return .appendCollision } assert(r.code == 2) - return .expansion(r.expansionHash) + return .expansion } let bucket = hash[level] if itemMap.contains(bucket) { @@ -153,15 +159,14 @@ extension _Node.UnsafeHandle { if self[item: slot].key == key { return .found(bucket, slot) } - return .newCollision(bucket, slot) + return .spawnChild(bucket, slot) } if childMap.contains(bucket) { let slot = childMap.slot(of: bucket) return .descend(bucket, slot) } - // Don't calculate the slot unless the caller will need it. - let slot = forInsert ? itemMap.slot(of: bucket) : .zero - return .notFound(bucket, slot) + let slot = itemMap.slot(of: bucket) + return .insert(bucket, slot) } } @@ -170,16 +175,18 @@ extension _Node.UnsafeHandle { extension _Node { @inlinable internal func get(_ level: _Level, _ key: Key, _ hash: _Hash) -> Value? { - read { - let r = $0.find(level, key, hash, forInsert: false) - switch r { - case .found(_, let slot): - return $0[item: slot].value - case .notFound, .newCollision, .expansion: + var node = unmanaged + var level = level + while true { + let r = UnsafeHandle.read(node) { $0.find(level, key, hash) } + guard let r = r else { return nil - case .descend(_, let slot): - return $0[child: slot].get(level.descend(), key, hash) } + guard r.descend else { + return UnsafeHandle.read(node) { $0[item: r.slot].value } + } + node = node.unmanagedChild(at: r.slot) + level = level.descend() } } } @@ -189,23 +196,35 @@ extension _Node { internal func containsKey( _ level: _Level, _ key: Key, _ hash: _Hash ) -> Bool { - read { $0.containsKey(level, key, hash) } + var node = unmanaged + var level = level + while true { + let r = UnsafeHandle.read(node) { $0.find(level, key, hash) } + guard let r = r else { return false } + guard r.descend else { return true } + node = node.unmanagedChild(at: r.slot) + level = level.descend() + } } } -extension _Node.UnsafeHandle { +extension _Node { @inlinable - internal func containsKey( + internal func lookup( _ level: _Level, _ key: Key, _ hash: _Hash - ) -> Bool { - let r = find(level, key, hash, forInsert: false) - switch r { - case .found: - return true - case .notFound, .newCollision, .expansion: - return false - case .descend(_, let slot): - return self[child: slot].containsKey(level.descend(), key, hash) + ) -> (node: _UnmanagedNode, slot: _Slot)? { + var node = unmanaged + var level = level + while true { + let r = UnsafeHandle.read(node) { $0.find(level, key, hash) } + guard let r = r else { + return nil + } + guard r.descend else { + return (node, r.slot) + } + node = node.unmanagedChild(at: r.slot) + level = level.descend() } } } @@ -215,21 +234,15 @@ extension _Node { internal func position( forKey key: Key, _ level: _Level, _ hash: _Hash ) -> Int? { - let r = find(level, key, hash, forInsert: false) - switch r { - case .found(_, let slot): - return slot.value - case .notFound, .newCollision, .expansion: - return nil - case .descend(_, let slot): - return read { h in - let children = h.children - let p = children[slot.value] - .position(forKey: key, level.descend(), hash) - guard let p = p else { return nil } - let c = h.itemCount &+ p - return children[...alignment let childAlignment = MemoryLayout<_Node>.alignment @@ -86,16 +92,6 @@ extension _Node.Storage { } return unsafeDowncast(object, to: _Node.Storage.self) } - - @inlinable @inline(__always) - internal static func allocate( - itemCapacity: Int = 0, childCapacity: Int = 0 - ) -> _Node.Storage { - assert(itemCapacity >= 0 && childCapacity >= 0) - let itemBytes = itemCapacity * MemoryLayout.stride - let childBytes = childCapacity * MemoryLayout<_Node>.stride - return allocate(byteCapacity: itemBytes &+ childBytes) - } } extension _Node { @@ -110,13 +106,13 @@ extension _Node { } @inlinable @inline(__always) - internal static var spaceForNewCollision: Int { - Swift.max(0, MemoryLayout<_Node>.stride - MemoryLayout.stride) + internal static var spaceForSpawningChild: Int { + Swift.max(0, spaceForNewChild - spaceForNewItem) } @inlinable @inline(__always) internal static var spaceForInlinedChild: Int { - Swift.max(0, MemoryLayout.stride - MemoryLayout<_Node>.stride) + Swift.max(0, spaceForNewItem - spaceForNewChild) } @inlinable @@ -153,130 +149,129 @@ extension _Node { internal static func allocate( itemMap: _Bitmap, childMap: _Bitmap, count: Int, + extraBytes: Int = 0, initializingWith initializer: ( UnsafeMutableBufferPointer<_Node>, UnsafeMutableBufferPointer ) -> R ) -> (node: _Node, result: R) { - let (itemCount, childCount) = _StorageHeader.counts( - itemMap: itemMap, childMap: childMap) + assert(extraBytes >= 0) + assert(itemMap.isDisjoint(with: childMap)) // No collisions + let itemCount = itemMap.count + let childCount = childMap.count + + let itemStride = MemoryLayout.stride + let childStride = MemoryLayout<_Node>.stride + + let itemBytes = itemCount * itemStride + let childBytes = childCount * childStride + let occupiedBytes = itemBytes &+ childBytes let storage = Storage.allocate( - itemCapacity: itemCount, childCapacity: childCount) + byteCapacity: occupiedBytes &+ extraBytes) + var node = _Node(storage: storage, count: count) + let result = node.update { + $0.itemMap = itemMap + $0.childMap = childMap + + assert(occupiedBytes <= $0.bytesFree) + $0.bytesFree &-= occupiedBytes + + let childStart = $0._memory + .bindMemory(to: _Node.self, capacity: childCount) + let itemStart = ($0._memory + ($0.byteCapacity - itemBytes)) + .bindMemory(to: Element.self, capacity: itemCount) + + return initializer( + UnsafeMutableBufferPointer(start: childStart, count: childCount), + UnsafeMutableBufferPointer(start: itemStart, count: itemCount)) + } + return (node, result) + } + + @inlinable + internal static func allocateCollision( + count: Int, + _ hash: _Hash, + extraBytes: Int = 0, + initializingWith initializer: (UnsafeMutableBufferPointer) -> R + ) -> (node: _Node, result: R) { + assert(count >= 2) + assert(extraBytes >= 0) + let itemBytes = count * MemoryLayout.stride + let hashBytes = MemoryLayout<_Hash>.stride + let bytes = itemBytes &+ hashBytes + assert(MemoryLayout<_Hash>.alignment <= MemoryLayout<_RawNode>.alignment) + let storage = Storage.allocate(byteCapacity: bytes &+ extraBytes) var node = _Node(storage: storage, count: count) let result = node.update { - let (children, items) = $0._prepare( - itemMap: itemMap, - itemCount: itemCount, - childMap: childMap, - childCount: childCount - ) - return initializer(children, items) + $0.itemMap = _Bitmap(bitPattern: count) + $0.childMap = $0.itemMap + assert(bytes <= $0.bytesFree) + $0.bytesFree &-= bytes + + $0._memory.storeBytes(of: hash, as: _Hash.self) + + let itemStart = ($0._memory + ($0.byteCapacity &- itemBytes)) + .bindMemory(to: Element.self, capacity: count) + + let items = UnsafeMutableBufferPointer(start: itemStart, count: count) + return initializer(items) } return (node, result) } + @inlinable @inline(never) internal func copy(withFreeSpace space: Int = 0) -> _Node { assert(space >= 0) - let capacity = read { $0.byteCapacity &- $0.bytesFree &+ space } - var new = Self( - storage: Storage.allocate(byteCapacity: capacity), - count: count) - read { src in - new.update { dst in - let (dstChildren, dstItems) = dst._prepare( - itemMap: src.itemMap, childMap: src.childMap) + if isCollisionNode { + return read { src in + Self.allocateCollision( + count: self.count, self.collisionHash + ) { dstItems in + dstItems.initializeAll(fromContentsOf: src.reverseItems) + }.node + } + } + return read { src in + Self.allocate( + itemMap: src.itemMap, + childMap: src.childMap, + count: self.count, + extraBytes: space + ) { dstChildren, dstItems in dstChildren.initializeAll(fromContentsOf: src.children) dstItems.initializeAll(fromContentsOf: src.reverseItems) - } + }.node } - new._invariantCheck() - return new } @inlinable @inline(never) internal mutating func move(withFreeSpace space: Int = 0) { assert(space >= 0) - let capacity = read { $0.byteCapacity &- $0.bytesFree &+ space } - var new = Self( - storage: Storage.allocate(byteCapacity: capacity), - count: self.count) - self.update { src in - new.update { dst in - let (dstChildren, dstItems) = dst._prepare( - itemMap: src.itemMap, childMap: src.childMap) - + let c = self.count + if isCollisionNode { + self = update { src in + Self.allocateCollision( + count: c, src.collisionHash + ) { dstItems in + dstItems.moveInitializeAll(fromContentsOf: src.reverseItems) + src.clear() + }.node + } + return + } + self = update { src in + Self.allocate( + itemMap: src.itemMap, + childMap: src.childMap, + count: c, + extraBytes: space + ) { dstChildren, dstItems in dstChildren.moveInitializeAll(fromContentsOf: src.children) dstItems.moveInitializeAll(fromContentsOf: src.reverseItems) - src.clear() - assert(dst.bytesFree >= space) - } + }.node } - self.count = 0 - self._invariantCheck() - new._invariantCheck() - self = new } } - -extension _Node.UnsafeHandle { - @inlinable - internal func _prepare( - itemMap: _Bitmap, childMap: _Bitmap - ) -> ( - children: UnsafeMutableBufferPointer<_Node>, - items: UnsafeMutableBufferPointer - ) { - let (itemCount, childCount) = _StorageHeader.counts( - itemMap: itemMap, childMap: childMap) - return self._prepare( - itemMap: itemMap, - itemCount: itemCount, - childMap: childMap, - childCount: childCount) - } - - @inlinable - internal func _prepare( - itemMap: _Bitmap, - itemCount: Int, - childMap: _Bitmap, - childCount: Int - ) -> ( - children: UnsafeMutableBufferPointer<_Node>, - items: UnsafeMutableBufferPointer - ) { - assert(self.itemMap.isEmpty && self.childMap.isEmpty) - assert(self.byteCapacity == self.bytesFree) - - assert( - itemMap == childMap - || (itemMap.count == itemCount && childMap.count == childCount)) - - assert( - itemMap != childMap - || (itemCount == itemMap._value && childCount == 0)) - - self.itemMap = itemMap - self.childMap = childMap - - let itemStride = MemoryLayout.stride - let childStride = MemoryLayout<_Node>.stride - - let itemBytes = itemCount &* itemStride - let childBytes = childCount &* childStride - let occupiedBytes = itemBytes &+ childBytes - assert(occupiedBytes <= byteCapacity) - bytesFree = byteCapacity &- occupiedBytes - - let childStart = self._memory - .bindMemory(to: _Node.self, capacity: childCount) - let itemStart = (self._memory + (byteCapacity - itemBytes)) - .bindMemory(to: Element.self, capacity: itemCount) - return ( - .init(start: childStart, count: childCount), - .init(start: itemStart, count: itemCount)) - } -} - diff --git a/Sources/PersistentCollections/Node/_Node+Structural compactMapValues.swift b/Sources/PersistentCollections/Node/_Node+Structural compactMapValues.swift new file mode 100644 index 000000000..1cd0e6d32 --- /dev/null +++ b/Sources/PersistentCollections/Node/_Node+Structural compactMapValues.swift @@ -0,0 +1,49 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +extension _Node { + @inlinable + internal func compactMapValues( + _ level: _Level, + _ hashPrefix: _Hash, + _ transform: (Value) throws -> T? + ) rethrows -> _Node.Builder { + return try self.read { + var result: _Node.Builder = .empty + + if isCollisionNode { + let items = $0.reverseItems + for i in items.indices { + if let v = try transform(items[i].value) { + result.addNewCollision((items[i].key, v), $0.collisionHash) + } + } + return result + } + + for (bucket, slot) in $0.itemMap { + let p = $0.itemPtr(at: slot) + if let v = try transform(p.pointee.value) { + let h = hashPrefix.appending(bucket, at: level) + result.addNewItem(level, (p.pointee.key, v), h) + } + } + + for (bucket, slot) in $0.childMap { + let h = hashPrefix.appending(bucket, at: level) + let branch = try $0[child: slot].compactMapValues( + level.descend(), h, transform) + result.addNewChildBranch(level, branch) + } + return result + } + } +} diff --git a/Sources/PersistentCollections/Node/_Node+Structural filter.swift b/Sources/PersistentCollections/Node/_Node+Structural filter.swift new file mode 100644 index 000000000..2b79e0982 --- /dev/null +++ b/Sources/PersistentCollections/Node/_Node+Structural filter.swift @@ -0,0 +1,49 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +extension _Node { + @inlinable + internal func filter( + _ level: _Level, + _ hashPrefix: _Hash, + _ isIncluded: (Element) throws -> Bool + ) rethrows -> Builder { + // FIXME: Consider preserving `self` when nothing needs to be removed. + return try self.read { + var result: Builder = .empty + + if isCollisionNode { + let items = $0.reverseItems + for i in items.indices { + if try isIncluded(items[i]) { + result.addNewCollision(items[i], $0.collisionHash) + } + } + return result + } + + for (bucket, slot) in $0.itemMap { + let p = $0.itemPtr(at: slot) + if try isIncluded(p.pointee) { + let h = hashPrefix.appending(bucket, at: level) + result.addNewItem(level, p.pointee, h) + } + } + + for (bucket, slot) in $0.childMap { + let h = hashPrefix.appending(bucket, at: level) + let branch = try $0[child: slot].filter(level.descend(), h, isIncluded) + result.addNewChildBranch(level, branch) + } + return result + } + } +} diff --git a/Sources/PersistentCollections/Node/_Node+Structural intersection.swift b/Sources/PersistentCollections/Node/_Node+Structural intersection.swift new file mode 100644 index 000000000..6cd331d9b --- /dev/null +++ b/Sources/PersistentCollections/Node/_Node+Structural intersection.swift @@ -0,0 +1,156 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +extension _Node { + @inlinable + internal func intersection( + _ level: _Level, + _ hashPrefix: _Hash, + _ other: _Node + ) -> Builder { + // FIXME: Consider preserving `self` when nothing needs to be removed. + if self.raw.storage === other.raw.storage { return .node(self, hashPrefix) } + + if self.isCollisionNode || other.isCollisionNode { + return _intersection_slow(level, hashPrefix, other) + } + + return self.read { l in + other.read { r in + var result: Builder = .empty + for (bucket, _) in l.itemMap.intersection(r.itemMap) { + let lslot = l.itemMap.slot(of: bucket) + let rslot = r.itemMap.slot(of: bucket) + let lp = l.itemPtr(at: lslot) + if lp.pointee.key == r[item: rslot].key { + let hashPrefix = hashPrefix.appending(bucket, at: level) + result.addNewItem(level, lp.pointee, hashPrefix) + } + } + + for (bucket, _) in l.itemMap.intersection(r.childMap) { + let lslot = l.itemMap.slot(of: bucket) + let rslot = r.childMap.slot(of: bucket) + let lp = l.itemPtr(at: lslot) + let h = _Hash(lp.pointee.key) + if r[child: rslot].containsKey(level.descend(), lp.pointee.key, h) { + let hashPrefix = hashPrefix.appending(bucket, at: level) + result.addNewItem(level, lp.pointee, hashPrefix) + } + } + + for (bucket, _) in l.childMap.intersection(r.itemMap) { + let lslot = l.childMap.slot(of: bucket) + let rslot = r.itemMap.slot(of: bucket) + let rp = r.itemPtr(at: rslot) + let h = _Hash(rp.pointee.key) + let res = l[child: lslot].lookup(level.descend(), rp.pointee.key, h) + if let res = res { + UnsafeHandle.read(res.node) { + let p = $0.itemPtr(at: res.slot) + result.addNewItem(level, p.pointee, h) + } + } + } + + for (bucket, _) in l.childMap.intersection(r.childMap) { + let lslot = l.childMap.slot(of: bucket) + let rslot = r.childMap.slot(of: bucket) + let branch = l[child: lslot].intersection( + level.descend(), + hashPrefix.appending(bucket, at: level), + r[child: rslot]) + result.addNewChildBranch(level, branch) + } + return result + } + } + } + + @inlinable @inline(never) + internal func _intersection_slow( + _ level: _Level, + _ hashPrefix: _Hash, + _ other: _Node + ) -> Builder { + let lc = self.isCollisionNode + let rc = other.isCollisionNode + if lc && rc { + return read { l in + other.read { r in + guard l.collisionHash == r.collisionHash else { return .empty } + var result: Builder = .empty + let litems = l.reverseItems + let ritems = r.reverseItems + for i in litems.indices { + if ritems.contains(where: { $0.key == litems[i].key }) { + result.addNewCollision(litems[i], l.collisionHash) + } + } + return result + } + } + } + + // One of the nodes must be on a compressed path. + assert(!level.isAtBottom) + + if lc { + // `self` is a collision node on a compressed path. The other tree might + // have the same set of collisions, just expanded a bit deeper. + return read { l in + other.read { r in + let bucket = l.collisionHash[level] + if r.itemMap.contains(bucket) { + let rslot = r.itemMap.slot(of: bucket) + let ritem = r.itemPtr(at: rslot) + let litems = l.reverseItems + let i = litems.firstIndex { $0.key == ritem.pointee.key } + guard let i = i else { return .empty } + return .item(litems[i], l.collisionHash) + } + if r.childMap.contains(bucket) { + let rslot = r.childMap.slot(of: bucket) + return intersection( + level.descend(), + hashPrefix.appending(bucket, at: level), + r[child: rslot]) + } + return .empty + } + } + } + + assert(rc) + // `other` is a collision node on a compressed path. + return read { l in + other.read { r in + let bucket = r.collisionHash[level] + if l.itemMap.contains(bucket) { + let lslot = l.itemMap.slot(of: bucket) + let litem = l.itemPtr(at: lslot) + let ritems = r.reverseItems + let found = ritems.contains { $0.key == litem.pointee.key } + guard found else { return .empty } + return .item(litem.pointee, r.collisionHash) + } + if l.childMap.contains(bucket) { + let lslot = l.itemMap.slot(of: bucket) + return intersection( + level.descend(), + hashPrefix.appending(bucket, at: level), + l[child: lslot]) + } + return .empty + } + } + } +} diff --git a/Sources/PersistentCollections/Node/_Node+isDisjoint.swift b/Sources/PersistentCollections/Node/_Node+Structural isDisjoint.swift similarity index 73% rename from Sources/PersistentCollections/Node/_Node+isDisjoint.swift rename to Sources/PersistentCollections/Node/_Node+Structural isDisjoint.swift index 3e1abb92b..eee101b0b 100644 --- a/Sources/PersistentCollections/Node/_Node+isDisjoint.swift +++ b/Sources/PersistentCollections/Node/_Node+Structural isDisjoint.swift @@ -32,12 +32,12 @@ extension _Node { let rmap = r.itemMap.union(r.childMap) if lmap.isDisjoint(with: rmap) { return true } - for bucket in l.itemMap.intersection(r.itemMap) { + for (bucket, _) in l.itemMap.intersection(r.itemMap) { let lslot = l.itemMap.slot(of: bucket) let rslot = r.itemMap.slot(of: bucket) guard l[item: lslot].key != r[item: rslot].key else { return false } } - for bucket in l.itemMap.intersection(r.childMap) { + for (bucket, _) in l.itemMap.intersection(r.childMap) { let lslot = l.itemMap.slot(of: bucket) let hash = _Hash(l[item: lslot].key) let rslot = r.childMap.slot(of: bucket) @@ -47,7 +47,7 @@ extension _Node { hash) if found { return false } } - for bucket in l.childMap.intersection(r.itemMap) { + for (bucket, _) in l.childMap.intersection(r.itemMap) { let lslot = l.childMap.slot(of: bucket) let rslot = r.itemMap.slot(of: bucket) let hash = _Hash(r[item: rslot].key) @@ -57,7 +57,7 @@ extension _Node { hash) if found { return false } } - for bucket in l.childMap.intersection(r.childMap) { + for (bucket, _) in l.childMap.intersection(r.childMap) { let lslot = l.childMap.slot(of: bucket) let rslot = r.childMap.slot(of: bucket) guard @@ -74,14 +74,11 @@ extension _Node { _ level: _Level, with other: _Node ) -> Bool { - // Beware, self might be on a compressed path assert(isCollisionNode) if other.isCollisionNode { return read { l in other.read { r in - let lh = l.collisionHash - let rh = r.collisionHash - guard lh == rh else { return true } + guard l.collisionHash == r.collisionHash else { return true } let litems = l.reverseItems let ritems = r.reverseItems return litems.allSatisfy { li in @@ -90,12 +87,24 @@ extension _Node { } } } - return read { - let items = $0.reverseItems - let hash = $0.collisionHash - return items.indices.allSatisfy { - !other.containsKey(level, items[$0].key, hash) + // `self` is on a compressed path. Try descending down by one level. + assert(!level.isAtBottom) + let bucket = self.collisionHash[level] + return other.read { r in + if r.childMap.contains(bucket) { + let slot = r.childMap.slot(of: bucket) + return isDisjoint(level.descend(), with: r[child: slot]) } + if r.itemMap.contains(bucket) { + let slot = r.itemMap.slot(of: bucket) + let p = r.itemPtr(at: slot) + let hash = _Hash(p.pointee.key) + return read { l in + guard hash == l.collisionHash else { return true } + return !l.reverseItems.contains { $0.key == p.pointee.key } + } + } + return true } } } diff --git a/Sources/PersistentCollections/Node/_Node+isEqual.swift b/Sources/PersistentCollections/Node/_Node+Structural isEqual.swift similarity index 100% rename from Sources/PersistentCollections/Node/_Node+isEqual.swift rename to Sources/PersistentCollections/Node/_Node+Structural isEqual.swift diff --git a/Sources/PersistentCollections/Node/_Node+isSubset.swift b/Sources/PersistentCollections/Node/_Node+Structural isSubset.swift similarity index 69% rename from Sources/PersistentCollections/Node/_Node+isSubset.swift rename to Sources/PersistentCollections/Node/_Node+Structural isSubset.swift index 9a1a3eafc..de3f3d57d 100644 --- a/Sources/PersistentCollections/Node/_Node+isSubset.swift +++ b/Sources/PersistentCollections/Node/_Node+Structural isSubset.swift @@ -21,15 +21,26 @@ extension _Node { guard self.count <= other.count else { return false } if self.isCollisionNode { - // Beware, self might be on a compressed path - return read { - let items = $0.reverseItems - let hash = $0.collisionHash - return items.indices.allSatisfy { - // FIXME: This will repeatedly & unnecessarily call `collisionHash` - other.containsKey(level, items[$0].key, hash) + if other.isCollisionNode { + guard self.collisionHash == other.collisionHash else { return false } + return read { l in + other.read { r in + let li = l.reverseItems + let ri = r.reverseItems + return l.reverseItems.indices.allSatisfy { i in + ri.contains { $0.key == li[i].key } + } + } } } + // `self` is on a compressed path. Try to descend down by one level. + assert(!level.isAtBottom) + let bucket = self.collisionHash[level] + return other.read { + guard $0.childMap.contains(bucket) else { return false } + let slot = $0.childMap.slot(of: bucket) + return self.isSubset(level.descend(), of: $0[child: slot]) + } } guard !other.isCollisionNode else { return false } @@ -40,13 +51,11 @@ extension _Node { guard l.itemMap.isSubset(of: r.itemMap.union(r.childMap)) else { return false } - for bucket in l.itemMap { + for (bucket, lslot) in l.itemMap { if r.itemMap.contains(bucket) { - let lslot = l.itemMap.slot(of: bucket) let rslot = r.itemMap.slot(of: bucket) guard l[item: lslot].key == r[item: rslot].key else { return false } } else { - let lslot = l.itemMap.slot(of: bucket) let hash = _Hash(l[item: lslot].key) let rslot = r.childMap.slot(of: bucket) guard @@ -58,8 +67,7 @@ extension _Node { } } - for bucket in l.childMap { - let lslot = l.childMap.slot(of: bucket) + for (bucket, lslot) in l.childMap { let rslot = r.childMap.slot(of: bucket) guard l[child: lslot].isSubset(level.descend(), of: r[child: rslot]) else { return false } diff --git a/Sources/PersistentCollections/Node/_Node+Transform.swift b/Sources/PersistentCollections/Node/_Node+Structural mapValues.swift similarity index 75% rename from Sources/PersistentCollections/Node/_Node+Transform.swift rename to Sources/PersistentCollections/Node/_Node+Structural mapValues.swift index ac838d7bb..e6970df55 100644 --- a/Sources/PersistentCollections/Node/_Node+Transform.swift +++ b/Sources/PersistentCollections/Node/_Node+Structural mapValues.swift @@ -14,16 +14,24 @@ import _CollectionsUtilities extension _Node { @inlinable internal func mapValues( - _ transform: (Value) throws -> T + _ transform: (Element) throws -> T ) rethrows -> _Node { let c = self.count return try read { source in - var result = _Node.allocate( - itemMap: source.itemMap, - childMap: source.childMap, - count: c, - initializingWith: { _, _ in } - ).node + var result: _Node + if isCollisionNode { + result = _Node.allocateCollision( + count: c, source.collisionHash, + initializingWith: { _ in } + ).node + } else { + result = _Node.allocate( + itemMap: source.itemMap, + childMap: source.childMap, + count: c, + initializingWith: { _, _ in } + ).node + } try result.update { target in let sourceItems = source.reverseItems let targetItems = target.reverseItems @@ -48,7 +56,7 @@ extension _Node { while i < targetItems.count { let key = sourceItems[i].key - let value = try transform(sourceItems[i].value) + let value = try transform(sourceItems[i]) targetItems.initializeElement(at: i, to: (key, value)) i += 1 } @@ -59,6 +67,7 @@ extension _Node { } success = true } + result._invariantCheck() return result } } diff --git a/Sources/PersistentCollections/Node/_Node+Structural subtracting.swift b/Sources/PersistentCollections/Node/_Node+Structural subtracting.swift new file mode 100644 index 000000000..2d72b4731 --- /dev/null +++ b/Sources/PersistentCollections/Node/_Node+Structural subtracting.swift @@ -0,0 +1,163 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +extension _Node { + @inlinable + internal func subtracting( + _ level: _Level, + _ hashPrefix: _Hash, + _ other: _Node + ) -> Builder { + // FIXME: Consider preserving `self` when nothing needs to be removed. + if self.raw.storage === other.raw.storage { return .empty } + + if self.isCollisionNode || other.isCollisionNode { + return _subtracting_slow(level, hashPrefix, other) + } + + return self.read { l in + other.read { r in + var result: Builder = .empty + for (bucket, _) in l.itemMap.intersection(r.itemMap) { + let lslot = l.itemMap.slot(of: bucket) + let rslot = r.itemMap.slot(of: bucket) + let lp = l.itemPtr(at: lslot) + if lp.pointee.key != r[item: rslot].key { + let hashPrefix = hashPrefix.appending(bucket, at: level) + result.addNewItem(level, lp.pointee, hashPrefix) + } + } + + for (bucket, _) in l.itemMap.intersection(r.childMap) { + let lslot = l.itemMap.slot(of: bucket) + let rslot = r.childMap.slot(of: bucket) + let lp = l.itemPtr(at: lslot) + let h = _Hash(lp.pointee.key) + if !r[child: rslot].containsKey(level.descend(), lp.pointee.key, h) { + let hashPrefix = hashPrefix.appending(bucket, at: level) + result.addNewItem(level, lp.pointee, hashPrefix) + } + } + + for (bucket, _) in l.childMap.intersection(r.itemMap) { + let lslot = l.childMap.slot(of: bucket) + let rslot = r.itemMap.slot(of: bucket) + let rp = r.itemPtr(at: rslot) + let h = _Hash(rp.pointee.key) + let node = ( + l[child: lslot] + .removing(level.descend(), rp.pointee.key, h)?.replacement + ?? l[child: lslot]) + let branch = Builder( + level.descend(), node, hashPrefix.appending(bucket, at: level)) + result.addNewChildBranch(level, branch) + } + + for (bucket, _) in l.childMap.intersection(r.childMap) { + let lslot = l.childMap.slot(of: bucket) + let rslot = r.childMap.slot(of: bucket) + let branch = l[child: lslot].subtracting( + level.descend(), + hashPrefix.appending(bucket, at: level), + r[child: rslot]) + result.addNewChildBranch(level, branch) + } + return result + } + } + } + + @inlinable @inline(never) + internal func _subtracting_slow( + _ level: _Level, + _ hashPrefix: _Hash, + _ other: _Node + ) -> Builder { + let lc = self.isCollisionNode + let rc = other.isCollisionNode + if lc && rc { + return read { l in + other.read { r in + guard l.collisionHash == r.collisionHash else { return .empty } + var result: Builder = .empty + let litems = l.reverseItems + let ritems = r.reverseItems + for i in litems.indices { + if !ritems.contains(where: { $0.key == litems[i].key }) { + result.addNewCollision(litems[i], l.collisionHash) + } + } + return result + } + } + } + + // One of the nodes must be on a compressed path. + assert(!level.isAtBottom) + + if lc { + // `self` is a collision node on a compressed path. The other tree might + // have the same set of collisions, just expanded a bit deeper. + return read { l in + other.read { r in + let bucket = l.collisionHash[level] + if r.itemMap.contains(bucket) { + let rslot = r.itemMap.slot(of: bucket) + let ritem = r.itemPtr(at: rslot) + let res = l.find(level, ritem.pointee.key, _Hash(ritem.pointee.key)) + guard let res = res else { + return .node(self, l.collisionHash) + } + assert(!res.descend) + var node = self.copy() + node.removeItem(at: res.slot, .invalid) { + _ = $0.deinitialize(count: 1) + } + return Builder(level, node, l.collisionHash) + } + if r.childMap.contains(bucket) { + let rslot = r.childMap.slot(of: bucket) + return subtracting( + level.descend(), + hashPrefix.appending(bucket, at: level), + r[child: rslot]) + } + return .empty + } + } + } + + assert(rc) + // `other` is a collision node on a compressed path. + return read { l in + other.read { r in + let bucket = r.collisionHash[level] + if l.itemMap.contains(bucket) { + let lslot = l.itemMap.slot(of: bucket) + let litem = l.itemPtr(at: lslot) + let h = _Hash(litem.pointee.key) + let res = r.find(level, litem.pointee.key, h) + if res != nil { return .empty } + return .item(litem.pointee, h) + } + if l.childMap.contains(bucket) { + let lslot = l.itemMap.slot(of: bucket) + return subtracting( + level.descend(), + hashPrefix.appending(bucket, at: level), + l[child: lslot]) + } + return .empty + } + } + } + +} diff --git a/Sources/PersistentCollections/Node/_Node+Subtree Insertions.swift b/Sources/PersistentCollections/Node/_Node+Subtree Insertions.swift index 6440f1fc0..0cc49f9d2 100644 --- a/Sources/PersistentCollections/Node/_Node+Subtree Insertions.swift +++ b/Sources/PersistentCollections/Node/_Node+Subtree Insertions.swift @@ -35,16 +35,19 @@ extension _Node { ) -> (inserted: Bool, leaf: _UnmanagedNode, slot: _Slot) { defer { _invariantCheck() } let isUnique = self.isUnique() - let r = find(level, key, hash, forInsert: true) + let r = findForInsertion(level, key, hash) switch r { case .found(_, let slot): ensureUnique(isUnique: isUnique) return (false, unmanaged, slot) - case .notFound(let bucket, let slot): + case .insert(let bucket, let slot): ensureUniqueAndInsertItem(isUnique: isUnique, slot, bucket) { _ in } return (true, unmanaged, slot) - case .newCollision(let bucket, let slot): - let r = ensureUniqueAndMakeNewCollision( + case .appendCollision: + ensureUniqueAndAppendCollision(isUnique: isUnique) { _ in } + return (true, unmanaged, _Slot(self.count &- 1)) + case .spawnChild(let bucket, let slot): + let r = ensureUniqueAndSpawnChild( isUnique: isUnique, level: level, replacing: slot, bucket, @@ -52,12 +55,11 @@ extension _Node { inserter: { _ in } ) return (true, r.leaf, r.slot) - case .expansion(let collisionHash): + case .expansion: let r = _Node.build( level: level, item1: { _ in }, hash, - child2: self, collisionHash - ) + child2: self, self.collisionHash) self = r.top return (true, r.leaf, r.slot1) case .descend(_, let slot): @@ -69,111 +71,21 @@ extension _Node { return r } } +} - #if false - @inlinable - internal mutating func updateValue( - _ value: __owned Value, - forKey key: Key, - _ level: _Level, - _ hash: _Hash - ) -> Value? { - defer { _invariantCheck() } - let isUnique = self.isUnique() - let r = find(level, key, hash, forInsert: true) - switch r { - case .found(_, let slot): - ensureUnique(isUnique: isUnique) - return update { - let p = $0.itemPtr(at: slot) - let old = p.pointee.value - p.pointee.value = value - return old - } - case .notFound(let bucket, let slot): - ensureUniqueAndInsertItem(isUnique: isUnique, slot, bucket) { - $0.initialize(to: (key, value)) - } - return nil - case .newCollision(let bucket, let slot): - _ = ensureUniqueAndMakeNewCollision( - isUnique: isUnique, - level: level, - replacing: slot, bucket, - newHash: hash - ) { - $0.initialize(to: (key, value)) - } - return nil - case .expansion(let collisionHash): - self = _Node.build( - level: level, - item1: { $0.initialize(to: (key, value)) }, hash, - child2: self, collisionHash - ).top - return nil - case .descend(_, let slot): - ensureUnique(isUnique: isUnique) - let old = update { - $0[child: slot].updateValue(value, forKey: key, level.descend(), hash) - } - if old == nil { count &+= 1 } - return old - } - } - +extension _Node { @inlinable - internal mutating func insertValue( - forKey key: Key, - _ level: _Level, - _ hash: _Hash, - with value: () -> Value - ) -> (inserted: Bool, leaf: _UnmanagedNode, slot: _Slot) { - defer { _invariantCheck() } - let isUnique = self.isUnique() - let r = find(level, key, hash, forInsert: true) - switch r { - case .found(_, let slot): - ensureUnique(isUnique: isUnique) - _invariantCheck() // FIXME - return (false, unmanaged, slot) - case .notFound(let bucket, let slot): - ensureUniqueAndInsertItem(isUnique: isUnique, slot, bucket) { - $0.initialize(to: (key, value())) - } - return (true, unmanaged, slot) - case .newCollision(let bucket, let slot): - let r = ensureUniqueAndMakeNewCollision( - isUnique: isUnique, - level: level, - replacing: slot, bucket, - newHash: hash - ) { - $0.initialize(to: (key, value())) - } - return (true, r.leaf, r.slot) - case .expansion(let collisionHash): - let r = _Node.build( - level: level, - item1: { $0.initialize(to: (key, value())) }, hash, - child2: self, collisionHash - ) - self = r.top - return (true, r.leaf, r.slot1) - case .descend(_, let slot): - ensureUnique(isUnique: isUnique) - let r = update { - $0[child: slot] - .insertValue(forKey: key, level.descend(), hash, with: value) - } - if r.inserted { count &+= 1 } - return r + internal mutating func ensureUniqueAndInsertItem( + isUnique: Bool, + _ item: Element, + _ bucket: _Bucket + ) { + let slot = self.read { $0.itemMap.slot(of: bucket) } + ensureUniqueAndInsertItem(isUnique: isUnique, slot, bucket) { + $0.initialize(to: item) } } - #endif -} -extension _Node { @inlinable internal mutating func ensureUniqueAndInsertItem( isUnique: Bool, @@ -181,6 +93,8 @@ extension _Node { _ bucket: _Bucket, inserter: (UnsafeMutablePointer) -> Void ) { + assert(!isCollisionNode) + if !isUnique { self = copyNodeAndInsertItem(at: slot, bucket, inserter: inserter) return @@ -203,7 +117,7 @@ extension _Node { _ bucket: _Bucket, inserter: (UnsafeMutablePointer) -> Void ) -> _Node { - // Copy items into new storage. + assert(!isCollisionNode) let (itemMap, childMap) = ( self.raw.storage.header.bitmapsForInsertingItem(at: slot, bucket)) let c = self.count @@ -232,7 +146,7 @@ extension _Node { _ bucket: _Bucket, inserter: (UnsafeMutablePointer) -> Void ) { - // Copy items into new storage. + assert(!isCollisionNode) let (itemMap, childMap) = ( self.raw.storage.header.bitmapsForInsertingItem(at: slot, bucket)) let c = self.count @@ -260,7 +174,77 @@ extension _Node { extension _Node { @inlinable - internal mutating func ensureUniqueAndMakeNewCollision( + internal mutating func ensureUniqueAndAppendCollision( + isUnique: Bool, + _ item: Element + ) { + ensureUniqueAndAppendCollision(isUnique: isUnique) { + $0.initialize(to: item) + } + } + + @inlinable + internal mutating func ensureUniqueAndAppendCollision( + isUnique: Bool, + inserter: (UnsafeMutablePointer) -> Void + ) { + assert(isCollisionNode) + if !isUnique { + self = copyNodeAndAppendCollision(inserter: inserter) + return + } + if !hasFreeSpace(Self.spaceForNewItem) { + moveNodeAndAppendCollision(inserter: inserter) + return + } + // In-place insert. + update { + let p = $0._makeRoomForNewItem(at: $0.itemsEndSlot, .invalid) + inserter(p) + } + self.count &+= 1 + } + + @inlinable @inline(never) + internal func copyNodeAndAppendCollision( + inserter: (UnsafeMutablePointer) -> Void + ) -> _Node { + assert(isCollisionNode) + assert(self.count == read { $0.collisionCount }) + let c = self.count + return read { src in + Self.allocateCollision(count: c &+ 1, src.collisionHash) { dstItems in + let srcItems = src.reverseItems + assert(dstItems.count == srcItems.count + 1) + dstItems.dropFirst().initializeAll(fromContentsOf: srcItems) + inserter(dstItems.baseAddress!) + }.node + } + } + + @inlinable @inline(never) + internal mutating func moveNodeAndAppendCollision( + inserter: (UnsafeMutablePointer) -> Void + ) { + assert(isCollisionNode) + assert(self.count == read { $0.collisionCount }) + let c = self.count + self = update { src in + Self.allocateCollision(count: c &+ 1, src.collisionHash) { dstItems in + let srcItems = src.reverseItems + assert(dstItems.count == srcItems.count + 1) + dstItems.dropFirst().moveInitializeAll(fromContentsOf: srcItems) + inserter(dstItems.baseAddress!) + + src.clear() + }.node + } + } +} + +extension _Node { + @inlinable + internal mutating func ensureUniqueAndSpawnChild( isUnique: Bool, level: _Level, replacing slot: _Slot, @@ -271,18 +255,12 @@ extension _Node { let existingHash = read { _Hash($0[item: slot].key) } if newHash == existingHash, hasSingletonItem { // Convert current node to a collision node. - ensureUnique(isUnique: isUnique, withFreeSpace: Self.spaceForNewItem) - count &+= 1 - update { - $0.collisionCount = 1 - let p = $0._makeRoomForNewItem(at: _Slot(1), .invalid) - inserter(p) - } + self = _Node._collisionNode(newHash, read { $0[item: .zero] }, inserter) return (unmanaged, _Slot(1)) } if !isUnique { - let r = copyNodeAndMakeNewCollision( + let r = copyNodeAndSpawnChild( level: level, replacing: slot, bucket, existingHash: existingHash, @@ -291,8 +269,8 @@ extension _Node { self = r.node return (r.leaf, r.slot) } - if !hasFreeSpace(Self.spaceForNewCollision) { - return moveNodeAndMakeNewCollision( + if !hasFreeSpace(Self.spaceForSpawningChild) { + return moveNodeAndSpawnChild( level: level, replacing: slot, bucket, existingHash: existingHash, @@ -310,7 +288,7 @@ extension _Node { } @inlinable @inline(never) - internal func copyNodeAndMakeNewCollision( + internal func copyNodeAndSpawnChild( level: _Level, replacing slot: _Slot, _ bucket: _Bucket, @@ -319,7 +297,7 @@ extension _Node { inserter: (UnsafeMutablePointer) -> Void ) -> (node: _Node, leaf: _UnmanagedNode, slot: _Slot) { let (itemMap, childMap) = ( - self.raw.storage.header.bitmapsForNewCollision(at: slot, bucket)) + self.raw.storage.header.bitmapsForSpawningChild(at: slot, bucket)) let c = self.count let childSlot = childMap.slot(of: bucket) let r = read { src in @@ -328,6 +306,7 @@ extension _Node { ) { dstChildren, dstItems in let srcChildren = src.children let srcItems = src.reverseItems + let i = srcItems.count &- 1 &- slot.value // Initialize children. dstChildren.prefix(childSlot.value) @@ -338,14 +317,13 @@ extension _Node { let r = _Node.build( level: level.descend(), - item1: srcItems[slot.value], existingHash, + item1: srcItems[i], existingHash, item2: inserter, newHash) dstChildren.initializeElement(at: childSlot.value, to: r.top) // Initialize items. - dstItems.prefix(slot.value) - .initializeAll(fromContentsOf: srcItems.prefix(slot.value)) - let rest2 = dstItems.count &- slot.value + dstItems.prefix(i).initializeAll(fromContentsOf: srcItems.prefix(i)) + let rest2 = dstItems.count &- i dstItems.suffix(rest2) .initializeAll(fromContentsOf: srcItems.suffix(rest2)) @@ -356,7 +334,7 @@ extension _Node { } @inlinable @inline(never) - internal mutating func moveNodeAndMakeNewCollision( + internal mutating func moveNodeAndSpawnChild( level: _Level, replacing slot: _Slot, _ bucket: _Bucket, @@ -365,7 +343,7 @@ extension _Node { inserter: (UnsafeMutablePointer) -> Void ) -> (leaf: _UnmanagedNode, slot: _Slot) { let (itemMap, childMap) = ( - self.raw.storage.header.bitmapsForNewCollision(at: slot, bucket)) + self.raw.storage.header.bitmapsForSpawningChild(at: slot, bucket)) let c = self.count let childSlot = childMap.slot(of: bucket) let r = update { src in @@ -374,6 +352,7 @@ extension _Node { ) { dstChildren, dstItems in let srcChildren = src.children let srcItems = src.reverseItems + let i = srcItems.count &- 1 &- slot.value // Initialize children. dstChildren.prefix(childSlot.value) @@ -384,14 +363,13 @@ extension _Node { let r = _Node.build( level: level.descend(), - item1: srcItems.moveElement(from: slot.value), existingHash, + item1: srcItems.moveElement(from: i), existingHash, item2: inserter, newHash) dstChildren.initializeElement(at: childSlot.value, to: r.top) // Initialize items. - dstItems.prefix(slot.value) - .moveInitializeAll(fromContentsOf: srcItems.prefix(slot.value)) - let rest2 = dstItems.count &- slot.value + dstItems.prefix(i).moveInitializeAll(fromContentsOf: srcItems.prefix(i)) + let rest2 = dstItems.count &- i dstItems.suffix(rest2) .moveInitializeAll(fromContentsOf: srcItems.suffix(rest2)) diff --git a/Sources/PersistentCollections/Node/_Node+Subtree Modify.swift b/Sources/PersistentCollections/Node/_Node+Subtree Modify.swift index 683bc5a38..a747d675a 100644 --- a/Sources/PersistentCollections/Node/_Node+Subtree Modify.swift +++ b/Sources/PersistentCollections/Node/_Node+Subtree Modify.swift @@ -79,7 +79,7 @@ extension _Node { // If the key already exists, we ensure uniqueness for its node and extract // its item but otherwise leave the tree as it was. let isUnique = self.isUnique() - let r = find(state.path.level, state.key, state.hash, forInsert: true) + let r = findForInsertion(state.path.level, state.key, state.hash) switch r { case .found(_, let slot): ensureUnique(isUnique: isUnique) @@ -88,20 +88,26 @@ extension _Node { state.found = true (state.key, state.value) = update { $0.itemPtr(at: slot).move() } - case .notFound(_, let slot): + + case .insert(_, let slot): state.path.selectItem(at: slot) - case .newCollision(_, let slot): + case .appendCollision: + state.path.selectItem(at: _Slot(self.count)) + + case .spawnChild(_, let slot): state.path.selectItem(at: slot) - case .expansion(_): + case .expansion: state.path.selectEnd() case .descend(_, let slot): ensureUnique(isUnique: isUnique) - state.path.selectChild(at: slot) - state.path.descend() - update { $0[child: slot]._prepareValueUpdate(&state) } + update { + let p = $0.childPtr(at: slot) + state.path.descendToChild(p.pointee.unmanaged, at: slot) + p.pointee._prepareValueUpdate(&state) + } } } @@ -189,7 +195,7 @@ extension _Node { _ hash: _Hash ) -> DefaultedValueUpdateState { let isUnique = self.isUnique() - let r = find(level, key, hash, forInsert: true) + let r = findForInsertion(level, key, hash) switch r { case .found(_, let slot): ensureUnique(isUnique: isUnique) @@ -199,7 +205,7 @@ extension _Node { at: slot, inserted: false) - case .notFound(let bucket, let slot): + case .insert(let bucket, let slot): ensureUniqueAndInsertItem(isUnique: isUnique, slot, bucket) { _ in } return DefaultedValueUpdateState( (key, defaultValue()), @@ -207,8 +213,16 @@ extension _Node { at: slot, inserted: true) - case .newCollision(let bucket, let slot): - let r = ensureUniqueAndMakeNewCollision( + case .appendCollision: + ensureUniqueAndAppendCollision(isUnique: isUnique) { _ in } + return DefaultedValueUpdateState( + (key, defaultValue()), + in: unmanaged, + at: _Slot(self.count &- 1), + inserted: true) + + case .spawnChild(let bucket, let slot): + let r = ensureUniqueAndSpawnChild( isUnique: isUnique, level: level, replacing: slot, bucket, @@ -219,11 +233,11 @@ extension _Node { at: r.slot, inserted: true) - case .expansion(let collisionHash): + case .expansion: let r = _Node.build( level: level, item1: { _ in }, hash, - child2: self, collisionHash + child2: self, self.collisionHash ) self = r.top return DefaultedValueUpdateState( diff --git a/Sources/PersistentCollections/Node/_Node+Subtree Removals.swift b/Sources/PersistentCollections/Node/_Node+Subtree Removals.swift index 4af6e0ac4..e7e73ac79 100644 --- a/Sources/PersistentCollections/Node/_Node+Subtree Removals.swift +++ b/Sources/PersistentCollections/Node/_Node+Subtree Removals.swift @@ -19,54 +19,50 @@ extension _Node { /// by inlining the remaining item into the parent node. @inlinable internal mutating func remove( - _ key: Key, _ level: _Level, _ hash: _Hash + _ level: _Level, _ key: Key, _ hash: _Hash ) -> Element? { defer { _invariantCheck() } guard self.isUnique() else { - guard let r = removing(key, level, hash) else { return nil } + guard let r = removing(level, key, hash) else { return nil } self = r.replacement return r.old } - let r = find(level, key, hash, forInsert: false) - switch r { - case .found(let bucket, let slot): - return _removeItemFromUniqueLeafNode(level, bucket, slot) { $0.move() } - case .notFound, .newCollision, .expansion: - return nil - case .descend(let bucket, let slot): - let (old, needsInlining) = update { - let child = $0.childPtr(at: slot) - let old = child.pointee.remove(key, level.descend(), hash) - guard old != nil else { return (old, false) } - let needsInlining = child.pointee.hasSingletonItem - return (old, needsInlining) - } - guard old != nil else { return nil } - _fixupUniqueAncestorAfterItemRemoval( - slot, { _ in bucket }, needsInlining: needsInlining) - return old + guard let r = find(level, key, hash) else { return nil } + guard r.descend else { + let bucket = hash[level] + return _removeItemFromUniqueLeafNode(level, bucket, r.slot) { $0.move() } } + + let (old, needsInlining) = update { + let child = $0.childPtr(at: r.slot) + let old = child.pointee.remove(level.descend(), key, hash) + guard old != nil else { return (old, false) } + let needsInlining = child.pointee.hasSingletonItem + return (old, needsInlining) + } + guard old != nil else { return nil } + _fixupUniqueAncestorAfterItemRemoval( + r.slot, { _ in hash[level] }, needsInlining: needsInlining) + return old } } extension _Node { + // FIXME: Make this return a Builder @inlinable internal func removing( - _ key: Key, _ level: _Level, _ hash: _Hash + _ level: _Level, _ key: Key, _ hash: _Hash ) -> (replacement: _Node, old: Element)? { - let r = find(level, key, hash, forInsert: false) - switch r { - case .found(let bucket, let slot): - return _removingItemFromLeaf(level, slot, bucket) - case .notFound, .newCollision, .expansion: - return nil - case .descend(let bucket, let slot): - let r = read { $0[child: slot].removing(key, level.descend(), hash) } - guard let r = r else { return nil } - return ( - _fixedUpAncestorAfterItemRemoval(level, slot, bucket, r.replacement), - r.old) + guard let r = find(level, key, hash) else { return nil } + guard r.descend else { + return _removingItemFromLeaf(level, hash[level], r.slot) } + let r2 = read { $0[child: r.slot].removing(level.descend(), key, hash) } + guard let r2 = r2 else { return nil } + return ( + _fixedUpAncestorAfterItemRemoval( + level, r.slot, hash[level], r2.replacement), + r2.old) } } @@ -107,7 +103,7 @@ extension _Node { if level == path.level { let slot = path.currentItemSlot let bucket = read { $0.itemBucket(at: slot) } - return _removingItemFromLeaf(level, slot, bucket) + return _removingItemFromLeaf(level, bucket, slot) } let slot = path.childSlot(at: level) let (bucket, r) = read { @@ -142,7 +138,7 @@ extension _Node { @inlinable internal func _removingItemFromLeaf( - _ level: _Level, _ slot: _Slot, _ bucket: _Bucket + _ level: _Level, _ bucket: _Bucket, _ slot: _Slot ) -> (replacement: _Node, old: Element) { // Don't copy the node if we'd immediately discard it. let willAtrophy = read { @@ -218,9 +214,9 @@ extension _Node { assert(isCollisionNode && hasSingletonItem) assert(isUnique()) update { - let hash = $0.collisionHash - $0.itemMap = _Bitmap(hash[.top]) + $0.itemMap = _Bitmap($0.collisionHash[.top]) $0.childMap = .empty + $0.bytesFree &+= MemoryLayout<_Hash>.stride } } } diff --git a/Sources/PersistentCollections/Node/_Node+UnsafeHandle.swift b/Sources/PersistentCollections/Node/_Node+UnsafeHandle.swift index 7491c85e8..583abccd3 100644 --- a/Sources/PersistentCollections/Node/_Node+UnsafeHandle.swift +++ b/Sources/PersistentCollections/Node/_Node+UnsafeHandle.swift @@ -153,13 +153,23 @@ extension _Node.UnsafeHandle { @inlinable @inline(__always) internal var collisionCount: Int { get { _header.pointee.collisionCount } - nonmutating set { _header.pointee.collisionCount = newValue } + nonmutating set { + assertMutable() + _header.pointee.collisionCount = newValue + } } - @inlinable + @inlinable @inline(__always) internal var collisionHash: _Hash { - assert(isCollisionNode) - return _Hash(self[item: .zero].key) + get { + assert(isCollisionNode) + return _memory.load(as: _Hash.self) + } + nonmutating set { + assertMutable() + assert(isCollisionNode) + _memory.storeBytes(of: newValue, as: _Hash.self) + } } @inlinable @inline(__always) diff --git a/Sources/PersistentCollections/Node/_Node.swift b/Sources/PersistentCollections/Node/_Node.swift index de2159cab..9a0a3c17e 100644 --- a/Sources/PersistentCollections/Node/_Node.swift +++ b/Sources/PersistentCollections/Node/_Node.swift @@ -104,6 +104,11 @@ extension _Node { read { $0.isCollisionNode } } + @inlinable + internal var collisionHash: _Hash { + read { $0.collisionHash } + } + @inlinable internal var hasSingletonItem: Bool { read { $0.hasSingletonItem } diff --git a/Sources/PersistentCollections/Node/_RawNode+UnsafeHandle.swift b/Sources/PersistentCollections/Node/_RawNode+UnsafeHandle.swift index 169e9a352..f7ea85577 100644 --- a/Sources/PersistentCollections/Node/_RawNode+UnsafeHandle.swift +++ b/Sources/PersistentCollections/Node/_RawNode+UnsafeHandle.swift @@ -42,12 +42,32 @@ extension _RawNode { } } +extension _RawNode.UnsafeHandle { + @inlinable @inline(__always) + static func read( + _ node: _UnmanagedNode, + _ body: (Self) throws -> R + ) rethrows -> R { + try node.ref._withUnsafeGuaranteedRef { storage in + try storage.withUnsafeMutablePointers { header, elements in + try body(Self(header, UnsafeRawPointer(elements))) + } + } + } +} + extension _RawNode.UnsafeHandle { @inline(__always) internal var isCollisionNode: Bool { _header.pointee.isCollisionNode } + @inline(__always) + internal var collisionHash: _Hash { + assert(isCollisionNode) + return _memory.load(as: _Hash.self) + } + @inline(__always) internal var hasChildren: Bool { _header.pointee.hasChildren diff --git a/Sources/PersistentCollections/Node/_Slot.swift b/Sources/PersistentCollections/Node/_Slot.swift index b22035a26..788e43692 100644 --- a/Sources/PersistentCollections/Node/_Slot.swift +++ b/Sources/PersistentCollections/Node/_Slot.swift @@ -83,6 +83,7 @@ extension _Slot: CustomStringConvertible { extension _Slot: Strideable { @inlinable @inline(__always) internal func advanced(by n: Int) -> _Slot { + assert(n >= 0 || value + n >= 0) return _Slot(_value &+ UInt32(truncatingIfNeeded: n)) } diff --git a/Sources/PersistentCollections/Node/_StorageHeader.swift b/Sources/PersistentCollections/Node/_StorageHeader.swift index fa56caa93..da75a59c7 100644 --- a/Sources/PersistentCollections/Node/_StorageHeader.swift +++ b/Sources/PersistentCollections/Node/_StorageHeader.swift @@ -62,7 +62,7 @@ extension _StorageHeader { @inlinable @inline(__always) internal var isCollisionNode: Bool { - !itemMap.intersection(childMap).isEmpty + !itemMap.isDisjoint(with: childMap) } @inlinable @inline(__always) @@ -122,38 +122,22 @@ extension _StorageHeader { } extension _StorageHeader { - @inlinable - internal static func counts( - itemMap: _Bitmap, childMap: _Bitmap - ) -> (itemCount: Int, childCount: Int) { - if itemMap == childMap { - return (Int(truncatingIfNeeded: itemMap._value), 0) - } - return (itemMap.count, childMap.count) - } - @inlinable internal func bitmapsForInsertingItem( at slot: _Slot, _ bucket: _Bucket ) -> (itemMap: _Bitmap, childMap: _Bitmap) { + assert(!isCollisionNode) + assert(!bucket.isInvalid) + assert(!itemMap.contains(bucket) && !childMap.contains(bucket)) var itemMap = self.itemMap - var childMap = self.childMap - if bucket.isInvalid { - assert(isCollisionNode) - itemMap._value += 1 - childMap = itemMap - } else { - assert(!isCollisionNode) - assert(!itemMap.contains(bucket) && !childMap.contains(bucket)) - itemMap.insert(bucket) - assert(itemMap.slot(of: bucket) == slot) - } + itemMap.insert(bucket) + assert(itemMap.slot(of: bucket) == slot) return (itemMap, childMap) } @inlinable - internal func bitmapsForNewCollision( + internal func bitmapsForSpawningChild( at slot: _Slot, _ bucket: _Bucket ) -> (itemMap: _Bitmap, childMap: _Bitmap) { diff --git a/Sources/PersistentCollections/Node/_UnsafePath.swift b/Sources/PersistentCollections/Node/_UnsafePath.swift index 05faf7b24..a3a878a39 100644 --- a/Sources/PersistentCollections/Node/_UnsafePath.swift +++ b/Sources/PersistentCollections/Node/_UnsafePath.swift @@ -316,6 +316,36 @@ extension _UnsafePath { self.level = level.descend() } + /// Descend onto the first path within the currently selected child. + /// (Either the first item if it exists, or the first child. If the child + /// is an empty node (which should not happen in a valid hash tree), then this + /// selects the empty slot at the end of it. + /// + /// - Note: It is undefined behavior to call this on a path that is no longer + /// valid. + @inlinable + internal mutating func descendToChild( + _ child: _UnmanagedNode, at slot: _Slot + ) { + assert(slot < node.childrenEndSlot) + assert(child == node.unmanagedChild(at: slot)) + self.node = child + self.ancestors[level] = slot + self.nodeSlot = .zero + self._isItem = node.hasItems + self.level = level.descend() + } + + internal mutating func ascend(to ancestor: _UnmanagedNode, at level: _Level) { + guard level != self.level else { return } + assert(level < self.level) + self.level = level + self.node = ancestor + self.nodeSlot = ancestors[level] + self.ancestors.clear(atOrBelow: level) + self._isItem = false + } + /// Ascend to the nearest ancestor for which the `test` predicate returns /// true. Because paths do not contain references to every node on them, /// you need to manually supply a valid reference to the root node. This @@ -531,7 +561,52 @@ extension _RawNode { assert(path.level == level) return path.nodeSlot.value } +} + +extension _UnsafePath { + /// Set the path to the item at the specified position in a preorder walk + /// of the subtree rooted at the current node. + /// + /// - Returns: `(found, remaining)`, where found is true if the item was + /// successfully found, and false otherwise. If `found` is false then + /// `remaining` is the number of items that still need to be skipped to + /// find the correct item (outside this subtree). + /// If `found` is true, then `remaining` is zero. + internal mutating func findItemAtPreorderPosition( + _ position: Int + ) -> (found: Bool, remaining: Int) { + assert(position >= 0) + let top = node + let topLevel = level + var stop = false + var remaining = position + while !stop { + let itemCount = node.itemCount + if remaining < itemCount { + selectItem(at: _Slot(remaining)) + return (true, 0) + } + remaining -= itemCount + node.read { + let children = $0.children + for i in children.indices { + let c = children[i].count + if remaining < c { + descendToChild(children[i].unmanaged, at: _Slot(i)) + return + } + remaining &-= c + } + stop = true + } + } + ascend(to: top, at: topLevel) + selectEnd() + return (false, remaining) + } +} +extension _RawNode { /// Return the number of steps between two paths within a preorder walk of the /// tree. The two paths must not address a child node. /// @@ -646,3 +721,200 @@ extension _Node { return nil } } + +extension _RawNode { + @usableFromInline + @_effects(releasenone) + internal func seek( + _ level: _Level, + _ path: inout _UnsafePath, + offsetBy distance: Int, + limitedBy limit: _UnsafePath + ) -> (found: Bool, limited: Bool) { + assert(level.isAtRoot) + if (distance > 0 && limit < path) || (distance < 0 && limit > path) { + return (seek(level, &path, offsetBy: distance), false) + } + var d = distance + guard self._seek(level, &path, offsetBy: &d) else { + path = limit + return (distance >= 0 && d == 0 && limit.isOnNodeEnd, true) + } + let found = ( + distance == 0 + || (distance > 0 && path <= limit) + || (distance < 0 && path >= limit)) + return (found, true) + } + + @usableFromInline + @_effects(releasenone) + internal func seek( + _ level: _Level, + _ path: inout _UnsafePath, + offsetBy distance: Int + ) -> Bool { + var d = distance + if self._seek(level, &path, offsetBy: &d) { + return true + } + if distance > 0, d == 0 { // endIndex + return true + } + return false + } + + internal func _seek( + _ level: _Level, + _ path: inout _UnsafePath, + offsetBy distance: inout Int + ) -> Bool { + // This is a bit complicated, because we only have a direct reference to the + // final node on the path, and we want to avoid having to descend + // from the root down if the target item stays within the original node's + // subtree. So we first figure out the subtree situation, and only start the + // recursion if the target is outside of it. + assert(level.isAtRoot) + assert(path.isOnItem || path.isOnNodeEnd) + guard distance != 0 else { return true } + if distance > 0 { + if !path.isOnItem { return false } + // Try a local search within the subtree starting at the current node. + let slot = path.currentItemSlot + let r = path.findItemAtPreorderPosition(distance &+ slot.value) + if r.found { + assert(r.remaining == 0) + return true + } + assert(r.remaining >= 0 && r.remaining <= distance) + distance = r.remaining + + // Fall back to recursively descending from the root. + return _seekForward(level, by: &distance, fromSubtree: &path) + } + // distance < 0 + if !path.isOnNodeEnd { + // Shortcut: see if the destination item is within the same node. + // (Doing this here allows us to avoid having to descend from the root + // down only to figure this out.) + let slot = path.nodeSlot + distance &+= slot.value + if distance >= 0 { + path.selectItem(at: _Slot(distance)) + distance = 0 + return true + } + } + // Otherwise we need to visit ancestor nodes to find the item at the right + // position. We also do this when we start from the end index -- there + // will be no recursion in that case anyway. + return _seekBackward(level, by: &distance, fromSubtree: &path) + } + + /// Find the item at the given positive distance from the last item within the + /// subtree rooted at the current node in `path`. + internal func _seekForward( + _ level: _Level, + by distance: inout Int, + fromSubtree path: inout _UnsafePath + ) -> Bool { + assert(distance >= 0) + assert(level <= path.level) + guard level < path.level else { + path.selectEnd() + return false + } + return read { + let children = $0.children + var i = path.childSlot(at: level).value + if children[i]._seekForward( + level.descend(), by: &distance, fromSubtree: &path + ) { + assert(distance == 0) + return true + } + path.ascend(to: unmanaged, at: level) + i &+= 1 + while i < children.endIndex { + let c = children[i].count + if distance < c { + path.descendToChild(children[i].unmanaged, at: _Slot(i)) + let r = path.findItemAtPreorderPosition(distance) + precondition(r.found, "Internal inconsistency: invalid node counts") + assert(r.remaining == 0) + distance = 0 + return true + } + distance &-= c + i &+= 1 + } + path.selectEnd() + return false + } + } + + /// Find the item at the given negative distance from the first item within the + /// subtree rooted at the current node in `path`. + internal func _seekBackward( + _ level: _Level, + by distance: inout Int, + fromSubtree path: inout _UnsafePath + ) -> Bool { + assert(distance < 0) + assert(level <= path.level) + + return read { + let children = $0.children + var slot: _Slot + if level < path.level { + // We need to descend to the end of the path before we can start the + // search for real. + slot = path.childSlot(at: level) + if children[slot.value]._seekBackward( + level.descend(), by: &distance, fromSubtree: &path + ) { + // A deeper level has found the target item. + assert(distance == 0) + return true + } + // No luck yet -- ascend to this node and look through preceding data. + path.ascend(to: unmanaged, at: level) + } else if path.isOnNodeEnd { + // When we start from the root's end (the end index), we don't need + // to descend before starting to look at previous children. + assert(level.isAtRoot && path.node == self.unmanaged) + slot = path.node.childrenEndSlot + } else { // level == path.level + // The outermost caller has already gone as far back as possible + // within the original subtree. Return a level higher to actually + // start the rest of the search. + return false + } + + // Look through all preceding children for the target item. + while slot > .zero { + slot = slot.previous() + let c = children[slot.value].count + if c + distance >= 0 { + path.descendToChild(children[slot.value].unmanaged, at: slot) + let r = path.findItemAtPreorderPosition(c + distance) + precondition(r.found, "Internal inconsistency: invalid node counts") + distance = 0 + return true + } + distance += c + } + // See if the target is hiding somwhere in our immediate items. + distance &+= $0.itemCount + if distance >= 0 { + path.selectItem(at: _Slot(distance)) + distance = 0 + return true + } + // No joy -- we need to continue searching a level higher. + assert(distance < 0) + return false + } + } +} + diff --git a/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Collection.swift b/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Collection.swift index 99ad5f5df..42813f5ca 100644 --- a/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Collection.swift +++ b/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Collection.swift @@ -145,5 +145,27 @@ extension PersistentDictionary: BidirectionalCollection { return _root.raw.distance(.top, from: start._path, to: end._path) } - // FIXME: Implement index(_:offsetBy:), index(_:offsetBy:limitedBy:) + @inlinable + public func index(_ i: Index, offsetBy distance: Int) -> Index { + precondition(_isValid(i), "Invalid index") + var i = i + let r = _root.raw.seek(.top, &i._path, offsetBy: distance) + precondition(r, "Index offset out of bounds") + return i + } + + @inlinable + public func index( + _ i: Index, offsetBy distance: Int, limitedBy limit: Index + ) -> Index? { + precondition(_isValid(i), "Invalid index") + precondition(_isValid(limit), "Invalid limit index") + var i = i + let (found, limited) = _root.raw.seek( + .top, &i._path, offsetBy: distance, limitedBy: limit._path + ) + if found { return i } + precondition(limited, "Index offset out of bounds") + return nil + } } diff --git a/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+CustomReflectible.swift b/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+CustomReflectable.swift similarity index 100% rename from Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+CustomReflectible.swift rename to Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+CustomReflectable.swift diff --git a/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Debugging.swift b/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Debugging.swift index 30fc20a55..ed5645fb8 100644 --- a/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Debugging.swift +++ b/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Debugging.swift @@ -18,7 +18,7 @@ extension PersistentDictionary { @inlinable public func _invariantCheck() { - _root._fullInvariantCheck(.top, _Hash(_value: 0)) + _root._fullInvariantCheck(.top, .emptyPrefix) } public func _dump(iterationOrder: Bool = false) { @@ -28,4 +28,10 @@ extension PersistentDictionary { public static var _maxDepth: Int { _Level.limit } + + public var _statistics: _HashTreeStatistics { + var stats = _HashTreeStatistics() + _root.gatherStatistics(.top, &stats) + return stats + } } diff --git a/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Filter.swift b/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Filter.swift index 911a828fc..ce3cec2d1 100644 --- a/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Filter.swift +++ b/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Filter.swift @@ -14,14 +14,7 @@ extension PersistentDictionary { public func filter( _ isIncluded: (Element) throws -> Bool ) rethrows -> Self { - var result: PersistentDictionary = [:] - for item in self { - guard try isIncluded(item) else { continue } - // FIXME: We could do this as a structural transformation. - let hash = _Hash(item.key) - let inserted = result._root.insert(item, .top, hash) - assert(inserted) - } - return result + let result = try _root.filter(.top, .emptyPrefix, isIncluded) + return PersistentDictionary(_new: result.finalize(.top)) } } diff --git a/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Initializers.swift b/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Initializers.swift index 354e21c1b..b89db727a 100644 --- a/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Initializers.swift +++ b/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Initializers.swift @@ -20,6 +20,16 @@ extension PersistentDictionary { self = other } + // FIXME: This is a non-standard addition + @inlinable + public init( + keys: PersistentSet, + _ valueTransform: (Key) throws -> Value + ) rethrows { + let root = try keys._root.mapValues { try valueTransform($0.key) } + self.init(_new: root) + } + @inlinable public init( uniqueKeysWithValues keysAndValues: S diff --git a/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Keys.swift b/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Keys.swift index 6386e4022..36e7f10ed 100644 --- a/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Keys.swift +++ b/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Keys.swift @@ -9,6 +9,8 @@ // //===----------------------------------------------------------------------===// +import _CollectionsUtilities + extension PersistentDictionary { /// A view of a dictionary’s keys. @frozen @@ -67,6 +69,13 @@ extension PersistentDictionary.Keys: Sequence { } } +extension PersistentDictionary.Keys: _FastMembershipCheckable { + @inlinable + public func contains(_ element: Element) -> Bool { + _base._root.containsKey(.top, element, _Hash(element)) + } +} + extension PersistentDictionary.Keys: BidirectionalCollection { public typealias Index = PersistentDictionary.Index diff --git a/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+MapValues.swift b/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+MapValues.swift index bbbac4163..ae9968792 100644 --- a/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+MapValues.swift +++ b/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+MapValues.swift @@ -14,7 +14,7 @@ extension PersistentDictionary { public func mapValues( _ transform: (Value) throws -> T ) rethrows -> PersistentDictionary { - let transformed = try _root.mapValues(transform) + let transformed = try _root.mapValues { try transform($0.value) } return PersistentDictionary(_new: transformed) } @@ -22,14 +22,7 @@ extension PersistentDictionary { public func compactMapValues( _ transform: (Value) throws -> T? ) rethrows -> PersistentDictionary { - // FIXME: We could do this as a structural transformation. - var result: PersistentDictionary = [:] - for (key, v) in self { - guard let value = try transform(v) else { continue } - let hash = _Hash(key) - let inserted = result._root.insert((key, value), .top, hash) - assert(inserted) - } - return result + let result = try _root.compactMapValues(.top, .emptyPrefix, transform) + return PersistentDictionary(_new: result.finalize(.top)) } } diff --git a/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary.swift b/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary.swift index b6907fdbd..f47409cc0 100644 --- a/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary.swift +++ b/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary.swift @@ -383,7 +383,7 @@ extension PersistentDictionary { @discardableResult public mutating func removeValue(forKey key: Key) -> Value? { _invalidateIndices() - return _root.remove(key, .top, _Hash(key))?.value + return _root.remove(.top, key, _Hash(key))?.value } // fluid/immutable API diff --git a/Sources/PersistentCollections/PersistentSet/PersistentSet+Codable.swift b/Sources/PersistentCollections/PersistentSet/PersistentSet+Codable.swift new file mode 100644 index 000000000..d4a41b774 --- /dev/null +++ b/Sources/PersistentCollections/PersistentSet/PersistentSet+Codable.swift @@ -0,0 +1,46 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +extension PersistentSet: Encodable where Element: Encodable { + /// Encodes the elements of this ordered set into the given encoder. + /// + /// - Parameter encoder: The encoder to write data to. + @inlinable + public func encode(to encoder: Encoder) throws { + var container = encoder.unkeyedContainer() + try container.encode(contentsOf: self) + } +} + +extension PersistentSet: Decodable where Element: Decodable { + /// Creates a new ordered set by decoding from the given decoder. + /// + /// This initializer throws an error if reading from the decoder fails, or + /// if the decoded contents contain duplicate values. + /// + /// - Parameter decoder: The decoder to read data from. + @inlinable + public init(from decoder: Decoder) throws { + self.init() + + var container = try decoder.unkeyedContainer() + while !container.isAtEnd { + let element = try container.decode(Element.self) + let inserted = self._insert(element) + guard inserted else { + let context = DecodingError.Context( + codingPath: container.codingPath, + debugDescription: "Decoded elements aren't unique (first duplicate at offset \(self.count))") + throw DecodingError.dataCorrupted(context) + } + } + } +} diff --git a/Sources/PersistentCollections/PersistentSet/PersistentSet+Collection.swift b/Sources/PersistentCollections/PersistentSet/PersistentSet+Collection.swift index 9bf1e4722..e648d72df 100644 --- a/Sources/PersistentCollections/PersistentSet/PersistentSet+Collection.swift +++ b/Sources/PersistentCollections/PersistentSet/PersistentSet+Collection.swift @@ -145,5 +145,27 @@ extension PersistentSet: BidirectionalCollection { return _root.raw.distance(.top, from: start._path, to: end._path) } - // FIXME: Implement index(_:offsetBy:), index(_:offsetBy:limitedBy:) + @inlinable + public func index(_ i: Index, offsetBy distance: Int) -> Index { + precondition(_isValid(i), "Invalid index") + var i = i + let r = _root.raw.seek(.top, &i._path, offsetBy: distance) + precondition(r, "Index offset out of bounds") + return i + } + + @inlinable + public func index( + _ i: Index, offsetBy distance: Int, limitedBy limit: Index + ) -> Index? { + precondition(_isValid(i), "Invalid index") + precondition(_isValid(limit), "Invalid limit index") + var i = i + let (found, limited) = _root.raw.seek( + .top, &i._path, offsetBy: distance, limitedBy: limit._path + ) + if found { return i } + precondition(limited, "Index offset out of bounds") + return nil + } } diff --git a/Sources/PersistentCollections/PersistentSet/PersistentSet+CustomReflectable.swift b/Sources/PersistentCollections/PersistentSet/PersistentSet+CustomReflectable.swift new file mode 100644 index 000000000..1a22473c2 --- /dev/null +++ b/Sources/PersistentCollections/PersistentSet/PersistentSet+CustomReflectable.swift @@ -0,0 +1,17 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +extension PersistentSet: CustomReflectable { + /// The custom mirror for this instance. + public var customMirror: Mirror { + Mirror(self, unlabeledChildren: self, displayStyle: .collection) + } +} diff --git a/Sources/PersistentCollections/PersistentSet/PersistentSet+Debugging.swift b/Sources/PersistentCollections/PersistentSet/PersistentSet+Debugging.swift new file mode 100644 index 000000000..593638584 --- /dev/null +++ b/Sources/PersistentCollections/PersistentSet/PersistentSet+Debugging.swift @@ -0,0 +1,37 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import _CollectionsUtilities + +extension PersistentSet { + public static var _isConsistencyCheckingEnabled: Bool { + _isCollectionsInternalCheckingEnabled + } + + @inlinable + public func _invariantCheck() { + _root._fullInvariantCheck(.top, .emptyPrefix) + } + + public func _dump(iterationOrder: Bool = false) { + _root.dump(iterationOrder: iterationOrder) + } + + public static var _maxDepth: Int { + _Level.limit + } + + public var _statistics: _HashTreeStatistics { + var stats = _HashTreeStatistics() + _root.gatherStatistics(.top, &stats) + return stats + } +} diff --git a/Sources/PersistentCollections/PersistentSet/PersistentSet+Descriptions.swift b/Sources/PersistentCollections/PersistentSet/PersistentSet+Descriptions.swift new file mode 100644 index 000000000..13a3dd7fd --- /dev/null +++ b/Sources/PersistentCollections/PersistentSet/PersistentSet+Descriptions.swift @@ -0,0 +1,31 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import _CollectionsUtilities + +extension PersistentSet: CustomStringConvertible { + /// A textual representation of this instance. + public var description: String { + _arrayDescription(for: self) + } +} + +extension PersistentSet: CustomDebugStringConvertible { + /// A textual representation of this instance, suitable for debugging. + public var debugDescription: String { + _arrayDescription( + for: self, debug: true, typeName: _debugTypeName()) + } + + internal func _debugTypeName() -> String { + "PersistentSet<\(Element.self)>" + } +} diff --git a/Sources/PersistentCollections/PersistentSet/PersistentSet+Equatable.swift b/Sources/PersistentCollections/PersistentSet/PersistentSet+Equatable.swift index 3e3e1d289..806d70c8a 100644 --- a/Sources/PersistentCollections/PersistentSet/PersistentSet+Equatable.swift +++ b/Sources/PersistentCollections/PersistentSet/PersistentSet+Equatable.swift @@ -9,9 +9,40 @@ // //===----------------------------------------------------------------------===// +import _CollectionsUtilities + extension PersistentSet: Equatable { - @inlinable + @inlinable @inline(__always) public static func == (left: Self, right: Self) -> Bool { - left._root.isEqual(to: right._root, by: { _, _ in true }) + left.isEqual(to: right) + } +} + +// FIXME: These are non-standard extensions generalizing ==. +extension PersistentSet { + @inlinable + public func isEqual(to other: Self) -> Bool { + _root.isEqual(to: other._root, by: { _, _ in true }) + } + + @inlinable + public func isEqual( + to other: PersistentDictionary.Keys + ) -> Bool { + _root.isEqual(to: other._base._root, by: { _, _ in true }) + } + + @inlinable + public func isEqual(to other: S) -> Bool + where S.Element == Element + { + guard other.underestimatedCount <= self.count else { return false } + // FIXME: Would making this a BitSet of seen positions be better? + var seen: PersistentSet = [] + for item in other { + guard self.contains(item) else { return false } + guard seen._insert(item) else { return false } + } + return seen.count == self.count } } diff --git a/Sources/PersistentCollections/PersistentSet/PersistentSet+ExpressibleByArrayLiteral.swift b/Sources/PersistentCollections/PersistentSet/PersistentSet+ExpressibleByArrayLiteral.swift new file mode 100644 index 000000000..b5b452973 --- /dev/null +++ b/Sources/PersistentCollections/PersistentSet/PersistentSet+ExpressibleByArrayLiteral.swift @@ -0,0 +1,31 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +extension PersistentSet: ExpressibleByArrayLiteral { + /// Creates a new set from the contents of an array literal. + /// + /// Duplicate elements in the literal are allowed, but the resulting ordered + /// set will only contain the first occurrence of each. + /// + /// Do not call this initializer directly. It is used by the compiler when + /// you use an array literal. Instead, create a new ordered set using an array + /// literal as its value by enclosing a comma-separated list of values in + /// square brackets. You can use an array literal anywhere an ordered set is + /// expected by the type context. + /// + /// - Parameter elements: A variadic list of elements of the new ordered set. + /// + /// - Complexity: O(`elements.count`) if `Element` implements + /// high-quality hashing. + public init(arrayLiteral elements: Element...) { + self.init(elements) + } +} diff --git a/Sources/PersistentCollections/PersistentSet/PersistentSet+Filter.swift b/Sources/PersistentCollections/PersistentSet/PersistentSet+Filter.swift new file mode 100644 index 000000000..031fb952a --- /dev/null +++ b/Sources/PersistentCollections/PersistentSet/PersistentSet+Filter.swift @@ -0,0 +1,22 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +extension PersistentSet { + @inlinable + public func filter( + _ isIncluded: (Element) throws -> Bool + ) rethrows -> Self { + let result = try _root.filter(.top, .emptyPrefix) { + try isIncluded($0.key) + } + return PersistentSet(_new: result.finalize(.top)) + } +} diff --git a/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra basics.swift b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra basics.swift index 341acf5c0..840dacc65 100644 --- a/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra basics.swift +++ b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra basics.swift @@ -9,6 +9,15 @@ // //===----------------------------------------------------------------------===// +import _CollectionsUtilities + +extension PersistentSet: _FastMembershipCheckable { + @inlinable + public func contains(_ item: Element) -> Bool { + _root.containsKey(.top, item, _Hash(item)) + } +} + extension PersistentSet: SetAlgebra { @discardableResult @inlinable @@ -47,7 +56,7 @@ extension PersistentSet: SetAlgebra { public mutating func remove(_ member: Element) -> Element? { let hash = _Hash(member) _invalidateIndices() - return _root.remove(member, .top, hash)?.key + return _root.remove(.top, member, hash)?.key } @discardableResult diff --git a/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra formIntersection.swift b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra formIntersection.swift index 53f1e585b..7ddf77741 100644 --- a/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra formIntersection.swift +++ b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra formIntersection.swift @@ -12,6 +12,6 @@ extension PersistentSet { @inlinable public mutating func formIntersection(_ other: Self) { - fatalError("FIXME") + self = intersection(other) } } diff --git a/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra formSymmetricDifference.swift b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra formSymmetricDifference.swift index 6cb29c34a..afe0a3eb9 100644 --- a/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra formSymmetricDifference.swift +++ b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra formSymmetricDifference.swift @@ -12,6 +12,6 @@ extension PersistentSet { @inlinable public mutating func formSymmetricDifference(_ other: __owned Self) { - fatalError("FIXME") + self = symmetricDifference(other) } } diff --git a/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra formUnion.swift b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra formUnion.swift index daeafb4b9..dab9c748b 100644 --- a/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra formUnion.swift +++ b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra formUnion.swift @@ -12,6 +12,6 @@ extension PersistentSet { @inlinable public mutating func formUnion(_ other: __owned Self) { - fatalError("FIXME") + self = union(other) } } diff --git a/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra intersection.swift b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra intersection.swift index bc93d3187..fced90a1c 100644 --- a/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra intersection.swift +++ b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra intersection.swift @@ -12,6 +12,7 @@ extension PersistentSet { @inlinable public func intersection(_ other: Self) -> Self { - fatalError("FIXME") + let result = _root.intersection(.top, .emptyPrefix, other._root) + return Self(_new: result.finalize(.top)) } } diff --git a/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra isDisjoint.swift b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra isDisjoint.swift index 8d499bde6..937e3a037 100644 --- a/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra isDisjoint.swift +++ b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra isDisjoint.swift @@ -17,8 +17,15 @@ extension PersistentSet { @inlinable public func isDisjoint( - with other: PersistentDictionary + with other: PersistentDictionary.Keys ) -> Bool { - self._root.isDisjoint(.top, with: other._root) + self._root.isDisjoint(.top, with: other._base._root) + } + + @inlinable + public func isDisjoint(with other: S) -> Bool + where S.Element == Element + { + other.allSatisfy { !self.contains($0) } } } diff --git a/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra isStrictSubset.swift b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra isStrictSubset.swift index dd44e88ca..f4505653e 100644 --- a/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra isStrictSubset.swift +++ b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra isStrictSubset.swift @@ -9,6 +9,8 @@ // //===----------------------------------------------------------------------===// +import _CollectionsUtilities + extension PersistentSet { @inlinable public func isStrictSubset(of other: Self) -> Bool { @@ -18,9 +20,45 @@ extension PersistentSet { @inlinable public func isStrictSubset( - of other: PersistentDictionary + of other: PersistentDictionary.Keys ) -> Bool { guard self.count < other.count else { return false } return isSubset(of: other) } + + @inlinable + public func isStrictSubset( + of other: S + ) -> Bool + where S.Element == Element { + guard self.isSubset(of: other) else { return false } + return !other.allSatisfy { self.contains($0) } + } + + + @inlinable + public func isStrictSubset(of other: S) -> Bool + where S.Element == Element + { + if other.underestimatedCount > self.count { + return isSubset(of: other) + } + // FIXME: Would making this a BitSet of seen positions be better? + var seen: PersistentSet? = [] + var isStrict = false + for item in other { + if self.contains(item), seen?._insert(item) == true { + if seen?.count == self.count { + if isStrict { return true } + // Stop collecting seen items -- we just need to decide + // strictness now. + seen = nil + } + } else { + isStrict = true + if seen == nil { return true } + } + } + return false + } } diff --git a/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra isStrictSuperset.swift b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra isStrictSuperset.swift index 8bdc3d707..94b54d031 100644 --- a/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra isStrictSuperset.swift +++ b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra isStrictSuperset.swift @@ -9,6 +9,8 @@ // //===----------------------------------------------------------------------===// +import _CollectionsUtilities + extension PersistentSet { @inlinable public func isStrictSuperset(of other: Self) -> Bool { @@ -18,9 +20,39 @@ extension PersistentSet { @inlinable public func isStrictSuperset( - of other: PersistentDictionary + of other: PersistentDictionary.Keys ) -> Bool { guard self.count > other.count else { return false } - return other._root.isSubset(.top, of: self._root) + return other._base._root.isSubset(.top, of: self._root) + } + + @inlinable + public func isStrictSuperset( + of other: S + ) -> Bool + where S.Element == Element + { + if self.count < other.underestimatedCount { return false } + if !other.allSatisfy({ self.contains($0) }) { return false } + return !self.allSatisfy { other.contains($0) } + } + + @inlinable + public func isStrictSuperset(of other: S) -> Bool + where S.Element == Element + { + guard self.count >= other.underestimatedCount else { + return false + } + // FIXME: Would making this a BitSet of seen positions be better? + var seen: PersistentSet = [] + for item in other { + guard self.contains(item) else { return false } + if seen._insert(item), seen.count == self.count { + return false + } + } + assert(seen.count < self.count) + return true } } diff --git a/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra isSubset.swift b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra isSubset.swift index ab735729c..608564d71 100644 --- a/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra isSubset.swift +++ b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra isSubset.swift @@ -9,6 +9,8 @@ // //===----------------------------------------------------------------------===// +import _CollectionsUtilities + extension PersistentSet { @inlinable public func isSubset(of other: Self) -> Bool { @@ -17,8 +19,38 @@ extension PersistentSet { @inlinable public func isSubset( - of other: PersistentDictionary + of other: PersistentDictionary.Keys ) -> Bool { - self._root.isSubset(.top, of: other._root) + self._root.isSubset(.top, of: other._base._root) + } + + @inlinable + public func isSubset( + of other: S + ) -> Bool + where S.Element == Element { + self.allSatisfy { other.contains($0) } + } + + @inlinable + public func isSubset( + of other: S + ) -> Bool + where S.Element == Element { + self.allSatisfy { other.contains($0) } + } + + @inlinable + public func isSubset(of other: S) -> Bool + where S.Element == Element + { + // FIXME: Would making this a BitSet of seen positions be better? + var seen: PersistentSet = [] + for item in other { + if contains(item), seen._insert(item), seen.count == self.count { + return true + } + } + return false } } diff --git a/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra isSuperset.swift b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra isSuperset.swift index 49fdef53b..3641eed61 100644 --- a/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra isSuperset.swift +++ b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra isSuperset.swift @@ -19,6 +19,16 @@ extension PersistentSet { public func isSuperset( of other: PersistentDictionary ) -> Bool { - return other._root.isSubset(.top, of: self._root) + other._root.isSubset(.top, of: self._root) + } + + @inlinable + public func isSuperset(of other: S) -> Bool + where S.Element == Element + { + guard self.count >= other.underestimatedCount else { + return false + } + return other.allSatisfy { self.contains($0) } } } diff --git a/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra subtract.swift b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra subtract.swift index 7f50bc3ec..83eb13768 100644 --- a/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra subtract.swift +++ b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra subtract.swift @@ -12,6 +12,6 @@ extension PersistentSet { @inlinable public mutating func subtract(_ other: Self) { - fatalError("FIXME") + self = subtracting(other) } } diff --git a/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra subtracting.swift b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra subtracting.swift index d88c97379..a0caadb71 100644 --- a/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra subtracting.swift +++ b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra subtracting.swift @@ -12,6 +12,7 @@ extension PersistentSet { @inlinable public __consuming func subtracting(_ other: Self) -> Self { - fatalError("FIXME") + let result = _root.subtracting(.top, .emptyPrefix, other._root) + return Self(_new: result.finalize(.top)) } } diff --git a/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra symmetricDifference.swift b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra symmetricDifference.swift index 90616f9da..317dda3b4 100644 --- a/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra symmetricDifference.swift +++ b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra symmetricDifference.swift @@ -12,6 +12,7 @@ extension PersistentSet { @inlinable public func symmetricDifference(_ other: __owned Self) -> Self { - fatalError("FIXME") + // FIXME: Do this with a structural merge. + self.subtracting(self.intersection(other)) } } diff --git a/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra union.swift b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra union.swift index 1d208f6fc..3dd367d97 100644 --- a/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra union.swift +++ b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra union.swift @@ -12,6 +12,11 @@ extension PersistentSet { @inlinable public func union(_ other: __owned Self) -> Self { - fatalError("FIXME") + // FIXME: Do this with a structural merge. + var copy = self + for item in other { + copy.insert(item) + } + return copy } } diff --git a/Sources/PersistentCollections/PersistentSet/PersistentSet.swift b/Sources/PersistentCollections/PersistentSet/PersistentSet.swift index 428edf5e9..ca3bd7323 100644 --- a/Sources/PersistentCollections/PersistentSet/PersistentSet.swift +++ b/Sources/PersistentCollections/PersistentSet/PersistentSet.swift @@ -30,11 +30,3 @@ public struct PersistentSet { self.init(_root: _new, version: _new.initialVersionNumber) } } - - -extension PersistentSet { - @inlinable - public func _invariantCheck() { - _root._fullInvariantCheck(.top, _Hash(_value: 0)) - } -} diff --git a/Sources/_CollectionsTestSupport/ConformanceCheckers/CheckBidirectionalCollection.swift b/Sources/_CollectionsTestSupport/ConformanceCheckers/CheckBidirectionalCollection.swift index dffead875..e0fe1596b 100644 --- a/Sources/_CollectionsTestSupport/ConformanceCheckers/CheckBidirectionalCollection.swift +++ b/Sources/_CollectionsTestSupport/ConformanceCheckers/CheckBidirectionalCollection.swift @@ -112,14 +112,6 @@ public func _checkBidirectionalCollection.self { + TestContext.current.withTrace("Indices") { + checkBidirectionalCollection( + collection.indices, + expectedContents: indicesByIndexAfter, + maxSamples: maxSamples) + } + } } diff --git a/Sources/_CollectionsTestSupport/ConformanceCheckers/CheckCollection.swift b/Sources/_CollectionsTestSupport/ConformanceCheckers/CheckCollection.swift index 7e41319e9..52c3cb981 100644 --- a/Sources/_CollectionsTestSupport/ConformanceCheckers/CheckCollection.swift +++ b/Sources/_CollectionsTestSupport/ConformanceCheckers/CheckCollection.swift @@ -161,16 +161,9 @@ public func _checkCollection( // Check the endIndex. expectEqual(collection.endIndex, collection.indices.endIndex) - // Check the Indices associated type - if C.self != C.Indices.self { - checkCollection( - collection.indices, - expectedContents: indicesByIndexAfter, - maxSamples: maxSamples) - } else { - expectEqual(collection.indices.count, collection.count) - expectEqualElements(collection.indices, indicesByIndexAfter) - } + // Quickly check the Indices associated type + expectEqual(collection.indices.count, collection.count) + expectEqualElements(collection.indices, indicesByIndexAfter) expectEqual(collection.indices.endIndex, collection.endIndex) // The sequence of indices must be monotonically increasing. @@ -278,4 +271,14 @@ public func _checkCollection( slice[range], expectedContents[i ..< j], by: areEquivalent) } + + if C.Indices.self != C.self && C.Indices.self != DefaultIndices.self { + // Do a more exhaustive check on Indices. + TestContext.current.withTrace("Indices") { + checkCollection( + collection.indices, + expectedContents: indicesByIndexAfter, + maxSamples: maxSamples) + } + } } diff --git a/Sources/_CollectionsUtilities/_FastMembershipCheckable.swift b/Sources/_CollectionsUtilities/_FastMembershipCheckable.swift new file mode 100644 index 000000000..4b9fa6087 --- /dev/null +++ b/Sources/_CollectionsUtilities/_FastMembershipCheckable.swift @@ -0,0 +1,27 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +/// A utility protocol for marking container types (not necessarily +/// conforming to `Sequence` or `Collection`) that provide a +/// fast `contains` operation. +public protocol _FastMembershipCheckable { + // FIXME: Add as primary associated type on >=5.7 + associatedtype Element: Equatable + + /// Returns a Boolean value that indicates whether the given element exists in `self`. + /// + /// - Performance: O(log(*n*)) or better, where *n* is the size + /// of `self` (for some definition of "size"). + func contains(_ item: Element) -> Bool +} + +extension Set: _FastMembershipCheckable {} +extension Dictionary.Keys: _FastMembershipCheckable {} diff --git a/Tests/PersistentCollectionsTests/PersistentCollections Smoke Tests.swift b/Tests/PersistentCollectionsTests/PersistentDictionary Smoke Tests.swift similarity index 100% rename from Tests/PersistentCollectionsTests/PersistentCollections Smoke Tests.swift rename to Tests/PersistentCollectionsTests/PersistentDictionary Smoke Tests.swift diff --git a/Tests/PersistentCollectionsTests/PersistentCollections Tests.swift b/Tests/PersistentCollectionsTests/PersistentDictionary Tests.swift similarity index 95% rename from Tests/PersistentCollectionsTests/PersistentCollections Tests.swift rename to Tests/PersistentCollectionsTests/PersistentDictionary Tests.swift index bec861caa..53da942d7 100644 --- a/Tests/PersistentCollectionsTests/PersistentCollections Tests.swift +++ b/Tests/PersistentCollectionsTests/PersistentDictionary Tests.swift @@ -1081,6 +1081,132 @@ class PersistentDictionaryTests: CollectionTestCase { } } + func test_mapValues_basics() { + let items = (0 ..< 100).map { ($0, 100 * $0) } + let d = PersistentDictionary(uniqueKeysWithValues: items) + + var c = 0 + let d2 = d.mapValues { value -> String in + c += 1 + expectTrue(value.isMultiple(of: 100)) + return "\(value)" + } + expectEqual(c, 100) + expectEqualDictionaries(d, items) + + expectEqualDictionaries(d2, (0 ..< 100).compactMap { key in + (key: key, value: "\(100 * key)") + }) + } + + func test_mapValues_fixtures() { + withEachFixture { fixture in + withLifetimeTracking { tracker in + withEvery("isShared", in: [false, true]) { isShared in + var (d, ref) = tracker.persistentDictionary(for: fixture) + withHiddenCopies(if: isShared, of: &d) { d in + let d2 = d.mapValues { tracker.instance(for: "\($0.payload)") } + let ref2 = Dictionary(uniqueKeysWithValues: ref.lazy.map { + ($0.key, tracker.instance(for: "\($0.value.payload)")) + }) + expectEqualDictionaries(d2, ref2) + } + } + } + } + } + + func test_compactMapValues_basics() { + let items = (0 ..< 100).map { ($0, 100 * $0) } + let d = PersistentDictionary(uniqueKeysWithValues: items) + + var c = 0 + let d2 = d.compactMapValues { value -> String? in + c += 1 + guard value.isMultiple(of: 200) else { return nil } + expectTrue(value.isMultiple(of: 100)) + return "\(value)" + } + expectEqual(c, 100) + expectEqualDictionaries(d, items) + + expectEqualDictionaries(d2, (0 ..< 50).map { key in + (key: 2 * key, value: "\(200 * key)") + }) + } + + func test_compactMapValues_fixtures() { + typealias Key = LifetimeTracked + typealias Value = LifetimeTracked + typealias Value2 = LifetimeTracked + + withEachFixture { fixture in + print(fixture.title, fixture.items) + withLifetimeTracking { tracker in + func transform(_ value: Value) -> Value2? { + guard value.payload.isMultiple(of: 2) else { return nil } + return tracker.instance(for: "\(value.payload)") + } + + withEvery("isShared", in: [false, true]) { isShared in + var (d, ref) = tracker.persistentDictionary(for: fixture) + withHiddenCopies(if: isShared, of: &d) { d in + let d2 = d.compactMapValues(transform) + let r: [(Key, Value2)] = ref.compactMap { + guard let v = transform($0.value) else { return nil } + return ($0.key, v) + } + let ref2 = Dictionary(uniqueKeysWithValues: r) + expectEqualDictionaries(d2, ref2) + } + } + } + } + } + + func test_filter_basics() { + let items = (0 ..< 100).map { ($0, 100 * $0) } + let d = PersistentDictionary(uniqueKeysWithValues: items) + + var c = 0 + let d2 = d.filter { item in + c += 1 + expectEqual(item.value, 100 * item.key) + return item.key.isMultiple(of: 2) + } + expectEqual(c, 100) + expectEqualDictionaries(d, items) + + expectEqualDictionaries(d2, (0 ..< 50).compactMap { key in + return (key: 2 * key, value: 200 * key) + }) + } + + func test_filter_fixtures() { + typealias Key = LifetimeTracked + typealias Value = LifetimeTracked + + withEachFixture { fixture in + print(fixture.title, fixture.items) + withLifetimeTracking { tracker in + withEvery("isShared", in: [false, true]) { isShared in + var (d, ref) = tracker.persistentDictionary(for: fixture) + withHiddenCopies(if: isShared, of: &d) { d in + func predicate(_ item: (key: Key, value: Value)) -> Bool { + expectEqual(item.value.payload, 100 + item.key.payload.identity) + return item.value.payload.isMultiple(of: 2) + } + let d2 = d.filter(predicate) + let ref2 = Dictionary( + uniqueKeysWithValues: ref.filter(predicate)) + expectEqualDictionaries(d2, ref2) + } + } + } + } + } + + // MARK: - // func test_uniqueKeysWithValues_Dictionary() {