Skip to content

Commit de5f1e6

Browse files
authored
[Deque] Work around stdlib issue with Array.withContiguousStorageIfAvailable (apple#44)
Resolves apple#27.
1 parent 4d73202 commit de5f1e6

File tree

5 files changed

+172
-8
lines changed

5 files changed

+172
-8
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift Collections open source project
4+
//
5+
// Copyright (c) 2021 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
//
10+
//===----------------------------------------------------------------------===//
11+
12+
extension Array {
13+
/// Returns true if `Array.withContiguousStorageIfAvailable` is broken
14+
/// in the stdlib we're currently running on.
15+
///
16+
/// See https://bugs.swift.org/browse/SR-14663.
17+
@inlinable
18+
internal static func _isWCSIABroken() -> Bool {
19+
#if _runtime(_ObjC)
20+
guard _isBridgedVerbatimToObjectiveC(Element.self) else {
21+
// SR-14663 only triggers on array values that are verbatim bridged
22+
// from Objective-C, so it cannot ever trigger for element types
23+
// that aren't verbatim bridged.
24+
return false
25+
}
26+
27+
// SR-14663 was introduced in Swift 5.1. Check if we have a broken stdlib.
28+
29+
// The bug is caused by a bogus precondition inside a non-inlinable stdlib
30+
// method, so to determine if we're affected, we need to check the currently
31+
// running OS version.
32+
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
33+
guard #available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) else {
34+
// The OS is too old to be affected by this bug.
35+
return false
36+
}
37+
#endif
38+
// FIXME: When a stdlib is released that contains a fix, add a check for it.
39+
return true
40+
41+
#else
42+
// Platforms that don't have an Objective-C runtime don't have verbatim
43+
// bridged array values, so the bug doesn't apply to them.
44+
return false
45+
#endif
46+
}
47+
}
48+
49+
extension Sequence {
50+
// An adjusted version of the standard `withContiguousStorageIfAvailable`
51+
// method that works around https://bugs.swift.org/browse/SR-14663.
52+
@inlinable
53+
internal func _withContiguousStorageIfAvailable_SR14663<R>(
54+
_ body: (UnsafeBufferPointer<Element>) throws -> R
55+
) rethrows -> R? {
56+
if Self.self == Array<Element>.self && Array<Element>._isWCSIABroken() {
57+
return nil
58+
}
59+
60+
return try self.withContiguousStorageIfAvailable(body)
61+
}
62+
}

