Skip to content

Commit c6183ee

Browse files
authored
Add RangeSet and discontiguous collection operations (#28161)
This adds the RangeSet and DiscontiguousSlice types, as well as collection operations for working with discontiguous ranges of elements. This also adds a COWLoggingArray type to the test suite to verify that mutable collection algorithms don't perform unexpected copy-on-write operations when mutating slices mid-operation.
1 parent 0626dc7 commit c6183ee

File tree

13 files changed

+1863
-17
lines changed

13 files changed

+1863
-17
lines changed

stdlib/private/StdlibCollectionUnittest/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ add_swift_target_library(swiftStdlibCollectionUnittest ${SWIFT_STDLIB_LIBRARY_BU
1212
CheckRangeReplaceableSliceType.swift
1313
CheckSequenceInstance.swift
1414
CheckSequenceType.swift
15+
COWLoggingArray.swift
1516
LoggingWrappers.swift
1617
MinimalCollections.swift
1718
RangeSelection.swift
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import StdlibUnittest
2+
3+
fileprivate var COWLoggingArray_CopyCount = 0
4+
5+
public func expectNoCopyOnWrite<T>(
6+
_ elements: [T],
7+
_ message: @autoclosure () -> String = "",
8+
stackTrace: SourceLocStack = SourceLocStack(),
9+
showFrame: Bool = true,
10+
file: String = #file,
11+
line: UInt = #line,
12+
_ body: (inout COWLoggingArray<T>) -> Void
13+
) {
14+
let copyCountBeforeBody = COWLoggingArray_CopyCount
15+
var loggingArray = COWLoggingArray(elements)
16+
body(&loggingArray)
17+
expectEqual(copyCountBeforeBody, COWLoggingArray_CopyCount, message(),
18+
stackTrace: stackTrace.pushIf(showFrame, file: file, line: line),
19+
showFrame: false)
20+
}
21+
22+
public struct COWLoggingArray<Element> {
23+
var storage: Storage
24+
25+
class Storage {
26+
var buffer: UnsafeMutableBufferPointer<Element>
27+
var count: Int
28+
var capacity: Int {
29+
buffer.count
30+
}
31+
32+
init(capacity: Int) {
33+
self.buffer = .allocate(capacity: capacity)
34+
self.count = 0
35+
}
36+
37+
deinit {
38+
buffer.baseAddress!.deinitialize(count: count)
39+
buffer.deallocate()
40+
}
41+
42+
func cloned(capacity: Int? = nil) -> Storage {
43+
let newCapacity = Swift.max(capacity ?? self.capacity, self.capacity)
44+
let newStorage = Storage(capacity: newCapacity)
45+
newStorage.buffer.baseAddress!
46+
.initialize(from: buffer.baseAddress!, count: count)
47+
newStorage.count = count
48+
return newStorage
49+
}
50+
}
51+
52+
mutating func _makeUnique() {
53+
if !isKnownUniquelyReferenced(&storage) {
54+
storage = storage.cloned()
55+
COWLoggingArray_CopyCount += 1
56+
}
57+
}
58+
}
59+
60+
extension COWLoggingArray: RandomAccessCollection, RangeReplaceableCollection,
61+
MutableCollection, ExpressibleByArrayLiteral
62+
{
63+
public var count: Int { storage.count }
64+
public var startIndex: Int { 0 }
65+
public var endIndex: Int { count }
66+
67+
public subscript(i: Int) -> Element {
68+
get {
69+
storage.buffer[i]
70+
}
71+
set {
72+
_makeUnique()
73+
storage.buffer[i] = newValue
74+
}
75+
}
76+
77+
public init() {
78+
storage = Storage(capacity: 10)
79+
}
80+
81+
public mutating func reserveCapacity(_ n: Int) {
82+
if !isKnownUniquelyReferenced(&storage) {
83+
COWLoggingArray_CopyCount += 1
84+
storage = storage.cloned(capacity: n)
85+
} else if count < n {
86+
storage = storage.cloned(capacity: n)
87+
}
88+
}
89+
90+
public mutating func replaceSubrange<C>(_ subrange: Range<Int>, with newElements: C)
91+
where C : Collection, Element == C.Element
92+
{
93+
_makeUnique()
94+
let newCount = (count - subrange.count) + newElements.count
95+
if newCount > storage.capacity {
96+
storage = storage.cloned(capacity: newCount)
97+
}
98+
99+
let startOfSubrange = storage.buffer.baseAddress! + subrange.lowerBound
100+
let endOfSubrange = startOfSubrange + subrange.count
101+
let endOfNewElements = startOfSubrange + newElements.count
102+
let countAfterSubrange = count - subrange.upperBound
103+
104+
// clear out old elements
105+
startOfSubrange.deinitialize(count: subrange.count)
106+
107+
// move elements above subrange
108+
endOfNewElements.moveInitialize(from: endOfSubrange, count: countAfterSubrange)
109+
110+
// assign new elements
111+
for (pointer, element) in zip(startOfSubrange..., newElements) {
112+
pointer.initialize(to: element)
113+
}
114+
115+
// update count
116+
storage.count = newCount
117+
}
118+
119+
public init(arrayLiteral elements: Element...) {
120+
storage = Storage(capacity: elements.count)
121+
replaceSubrange(0..<0, with: elements)
122+
}
123+
}

stdlib/public/core/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,10 +206,13 @@ set(SWIFTLIB_SOURCES
206206
Availability.swift
207207
CollectionDifference.swift
208208
CollectionOfOne.swift
209+
DiscontiguousSlice.swift
209210
Diffing.swift
210211
Mirror.swift
211212
PlaygroundDisplay.swift
212213
CommandLine.swift
214+
RangeSet.swift
215+
RangeSetStorage.swift
213216
SliceBuffer.swift
214217
SIMDVector.swift
215218
UnfoldSequence.swift

stdlib/public/core/CollectionAlgorithms.swift

Lines changed: 140 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the Swift.org open source project
44
//
5-
// Copyright (c) 2014 - 2018 Apple Inc. and the Swift project authors
5+
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
66
// Licensed under Apache License v2.0 with Runtime Library Exception
77
//
88
// See https://swift.org/LICENSE.txt for license information
@@ -209,6 +209,70 @@ extension BidirectionalCollection where Element: Equatable {
209209
}
210210
}
211211

212+
//===----------------------------------------------------------------------===//
213+
// subranges(where:) / subranges(of:)
214+
//===----------------------------------------------------------------------===//
215+
216+
extension Collection {
217+
/// Returns the indices of all the elements that match the given predicate.
218+
///
219+
/// For example, you can use this method to find all the places that a
220+
/// vowel occurs in a string.
221+
///
222+
/// let str = "Fresh cheese in a breeze"
223+
/// let vowels: Set<Character> = ["a", "e", "i", "o", "u"]
224+
/// let allTheVowels = str.subranges(where: { vowels.contains($0) })
225+
/// // str[allTheVowels].count == 9
226+
///
227+
/// - Parameter predicate: A closure that takes an element as its argument
228+
/// and returns a Boolean value that indicates whether the passed element
229+
/// represents a match.
230+
/// - Returns: A set of the indices of the elements for which `predicate`
231+
/// returns `true`.
232+
///
233+
/// - Complexity: O(*n*), where *n* is the length of the collection.
234+
@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *)
235+
public func subranges(where predicate: (Element) throws -> Bool) rethrows
236+
-> RangeSet<Index>
237+
{
238+
if isEmpty { return RangeSet() }
239+
240+
var result = RangeSet<Index>()
241+
var i = startIndex
242+
while i != endIndex {
243+
let next = index(after: i)
244+
if try predicate(self[i]) {
245+
result._append(i..<next)
246+
}
247+
i = next
248+
}
249+
250+
return result
251+
}
252+
}
253+
254+
extension Collection where Element: Equatable {
255+
/// Returns the indices of all the elements that are equal to the given
256+
/// element.
257+
///
258+
/// For example, you can use this method to find all the places that a
259+
/// particular letter occurs in a string.
260+
///
261+
/// let str = "Fresh cheese in a breeze"
262+
/// let allTheEs = str.subranges(of: "e")
263+
/// // str[allTheEs].count == 7
264+
///
265+
/// - Parameter element: An element to look for in the collection.
266+
/// - Returns: A set of the indices of the elements that are equal to
267+
/// `element`.
268+
///
269+
/// - Complexity: O(*n*), where *n* is the length of the collection.
270+
@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *)
271+
public func subranges(of element: Element) -> RangeSet<Index> {
272+
subranges(where: { $0 == element })
273+
}
274+
}
275+
212276
//===----------------------------------------------------------------------===//
213277
// partition(by:)
214278
//===----------------------------------------------------------------------===//
@@ -369,6 +433,81 @@ extension MutableCollection where Self: BidirectionalCollection {
369433
}
370434
}
371435

