-
Notifications
You must be signed in to change notification settings - Fork 356
[Deque] Work around stdlib issue with Array.withContiguousStorageIfAvailable #44
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| //===----------------------------------------------------------------------===// | ||
| // | ||
| // This source file is part of the Swift Collections open source project | ||
| // | ||
| // Copyright (c) 2021 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 Array { | ||
| /// Returns true if `Array.withContiguousStorageIfAvailable` is broken | ||
| /// in the stdlib we're currently running on. | ||
| /// | ||
| /// See https://bugs.swift.org/browse/SR-14663. | ||
| @inlinable | ||
| internal static func _isWCSIABroken() -> Bool { | ||
| #if _runtime(_ObjC) | ||
| guard _isBridgedVerbatimToObjectiveC(Element.self) else { | ||
| // SR-14663 only triggers on array values that are verbatim bridged | ||
| // from Objective-C, so it cannot ever trigger for element types | ||
| // that aren't verbatim bridged. | ||
| return false | ||
| } | ||
|
|
||
| // SR-14663 was introduced in Swift 5.1. Check if we have a broken stdlib. | ||
|
|
||
| // The bug is caused by a bogus precondition inside a non-inlinable stdlib | ||
| // method, so to determine if we're affected, we need to check the currently | ||
| // running OS version. | ||
| #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) | ||
| guard #available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) else { | ||
| // The OS is too old to be affected by this bug. | ||
| return false | ||
| } | ||
| #endif | ||
| // FIXME: When a stdlib is released that contains a fix, add a check for it. | ||
| return true | ||
|
|
||
| #else | ||
| // Platforms that don't have an Objective-C runtime don't have verbatim | ||
| // bridged array values, so the bug doesn't apply to them. | ||
| return false | ||
| #endif | ||
| } | ||
| } | ||
|
|
||
| extension Sequence { | ||
| // An adjusted version of the standard `withContiguousStorageIfAvailable` | ||
| // method that works around https://bugs.swift.org/browse/SR-14663. | ||
| @inlinable | ||
| internal func _withContiguousStorageIfAvailable_SR14663<R>( | ||
| _ body: (UnsafeBufferPointer<Element>) throws -> R | ||
| ) rethrows -> R? { | ||
| if Self.self == Array<Element>.self && Array<Element>._isWCSIABroken() { | ||
| return nil | ||
| } | ||
|
|
||
| return try self.withContiguousStorageIfAvailable(body) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -131,7 +131,7 @@ extension Deque { | |
| /// - SeeAlso: `append(contentsOf:)` | ||
| @inlinable | ||
| public mutating func prepend<C: Collection>(contentsOf newElements: C) where C.Element == Element { | ||
| let done: Void? = newElements.withContiguousStorageIfAvailable { source in | ||
| let done: Void? = newElements._withContiguousStorageIfAvailable_SR14663 { source in | ||
| _storage.ensureUnique(minimumCapacity: count + source.count) | ||
| _storage.update { $0.uncheckedPrepend(contentsOf: source) } | ||
| } | ||
|
|
@@ -166,6 +166,12 @@ extension Deque { | |
| /// - SeeAlso: `append(contentsOf:)` | ||
| @inlinable | ||
| public mutating func prepend<S: Sequence>(contentsOf newElements: S) where S.Element == Element { | ||
| let done: Void? = newElements._withContiguousStorageIfAvailable_SR14663 { source in | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I couldn't resist slipping in this trivial performance improvement, if only for symmetry with the sequence-based |
||
| _storage.ensureUnique(minimumCapacity: count + source.count) | ||
| _storage.update { $0.uncheckedPrepend(contentsOf: source) } | ||
| } | ||
| guard done == nil else { return } | ||
|
|
||
| let originalCount = self.count | ||
| self.append(contentsOf: newElements) | ||
| let newCount = self.count | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -301,13 +301,14 @@ final class DequeTests: CollectionTestCase { | |
| } | ||
| } | ||
|
|
||
| func test_prependManyFromArray() { | ||
| func test_prependManyFromContiguousArray_asCollection() { | ||
| withEveryDeque("deque", ofCapacities: [0, 1, 2, 3, 5, 10]) { layout in | ||
| withEvery("prependCount", in: 0 ..< 10) { prependCount in | ||
| withEvery("isShared", in: [false, true]) { isShared in | ||
| withLifetimeTracking { tracker in | ||
| var (deque, contents) = tracker.deque(with: layout) | ||
| let extra = tracker.instances(for: layout.count ..< layout.count + prependCount) | ||
| let extraRange = layout.count ..< layout.count + prependCount | ||
| let extra = ContiguousArray(tracker.instances(for: extraRange)) | ||
| withHiddenCopies(if: isShared, of: &deque) { deque in | ||
| contents.insert(contentsOf: extra, at: 0) | ||
| deque.prepend(contentsOf: extra) | ||
|
|
@@ -319,5 +320,55 @@ final class DequeTests: CollectionTestCase { | |
| } | ||
| } | ||
|
|
||
| func test_prependManyFromContiguousArray_asSequence() { | ||
| // This calls the Sequence-based `Deque.prepend` overload, even if | ||
| // `elements` happens to be of a Collection type. | ||
| func prependSequence<S: Sequence>( | ||
| contentsOf elements: S, | ||
| to deque: inout Deque<S.Element> | ||
| ) { | ||
| deque.prepend(contentsOf: elements) | ||
| } | ||
|
|
||
| withEveryDeque("deque", ofCapacities: [0, 1, 2, 3, 5, 10]) { layout in | ||
| withEvery("prependCount", in: 0 ..< 10) { prependCount in | ||
| withEvery("isShared", in: [false, true]) { isShared in | ||
| withLifetimeTracking { tracker in | ||
| var (deque, contents) = tracker.deque(with: layout) | ||
| let extraRange = layout.count ..< layout.count + prependCount | ||
| let extra = ContiguousArray(tracker.instances(for: extraRange)) | ||
| withHiddenCopies(if: isShared, of: &deque) { deque in | ||
| contents.insert(contentsOf: extra, at: 0) | ||
| prependSequence(contentsOf: extra, to: &deque) | ||
| expectEqualElements(deque, contents) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func test_prependManyFromBridgedArray() { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you for adding the additional test cases =) |
||
| // https://github.com/apple/swift-collections/issues/27 | ||
| withEveryDeque("deque", ofCapacities: [0, 1, 2, 3, 5, 10]) { layout in | ||
| withEvery("appendCount", in: 0 ..< 10) { appendCount in | ||
| withEvery("isShared", in: [false, true]) { isShared in | ||
| var contents: [NSObject] = (0 ..< layout.count).map { _ in NSObject() } | ||
| var deque = Deque(layout: layout, contents: contents) | ||
| let extra: [NSObject] = (0 ..< appendCount) | ||
| .map { _ in NSObject() } | ||
| .withUnsafeBufferPointer { buffer in | ||
| NSArray(objects: buffer.baseAddress, count: buffer.count) as! [NSObject] | ||
| } | ||
| withHiddenCopies(if: isShared, of: &deque) { deque in | ||
| contents.insert(contentsOf: extra, at: 0) | ||
| deque.prepend(contentsOf: extra) | ||
| expectEquivalentElements(deque, contents, by: ===) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we had a reliable/easy way to test if a particular array instance is bridged, then we could let native arrays pass through here. 🤔
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe:
This will call down in to
ArrayBuffer.firstElementAddressIfContiguous, which returns nil if not native.Tested in a Playground:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Huh, that's a good find. Looking at similarly exposed members,
Array._hoistableIsNativeTypeChecked()might be even better.There are (at least) two worries with using these underscored APIs: (including
_isBridgedVerbatimToObjectiveCabove)Swift doesn't promise it won't change their behavior, so it's tricky to safely use them in code that is not part of the stdlib. E.g., I imagine for element types that don't require a dynamic type check at every access, we may want to change
_baseAddressIfContiguousto return non-nil for bridged arrays that use specific known NSArray subclasses.These have also been made public for historical reasons, rather than any intentional design, and we may find a good reason to change them to
@inlinable internal, disallowing direct calls like this in future SDKs. We wouldn't be able to remove the entry points themselves (they are part of Swift's ABI), but we might need to restrict them to only be called from inlinable functions within the stdlib.As this package is maintained by the same people who work on the Swift stdlib, I think it's not unreasonable for this package to use these. At the first sign of trouble, we can remove the calls, or limit them to older Swift toolchains. The primary danger is that this code will spread beyond this package through vendoring or uncritical copy-n-paste.
My instinct as a stdlib engineer tells me that the risk is low with
_hoistableIsNativeTypeChecked(and pretty much nonexistent for_isBridgedVerbatimToObjectiveC); on the other hand, this package is inherently averse to even low-risk stuff. Hm.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the best thing to do is to keep this as is for now until we have a Swift release (let's call it x.y) that contains a fix for the underlying issue. In the (unlikely) case that the fix won't back deploy, then we can add a call to
_hoistableIsNativeTypeCheckedrestricted to only be called on Swift 5.1 ..< x.y.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that's fine. I would also be OK with doing the workaround here now, so long as we put a comment in the stdlib that references this bug.