//===----------------------------------------------------------------------===// // // 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 // //===----------------------------------------------------------------------===// // MARK: Diff application to RangeReplaceableCollection @available(SwiftStdlib 5.1, *) extension CollectionDifference { fileprivate func _fastEnumeratedApply( _ consume: (Change) throws -> Void ) rethrows { let totalRemoves = removals.count let totalInserts = insertions.count var enumeratedRemoves = 0 var enumeratedInserts = 0 while enumeratedRemoves < totalRemoves || enumeratedInserts < totalInserts { let change: Change if enumeratedRemoves < removals.count && enumeratedInserts < insertions.count { let removeOffset = removals[enumeratedRemoves]._offset let insertOffset = insertions[enumeratedInserts]._offset if removeOffset - enumeratedRemoves <= insertOffset - enumeratedInserts { change = removals[enumeratedRemoves] } else { change = insertions[enumeratedInserts] } } else if enumeratedRemoves < totalRemoves { change = removals[enumeratedRemoves] } else if enumeratedInserts < totalInserts { change = insertions[enumeratedInserts] } else { // Not reached, loop should have exited. preconditionFailure() } try consume(change) switch change { case .remove(_, _, _): enumeratedRemoves += 1 case .insert(_, _, _): enumeratedInserts += 1 } } } } // Error type allows the use of throw to unroll state on application failure private enum _ApplicationError : Error { case failed } extension RangeReplaceableCollection { /// Applies the given difference to this collection. /// /// - Parameter difference: The difference to be applied. /// /// - Returns: An instance representing the state of the receiver with the /// difference applied, or `nil` if the difference is incompatible with /// the receiver's state. /// /// - Complexity: O(*n* + *c*), where *n* is `self.count` and *c* is the /// number of changes contained by the parameter. @available(SwiftStdlib 5.1, *) public func applying(_ difference: CollectionDifference) -> Self? { func append( into target: inout Self, contentsOf source: Self, from index: inout Self.Index, count: Int ) throws { let start = index if !source.formIndex(&index, offsetBy: count, limitedBy: source.endIndex) { throw _ApplicationError.failed } target.append(contentsOf: source[start..( from other: C, by areEquivalent: (C.Element, Element) -> Bool ) -> CollectionDifference where C.Element == Self.Element { _linearSpaceMyers(from: other, to: self, using: areEquivalent) } } extension BidirectionalCollection where Element: Equatable { /// Returns the difference needed to produce this collection's ordered /// elements from the given collection. /// /// This function does not infer element moves. If you need to infer moves, /// call the `inferringMoves()` method on the resulting difference. /// /// - Parameters: /// - other: The base state. /// /// - Returns: The difference needed to produce this collection's ordered /// elements from the given collection. /// /// - Complexity: Worst case performance is O(*n* * *m*), where *n* is the /// count of this collection and *m* is `other.count`. You can expect /// faster execution when the collections share many common elements, or /// if `Element` conforms to `Hashable`. @available(SwiftStdlib 5.1, *) public func difference( from other: C ) -> CollectionDifference where C.Element == Self.Element { difference(from: other, by: ==) } } // MARK: Internal implementation /// Storage for the two "k-vectors" used by the Myers diffing /// algorithm, using a single allocation. /// /// The two k-vectors are stored one after the other in this type's /// buffer. For a given size `s`, each k-vector provides space for /// offsets in the range `-s...s`, using this layout for the buffer /// and offsets when `s == 2`: /// /// ``` /// ┌─────────────────────────────────────────────────────────────┐ /// │ 0 1 2 3 4 5 6 7 8 9 │ /// │ ┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐│ /// │ buffer: │ .. │ .. │ .. │ .. │ .. │ .. │ .. │ .. │ .. │ .. ││ /// │ └────┴────┴────┴────┴────┴────┴────┴────┴────┴────┘│ /// | ^ ^ | /// | forwardOffset backwardOffset | /// │ ┌────┬────┬────┬────┬────┐ │ /// │ forward: │ -2 │ -1 │ 0 │ 1 │ 2 │ │ /// │ └────┴────┴────┴────┴────┘ │ /// │ ┌────┬────┬────┬────┬────┐│ /// │ backward: │ -2 │ -1 │ 0 │ 1 │ 2 ││ /// │ └────┴────┴────┴────┴────┘│ /// └─────────────────────────────────────────────────────────────┘ /// ``` @safe fileprivate struct DoubleKVector: ~Copyable { let buffer: UnsafeMutableBufferPointer let forwardOffset: Int let backwardOffset: Int #if INTERNAL_CHECKS_ENABLED private let range: Int #endif /// Creates a new double k-vector. /// /// The size of the resulting array is `((2 * size) + 1) * 2`, /// enough space to store two k-vectors ranging from `-size` /// through `size`. init(size: Int) { let range = size * 2 + 1 unsafe self.buffer = .allocate(capacity: range * 2) self.forwardOffset = size self.backwardOffset = range + size unsafe buffer.initialize(repeating: 0) #if INTERNAL_CHECKS_ENABLED self.range = range #endif } deinit { unsafe buffer.deallocate() } @inline(__always) fileprivate func forwardTransform(_ index: Int) -> Int { forwardOffset &+ index } @inline(__always) fileprivate func backwardTransform(_ index: Int) -> Int { backwardOffset &+ index } subscript(forward index: Int) -> Int { get { #if INTERNAL_CHECKS_ENABLED precondition(((-range)...range).contains(index)) #endif return unsafe buffer[forwardTransform(index)] } set(newValue) { #if INTERNAL_CHECKS_ENABLED precondition(((-range)...range).contains(index)) #endif unsafe buffer[forwardTransform(index)] = newValue } } subscript(backward index: Int) -> Int { get { #if INTERNAL_CHECKS_ENABLED precondition(((-range)...range).contains(index)) #endif return unsafe buffer[backwardTransform(index)] } set(newValue) { #if INTERNAL_CHECKS_ENABLED precondition(((-range)...range).contains(index)) #endif unsafe buffer[backwardTransform(index)] = newValue } } } /// A two-dimensional region of a Myers diff algorithm edit graph. fileprivate struct EditGraphRect: Equatable { var left: Int var top: Int var right: Int var bottom: Int var width: Int { right &- left } var height: Int { bottom &- top } var size: Int { width &+ height } var delta: Int { width &- height } var isEven: Bool { delta.isMultiple(of: 2) } var isOdd: Bool { !delta.isMultiple(of: 2) } var max: Int { (size &+ 1) / 2 } /// Shrinks this box so that its bottom right corner is the top left corner of /// the given box. func cropped(toTopLeftOf limit: EditGraphRect) -> EditGraphRect { .init(left: left, top: top, right: limit.left, bottom: limit.top) } /// Shrinks this box so that its top left corner is the bottom right corner of /// the given box. func cropped(toBottomRightOf limit: EditGraphRect) -> EditGraphRect { .init(left: limit.right, top: limit.bottom, right: right, bottom: bottom) } /// Shrinks this box from both top and bottom by following diagonal paths /// (equal corresponding elements) in the two collections that are the target /// of diffing. func shrunk( old: UnsafeBufferPointer, new: UnsafeBufferPointer, cmp: (T, T) -> Bool ) -> EditGraphRect { var r = self while r.left < r.right, r.top < r.bottom, unsafe cmp(old[r.left], new[r.top]) { r.left &+= 1 r.top &+= 1 } while r.right > r.left, r.bottom > r.top, unsafe cmp(old[r.right - 1], new[r.bottom - 1]) { r.right &-= 1 r.bottom &-= 1 } return r } } fileprivate struct LinearMyers: ~Copyable { var kVector: DoubleKVector /// Implements a refinement of the Myers diffing algorithm that uses /// linear space for storing the "k vectors" during an iterative divide- /// and-conquer search. @available(SwiftStdlib 5.1, *) mutating func findDifferences( in initial: EditGraphRect, old: UnsafeBufferPointer, new: UnsafeBufferPointer, cmp: (T, T) -> Bool ) -> [CollectionDifference.Change] { var result: [CollectionDifference.Change] = [] var stack: [EditGraphRect] = [] var current = initial while true { let (edit, snakeBox) = unsafe middleSnake( in: current, old: old, new: new, cmp: cmp) // Append edit if exists if let edit { result.append(edit) } guard let snakeBox else { // Didn't find a new middle snake, so we'll look at the next area on the // stack next (the right side of the last snake) guard let next = stack.popLast() else { // Stack is empty -- all done with diff! return result } current = next continue } let headBox = unsafe current.cropped(toTopLeftOf: snakeBox) .shrunk(old: old, new: new, cmp: cmp) let tailBox = unsafe current.cropped(toBottomRightOf: snakeBox) .shrunk(old: old, new: new, cmp: cmp) // Process the left side of the snake next current = headBox // Save the right side of the snake for later stack.append(tailBox) } fatalError("Unreachable") } @available(SwiftStdlib 5.1, *) fileprivate mutating func middleSnake( in box: EditGraphRect, old: UnsafeBufferPointer, new: UnsafeBufferPointer, cmp: (T, T) -> Bool ) -> (CollectionDifference.Change?, EditGraphRect?) { guard box.size > 1 else { // One rightward or downward move represents a removal or insertion. if box.size == 0 { return (nil, nil) } else if box.width == 1 { return unsafe (.remove(offset: box.left, element: old[box.left], associatedWith: nil), nil) } else { return unsafe (.insert(offset: box.top, element: new[box.top], associatedWith: nil), nil) } } // Reset for depth == 0 kVector[forward: 1] = box.left kVector[backward: 1] = box.bottom for depth in 0...box.max { if box.isOdd { if let result = unsafe forwardSearch( in: box, depth: depth, old: old, new: new, cmp: cmp) { return result } _ = unsafe backwardSearch(in: box, depth: depth, old: old, new: new, cmp: cmp) } else { _ = unsafe forwardSearch(in: box, depth: depth, old: old, new: new, cmp: cmp) if let result = unsafe backwardSearch( in: box, depth: depth, old: old, new: new, cmp: cmp) { return result } } } fatalError("Unreachable") } @available(SwiftStdlib 5.1, *) fileprivate mutating func forwardSearch( in box: EditGraphRect, depth: Int, old: UnsafeBufferPointer, new: UnsafeBufferPointer, cmp: (T, T) -> Bool ) -> (CollectionDifference.Change, EditGraphRect)? { for k in stride(from: depth, through: -depth, by: -2) { // The furthest right and down we search var newRight, newBottom: Int // The top/left of the candidate middle snake let newLeft, newTop: Int // Information about the selected edit, if used let isInsertion: Bool let editIndex: Int // Choose whether this is an insertion or a deletion, then set up // bounds of candidate middle snake. if k == -depth || (k != depth && kVector[forward: k - 1] < kVector[forward: k + 1]) { newRight = kVector[forward: k + 1] newLeft = newRight newBottom = box.top + (newRight - box.left) - k isInsertion = true editIndex = newBottom - 1 } else { newLeft = kVector[forward: k - 1] newRight = newLeft + 1 newBottom = box.top + (newRight - box.left) - k isInsertion = false editIndex = newLeft } newTop = if depth == 0 || newRight != newLeft { newBottom } else { newBottom - 1 } // Follow any diagonal after the movement while newRight < box.right && newBottom < box.bottom, unsafe cmp(old[newRight], new[newBottom]) { newRight &+= 1 newBottom &+= 1 } kVector[forward: k] = newRight // Only check for overlap in _odd-sized_ boxes when moving _forward_ guard box.isOdd else { continue } // If `c` is in bounds and there's an overlap, return the identified edit // and the eliminated box. let c = k - box.delta if (c >= -(depth - 1) && c <= depth - 1) && newBottom >= kVector[backward: c] { let edit: CollectionDifference.Change = if isInsertion { unsafe .insert(offset: editIndex, element: new[editIndex], associatedWith: nil) } else { unsafe .remove(offset: editIndex, element: old[editIndex], associatedWith: nil) } return ( edit, EditGraphRect(left: newLeft, top: newTop, right: newRight, bottom: newBottom)) } } return nil } @available(SwiftStdlib 5.1, *) fileprivate mutating func backwardSearch( in box: EditGraphRect, depth: Int, old: UnsafeBufferPointer, new: UnsafeBufferPointer, cmp: (T, T) -> Bool ) -> (CollectionDifference.Change, EditGraphRect)? { for c in stride(from: depth, through: -depth, by: -2) { let k = c + box.delta // The furthest up and left we search var newLeft, newTop: Int // The bottom/right of the candidate middle snake let newRight, newBottom: Int // Information about the selected edit, if used let isInsertion: Bool let editIndex: Int // Choose whether this is an insertion or a deletion, then set up // bounds of candidate middle snake. if c == -depth || (c != depth && kVector[backward: c - 1] > kVector[backward: c + 1]) { newTop = kVector[backward: c + 1] newBottom = newTop newLeft = box.left + (newTop - box.top) + k isInsertion = false editIndex = newLeft } else { newBottom = kVector[backward: c - 1] newTop = newBottom - 1 newLeft = box.left + (newTop - box.top) + k isInsertion = true editIndex = newTop } newRight = if depth == 0 || newTop != newBottom { newLeft } else { newLeft + 1 } // Follow any diagonal after the movement while newLeft > box.left && newTop > box.top, unsafe cmp(old[newLeft - 1], new[newTop - 1]) { newLeft &-= 1 newTop &-= 1 } kVector[backward: c] = newTop // Only check for overlap in _even-sized_ boxes when moving _backward_ guard box.isEven else { continue } // If `k` is in bounds and there's an overlap, return the identified edit // and the eliminated box. if (k >= -depth && k <= depth) && newLeft <= kVector[forward: k] { let edit: CollectionDifference.Change = if isInsertion { unsafe .insert(offset: editIndex, element: new[editIndex], associatedWith: nil) } else { unsafe .remove(offset: editIndex, element: old[editIndex], associatedWith: nil) } return ( edit, EditGraphRect(left: newLeft, top: newTop, right: newRight, bottom: newBottom)) } } return nil } } extension LinearMyers { fileprivate init?( initialSize size: Int ) { guard size >= 0 else { return nil } self.kVector = DoubleKVector(size: size) } } @available(SwiftStdlib 5.1, *) func _linearSpaceMyers( from old: C, to new: D, using cmp: (C.Element, D.Element) -> Bool ) -> CollectionDifference where C: BidirectionalCollection, D: BidirectionalCollection, C.Element == D.Element { /* Splatting the collections into contiguous storage has two advantages: * * 1) Subscript access is much faster * 2) Subscript index becomes Int, matching the iterator types in the algorithm * * Combined, these effects dramatically improve performance when * collections differ significantly, without unduly degrading runtime when * the parameters are very similar. */ func _withContiguousStorage( for values: Col, _ body: (UnsafeBufferPointer) -> R ) -> R { if let result = values.withContiguousStorageIfAvailable(body) { return result } let array = ContiguousArray(values) return unsafe array.withUnsafeBufferPointer(body) } return unsafe _withContiguousStorage(for: old) { a in return unsafe _withContiguousStorage(for: new) { b in let box = unsafe EditGraphRect(left: 0, top: 0, right: old.count, bottom: new.count) .shrunk(old: a, new: b, cmp: cmp) guard var myers = LinearMyers(initialSize: box.size) else { return CollectionDifference([])! } let diffs = unsafe myers.findDifferences(in: box, old: a, new: b, cmp: cmp) return CollectionDifference(diffs)! } } }