Skip to content

Commit 5690056

Browse files
committed
Add cursor type for imperative tree modification.
Key Changes: - Implement _BTree.Cursor for performing efficient mutations on a tree Minor Changes: - New implementation of _BTree.Index and iteration
1 parent 9b470c8 commit 5690056

26 files changed

+1153
-566
lines changed

Sources/SortedCollections/BTree/_BTree+BidirectionalCollection.swift

Lines changed: 55 additions & 208 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@
99
//
1010
//===----------------------------------------------------------------------===//
1111

12+
// This contains `_BTree`'s general implementation of BidirectionalCollection.
13+
// These operations are bounds in contrast to most other methods on _BTree as
14+
// they are designed to be easily propogated to a higher-level data type.
15+
// However, they still do not perform index validation
16+
1217
extension _BTree: BidirectionalCollection {
1318
/// The total number of elements contained within the BTree
1419
/// - Complexity: O(1)
@@ -26,36 +31,45 @@ extension _BTree: BidirectionalCollection {
2631
/// - Complexity: O(`log n`)
2732
@inlinable
2833
internal var startIndex: Index {
29-
if count == 0 { return Index(nil, forTree: self) }
30-
var depth: UInt8 = 0
31-
var currentNode = self.root
32-
while !currentNode.read({ $0.isLeaf }) {
33-
// TODO: figure out how to avoid the swift retain here
34-
currentNode = currentNode.read({ $0[childAt: 0] })
35-
depth += 1
34+
if count == 0 { return endIndex }
35+
var depth: Int8 = 0
36+
var currentNode: Unmanaged = .passUnretained(self.root.storage)
37+
while true {
38+
let shouldStop: Bool = currentNode._withUnsafeGuaranteedRef {
39+
$0.read { handle in
40+
if handle.isLeaf {
41+
return true
42+
} else {
43+
depth += 1
44+
currentNode = .passUnretained(handle[childAt: 0].storage)
45+
return false
46+
}
47+
}
48+
}
49+
50+
if shouldStop { break }
3651
}
3752

38-
let path = UnsafePath(
53+
return Index(
3954
node: currentNode,
4055
slot: 0,
4156
childSlots: FixedSizeArray(repeating: 0, depth: depth),
42-
offset: 0
57+
offset: 0,
58+
forTree: self
4359
)
44-
45-
return Index(path, forTree: self)
4660
}
4761

4862
/// Returns a sentinel value for the last element
4963
/// - Complexity: O(1)
5064
@inlinable
51-
internal var endIndex: Index { Index(nil, forTree: self) }
52-
53-
/// Gets the effective offset of an index
54-
/// - Warning: this does not
55-
@inlinable
56-
@inline(__always)
57-
internal func offset(of index: Index) -> Int {
58-
return index.path?.offset ?? self.count
65+
internal var endIndex: Index {
66+
Index(
67+
node: .passUnretained(self.root.storage),
68+
slot: -1,
69+
childSlots: Index.Offsets(repeating: 0),
70+
offset: self.count,
71+
forTree: self
72+
)
5973
}
6074

6175
/// Returns the distance between two indices.
@@ -67,27 +81,25 @@ extension _BTree: BidirectionalCollection {
6781
/// - Complexity: O(1)
6882
@inlinable
6983
internal func distance(from start: Index, to end: Index) -> Int {
70-
return self.offset(of: end) - self.offset(of: start)
84+
return end.offset - start.offset
7185
}
7286

7387
/// Replaces the given index with its successor.
7488
/// - Parameter index: A valid index of the collection. i must be less than endIndex.
7589
/// - Complexity: O(`log n`) in the worst-case.
7690
@inlinable
7791
internal func formIndex(after index: inout Index) {
78-
guard var path = index.path else {
79-
preconditionFailure("Attempt to advance out of collection bounds.")
80-
}
92+
precondition(index.offset < self.count,
93+
"Attempt to advance out of collection bounds.")
8194

82-
let shouldSeekWithinLeaf = path.readNode {
83-
$0.isLeaf && _fastPath(path.slot + 1 < $0.elementCount)
95+
let shouldSeekWithinLeaf = index.readNode {
96+
$0.isLeaf && _fastPath(index.slot + 1 < $0.elementCount)
8497
}
8598

8699
if shouldSeekWithinLeaf {
87100
// Continue searching within the same leaf
88-
path.slot += 1
89-
path.offset += 1
90-
index.path = path
101+
index.slot += 1
102+
index.offset += 1
91103
} else {
92104
self.formIndex(&index, offsetBy: 1)
93105
}
@@ -109,7 +121,8 @@ extension _BTree: BidirectionalCollection {
109121
/// - Complexity: O(`log n`) in the worst-case.
110122
@inlinable
111123
internal func formIndex(before index: inout Index) {
112-
assert(!self.isEmpty && self.offset(of: index) != 0, "Attempt to advance out of collection bounds.")
124+
precondition(!self.isEmpty && index.offset != 0,
125+
"Attempt to advance out of collection bounds.")
113126
// TODO: implement more efficient logic to better move through the tree
114127
self.formIndex(&index, offsetBy: -1)
115128
}
@@ -135,29 +148,29 @@ extension _BTree: BidirectionalCollection {
135148
/// - Complexity: O(`log n`) in the worst-case.
136149
@inlinable
137150
internal func formIndex(_ i: inout Index, offsetBy distance: Int) {
138-
let newIndex = self.offset(of: i) + distance
139-
assert(0 <= newIndex && newIndex <= self.count, "Attempt to advance out of collection bounds.")
151+
let newIndex = i.offset + distance
152+
precondition(0 <= newIndex && newIndex <= self.count,
153+
"Attempt to advance out of collection bounds.")
140154

141155
if newIndex == self.count {
142-
i.path = nil
156+
i = endIndex
143157
return
144158
}
145159

146160
// TODO: optimization for searching within children
147161

148-
if var path = i.path, path.readNode({ $0.isLeaf }) {
162+
if i != endIndex && i.readNode({ $0.isLeaf }) {
149163
// Check if the target element will be in the same node
150-
let targetSlot = path.slot + distance
151-
if 0 <= targetSlot && targetSlot < path.readNode({ $0.elementCount }) {
152-
path.slot = targetSlot
153-
path.offset = newIndex
154-
i.path = path
164+
let targetSlot = i.slot + distance
165+
if 0 <= targetSlot && targetSlot < i.readNode({ $0.elementCount }) {
166+
i.slot = targetSlot
167+
i.offset = newIndex
155168
return
156169
}
157170
}
158171

159172
// Otherwise, re-seek
160-
i = Index(self.path(atOffset: newIndex), forTree: self)
173+
i = self.index(atOffset: newIndex)
161174
}
162175

163176
/// Returns an index that is the specified distance from the given index.
@@ -175,177 +188,11 @@ extension _BTree: BidirectionalCollection {
175188
return newIndex
176189
}
177190

178-
/// Offsets the given index by the specified distance, or so that it equals the given limiting index.
179-
///
180-
/// - Parameters:
181-
/// - i: A valid index of the collection.
182-
/// - distance: The distance to offset `i`.
183-
/// - limit: A valid index of the collection to use as a limit. If `distance > 0`, a limit that is
184-
/// less than `i` has no effect. Likewise, if `distance < 0`, a limit that is greater than `i`
185-
/// has no effect.
186-
/// - Returns: `true` if `i` has been offset by exactly `distance` steps without going beyond
187-
/// `limit`; otherwise, `false`. When the return value is `false`, the value of `i` is equal
188-
/// to `limit`.
189-
/// - Complexity: O(`log n`) in the worst-case.
190-
@inlinable
191-
internal func formIndex(_ i: inout Index, offsetBy distance: Int, limitedBy limit: Index) -> Bool {
192-
let distanceToLimit = self.distance(from: i, to: limit)
193-
if distance < 0 ? distanceToLimit > distance : distanceToLimit < distance {
194-
self.formIndex(&i, offsetBy: distanceToLimit)
195-
return false
196-
} else {
197-
self.formIndex(&i, offsetBy: distance)
198-
return true
199-
}
200-
}
201-
202-
/// Returns an index that is the specified distance from the given index, unless that distance
203-
/// is beyond a given limiting index.
204-
///
205-
/// - Parameters:
206-
/// - i: A valid index of the collection.
207-
/// - distance: The distance to offset `i`.
208-
/// - limit: A valid index of the collection to use as a limit. If `distance > 0`, a `limit`
209-
/// that is less than `i` has no effect. Likewise, if `distance < 0`, a `limit` that is
210-
/// greater twhan `i` has no effect.
211-
/// - Returns: An index offset by `distance` from the index `i`, unless that index would
212-
/// be beyond `limit` in the direction of movement. In that case, the method returns `nil`.
213-
@inlinable
214-
internal func index(_ i: Index, offsetBy distance: Int, limitedBy limit: Index) -> Index? {
215-
let distanceToLimit = self.distance(from: i, to: limit)
216-
if distance < 0 ? distanceToLimit > distance : distanceToLimit < distance {
217-
return nil
218-
} else {
219-
var newIndex = i
220-
self.formIndex(&newIndex, offsetBy: distance)
221-
return newIndex
222-
}
223-
}
224-
225191
@inlinable
226192
@inline(__always)
227193
internal subscript(index: Index) -> Element {
228-
assert(index.path != nil, "Attempt to subscript out of range index.")
229-
return index.path.unsafelyUnwrapped.element
230-
}
231-
}
232-
233-
// MARK: Custom Indexing Operations
234-
extension _BTree {
235-
/// Returns a path to the key at absolute offset `i`.
236-
/// - Parameter offset: 0-indexed offset within BTree bounds, else may panic.
237-
/// - Returns: the index of the appropriate element.
238-
/// - Complexity: O(`log n`)
239-
240-
241-
/// Obtains the start index for a key (or where it would exist).
242-
@inlinable
243-
internal func startIndex(forKey key: Key) -> Index {
244-
var childSlots = UnsafePath.Offsets(repeating: 0)
245-
var targetSlot: Int = 0
246-
var offset = 0
247-
248-
func search(in node: Node) -> Unmanaged<Node.Storage>? {
249-
node.read({ handle in
250-
let slot = handle.startSlot(forKey: key)
251-
if slot < handle.elementCount {
252-
if handle.isLeaf {
253-
offset += slot
254-
targetSlot = slot
255-
return .passUnretained(node.storage)
256-
} else {
257-
// Calculate offset by summing previous subtrees
258-
for i in 0...slot {
259-
offset += handle[childAt: i].read({ $0.subtreeCount })
260-
}
261-
262-
let currentOffset = offset
263-
let currentDepth = childSlots.depth
264-
childSlots.append(UInt16(slot))
265-
266-
if let foundEarlier = search(in: handle[childAt: slot]) {
267-
return foundEarlier
268-
} else {
269-
childSlots.depth = currentDepth
270-
targetSlot = slot
271-
offset = currentOffset
272-
273-
return .passUnretained(node.storage)
274-
}
275-
}
276-
} else {
277-
// Start index exceeds node and is therefore not in this.
278-
return nil
279-
}
280-
})
281-
}
282-
283-
if let targetChild = search(in: self.root) {
284-
return Index(UnsafePath(
285-
node: targetChild,
286-
slot: targetSlot,
287-
childSlots: childSlots,
288-
offset: offset
289-
), forTree: self)
290-
} else {
291-
return Index(nil, forTree: self)
292-
}
194+
// Ensure we don't attempt to dereference the endIndex
195+
precondition(index != endIndex, "Attempt to subscript out of range index.")
196+
return index.element
293197
}
294-
295-
/// Obtains the last index at which a value less than or equal to the key appears.
296-
@inlinable
297-
internal func lastIndex(forKey key: Key) -> Index {
298-
var childSlots = UnsafePath.Offsets(repeating: 0)
299-
var targetSlot: Int = 0
300-
var offset = 0
301-
302-
func search(in node: Node) -> Unmanaged<Node.Storage>? {
303-
node.read({ handle in
304-
let slot = handle.endSlot(forKey: key) - 1
305-
if slot > 0 {
306-
// Sanity Check
307-
assert(slot < handle.elementCount, "Slot out of bounds.")
308-
309-
if handle.isLeaf {
310-
offset += slot
311-
targetSlot = slot
312-
return .passUnretained(node.storage)
313-
} else {
314-
for i in 0...slot {
315-
offset += handle[childAt: i].read({ $0.subtreeCount })
316-
}
317-
318-
let currentOffset = offset
319-
let currentDepth = childSlots.depth
320-
childSlots.append(UInt16(slot + 1))
321-
322-
if let foundLater = search(in: handle[childAt: slot + 1]) {
323-
return foundLater
324-
} else {
325-
childSlots.depth = currentDepth
326-
targetSlot = slot
327-
offset = currentOffset
328-
329-
return .passUnretained(node.storage)
330-
}
331-
}
332-
} else {
333-
// Start index exceeds node and is therefore not in this.
334-
return nil
335-
}
336-
})
337-
}
338-
339-
if let targetChild = search(in: self.root) {
340-
return Index(UnsafePath(
341-
node: targetChild,
342-
slot: targetSlot,
343-
childSlots: childSlots,
344-
offset: offset
345-
), forTree: self)
346-
} else {
347-
return Index(nil, forTree: self)
348-
}
349-
}
350-
351198
}

Sources/SortedCollections/BTree/_BTree+Partial RangeReplaceableCollection.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,8 @@ extension _BTree {
112112
@discardableResult
113113
internal mutating func remove(at index: Index) -> Element {
114114
invalidateIndices()
115-
guard let path = index.path else { preconditionFailure("Index out of bounds.") }
116-
return self.remove(atOffset: path.offset)
115+
guard index != endIndex else { preconditionFailure("Index out of bounds.") }
116+
return self.remove(atOffset: index.offset)
117117
}
118118

119119
// MARK: Bulk Removal
@@ -128,11 +128,11 @@ extension _BTree {
128128
/// Removes the elements in the specified subrange from the collection.
129129
@inlinable
130130
internal mutating func removeSubrange(_ bounds: Range<Index>) {
131-
guard let startPath = bounds.lowerBound.path else { preconditionFailure("Index out of bounds.") }
132-
guard let _ = bounds.upperBound.path else { preconditionFailure("Index out of bounds.") }
131+
guard bounds.lowerBound != endIndex else { preconditionFailure("Index out of bounds.") }
132+
guard bounds.upperBound != endIndex else { preconditionFailure("Index out of bounds.") }
133133

134134
let rangeSize = self.distance(from: bounds.lowerBound, to: bounds.upperBound)
135-
let startOffset = startPath.offset
135+
let startOffset = bounds.lowerBound.offset
136136

137137
for _ in 0..<rangeSize {
138138
self.remove(atOffset: startOffset)

Sources/SortedCollections/BTree/_BTree+Sequence.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ extension _BTree: Sequence {
1616
internal let tree: _BTree
1717

1818
@usableFromInline
19-
internal var offsets: UnsafePath.Offsets
19+
internal var offsets: Index.Offsets
2020

2121
@usableFromInline
2222
internal var parents: FixedSizeArray<Unmanaged<Node.Storage>>
@@ -109,7 +109,7 @@ extension _BTree: Sequence {
109109
}
110110
}
111111

112-
return storage.read({ $0[elementAt: self.slot] })
112+
return storage.read({ $0[elementAtSlot: self.slot] })
113113
})
114114
}
115115
}

0 commit comments

Comments
 (0)