Files
swift-mirror/stdlib/public/core/CollectionDifference.swift
Doug Gregor 1a1f79c0de Introduce safety checkin for ConcurrentValue conformance.
Introduce checking of ConcurrentValue conformances:
- For structs, check that each stored property conforms to ConcurrentValue
- For enums, check that each associated value conforms to ConcurrentValue
- For classes, check that each stored property is immutable and conforms
  to ConcurrentValue

Because all of the stored properties / associated values need to be
visible for this check to work, limit ConcurrentValue conformances to
be in the same source file as the type definition.

This checking can be disabled by conforming to a new marker protocol,
UnsafeConcurrentValue, that refines ConcurrentValue.
UnsafeConcurrentValue otherwise his no specific meaning. This allows
both "I know what I'm doing" for types that manage concurrent access
themselves as well as enabling retroactive conformance, both of which
are fundamentally unsafe but also quite necessary.

The bulk of this change ended up being to the standard library, because
all conformances of standard library types to the ConcurrentValue
protocol needed to be sunk down into the standard library so they
would benefit from the checking above. There were numerous little
mistakes in the initial pass through the stsandard library types that
have now been corrected.
2021-02-04 03:45:09 -08:00

429 lines
14 KiB
Swift

//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2015 - 2019 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
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
/// A collection of insertions and removals that describe the difference
/// between two ordered collection states.
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
public struct CollectionDifference<ChangeElement> {
/// A single change to a collection.
@frozen
public enum Change {
/// An insertion.
///
/// The `offset` value is the offset of the inserted element in the final
/// state of the collection after the difference is fully applied.
/// A non-`nil` `associatedWith` value is the offset of the complementary
/// change.
case insert(offset: Int, element: ChangeElement, associatedWith: Int?)
/// A removal.
///
/// The `offset` value is the offset of the element to be removed in the
/// original state of the collection. A non-`nil` `associatedWith` value is
/// the offset of the complementary change.
case remove(offset: Int, element: ChangeElement, associatedWith: Int?)
// Internal common field accessors
internal var _offset: Int {
get {
switch self {
case .insert(offset: let o, element: _, associatedWith: _):
return o
case .remove(offset: let o, element: _, associatedWith: _):
return o
}
}
}
internal var _element: ChangeElement {
get {
switch self {
case .insert(offset: _, element: let e, associatedWith: _):
return e
case .remove(offset: _, element: let e, associatedWith: _):
return e
}
}
}
internal var _associatedOffset: Int? {
get {
switch self {
case .insert(offset: _, element: _, associatedWith: let o):
return o
case .remove(offset: _, element: _, associatedWith: let o):
return o
}
}
}
}
/// The insertions contained by this difference, from lowest offset to
/// highest.
public let insertions: [Change]
/// The removals contained by this difference, from lowest offset to highest.
public let removals: [Change]
/// The public initializer calls this function to ensure that its parameter
/// meets the conditions set in its documentation.
///
/// - Parameter changes: a collection of `CollectionDifference.Change`
/// instances intended to represent a valid state transition for
/// `CollectionDifference`.
///
/// - Returns: whether the parameter meets the following criteria:
///
/// 1. All insertion offsets are unique
/// 2. All removal offsets are unique
/// 3. All associations between insertions and removals are symmetric
///
/// Complexity: O(`changes.count`)
private static func _validateChanges<Changes: Collection>(
_ changes : Changes
) -> Bool where Changes.Element == Change {
if changes.isEmpty { return true }
var insertAssocToOffset = Dictionary<Int,Int>()
var removeOffsetToAssoc = Dictionary<Int,Int>()
var insertOffset = Set<Int>()
var removeOffset = Set<Int>()
for change in changes {
let offset = change._offset
if offset < 0 { return false }
switch change {
case .remove(_, _, _):
if removeOffset.contains(offset) { return false }
removeOffset.insert(offset)
case .insert(_, _, _):
if insertOffset.contains(offset) { return false }
insertOffset.insert(offset)
}
if let assoc = change._associatedOffset {
if assoc < 0 { return false }
switch change {
case .remove(_, _, _):
if removeOffsetToAssoc[offset] != nil { return false }
removeOffsetToAssoc[offset] = assoc
case .insert(_, _, _):
if insertAssocToOffset[assoc] != nil { return false }
insertAssocToOffset[assoc] = offset
}
}
}
return removeOffsetToAssoc == insertAssocToOffset
}
/// Creates a new collection difference from a collection of changes.
///
/// To find the difference between two collections, use the
/// `difference(from:)` method declared on the `BidirectionalCollection`
/// protocol.
///
/// The collection of changes passed as `changes` must meet these
/// requirements:
///
/// - All insertion offsets are unique
/// - All removal offsets are unique
/// - All associations between insertions and removals are symmetric
///
/// - Parameter changes: A collection of changes that represent a transition
/// between two states.
///
/// - Complexity: O(*n* * log(*n*)), where *n* is the length of the
/// parameter.
public init?<Changes: Collection>(
_ changes: Changes
) where Changes.Element == Change {
guard CollectionDifference<ChangeElement>._validateChanges(changes) else {
return nil
}
self.init(_validatedChanges: changes)
}
/// Internal initializer for use by algorithms that cannot produce invalid
/// collections of changes. These include the Myers' diff algorithm,
/// self.inverse(), and the move inferencer.
///
/// If parameter validity cannot be guaranteed by the caller then
/// `CollectionDifference.init?(_:)` should be used instead.
///
/// - Parameter c: A valid collection of changes that represent a transition
/// between two states.
///
/// - Complexity: O(*n* * log(*n*)), where *n* is the length of the
/// parameter.
internal init<Changes: Collection>(
_validatedChanges changes: Changes
) where Changes.Element == Change {
let sortedChanges = changes.sorted { (a, b) -> Bool in
switch (a, b) {
case (.remove(_, _, _), .insert(_, _, _)):
return true
case (.insert(_, _, _), .remove(_, _, _)):
return false
default:
return a._offset < b._offset
}
}
// Find first insertion via binary search
let firstInsertIndex: Int
if sortedChanges.isEmpty {
firstInsertIndex = 0
} else {
var range = 0...sortedChanges.count
while range.lowerBound != range.upperBound {
let i = (range.lowerBound + range.upperBound) / 2
switch sortedChanges[i] {
case .insert(_, _, _):
range = range.lowerBound...i
case .remove(_, _, _):
range = (i + 1)...range.upperBound
}
}
firstInsertIndex = range.lowerBound
}
removals = Array(sortedChanges[0..<firstInsertIndex])
insertions = Array(sortedChanges[firstInsertIndex..<sortedChanges.count])
}
public func inverse() -> Self {
return CollectionDifference(_validatedChanges: self.map { c in
switch c {
case .remove(let o, let e, let a):
return .insert(offset: o, element: e, associatedWith: a)
case .insert(let o, let e, let a):
return .remove(offset: o, element: e, associatedWith: a)
}
})
}
}
/// A CollectionDifference is itself a Collection.
///
/// The enumeration order of `Change` elements is:
///
/// 1. `.remove`s, from highest `offset` to lowest
/// 2. `.insert`s, from lowest `offset` to highest
///
/// This guarantees that applicators on compatible base states are safe when
/// written in the form:
///
/// ```
/// for c in diff {
/// switch c {
/// case .remove(offset: let o, element: _, associatedWith: _):
/// arr.remove(at: o)
/// case .insert(offset: let o, element: let e, associatedWith: _):
/// arr.insert(e, at: o)
/// }
/// }
/// ```
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
extension CollectionDifference: Collection {
public typealias Element = Change
/// The position of a collection difference.
@frozen
public struct Index {
// Opaque index type is isomorphic to Int
@usableFromInline
internal let _offset: Int
internal init(_offset offset: Int) {
_offset = offset
}
}
public var startIndex: Index {
return Index(_offset: 0)
}
public var endIndex: Index {
return Index(_offset: removals.count + insertions.count)
}
public func index(after index: Index) -> Index {
return Index(_offset: index._offset + 1)
}
public subscript(position: Index) -> Element {
if position._offset < removals.count {
return removals[removals.count - (position._offset + 1)]
}
return insertions[position._offset - removals.count]
}
public func index(before index: Index) -> Index {
return Index(_offset: index._offset - 1)
}
public func formIndex(_ index: inout Index, offsetBy distance: Int) {
index = Index(_offset: index._offset + distance)
}
public func distance(from start: Index, to end: Index) -> Int {
return end._offset - start._offset
}
}
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
extension CollectionDifference.Index: Equatable {
@inlinable
public static func == (
lhs: CollectionDifference.Index,
rhs: CollectionDifference.Index
) -> Bool {
return lhs._offset == rhs._offset
}
}
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
extension CollectionDifference.Index: Comparable {
@inlinable
public static func < (
lhs: CollectionDifference.Index,
rhs: CollectionDifference.Index
) -> Bool {
return lhs._offset < rhs._offset
}
}
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
extension CollectionDifference.Index: Hashable {
@inlinable
public func hash(into hasher: inout Hasher) {
hasher.combine(_offset)
}
}
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
extension CollectionDifference.Change: Equatable where ChangeElement: Equatable {}
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
extension CollectionDifference: Equatable where ChangeElement: Equatable {}
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
extension CollectionDifference.Change: Hashable where ChangeElement: Hashable {}
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
extension CollectionDifference: Hashable where ChangeElement: Hashable {}
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
extension CollectionDifference where ChangeElement: Hashable {
/// Returns a new collection difference with associations between individual
/// elements that have been removed and inserted only once.
///
/// - Returns: A collection difference with all possible moves inferred.
///
/// - Complexity: O(*n*) where *n* is the number of collection differences.
public func inferringMoves() -> CollectionDifference<ChangeElement> {
let uniqueRemovals: [ChangeElement:Int?] = {
var result = [ChangeElement:Int?](minimumCapacity: Swift.min(removals.count, insertions.count))
for removal in removals {
let element = removal._element
if result[element] != .none {
result[element] = .some(.none)
} else {
result[element] = .some(removal._offset)
}
}
return result.filter { (_, v) -> Bool in v != .none }
}()
let uniqueInsertions: [ChangeElement:Int?] = {
var result = [ChangeElement:Int?](minimumCapacity: Swift.min(removals.count, insertions.count))
for insertion in insertions {
let element = insertion._element
if result[element] != .none {
result[element] = .some(.none)
} else {
result[element] = .some(insertion._offset)
}
}
return result.filter { (_, v) -> Bool in v != .none }
}()
return CollectionDifference(_validatedChanges: map({ (change: Change) -> Change in
switch change {
case .remove(offset: let offset, element: let element, associatedWith: _):
if uniqueRemovals[element] == nil {
return change
}
if let assoc = uniqueInsertions[element] {
return .remove(offset: offset, element: element, associatedWith: assoc)
}
case .insert(offset: let offset, element: let element, associatedWith: _):
if uniqueInsertions[element] == nil {
return change
}
if let assoc = uniqueRemovals[element] {
return .insert(offset: offset, element: element, associatedWith: assoc)
}
}
return change
}))
}
}
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
extension CollectionDifference.Change: Codable where ChangeElement: Codable {
private enum _CodingKeys: String, CodingKey {
case offset
case element
case associatedOffset
case isRemove
}
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: _CodingKeys.self)
let offset = try values.decode(Int.self, forKey: .offset)
let element = try values.decode(ChangeElement.self, forKey: .element)
let associatedOffset = try values.decode(Int?.self, forKey: .associatedOffset)
let isRemove = try values.decode(Bool.self, forKey: .isRemove)
if isRemove {
self = .remove(offset: offset, element: element, associatedWith: associatedOffset)
} else {
self = .insert(offset: offset, element: element, associatedWith: associatedOffset)
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: _CodingKeys.self)
switch self {
case .remove(_, _, _):
try container.encode(true, forKey: .isRemove)
case .insert(_, _, _):
try container.encode(false, forKey: .isRemove)
}
try container.encode(_offset, forKey: .offset)
try container.encode(_element, forKey: .element)
try container.encode(_associatedOffset, forKey: .associatedOffset)
}
}
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
extension CollectionDifference: Codable where ChangeElement: Codable {}
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
extension CollectionDifference: ConcurrentValue where ChangeElement: ConcurrentValue { }
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
extension CollectionDifference.Change: ConcurrentValue where ChangeElement: ConcurrentValue { }
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
extension CollectionDifference.Index: ConcurrentValue where ChangeElement: ConcurrentValue { }