436+
//===----------------------------------------------------------------------===//
437+
// _indexedStablePartition / _partitioningIndex
438+
//===----------------------------------------------------------------------===//
439+
440+
extension MutableCollection {
441+
/// Moves all elements at the indices satisfying `belongsInSecondPartition`
442+
/// into a suffix of the collection, preserving their relative order, and
443+
/// returns the start of the resulting suffix.
444+
///
445+
/// - Complexity: O(*n* log *n*) where *n* is the number of elements.
446+
/// - Precondition:
447+
/// `n == distance(from: range.lowerBound, to: range.upperBound)`
448+
internal mutating func _indexedStablePartition(
449+
count n: Int,
450+
range: Range<Index>,
451+
by belongsInSecondPartition: (Index) throws-> Bool
452+
) rethrows -> Index {
453+
if n == 0 { return range.lowerBound }
454+
if n == 1 {
455+
return try belongsInSecondPartition(range.lowerBound)
456+
? range.lowerBound
457+
: range.upperBound
458+
}
459+
let h = n / 2, i = index(range.lowerBound, offsetBy: h)
460+
let j = try _indexedStablePartition(
461+
count: h,
462+
range: range.lowerBound..<i,
463+
by: belongsInSecondPartition)
464+
let k = try _indexedStablePartition(
465+
count: n - h,
466+
range: i..<range.upperBound,
467+
by: belongsInSecondPartition)
468+
return _rotate(in: j..<k, shiftingToStart: i)
469+
}
470+
}
471+
472+
//===----------------------------------------------------------------------===//
473+
// _partitioningIndex(where:)
474+
//===----------------------------------------------------------------------===//
475+
476+
extension Collection {
477+
/// Returns the index of the first element in the collection that matches
478+
/// the predicate.
479+
///
480+
/// The collection must already be partitioned according to the predicate.
481+
/// That is, there should be an index `i` where for every element in
482+
/// `collection[..<i]` the predicate is `false`, and for every element
483+
/// in `collection[i...]` the predicate is `true`.
484+
///
485+
/// - Parameter predicate: A predicate that partitions the collection.
486+
/// - Returns: The index of the first element in the collection for which
487+
/// `predicate` returns `true`.
488+
///
489+
/// - Complexity: O(log *n*), where *n* is the length of this collection if
490+
/// the collection conforms to `RandomAccessCollection`, otherwise O(*n*).
491+
internal func _partitioningIndex(
492+
where predicate: (Element) throws -> Bool
493+
) rethrows -> Index {
494+
var n = count
495+
var l = startIndex
496+
497+
while n > 0 {
498+
let half = n / 2
499+
let mid = index(l, offsetBy: half)
500+
if try predicate(self[mid]) {
501+
n = half
502+
} else {
503+
l = index(after: mid)
504+
n -= half + 1
505+
}
506+
}
507+
return l
508+
}
509+
}
510+
372511
//===----------------------------------------------------------------------===//
373512
// shuffled()/shuffle()
374513
//===----------------------------------------------------------------------===//

0 commit comments

Comments
 (0)