Sources/DequeModule/Deque+Extras.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ extension Deque {
131131
/// - SeeAlso: `append(contentsOf:)`
132132
@inlinable
133133
public mutating func prepend<C: Collection>(contentsOf newElements: C) where C.Element == Element {
134-
let done: Void? = newElements.withContiguousStorageIfAvailable { source in
134+
let done: Void? = newElements._withContiguousStorageIfAvailable_SR14663 { source in
135135
_storage.ensureUnique(minimumCapacity: count + source.count)
136136
_storage.update { $0.uncheckedPrepend(contentsOf: source) }
137137
}
@@ -166,6 +166,12 @@ extension Deque {
166166
/// - SeeAlso: `append(contentsOf:)`
167167
@inlinable
168168
public mutating func prepend<S: Sequence>(contentsOf newElements: S) where S.Element == Element {
169+
let done: Void? = newElements._withContiguousStorageIfAvailable_SR14663 { source in
170+
_storage.ensureUnique(minimumCapacity: count + source.count)
171+
_storage.update { $0.uncheckedPrepend(contentsOf: source) }
172+
}
173+
guard done == nil else { return }
174+
169175
let originalCount = self.count
170176
self.append(contentsOf: newElements)
171177
let newCount = self.count

Sources/DequeModule/Deque+RangeReplaceableCollection.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ extension Deque: RangeReplaceableCollection {
141141
_storage.update { handle in
142142
assert(handle.startSlot == .zero)
143143
let target = handle.mutableBuffer(for: .zero ..< _Slot(at: c))
144-
let done: Void? = elements.withContiguousStorageIfAvailable { source in
144+
let done: Void? = elements._withContiguousStorageIfAvailable_SR14663 { source in
145145
target._initialize(from: source)
146146
}
147147
if done == nil {
@@ -198,7 +198,7 @@ extension Deque: RangeReplaceableCollection {
198198
/// - Complexity: Amortized O(`newElements.count`).
199199
@inlinable
200200
public mutating func append<S: Sequence>(contentsOf newElements: S) where S.Element == Element {
201-
let done: Void? = newElements.withContiguousStorageIfAvailable { source in
201+
let done: Void? = newElements._withContiguousStorageIfAvailable_SR14663 { source in
202202
_storage.ensureUnique(minimumCapacity: count + source.count)
203203
_storage.update { $0.uncheckedAppend(contentsOf: source) }
204204
}
@@ -240,7 +240,7 @@ extension Deque: RangeReplaceableCollection {
240240
/// - Complexity: Amortized O(`newElements.count`).
241241
@inlinable
242242
public mutating func append<C: Collection>(contentsOf newElements: C) where C.Element == Element {
243-
let done: Void? = newElements.withContiguousStorageIfAvailable { source in
243+
let done: Void? = newElements._withContiguousStorageIfAvailable_SR14663 { source in
244244
_storage.ensureUnique(minimumCapacity: count + source.count)
245245
_storage.update { $0.uncheckedAppend(contentsOf: source) }
246246
}

Tests/DequeTests/DequeTests.swift

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -301,13 +301,14 @@ final class DequeTests: CollectionTestCase {
301301
}
302302
}
303303

304-
func test_prependManyFromArray() {
304+
func test_prependManyFromContiguousArray_asCollection() {
305305
withEveryDeque("deque", ofCapacities: [0, 1, 2, 3, 5, 10]) { layout in
306306
withEvery("prependCount", in: 0 ..< 10) { prependCount in
307307
withEvery("isShared", in: [false, true]) { isShared in
308308
withLifetimeTracking { tracker in
309309
var (deque, contents) = tracker.deque(with: layout)
310-
let extra = tracker.instances(for: layout.count ..< layout.count + prependCount)
310+
let extraRange = layout.count ..< layout.count + prependCount
311+
let extra = ContiguousArray(tracker.instances(for: extraRange))
311312
withHiddenCopies(if: isShared, of: &deque) { deque in
312313
contents.insert(contentsOf: extra, at: 0)
313314
deque.prepend(contentsOf: extra)
@@ -319,5 +320,55 @@ final class DequeTests: CollectionTestCase {
319320
}
320321
}
321322

323+
func test_prependManyFromContiguousArray_asSequence() {
324+
// This calls the Sequence-based `Deque.prepend` overload, even if
325+
// `elements` happens to be of a Collection type.
326+
func prependSequence<S: Sequence>(
327+
contentsOf elements: S,
328+
to deque: inout Deque<S.Element>
329+
) {
330+
deque.prepend(contentsOf: elements)
331+
}
332+
333+
withEveryDeque("deque", ofCapacities: [0, 1, 2, 3, 5, 10]) { layout in
334+
withEvery("prependCount", in: 0 ..< 10) { prependCount in
335+
withEvery("isShared", in: [false, true]) { isShared in
336+
withLifetimeTracking { tracker in
337+
var (deque, contents) = tracker.deque(with: layout)
338+
let extraRange = layout.count ..< layout.count + prependCount
339+
let extra = ContiguousArray(tracker.instances(for: extraRange))
340+
withHiddenCopies(if: isShared, of: &deque) { deque in
341+
contents.insert(contentsOf: extra, at: 0)
342+
prependSequence(contentsOf: extra, to: &deque)
343+
expectEqualElements(deque, contents)
344+
}
345+
}
346+
}
347+
}
348+
}
349+
}
350+
351+
func test_prependManyFromBridgedArray() {
352+
// https://github.com/apple/swift-collections/issues/27
353+
withEveryDeque("deque", ofCapacities: [0, 1, 2, 3, 5, 10]) { layout in
354+
withEvery("appendCount", in: 0 ..< 10) { appendCount in
355+
withEvery("isShared", in: [false, true]) { isShared in
356+
var contents: [NSObject] = (0 ..< layout.count).map { _ in NSObject() }
357+
var deque = Deque(layout: layout, contents: contents)
358+
let extra: [NSObject] = (0 ..< appendCount)
359+
.map { _ in NSObject() }
360+
.withUnsafeBufferPointer { buffer in
361+
NSArray(objects: buffer.baseAddress, count: buffer.count) as! [NSObject]
362+
}
363+
withHiddenCopies(if: isShared, of: &deque) { deque in
364+
contents.insert(contentsOf: extra, at: 0)
365+
deque.prepend(contentsOf: extra)
366+
expectEquivalentElements(deque, contents, by: ===)
367+
}
368+
}
369+
}
370+
}
371+
}
372+
322373
}
323374

Tests/DequeTests/RangeReplaceableCollectionTests.swift

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,28 @@ final class RangeReplaceableCollectionTests: CollectionTestCase {
5555
}
5656
}
5757

58+
func test_sequenceInitializer_ContiguousArray() {
59+
withEvery("count", in: [0, 1, 2, 10, 100]) { count in
60+
withLifetimeTracking { tracker in
61+
let contents = ContiguousArray(tracker.instances(for: 0 ..< count))
62+
let d1 = Deque(contents)
63+
expectEqualElements(d1, contents)
64+
}
65+
}
66+
}
67+
68+
func test_sequenceInitializer_bridgedArray() {
69+
// https://github.com/apple/swift-collections/issues/27
70+
withEvery("count", in: [0, 1, 2, 10, 100]) { count in
71+
let contents: [AnyObject] = (0 ..< count).map { _ in NSObject() }
72+
let array: [AnyObject] = contents.withUnsafeBufferPointer { buffer in
73+
NSArray(objects: buffer.baseAddress, count: buffer.count) as [AnyObject]
74+
}
75+
let deque = Deque(array)
76+
expectEquivalentElements(deque, contents, by: ===)
77+
}
78+
}
79+
5880
func test_replaceSubrange_withMinimalCollection() {
5981
withEveryDeque("deque", ofCapacities: [0, 1, 2, 3, 5, 10]) { layout in
6082
withEveryRange("range", in: 0 ..< layout.count) { range in
@@ -172,13 +194,14 @@ final class RangeReplaceableCollectionTests: CollectionTestCase {
172194
}
173195
}
174196

175-
func test_appendManyFromArray() {
197+
func test_appendManyFromContiguousArray() {
176198
withEveryDeque("deque", ofCapacities: [0, 1, 2, 3, 5, 10]) { layout in
177199
withEvery("appendCount", in: 0 ..< 10) { appendCount in
178200
withEvery("isShared", in: [false, true]) { isShared in
179201
withLifetimeTracking { tracker in
180202
var (deque, contents) = tracker.deque(with: layout)
181-
let extra = tracker.instances(for: layout.count ..< layout.count + appendCount)
203+
let extraRange = layout.count ..< layout.count + appendCount
204+
let extra = ContiguousArray(tracker.instances(for: extraRange))
182205
withHiddenCopies(if: isShared, of: &deque) { deque in
183206
contents.append(contentsOf: extra)
184207
deque.append(contentsOf: extra)
@@ -190,6 +213,28 @@ final class RangeReplaceableCollectionTests: CollectionTestCase {
190213
}
191214
}
192215

216+
func test_appendManyFromBridgedArray() {
217+
// https://github.com/apple/swift-collections/issues/27
218+
withEveryDeque("deque", ofCapacities: [0, 1, 2, 3, 5, 10]) { layout in
219+
withEvery("appendCount", in: 0 ..< 10) { appendCount in
220+
withEvery("isShared", in: [false, true]) { isShared in
221+
var contents: [NSObject] = (0 ..< layout.count).map { _ in NSObject() }
222+
var deque = Deque(layout: layout, contents: contents)
223+
let extra: [NSObject] = (0 ..< appendCount)
224+
.map { _ in NSObject() }
225+
.withUnsafeBufferPointer { buffer in
226+
NSArray(objects: buffer.baseAddress, count: buffer.count) as! [NSObject]
227+
}
228+
withHiddenCopies(if: isShared, of: &deque) { deque in
229+
contents.append(contentsOf: extra)
230+
deque.append(contentsOf: extra)
231+
expectEquivalentElements(deque, contents, by: ===)
232+
}
233+
}
234+
}
235+
}
236+
}
237+
193238
func test_insertOneElement() {
194239
withEveryDeque("deque", ofCapacities: [0, 1, 2, 3, 5, 10]) { layout in
195240
withEvery("offset", in: 0 ... layout.count) { offset in

0 commit comments

Comments
 (0)