diff --git a/Benchmarks/Sources/CppBenchmarks/src/MapBenchmarks.cpp b/Benchmarks/Sources/CppBenchmarks/src/MapBenchmarks.cpp index 60488b78c..d63824428 100644 --- a/Benchmarks/Sources/CppBenchmarks/src/MapBenchmarks.cpp +++ b/Benchmarks/Sources/CppBenchmarks/src/MapBenchmarks.cpp @@ -12,7 +12,7 @@ #include "MapBenchmarks.h" #include #include -#include "utils.h" +#include "Utils.h" typedef std::map custom_map; diff --git a/Package.swift b/Package.swift index 52ff207d2..2a81d4299 100644 --- a/Package.swift +++ b/Package.swift @@ -96,9 +96,10 @@ let package = Package( // BitSet, BitArray .target( - name: "BitCollections", - path: "Sources/BitCollections", - swiftSettings: settings), + name: "BitCollections", + dependencies: ["_CollectionsUtilities"], + path: "Sources/BitCollections", + swiftSettings: settings), .testTarget( name: "BitCollectionsTests", dependencies: ["BitCollections", "_CollectionsTestSupport"], @@ -118,6 +119,7 @@ let package = Package( // Heap .target( name: "PriorityQueueModule", + dependencies: ["_CollectionsUtilities"], exclude: ["CMakeLists.txt"], swiftSettings: settings), .testTarget( @@ -149,6 +151,7 @@ let package = Package( // SortedSet, SortedDictionary .target( name: "SortedCollections", + dependencies: ["_CollectionsUtilities"], swiftSettings: settings), .testTarget( name: "SortedCollectionsTests", diff --git a/Sources/BitCollections/BitSet/BitSet.Counted.swift b/Sources/BitCollections/BitSet/BitSet.Counted.swift index a4e7cf6c0..02fc831f1 100644 --- a/Sources/BitCollections/BitSet/BitSet.Counted.swift +++ b/Sources/BitCollections/BitSet/BitSet.Counted.swift @@ -29,9 +29,8 @@ extension BitSet.Counted { @inline(never) @_effects(releasenone) public func _checkInvariants() { - precondition(_count == _bits.count, "Invalid count") - precondition(_storage.isEmpty || !_storage.last!.isEmpty, - "Extraneous tail slot") + _bits._checkInvariants() + precondition(_count == _bits.count) } #else @inline(__always) @inlinable diff --git a/Sources/PersistentCollections/Node/_AncestorOffsets.swift b/Sources/PersistentCollections/Node/_AncestorSlots.swift similarity index 98% rename from Sources/PersistentCollections/Node/_AncestorOffsets.swift rename to Sources/PersistentCollections/Node/_AncestorSlots.swift index f686ec9a1..47f14ec18 100644 --- a/Sources/PersistentCollections/Node/_AncestorOffsets.swift +++ b/Sources/PersistentCollections/Node/_AncestorSlots.swift @@ -52,7 +52,7 @@ extension _AncestorSlots { return _Slot((path &>> level.shift) & _Bucket.bitMask) } set { - assert(newValue._value < UInt.bitWidth) + assert(newValue._value < _Bitmap.capacity) assert(self[level] == .zero) path |= (UInt(truncatingIfNeeded: newValue._value) &<< level.shift) } diff --git a/Sources/PersistentCollections/Node/_Bitmap.swift b/Sources/PersistentCollections/Node/_Bitmap.swift index 946cba232..7f3267a8f 100644 --- a/Sources/PersistentCollections/Node/_Bitmap.swift +++ b/Sources/PersistentCollections/Node/_Bitmap.swift @@ -116,9 +116,14 @@ extension _Bitmap { } extension _Bitmap { + @inlinable @inline(__always) + internal func isSubset(of other: Self) -> Bool { + _value & ~other._value == 0 + } + @inlinable @inline(__always) internal func isDisjoint(with other: Self) -> Bool { - _value & other._value != 0 + _value & other._value == 0 } @inlinable @inline(__always) diff --git a/Sources/PersistentCollections/Node/_HashTreeIterator.swift b/Sources/PersistentCollections/Node/_HashTreeIterator.swift new file mode 100644 index 000000000..402e4b127 --- /dev/null +++ b/Sources/PersistentCollections/Node/_HashTreeIterator.swift @@ -0,0 +1,129 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +@usableFromInline +@frozen +internal struct _HashTreeIterator { + @usableFromInline + internal struct _Opaque { + internal var ancestorSlots: _AncestorSlots + internal var ancestorNodes: _Stack<_UnmanagedNode> + internal var level: _Level + internal var isAtEnd: Bool + + @usableFromInline + @_effects(releasenone) + internal init(_ root: _UnmanagedNode) { + self.ancestorSlots = .empty + self.ancestorNodes = _Stack(filledWith: root) + self.level = .top + self.isAtEnd = false + } + } + + @usableFromInline + internal let root: _RawStorage + + @usableFromInline + internal var node: _UnmanagedNode + + @usableFromInline + internal var slot: _Slot + + @usableFromInline + internal var endSlot: _Slot + + @usableFromInline + internal var _o: _Opaque + + @usableFromInline + @_effects(releasenone) + internal init(root: __shared _RawNode) { + self.root = root.storage + self.node = root.unmanaged + self.slot = .zero + self.endSlot = node.itemsEndSlot + self._o = _Opaque(self.node) + + if node.hasItems { return } + if node.hasChildren { + _descendToLeftmostItem(ofChildAtSlot: .zero) + } else { + self._o.isAtEnd = true + } + } +} + +extension _HashTreeIterator: IteratorProtocol { + @inlinable + internal mutating func next() -> (node: _UnmanagedNode, slot: _Slot)? { + guard slot < endSlot else { + return _next() + } + defer { slot = slot.next() } + return (node, slot) + } + + @usableFromInline + @_effects(releasenone) + internal mutating func _next() -> (node: _UnmanagedNode, slot: _Slot)? { + if _o.isAtEnd { return nil } + if node.hasChildren { + _descendToLeftmostItem(ofChildAtSlot: .zero) + slot = slot.next() + return (node, .zero) + } + while !_o.level.isAtRoot { + let nextChild = _ascend().next() + if nextChild < node.childrenEndSlot { + _descendToLeftmostItem(ofChildAtSlot: nextChild) + slot = slot.next() + return (node, .zero) + } + } + // At end + endSlot = node.itemsEndSlot + slot = endSlot + _o.isAtEnd = true + return nil + } +} + +extension _HashTreeIterator { + internal mutating func _descend(toChildSlot childSlot: _Slot) { + assert(childSlot < node.childrenEndSlot) + _o.ancestorSlots[_o.level] = childSlot + _o.ancestorNodes.push(node) + _o.level = _o.level.descend() + node = node.unmanagedChild(at: childSlot) + slot = .zero + endSlot = node.itemsEndSlot + } + + internal mutating func _ascend() -> _Slot { + assert(!_o.level.isAtRoot) + node = _o.ancestorNodes.pop() + _o.level = _o.level.ascend() + let childSlot = _o.ancestorSlots[_o.level] + _o.ancestorSlots.clear(_o.level) + return childSlot + } + + internal mutating func _descendToLeftmostItem( + ofChildAtSlot childSlot: _Slot + ) { + _descend(toChildSlot: childSlot) + while endSlot == .zero { + assert(node.hasChildren) + _descend(toChildSlot: .zero) + } + } +} diff --git a/Sources/PersistentCollections/Node/_Level.swift b/Sources/PersistentCollections/Node/_Level.swift index a4f3f7d4a..ecc6c568b 100644 --- a/Sources/PersistentCollections/Node/_Level.swift +++ b/Sources/PersistentCollections/Node/_Level.swift @@ -32,6 +32,12 @@ internal struct _Level { assert(shift <= UInt8.max) self._shift = UInt8(truncatingIfNeeded: shift) } + + @inlinable @inline(__always) + init(depth: Int) { + assert(depth > 0 && depth < _Level.limit) + self.init(shift: UInt(bitPattern: depth * _Bitmap.bitWidth)) + } } extension _Level { diff --git a/Sources/PersistentCollections/Node/_Node+Equatable.swift b/Sources/PersistentCollections/Node/_Node+Equatable.swift deleted file mode 100644 index 6eca45d01..000000000 --- a/Sources/PersistentCollections/Node/_Node+Equatable.swift +++ /dev/null @@ -1,50 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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 -// -//===----------------------------------------------------------------------===// - -// TODO: `Equatable` needs more test coverage, apart from hash-collision smoke test -extension _Node: Equatable where Value: Equatable { - @inlinable - static func == (left: _Node, right: _Node) -> Bool { - if left.raw.storage === right.raw.storage { return true } - - guard left.count == right.count else { return false } - - if left.isCollisionNode { - guard right.isCollisionNode else { return false } - return left.read { lhs in - right.read { rhs in - let l = lhs.reverseItems - let r = rhs.reverseItems - guard l.count == r.count else { return false } - for i in l.indices { - guard r.contains(where: { $0 == l[i] }) else { return false } - } - return true - } - } - } - guard !right.isCollisionNode else { return false } - - guard left.count == right.count else { return false } - return left.read { l in - right.read { r in - guard l.itemMap == r.itemMap else { return false } - guard l.childMap == r.childMap else { return false } - - guard l.reverseItems.elementsEqual(r.reverseItems, by: { $0 == $1 }) - else { return false } - - guard l.children.elementsEqual(r.children) else { return false } - return true - } - } - } -} diff --git a/Sources/PersistentCollections/Node/_Node+Invariants.swift b/Sources/PersistentCollections/Node/_Node+Invariants.swift index dc8291563..c9b18e5d8 100644 --- a/Sources/PersistentCollections/Node/_Node+Invariants.swift +++ b/Sources/PersistentCollections/Node/_Node+Invariants.swift @@ -55,7 +55,7 @@ extension _Node { if $0.isCollisionNode { precondition(count == $0.itemCount) precondition(count > 0) - let key = $0[item: .zero].key + let key = $0.collisionHash let hash = _Hash(key) precondition( hash.isEqual(to: path, upTo: level), diff --git a/Sources/PersistentCollections/Node/_Node+Lookups.swift b/Sources/PersistentCollections/Node/_Node+Lookups.swift index f74efda0f..c3c42445c 100644 --- a/Sources/PersistentCollections/Node/_Node+Lookups.swift +++ b/Sources/PersistentCollections/Node/_Node+Lookups.swift @@ -49,7 +49,7 @@ extension _Node.UnsafeHandle { ) -> (code: Int, slot: _Slot, expansionHash: _Hash) { assert(isCollisionNode) if !level.isAtBottom { - let h = _Hash(self[item: .zero].key) + let h = self.collisionHash if h != hash { return (2, .zero, h) } } // Note: this searches the items in reverse insertion order. @@ -182,21 +182,30 @@ extension _Node { } } } +} +extension _Node { @inlinable internal func containsKey( _ level: _Level, _ key: Key, _ hash: _Hash ) -> Bool { - read { - let r = $0.find(level, key, hash, forInsert: false) - switch r { - case .found: - return true - case .notFound, .newCollision, .expansion: - return false - case .descend(_, let slot): - return $0[child: slot].containsKey(level.descend(), key, hash) - } + read { $0.containsKey(level, key, hash) } + } +} + +extension _Node.UnsafeHandle { + @inlinable + internal func containsKey( + _ 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) } } } diff --git a/Sources/PersistentCollections/Node/_Node+Subtree Insertions.swift b/Sources/PersistentCollections/Node/_Node+Subtree Insertions.swift index 344abbe06..6440f1fc0 100644 --- a/Sources/PersistentCollections/Node/_Node+Subtree Insertions.swift +++ b/Sources/PersistentCollections/Node/_Node+Subtree Insertions.swift @@ -12,6 +12,65 @@ import _CollectionsUtilities extension _Node { + @inlinable + internal mutating func insert( + _ item: __owned Element, + _ level: _Level, + _ hash: _Hash + ) -> Bool { + let r = update(item.key, level, hash) + guard r.inserted else { return false } + UnsafeHandle.update(r.leaf) { + let p = $0.itemPtr(at: r.slot) + p.initialize(to: item) + } + return true + } + + @inlinable + internal mutating func update( + _ key: Key, + _ level: _Level, + _ hash: _Hash + ) -> (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) + return (false, unmanaged, slot) + case .notFound(let bucket, let slot): + ensureUniqueAndInsertItem(isUnique: isUnique, slot, bucket) { _ in } + return (true, unmanaged, slot) + case .newCollision(let bucket, let slot): + let r = ensureUniqueAndMakeNewCollision( + isUnique: isUnique, + level: level, + replacing: slot, bucket, + newHash: hash, + inserter: { _ in } + ) + return (true, r.leaf, r.slot) + case .expansion(let collisionHash): + let r = _Node.build( + level: level, + item1: { _ in }, 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].update(key, level.descend(), hash) + } + if r.inserted { count &+= 1 } + return r + } + } + + #if false @inlinable internal mutating func updateValue( _ value: __owned Value, @@ -25,7 +84,6 @@ extension _Node { switch r { case .found(_, let slot): ensureUnique(isUnique: isUnique) - _invariantCheck() // FIXME return update { let p = $0.itemPtr(at: slot) let old = p.pointee.value @@ -112,6 +170,7 @@ extension _Node { return r } } + #endif } extension _Node { diff --git a/Sources/PersistentCollections/Node/_Node+Subtree Modify.swift b/Sources/PersistentCollections/Node/_Node+Subtree Modify.swift index a10db5c4e..683bc5a38 100644 --- a/Sources/PersistentCollections/Node/_Node+Subtree Modify.swift +++ b/Sources/PersistentCollections/Node/_Node+Subtree Modify.swift @@ -121,8 +121,9 @@ extension _Node { _finalizeRemoval(.top, state.hash, at: state.path) case (false, true): // Insertion - _ = updateValue( - state.value.unsafelyUnwrapped, forKey: state.key, .top, state.hash) + let inserted = insert( + (state.key, state.value.unsafelyUnwrapped), .top, state.hash) + assert(inserted) case (false, false): // Noop break diff --git a/Sources/PersistentCollections/Node/_Node+Subtree Removals.swift b/Sources/PersistentCollections/Node/_Node+Subtree Removals.swift index 907b76b3c..4af6e0ac4 100644 --- a/Sources/PersistentCollections/Node/_Node+Subtree Removals.swift +++ b/Sources/PersistentCollections/Node/_Node+Subtree Removals.swift @@ -218,7 +218,7 @@ extension _Node { assert(isCollisionNode && hasSingletonItem) assert(isUnique()) update { - let hash = _Hash($0[item: .zero].key) + let hash = $0.collisionHash $0.itemMap = _Bitmap(hash[.top]) $0.childMap = .empty } diff --git a/Sources/PersistentCollections/Node/_Node+UnsafeHandle.swift b/Sources/PersistentCollections/Node/_Node+UnsafeHandle.swift index 79751728c..7491c85e8 100644 --- a/Sources/PersistentCollections/Node/_Node+UnsafeHandle.swift +++ b/Sources/PersistentCollections/Node/_Node+UnsafeHandle.swift @@ -156,6 +156,12 @@ extension _Node.UnsafeHandle { nonmutating set { _header.pointee.collisionCount = newValue } } + @inlinable + internal var collisionHash: _Hash { + assert(isCollisionNode) + return _Hash(self[item: .zero].key) + } + @inlinable @inline(__always) internal var _childrenStart: UnsafeMutablePointer<_Node> { _memory.assumingMemoryBound(to: _Node.self) diff --git a/Sources/PersistentCollections/Node/_Node+isDisjoint.swift b/Sources/PersistentCollections/Node/_Node+isDisjoint.swift new file mode 100644 index 000000000..3e1abb92b --- /dev/null +++ b/Sources/PersistentCollections/Node/_Node+isDisjoint.swift @@ -0,0 +1,101 @@ +//===----------------------------------------------------------------------===// +// +// 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 { + /// Returns true if `self` contains a disjoint set of keys than `other`. + /// Otherwise, returns false. + @inlinable @inline(never) + internal func isDisjoint( + _ level: _Level, + with other: _Node + ) -> Bool { + if self.raw.storage === other.raw.storage { return count == 0 } + + if self.isCollisionNode { + return _isDisjointCollision(level, with: other) + } + if other.isCollisionNode { + return other._isDisjointCollision(level, with: other) + } + + return self.read { l in + other.read { r in + let lmap = l.itemMap.union(l.childMap) + let rmap = r.itemMap.union(r.childMap) + if lmap.isDisjoint(with: rmap) { return true } + + 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) { + let lslot = l.itemMap.slot(of: bucket) + let hash = _Hash(l[item: lslot].key) + let rslot = r.childMap.slot(of: bucket) + let found = r[child: rslot].containsKey( + level.descend(), + l[item: lslot].key, + hash) + if found { return false } + } + 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) + let found = l[child: lslot].containsKey( + level.descend(), + r[item: rslot].key, + hash) + if found { return false } + } + for bucket in l.childMap.intersection(r.childMap) { + let lslot = l.childMap.slot(of: bucket) + let rslot = r.childMap.slot(of: bucket) + guard + l[child: lslot].isDisjoint(level.descend(), with: r[child: rslot]) + else { return false } + } + return true + } + } + } + + @inlinable @inline(never) + internal func _isDisjointCollision( + _ 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 } + let litems = l.reverseItems + let ritems = r.reverseItems + return litems.allSatisfy { li in + !ritems.contains { ri in li.key == ri.key } + } + } + } + } + return read { + let items = $0.reverseItems + let hash = $0.collisionHash + return items.indices.allSatisfy { + !other.containsKey(level, items[$0].key, hash) + } + } + } +} diff --git a/Sources/PersistentCollections/Node/_Node+isEqual.swift b/Sources/PersistentCollections/Node/_Node+isEqual.swift new file mode 100644 index 000000000..8a2e1e3d0 --- /dev/null +++ b/Sources/PersistentCollections/Node/_Node+isEqual.swift @@ -0,0 +1,61 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +// TODO: `Equatable` needs more test coverage, apart from hash-collision smoke test +extension _Node { + @inlinable + internal func isEqual( + to other: _Node, + by areEquivalent: (Value, Value2) -> Bool + ) -> Bool { + if self.raw.storage === other.raw.storage { return true } + + guard self.count == other.count else { return false } + + if self.isCollisionNode { + guard other.isCollisionNode else { return false } + return self.read { lhs in + other.read { rhs in + guard lhs.collisionHash == rhs.collisionHash else { return false } + let l = lhs.reverseItems + let r = rhs.reverseItems + guard l.count == r.count else { return false } + for i in l.indices { + let found = r.contains { + l[i].key == $0.key && areEquivalent(l[i].value, $0.value) + } + guard found else { return false } + } + return true + } + } + } + guard !other.isCollisionNode else { return false } + + return self.read { l in + other.read { r in + guard l.itemMap == r.itemMap else { return false } + guard l.childMap == r.childMap else { return false } + + guard l.reverseItems.elementsEqual( + r.reverseItems, + by: { $0.key == $1.key && areEquivalent($0.value, $1.value) }) + else { return false } + + let lc = l.children + let rc = r.children + return lc.elementsEqual( + rc, + by: { $0.isEqual(to: $1, by: areEquivalent) }) + } + } + } +} diff --git a/Sources/PersistentCollections/Node/_Node+isSubset.swift b/Sources/PersistentCollections/Node/_Node+isSubset.swift new file mode 100644 index 000000000..9a1a3eafc --- /dev/null +++ b/Sources/PersistentCollections/Node/_Node+isSubset.swift @@ -0,0 +1,71 @@ +//===----------------------------------------------------------------------===// +// +// 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 { + /// Returns true if `self` contains a subset of the keys in `other`. + /// Otherwise, returns false. + @inlinable @inline(never) + internal func isSubset( + _ level: _Level, + of other: _Node + ) -> Bool { + if self.raw.storage === other.raw.storage { return true } + 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) + } + } + } + + guard !other.isCollisionNode else { return false } + + return self.read { l in + other.read { r in + guard l.childMap.isSubset(of: r.childMap) else { return false } + guard l.itemMap.isSubset(of: r.itemMap.union(r.childMap)) else { + return false + } + for bucket 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 + r[child: rslot].containsKey( + level.descend(), + l[item: lslot].key, + hash) + else { return false } + } + } + + for bucket in l.childMap { + let lslot = l.childMap.slot(of: bucket) + let rslot = r.childMap.slot(of: bucket) + guard l[child: lslot].isSubset(level.descend(), of: r[child: rslot]) + else { return false } + } + return true + } + } + } +} diff --git a/Sources/PersistentCollections/Node/_Node.swift b/Sources/PersistentCollections/Node/_Node.swift index f225bf84a..de2159cab 100644 --- a/Sources/PersistentCollections/Node/_Node.swift +++ b/Sources/PersistentCollections/Node/_Node.swift @@ -119,3 +119,15 @@ extension _Node { read { $0.isAtrophiedNode } } } + +extension _Node { + @inlinable + internal var initialVersionNumber: UInt { + // Ideally we would simply just generate a true random number, but the + // memory address of the root node is a reasonable substitute. + // Alternatively, we could use a per-thread counter along with a thread + // id, or some sort of global banks of atomic integer counters. + let address = Unmanaged.passUnretained(raw.storage).toOpaque() + return UInt(bitPattern: address) + } +} diff --git a/Sources/PersistentCollections/Node/_Stack.swift b/Sources/PersistentCollections/Node/_Stack.swift new file mode 100644 index 000000000..d862e92be --- /dev/null +++ b/Sources/PersistentCollections/Node/_Stack.swift @@ -0,0 +1,116 @@ +//===----------------------------------------------------------------------===// +// +// 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 fixed-size array of just enough size to hold an ancestor path in a +/// `PersistentDictionary`. +@usableFromInline +@frozen +internal struct _Stack { +#if arch(x86_64) || arch(arm64) + @inlinable + @inline(__always) + internal static var capacity: Int { 13 } + + // xxxxx xxxxx xxxxx xxxxx xxxxx xxxxx xxxxx xxxxx xxxxx xxxxx xxxxx xxxxx xxxx + @usableFromInline + internal var _contents: ( + Element, Element, Element, Element, + Element, Element, Element, Element, + Element, Element, Element, Element, + Element + ) +#else + @inlinable + @inline(__always) + internal static var capacity: Int { 7 } + + // xxxxx xxxxx xxxxx xxxxx xxxxx xxxxx xx + @usableFromInline + internal var _contents: ( + Element, Element, Element, Element, + Element, Element, Element + ) +#endif + + @usableFromInline + internal var _count: UInt8 + + @inlinable + internal init(filledWith value: Element) { + assert(Self.capacity == _Level.limit) +#if arch(x86_64) || arch(arm64) + _contents = ( + value, value, value, value, + value, value, value, value, + value, value, value, value, + value + ) +#else + _contents = ( + value, value, value, value, + value, value, value + ) +#endif + self._count = 0 + } + + @inlinable + @inline(__always) + internal var capacity: Int { Self.capacity } + + @inlinable + @inline(__always) + internal var count: Int { Int(truncatingIfNeeded: _count) } + + @inlinable + @inline(__always) + internal var isEmpty: Bool { _count == 0 } + + @inlinable + subscript(level: UInt8) -> Element { + mutating get { + assert(level < _count) + return withUnsafeBytes(of: &_contents) { buffer in + // Homogeneous tuples are layout compatible with their element type + let start = buffer.baseAddress!.assumingMemoryBound(to: Element.self) + return start[Int(truncatingIfNeeded: level)] + } + } + set { + assert(level < capacity) + withUnsafeMutableBytes(of: &_contents) { buffer in + // Homogeneous tuples are layout compatible with their element type + let start = buffer.baseAddress!.assumingMemoryBound(to: Element.self) + start[Int(truncatingIfNeeded: level)] = newValue + } + } + } + + @inlinable + mutating func push(_ item: Element) { + assert(_count < capacity) + self[_count] = item + _count &+= 1 + } + + @inlinable + mutating func pop() -> Element { + assert(_count > 0) + defer { _count &-= 1 } + return self[_count &- 1] + } + + @inlinable + mutating func peek() -> Element { + assert(count > 0) + return self[_count &- 1] + } +} diff --git a/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Collection.swift b/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Collection.swift index 32f1b8abc..99ad5f5df 100644 --- a/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Collection.swift +++ b/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Collection.swift @@ -144,4 +144,6 @@ extension PersistentDictionary: BidirectionalCollection { precondition(_isValid(start) && _isValid(end), "Invalid index") return _root.raw.distance(.top, from: start._path, to: end._path) } + + // FIXME: Implement index(_:offsetBy:), index(_:offsetBy:limitedBy:) } diff --git a/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Debugging.swift b/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Debugging.swift index d6d0ef13c..30fc20a55 100644 --- a/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Debugging.swift +++ b/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Debugging.swift @@ -16,6 +16,11 @@ extension PersistentDictionary { _isCollectionsInternalCheckingEnabled } + @inlinable + public func _invariantCheck() { + _root._fullInvariantCheck(.top, _Hash(_value: 0)) + } + public func _dump(iterationOrder: Bool = false) { _root.dump(iterationOrder: iterationOrder) } diff --git a/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Equatable.swift b/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Equatable.swift index 91ff670d5..f4d8f23aa 100644 --- a/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Equatable.swift +++ b/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Equatable.swift @@ -12,6 +12,6 @@ extension PersistentDictionary: Equatable where Value: Equatable { @inlinable public static func == (left: Self, right: Self) -> Bool { - left._root == right._root + left._root.isEqual(to: right._root, by: { $0 == $1 }) } } diff --git a/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Filter.swift b/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Filter.swift index 293f39280..911a828fc 100644 --- a/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Filter.swift +++ b/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Filter.swift @@ -17,14 +17,10 @@ extension PersistentDictionary { var result: PersistentDictionary = [:] for item in self { guard try isIncluded(item) else { continue } - // FIXME: We could recover the key's hash from self. - // (As many bits of it as we need to insert it into the new tree.) - // However, this requires a series of `UInt32._bit(ranked:)` invocations - // that may or may not be meaningfully better than simply rehashing the - // key. + // FIXME: We could do this as a structural transformation. let hash = _Hash(item.key) - let r = result._root.updateValue(item.value, forKey: item.key, .top, hash) - assert(r == nil) + let inserted = result._root.insert(item, .top, hash) + assert(inserted) } return result } diff --git a/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Initializers.swift b/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Initializers.swift index 5fbc30661..354e21c1b 100644 --- a/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Initializers.swift +++ b/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Initializers.swift @@ -27,8 +27,8 @@ extension PersistentDictionary { self.init() for (key, value) in keysAndValues { let hash = _Hash(key) - let unique = nil == _root.updateValue(value, forKey: key, .top, hash) - precondition(unique, "Duplicate key: '\(key)'") + let inserted = _root.insert((key, value), .top, hash) + precondition(inserted, "Duplicate key: '\(key)'") } _invariantCheck() } diff --git a/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+MapValues.swift b/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+MapValues.swift index 16008a83d..bbbac4163 100644 --- a/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+MapValues.swift +++ b/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+MapValues.swift @@ -22,17 +22,13 @@ 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, value) in self { - guard let value = try transform(value) else { continue } - // FIXME: We could recover the key's hash from self. - // (As many bits of it as we need to insert it into the new tree.) - // However, this requires a series of `UInt32._bit(ranked:)` invocations - // that may or may not be meaningfully better than simply rehashing the - // key. + for (key, v) in self { + guard let value = try transform(v) else { continue } let hash = _Hash(key) - let r = result._root.updateValue(value, forKey: key, .top, hash) - assert(r == nil) + let inserted = result._root.insert((key, value), .top, hash) + assert(inserted) } return result } diff --git a/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Merge.swift b/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Merge.swift index bbedd0f99..410ec5813 100644 --- a/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Merge.swift +++ b/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Merge.swift @@ -15,6 +15,7 @@ extension PersistentDictionary { _ keysAndValues: __owned S, uniquingKeysWith combine: (Value, Value) throws -> Value ) rethrows where S.Element == (Key, Value) { + // FIXME: Do a structural merge for (key, value) in keysAndValues { try self.updateValue(forKey: key) { target in if let old = target { diff --git a/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Sequence.swift b/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Sequence.swift index 7de86638e..1ab3cbe65 100644 --- a/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Sequence.swift +++ b/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary+Sequence.swift @@ -12,43 +12,21 @@ extension PersistentDictionary: Sequence { public typealias Element = (key: Key, value: Value) + @frozen public struct Iterator { // Fixed-stack iterator for traversing a hash tree. // The iterator performs a pre-order traversal, with items at a node visited // before any items within children. @usableFromInline - typealias _ItemBuffer = ReversedCollection> + internal typealias _UnsafeHandle = _Node.UnsafeHandle @usableFromInline - typealias _ChildBuffer = UnsafeBufferPointer<_Node> - - @usableFromInline - internal var _root: _Node - - @usableFromInline - internal var _itemIterator: _ItemBuffer.Iterator? - - @usableFromInline - internal var _pathTop: _ChildBuffer.Iterator? - - @usableFromInline - internal var _pathRest: [_ChildBuffer.Iterator] + internal var _it: _HashTreeIterator @inlinable - internal init(_root: _Node) { - self._root = _root - self._pathRest = [] - self._pathRest.reserveCapacity(_Level.limit) - - // FIXME: This is illegal, as it escapes pointers to _root contents - // outside the closure passed to `read`. :-( - self._itemIterator = _root.read { - $0.hasItems ? $0.reverseItems.reversed().makeIterator() : nil - } - self._pathTop = _root.read { - $0.hasChildren ? $0.children.makeIterator() : nil - } + internal init(_root: _RawNode) { + self._it = _HashTreeIterator(root: _root) } } @@ -59,7 +37,7 @@ extension PersistentDictionary: Sequence { @inlinable public __consuming func makeIterator() -> Iterator { - return Iterator(_root: _root) + return Iterator(_root: _root.raw) } } @@ -68,30 +46,7 @@ extension PersistentDictionary.Iterator: IteratorProtocol { @inlinable public mutating func next() -> Element? { - if let item = _itemIterator?.next() { - return item - } - - _itemIterator = nil - - while _pathTop != nil { - guard let nextNode = _pathTop!.next() else { - _pathTop = _pathRest.popLast() - continue - } - if nextNode.read({ $0.hasChildren }) { - _pathRest.append(_pathTop!) - _pathTop = nextNode.read { $0.children.makeIterator() } // 💥ILLEGAL - } - if nextNode.read({ $0.hasItems }) { - _itemIterator = nextNode.read { $0.reverseItems.reversed().makeIterator() } // 💥ILLEGAL - return _itemIterator!.next() - } - } - - assert(_itemIterator == nil) - assert(_pathTop == nil) - assert(_pathRest.isEmpty) - return nil + guard let (node, slot) = _it.next() else { return nil } + return _UnsafeHandle.read(node) { $0[item: slot] } } } diff --git a/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary.swift b/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary.swift index 73736364c..b6907fdbd 100644 --- a/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary.swift +++ b/Sources/PersistentCollections/PersistentDictionary/PersistentDictionary.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -public struct PersistentDictionary where Key: Hashable { +public struct PersistentDictionary { @usableFromInline internal typealias _Node = PersistentCollections._Node @@ -30,19 +30,11 @@ public struct PersistentDictionary where Key: Hashable { @inlinable internal init(_new: _Node) { - // Ideally we would simply just generate a true random number, but the - // memory address of the root node is a reasonable substitute. - let address = Unmanaged.passUnretained(_new.raw.storage).toOpaque() - self.init(_root: _new, version: UInt(bitPattern: address)) + self.init(_root: _new, version: _new.initialVersionNumber) } } extension PersistentDictionary { - @inlinable - public func _invariantCheck() { - _root._fullInvariantCheck(.top, _Hash(_value: 0)) - } - /// Accesses the value associated with the given key for reading and writing. /// /// This *key-based* subscript returns the value for the given key if the key @@ -110,7 +102,8 @@ extension PersistentDictionary { } set { if let value = newValue { - updateValue(value, forKey: key) + _updateValue(value, forKey: key) + _invalidateIndices() } else { removeValue(forKey: key) } @@ -202,13 +195,14 @@ extension PersistentDictionary { _root.get(.top, key, _Hash(key)) ?? defaultValue() } set { - updateValue(newValue, forKey: key) + _updateValue(newValue, forKey: key) + _invalidateIndices() } @inline(__always) // https://github.com/apple/swift-collections/issues/164 _modify { + _invalidateIndices() var state = _root.prepareDefaultedValueUpdate( .top, key, defaultValue, _Hash(key)) - if state.inserted { _invalidateIndices() } defer { _root.finalizeDefaultedValueUpdate(state) } @@ -284,9 +278,35 @@ extension PersistentDictionary { public mutating func updateValue( _ value: __owned Value, forKey key: Key ) -> Value? { - let old = _root.updateValue(value, forKey: key, .top, _Hash(key)) - if old == nil { _invalidateIndices() } - return old + let hash = _Hash(key) + let r = _root.update(key, .top, hash) + _invalidateIndices() + return _Node.UnsafeHandle.update(r.leaf) { + let p = $0.itemPtr(at: r.slot) + if r.inserted { + p.initialize(to: (key, value)) + return nil + } + let old = p.pointee.value + p.pointee.value = value + return old + } + } + + @inlinable + internal mutating func _updateValue( + _ value: __owned Value, forKey key: Key + ) { + let hash = _Hash(key) + let r = _root.update(key, .top, hash) + return _Node.UnsafeHandle.update(r.leaf) { + let p = $0.itemPtr(at: r.slot) + if r.inserted { + p.initialize(to: (key, value)) + } else { + p.pointee.value = value + } + } } // fluid/immutable API @@ -313,12 +333,13 @@ extension PersistentDictionary { with body: (inout Value) throws -> R ) rethrows -> R { let hash = _Hash(key) - let r = _root.insertValue(forKey: key, .top, hash, with: defaultValue) - if r.inserted { - _invalidateIndices() - } + let r = _root.update(key, .top, hash) return try _Node.UnsafeHandle.update(r.leaf) { - try body(&$0[item: r.slot].value) + let p = $0.itemPtr(at: r.slot) + if r.inserted { + p.initialize(to: (key, defaultValue())) + } + return try body(&p.pointee.value) } } @@ -361,9 +382,8 @@ extension PersistentDictionary { @inlinable @discardableResult public mutating func removeValue(forKey key: Key) -> Value? { - let old = _root.remove(key, .top, _Hash(key))?.value - if old != nil { _invalidateIndices() } - return old + _invalidateIndices() + return _root.remove(key, .top, _Hash(key))?.value } // fluid/immutable API diff --git a/Sources/PersistentCollections/PersistentSet/PersistentSet+Collection.swift b/Sources/PersistentCollections/PersistentSet/PersistentSet+Collection.swift new file mode 100644 index 000000000..9bf1e4722 --- /dev/null +++ b/Sources/PersistentCollections/PersistentSet/PersistentSet+Collection.swift @@ -0,0 +1,149 @@ +//===----------------------------------------------------------------------===// +// +// 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 { + @frozen + public struct Index { + @usableFromInline + internal let _root: _UnmanagedNode + + @usableFromInline + internal var _version: UInt + + @usableFromInline + internal var _path: _UnsafePath + + @inlinable @inline(__always) + internal init( + _root: _UnmanagedNode, version: UInt, path: _UnsafePath + ) { + self._root = _root + self._version = version + self._path = path + } + } +} + +extension PersistentSet.Index: Equatable { + @inlinable + public static func ==(left: Self, right: Self) -> Bool { + precondition( + left._root == right._root && left._version == right._version, + "Indices from different set values aren't comparable") + return left._path == right._path + } +} + +extension PersistentSet.Index: Comparable { + @inlinable + public static func <(left: Self, right: Self) -> Bool { + precondition( + left._root == right._root && left._version == right._version, + "Indices from different set values aren't comparable") + return left._path < right._path + } +} + +extension PersistentSet.Index: Hashable { + @inlinable + public func hash(into hasher: inout Hasher) { + hasher.combine(_path) + } +} + +extension PersistentSet.Index: CustomStringConvertible { + public var description: String { + _path.description + } +} + +extension PersistentSet: BidirectionalCollection { + @inlinable + public var isEmpty: Bool { + _root.count == 0 + } + + @inlinable + public var count: Int { + _root.count + } + + public var startIndex: Index { + var path = _UnsafePath(root: _root.raw) + path.descendToLeftMostItem() + return Index(_root: _root.unmanaged, version: _version, path: path) + } + + @inlinable + public var endIndex: Index { + var path = _UnsafePath(root: _root.raw) + path.selectEnd() + return Index(_root: _root.unmanaged, version: _version, path: path) + } + + @inlinable @inline(__always) + internal func _isValid(_ i: Index) -> Bool { + _root.isIdentical(to: i._root) && i._version == self._version + } + + @inlinable @inline(__always) + internal mutating func _invalidateIndices() { + _version &+= 1 + } + + /// Accesses the key-value pair at the specified position. + @inlinable + public subscript(i: Index) -> Element { + precondition(_isValid(i), "Invalid index") + precondition(i._path.isOnItem, "Can't get element at endIndex") + return _Node.UnsafeHandle.read(i._path.node) { + $0[item: i._path.currentItemSlot].key + } + } + + @inlinable + public func formIndex(after i: inout Index) { + precondition(_isValid(i), "Invalid index") + guard i._path.findSuccessorItem(under: _root.raw) else { + preconditionFailure("The end index has no successor") + } + } + + @inlinable + public func formIndex(before i: inout Index) { + precondition(_isValid(i), "Invalid index") + guard i._path.findPredecessorItem(under: _root.raw) else { + preconditionFailure("The start index has no predecessor") + } + } + + @inlinable @inline(__always) + public func index(after i: Index) -> Index { + var i = i + formIndex(after: &i) + return i + } + + @inlinable @inline(__always) + public func index(before i: Index) -> Index { + var i = i + formIndex(before: &i) + return i + } + + @inlinable + public func distance(from start: Index, to end: Index) -> Int { + precondition(_isValid(start) && _isValid(end), "Invalid index") + return _root.raw.distance(.top, from: start._path, to: end._path) + } + + // FIXME: Implement index(_:offsetBy:), index(_:offsetBy:limitedBy:) +} diff --git a/Sources/PersistentCollections/PersistentSet/PersistentSet+Equatable.swift b/Sources/PersistentCollections/PersistentSet/PersistentSet+Equatable.swift new file mode 100644 index 000000000..3e3e1d289 --- /dev/null +++ b/Sources/PersistentCollections/PersistentSet/PersistentSet+Equatable.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: Equatable { + @inlinable + public static func == (left: Self, right: Self) -> Bool { + left._root.isEqual(to: right._root, by: { _, _ in true }) + } +} diff --git a/Sources/PersistentCollections/PersistentSet/PersistentSet+Hashable.swift b/Sources/PersistentCollections/PersistentSet/PersistentSet+Hashable.swift new file mode 100644 index 000000000..c9e3c0d15 --- /dev/null +++ b/Sources/PersistentCollections/PersistentSet/PersistentSet+Hashable.swift @@ -0,0 +1,24 @@ +//===----------------------------------------------------------------------===// +// +// 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: Hashable { + @inlinable + public func hash(into hasher: inout Hasher) { + let copy = hasher + let seed = copy.finalize() + + var hash = 0 + for member in self { + hash ^= member._rawHashValue(seed: seed) + } + hasher.combine(hash) + } +} diff --git a/Sources/PersistentCollections/PersistentSet/PersistentSet+Sequence.swift b/Sources/PersistentCollections/PersistentSet/PersistentSet+Sequence.swift new file mode 100644 index 000000000..8b7b036c8 --- /dev/null +++ b/Sources/PersistentCollections/PersistentSet/PersistentSet+Sequence.swift @@ -0,0 +1,41 @@ +//===----------------------------------------------------------------------===// +// +// 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: Sequence { + @frozen + public struct Iterator: IteratorProtocol { + @usableFromInline + internal typealias _UnsafeHandle = _Node.UnsafeHandle + + @usableFromInline + internal var _it: _HashTreeIterator + + @inlinable + internal init(_root: _RawNode) { + _it = _HashTreeIterator(root: _root) + } + + public mutating func next() -> Element? { + guard let (node, slot) = _it.next() else { return nil } + return _UnsafeHandle.read(node) { $0[item: slot].key } + } + } + + @inlinable + public func makeIterator() -> Iterator { + Iterator(_root: _root.raw) + } + + @inlinable + public func _customContainsEquatableElement(_ element: Element) -> Bool? { + _root.containsKey(.top, element, _Hash(element)) + } +} diff --git a/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra Initializers.swift b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra Initializers.swift new file mode 100644 index 000000000..f6c9f5ba7 --- /dev/null +++ b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra Initializers.swift @@ -0,0 +1,41 @@ +//===----------------------------------------------------------------------===// +// +// 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 init() { + self.init(_new: _Node(storage: _emptySingleton, count: 0)) + } + + @inlinable + public init(_ items: __owned S) where S.Element == Element { + if S.self == Self.self { + self = items as! Self + return + } + self.init() + for item in items { + self._insert(item) + } + } + + @inlinable + public init(_ items: __owned Self) { + self = items + } + + @inlinable + public init( + _ item: __owned PersistentDictionary.Keys + ) { + self.init(_new: item._base._root.mapValues { _ in () }) + } +} diff --git a/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra basics.swift b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra basics.swift new file mode 100644 index 000000000..341acf5c0 --- /dev/null +++ b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra basics.swift @@ -0,0 +1,69 @@ +//===----------------------------------------------------------------------===// +// +// 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: SetAlgebra { + @discardableResult + @inlinable + public mutating func insert( + _ newMember: __owned Element + ) -> (inserted: Bool, memberAfterInsert: Element) { + let hash = _Hash(newMember) + _invalidateIndices() + let r = _root.update(newMember, .top, hash) + return _Node.UnsafeHandle.update(r.leaf) { + let p = $0.itemPtr(at: r.slot) + if r.inserted { + p.initialize(to: (newMember, ())) + return (true, newMember) + } + return (false, p.pointee.key) + } + } + + @discardableResult + @inlinable + internal mutating func _insert(_ newMember: __owned Element) -> Bool { + let hash = _Hash(newMember) + let r = _root.update(newMember, .top, hash) + guard r.inserted else { return false } + _Node.UnsafeHandle.update(r.leaf) { + let p = $0.itemPtr(at: r.slot) + p.initialize(to: (newMember, ())) + } + return true + } + + + @discardableResult + @inlinable + public mutating func remove(_ member: Element) -> Element? { + let hash = _Hash(member) + _invalidateIndices() + return _root.remove(member, .top, hash)?.key + } + + @discardableResult + @inlinable + public mutating func update(with newMember: __owned Element) -> Element? { + let hash = _Hash(newMember) + let r = _root.update(newMember, .top, hash) + return _Node.UnsafeHandle.update(r.leaf) { + let p = $0.itemPtr(at: r.slot) + if r.inserted { + p.initialize(to: (newMember, ())) + return nil + } + let old = p.pointee.key + p.pointee = (newMember, ()) + return old + } + } +} diff --git a/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra formIntersection.swift b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra formIntersection.swift new file mode 100644 index 000000000..53f1e585b --- /dev/null +++ b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra formIntersection.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 { + @inlinable + public mutating func formIntersection(_ other: Self) { + fatalError("FIXME") + } +} diff --git a/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra formSymmetricDifference.swift b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra formSymmetricDifference.swift new file mode 100644 index 000000000..6cb29c34a --- /dev/null +++ b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra formSymmetricDifference.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 { + @inlinable + public mutating func formSymmetricDifference(_ other: __owned Self) { + fatalError("FIXME") + } +} diff --git a/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra formUnion.swift b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra formUnion.swift new file mode 100644 index 000000000..daeafb4b9 --- /dev/null +++ b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra formUnion.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 { + @inlinable + public mutating func formUnion(_ other: __owned Self) { + fatalError("FIXME") + } +} diff --git a/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra intersection.swift b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra intersection.swift new file mode 100644 index 000000000..bc93d3187 --- /dev/null +++ b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra intersection.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 { + @inlinable + public func intersection(_ other: Self) -> Self { + fatalError("FIXME") + } +} diff --git a/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra isDisjoint.swift b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra isDisjoint.swift new file mode 100644 index 000000000..8d499bde6 --- /dev/null +++ b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra isDisjoint.swift @@ -0,0 +1,24 @@ +//===----------------------------------------------------------------------===// +// +// 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 isDisjoint(with other: Self) -> Bool { + self._root.isDisjoint(.top, with: other._root) + } + + @inlinable + public func isDisjoint( + with other: PersistentDictionary + ) -> Bool { + self._root.isDisjoint(.top, with: other._root) + } +} diff --git a/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra isStrictSubset.swift b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra isStrictSubset.swift new file mode 100644 index 000000000..dd44e88ca --- /dev/null +++ b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra isStrictSubset.swift @@ -0,0 +1,26 @@ +//===----------------------------------------------------------------------===// +// +// 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 isStrictSubset(of other: Self) -> Bool { + guard self.count < other.count else { return false } + return isSubset(of: other) + } + + @inlinable + public func isStrictSubset( + of other: PersistentDictionary + ) -> Bool { + guard self.count < other.count else { return false } + return isSubset(of: other) + } +} diff --git a/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra isStrictSuperset.swift b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra isStrictSuperset.swift new file mode 100644 index 000000000..8bdc3d707 --- /dev/null +++ b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra isStrictSuperset.swift @@ -0,0 +1,26 @@ +//===----------------------------------------------------------------------===// +// +// 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 isStrictSuperset(of other: Self) -> Bool { + guard self.count > other.count else { return false } + return other._root.isSubset(.top, of: self._root) + } + + @inlinable + public func isStrictSuperset( + of other: PersistentDictionary + ) -> Bool { + guard self.count > other.count else { return false } + return other._root.isSubset(.top, of: self._root) + } +} diff --git a/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra isSubset.swift b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra isSubset.swift new file mode 100644 index 000000000..ab735729c --- /dev/null +++ b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra isSubset.swift @@ -0,0 +1,24 @@ +//===----------------------------------------------------------------------===// +// +// 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 isSubset(of other: Self) -> Bool { + self._root.isSubset(.top, of: other._root) + } + + @inlinable + public func isSubset( + of other: PersistentDictionary + ) -> Bool { + self._root.isSubset(.top, of: other._root) + } +} diff --git a/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra isSuperset.swift b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra isSuperset.swift new file mode 100644 index 000000000..49fdef53b --- /dev/null +++ b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra isSuperset.swift @@ -0,0 +1,24 @@ +//===----------------------------------------------------------------------===// +// +// 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 isSuperset(of other: Self) -> Bool { + other._root.isSubset(.top, of: self._root) + } + + @inlinable + public func isSuperset( + of other: PersistentDictionary + ) -> Bool { + return other._root.isSubset(.top, of: self._root) + } +} diff --git a/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra subtract.swift b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra subtract.swift new file mode 100644 index 000000000..7f50bc3ec --- /dev/null +++ b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra subtract.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 { + @inlinable + public mutating func subtract(_ other: Self) { + fatalError("FIXME") + } +} diff --git a/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra subtracting.swift b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra subtracting.swift new file mode 100644 index 000000000..d88c97379 --- /dev/null +++ b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra subtracting.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 { + @inlinable + public __consuming func subtracting(_ other: Self) -> Self { + fatalError("FIXME") + } +} diff --git a/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra symmetricDifference.swift b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra symmetricDifference.swift new file mode 100644 index 000000000..90616f9da --- /dev/null +++ b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra symmetricDifference.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 { + @inlinable + public func symmetricDifference(_ other: __owned Self) -> Self { + fatalError("FIXME") + } +} diff --git a/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra union.swift b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra union.swift new file mode 100644 index 000000000..1d208f6fc --- /dev/null +++ b/Sources/PersistentCollections/PersistentSet/PersistentSet+SetAlgebra union.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 { + @inlinable + public func union(_ other: __owned Self) -> Self { + fatalError("FIXME") + } +} diff --git a/Sources/PersistentCollections/PersistentSet/PersistentSet.swift b/Sources/PersistentCollections/PersistentSet/PersistentSet.swift new file mode 100644 index 000000000..428edf5e9 --- /dev/null +++ b/Sources/PersistentCollections/PersistentSet/PersistentSet.swift @@ -0,0 +1,40 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +public struct PersistentSet { + @usableFromInline + internal typealias _Node = PersistentCollections._Node + + @usableFromInline + internal var _root: _Node + + @usableFromInline + internal var _version: UInt + + @inlinable + internal init(_root: _Node, version: UInt) { + self._root = _root + self._version = version + } + + @inlinable + internal init(_new: _Node) { + self.init(_root: _new, version: _new.initialVersionNumber) + } +} + + +extension PersistentSet { + @inlinable + public func _invariantCheck() { + _root._fullInvariantCheck(.top, _Hash(_value: 0)) + } +} diff --git a/Sources/_CollectionsUtilities/UnsafeRawPointer extensions.swift b/Sources/_CollectionsUtilities/UnsafeRawPointer extensions.swift index a972d1a22..dfd4f455d 100644 --- a/Sources/_CollectionsUtilities/UnsafeRawPointer extensions.swift +++ b/Sources/_CollectionsUtilities/UnsafeRawPointer extensions.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -#if compiler(<5.7) // SE-0334 +#if compiler(<5.7) || (os(macOS) && compiler(<5.8)) // SE-0334 extension UnsafeRawPointer { /// Obtain the next pointer properly aligned to store a value of type `T`. /// diff --git a/Tests/PersistentCollectionsTests/PersistentCollections Smoke Tests.swift b/Tests/PersistentCollectionsTests/PersistentCollections Smoke Tests.swift index c7dc26ead..ce2981b5c 100644 --- a/Tests/PersistentCollectionsTests/PersistentCollections Smoke Tests.swift +++ b/Tests/PersistentCollectionsTests/PersistentCollections Smoke Tests.swift @@ -13,6 +13,26 @@ import _CollectionsTestSupport @testable import PersistentCollections final class PersistentDictionarySmokeTests: CollectionTestCase { + func testDummy() { + let map = PersistentDictionary( + uniqueKeysWithValues: (0 ..< 100).map { ($0, 2 * $0) }) + var it = map.makeIterator() + var seen: Set = [] + while let item = it.next() { + if !seen.insert(item.key).inserted { + print(item) + } + } + expectEqual(seen.count, 100) + print("---") + for item in map { + if seen.remove(item.key) == nil { + print(item) + } + } + expectEqual(seen.count, 0) + } + func testSubscriptAdd() { var map: PersistentDictionary = [1: "a", 2: "b"] @@ -129,12 +149,7 @@ final class PersistentDictionarySmokeTests: CollectionTestCase { var map3: PersistentDictionary = map2 for index in 0..( var dict = dict var seen: Set = [] var mismatches: [(key: Key, map: Value?, dict: Value?)] = [] - + var dupes: [(key: Key, map: Value)] = [] for (key, value) in map { if !seen.insert(key).inserted { - mismatches.append((key, value, nil)) + dupes.append((key, value)) } else { let expected = dict.removeValue(forKey: key) if value != expected { @@ -169,12 +169,15 @@ func expectEqualDictionaries( for (key, value) in dict { mismatches.append((key, nil, value)) } - if !mismatches.isEmpty { - let msg = mismatches.lazy.map { k, m, d in + if !mismatches.isEmpty || !dupes.isEmpty { + let msg1 = mismatches.lazy.map { k, m, d in "\n \(k): \(m == nil ? "nil" : "\(m!)") vs \(d == nil ? "nil" : "\(d!)")" }.joined(separator: "") + let msg2 = dupes.lazy.map { k, v in + "\n \(k): \(v) (duped)" + }.joined(separator: "") _expectFailure( - "\n\(mismatches.count) mismatches (actual vs expected):\(msg)", + "\n\(mismatches.count) mismatches (actual vs expected):\(msg1)\(msg2)", message, trapping: trapping, file: file, line: line) } }