diff --git a/.gitignore b/.gitignore index 397ae000f..cb33e5fe9 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ xcuserdata/ /Benchmarks/.swiftpm /Benchmarks/.build .docc-build +__pycache__ diff --git a/.spi.yml b/.spi.yml index adcda23e8..2aecf0613 100644 --- a/.spi.yml +++ b/.spi.yml @@ -1,4 +1,4 @@ version: 1 builder: configs: - - documentation_targets: [Collections, DequeModule, OrderedCollections] + - documentation_targets: [Collections, BitCollections, DequeModule, HashTreeCollections, HeapModule, OrderedCollections] diff --git a/Benchmarks/Benchmarks/CppBenchmarks.swift b/Benchmarks/Benchmarks/CppBenchmarks.swift deleted file mode 100644 index 19b51d8fe..000000000 --- a/Benchmarks/Benchmarks/CppBenchmarks.swift +++ /dev/null @@ -1,629 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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 -// -//===----------------------------------------------------------------------===// - -import CollectionsBenchmark -import CppBenchmarks - -internal class CppVector { - var ptr: UnsafeMutableRawPointer? - - init(_ input: [Int]) { - self.ptr = input.withUnsafeBufferPointer { buffer in - cpp_vector_create(buffer.baseAddress, buffer.count) - } - } - - deinit { - destroy() - } - - func destroy() { - if let ptr = ptr { - cpp_vector_destroy(ptr) - } - ptr = nil - } -} - -internal class CppDeque { - var ptr: UnsafeMutableRawPointer? - - init(_ input: [Int]) { - self.ptr = input.withUnsafeBufferPointer { buffer in - cpp_deque_create(buffer.baseAddress, buffer.count) - } - } - - deinit { - destroy() - } - - func destroy() { - if let ptr = ptr { - cpp_deque_destroy(ptr) - } - ptr = nil - } -} - -internal class CppUnorderedSet { - var ptr: UnsafeMutableRawPointer? - - init(_ input: [Int]) { - self.ptr = input.withUnsafeBufferPointer { buffer in - cpp_unordered_set_create(buffer.baseAddress, buffer.count) - } - } - - deinit { - destroy() - } - - func destroy() { - if let ptr = ptr { - cpp_unordered_set_destroy(ptr) - } - ptr = nil - } -} - -internal class CppUnorderedMap { - var ptr: UnsafeMutableRawPointer? - - init(_ input: [Int]) { - self.ptr = input.withUnsafeBufferPointer { buffer in - cpp_unordered_map_create(buffer.baseAddress, buffer.count) - } - } - - deinit { - destroy() - } - - func destroy() { - if let ptr = ptr { - cpp_unordered_map_destroy(ptr) - } - ptr = nil - } -} - - -extension Benchmark { - public mutating func addCppBenchmarks() { - cpp_set_hash_fn { value in value._rawHashValue(seed: 0) } - - self.addSimple( - title: "std::hash", - input: [Int].self - ) { input in - input.withUnsafeBufferPointer { buffer in - cpp_hash(buffer.baseAddress, buffer.count) - } - } - - self.addSimple( - title: "custom_intptr_hash (using Swift.Hasher)", - input: [Int].self - ) { input in - input.withUnsafeBufferPointer { buffer in - cpp_custom_hash(buffer.baseAddress, buffer.count) - } - } - - self.addSimple( - title: "std::vector push_back from integer range", - input: Int.self - ) { count in - cpp_vector_from_int_range(count) - } - - self.addSimple( - title: "std::deque push_back from integer range", - input: Int.self - ) { count in - cpp_deque_from_int_range(count) - } - - //-------------------------------------------------------------------------- - - self.addSimple( - title: "std::vector constructor from buffer", - input: [Int].self - ) { input in - input.withUnsafeBufferPointer { buffer in - cpp_vector_from_int_buffer(buffer.baseAddress, buffer.count) - } - } - - self.addSimple( - title: "std::deque constructor from buffer", - input: [Int].self - ) { input in - input.withUnsafeBufferPointer { buffer in - cpp_deque_from_int_buffer(buffer.baseAddress, buffer.count) - } - } - - //-------------------------------------------------------------------------- - - self.add( - title: "std::vector sequential iteration", - input: [Int].self - ) { input in - let vector = CppVector(input) - return { timer in - cpp_vector_iterate(vector.ptr) - } - } - - self.add( - title: "std::deque sequential iteration", - input: [Int].self - ) { input in - let deque = CppDeque(input) - return { timer in - cpp_deque_iterate(deque.ptr) - } - } - - //-------------------------------------------------------------------------- - - self.add( - title: "std::vector random-access offset lookups (operator [])", - input: ([Int], [Int]).self - ) { input, lookups in - let vector = CppVector(input) - return { timer in - lookups.withUnsafeBufferPointer { buffer in - cpp_vector_lookups_subscript(vector.ptr, buffer.baseAddress, buffer.count) - } - } - } - - self.add( - title: "std::deque random-access offset lookups (operator [])", - input: ([Int], [Int]).self - ) { input, lookups in - let vector = CppDeque(input) - return { timer in - lookups.withUnsafeBufferPointer { buffer in - cpp_deque_lookups_subscript(vector.ptr, buffer.baseAddress, buffer.count) - } - } - } - - //-------------------------------------------------------------------------- - - self.add( - title: "std::vector random-access offset lookups (at)", - input: ([Int], [Int]).self - ) { input, lookups in - let deque = CppVector(input) - return { timer in - lookups.withUnsafeBufferPointer { buffer in - cpp_vector_lookups_at(deque.ptr, buffer.baseAddress, buffer.count) - } - } - } - - self.add( - title: "std::deque at, random offsets", - input: ([Int], [Int]).self - ) { input, lookups in - let deque = CppDeque(input) - return { timer in - lookups.withUnsafeBufferPointer { buffer in - cpp_deque_lookups_at(deque.ptr, buffer.baseAddress, buffer.count) - } - } - } - - //-------------------------------------------------------------------------- - - self.addSimple( - title: "std::vector push_back", - input: [Int].self - ) { input in - input.withUnsafeBufferPointer { buffer in - cpp_vector_append_integers(buffer.baseAddress, buffer.count, false) - } - } - - self.addSimple( - title: "std::vector push_back, reserving capacity", - input: [Int].self - ) { input in - input.withUnsafeBufferPointer { buffer in - cpp_vector_append_integers(buffer.baseAddress, buffer.count, true) - } - } - - self.addSimple( - title: "std::deque push_back", - input: [Int].self - ) { input in - input.withUnsafeBufferPointer { buffer in - cpp_deque_append_integers(buffer.baseAddress, buffer.count) - } - } - - //-------------------------------------------------------------------------- - - self.addSimple( - title: "std::vector insert at front", - input: [Int].self - ) { input in - input.withUnsafeBufferPointer { buffer in - cpp_vector_prepend_integers(buffer.baseAddress, buffer.count, false) - } - } - - self.addSimple( - title: "std::vector insert at front, reserving capacity", - input: [Int].self - ) { input in - input.withUnsafeBufferPointer { buffer in - cpp_vector_prepend_integers(buffer.baseAddress, buffer.count, true) - } - } - - self.addSimple( - title: "std::deque push_front", - input: [Int].self - ) { input in - input.withUnsafeBufferPointer { buffer in - cpp_deque_prepend_integers(buffer.baseAddress, buffer.count) - } - } - - //-------------------------------------------------------------------------- - - self.addSimple( - title: "std::vector random insertions", - input: Insertions.self - ) { insertions in - insertions.values.withUnsafeBufferPointer { buffer in - cpp_vector_random_insertions(buffer.baseAddress, buffer.count, false) - } - } - - self.addSimple( - title: "std::deque random insertions", - input: Insertions.self - ) { insertions in - insertions.values.withUnsafeBufferPointer { buffer in - cpp_deque_random_insertions(buffer.baseAddress, buffer.count) - } - } - - //-------------------------------------------------------------------------- - - self.add( - title: "std::vector pop_back", - input: Int.self - ) { size in - return { timer in - let vector = CppVector(Array(0 ..< size)) - timer.measure { - cpp_vector_pop_back(vector.ptr) - } - vector.destroy() - } - } - - self.add( - title: "std::deque pop_back", - input: Int.self - ) { size in - return { timer in - let deque = CppDeque(Array(0 ..< size)) - timer.measure { - cpp_deque_pop_back(deque.ptr) - } - deque.destroy() - } - } - - //-------------------------------------------------------------------------- - - self.add( - title: "std::vector erase first", - input: Int.self - ) { size in - return { timer in - let vector = CppVector(Array(0 ..< size)) - timer.measure { - cpp_vector_pop_front(vector.ptr) - } - vector.destroy() - } - } - - self.add( - title: "std::deque pop_front", - input: Int.self - ) { size in - return { timer in - let deque = CppDeque(Array(0 ..< size)) - timer.measure { - cpp_deque_pop_front(deque.ptr) - } - deque.destroy() - } - } - - //-------------------------------------------------------------------------- - - self.add( - title: "std::vector random removals", - input: Insertions.self - ) { insertions in - let removals = Array(insertions.values.reversed()) - return { timer in - let vector = CppVector(Array(0 ..< removals.count)) - timer.measure { - removals.withUnsafeBufferPointer { buffer in - cpp_vector_random_removals(vector.ptr, buffer.baseAddress, buffer.count) - } - } - vector.destroy() - } - } - - self.add( - title: "std::deque random removals", - input: Insertions.self - ) { insertions in - let removals = Array(insertions.values.reversed()) - return { timer in - let deque = CppDeque(Array(0 ..< removals.count)) - timer.measure { - removals.withUnsafeBufferPointer { buffer in - cpp_deque_random_removals(deque.ptr, buffer.baseAddress, buffer.count) - } - } - deque.destroy() - } - } - - //-------------------------------------------------------------------------- - - self.add( - title: "std::vector sort", - input: [Int].self - ) { input in - return { timer in - let vector = CppVector(input) - timer.measure { - cpp_vector_sort(vector.ptr) - } - vector.destroy() - } - } - - self.add( - title: "std::deque sort", - input: [Int].self - ) { input in - return { timer in - let deque = CppDeque(input) - timer.measure { - cpp_deque_sort(deque.ptr) - } - deque.destroy() - } - } - - //-------------------------------------------------------------------------- - - self.addSimple( - title: "std::unordered_set insert from integer range", - input: Int.self - ) { count in - cpp_unordered_set_from_int_range(count) - } - - self.addSimple( - title: "std::unordered_set constructor from buffer", - input: [Int].self - ) { input in - input.withUnsafeBufferPointer { buffer in - cpp_unordered_set_from_int_buffer(buffer.baseAddress, buffer.count) - } - } - - self.add( - title: "std::unordered_set sequential iteration", - input: [Int].self - ) { input in - let set = CppUnorderedSet(input) - return { timer in - cpp_unordered_set_iterate(set.ptr) - } - } - - self.add( - title: "std::unordered_set successful find", - input: ([Int], [Int]).self - ) { input, lookups in - let set = CppUnorderedSet(input) - return { timer in - lookups.withUnsafeBufferPointer { buffer in - cpp_unordered_set_lookups(set.ptr, buffer.baseAddress, buffer.count, true) - } - } - } - - self.add( - title: "std::unordered_set unsuccessful find", - input: ([Int], [Int]).self - ) { input, lookups in - let set = CppUnorderedSet(input) - let lookups = lookups.map { $0 + input.count } - return { timer in - lookups.withUnsafeBufferPointer { buffer in - cpp_unordered_set_lookups(set.ptr, buffer.baseAddress, buffer.count, false) - } - } - } - - self.addSimple( - title: "std::unordered_set insert", - input: [Int].self - ) { input in - input.withUnsafeBufferPointer { buffer in - cpp_unordered_set_insert_integers(buffer.baseAddress, buffer.count, false) - } - } - - self.addSimple( - title: "std::unordered_set insert, reserving capacity", - input: [Int].self - ) { input in - input.withUnsafeBufferPointer { buffer in - cpp_unordered_set_insert_integers(buffer.baseAddress, buffer.count, true) - } - } - - self.add( - title: "std::unordered_set erase", - input: ([Int], [Int]).self - ) { input, removals in - return { timer in - let set = CppUnorderedSet(input) - timer.measure { - removals.withUnsafeBufferPointer { buffer in - cpp_unordered_set_removals(set.ptr, buffer.baseAddress, buffer.count) - } - } - set.destroy() - } - } - - //-------------------------------------------------------------------------- - - self.addSimple( - title: "std::unordered_map insert from integer range", - input: Int.self - ) { count in - cpp_unordered_map_from_int_range(count) - } - - self.add( - title: "std::unordered_map sequential iteration", - input: [Int].self - ) { input in - let set = CppUnorderedMap(input) - return { timer in - cpp_unordered_map_iterate(set.ptr) - } - } - - self.add( - title: "std::unordered_map successful find", - input: ([Int], [Int]).self - ) { input, lookups in - let map = CppUnorderedMap(input) - return { timer in - lookups.withUnsafeBufferPointer { buffer in - cpp_unordered_map_lookups(map.ptr, buffer.baseAddress, buffer.count, true) - } - } - } - - self.add( - title: "std::unordered_map unsuccessful find", - input: ([Int], [Int]).self - ) { input, lookups in - let map = CppUnorderedMap(input) - let lookups = lookups.map { $0 + input.count } - return { timer in - lookups.withUnsafeBufferPointer { buffer in - cpp_unordered_map_lookups(map.ptr, buffer.baseAddress, buffer.count, false) - } - } - } - - self.add( - title: "std::unordered_map subscript, existing key", - input: ([Int], [Int]).self - ) { input, lookups in - let map = CppUnorderedMap(input) - return { timer in - lookups.withUnsafeBufferPointer { buffer in - cpp_unordered_map_subscript(map.ptr, buffer.baseAddress, buffer.count) - } - } - } - - self.add( - title: "std::unordered_map subscript, new key", - input: ([Int], [Int]).self - ) { input, lookups in - let map = CppUnorderedMap(input) - let lookups = lookups.map { $0 + input.count } - return { timer in - lookups.withUnsafeBufferPointer { buffer in - cpp_unordered_map_subscript(map.ptr, buffer.baseAddress, buffer.count) - } - } - } - - self.addSimple( - title: "std::unordered_map insert", - input: [Int].self - ) { input in - input.withUnsafeBufferPointer { buffer in - cpp_unordered_map_insert_integers(buffer.baseAddress, buffer.count, false) - } - } - - self.addSimple( - title: "std::unordered_map insert, reserving capacity", - input: [Int].self - ) { input in - input.withUnsafeBufferPointer { buffer in - cpp_unordered_map_insert_integers(buffer.baseAddress, buffer.count, true) - } - } - - self.add( - title: "std::unordered_map erase existing", - input: ([Int], [Int]).self - ) { input, removals in - return { timer in - let map = CppUnorderedMap(input) - timer.measure { - removals.withUnsafeBufferPointer { buffer in - cpp_unordered_map_removals(map.ptr, buffer.baseAddress, buffer.count) - } - } - map.destroy() - } - } - - self.add( - title: "std::unordered_map erase missing", - input: ([Int], [Int]).self - ) { input, removals in - return { timer in - let map = CppUnorderedMap(input.map { input.count + $0 }) - timer.measure { - removals.withUnsafeBufferPointer { buffer in - cpp_unordered_map_removals(map.ptr, buffer.baseAddress, buffer.count) - } - } - map.destroy() - } - } - } -} diff --git a/Benchmarks/Benchmarks/Library.json b/Benchmarks/Benchmarks/Library.json deleted file mode 100644 index 4d3cad8b4..000000000 --- a/Benchmarks/Benchmarks/Library.json +++ /dev/null @@ -1,2001 +0,0 @@ -{ - "kind": "group", - "title": "All results", - "directory": "Results", - "contents": [ - { - "kind": "group", - "title": "Individual types", - "directory": "single", - "contents": [ - { - "kind": "group", - "title": "Array", - "contents": [ - { - "kind": "chart", - "title": "operations", - "tasks": [ - "Array init from unsafe buffer", - "Array sequential iteration", - "Array subscript get, random offsets", - "Array append", - "Array append, reserving capacity", - "Array prepend", - "Array prepend, reserving capacity", - "Array removeFirst", - "Array removeLast", - "Array sort" - ] - }, - { - "kind": "chart", - "title": "access", - "tasks": [ - "Array sequential iteration", - "Array subscript get, random offsets" - ] - }, - { - "kind": "chart", - "title": "mutate", - "tasks": [ - "Array mutate through subscript", - "Array random swaps", - "Array partitioning around middle", - "Array sort" - ] - }, - ] - }, - { - "kind": "group", - "title": "Set", - "contents": [ - { - "kind": "chart", - "title": "operations", - "tasks": [ - "Set init from range", - "Set init from unsafe buffer", - "Set sequential iteration", - "Set successful contains", - "Set unsuccessful contains", - "Set insert", - "Set insert, reserving capacity", - "Set remove" - ] - }, - { - "kind": "variants", - "charts": [ - { - "kind": "chart", - "title": "union with Self", - "tasks": [ - "Set union with Self (0% overlap)", - "Set union with Self (25% overlap)", - "Set union with Self (50% overlap)", - "Set union with Self (75% overlap)", - "Set union with Self (100% overlap)" - ] - }, - { - "kind": "chart", - "title": "union with Array", - "tasks": [ - "Set union with Array (0% overlap)", - "Set union with Array (25% overlap)", - "Set union with Array (50% overlap)", - "Set union with Array (75% overlap)", - "Set union with Array (100% overlap)", - ] - }, - { - "kind": "chart", - "title": "formUnion with Self", - "tasks": [ - "Set formUnion with Self (0% overlap)", - "Set formUnion with Self (25% overlap)", - "Set formUnion with Self (50% overlap)", - "Set formUnion with Self (75% overlap)", - "Set formUnion with Self (100% overlap)", - ] - }, - { - "kind": "chart", - "title": "formUnion with Array", - "tasks": [ - "Set formUnion with Array (0% overlap)", - "Set formUnion with Array (25% overlap)", - "Set formUnion with Array (50% overlap)", - "Set formUnion with Array (75% overlap)", - "Set formUnion with Array (100% overlap)", - ] - }, - ] - }, - { - "kind": "variants", - "charts": [ - { - "kind": "chart", - "title": "intersection with Self", - "tasks": [ - "Set intersection with Self (0% overlap)", - "Set intersection with Self (25% overlap)", - "Set intersection with Self (50% overlap)", - "Set intersection with Self (75% overlap)", - "Set intersection with Self (100% overlap)", - ] - }, - { - "kind": "chart", - "title": "intersection with Array", - "tasks": [ - "Set intersection with Array (0% overlap)", - "Set intersection with Array (25% overlap)", - "Set intersection with Array (50% overlap)", - "Set intersection with Array (75% overlap)", - "Set intersection with Array (100% overlap)", - ] - }, - { - "kind": "chart", - "title": "formIntersection with Self", - "tasks": [ - "Set formIntersection with Self (0% overlap)", - "Set formIntersection with Self (25% overlap)", - "Set formIntersection with Self (50% overlap)", - "Set formIntersection with Self (75% overlap)", - "Set formIntersection with Self (100% overlap)", - ] - }, - { - "kind": "chart", - "title": "formIntersection with Array", - "tasks": [ - "Set formIntersection with Array (0% overlap)", - "Set formIntersection with Array (25% overlap)", - "Set formIntersection with Array (50% overlap)", - "Set formIntersection with Array (75% overlap)", - "Set formIntersection with Array (100% overlap)", - ] - }, - ] - }, - { - "kind": "variants", - "charts": [ - { - "kind": "chart", - "title": "symmetricDifference with Self", - "tasks": [ - "Set symmetricDifference with Self (0% overlap)", - "Set symmetricDifference with Self (25% overlap)", - "Set symmetricDifference with Self (50% overlap)", - "Set symmetricDifference with Self (75% overlap)", - "Set symmetricDifference with Self (100% overlap)", - ] - }, - { - "kind": "chart", - "title": "symmetricDifference with Array", - "tasks": [ - "Set symmetricDifference with Array (0% overlap)", - "Set symmetricDifference with Array (25% overlap)", - "Set symmetricDifference with Array (50% overlap)", - "Set symmetricDifference with Array (75% overlap)", - "Set symmetricDifference with Array (100% overlap)", - ] - }, - { - "kind": "chart", - "title": "formSymmetricDifference with Self", - "tasks": [ - "Set formSymmetricDifference with Self (0% overlap)", - "Set formSymmetricDifference with Self (25% overlap)", - "Set formSymmetricDifference with Self (50% overlap)", - "Set formSymmetricDifference with Self (75% overlap)", - "Set formSymmetricDifference with Self (100% overlap)", - ] - }, - { - "kind": "chart", - "title": "formSymmetricDifference with Array", - "tasks": [ - "Set formSymmetricDifference with Array (0% overlap)", - "Set formSymmetricDifference with Array (25% overlap)", - "Set formSymmetricDifference with Array (50% overlap)", - "Set formSymmetricDifference with Array (75% overlap)", - "Set formSymmetricDifference with Array (100% overlap)", - ] - }, - ] - }, - { - "kind": "variants", - "charts": [ - { - "kind": "chart", - "title": "subtracting Self", - "tasks": [ - "Set subtracting Self (0% overlap)", - "Set subtracting Self (25% overlap)", - "Set subtracting Self (50% overlap)", - "Set subtracting Self (75% overlap)", - "Set subtracting Self (100% overlap)", - ] - }, - { - "kind": "chart", - "title": "subtracting Array", - "tasks": [ - "Set subtracting Array (0% overlap)", - "Set subtracting Array (25% overlap)", - "Set subtracting Array (50% overlap)", - "Set subtracting Array (75% overlap)", - "Set subtracting Array (100% overlap)", - ] - }, - { - "kind": "chart", - "title": "subtract Self", - "tasks": [ - "Set subtract Self (0% overlap)", - "Set subtract Self (25% overlap)", - "Set subtract Self (50% overlap)", - "Set subtract Self (75% overlap)", - "Set subtract Self (100% overlap)", - ] - }, - { - "kind": "chart", - "title": "subtract with Array", - "tasks": [ - "Set subtract Array (0% overlap)", - "Set subtract Array (25% overlap)", - "Set subtract Array (50% overlap)", - "Set subtract Array (75% overlap)", - "Set subtract Array (100% overlap)", - ] - }, - ] - }, - ] - }, - { - "kind": "group", - "title": "Dictionary", - "contents": [ - { - "kind": "chart", - "title": "operations", - "tasks": [ - "Dictionary init(uniqueKeysWithValues:)", - "Dictionary sequential iteration", - "Dictionary subscript, successful lookups", - "Dictionary subscript, insert", - "Dictionary subscript, remove existing", - ] - }, - { - "kind": "chart", - "title": "iteration", - "tasks": [ - "Dictionary sequential iteration", - "Dictionary.Keys sequential iteration", - "Dictionary.Values sequential iteration" - ] - }, - { - "kind": "chart", - "title": "lookups", - "tasks": [ - "Dictionary subscript, successful lookups", - "Dictionary subscript, unsuccessful lookups", - "Dictionary defaulted subscript, successful lookups", - "Dictionary defaulted subscript, unsuccessful lookups", - "Dictionary successful index(forKey:)", - "Dictionary unsuccessful index(forKey:)", - ] - }, - { - "kind": "chart", - "title": "subscript", - "tasks": [ - "Dictionary subscript, successful lookups", - "Dictionary subscript, unsuccessful lookups", - "Dictionary subscript, noop setter", - "Dictionary subscript, set existing", - "Dictionary subscript, _modify", - "Dictionary subscript, insert", - "Dictionary subscript, insert, reserving capacity", - "Dictionary subscript, remove existing", - "Dictionary subscript, remove missing", - ] - }, - { - "kind": "chart", - "title": "defaulted subscript", - "tasks": [ - "Dictionary defaulted subscript, successful lookups", - "Dictionary defaulted subscript, unsuccessful lookups", - "Dictionary defaulted subscript, _modify existing", - "Dictionary defaulted subscript, _modify missing", - ] - }, - { - "kind": "chart", - "title": "mutations", - "tasks": [ - "Dictionary updateValue(_:forKey:), existing", - "Dictionary subscript, set existing", - "Dictionary subscript, _modify", - "Dictionary defaulted subscript, _modify existing", - ] - }, - { - "kind": "chart", - "title": "removals", - "tasks": [ - "Dictionary subscript, remove existing", - "Dictionary subscript, remove missing", - "Dictionary random removals (existing keys)", - "Dictionary random removals (missing keys)", - ] - }, - ] - }, - { - "kind": "group", - "title": "Deque", - "contents": [ - { - "kind": "chart", - "title": "operations", - "tasks": [ - "Deque init from unsafe buffer", - "Deque sequential iteration (contiguous)", - "Deque subscript get, random offsets (contiguous)", - "Deque append", - "Deque append, reserving capacity", - "Deque prepend", - "Deque prepend, reserving capacity", - "Deque removeFirst (contiguous)", - "Deque removeLast (contiguous)", - "Deque sort (contiguous)", - "Deque sort (discontiguous)" - ] - }, - { - "kind": "chart", - "title": "access", - "tasks": [ - "Deque sequential iteration (contiguous)", - "Deque sequential iteration (discontiguous)", - "Deque subscript get, random offsets (contiguous)", - "Deque subscript get, random offsets (discontiguous)", - "Deque mutate through subscript (contiguous)", - "Deque mutate through subscript (discontiguous)" - ] - }, - { - "kind": "chart", - "title": "mutate", - "tasks": [ - "Deque mutate through subscript (contiguous)", - "Deque mutate through subscript (discontiguous)", - "Deque random swaps (contiguous)", - "Deque random swaps (discontiguous)", - "Deque partitioning around middle (contiguous)", - "Deque partitioning around middle (discontiguous)", - "Deque sort (contiguous)", - "Deque sort (discontiguous)" - ] - }, - { - "kind": "chart", - "title": "push", - "tasks": [ - "Deque append", - "Deque append, reserving capacity", - "Deque prepend", - "Deque prepend, reserving capacity" - ] - }, - { - "kind": "chart", - "title": "pop", - "tasks": [ - "Deque removeFirst (contiguous)", - "Deque removeFirst (discontiguous)", - "Deque removeLast (contiguous)", - "Deque removeLast (discontiguous)" - ] - }, - ] - }, - { - "kind": "group", - "title": "OrderedSet", - "contents": [ - { - "kind": "chart", - "title": "operations", - "tasks": [ - "OrderedSet init from range", - "OrderedSet init from unsafe buffer", - "OrderedSet sequential iteration", - "OrderedSet successful contains", - "OrderedSet unsuccessful contains", - "OrderedSet append", - "OrderedSet append, reserving capacity", - "OrderedSet remove", - ] - }, - { - "kind": "chart", - "title": "initializers", - "tasks": [ - "OrderedSet init from range", - "OrderedSet init from unsafe buffer", - "OrderedSet init(uncheckedUniqueElements:) from range" - ] - }, - { - "kind": "chart", - "title": "mutations", - "tasks": [ - "OrderedSet random swaps", - "OrderedSet partitioning around middle", - "OrderedSet sort", - ] - }, - { - "kind": "chart", - "title": "range replaceable", - "tasks": [ - "OrderedSet append", - "OrderedSet append, reserving capacity", - "OrderedSet prepend", - "OrderedSet prepend, reserving capacity", - "OrderedSet random insertions, reserving capacity", - "OrderedSet remove", - "OrderedSet removeLast", - "OrderedSet removeFirst", - ] - }, - { - "kind": "variants", - "charts": [ - { - "kind": "chart", - "title": "union with Self", - "tasks": [ - "OrderedSet union with Self (0% overlap)", - "OrderedSet union with Self (25% overlap)", - "OrderedSet union with Self (50% overlap)", - "OrderedSet union with Self (75% overlap)", - "OrderedSet union with Self (100% overlap)", - ] - }, - { - "kind": "chart", - "title": "union with Array", - "tasks": [ - "OrderedSet union with Array (0% overlap)", - "OrderedSet union with Array (25% overlap)", - "OrderedSet union with Array (50% overlap)", - "OrderedSet union with Array (75% overlap)", - "OrderedSet union with Array (100% overlap)", - ] - }, - { - "kind": "chart", - "title": "formUnion with Self", - "tasks": [ - "OrderedSet formUnion with Self (0% overlap)", - "OrderedSet formUnion with Self (25% overlap)", - "OrderedSet formUnion with Self (50% overlap)", - "OrderedSet formUnion with Self (75% overlap)", - "OrderedSet formUnion with Self (100% overlap)", - ] - }, - { - "kind": "chart", - "title": "formUnion with Array", - "tasks": [ - "OrderedSet formUnion with Array (0% overlap)", - "OrderedSet formUnion with Array (25% overlap)", - "OrderedSet formUnion with Array (50% overlap)", - "OrderedSet formUnion with Array (75% overlap)", - "OrderedSet formUnion with Array (100% overlap)", - ] - }, - ] - }, - { - "kind": "variants", - "charts": [ - { - "kind": "chart", - "title": "intersection with Self", - "tasks": [ - "OrderedSet intersection with Self (0% overlap)", - "OrderedSet intersection with Self (25% overlap)", - "OrderedSet intersection with Self (50% overlap)", - "OrderedSet intersection with Self (75% overlap)", - "OrderedSet intersection with Self (100% overlap)", - ] - }, - { - "kind": "chart", - "title": "intersection with Array", - "tasks": [ - "OrderedSet intersection with Array (0% overlap)", - "OrderedSet intersection with Array (25% overlap)", - "OrderedSet intersection with Array (50% overlap)", - "OrderedSet intersection with Array (75% overlap)", - "OrderedSet intersection with Array (100% overlap)", - ] - }, - { - "kind": "chart", - "title": "formIntersection with Self", - "tasks": [ - "OrderedSet formIntersection with Self (0% overlap)", - "OrderedSet formIntersection with Self (25% overlap)", - "OrderedSet formIntersection with Self (50% overlap)", - "OrderedSet formIntersection with Self (75% overlap)", - "OrderedSet formIntersection with Self (100% overlap)", - ] - }, - { - "kind": "chart", - "title": "formIntersection with Array", - "tasks": [ - "OrderedSet formIntersection with Array (0% overlap)", - "OrderedSet formIntersection with Array (25% overlap)", - "OrderedSet formIntersection with Array (50% overlap)", - "OrderedSet formIntersection with Array (75% overlap)", - "OrderedSet formIntersection with Array (100% overlap)", - ] - }, - ] - }, - { - "kind": "variants", - "charts": [ - { - "kind": "chart", - "title": "symmetricDifference with Self", - "tasks": [ - "OrderedSet symmetricDifference with Self (0% overlap)", - "OrderedSet symmetricDifference with Self (25% overlap)", - "OrderedSet symmetricDifference with Self (50% overlap)", - "OrderedSet symmetricDifference with Self (75% overlap)", - "OrderedSet symmetricDifference with Self (100% overlap)", - ] - }, - { - "kind": "chart", - "title": "symmetricDifference with Array", - "tasks": [ - "OrderedSet symmetricDifference with Array (0% overlap)", - "OrderedSet symmetricDifference with Array (25% overlap)", - "OrderedSet symmetricDifference with Array (50% overlap)", - "OrderedSet symmetricDifference with Array (75% overlap)", - "OrderedSet symmetricDifference with Array (100% overlap)", - ] - }, - { - "kind": "chart", - "title": "formSymmetricDifference with Self", - "tasks": [ - "OrderedSet formSymmetricDifference with Self (0% overlap)", - "OrderedSet formSymmetricDifference with Self (25% overlap)", - "OrderedSet formSymmetricDifference with Self (50% overlap)", - "OrderedSet formSymmetricDifference with Self (75% overlap)", - "OrderedSet formSymmetricDifference with Self (100% overlap)", - ] - }, - { - "kind": "chart", - "title": "formSymmetricDifference with Array", - "tasks": [ - "OrderedSet formSymmetricDifference with Array (0% overlap)", - "OrderedSet formSymmetricDifference with Array (25% overlap)", - "OrderedSet formSymmetricDifference with Array (50% overlap)", - "OrderedSet formSymmetricDifference with Array (75% overlap)", - "OrderedSet formSymmetricDifference with Array (100% overlap)", - ] - }, - ] - }, - { - "kind": "variants", - "charts": [ - { - "kind": "chart", - "title": "subtracting Self", - "tasks": [ - "OrderedSet subtracting Self (0% overlap)", - "OrderedSet subtracting Self (25% overlap)", - "OrderedSet subtracting Self (50% overlap)", - "OrderedSet subtracting Self (75% overlap)", - "OrderedSet subtracting Self (100% overlap)", - ] - }, - { - "kind": "chart", - "title": "subtracting Array", - "tasks": [ - "OrderedSet subtracting Array (0% overlap)", - "OrderedSet subtracting Array (25% overlap)", - "OrderedSet subtracting Array (50% overlap)", - "OrderedSet subtracting Array (75% overlap)", - "OrderedSet subtracting Array (100% overlap)", - ] - }, - { - "kind": "chart", - "title": "subtract Self", - "tasks": [ - "OrderedSet subtract Self (0% overlap)", - "OrderedSet subtract Self (25% overlap)", - "OrderedSet subtract Self (50% overlap)", - "OrderedSet subtract Self (75% overlap)", - "OrderedSet subtract Self (100% overlap)", - ] - }, - { - "kind": "chart", - "title": "subtract with Array", - "tasks": [ - "OrderedSet subtract Array (0% overlap)", - "OrderedSet subtract Array (25% overlap)", - "OrderedSet subtract Array (50% overlap)", - "OrderedSet subtract Array (75% overlap)", - "OrderedSet subtract Array (100% overlap)", - ] - }, - ] - }, - ] - }, - { - "kind": "group", - "title": "OrderedDictionary", - "contents": [ - { - "kind": "chart", - "title": "operations", - "tasks": [ - "OrderedDictionary init(uniqueKeysWithValues:)", - "OrderedDictionary sequential iteration", - "OrderedDictionary subscript, successful lookups", - "OrderedDictionary subscript, append", - "OrderedDictionary subscript, remove existing", - ] - }, - { - "kind": "chart", - "title": "initializers", - "tasks": [ - "OrderedDictionary init(uniqueKeysWithValues:)", - "OrderedDictionary init(uncheckedUniqueKeysWithValues:)", - "OrderedDictionary init(uncheckedUniqueKeys:values:)" - ] - }, - { - "kind": "chart", - "title": "iteration", - "tasks": [ - "OrderedDictionary sequential iteration", - "OrderedDictionary.Keys sequential iteration", - "OrderedDictionary.Values sequential iteration" - ] - }, - { - "kind": "chart", - "title": "lookups", - "tasks": [ - "OrderedDictionary subscript, successful lookups", - "OrderedDictionary subscript, unsuccessful lookups", - "OrderedDictionary defaulted subscript, successful lookups", - "OrderedDictionary defaulted subscript, unsuccessful lookups", - "OrderedDictionary successful index(forKey:)", - "OrderedDictionary unsuccessful index(forKey:)", - ] - }, - { - "kind": "chart", - "title": "subscript", - "tasks": [ - "OrderedDictionary subscript, successful lookups", - "OrderedDictionary subscript, unsuccessful lookups", - "OrderedDictionary subscript, noop setter", - "OrderedDictionary subscript, set existing", - "OrderedDictionary subscript, _modify", - "OrderedDictionary subscript, append", - "OrderedDictionary subscript, append, reserving capacity", - "OrderedDictionary subscript, remove existing", - "OrderedDictionary subscript, remove missing", - ] - }, - { - "kind": "chart", - "title": "defaulted subscript", - "tasks": [ - "OrderedDictionary defaulted subscript, successful lookups", - "OrderedDictionary defaulted subscript, unsuccessful lookups", - "OrderedDictionary defaulted subscript, _modify existing", - "OrderedDictionary defaulted subscript, _modify missing", - ] - }, - { - "kind": "chart", - "title": "mutations", - "tasks": [ - "OrderedDictionary updateValue(_:forKey:), existing", - "OrderedDictionary subscript, set existing", - "OrderedDictionary subscript, _modify", - "OrderedDictionary defaulted subscript, _modify existing", - "OrderedDictionary random swaps", - "OrderedDictionary partitioning around middle", - "OrderedDictionary sort", - ] - }, - { - "kind": "chart", - "title": "removals", - "tasks": [ - "OrderedDictionary subscript, remove existing", - "OrderedDictionary subscript, remove missing", - "OrderedDictionary removeLast", - "OrderedDictionary removeFirst", - "OrderedDictionary random removals (offset-based)", - "OrderedDictionary random removals (existing keys)", - "OrderedDictionary random removals (missing keys)", - ] - }, - ] - } - ] - }, - { - "kind": "group", - "title": "Against other Swift collections", - "directory": "stdlib", - "contents": [ - { - "kind": "group", - "title": "Deque vs Array", - "contents": [ - { - "kind": "chart", - "title": "init from buffer of integers", - "tasks": [ - "Deque init from unsafe buffer", - "Array init from unsafe buffer" - ] - }, - { - "kind": "chart", - "title": "sequential iteration", - "tasks": [ - "Deque sequential iteration (contiguous)", - "Deque sequential iteration (discontiguous)", - "Array sequential iteration", - ] - }, - { - "kind": "chart", - "title": "random-access offset lookups", - "tasks": [ - "Deque subscript get, random offsets (contiguous)", - "Deque subscript get, random offsets (discontiguous)", - "Array subscript get, random offsets" - ] - }, - { - "kind": "chart", - "title": "mutate through subscript", - "tasks": [ - "Deque mutate through subscript (contiguous)", - "Deque mutate through subscript (discontiguous)", - "Array mutate through subscript" - ] - }, - { - "kind": "chart", - "title": "random swaps", - "tasks": [ - "Deque random swaps (contiguous)", - "Deque random swaps (discontiguous)", - "Array random swaps", - ] - }, - { - "kind": "chart", - "title": "partitioning around middle", - "tasks": [ - "Deque partitioning around middle (contiguous)", - "Deque partitioning around middle (discontiguous)", - "Array partitioning around middle", - ] - }, - { - "kind": "chart", - "title": "sort", - "tasks": [ - "Deque sort (contiguous)", - "Deque sort (discontiguous)", - "Array sort", - ] - }, - { - "kind": "chart", - "title": "append individual integers", - "tasks": [ - "Deque append", - "Deque append, reserving capacity", - "Array append", - "Array append, reserving capacity" - ] - }, - { - "kind": "chart", - "title": "prepend individual integers", - "tasks": [ - "Deque prepend", - "Deque prepend, reserving capacity", - "Array prepend", - "Array prepend, reserving capacity", - ] - }, - { - "kind": "chart", - "title": "random insertions", - "tasks": [ - "Deque random insertions", - "Array random insertions", - ] - }, - { - "kind": "chart", - "title": "removeFirst", - "tasks": [ - "Deque removeFirst (contiguous)", - "Deque removeFirst (discontiguous)", - "Array removeFirst", - ] - }, - { - "kind": "chart", - "title": "removeLast", - "tasks": [ - "Deque removeLast (contiguous)", - "Deque removeLast (discontiguous)", - "Array removeLast", - ] - }, - { - "kind": "chart", - "title": "random removals", - "tasks": [ - "Deque random removals (contiguous)", - "Deque random removals (discontiguous)", - "Array random removals", - ] - }, - ] - }, - { - "kind": "group", - "title": "OrderedSet vs Set", - "contents": [ - { - "kind": "chart", - "title": "init from buffer of integers", - "tasks": [ - "OrderedSet init from unsafe buffer", - "Set init from unsafe buffer", - ] - }, - { - "kind": "chart", - "title": "init from range of integers", - "tasks": [ - "OrderedSet init from range", - "Set init from range", - ] - }, - { - "kind": "chart", - "title": "sequential iteration", - "tasks": [ - "OrderedSet sequential iteration", - "Set sequential iteration", - ] - }, - { - "kind": "chart", - "title": "successful random lookups", - "tasks": [ - "OrderedSet successful contains", - "Set successful contains", - ] - }, - { - "kind": "chart", - "title": "unsuccessful random lookups", - "tasks": [ - "OrderedSet unsuccessful contains", - "Set unsuccessful contains", - ] - }, - { - "kind": "chart", - "title": "insert", - "tasks": [ - "OrderedSet append", - "OrderedSet append, reserving capacity", - "Set insert", - "Set insert, reserving capacity", - ] - }, - { - "kind": "chart", - "title": "remove", - "tasks": [ - "OrderedSet remove", - "Set remove", - ] - }, - { - "kind": "variants", - "charts": [ - { - "kind": "chart", - "title": "union (0% overlap)", - "tasks": [ - "OrderedSet union with Self (0% overlap)", - "OrderedSet union with Array (0% overlap)", - "Set union with Self (0% overlap)", - "Set union with Array (0% overlap)", - ] - }, - { - "kind": "chart", - "title": "union (25% overlap)", - "tasks": [ - "OrderedSet union with Self (25% overlap)", - "OrderedSet union with Array (25% overlap)", - "Set union with Self (25% overlap)", - "Set union with Array (25% overlap)", - ] - }, - { - "kind": "chart", - "title": "union (50% overlap)", - "tasks": [ - "OrderedSet union with Self (50% overlap)", - "OrderedSet union with Array (50% overlap)", - "Set union with Self (50% overlap)", - "Set union with Array (50% overlap)", - ] - }, - { - "kind": "chart", - "title": "union (75% overlap)", - "tasks": [ - "OrderedSet union with Self (75% overlap)", - "OrderedSet union with Array (75% overlap)", - "Set union with Self (75% overlap)", - "Set union with Array (75% overlap)", - ] - }, - { - "kind": "chart", - "title": "union (100% overlap)", - "tasks": [ - "OrderedSet union with Self (100% overlap)", - "OrderedSet union with Array (100% overlap)", - "Set union with Self (100% overlap)", - "Set union with Array (100% overlap)", - ] - }, - ] - }, - { - "kind": "variants", - "charts": [ - { - "kind": "chart", - "title": "formUnion (0% overlap)", - "tasks": [ - "OrderedSet formUnion with Self (0% overlap)", - "OrderedSet formUnion with Array (0% overlap)", - "Set formUnion with Self (0% overlap)", - "Set formUnion with Array (0% overlap)", - ] - }, - { - "kind": "chart", - "title": "formUnion (25% overlap)", - "tasks": [ - "OrderedSet formUnion with Self (25% overlap)", - "OrderedSet formUnion with Array (25% overlap)", - "Set formUnion with Self (25% overlap)", - "Set formUnion with Array (25% overlap)", - ] - }, - { - "kind": "chart", - "title": "formUnion (50% overlap)", - "tasks": [ - "OrderedSet formUnion with Self (50% overlap)", - "OrderedSet formUnion with Array (50% overlap)", - "Set formUnion with Self (50% overlap)", - "Set formUnion with Array (50% overlap)", - ] - }, - { - "kind": "chart", - "title": "formUnion (75% overlap)", - "tasks": [ - "OrderedSet formUnion with Self (75% overlap)", - "OrderedSet formUnion with Array (75% overlap)", - "Set formUnion with Self (75% overlap)", - "Set formUnion with Array (75% overlap)", - ] - }, - { - "kind": "chart", - "title": "formUnion (100% overlap)", - "tasks": [ - "OrderedSet formUnion with Self (100% overlap)", - "OrderedSet formUnion with Array (100% overlap)", - "Set formUnion with Self (100% overlap)", - "Set formUnion with Array (100% overlap)", - ] - }, - ] - }, - { - "kind": "variants", - "charts": [ - { - "kind": "chart", - "title": "intersection (0% overlap)", - "tasks": [ - "OrderedSet intersection with Self (0% overlap)", - "OrderedSet intersection with Array (0% overlap)", - "Set intersection with Self (0% overlap)", - "Set intersection with Array (0% overlap)", - ] - }, - { - "kind": "chart", - "title": "intersection (25% overlap)", - "tasks": [ - "OrderedSet intersection with Self (25% overlap)", - "OrderedSet intersection with Array (25% overlap)", - "Set intersection with Self (25% overlap)", - "Set intersection with Array (25% overlap)", - ] - }, - { - "kind": "chart", - "title": "intersection (50% overlap)", - "tasks": [ - "OrderedSet intersection with Self (50% overlap)", - "OrderedSet intersection with Array (50% overlap)", - "Set intersection with Self (50% overlap)", - "Set intersection with Array (50% overlap)", - ] - }, - { - "kind": "chart", - "title": "intersection (75% overlap)", - "tasks": [ - "OrderedSet intersection with Self (75% overlap)", - "OrderedSet intersection with Array (75% overlap)", - "Set intersection with Self (75% overlap)", - "Set intersection with Array (75% overlap)", - ] - }, - { - "kind": "chart", - "title": "intersection (100% overlap)", - "tasks": [ - "OrderedSet intersection with Self (100% overlap)", - "OrderedSet intersection with Array (100% overlap)", - "Set intersection with Self (100% overlap)", - "Set intersection with Array (100% overlap)", - ] - }, - ] - }, - { - "kind": "variants", - "charts": [ - { - "kind": "chart", - "title": "formIntersection (0% overlap)", - "tasks": [ - "OrderedSet formIntersection with Self (0% overlap)", - "OrderedSet formIntersection with Array (0% overlap)", - "Set formIntersection with Self (0% overlap)", - "Set formIntersection with Array (0% overlap)", - ] - }, - { - "kind": "chart", - "title": "formIntersection (25% overlap)", - "tasks": [ - "OrderedSet formIntersection with Self (25% overlap)", - "OrderedSet formIntersection with Array (25% overlap)", - "Set formIntersection with Self (25% overlap)", - "Set formIntersection with Array (25% overlap)", - ] - }, - { - "kind": "chart", - "title": "formIntersection (50% overlap)", - "tasks": [ - "OrderedSet formIntersection with Self (50% overlap)", - "OrderedSet formIntersection with Array (50% overlap)", - "Set formIntersection with Self (50% overlap)", - "Set formIntersection with Array (50% overlap)", - ] - }, - { - "kind": "chart", - "title": "formIntersection (75% overlap)", - "tasks": [ - "OrderedSet formIntersection with Self (75% overlap)", - "OrderedSet formIntersection with Array (75% overlap)", - "Set formIntersection with Self (75% overlap)", - "Set formIntersection with Array (75% overlap)", - ] - }, - { - "kind": "chart", - "title": "formIntersection (100% overlap)", - "tasks": [ - "OrderedSet formIntersection with Self (100% overlap)", - "OrderedSet formIntersection with Array (100% overlap)", - "Set formIntersection with Self (100% overlap)", - "Set formIntersection with Array (100% overlap)", - ] - }, - ] - }, - { - "kind": "variants", - "charts": [ - { - "kind": "chart", - "title": "symmetricDifference (0% overlap)", - "tasks": [ - "OrderedSet symmetricDifference with Self (0% overlap)", - "OrderedSet symmetricDifference with Array (0% overlap)", - "Set symmetricDifference with Self (0% overlap)", - "Set symmetricDifference with Array (0% overlap)", - ] - }, - { - "kind": "chart", - "title": "symmetricDifference (25% overlap)", - "tasks": [ - "OrderedSet symmetricDifference with Self (25% overlap)", - "OrderedSet symmetricDifference with Array (25% overlap)", - "Set symmetricDifference with Self (25% overlap)", - "Set symmetricDifference with Array (25% overlap)", - ] - }, - { - "kind": "chart", - "title": "symmetricDifference (50% overlap)", - "tasks": [ - "OrderedSet symmetricDifference with Self (50% overlap)", - "OrderedSet symmetricDifference with Array (50% overlap)", - "Set symmetricDifference with Self (50% overlap)", - "Set symmetricDifference with Array (50% overlap)", - ] - }, - { - "kind": "chart", - "title": "symmetricDifference (75% overlap)", - "tasks": [ - "OrderedSet symmetricDifference with Self (75% overlap)", - "OrderedSet symmetricDifference with Array (75% overlap)", - "Set symmetricDifference with Self (75% overlap)", - "Set symmetricDifference with Array (75% overlap)", - ] - }, - { - "kind": "chart", - "title": "symmetricDifference (100% overlap)", - "tasks": [ - "OrderedSet symmetricDifference with Self (100% overlap)", - "OrderedSet symmetricDifference with Array (100% overlap)", - "Set symmetricDifference with Self (100% overlap)", - "Set symmetricDifference with Array (100% overlap)", - ] - }, - ] - }, - { - "kind": "variants", - "charts": [ - { - "kind": "chart", - "title": "formSymmetricDifference (0% overlap)", - "tasks": [ - "OrderedSet formSymmetricDifference with Self (0% overlap)", - "OrderedSet formSymmetricDifference with Array (0% overlap)", - "Set formSymmetricDifference with Self (0% overlap)", - "Set formSymmetricDifference with Array (0% overlap)", - ] - }, - { - "kind": "chart", - "title": "formSymmetricDifference (25% overlap)", - "tasks": [ - "OrderedSet formSymmetricDifference with Self (25% overlap)", - "OrderedSet formSymmetricDifference with Array (25% overlap)", - "Set formSymmetricDifference with Self (25% overlap)", - "Set formSymmetricDifference with Array (25% overlap)", - ] - }, - { - "kind": "chart", - "title": "formSymmetricDifference (50% overlap)", - "tasks": [ - "OrderedSet formSymmetricDifference with Self (50% overlap)", - "OrderedSet formSymmetricDifference with Array (50% overlap)", - "Set formSymmetricDifference with Self (50% overlap)", - "Set formSymmetricDifference with Array (50% overlap)", - ] - }, - { - "kind": "chart", - "title": "formSymmetricDifference (75% overlap)", - "tasks": [ - "OrderedSet formSymmetricDifference with Self (75% overlap)", - "OrderedSet formSymmetricDifference with Array (75% overlap)", - "Set formSymmetricDifference with Self (75% overlap)", - "Set formSymmetricDifference with Array (75% overlap)", - ] - }, - { - "kind": "chart", - "title": "formSymmetricDifference (100% overlap)", - "tasks": [ - "OrderedSet formSymmetricDifference with Self (100% overlap)", - "OrderedSet formSymmetricDifference with Array (100% overlap)", - "Set formSymmetricDifference with Self (100% overlap)", - "Set formSymmetricDifference with Array (100% overlap)", - ] - }, - ] - }, - { - "kind": "variants", - "charts": [ - { - "kind": "chart", - "title": "subtracting (0% overlap)", - "tasks": [ - "OrderedSet subtracting Self (0% overlap)", - "OrderedSet subtracting Array (0% overlap)", - "Set subtracting Self (0% overlap)", - "Set subtracting Array (0% overlap)", - ] - }, - { - "kind": "chart", - "title": "subtracting (25% overlap)", - "tasks": [ - "OrderedSet subtracting Self (25% overlap)", - "OrderedSet subtracting Array (25% overlap)", - "Set subtracting Self (25% overlap)", - "Set subtracting Array (25% overlap)", - ] - }, - { - "kind": "chart", - "title": "subtracting (50% overlap)", - "tasks": [ - "OrderedSet subtracting Self (50% overlap)", - "OrderedSet subtracting Array (50% overlap)", - "Set subtracting Self (50% overlap)", - "Set subtracting Array (50% overlap)", - ] - }, - { - "kind": "chart", - "title": "subtracting (75% overlap)", - "tasks": [ - "OrderedSet subtracting Self (75% overlap)", - "OrderedSet subtracting Array (75% overlap)", - "Set subtracting Self (75% overlap)", - "Set subtracting Array (75% overlap)", - ] - }, - { - "kind": "chart", - "title": "subtracting (100% overlap)", - "tasks": [ - "OrderedSet subtracting Self (100% overlap)", - "OrderedSet subtracting Array (100% overlap)", - "Set subtracting Self (100% overlap)", - "Set subtracting Array (100% overlap)", - ] - }, - ] - }, - { - "kind": "variants", - "charts": [ - { - "kind": "chart", - "title": "subtract (0% overlap)", - "tasks": [ - "OrderedSet subtract Self (0% overlap)", - "OrderedSet subtract Array (0% overlap)", - "Set subtract Self (0% overlap)", - "Set subtract Array (0% overlap)", - ] - }, - { - "kind": "chart", - "title": "subtract (25% overlap)", - "tasks": [ - "OrderedSet subtract Self (25% overlap)", - "OrderedSet subtract Array (25% overlap)", - "Set subtract Self (25% overlap)", - "Set subtract Array (25% overlap)", - ] - }, - { - "kind": "chart", - "title": "subtract (50% overlap)", - "tasks": [ - "OrderedSet subtract Self (50% overlap)", - "OrderedSet subtract Array (50% overlap)", - "Set subtract Self (50% overlap)", - "Set subtract Array (50% overlap)", - ] - }, - { - "kind": "chart", - "title": "subtract (75% overlap)", - "tasks": [ - "OrderedSet subtract Self (75% overlap)", - "OrderedSet subtract Array (75% overlap)", - "Set subtract Self (75% overlap)", - "Set subtract Array (75% overlap)", - ] - }, - { - "kind": "chart", - "title": "subtract (100% overlap)", - "tasks": [ - "OrderedSet subtract Self (100% overlap)", - "OrderedSet subtract Array (100% overlap)", - "Set subtract Self (100% overlap)", - "Set subtract Array (100% overlap)", - ] - }, - ] - }, - ] - }, - { - "kind": "group", - "title": "OrderedDictionary vs Dictionary", - "contents": [ - { - "kind": "chart", - "title": "initializers", - "tasks": [ - "Dictionary init(uniqueKeysWithValues:)", - "OrderedDictionary init(uniqueKeysWithValues:)", - "OrderedDictionary init(uncheckedUniqueKeysWithValues:)", - "OrderedDictionary init(uncheckedUniqueKeys:values:)" - ] - }, - { - "kind": "variants", - "charts": [ - { - "kind": "chart", - "title": "iteration", - "tasks": [ - "Dictionary sequential iteration", - "OrderedDictionary sequential iteration", - ] - }, - { - "kind": "chart", - "title": "Keys iteration", - "tasks": [ - "Dictionary.Keys sequential iteration", - "OrderedDictionary.Keys sequential iteration", - ] - }, - { - "kind": "chart", - "title": "Values iteration", - "tasks": [ - "Dictionary.Values sequential iteration", - "OrderedDictionary.Values sequential iteration", - ] - }, - { - "kind": "chart", - "title": "Values iteration", - "tasks": [ - "Dictionary.Values sequential iteration", - "OrderedDictionary.Values sequential iteration", - ] - }, - ] - }, - { - "kind": "chart", - "title": "index(forKey:)", - "tasks": [ - "Dictionary successful index(forKey:)", - "Dictionary unsuccessful index(forKey:)", - "OrderedDictionary successful index(forKey:)", - "OrderedDictionary unsuccessful index(forKey:)", - ] - }, - { - "kind": "variants", - "charts": [ - { - "kind": "chart", - "title": "subscript lookups", - "tasks": [ - "Dictionary subscript, successful lookups", - "Dictionary subscript, unsuccessful lookups", - "OrderedDictionary subscript, successful lookups", - "OrderedDictionary subscript, unsuccessful lookups", - ] - }, - { - "kind": "chart", - "title": "subscript setter, simple", - "tasks": [ - "Dictionary subscript, noop setter", - "Dictionary subscript, set existing", - "Dictionary subscript, _modify", - "OrderedDictionary subscript, noop setter", - "OrderedDictionary subscript, set existing", - "OrderedDictionary subscript, _modify", - ] - }, - { - "kind": "chart", - "title": "subscript insert/append", - "tasks": [ - "Dictionary subscript, insert", - "Dictionary subscript, insert, reserving capacity", - "OrderedDictionary subscript, append", - "OrderedDictionary subscript, append, reserving capacity", - ] - }, - { - "kind": "chart", - "title": "subscript remove", - "tasks": [ - "Dictionary subscript, remove existing", - "Dictionary subscript, remove missing", - "OrderedDictionary subscript, remove existing", - "OrderedDictionary subscript, remove missing", - ] - }, - ] - }, - { - "kind": "variants", - "charts": [ - { - "kind": "chart", - "title": "defaulted subscript lookups", - "tasks": [ - "Dictionary defaulted subscript, successful lookups", - "Dictionary defaulted subscript, unsuccessful lookups", - "OrderedDictionary defaulted subscript, successful lookups", - "OrderedDictionary defaulted subscript, unsuccessful lookups", - ] - }, - { - "kind": "chart", - "title": "defaulted subscript mutations", - "tasks": [ - "Dictionary defaulted subscript, _modify existing", - "Dictionary defaulted subscript, _modify missing", - "OrderedDictionary defaulted subscript, _modify existing", - "OrderedDictionary defaulted subscript, _modify missing", - ] - }, - ] - }, - { - "kind": "chart", - "title": "updateValue(_:forKey:)", - "tasks": [ - "Dictionary updateValue(_:forKey:), existing", - "Dictionary updateValue(_:forKey:), insert", - "OrderedDictionary updateValue(_:forKey:), existing", - "OrderedDictionary updateValue(_:forKey:), append", - ] - }, - ] - }, - ] - }, - { - "kind": "group", - "title": "Against containers in the C++ Standard Template Library", - "directory": "stl", - "contents": [ - { - "kind": "chart", - "title": "Hashing", - "tasks": [ - "std::hash", - "custom_intptr_hash (using Swift.Hasher)", - "Hasher.combine on a single buffer of integers", - "Int.hashValue on each value", - ] - }, - { - "kind": "group", - "title": "Array vs std::vector", - "directory": "Array + vector", - "contents": [ - { - "kind": "chart", - "title": "init from buffer of integers", - "tasks": [ - "std::vector constructor from buffer", - "Array init from unsafe buffer", - ] - }, - { - "kind": "chart", - "title": "sequential iteration", - "tasks": [ - "std::vector sequential iteration", - "Array sequential iteration", - ] - }, - { - "kind": "chart", - "title": "random-access offset lookups", - "tasks": [ - "std::vector random-access offset lookups (operator [])", - "std::vector random-access offset lookups (at)", - "Array subscript get, random offsets", - ] - }, - { - "kind": "chart", - "title": "append individual integers", - "tasks": [ - "std::vector push_back", - "std::vector push_back, reserving capacity", - "Array append", - "Array append, reserving capacity", - ] - }, - { - "kind": "chart", - "title": "prepend individual integers", - "tasks": [ - "std::vector insert at front", - "std::vector insert at front, reserving capacity", - "Array prepend", - "Array prepend, reserving capacity", - ] - }, - { - "kind": "chart", - "title": "random insertions", - "tasks": [ - "std::vector random insertions", - "Array random insertions", - ] - }, - { - "kind": "chart", - "title": "removeFirst", - "tasks": [ - "std::vector erase first", - "Array removeFirst", - ] - }, - { - "kind": "chart", - "title": "removeLast", - "tasks": [ - "std::vector pop_back", - "Array removeLast", - ] - }, - { - "kind": "chart", - "title": "random removals", - "tasks": [ - "std::vector random removals", - "Array random removals", - ] - }, - { - "kind": "chart", - "title": "sort", - "tasks": [ - "std::vector sort", - "Array sort", - ] - }, - ] - }, - { - "kind": "group", - "title": "Deque vs std::deque", - "directory": "Deque + deque", - "contents": [ - { - "kind": "chart", - "title": "init from buffer of integers", - "tasks": [ - "std::deque constructor from buffer", - "Deque init from unsafe buffer", - ] - }, - { - "kind": "chart", - "title": "sequential iteration", - "tasks": [ - "std::deque sequential iteration", - "Deque sequential iteration (contiguous)", - "Deque sequential iteration (discontiguous)", - ] - }, - { - "kind": "chart", - "title": "random-access offset lookups", - "tasks": [ - "std::deque random-access offset lookups (operator [])", - "std::deque at, random offsets", - "Deque subscript get, random offsets (contiguous)", - "Deque subscript get, random offsets (discontiguous)", - ] - }, - { - "kind": "chart", - "title": "append individual integers", - "tasks": [ - "std::deque push_back", - "Deque append", - "Deque append, reserving capacity", - ] - }, - { - "kind": "chart", - "title": "prepend individual integers", - "tasks": [ - "std::deque push_front", - "Deque prepend", - "Deque prepend, reserving capacity", - ] - }, - { - "kind": "chart", - "title": "random insertions", - "tasks": [ - "std::deque random insertions", - "Deque random insertions", - ] - }, - { - "kind": "chart", - "title": "removeFirst", - "tasks": [ - "std::deque pop_front", - "Deque removeFirst (contiguous)", - "Deque removeFirst (discontiguous)", - ] - }, - { - "kind": "chart", - "title": "removeLast", - "tasks": [ - "std::deque pop_back", - "Deque removeLast (contiguous)", - "Deque removeLast (discontiguous)", - ] - }, - { - "kind": "chart", - "title": "random removals", - "tasks": [ - "std::deque random removals", - "Deque random removals (contiguous)", - "Deque random removals (discontiguous)", - ] - }, - { - "kind": "chart", - "title": "sort", - "tasks": [ - "std::deque sort", - "Deque sort (contiguous)", - "Deque sort (discontiguous)", - ] - }, - ] - }, - { - "kind": "group", - "title": "Set vs std::unordered_set", - "directory": "Set + unordered_set", - "contents": [ - { - "kind": "chart", - "title": "init from integer range", - "tasks": [ - "std::unordered_set insert from integer range", - "Set init from range", - ] - }, - { - "kind": "chart", - "title": "init from buffer of integers", - "tasks": [ - "std::unordered_set constructor from buffer", - "Set init from unsafe buffer", - ] - }, - { - "kind": "chart", - "title": "sequential iteration", - "tasks": [ - "std::unordered_set sequential iteration", - "Set sequential iteration", - ] - }, - { - "kind": "chart", - "title": "successful lookups", - "tasks": [ - "std::unordered_set successful find", - "Set successful contains", - ] - }, - { - "kind": "chart", - "title": "unsuccessful lookups", - "tasks": [ - "std::unordered_set unsuccessful find", - "Set unsuccessful contains", - ] - }, - { - "kind": "chart", - "title": "random insertions", - "tasks": [ - "std::unordered_set insert", - "std::unordered_set insert, reserving capacity", - "Set insert", - "Set insert, reserving capacity", - ] - }, - { - "kind": "chart", - "title": "random removals", - "tasks": [ - "std::unordered_set erase", - "Set remove", - ] - }, - ] - }, - { - "kind": "group", - "title": "OrderedSet vs std::unordered_set", - "directory": "OrderedSet + unordered_set", - "contents": [ - { - "kind": "chart", - "title": "init from integer range", - "tasks": [ - "std::unordered_set insert from integer range", - "Set init from range", - "OrderedSet init from range", - ] - }, - { - "kind": "chart", - "title": "init from buffer of integers", - "tasks": [ - "std::unordered_set constructor from buffer", - "Set init from unsafe buffer", - "OrderedSet init from unsafe buffer", - ] - }, - { - "kind": "chart", - "title": "sequential iteration", - "tasks": [ - "std::unordered_set sequential iteration", - "Set sequential iteration", - "OrderedSet sequential iteration", - ] - }, - { - "kind": "chart", - "title": "successful lookups", - "tasks": [ - "std::unordered_set successful find", - "OrderedSet successful contains", - "Set successful contains", - ] - }, - { - "kind": "chart", - "title": "unsuccessful lookups", - "tasks": [ - "std::unordered_set unsuccessful find", - "OrderedSet unsuccessful contains", - "Set unsuccessful contains", - ] - }, - { - "kind": "chart", - "title": "insertions", - "tasks": [ - "std::unordered_set insert", - "Set insert", - "OrderedSet append", - ] - }, - { - "kind": "chart", - "title": "insertions, reserving capacity", - "tasks": [ - "std::unordered_set insert, reserving capacity", - "Set insert, reserving capacity", - "OrderedSet append, reserving capacity", - ] - }, - { - "kind": "chart", - "title": "random removals", - "tasks": [ - "std::unordered_set erase", - "Set remove", - "OrderedSet remove", - ] - }, - ] - }, - { - "kind": "group", - "title": "OrderedDictionary vs std::unordered_map", - "directory": "OrderedDictionary + unordered_map", - "contents": [ - { - "kind": "chart", - "title": "initializers", - "tasks": [ - "std::unordered_map insert", - "Dictionary init(uniqueKeysWithValues:)", - "OrderedDictionary init(uniqueKeysWithValues:)", - "OrderedDictionary init(uncheckedUniqueKeysWithValues:)", - "OrderedDictionary init(uncheckedUniqueKeys:values:)", - ] - }, - { - "kind": "chart", - "title": "iteration", - "tasks": [ - "std::unordered_map sequential iteration", - "Dictionary sequential iteration", - "OrderedDictionary sequential iteration", - ] - }, - { - "kind": "chart", - "title": "successful find index", - "tasks": [ - "std::unordered_map successful find", - "OrderedDictionary successful index(forKey:)", - "Dictionary successful index(forKey:)", - "Int.hashValue on each value", - ] - }, - { - "kind": "chart", - "title": "unsuccessful find index", - "tasks": [ - "std::unordered_map unsuccessful find", - "OrderedDictionary unsuccessful index(forKey:)", - "Dictionary unsuccessful index(forKey:)", - "Int.hashValue on each value", - ] - }, - { - "kind": "chart", - "title": "defaulted subscript, existing key", - "tasks": [ - "std::unordered_map subscript, existing key", - "OrderedDictionary defaulted subscript, _modify existing", - "Dictionary defaulted subscript, _modify existing", - "Int.hashValue on each value", - ] - }, - { - "kind": "chart", - "title": "defaulted subscript, new key", - "tasks": [ - "std::unordered_map subscript, new key", - "OrderedDictionary defaulted subscript, _modify missing", - "Dictionary defaulted subscript, _modify missing", - "Int.hashValue on each value", - ] - }, - { - "kind": "chart", - "title": "insert", - "tasks": [ - "std::unordered_map insert", - "OrderedDictionary subscript, append", - "Dictionary subscript, insert", - "Int.hashValue on each value", - ] - }, - { - "kind": "chart", - "title": "insert, reserving capacity", - "tasks": [ - "std::unordered_map insert, reserving capacity", - "OrderedDictionary subscript, append, reserving capacity", - "Dictionary subscript, insert, reserving capacity", - "Int.hashValue on each value", - ] - }, - { - "kind": "chart", - "title": "removing existing elements", - "tasks": [ - "std::unordered_map erase existing", - "OrderedDictionary subscript, remove existing", - "Dictionary subscript, remove existing", - ] - }, - { - "kind": "chart", - "title": "removing missing elements", - "tasks": [ - "std::unordered_map erase missing", - "OrderedDictionary subscript, remove missing", - "Dictionary subscript, remove missing", - ] - }, - ] - }, - ] - }, - ] -} diff --git a/Benchmarks/Libraries/Array.json b/Benchmarks/Libraries/Array.json new file mode 100644 index 000000000..429d6b5f3 --- /dev/null +++ b/Benchmarks/Libraries/Array.json @@ -0,0 +1,140 @@ +{ + "kind": "group", + "title": "Array Benchmarks", + "directory": "Array", + "contents": [ + { + "kind": "group", + "title": "Operations", + "directory": "operations", + "contents": [ + { + "kind": "chart", + "title": "operations", + "tasks": [ + "Array init from unsafe buffer", + "Array sequential iteration", + "Array subscript get, random offsets", + "Array append", + "Array append, reserving capacity", + "Array prepend", + "Array prepend, reserving capacity", + "Array removeFirst", + "Array removeLast", + "Array sort" + ] + }, + { + "kind": "chart", + "title": "access", + "tasks": [ + "Array sequential iteration", + "Array subscript get, random offsets" + ] + }, + { + "kind": "chart", + "title": "mutate", + "tasks": [ + "Array mutate through subscript", + "Array random swaps", + "Array partitioning around middle", + "Array sort" + ] + } + ] + }, + { + "kind": "group", + "title": "Array vs std::vector", + "directory": "versus STL vector", + "contents": [ + { + "kind": "chart", + "title": "init from buffer of integers", + "tasks": [ + "std::vector constructor from buffer", + "Array init from unsafe buffer" + ] + }, + { + "kind": "chart", + "title": "sequential iteration", + "tasks": [ + "std::vector sequential iteration", + "Array sequential iteration" + ] + }, + { + "kind": "chart", + "title": "random-access offset lookups", + "tasks": [ + "std::vector random-access offset lookups (operator [])", + "std::vector random-access offset lookups (at)", + "Array subscript get, random offsets" + ] + }, + { + "kind": "chart", + "title": "append individual integers", + "tasks": [ + "std::vector push_back", + "std::vector push_back, reserving capacity", + "Array append", + "Array append, reserving capacity" + ] + }, + { + "kind": "chart", + "title": "prepend individual integers", + "tasks": [ + "std::vector insert at front", + "std::vector insert at front, reserving capacity", + "Array prepend", + "Array prepend, reserving capacity" + ] + }, + { + "kind": "chart", + "title": "random insertions", + "tasks": [ + "std::vector random insertions", + "Array random insertions" + ] + }, + { + "kind": "chart", + "title": "removeFirst", + "tasks": [ + "std::vector erase first", + "Array removeFirst" + ] + }, + { + "kind": "chart", + "title": "removeLast", + "tasks": [ + "std::vector pop_back", + "Array removeLast" + ] + }, + { + "kind": "chart", + "title": "random removals", + "tasks": [ + "std::vector random removals", + "Array random removals" + ] + }, + { + "kind": "chart", + "title": "sort", + "tasks": [ + "std::vector sort", + "Array sort" + ] + } + ] + } + ] +} diff --git a/Benchmarks/Libraries/BitSet.json b/Benchmarks/Libraries/BitSet.json new file mode 100644 index 000000000..3d9e0a528 --- /dev/null +++ b/Benchmarks/Libraries/BitSet.json @@ -0,0 +1,78 @@ +{ + "kind": "group", + "title": "BitSet Benchmarks", + "directory": "BitSet", + "contents": [ + { + "kind": "group", + "title": "BitSet Operations", + "contents": [ + ] + }, + { + "kind": "group", + "title": "BitArray Operations", + "contents": [ + ] + }, + { + "kind": "group", + "title": "Comparisons against reference implementations", + "directory": "versus", + "contents": [ + { + "kind": "chart", + "title": "initializers", + "tasks": [ + "std::vector create from integer buffer (subscript)", + "std::vector create from integer buffer (at)", + "CFBitVector create from integer buffer" + ] + }, + { + "kind": "chart", + "title": "sequential iteration", + "tasks": [ + "std::vector const_iterator", + "CFBitVectorGetBitAtIndex (sequential iteration)" + ] + }, + { + "kind": "chart", + "title": "random-access lookups", + "tasks": [ + "std::vector random-access offset lookups (subscript)", + "std::vector random-access offset lookups (at)", + "CFBitVectorGetBitAtIndex (random-access lookups)" + ] + }, + { + "kind": "chart", + "title": "set random bits to true", + "tasks": [ + "std::vector set bits to true (subscript)", + "std::vector set bits to true (at)", + "CFBitVectorSetBitAtIndex (random-access set)" + ] + }, + { + "kind": "chart", + "title": "find true bits", + "tasks": [ + "std::vector find true bits", + "CFBitVectorGetFirstIndexOfBit" + ] + }, + { + "kind": "chart", + "title": "count true bits", + "tasks": [ + "std::vector count true bits", + "CFBitVectorGetCountOfBit" + ] + }, + ] + } + ] + +} diff --git a/Benchmarks/Libraries/Deque.json b/Benchmarks/Libraries/Deque.json new file mode 100644 index 000000000..1d1adb1e0 --- /dev/null +++ b/Benchmarks/Libraries/Deque.json @@ -0,0 +1,309 @@ +{ + "kind": "group", + "title": "Deque Benchmarks", + "directory": "Deque", + "contents": [ + { + "kind": "group", + "title": "Operations", + "directory": "operations", + "contents": [ + { + "kind": "chart", + "title": "operations", + "tasks": [ + "Deque init from unsafe buffer", + "Deque sequential iteration (contiguous, iterator)", + "Deque subscript get, random offsets (contiguous)", + "Deque append", + "Deque append, reserving capacity", + "Deque prepend", + "Deque prepend, reserving capacity", + "Deque removeFirst (contiguous)", + "Deque removeLast (contiguous)", + "Deque sort (contiguous)", + "Deque sort (discontiguous)" + ] + }, + { + "kind": "chart", + "title": "iteration", + "tasks": [ + "Deque sequential iteration (contiguous, iterator)", + "Deque sequential iteration (discontiguous, iterator)", + "Deque sequential iteration (contiguous, indices)", + "Deque sequential iteration (discontiguous, indices)" + ] + }, + { + "kind": "chart", + "title": "access", + "tasks": [ + "Deque subscript get, random offsets (contiguous)", + "Deque subscript get, random offsets (discontiguous)", + "Deque mutate through subscript (contiguous)", + "Deque mutate through subscript (discontiguous)" + ] + }, + { + "kind": "chart", + "title": "mutate", + "tasks": [ + "Deque mutate through subscript (contiguous)", + "Deque mutate through subscript (discontiguous)", + "Deque random swaps (contiguous)", + "Deque random swaps (discontiguous)", + "Deque partitioning around middle (contiguous)", + "Deque partitioning around middle (discontiguous)", + "Deque sort (contiguous)", + "Deque sort (discontiguous)" + ] + }, + { + "kind": "chart", + "title": "push", + "tasks": [ + "Deque append", + "Deque append, reserving capacity", + "Deque prepend", + "Deque prepend, reserving capacity" + ] + }, + { + "kind": "chart", + "title": "pop", + "tasks": [ + "Deque removeFirst (contiguous)", + "Deque removeFirst (discontiguous)", + "Deque removeLast (contiguous)", + "Deque removeLast (discontiguous)" + ] + } + ] + }, + { + "kind": "group", + "title": "Deque vs Array", + "directory": "versus Array", + "contents": [ + { + "kind": "chart", + "title": "init from buffer of integers", + "tasks": [ + "Deque init from unsafe buffer", + "Array init from unsafe buffer" + ] + }, + { + "kind": "chart", + "title": "sequential iteration", + "tasks": [ + "Deque sequential iteration (contiguous, iterator)", + "Deque sequential iteration (contiguous, indices)", + "Deque sequential iteration (discontiguous, iterator)", + "Deque sequential iteration (discontiguous, indices)", + "Array sequential iteration" + ] + }, + { + "kind": "chart", + "title": "random-access offset lookups", + "tasks": [ + "Deque subscript get, random offsets (contiguous)", + "Deque subscript get, random offsets (discontiguous)", + "Array subscript get, random offsets" + ] + }, + { + "kind": "chart", + "title": "mutate through subscript", + "tasks": [ + "Deque mutate through subscript (contiguous)", + "Deque mutate through subscript (discontiguous)", + "Array mutate through subscript" + ] + }, + { + "kind": "chart", + "title": "random swaps", + "tasks": [ + "Deque random swaps (contiguous)", + "Deque random swaps (discontiguous)", + "Array random swaps" + ] + }, + { + "kind": "chart", + "title": "partitioning around middle", + "tasks": [ + "Deque partitioning around middle (contiguous)", + "Deque partitioning around middle (discontiguous)", + "Array partitioning around middle" + ] + }, + { + "kind": "chart", + "title": "sort", + "tasks": [ + "Deque sort (contiguous)", + "Deque sort (discontiguous)", + "Array sort" + ] + }, + { + "kind": "chart", + "title": "append individual integers", + "tasks": [ + "Deque append", + "Deque append, reserving capacity", + "Array append", + "Array append, reserving capacity" + ] + }, + { + "kind": "chart", + "title": "prepend individual integers", + "tasks": [ + "Deque prepend", + "Deque prepend, reserving capacity", + "Array prepend", + "Array prepend, reserving capacity" + ] + }, + { + "kind": "chart", + "title": "random insertions", + "tasks": [ + "Deque random insertions", + "Array random insertions" + ] + }, + { + "kind": "chart", + "title": "removeFirst", + "tasks": [ + "Deque removeFirst (contiguous)", + "Deque removeFirst (discontiguous)", + "Array removeFirst" + ] + }, + { + "kind": "chart", + "title": "removeLast", + "tasks": [ + "Deque removeLast (contiguous)", + "Deque removeLast (discontiguous)", + "Array removeLast" + ] + }, + { + "kind": "chart", + "title": "random removals", + "tasks": [ + "Deque random removals (contiguous)", + "Deque random removals (discontiguous)", + "Array random removals" + ] + } + ] + }, + { + "kind": "group", + "title": "Deque vs std::deque", + "directory": "versus STL deque", + "contents": [ + { + "kind": "chart", + "title": "init from buffer of integers", + "tasks": [ + "std::deque constructor from buffer", + "Deque init from unsafe buffer" + ] + }, + { + "kind": "chart", + "title": "sequential iteration", + "tasks": [ + "std::deque sequential iteration", + "Deque sequential iteration (contiguous, iterator)", + "Deque sequential iteration (contiguous, indices)", + "Deque sequential iteration (discontiguous, iterator)", + "Deque sequential iteration (discontiguous, indices)" + ] + }, + { + "kind": "chart", + "title": "random-access offset lookups", + "tasks": [ + "std::deque random-access offset lookups (operator [])", + "std::deque at, random offsets", + "Deque subscript get, random offsets (contiguous)", + "Deque subscript get, random offsets (discontiguous)" + ] + }, + { + "kind": "chart", + "title": "append individual integers", + "tasks": [ + "std::deque push_back", + "Deque append", + "Deque append, reserving capacity" + ] + }, + { + "kind": "chart", + "title": "prepend individual integers", + "tasks": [ + "std::deque push_front", + "Deque prepend", + "Deque prepend, reserving capacity" + ] + }, + { + "kind": "chart", + "title": "random insertions", + "tasks": [ + "std::deque random insertions", + "Deque random insertions" + ] + }, + { + "kind": "chart", + "title": "removeFirst", + "tasks": [ + "std::deque pop_front", + "Deque removeFirst (contiguous)", + "Deque removeFirst (discontiguous)" + ] + }, + { + "kind": "chart", + "title": "removeLast", + "tasks": [ + "std::deque pop_back", + "Deque removeLast (contiguous)", + "Deque removeLast (discontiguous)" + ] + }, + { + "kind": "chart", + "title": "random removals", + "tasks": [ + "std::deque random removals", + "Deque random removals (contiguous)", + "Deque random removals (discontiguous)" + ] + }, + { + "kind": "chart", + "title": "sort", + "tasks": [ + "std::deque sort", + "Deque sort (contiguous)", + "Deque sort (discontiguous)" + ] + } + ] + } + ] +} diff --git a/Benchmarks/Libraries/Dictionary.json b/Benchmarks/Libraries/Dictionary.json new file mode 100644 index 000000000..4c9f108ca --- /dev/null +++ b/Benchmarks/Libraries/Dictionary.json @@ -0,0 +1,92 @@ +{ + "kind": "group", + "title": "Dictionary Benchmarks", + "directory": "Dictionary", + "contents": [ + { + "kind": "group", + "title": "Operations", + "directory": "operations", + "contents": [ + { + "kind": "chart", + "title": "operations", + "tasks": [ + "Dictionary init(uniqueKeysWithValues:)", + "Dictionary sequential iteration", + "Dictionary subscript, successful lookups", + "Dictionary subscript, insert, unique", + "Dictionary subscript, remove existing, unique" + ] + }, + { + "kind": "chart", + "title": "iteration", + "tasks": [ + "Dictionary sequential iteration", + "Dictionary.Keys sequential iteration", + "Dictionary.Values sequential iteration", + "Dictionary sequential iteration, indices" + ] + }, + { + "kind": "chart", + "title": "lookups", + "tasks": [ + "Dictionary subscript, successful lookups", + "Dictionary subscript, unsuccessful lookups", + "Dictionary defaulted subscript, successful lookups", + "Dictionary defaulted subscript, unsuccessful lookups", + "Dictionary successful index(forKey:)", + "Dictionary unsuccessful index(forKey:)" + ] + }, + { + "kind": "chart", + "title": "subscript", + "tasks": [ + "Dictionary subscript, successful lookups", + "Dictionary subscript, unsuccessful lookups", + "Dictionary subscript, noop setter", + "Dictionary subscript, set existing", + "Dictionary subscript, _modify", + "Dictionary subscript, insert, unique", + "Dictionary subscript, insert, reserving capacity", + "Dictionary subscript, remove existing, unique", + "Dictionary subscript, remove missing" + ] + }, + { + "kind": "chart", + "title": "defaulted subscript", + "tasks": [ + "Dictionary defaulted subscript, successful lookups", + "Dictionary defaulted subscript, unsuccessful lookups", + "Dictionary defaulted subscript, _modify existing", + "Dictionary defaulted subscript, _modify missing" + ] + }, + { + "kind": "chart", + "title": "mutations", + "tasks": [ + "Dictionary updateValue(_:forKey:), existing", + "Dictionary subscript, set existing", + "Dictionary subscript, _modify", + "Dictionary defaulted subscript, _modify existing" + ] + }, + { + "kind": "chart", + "title": "removals", + "tasks": [ + "Dictionary subscript, remove existing, unique", + "Dictionary subscript, remove missing", + "Dictionary random removals (existing keys)", + "Dictionary random removals (missing keys)" + ] + } + ] + } + ] +} diff --git a/Benchmarks/Libraries/Heap.json b/Benchmarks/Libraries/Heap.json new file mode 100644 index 000000000..e3fd304ee --- /dev/null +++ b/Benchmarks/Libraries/Heap.json @@ -0,0 +1,83 @@ +{ + "kind": "group", + "title": "Heap Benchmarks", + "directory": "Heap", + "contents": [ + { + "kind": "group", + "title": "Heap Operations", + "contents": [ + { + "kind": "chart", + "title": "operations", + "tasks": [ + "Heap init from range", + "Heap insert", + "Heap insert(contentsOf:)", + "Heap popMax", + "Heap popMin" + ] + }, + { + "kind": "chart", + "title": "initializers", + "tasks": [ + "Heap init from range", + "Heap init from buffer" + ] + }, + { + "kind": "chart", + "title": "insert", + "tasks": [ + "Heap insert", + "Heap insert(contentsOf:)" + ] + }, + { + "kind": "chart", + "title": "remove", + "tasks": [ + "Heap popMax", + "Heap popMin" + ] + } + ] + }, + { + "kind": "group", + "title": "Heap vs CFBinaryHeap and std::priority_queue", + "directory": "versus", + "contents": [ + { + "kind": "chart", + "title": "initializers", + "tasks": [ + "std::priority_queue construct from buffer", + "Heap init from buffer", + ] + }, + { + "kind": "chart", + "title": "insert", + "tasks": [ + "std::priority_queue push", + "CFBinaryHeapAddValue", + "CFBinaryHeapAddValue, reserving capacity", + "Heap insert" + ] + }, + { + "kind": "chart", + "title": "pop", + "tasks": [ + "std::priority_queue pop", + "CFBinaryHeapRemoveMinimumValue", + "Heap popMin", + "Heap popMax" + ] + }, + ] + } + ] +} diff --git a/Benchmarks/Libraries/OrderedDictionary.json b/Benchmarks/Libraries/OrderedDictionary.json new file mode 100644 index 000000000..ef47f48ba --- /dev/null +++ b/Benchmarks/Libraries/OrderedDictionary.json @@ -0,0 +1,357 @@ +{ + "kind": "group", + "title": "OrderedDictionary", + "directory": "OrderedDictionary", + "contents": [ + { + "kind": "group", + "title": "Operations", + "directory": "operations", + "contents": [ + { + "kind": "chart", + "title": "operations", + "tasks": [ + "OrderedDictionary init(uniqueKeysWithValues:)", + "OrderedDictionary sequential iteration", + "OrderedDictionary subscript, successful lookups", + "OrderedDictionary subscript, append, unique", + "OrderedDictionary subscript, remove existing, unique", + ] + }, + { + "kind": "chart", + "title": "initializers", + "tasks": [ + "OrderedDictionary init(uniqueKeysWithValues:)", + "OrderedDictionary init(uncheckedUniqueKeysWithValues:)", + "OrderedDictionary init(uncheckedUniqueKeys:values:)" + ] + }, + { + "kind": "chart", + "title": "iteration", + "tasks": [ + "OrderedDictionary sequential iteration", + "OrderedDictionary.Keys sequential iteration", + "OrderedDictionary.Values sequential iteration" + ] + }, + { + "kind": "chart", + "title": "lookups", + "tasks": [ + "OrderedDictionary subscript, successful lookups", + "OrderedDictionary subscript, unsuccessful lookups", + "OrderedDictionary defaulted subscript, successful lookups", + "OrderedDictionary defaulted subscript, unsuccessful lookups", + "OrderedDictionary successful index(forKey:)", + "OrderedDictionary unsuccessful index(forKey:)", + ] + }, + { + "kind": "chart", + "title": "subscript", + "tasks": [ + "OrderedDictionary subscript, successful lookups", + "OrderedDictionary subscript, unsuccessful lookups", + "OrderedDictionary subscript, noop setter", + "OrderedDictionary subscript, set existing", + "OrderedDictionary subscript, _modify", + "OrderedDictionary subscript, append, unique", + "OrderedDictionary subscript, append, reserving capacity", + "OrderedDictionary subscript, remove existing, unique", + "OrderedDictionary subscript, remove missing", + ] + }, + { + "kind": "chart", + "title": "defaulted subscript", + "tasks": [ + "OrderedDictionary defaulted subscript, successful lookups", + "OrderedDictionary defaulted subscript, unsuccessful lookups", + "OrderedDictionary defaulted subscript, _modify existing", + "OrderedDictionary defaulted subscript, _modify missing", + ] + }, + { + "kind": "chart", + "title": "mutations", + "tasks": [ + "OrderedDictionary updateValue(_:forKey:), existing", + "OrderedDictionary subscript, set existing", + "OrderedDictionary subscript, _modify", + "OrderedDictionary defaulted subscript, _modify existing", + "OrderedDictionary random swaps", + "OrderedDictionary partitioning around middle", + "OrderedDictionary sort", + ] + }, + { + "kind": "chart", + "title": "removals", + "tasks": [ + "OrderedDictionary subscript, remove existing, unique", + "OrderedDictionary subscript, remove missing", + "OrderedDictionary removeLast", + "OrderedDictionary removeFirst", + "OrderedDictionary random removals (offset-based)", + "OrderedDictionary random removals (existing keys)", + "OrderedDictionary random removals (missing keys)", + ] + } + ] + }, + { + "kind": "group", + "title": "OrderedDictionary vs Dictionary", + "directory": "versus Dictionary", + "contents": [ + { + "kind": "chart", + "title": "initializers", + "tasks": [ + "Dictionary init(uniqueKeysWithValues:)", + "OrderedDictionary init(uniqueKeysWithValues:)", + "OrderedDictionary init(uncheckedUniqueKeysWithValues:)", + "OrderedDictionary init(uncheckedUniqueKeys:values:)" + ] + }, + { + "kind": "variants", + "charts": [ + { + "kind": "chart", + "title": "iteration", + "tasks": [ + "Dictionary sequential iteration", + "OrderedDictionary sequential iteration", + ] + }, + { + "kind": "chart", + "title": "Keys iteration", + "tasks": [ + "Dictionary.Keys sequential iteration", + "OrderedDictionary.Keys sequential iteration", + ] + }, + { + "kind": "chart", + "title": "Values iteration", + "tasks": [ + "Dictionary.Values sequential iteration", + "OrderedDictionary.Values sequential iteration", + ] + }, + { + "kind": "chart", + "title": "Values iteration", + "tasks": [ + "Dictionary.Values sequential iteration", + "OrderedDictionary.Values sequential iteration", + ] + }, + ] + }, + { + "kind": "chart", + "title": "index(forKey:)", + "tasks": [ + "Dictionary successful index(forKey:)", + "Dictionary unsuccessful index(forKey:)", + "OrderedDictionary successful index(forKey:)", + "OrderedDictionary unsuccessful index(forKey:)", + ] + }, + { + "kind": "variants", + "charts": [ + { + "kind": "chart", + "title": "subscript lookups", + "tasks": [ + "Dictionary subscript, successful lookups", + "Dictionary subscript, unsuccessful lookups", + "OrderedDictionary subscript, successful lookups", + "OrderedDictionary subscript, unsuccessful lookups", + ] + }, + { + "kind": "chart", + "title": "subscript setter, simple", + "tasks": [ + "Dictionary subscript, noop setter", + "Dictionary subscript, set existing", + "Dictionary subscript, _modify", + "OrderedDictionary subscript, noop setter", + "OrderedDictionary subscript, set existing", + "OrderedDictionary subscript, _modify", + ] + }, + { + "kind": "chart", + "title": "subscript insert/append", + "tasks": [ + "Dictionary subscript, insert, unique", + "Dictionary subscript, insert, reserving capacity", + "OrderedDictionary subscript, append, unique", + "OrderedDictionary subscript, append, reserving capacity", + ] + }, + { + "kind": "chart", + "title": "subscript remove", + "tasks": [ + "Dictionary subscript, remove existing, unique", + "Dictionary subscript, remove missing", + "OrderedDictionary subscript, remove existing, unique", + "OrderedDictionary subscript, remove missing", + ] + }, + ] + }, + { + "kind": "variants", + "charts": [ + { + "kind": "chart", + "title": "defaulted subscript lookups", + "tasks": [ + "Dictionary defaulted subscript, successful lookups", + "Dictionary defaulted subscript, unsuccessful lookups", + "OrderedDictionary defaulted subscript, successful lookups", + "OrderedDictionary defaulted subscript, unsuccessful lookups", + ] + }, + { + "kind": "chart", + "title": "defaulted subscript mutations", + "tasks": [ + "Dictionary defaulted subscript, _modify existing", + "Dictionary defaulted subscript, _modify missing", + "OrderedDictionary defaulted subscript, _modify existing", + "OrderedDictionary defaulted subscript, _modify missing", + ] + } + ] + }, + { + "kind": "chart", + "title": "updateValue(_:forKey:)", + "tasks": [ + "Dictionary updateValue(_:forKey:), existing", + "Dictionary updateValue(_:forKey:), insert", + "OrderedDictionary updateValue(_:forKey:), existing", + "OrderedDictionary updateValue(_:forKey:), append", + ] + } + ] + }, + { + "kind": "group", + "title": "OrderedDictionary vs std::unordered_map", + "directory": "versus STL unordered_map", + "contents": [ + { + "kind": "chart", + "title": "initializers", + "tasks": [ + "std::unordered_map insert", + "Dictionary init(uniqueKeysWithValues:)", + "OrderedDictionary init(uniqueKeysWithValues:)", + "OrderedDictionary init(uncheckedUniqueKeysWithValues:)", + "OrderedDictionary init(uncheckedUniqueKeys:values:)", + ] + }, + { + "kind": "chart", + "title": "iteration", + "tasks": [ + "std::unordered_map sequential iteration", + "Dictionary sequential iteration", + "OrderedDictionary sequential iteration", + ] + }, + { + "kind": "chart", + "title": "successful find index", + "tasks": [ + "std::unordered_map successful find", + "OrderedDictionary successful index(forKey:)", + "Dictionary successful index(forKey:)", + "Int.hashValue on each value", + ] + }, + { + "kind": "chart", + "title": "unsuccessful find index", + "tasks": [ + "std::unordered_map unsuccessful find", + "OrderedDictionary unsuccessful index(forKey:)", + "Dictionary unsuccessful index(forKey:)", + "Int.hashValue on each value", + ] + }, + { + "kind": "chart", + "title": "defaulted subscript, existing key", + "tasks": [ + "std::unordered_map subscript, existing key", + "OrderedDictionary defaulted subscript, _modify existing", + "Dictionary defaulted subscript, _modify existing", + "Int.hashValue on each value", + ] + }, + { + "kind": "chart", + "title": "defaulted subscript, new key", + "tasks": [ + "std::unordered_map subscript, new key", + "OrderedDictionary defaulted subscript, _modify missing", + "Dictionary defaulted subscript, _modify missing", + "Int.hashValue on each value", + ] + }, + { + "kind": "chart", + "title": "insert", + "tasks": [ + "std::unordered_map insert", + "OrderedDictionary subscript, append, unique", + "Dictionary subscript, insert, unique", + "Int.hashValue on each value", + ] + }, + { + "kind": "chart", + "title": "insert, reserving capacity", + "tasks": [ + "std::unordered_map insert, reserving capacity", + "OrderedDictionary subscript, append, reserving capacity", + "Dictionary subscript, insert, reserving capacity", + "Int.hashValue on each value", + ] + }, + { + "kind": "chart", + "title": "removing existing elements", + "tasks": [ + "std::unordered_map erase existing", + "OrderedDictionary subscript, remove existing, unique", + "Dictionary subscript, remove existing, unique", + ] + }, + { + "kind": "chart", + "title": "removing missing elements", + "tasks": [ + "std::unordered_map erase missing", + "OrderedDictionary subscript, remove missing", + "Dictionary subscript, remove missing", + ] + }, + ] + }, + ] +} diff --git a/Benchmarks/Libraries/OrderedSet.json b/Benchmarks/Libraries/OrderedSet.json new file mode 100644 index 000000000..e52b4a0f6 --- /dev/null +++ b/Benchmarks/Libraries/OrderedSet.json @@ -0,0 +1,841 @@ +{ + "kind": "group", + "title": "OrderedSet", + "directory": "OrderedSet", + "contents": [ + { + "kind": "group", + "title": "Operations", + "directory": "operations", + "contents": [ + { + "kind": "chart", + "title": "operations", + "tasks": [ + "OrderedSet init from range", + "OrderedSet init from unsafe buffer", + "OrderedSet sequential iteration", + "OrderedSet successful contains", + "OrderedSet unsuccessful contains", + "OrderedSet append", + "OrderedSet append, reserving capacity", + "OrderedSet remove", + ] + }, + { + "kind": "chart", + "title": "initializers", + "tasks": [ + "OrderedSet init from range", + "OrderedSet init from unsafe buffer", + "OrderedSet init(uncheckedUniqueElements:) from range" + ] + }, + { + "kind": "chart", + "title": "mutations", + "tasks": [ + "OrderedSet random swaps", + "OrderedSet partitioning around middle", + "OrderedSet sort", + ] + }, + { + "kind": "chart", + "title": "range replaceable", + "tasks": [ + "OrderedSet append", + "OrderedSet append, reserving capacity", + "OrderedSet prepend", + "OrderedSet prepend, reserving capacity", + "OrderedSet random insertions, reserving capacity", + "OrderedSet remove", + "OrderedSet removeLast", + "OrderedSet removeFirst", + ] + }, + { + "kind": "variants", + "charts": [ + { + "kind": "chart", + "title": "union with Self", + "tasks": [ + "OrderedSet union with Self (0% overlap)", + "OrderedSet union with Self (25% overlap)", + "OrderedSet union with Self (50% overlap)", + "OrderedSet union with Self (75% overlap)", + "OrderedSet union with Self (100% overlap)", + ] + }, + { + "kind": "chart", + "title": "union with Array", + "tasks": [ + "OrderedSet union with Array (0% overlap)", + "OrderedSet union with Array (25% overlap)", + "OrderedSet union with Array (50% overlap)", + "OrderedSet union with Array (75% overlap)", + "OrderedSet union with Array (100% overlap)", + ] + }, + { + "kind": "chart", + "title": "formUnion with Self", + "tasks": [ + "OrderedSet formUnion with Self (0% overlap)", + "OrderedSet formUnion with Self (25% overlap)", + "OrderedSet formUnion with Self (50% overlap)", + "OrderedSet formUnion with Self (75% overlap)", + "OrderedSet formUnion with Self (100% overlap)", + ] + }, + { + "kind": "chart", + "title": "formUnion with Array", + "tasks": [ + "OrderedSet formUnion with Array (0% overlap)", + "OrderedSet formUnion with Array (25% overlap)", + "OrderedSet formUnion with Array (50% overlap)", + "OrderedSet formUnion with Array (75% overlap)", + "OrderedSet formUnion with Array (100% overlap)", + ] + }, + ] + }, + { + "kind": "variants", + "charts": [ + { + "kind": "chart", + "title": "intersection with Self", + "tasks": [ + "OrderedSet intersection with Self (0% overlap)", + "OrderedSet intersection with Self (25% overlap)", + "OrderedSet intersection with Self (50% overlap)", + "OrderedSet intersection with Self (75% overlap)", + "OrderedSet intersection with Self (100% overlap)", + ] + }, + { + "kind": "chart", + "title": "intersection with Array", + "tasks": [ + "OrderedSet intersection with Array (0% overlap)", + "OrderedSet intersection with Array (25% overlap)", + "OrderedSet intersection with Array (50% overlap)", + "OrderedSet intersection with Array (75% overlap)", + "OrderedSet intersection with Array (100% overlap)", + ] + }, + { + "kind": "chart", + "title": "formIntersection with Self", + "tasks": [ + "OrderedSet formIntersection with Self (0% overlap)", + "OrderedSet formIntersection with Self (25% overlap)", + "OrderedSet formIntersection with Self (50% overlap)", + "OrderedSet formIntersection with Self (75% overlap)", + "OrderedSet formIntersection with Self (100% overlap)", + ] + }, + { + "kind": "chart", + "title": "formIntersection with Array", + "tasks": [ + "OrderedSet formIntersection with Array (0% overlap)", + "OrderedSet formIntersection with Array (25% overlap)", + "OrderedSet formIntersection with Array (50% overlap)", + "OrderedSet formIntersection with Array (75% overlap)", + "OrderedSet formIntersection with Array (100% overlap)", + ] + }, + ] + }, + { + "kind": "variants", + "charts": [ + { + "kind": "chart", + "title": "symmetricDifference with Self", + "tasks": [ + "OrderedSet symmetricDifference with Self (0% overlap)", + "OrderedSet symmetricDifference with Self (25% overlap)", + "OrderedSet symmetricDifference with Self (50% overlap)", + "OrderedSet symmetricDifference with Self (75% overlap)", + "OrderedSet symmetricDifference with Self (100% overlap)", + ] + }, + { + "kind": "chart", + "title": "symmetricDifference with Array", + "tasks": [ + "OrderedSet symmetricDifference with Array (0% overlap)", + "OrderedSet symmetricDifference with Array (25% overlap)", + "OrderedSet symmetricDifference with Array (50% overlap)", + "OrderedSet symmetricDifference with Array (75% overlap)", + "OrderedSet symmetricDifference with Array (100% overlap)", + ] + }, + { + "kind": "chart", + "title": "formSymmetricDifference with Self", + "tasks": [ + "OrderedSet formSymmetricDifference with Self (0% overlap)", + "OrderedSet formSymmetricDifference with Self (25% overlap)", + "OrderedSet formSymmetricDifference with Self (50% overlap)", + "OrderedSet formSymmetricDifference with Self (75% overlap)", + "OrderedSet formSymmetricDifference with Self (100% overlap)", + ] + }, + { + "kind": "chart", + "title": "formSymmetricDifference with Array", + "tasks": [ + "OrderedSet formSymmetricDifference with Array (0% overlap)", + "OrderedSet formSymmetricDifference with Array (25% overlap)", + "OrderedSet formSymmetricDifference with Array (50% overlap)", + "OrderedSet formSymmetricDifference with Array (75% overlap)", + "OrderedSet formSymmetricDifference with Array (100% overlap)", + ] + }, + ] + }, + { + "kind": "variants", + "charts": [ + { + "kind": "chart", + "title": "subtracting Self", + "tasks": [ + "OrderedSet subtracting Self (0% overlap)", + "OrderedSet subtracting Self (25% overlap)", + "OrderedSet subtracting Self (50% overlap)", + "OrderedSet subtracting Self (75% overlap)", + "OrderedSet subtracting Self (100% overlap)", + ] + }, + { + "kind": "chart", + "title": "subtracting Array", + "tasks": [ + "OrderedSet subtracting Array (0% overlap)", + "OrderedSet subtracting Array (25% overlap)", + "OrderedSet subtracting Array (50% overlap)", + "OrderedSet subtracting Array (75% overlap)", + "OrderedSet subtracting Array (100% overlap)", + ] + }, + { + "kind": "chart", + "title": "subtract Self", + "tasks": [ + "OrderedSet subtract Self (0% overlap)", + "OrderedSet subtract Self (25% overlap)", + "OrderedSet subtract Self (50% overlap)", + "OrderedSet subtract Self (75% overlap)", + "OrderedSet subtract Self (100% overlap)", + ] + }, + { + "kind": "chart", + "title": "subtract with Array", + "tasks": [ + "OrderedSet subtract Array (0% overlap)", + "OrderedSet subtract Array (25% overlap)", + "OrderedSet subtract Array (50% overlap)", + "OrderedSet subtract Array (75% overlap)", + "OrderedSet subtract Array (100% overlap)", + ] + }, + ] + }, + ] + }, + { + "kind": "group", + "title": "OrderedSet vs Set", + "directory": "versus Set", + "contents": [ + { + "kind": "chart", + "title": "init from buffer of integers", + "tasks": [ + "OrderedSet init from unsafe buffer", + "Set init from unsafe buffer", + ] + }, + { + "kind": "chart", + "title": "init from range of integers", + "tasks": [ + "OrderedSet init from range", + "Set init from range", + ] + }, + { + "kind": "chart", + "title": "sequential iteration", + "tasks": [ + "OrderedSet sequential iteration", + "Set sequential iteration", + ] + }, + { + "kind": "chart", + "title": "successful random lookups", + "tasks": [ + "OrderedSet successful contains", + "Set successful contains", + ] + }, + { + "kind": "chart", + "title": "unsuccessful random lookups", + "tasks": [ + "OrderedSet unsuccessful contains", + "Set unsuccessful contains", + ] + }, + { + "kind": "chart", + "title": "insert", + "tasks": [ + "OrderedSet append", + "OrderedSet append, reserving capacity", + "Set insert", + "Set insert, reserving capacity", + ] + }, + { + "kind": "chart", + "title": "remove", + "tasks": [ + "OrderedSet remove", + "Set remove", + ] + }, + { + "kind": "variants", + "charts": [ + { + "kind": "chart", + "title": "union (0% overlap)", + "tasks": [ + "OrderedSet union with Self (0% overlap)", + "OrderedSet union with Array (0% overlap)", + "Set union with Self (0% overlap)", + "Set union with Array (0% overlap)", + ] + }, + { + "kind": "chart", + "title": "union (25% overlap)", + "tasks": [ + "OrderedSet union with Self (25% overlap)", + "OrderedSet union with Array (25% overlap)", + "Set union with Self (25% overlap)", + "Set union with Array (25% overlap)", + ] + }, + { + "kind": "chart", + "title": "union (50% overlap)", + "tasks": [ + "OrderedSet union with Self (50% overlap)", + "OrderedSet union with Array (50% overlap)", + "Set union with Self (50% overlap)", + "Set union with Array (50% overlap)", + ] + }, + { + "kind": "chart", + "title": "union (75% overlap)", + "tasks": [ + "OrderedSet union with Self (75% overlap)", + "OrderedSet union with Array (75% overlap)", + "Set union with Self (75% overlap)", + "Set union with Array (75% overlap)", + ] + }, + { + "kind": "chart", + "title": "union (100% overlap)", + "tasks": [ + "OrderedSet union with Self (100% overlap)", + "OrderedSet union with Array (100% overlap)", + "Set union with Self (100% overlap)", + "Set union with Array (100% overlap)", + ] + }, + ] + }, + { + "kind": "variants", + "charts": [ + { + "kind": "chart", + "title": "formUnion (0% overlap)", + "tasks": [ + "OrderedSet formUnion with Self (0% overlap)", + "OrderedSet formUnion with Array (0% overlap)", + "Set formUnion with Self (0% overlap)", + "Set formUnion with Array (0% overlap)", + ] + }, + { + "kind": "chart", + "title": "formUnion (25% overlap)", + "tasks": [ + "OrderedSet formUnion with Self (25% overlap)", + "OrderedSet formUnion with Array (25% overlap)", + "Set formUnion with Self (25% overlap)", + "Set formUnion with Array (25% overlap)", + ] + }, + { + "kind": "chart", + "title": "formUnion (50% overlap)", + "tasks": [ + "OrderedSet formUnion with Self (50% overlap)", + "OrderedSet formUnion with Array (50% overlap)", + "Set formUnion with Self (50% overlap)", + "Set formUnion with Array (50% overlap)", + ] + }, + { + "kind": "chart", + "title": "formUnion (75% overlap)", + "tasks": [ + "OrderedSet formUnion with Self (75% overlap)", + "OrderedSet formUnion with Array (75% overlap)", + "Set formUnion with Self (75% overlap)", + "Set formUnion with Array (75% overlap)", + ] + }, + { + "kind": "chart", + "title": "formUnion (100% overlap)", + "tasks": [ + "OrderedSet formUnion with Self (100% overlap)", + "OrderedSet formUnion with Array (100% overlap)", + "Set formUnion with Self (100% overlap)", + "Set formUnion with Array (100% overlap)", + ] + }, + ] + }, + { + "kind": "variants", + "charts": [ + { + "kind": "chart", + "title": "intersection (0% overlap)", + "tasks": [ + "OrderedSet intersection with Self (0% overlap)", + "OrderedSet intersection with Array (0% overlap)", + "Set intersection with Self (0% overlap)", + "Set intersection with Array (0% overlap)", + ] + }, + { + "kind": "chart", + "title": "intersection (25% overlap)", + "tasks": [ + "OrderedSet intersection with Self (25% overlap)", + "OrderedSet intersection with Array (25% overlap)", + "Set intersection with Self (25% overlap)", + "Set intersection with Array (25% overlap)", + ] + }, + { + "kind": "chart", + "title": "intersection (50% overlap)", + "tasks": [ + "OrderedSet intersection with Self (50% overlap)", + "OrderedSet intersection with Array (50% overlap)", + "Set intersection with Self (50% overlap)", + "Set intersection with Array (50% overlap)", + ] + }, + { + "kind": "chart", + "title": "intersection (75% overlap)", + "tasks": [ + "OrderedSet intersection with Self (75% overlap)", + "OrderedSet intersection with Array (75% overlap)", + "Set intersection with Self (75% overlap)", + "Set intersection with Array (75% overlap)", + ] + }, + { + "kind": "chart", + "title": "intersection (100% overlap)", + "tasks": [ + "OrderedSet intersection with Self (100% overlap)", + "OrderedSet intersection with Array (100% overlap)", + "Set intersection with Self (100% overlap)", + "Set intersection with Array (100% overlap)", + ] + }, + ] + }, + { + "kind": "variants", + "charts": [ + { + "kind": "chart", + "title": "formIntersection (0% overlap)", + "tasks": [ + "OrderedSet formIntersection with Self (0% overlap)", + "OrderedSet formIntersection with Array (0% overlap)", + "Set formIntersection with Self (0% overlap)", + "Set formIntersection with Array (0% overlap)", + ] + }, + { + "kind": "chart", + "title": "formIntersection (25% overlap)", + "tasks": [ + "OrderedSet formIntersection with Self (25% overlap)", + "OrderedSet formIntersection with Array (25% overlap)", + "Set formIntersection with Self (25% overlap)", + "Set formIntersection with Array (25% overlap)", + ] + }, + { + "kind": "chart", + "title": "formIntersection (50% overlap)", + "tasks": [ + "OrderedSet formIntersection with Self (50% overlap)", + "OrderedSet formIntersection with Array (50% overlap)", + "Set formIntersection with Self (50% overlap)", + "Set formIntersection with Array (50% overlap)", + ] + }, + { + "kind": "chart", + "title": "formIntersection (75% overlap)", + "tasks": [ + "OrderedSet formIntersection with Self (75% overlap)", + "OrderedSet formIntersection with Array (75% overlap)", + "Set formIntersection with Self (75% overlap)", + "Set formIntersection with Array (75% overlap)", + ] + }, + { + "kind": "chart", + "title": "formIntersection (100% overlap)", + "tasks": [ + "OrderedSet formIntersection with Self (100% overlap)", + "OrderedSet formIntersection with Array (100% overlap)", + "Set formIntersection with Self (100% overlap)", + "Set formIntersection with Array (100% overlap)", + ] + }, + ] + }, + { + "kind": "variants", + "charts": [ + { + "kind": "chart", + "title": "symmetricDifference (0% overlap)", + "tasks": [ + "OrderedSet symmetricDifference with Self (0% overlap)", + "OrderedSet symmetricDifference with Array (0% overlap)", + "Set symmetricDifference with Self (0% overlap)", + "Set symmetricDifference with Array (0% overlap)", + ] + }, + { + "kind": "chart", + "title": "symmetricDifference (25% overlap)", + "tasks": [ + "OrderedSet symmetricDifference with Self (25% overlap)", + "OrderedSet symmetricDifference with Array (25% overlap)", + "Set symmetricDifference with Self (25% overlap)", + "Set symmetricDifference with Array (25% overlap)", + ] + }, + { + "kind": "chart", + "title": "symmetricDifference (50% overlap)", + "tasks": [ + "OrderedSet symmetricDifference with Self (50% overlap)", + "OrderedSet symmetricDifference with Array (50% overlap)", + "Set symmetricDifference with Self (50% overlap)", + "Set symmetricDifference with Array (50% overlap)", + ] + }, + { + "kind": "chart", + "title": "symmetricDifference (75% overlap)", + "tasks": [ + "OrderedSet symmetricDifference with Self (75% overlap)", + "OrderedSet symmetricDifference with Array (75% overlap)", + "Set symmetricDifference with Self (75% overlap)", + "Set symmetricDifference with Array (75% overlap)", + ] + }, + { + "kind": "chart", + "title": "symmetricDifference (100% overlap)", + "tasks": [ + "OrderedSet symmetricDifference with Self (100% overlap)", + "OrderedSet symmetricDifference with Array (100% overlap)", + "Set symmetricDifference with Self (100% overlap)", + "Set symmetricDifference with Array (100% overlap)", + ] + }, + ] + }, + { + "kind": "variants", + "charts": [ + { + "kind": "chart", + "title": "formSymmetricDifference (0% overlap)", + "tasks": [ + "OrderedSet formSymmetricDifference with Self (0% overlap)", + "OrderedSet formSymmetricDifference with Array (0% overlap)", + "Set formSymmetricDifference with Self (0% overlap)", + "Set formSymmetricDifference with Array (0% overlap)", + ] + }, + { + "kind": "chart", + "title": "formSymmetricDifference (25% overlap)", + "tasks": [ + "OrderedSet formSymmetricDifference with Self (25% overlap)", + "OrderedSet formSymmetricDifference with Array (25% overlap)", + "Set formSymmetricDifference with Self (25% overlap)", + "Set formSymmetricDifference with Array (25% overlap)", + ] + }, + { + "kind": "chart", + "title": "formSymmetricDifference (50% overlap)", + "tasks": [ + "OrderedSet formSymmetricDifference with Self (50% overlap)", + "OrderedSet formSymmetricDifference with Array (50% overlap)", + "Set formSymmetricDifference with Self (50% overlap)", + "Set formSymmetricDifference with Array (50% overlap)", + ] + }, + { + "kind": "chart", + "title": "formSymmetricDifference (75% overlap)", + "tasks": [ + "OrderedSet formSymmetricDifference with Self (75% overlap)", + "OrderedSet formSymmetricDifference with Array (75% overlap)", + "Set formSymmetricDifference with Self (75% overlap)", + "Set formSymmetricDifference with Array (75% overlap)", + ] + }, + { + "kind": "chart", + "title": "formSymmetricDifference (100% overlap)", + "tasks": [ + "OrderedSet formSymmetricDifference with Self (100% overlap)", + "OrderedSet formSymmetricDifference with Array (100% overlap)", + "Set formSymmetricDifference with Self (100% overlap)", + "Set formSymmetricDifference with Array (100% overlap)", + ] + }, + ] + }, + { + "kind": "variants", + "charts": [ + { + "kind": "chart", + "title": "subtracting (0% overlap)", + "tasks": [ + "OrderedSet subtracting Self (0% overlap)", + "OrderedSet subtracting Array (0% overlap)", + "Set subtracting Self (0% overlap)", + "Set subtracting Array (0% overlap)", + ] + }, + { + "kind": "chart", + "title": "subtracting (25% overlap)", + "tasks": [ + "OrderedSet subtracting Self (25% overlap)", + "OrderedSet subtracting Array (25% overlap)", + "Set subtracting Self (25% overlap)", + "Set subtracting Array (25% overlap)", + ] + }, + { + "kind": "chart", + "title": "subtracting (50% overlap)", + "tasks": [ + "OrderedSet subtracting Self (50% overlap)", + "OrderedSet subtracting Array (50% overlap)", + "Set subtracting Self (50% overlap)", + "Set subtracting Array (50% overlap)", + ] + }, + { + "kind": "chart", + "title": "subtracting (75% overlap)", + "tasks": [ + "OrderedSet subtracting Self (75% overlap)", + "OrderedSet subtracting Array (75% overlap)", + "Set subtracting Self (75% overlap)", + "Set subtracting Array (75% overlap)", + ] + }, + { + "kind": "chart", + "title": "subtracting (100% overlap)", + "tasks": [ + "OrderedSet subtracting Self (100% overlap)", + "OrderedSet subtracting Array (100% overlap)", + "Set subtracting Self (100% overlap)", + "Set subtracting Array (100% overlap)", + ] + }, + ] + }, + { + "kind": "variants", + "charts": [ + { + "kind": "chart", + "title": "subtract (0% overlap)", + "tasks": [ + "OrderedSet subtract Self (0% overlap)", + "OrderedSet subtract Array (0% overlap)", + "Set subtract Self (0% overlap)", + "Set subtract Array (0% overlap)", + ] + }, + { + "kind": "chart", + "title": "subtract (25% overlap)", + "tasks": [ + "OrderedSet subtract Self (25% overlap)", + "OrderedSet subtract Array (25% overlap)", + "Set subtract Self (25% overlap)", + "Set subtract Array (25% overlap)", + ] + }, + { + "kind": "chart", + "title": "subtract (50% overlap)", + "tasks": [ + "OrderedSet subtract Self (50% overlap)", + "OrderedSet subtract Array (50% overlap)", + "Set subtract Self (50% overlap)", + "Set subtract Array (50% overlap)", + ] + }, + { + "kind": "chart", + "title": "subtract (75% overlap)", + "tasks": [ + "OrderedSet subtract Self (75% overlap)", + "OrderedSet subtract Array (75% overlap)", + "Set subtract Self (75% overlap)", + "Set subtract Array (75% overlap)", + ] + }, + { + "kind": "chart", + "title": "subtract (100% overlap)", + "tasks": [ + "OrderedSet subtract Self (100% overlap)", + "OrderedSet subtract Array (100% overlap)", + "Set subtract Self (100% overlap)", + "Set subtract Array (100% overlap)", + ] + }, + ] + }, + ] + }, + { + "kind": "group", + "title": "OrderedSet vs std::unordered_set", + "directory": "versus STL unordered_set", + "contents": [ + { + "kind": "chart", + "title": "init from integer range", + "tasks": [ + "std::unordered_set insert from integer range", + "Set init from range", + "OrderedSet init from range", + ] + }, + { + "kind": "chart", + "title": "init from buffer of integers", + "tasks": [ + "std::unordered_set constructor from buffer", + "Set init from unsafe buffer", + "OrderedSet init from unsafe buffer", + ] + }, + { + "kind": "chart", + "title": "sequential iteration", + "tasks": [ + "std::unordered_set sequential iteration", + "Set sequential iteration", + "OrderedSet sequential iteration", + ] + }, + { + "kind": "chart", + "title": "successful lookups", + "tasks": [ + "std::unordered_set successful find", + "OrderedSet successful contains", + "Set successful contains", + ] + }, + { + "kind": "chart", + "title": "unsuccessful lookups", + "tasks": [ + "std::unordered_set unsuccessful find", + "OrderedSet unsuccessful contains", + "Set unsuccessful contains", + ] + }, + { + "kind": "chart", + "title": "insertions", + "tasks": [ + "std::unordered_set insert", + "Set insert", + "OrderedSet append", + ] + }, + { + "kind": "chart", + "title": "insertions, reserving capacity", + "tasks": [ + "std::unordered_set insert, reserving capacity", + "Set insert, reserving capacity", + "OrderedSet append, reserving capacity", + ] + }, + { + "kind": "chart", + "title": "random removals", + "tasks": [ + "std::unordered_set erase", + "Set remove", + "OrderedSet remove", + ] + }, + ] + }, + + ] +} diff --git a/Benchmarks/Libraries/Set.json b/Benchmarks/Libraries/Set.json new file mode 100644 index 000000000..0322122d4 --- /dev/null +++ b/Benchmarks/Libraries/Set.json @@ -0,0 +1,299 @@ +{ + "kind": "group", + "title": "Set Benchmarks", + "directory": "Set", + "contents": [ + { + "kind": "group", + "title": "Operations", + "directory": "operations", + "contents": [ + { + "kind": "chart", + "title": "operations", + "tasks": [ + "Set init from range", + "Set init from unsafe buffer", + "Set sequential iteration", + "Set successful contains", + "Set unsuccessful contains", + "Set insert", + "Set insert, reserving capacity", + "Set remove" + ] + }, + { + "kind": "variants", + "charts": [ + { + "kind": "chart", + "title": "union with Self", + "tasks": [ + "Set union with Self (0% overlap)", + "Set union with Self (25% overlap)", + "Set union with Self (50% overlap)", + "Set union with Self (75% overlap)", + "Set union with Self (100% overlap)" + ] + }, + { + "kind": "chart", + "title": "union with Array", + "tasks": [ + "Set union with Array (0% overlap)", + "Set union with Array (25% overlap)", + "Set union with Array (50% overlap)", + "Set union with Array (75% overlap)", + "Set union with Array (100% overlap)", + ] + }, + { + "kind": "chart", + "title": "formUnion with Self", + "tasks": [ + "Set formUnion with Self (0% overlap)", + "Set formUnion with Self (25% overlap)", + "Set formUnion with Self (50% overlap)", + "Set formUnion with Self (75% overlap)", + "Set formUnion with Self (100% overlap)", + ] + }, + { + "kind": "chart", + "title": "formUnion with Array", + "tasks": [ + "Set formUnion with Array (0% overlap)", + "Set formUnion with Array (25% overlap)", + "Set formUnion with Array (50% overlap)", + "Set formUnion with Array (75% overlap)", + "Set formUnion with Array (100% overlap)", + ] + }, + ] + }, + { + "kind": "variants", + "charts": [ + { + "kind": "chart", + "title": "intersection with Self", + "tasks": [ + "Set intersection with Self (0% overlap)", + "Set intersection with Self (25% overlap)", + "Set intersection with Self (50% overlap)", + "Set intersection with Self (75% overlap)", + "Set intersection with Self (100% overlap)", + ] + }, + { + "kind": "chart", + "title": "intersection with Array", + "tasks": [ + "Set intersection with Array (0% overlap)", + "Set intersection with Array (25% overlap)", + "Set intersection with Array (50% overlap)", + "Set intersection with Array (75% overlap)", + "Set intersection with Array (100% overlap)", + ] + }, + { + "kind": "chart", + "title": "formIntersection with Self", + "tasks": [ + "Set formIntersection with Self (0% overlap)", + "Set formIntersection with Self (25% overlap)", + "Set formIntersection with Self (50% overlap)", + "Set formIntersection with Self (75% overlap)", + "Set formIntersection with Self (100% overlap)", + ] + }, + { + "kind": "chart", + "title": "formIntersection with Array", + "tasks": [ + "Set formIntersection with Array (0% overlap)", + "Set formIntersection with Array (25% overlap)", + "Set formIntersection with Array (50% overlap)", + "Set formIntersection with Array (75% overlap)", + "Set formIntersection with Array (100% overlap)", + ] + }, + ] + }, + { + "kind": "variants", + "charts": [ + { + "kind": "chart", + "title": "symmetricDifference with Self", + "tasks": [ + "Set symmetricDifference with Self (0% overlap)", + "Set symmetricDifference with Self (25% overlap)", + "Set symmetricDifference with Self (50% overlap)", + "Set symmetricDifference with Self (75% overlap)", + "Set symmetricDifference with Self (100% overlap)", + ] + }, + { + "kind": "chart", + "title": "symmetricDifference with Array", + "tasks": [ + "Set symmetricDifference with Array (0% overlap)", + "Set symmetricDifference with Array (25% overlap)", + "Set symmetricDifference with Array (50% overlap)", + "Set symmetricDifference with Array (75% overlap)", + "Set symmetricDifference with Array (100% overlap)", + ] + }, + { + "kind": "chart", + "title": "formSymmetricDifference with Self", + "tasks": [ + "Set formSymmetricDifference with Self (0% overlap)", + "Set formSymmetricDifference with Self (25% overlap)", + "Set formSymmetricDifference with Self (50% overlap)", + "Set formSymmetricDifference with Self (75% overlap)", + "Set formSymmetricDifference with Self (100% overlap)", + ] + }, + { + "kind": "chart", + "title": "formSymmetricDifference with Array", + "tasks": [ + "Set formSymmetricDifference with Array (0% overlap)", + "Set formSymmetricDifference with Array (25% overlap)", + "Set formSymmetricDifference with Array (50% overlap)", + "Set formSymmetricDifference with Array (75% overlap)", + "Set formSymmetricDifference with Array (100% overlap)", + ] + }, + ] + }, + { + "kind": "variants", + "charts": [ + { + "kind": "chart", + "title": "subtracting Self", + "tasks": [ + "Set subtracting Self (0% overlap)", + "Set subtracting Self (25% overlap)", + "Set subtracting Self (50% overlap)", + "Set subtracting Self (75% overlap)", + "Set subtracting Self (100% overlap)", + ] + }, + { + "kind": "chart", + "title": "subtracting Array", + "tasks": [ + "Set subtracting Array (0% overlap)", + "Set subtracting Array (25% overlap)", + "Set subtracting Array (50% overlap)", + "Set subtracting Array (75% overlap)", + "Set subtracting Array (100% overlap)", + ] + }, + { + "kind": "chart", + "title": "subtract Self", + "tasks": [ + "Set subtract Self (0% overlap)", + "Set subtract Self (25% overlap)", + "Set subtract Self (50% overlap)", + "Set subtract Self (75% overlap)", + "Set subtract Self (100% overlap)", + ] + }, + { + "kind": "chart", + "title": "subtract with Array", + "tasks": [ + "Set subtract Array (0% overlap)", + "Set subtract Array (25% overlap)", + "Set subtract Array (50% overlap)", + "Set subtract Array (75% overlap)", + "Set subtract Array (100% overlap)", + ] + }, + ] + }, + ] + }, + { + "kind": "group", + "title": "Set vs std::unordered_set", + "directory": "versus STL unordered_set", + "contents": [ + { + "kind": "chart", + "title": "Hashing", + "tasks": [ + "std::hash", + "custom_intptr_hash (using Swift.Hasher)", + "Hasher.combine on a single buffer of integers", + "Int.hashValue on each value", + ] + }, + { + "kind": "chart", + "title": "init from integer range", + "tasks": [ + "std::unordered_set insert from integer range", + "Set init from range", + ] + }, + { + "kind": "chart", + "title": "init from buffer of integers", + "tasks": [ + "std::unordered_set constructor from buffer", + "Set init from unsafe buffer", + ] + }, + { + "kind": "chart", + "title": "sequential iteration", + "tasks": [ + "std::unordered_set sequential iteration", + "Set sequential iteration", + ] + }, + { + "kind": "chart", + "title": "successful lookups", + "tasks": [ + "std::unordered_set successful find", + "Set successful contains", + ] + }, + { + "kind": "chart", + "title": "unsuccessful lookups", + "tasks": [ + "std::unordered_set unsuccessful find", + "Set unsuccessful contains", + ] + }, + { + "kind": "chart", + "title": "random insertions", + "tasks": [ + "std::unordered_set insert", + "std::unordered_set insert, reserving capacity", + "Set insert", + "Set insert, reserving capacity", + ] + }, + { + "kind": "chart", + "title": "random removals", + "tasks": [ + "std::unordered_set erase", + "Set remove", + ] + }, + ] + } + ] +} diff --git a/Benchmarks/Libraries/TreeDictionary.json b/Benchmarks/Libraries/TreeDictionary.json new file mode 100644 index 000000000..29d697e3b --- /dev/null +++ b/Benchmarks/Libraries/TreeDictionary.json @@ -0,0 +1,211 @@ +{ + "kind": "group", + "title": "TreeDictionary Benchmarks", + "directory": "TreeDictionary", + "contents": [ + { + "kind": "group", + "title": "TreeDictionary Operations", + "contents": [ + { + "kind": "chart", + "title": "all", + "tasks": [ + "TreeDictionary init(uniqueKeysWithValues:)", + "TreeDictionary sequential iteration", + "TreeDictionary sequential iteration, indices", + "TreeDictionary.Keys sequential iteration", + "TreeDictionary.Values sequential iteration", + "TreeDictionary indexing subscript", + "TreeDictionary subscript, successful lookups", + "TreeDictionary subscript, unsuccessful lookups", + "TreeDictionary subscript, noop setter", + "TreeDictionary subscript, set existing", + "TreeDictionary subscript, _modify", + "TreeDictionary subscript, insert, unique", + "TreeDictionary subscript, insert, shared", + "TreeDictionary subscript, remove existing, unique", + "TreeDictionary subscript, remove existing, shared", + "TreeDictionary subscript, remove missing", + "TreeDictionary defaulted subscript, successful lookups", + "TreeDictionary defaulted subscript, unsuccessful lookups", + "TreeDictionary defaulted subscript, _modify existing", + "TreeDictionary defaulted subscript, _modify missing", + "TreeDictionary successful index(forKey:)", + "TreeDictionary unsuccessful index(forKey:)", + "TreeDictionary updateValue(_:forKey:), existing", + "TreeDictionary updateValue(_:forKey:), insert", + "TreeDictionary random removals (existing keys)", + "TreeDictionary random removals (missing keys)", + ] + } + ] + }, + { + "kind": "group", + "title": "Comparisons against reference implementations", + "directory": "versus", + "contents": [ + { + "kind": "chart", + "title": "init(uniqueKeysWithValues:)", + "tasks": [ + "TreeDictionary init(uniqueKeysWithValues:)", + "Dictionary init(uniqueKeysWithValues:)", + "OrderedDictionary init(uniqueKeysWithValues:)", + ] + }, + { + "kind": "chart", + "title": "sequential iteration", + "tasks": [ + "TreeDictionary sequential iteration", + "Dictionary sequential iteration", + "OrderedDictionary sequential iteration", + ] + }, + { + "kind": "chart", + "title": "sequential iteration [Keys]", + "tasks": [ + "TreeDictionary.Keys sequential iteration", + "Dictionary.Keys sequential iteration", + "OrderedDictionary.Keys sequential iteration", + ] + }, + { + "kind": "chart", + "title": "sequential iteration [Values]", + "tasks": [ + "TreeDictionary.Values sequential iteration", + "Dictionary.Values sequential iteration", + "OrderedDictionary.Values sequential iteration", + ] + }, + { + "kind": "chart", + "title": "sequential iteration using indices", + "tasks": [ + "TreeDictionary sequential iteration, indices", + "Dictionary sequential iteration, indices", + "OrderedDictionary sequential iteration, indices", + ] + }, + { + "kind": "chart", + "title": "indexing subscript", + "tasks": [ + "TreeDictionary indexing subscript", + "Dictionary indexing subscript", + "OrderedDictionary indexing subscript", + ] + }, + { + "kind": "chart", + "title": "subscript, successful lookups", + "tasks": [ + "TreeDictionary subscript, successful lookups", + "Dictionary subscript, successful lookups", + "OrderedDictionary subscript, successful lookups", + ] + }, + { + "kind": "chart", + "title": "subscript, unsuccessful lookups", + "tasks": [ + "TreeDictionary subscript, unsuccessful lookups", + "Dictionary subscript, unsuccessful lookups", + "OrderedDictionary subscript, unsuccessful lookups", + ] + }, + { + "kind": "chart", + "title": "subscript, insert into unique collection", + "tasks": [ + "TreeDictionary subscript, insert, unique", + "Dictionary subscript, insert, unique", + "Dictionary subscript, insert, reserving capacity", + "OrderedDictionary subscript, append, unique", + "OrderedDictionary subscript, append, reserving capacity", + ] + }, + { + "kind": "chart", + "title": "subscript, insert into shared collection", + "tasks": [ + "TreeDictionary subscript, insert, shared", + "Dictionary subscript, insert, shared", + "OrderedDictionary subscript, append, shared", + ] + }, + { + "kind": "chart", + "title": "subscript, remove existing from unique collection", + "tasks": [ + "TreeDictionary subscript, remove existing, unique", + "Dictionary subscript, remove existing, unique", + "OrderedDictionary subscript, remove existing, unique", + ] + }, + { + "kind": "chart", + "title": "subscript, remove existing from shared collection", + "tasks": [ + "TreeDictionary subscript, remove existing, shared", + "Dictionary subscript, remove existing, shared", + "OrderedDictionary subscript, remove existing, shared", + ] + }, + { + "kind": "chart", + "title": "subscript, _modify existing", + "tasks": [ + "TreeDictionary subscript, _modify", + "TreeDictionary defaulted subscript, _modify existing", + "Dictionary subscript, _modify", + "Dictionary defaulted subscript, _modify existing", + "OrderedDictionary subscript, _modify", + "OrderedDictionary defaulted subscript, _modify existing", + ] + }, + + { + "kind": "chart", + "title": "index(forKey:), successful index(forKey:)", + "tasks": [ + "TreeDictionary successful index(forKey:)", + "Dictionary successful index(forKey:)", + "OrderedDictionary successful index(forKey:)", + ] + }, + { + "kind": "chart", + "title": "index(forKey:), unsuccessful index(forKey:)", + "tasks": [ + "TreeDictionary unsuccessful index(forKey:)", + "Dictionary unsuccessful index(forKey:)", + "OrderedDictionary unsuccessful index(forKey:)", + ] + }, + { + "kind": "chart", + "title": "updateValue(_:forKey:), existing", + "tasks": [ + "TreeDictionary updateValue(_:forKey:), existing", + "Dictionary updateValue(_:forKey:), existing", + "OrderedDictionary updateValue(_:forKey:), existing" + ] + }, + { + "kind": "chart", + "title": "updateValue(_:forKey:), insert", + "tasks": [ + "TreeDictionary updateValue(_:forKey:), insert", + "Dictionary updateValue(_:forKey:), insert", + "OrderedDictionary updateValue(_:forKey:), append" + ] + }, + ] + }, + ] +} diff --git a/Benchmarks/Libraries/TreeSet.json b/Benchmarks/Libraries/TreeSet.json new file mode 100644 index 000000000..4532d99c9 --- /dev/null +++ b/Benchmarks/Libraries/TreeSet.json @@ -0,0 +1,889 @@ +{ + "kind": "group", + "title": "TreeSet Benchmarks", + "directory": "TreeSet", + "contents": [ + { + "kind": "group", + "title": "Operations", + "directory": "operations", + "contents": [ + { + "kind": "chart", + "title": "operations", + "tasks": [ + "TreeSet init from range", + "TreeSet init from unsafe buffer", + "TreeSet sequential iteration", + "TreeSet successful contains", + "TreeSet unsuccessful contains", + "TreeSet insert", + "TreeSet remove" + ] + }, + { + "kind": "chart", + "title": "initializers", + "tasks": [ + "TreeSet init from range", + "TreeSet init from unsafe buffer", + ] + }, + { + "kind": "variants", + "charts": [ + { + "kind": "chart", + "title": "union with Self, distinct", + "tasks": [ + "TreeSet union with Self (0% overlap, distinct)", + "TreeSet union with Self (25% overlap, distinct)", + "TreeSet union with Self (50% overlap, distinct)", + "TreeSet union with Self (75% overlap, distinct)", + "TreeSet union with Self (100% overlap, distinct)", + ] + }, + { + "kind": "chart", + "title": "union with Self, shared", + "tasks": [ + "TreeSet union with Self (0% overlap, shared)", + "TreeSet union with Self (25% overlap, shared)", + "TreeSet union with Self (50% overlap, shared)", + "TreeSet union with Self (75% overlap, shared)", + "TreeSet union with Self (100% overlap, shared)", + ] + }, + { + "kind": "chart", + "title": "union with Array", + "tasks": [ + "TreeSet union with Array (0% overlap)", + "TreeSet union with Array (25% overlap)", + "TreeSet union with Array (50% overlap)", + "TreeSet union with Array (75% overlap)", + "TreeSet union with Array (100% overlap)", + ] + }, + { + "kind": "chart", + "title": "formUnion with Self, distinct", + "tasks": [ + "TreeSet formUnion with Self (0% overlap, distinct)", + "TreeSet formUnion with Self (25% overlap, distinct)", + "TreeSet formUnion with Self (50% overlap, distinct)", + "TreeSet formUnion with Self (75% overlap, distinct)", + "TreeSet formUnion with Self (100% overlap, distinct)", + ] + }, + { + "kind": "chart", + "title": "formUnion with Self, shared", + "tasks": [ + "TreeSet formUnion with Self (0% overlap, shared)", + "TreeSet formUnion with Self (25% overlap, shared)", + "TreeSet formUnion with Self (50% overlap, shared)", + "TreeSet formUnion with Self (75% overlap, shared)", + "TreeSet formUnion with Self (100% overlap, shared)", + ] + }, + { + "kind": "chart", + "title": "formUnion with Array", + "tasks": [ + "TreeSet formUnion with Array (0% overlap)", + "TreeSet formUnion with Array (25% overlap)", + "TreeSet formUnion with Array (50% overlap)", + "TreeSet formUnion with Array (75% overlap)", + "TreeSet formUnion with Array (100% overlap)", + ] + }, + ] + }, + { + "kind": "variants", + "charts": [ + { + "kind": "chart", + "title": "intersection with Self, distinct", + "tasks": [ + "TreeSet intersection with Self (0% overlap, distinct)", + "TreeSet intersection with Self (25% overlap, distinct)", + "TreeSet intersection with Self (50% overlap, distinct)", + "TreeSet intersection with Self (75% overlap, distinct)", + "TreeSet intersection with Self (100% overlap, distinct)", + ] + }, + { + "kind": "chart", + "title": "intersection with Self, shared", + "tasks": [ + "TreeSet intersection with Self (0% overlap, shared)", + "TreeSet intersection with Self (25% overlap, shared)", + "TreeSet intersection with Self (50% overlap, shared)", + "TreeSet intersection with Self (75% overlap, shared)", + "TreeSet intersection with Self (100% overlap, shared)", + ] + }, + { + "kind": "chart", + "title": "intersection with Array", + "tasks": [ + "TreeSet intersection with Array (0% overlap)", + "TreeSet intersection with Array (25% overlap)", + "TreeSet intersection with Array (50% overlap)", + "TreeSet intersection with Array (75% overlap)", + "TreeSet intersection with Array (100% overlap)", + ] + }, + { + "kind": "chart", + "title": "formIntersection with Self, distinct", + "tasks": [ + "TreeSet formIntersection with Self (0% overlap, distinct)", + "TreeSet formIntersection with Self (25% overlap, distinct)", + "TreeSet formIntersection with Self (50% overlap, distinct)", + "TreeSet formIntersection with Self (75% overlap, distinct)", + "TreeSet formIntersection with Self (100% overlap, distinct)", + ] + }, + { + "kind": "chart", + "title": "formIntersection with Self, shared", + "tasks": [ + "TreeSet formIntersection with Self (0% overlap, shared)", + "TreeSet formIntersection with Self (25% overlap, shared)", + "TreeSet formIntersection with Self (50% overlap, shared)", + "TreeSet formIntersection with Self (75% overlap, shared)", + "TreeSet formIntersection with Self (100% overlap, shared)", + ] + }, + { + "kind": "chart", + "title": "formIntersection with Array", + "tasks": [ + "TreeSet formIntersection with Array (0% overlap)", + "TreeSet formIntersection with Array (25% overlap)", + "TreeSet formIntersection with Array (50% overlap)", + "TreeSet formIntersection with Array (75% overlap)", + "TreeSet formIntersection with Array (100% overlap)", + ] + }, + ] + }, + { + "kind": "variants", + "charts": [ + { + "kind": "chart", + "title": "symmetricDifference with Self, distinct", + "tasks": [ + "TreeSet symmetricDifference with Self (0% overlap, distinct)", + "TreeSet symmetricDifference with Self (25% overlap, distinct)", + "TreeSet symmetricDifference with Self (50% overlap, distinct)", + "TreeSet symmetricDifference with Self (75% overlap, distinct)", + "TreeSet symmetricDifference with Self (100% overlap, distinct)", + ] + }, + { + "kind": "chart", + "title": "symmetricDifference with Self, shared", + "tasks": [ + "TreeSet symmetricDifference with Self (0% overlap, shared)", + "TreeSet symmetricDifference with Self (25% overlap, shared)", + "TreeSet symmetricDifference with Self (50% overlap, shared)", + "TreeSet symmetricDifference with Self (75% overlap, shared)", + "TreeSet symmetricDifference with Self (100% overlap, shared)", + ] + }, + { + "kind": "chart", + "title": "symmetricDifference with Array", + "tasks": [ + "TreeSet symmetricDifference with Array (0% overlap)", + "TreeSet symmetricDifference with Array (25% overlap)", + "TreeSet symmetricDifference with Array (50% overlap)", + "TreeSet symmetricDifference with Array (75% overlap)", + "TreeSet symmetricDifference with Array (100% overlap)", + ] + }, + { + "kind": "chart", + "title": "formSymmetricDifference with Self, distinct", + "tasks": [ + "TreeSet formSymmetricDifference with Self (0% overlap, distinct)", + "TreeSet formSymmetricDifference with Self (25% overlap, distinct)", + "TreeSet formSymmetricDifference with Self (50% overlap, distinct)", + "TreeSet formSymmetricDifference with Self (75% overlap, distinct)", + "TreeSet formSymmetricDifference with Self (100% overlap, distinct)", + ] + }, + { + "kind": "chart", + "title": "formSymmetricDifference with Self, shared", + "tasks": [ + "TreeSet formSymmetricDifference with Self (0% overlap, shared)", + "TreeSet formSymmetricDifference with Self (25% overlap, shared)", + "TreeSet formSymmetricDifference with Self (50% overlap, shared)", + "TreeSet formSymmetricDifference with Self (75% overlap, shared)", + "TreeSet formSymmetricDifference with Self (100% overlap, shared)", + ] + }, + { + "kind": "chart", + "title": "formSymmetricDifference with Array", + "tasks": [ + "TreeSet formSymmetricDifference with Array (0% overlap)", + "TreeSet formSymmetricDifference with Array (25% overlap)", + "TreeSet formSymmetricDifference with Array (50% overlap)", + "TreeSet formSymmetricDifference with Array (75% overlap)", + "TreeSet formSymmetricDifference with Array (100% overlap)", + ] + }, + ] + }, + { + "kind": "variants", + "charts": [ + { + "kind": "chart", + "title": "subtracting Self, distinct", + "tasks": [ + "TreeSet subtracting Self (0% overlap, distinct)", + "TreeSet subtracting Self (25% overlap, distinct)", + "TreeSet subtracting Self (50% overlap, distinct)", + "TreeSet subtracting Self (75% overlap, distinct)", + "TreeSet subtracting Self (100% overlap, distinct)", + ] + }, + { + "kind": "chart", + "title": "subtracting Self, shared", + "tasks": [ + "TreeSet subtracting Self (0% overlap, shared)", + "TreeSet subtracting Self (25% overlap, shared)", + "TreeSet subtracting Self (50% overlap, shared)", + "TreeSet subtracting Self (75% overlap, shared)", + "TreeSet subtracting Self (100% overlap, shared)", + ] + }, + { + "kind": "chart", + "title": "subtracting Array", + "tasks": [ + "TreeSet subtracting Array (0% overlap)", + "TreeSet subtracting Array (25% overlap)", + "TreeSet subtracting Array (50% overlap)", + "TreeSet subtracting Array (75% overlap)", + "TreeSet subtracting Array (100% overlap)", + ] + }, + { + "kind": "chart", + "title": "subtract Self, distinct", + "tasks": [ + "TreeSet subtract Self (0% overlap, distinct)", + "TreeSet subtract Self (25% overlap, distinct)", + "TreeSet subtract Self (50% overlap, distinct)", + "TreeSet subtract Self (75% overlap, distinct)", + "TreeSet subtract Self (100% overlap, distinct)", + ] + }, + { + "kind": "chart", + "title": "subtract Self, shared", + "tasks": [ + "TreeSet subtract Self (0% overlap, shared)", + "TreeSet subtract Self (25% overlap, shared)", + "TreeSet subtract Self (50% overlap, shared)", + "TreeSet subtract Self (75% overlap, shared)", + "TreeSet subtract Self (100% overlap, shared)", + ] + }, + { + "kind": "chart", + "title": "subtract with Array", + "tasks": [ + "TreeSet subtract Array (0% overlap)", + "TreeSet subtract Array (25% overlap)", + "TreeSet subtract Array (50% overlap)", + "TreeSet subtract Array (75% overlap)", + "TreeSet subtract Array (100% overlap)", + ] + }, + ] + } + ] + }, + { + "kind": "group", + "title": "Reference comparisons", + "directory": "versus Set", + "contents": [ + { + "kind": "chart", + "title": "init from buffer of integers", + "tasks": [ + "TreeSet init from unsafe buffer", + "Set init from unsafe buffer", + ] + }, + { + "kind": "chart", + "title": "init from range of integers", + "tasks": [ + "TreeSet init from range", + "Set init from range", + ] + }, + { + "kind": "chart", + "title": "sequential iteration", + "tasks": [ + "TreeSet sequential iteration", + "Set sequential iteration", + ] + }, + { + "kind": "chart", + "title": "sequential iteration using indices", + "tasks": [ + "TreeSet sequential iteration, indices", + "Set sequential iteration, indices", + ] + }, + { + "kind": "chart", + "title": "successful random lookups", + "tasks": [ + "TreeSet successful contains", + "Set successful contains", + ] + }, + { + "kind": "chart", + "title": "unsuccessful random lookups", + "tasks": [ + "TreeSet unsuccessful contains", + "Set unsuccessful contains", + ] + }, + { + "kind": "chart", + "title": "insert", + "tasks": [ + "TreeSet insert", + "Set insert", + "Set insert, reserving capacity", + ] + }, + { + "kind": "chart", + "title": "insert-shared", + "tasks": [ + "TreeSet insert, shared", + "Set insert, shared", + ] + }, + { + "kind": "chart", + "title": "model building with constant diffing", + "tasks": [ + "TreeSet model diffing", + "Set model diffing" + ] + }, + { + "kind": "chart", + "title": "remove", + "tasks": [ + "TreeSet remove", + "TreeSet remove, shared", + "Set remove", + "Set remove, shared", + ] + }, + { + "kind": "variants", + "charts": [ + { + "kind": "chart", + "title": "union (0% overlap)", + "tasks": [ + "TreeSet union with Self (0% overlap, distinct)", + "TreeSet union with Self (0% overlap, shared)", + "TreeSet union with Array (0% overlap)", + "Set union with Self (0% overlap)", + "Set union with Array (0% overlap)", + ] + }, + { + "kind": "chart", + "title": "union (25% overlap)", + "tasks": [ + "TreeSet union with Self (25% overlap, distinct)", + "TreeSet union with Self (25% overlap, shared)", + "TreeSet union with Array (25% overlap)", + "Set union with Self (25% overlap)", + "Set union with Array (25% overlap)", + ] + }, + { + "kind": "chart", + "title": "union (50% overlap)", + "tasks": [ + "TreeSet union with Self (50% overlap, distinct)", + "TreeSet union with Self (50% overlap, shared)", + "TreeSet union with Array (50% overlap)", + "Set union with Self (50% overlap)", + "Set union with Array (50% overlap)", + ] + }, + { + "kind": "chart", + "title": "union (75% overlap)", + "tasks": [ + "TreeSet union with Self (75% overlap, distinct)", + "TreeSet union with Self (75% overlap, shared)", + "TreeSet union with Array (75% overlap)", + "Set union with Self (75% overlap)", + "Set union with Array (75% overlap)", + ] + }, + { + "kind": "chart", + "title": "union (100% overlap)", + "tasks": [ + "TreeSet union with Self (100% overlap, distinct)", + "TreeSet union with Self (100% overlap, shared)", + "TreeSet union with Array (100% overlap)", + "Set union with Self (100% overlap)", + "Set union with Array (100% overlap)", + ] + }, + ] + }, + { + "kind": "variants", + "charts": [ + { + "kind": "chart", + "title": "formUnion (0% overlap)", + "tasks": [ + "TreeSet formUnion with Self (0% overlap, distinct)", + "TreeSet formUnion with Self (0% overlap, shared)", + "TreeSet formUnion with Array (0% overlap)", + "Set formUnion with Self (0% overlap)", + "Set formUnion with Array (0% overlap)", + ] + }, + { + "kind": "chart", + "title": "formUnion (25% overlap)", + "tasks": [ + "TreeSet formUnion with Self (25% overlap, distinct)", + "TreeSet formUnion with Self (25% overlap, shared)", + "TreeSet formUnion with Array (25% overlap)", + "Set formUnion with Self (25% overlap)", + "Set formUnion with Array (25% overlap)", + ] + }, + { + "kind": "chart", + "title": "formUnion (50% overlap)", + "tasks": [ + "TreeSet formUnion with Self (50% overlap, distinct)", + "TreeSet formUnion with Self (50% overlap, shared)", + "TreeSet formUnion with Array (50% overlap)", + "Set formUnion with Self (50% overlap)", + "Set formUnion with Array (50% overlap)", + ] + }, + { + "kind": "chart", + "title": "formUnion (75% overlap)", + "tasks": [ + "TreeSet formUnion with Self (75% overlap, distinct)", + "TreeSet formUnion with Self (75% overlap, shared)", + "TreeSet formUnion with Array (75% overlap)", + "Set formUnion with Self (75% overlap)", + "Set formUnion with Array (75% overlap)", + ] + }, + { + "kind": "chart", + "title": "formUnion (100% overlap)", + "tasks": [ + "TreeSet formUnion with Self (100% overlap, distinct)", + "TreeSet formUnion with Self (100% overlap, shared)", + "TreeSet formUnion with Array (100% overlap)", + "Set formUnion with Self (100% overlap)", + "Set formUnion with Array (100% overlap)", + ] + }, + ] + }, + { + "kind": "variants", + "charts": [ + { + "kind": "chart", + "title": "intersection (0% overlap)", + "tasks": [ + "TreeSet intersection with Self (0% overlap, distinct)", + "TreeSet intersection with Self (0% overlap, shared)", + "TreeSet intersection with Array (0% overlap)", + "Set intersection with Self (0% overlap)", + "Set intersection with Array (0% overlap)", + ] + }, + { + "kind": "chart", + "title": "intersection (25% overlap)", + "tasks": [ + "TreeSet intersection with Self (25% overlap, distinct)", + "TreeSet intersection with Self (25% overlap, shared)", + "TreeSet intersection with Array (25% overlap)", + "Set intersection with Self (25% overlap)", + "Set intersection with Array (25% overlap)", + ] + }, + { + "kind": "chart", + "title": "intersection (50% overlap)", + "tasks": [ + "TreeSet intersection with Self (50% overlap, distinct)", + "TreeSet intersection with Self (50% overlap, shared)", + "TreeSet intersection with Array (50% overlap)", + "Set intersection with Self (50% overlap)", + "Set intersection with Array (50% overlap)", + ] + }, + { + "kind": "chart", + "title": "intersection (75% overlap)", + "tasks": [ + "TreeSet intersection with Self (75% overlap, distinct)", + "TreeSet intersection with Self (75% overlap, shared)", + "TreeSet intersection with Array (75% overlap)", + "Set intersection with Self (75% overlap)", + "Set intersection with Array (75% overlap)", + ] + }, + { + "kind": "chart", + "title": "intersection (100% overlap)", + "tasks": [ + "TreeSet intersection with Self (100% overlap, distinct)", + "TreeSet intersection with Self (100% overlap, shared)", + "TreeSet intersection with Array (100% overlap)", + "Set intersection with Self (100% overlap)", + "Set intersection with Array (100% overlap)", + ] + }, + ] + }, + { + "kind": "variants", + "charts": [ + { + "kind": "chart", + "title": "formIntersection (0% overlap)", + "tasks": [ + "TreeSet formIntersection with Self (0% overlap, distinct)", + "TreeSet formIntersection with Self (0% overlap, shared)", + "TreeSet formIntersection with Array (0% overlap)", + "Set formIntersection with Self (0% overlap)", + "Set formIntersection with Array (0% overlap)", + ] + }, + { + "kind": "chart", + "title": "formIntersection (25% overlap)", + "tasks": [ + "TreeSet formIntersection with Self (25% overlap, distinct)", + "TreeSet formIntersection with Self (25% overlap, shared)", + "TreeSet formIntersection with Array (25% overlap)", + "Set formIntersection with Self (25% overlap)", + "Set formIntersection with Array (25% overlap)", + ] + }, + { + "kind": "chart", + "title": "formIntersection (50% overlap)", + "tasks": [ + "TreeSet formIntersection with Self (50% overlap, distinct)", + "TreeSet formIntersection with Self (50% overlap, shared)", + "TreeSet formIntersection with Array (50% overlap)", + "Set formIntersection with Self (50% overlap)", + "Set formIntersection with Array (50% overlap)", + ] + }, + { + "kind": "chart", + "title": "formIntersection (75% overlap)", + "tasks": [ + "TreeSet formIntersection with Self (75% overlap, distinct)", + "TreeSet formIntersection with Self (75% overlap, shared)", + "TreeSet formIntersection with Array (75% overlap)", + "Set formIntersection with Self (75% overlap)", + "Set formIntersection with Array (75% overlap)", + ] + }, + { + "kind": "chart", + "title": "formIntersection (100% overlap)", + "tasks": [ + "TreeSet formIntersection with Self (100% overlap, distinct)", + "TreeSet formIntersection with Self (100% overlap, shared)", + "TreeSet formIntersection with Array (100% overlap)", + "Set formIntersection with Self (100% overlap)", + "Set formIntersection with Array (100% overlap)", + ] + }, + ] + }, + { + "kind": "variants", + "charts": [ + { + "kind": "chart", + "title": "symmetricDifference (0% overlap)", + "tasks": [ + "TreeSet symmetricDifference with Self (0% overlap, distinct)", + "TreeSet symmetricDifference with Self (0% overlap, shared)", + "TreeSet symmetricDifference with Array (0% overlap)", + "Set symmetricDifference with Self (0% overlap)", + "Set symmetricDifference with Array (0% overlap)", + ] + }, + { + "kind": "chart", + "title": "symmetricDifference (25% overlap)", + "tasks": [ + "TreeSet symmetricDifference with Self (25% overlap, distinct)", + "TreeSet symmetricDifference with Self (25% overlap, shared)", + "TreeSet symmetricDifference with Array (25% overlap)", + "Set symmetricDifference with Self (25% overlap)", + "Set symmetricDifference with Array (25% overlap)", + ] + }, + { + "kind": "chart", + "title": "symmetricDifference (50% overlap)", + "tasks": [ + "TreeSet symmetricDifference with Self (50% overlap, distinct)", + "TreeSet symmetricDifference with Self (50% overlap, shared)", + "TreeSet symmetricDifference with Array (50% overlap)", + "Set symmetricDifference with Self (50% overlap)", + "Set symmetricDifference with Array (50% overlap)", + ] + }, + { + "kind": "chart", + "title": "symmetricDifference (75% overlap)", + "tasks": [ + "TreeSet symmetricDifference with Self (75% overlap, distinct)", + "TreeSet symmetricDifference with Self (75% overlap, shared)", + "TreeSet symmetricDifference with Array (75% overlap)", + "Set symmetricDifference with Self (75% overlap)", + "Set symmetricDifference with Array (75% overlap)", + ] + }, + { + "kind": "chart", + "title": "symmetricDifference (100% overlap)", + "tasks": [ + "TreeSet symmetricDifference with Self (100% overlap, distinct)", + "TreeSet symmetricDifference with Self (100% overlap, shared)", + "TreeSet symmetricDifference with Array (100% overlap)", + "Set symmetricDifference with Self (100% overlap)", + "Set symmetricDifference with Array (100% overlap)", + ] + }, + ] + }, + { + "kind": "variants", + "charts": [ + { + "kind": "chart", + "title": "formSymmetricDifference (0% overlap)", + "tasks": [ + "TreeSet formSymmetricDifference with Self (0% overlap, distinct)", + "TreeSet formSymmetricDifference with Self (0% overlap, shared)", + "TreeSet formSymmetricDifference with Array (0% overlap)", + "Set formSymmetricDifference with Self (0% overlap)", + "Set formSymmetricDifference with Array (0% overlap)", + ] + }, + { + "kind": "chart", + "title": "formSymmetricDifference (25% overlap)", + "tasks": [ + "TreeSet formSymmetricDifference with Self (25% overlap, distinct)", + "TreeSet formSymmetricDifference with Self (25% overlap, shared)", + "TreeSet formSymmetricDifference with Array (25% overlap)", + "Set formSymmetricDifference with Self (25% overlap)", + "Set formSymmetricDifference with Array (25% overlap)", + ] + }, + { + "kind": "chart", + "title": "formSymmetricDifference (50% overlap)", + "tasks": [ + "TreeSet formSymmetricDifference with Self (50% overlap, distinct)", + "TreeSet formSymmetricDifference with Self (50% overlap, shared)", + "TreeSet formSymmetricDifference with Array (50% overlap)", + "Set formSymmetricDifference with Self (50% overlap)", + "Set formSymmetricDifference with Array (50% overlap)", + ] + }, + { + "kind": "chart", + "title": "formSymmetricDifference (75% overlap)", + "tasks": [ + "TreeSet formSymmetricDifference with Self (75% overlap, distinct)", + "TreeSet formSymmetricDifference with Self (75% overlap, shared)", + "TreeSet formSymmetricDifference with Array (75% overlap)", + "Set formSymmetricDifference with Self (75% overlap)", + "Set formSymmetricDifference with Array (75% overlap)", + ] + }, + { + "kind": "chart", + "title": "formSymmetricDifference (100% overlap)", + "tasks": [ + "TreeSet formSymmetricDifference with Self (100% overlap, distinct)", + "TreeSet formSymmetricDifference with Self (100% overlap, shared)", + "TreeSet formSymmetricDifference with Array (100% overlap)", + "Set formSymmetricDifference with Self (100% overlap)", + "Set formSymmetricDifference with Array (100% overlap)", + ] + }, + ] + }, + { + "kind": "variants", + "charts": [ + { + "kind": "chart", + "title": "subtracting (0% overlap)", + "tasks": [ + "TreeSet subtracting Self (0% overlap, distinct)", + "TreeSet subtracting Self (0% overlap, shared)", + "TreeSet subtracting Array (0% overlap)", + "Set subtracting Self (0% overlap)", + "Set subtracting Array (0% overlap)", + ] + }, + { + "kind": "chart", + "title": "subtracting (25% overlap)", + "tasks": [ + "TreeSet subtracting Self (25% overlap, distinct)", + "TreeSet subtracting Self (25% overlap, shared)", + "TreeSet subtracting Array (25% overlap)", + "Set subtracting Self (25% overlap)", + "Set subtracting Array (25% overlap)", + ] + }, + { + "kind": "chart", + "title": "subtracting (50% overlap)", + "tasks": [ + "TreeSet subtracting Self (50% overlap, distinct)", + "TreeSet subtracting Self (50% overlap, shared)", + "TreeSet subtracting Array (50% overlap)", + "Set subtracting Self (50% overlap)", + "Set subtracting Array (50% overlap)", + ] + }, + { + "kind": "chart", + "title": "subtracting (75% overlap)", + "tasks": [ + "TreeSet subtracting Self (75% overlap, distinct)", + "TreeSet subtracting Self (75% overlap, shared)", + "TreeSet subtracting Array (75% overlap)", + "Set subtracting Self (75% overlap)", + "Set subtracting Array (75% overlap)", + ] + }, + { + "kind": "chart", + "title": "subtracting (100% overlap)", + "tasks": [ + "TreeSet subtracting Self (100% overlap, distinct)", + "TreeSet subtracting Self (100% overlap, shared)", + "TreeSet subtracting Array (100% overlap)", + "Set subtracting Self (100% overlap)", + "Set subtracting Array (100% overlap)", + ] + }, + ] + }, + { + "kind": "variants", + "charts": [ + { + "kind": "chart", + "title": "subtract (0% overlap)", + "tasks": [ + "TreeSet subtract Self (0% overlap, distinct)", + "TreeSet subtract Self (0% overlap, shared)", + "TreeSet subtract Array (0% overlap)", + "Set subtract Self (0% overlap)", + "Set subtract Array (0% overlap)", + ] + }, + { + "kind": "chart", + "title": "subtract (25% overlap)", + "tasks": [ + "TreeSet subtract Self (25% overlap, distinct)", + "TreeSet subtract Self (25% overlap, shared)", + "TreeSet subtract Array (25% overlap)", + "Set subtract Self (25% overlap)", + "Set subtract Array (25% overlap)", + ] + }, + { + "kind": "chart", + "title": "subtract (50% overlap)", + "tasks": [ + "TreeSet subtract Self (50% overlap, distinct)", + "TreeSet subtract Self (50% overlap, shared)", + "TreeSet subtract Array (50% overlap)", + "Set subtract Self (50% overlap)", + "Set subtract Array (50% overlap)", + ] + }, + { + "kind": "chart", + "title": "subtract (75% overlap)", + "tasks": [ + "TreeSet subtract Self (75% overlap, distinct)", + "TreeSet subtract Self (75% overlap, shared)", + "TreeSet subtract Array (75% overlap)", + "Set subtract Self (75% overlap)", + "Set subtract Array (75% overlap)", + ] + }, + { + "kind": "chart", + "title": "subtract (100% overlap)", + "tasks": [ + "TreeSet subtract Self (100% overlap, distinct)", + "TreeSet subtract Self (100% overlap, shared)", + "TreeSet subtract Array (100% overlap)", + "Set subtract Self (100% overlap)", + "Set subtract Array (100% overlap)", + ] + }, + ] + }, + ] + }, + ] +} diff --git a/Benchmarks/Package.resolved b/Benchmarks/Package.resolved index e881f916b..dfb6da19d 100644 --- a/Benchmarks/Package.resolved +++ b/Benchmarks/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/apple/swift-argument-parser", "state": { "branch": null, - "revision": "83b23d940471b313427da226196661856f6ba3e0", - "version": "0.4.4" + "revision": "9f39744e025c7d377987f30b03770805dcb0bcd1", + "version": "1.1.4" } }, { @@ -15,8 +15,8 @@ "repositoryURL": "https://github.com/apple/swift-collections-benchmark", "state": { "branch": null, - "revision": "665cbb154a55f45bcedd2ab4faf17b1b7ac06de3", - "version": "0.0.2" + "revision": "e8b88af0d678eacd65da84e99ccc1f0f402e9a97", + "version": "0.0.3" } }, { @@ -24,8 +24,8 @@ "repositoryURL": "https://github.com/apple/swift-system", "state": { "branch": null, - "revision": "2bc160bfe34d843ae5ff47168080add24dfd7eac", - "version": "0.0.2" + "revision": "025bcb1165deab2e20d4eaba79967ce73013f496", + "version": "1.2.1" } } ] diff --git a/Benchmarks/Package.swift b/Benchmarks/Package.swift index d7a7ec641..f72fad195 100644 --- a/Benchmarks/Package.swift +++ b/Benchmarks/Package.swift @@ -1,9 +1,9 @@ -// swift-tools-version:5.3 +// swift-tools-version:5.7 //===----------------------------------------------------------------------===// // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 @@ -14,9 +14,13 @@ import PackageDescription let package = Package( name: "swift-collections.Benchmarks", + products: [ + .executable(name: "benchmark", targets: ["benchmark"]), + .executable(name: "memory-benchmark", targets: ["memory-benchmark"]), + ], dependencies: [ .package(name: "swift-collections", path: ".."), - .package(url: "https://github.com/apple/swift-collections-benchmark", from: "0.0.1"), + .package(url: "https://github.com/apple/swift-collections-benchmark", from: "0.0.3"), ], targets: [ .target( @@ -25,23 +29,25 @@ let package = Package( .product(name: "Collections", package: "swift-collections"), .product(name: "CollectionsBenchmark", package: "swift-collections-benchmark"), "CppBenchmarks", - ], - path: "Benchmarks", - resources: [ - .copy("Library.json"), ] ), .target( - name: "CppBenchmarks", - path: "CppBenchmarks" + name: "CppBenchmarks" ), - .target( + .executableTarget( name: "benchmark", dependencies: [ "Benchmarks", ], - path: "benchmark-tool" + path: "Sources/benchmark-tool" + ), + .executableTarget( + name: "memory-benchmark", + dependencies: [ + .product(name: "Collections", package: "swift-collections"), + .product(name: "CollectionsBenchmark", package: "swift-collections-benchmark"), + ] ), ], - cxxLanguageStandard: .cxx1z + cxxLanguageStandard: .cxx17 ) diff --git a/Benchmarks/Benchmarks/ArrayBenchmarks.swift b/Benchmarks/Sources/Benchmarks/ArrayBenchmarks.swift similarity index 98% rename from Benchmarks/Benchmarks/ArrayBenchmarks.swift rename to Benchmarks/Sources/Benchmarks/ArrayBenchmarks.swift index 188013490..7f2d0fb64 100644 --- a/Benchmarks/Benchmarks/ArrayBenchmarks.swift +++ b/Benchmarks/Sources/Benchmarks/ArrayBenchmarks.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 diff --git a/Benchmarks/Sources/Benchmarks/BitsetBenchmarks.swift b/Benchmarks/Sources/Benchmarks/BitsetBenchmarks.swift new file mode 100644 index 000000000..81c064c86 --- /dev/null +++ b/Benchmarks/Sources/Benchmarks/BitsetBenchmarks.swift @@ -0,0 +1,357 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +import CollectionsBenchmark +import BitCollections + +extension Benchmark { + + public mutating func addBitSetBenchmarks() { + let fillRatios: [(String, (Int) -> Int)] = [ + ("0%", { c in 0 }), + ("0.01%", { c in c / 10_000 }), + ("0.1%", { c in c / 1_000 }), + ("1%", { c in c / 100 }), + ("10%", { c in c / 10 }), + ("25%", { c in c / 4 }), + ("50%", { c in c / 2 }), + ("75%", { c in 3 * c / 4 }), + ("100%", { c in c }), + ] + + for (percentage, count) in fillRatios { + self.add( + title: "BitSet iteration (\(percentage) filled)", + input: Int.self + ) { input in + guard input > 0 else { return nil } + + var set = BitSet(reservingCapacity: input) + for i in (0 ..< input).shuffled().prefix(count(input)) { + set.insert(i) + } + // Make sure the set actually fills its storage capacity. + set.insert(input - 1) + + return { timer in + var c = 0 + timer.measure { + for _ in set { + c += 1 + } + } + precondition(c == set.count) + } + } + } + + self.add( + title: "BitSet distance(from:to:)", + input: Int.self + ) { input in + let set = BitSet.random(upTo: input) + let c = set.count + return { timer in + let d = set.distance(from: set.startIndex, to: set.endIndex) + precondition(d == c) + } + } + + self.add( + title: "BitSet index(offsetBy:) +25% steps", + input: Int.self + ) { input in + let set = BitSet.random(upTo: input) + let c = set.count + return { timer in + var i = set.startIndex + var j = 0 + for step in 1 ... 4 { + let nextJ = step * c / 4 + i = set.index(i, offsetBy: nextJ - j) + j = nextJ + } + precondition(i == set.endIndex) + } + } + + self.add( + title: "BitSet index(offsetBy:) -25% steps", + input: Int.self + ) { input in + let set = BitSet.random(upTo: input) + let c = set.count + return { timer in + var i = set.endIndex + var j = c + for step in stride(from: 3, through: 0, by: -1) { + let nextJ = step * c / 4 + i = set.index(i, offsetBy: nextJ - j) + j = nextJ + } + precondition(i == set.startIndex) + } + } + + self.add( + title: "BitSet contains (within bounds)", + input: Int.self + ) { input in + let set = BitSet.random(upTo: input) + return { timer in + var c = 0 + timer.measure { + for value in 0 ..< input { + if set.contains(value) { c += 1 } + } + } + precondition(c == set.count) + } + } + + self.add( + title: "BitSet contains (out of bounds)", + input: Int.self + ) { input in + let set = BitSet.random(upTo: input) + return { timer in + for value in input ..< input * 2 { + precondition(!set.contains(value)) + } + } + } + + self.add( + title: "BitSet insert", + input: [Int].self + ) { input in + return { timer in + var set: BitSet = [] + for i in input { + precondition(set.insert(i).inserted) + } + } + } + + self.add( + title: "BitSet insert, reserving capacity", + input: [Int].self + ) { input in + return { timer in + var set: BitSet = [] + set.reserveCapacity(input.count) + for i in input { + precondition(set.insert(i).inserted) + } + } + } + + self.add( + title: "BitSet union with Self", + input: Int.self + ) { input in + let a = BitSet.random(upTo: input) + let b = BitSet.random(upTo: input) + return { timer in + blackHole(a.union(b)) + } + } + + self.add( + title: "BitSet union with Array", + input: Int.self + ) { input in + let a = BitSet.random(upTo: input) + let b = Array((0 ..< input).shuffled()[0 ..< input / 2]) + return { timer in + blackHole(a.union(b)) + } + } + + self.add( + title: "BitSet formUnion with Self", + input: Int.self + ) { input in + let b = BitSet.random(upTo: input) + return { timer in + var a = BitSet.random(upTo: input) + timer.measure { + a.formUnion(b) + } + blackHole(a) + } + } + + self.add( + title: "BitSet formUnion with Array", + input: Int.self + ) { input in + let b = Array((0 ..< input).shuffled()[0 ..< input / 2]) + return { timer in + var a = BitSet.random(upTo: input) + timer.measure { + a.formUnion(b) + } + blackHole(a) + } + } + + self.add( + title: "BitSet intersection with Self", + input: Int.self + ) { input in + let a = BitSet.random(upTo: input) + let b = BitSet.random(upTo: input) + return { timer in + blackHole(a.intersection(b)) + } + } + + self.add( + title: "BitSet intersection with Array", + input: Int.self + ) { input in + let a = BitSet.random(upTo: input) + let b = Array((0 ..< input).shuffled()[0 ..< input / 2]) + return { timer in + blackHole(a.intersection(b)) + } + } + + self.add( + title: "BitSet formIntersection with Self", + input: Int.self + ) { input in + let b = BitSet.random(upTo: input) + return { timer in + var a = BitSet.random(upTo: input) + timer.measure { + a.formIntersection(b) + } + blackHole(a) + } + } + + self.add( + title: "BitSet formIntersection with Array", + input: Int.self + ) { input in + let b = Array((0 ..< input).shuffled()[0 ..< input / 2]) + return { timer in + var a = BitSet.random(upTo: input) + timer.measure { + a.formIntersection(b) + } + blackHole(a) + } + } + + self.add( + title: "BitSet symmetricDifference with Self", + input: Int.self + ) { input in + let a = BitSet.random(upTo: input) + let b = BitSet.random(upTo: input) + return { timer in + blackHole(a.symmetricDifference(b)) + } + } + + self.add( + title: "BitSet symmetricDifference with Array", + input: Int.self + ) { input in + let a = BitSet.random(upTo: input) + let b = Array((0 ..< input).shuffled()[0 ..< input / 2]) + return { timer in + blackHole(a.symmetricDifference(b)) + } + } + + self.add( + title: "BitSet formSymmetricDifference with Self", + input: Int.self + ) { input in + let b = BitSet.random(upTo: input) + return { timer in + var a = BitSet.random(upTo: input) + timer.measure { + a.formSymmetricDifference(b) + } + blackHole(a) + } + } + + self.add( + title: "BitSet formSymmetricDifference with Array", + input: Int.self + ) { input in + let b = Array((0 ..< input).shuffled()[0 ..< input / 2]) + return { timer in + var a = BitSet.random(upTo: input) + timer.measure { + a.formSymmetricDifference(b) + } + blackHole(a) + } + } + + self.add( + title: "BitSet subtracting Self", + input: Int.self + ) { input in + let a = BitSet.random(upTo: input) + let b = BitSet.random(upTo: input) + return { timer in + blackHole(a.subtracting(b)) + } + } + + self.add( + title: "BitSet subtracting Array", + input: Int.self + ) { input in + let a = BitSet.random(upTo: input) + let b = Array((0 ..< input).shuffled()[0 ..< input / 2]) + return { timer in + blackHole(a.subtracting(b)) + } + } + + self.add( + title: "BitSet subtract Self", + input: Int.self + ) { input in + let b = BitSet.random(upTo: input) + return { timer in + var a = BitSet.random(upTo: input) + timer.measure { + a.subtract(b) + } + blackHole(a) + } + } + + self.add( + title: "BitSet subtract Array", + input: Int.self + ) { input in + let b = Array((0 ..< input).shuffled()[0 ..< input / 2]) + return { timer in + var a = BitSet.random(upTo: input) + timer.measure { + a.subtract(b) + } + blackHole(a) + } + } + } +} diff --git a/Benchmarks/Sources/Benchmarks/Cpp/CppBenchmarks.swift b/Benchmarks/Sources/Benchmarks/Cpp/CppBenchmarks.swift new file mode 100644 index 000000000..bd2fc70f4 --- /dev/null +++ b/Benchmarks/Sources/Benchmarks/Cpp/CppBenchmarks.swift @@ -0,0 +1,45 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +import CollectionsBenchmark +import CppBenchmarks + +extension Benchmark { + public mutating func addCppBenchmarks() { + cpp_set_hash_fn { value in value._rawHashValue(seed: 0) } + + self.addSimple( + title: "std::hash", + input: [Int].self + ) { input in + input.withUnsafeBufferPointer { buffer in + cpp_hash(buffer.baseAddress, buffer.count) + } + } + + self.addSimple( + title: "custom_intptr_hash (using Swift.Hasher)", + input: [Int].self + ) { input in + input.withUnsafeBufferPointer { buffer in + cpp_custom_hash(buffer.baseAddress, buffer.count) + } + } + + _addCppVectorBenchmarks() + _addCppDequeBenchmarks() + _addCppUnorderedSetBenchmarks() + _addCppUnorderedMapBenchmarks() + _addCppPriorityQueueBenchmarks() + _addCppVectorBoolBenchmarks() + _addCppMapBenchmarks() + } +} diff --git a/Benchmarks/Sources/Benchmarks/Cpp/CppDequeBenchmarks.swift b/Benchmarks/Sources/Benchmarks/Cpp/CppDequeBenchmarks.swift new file mode 100644 index 000000000..df25e0b41 --- /dev/null +++ b/Benchmarks/Sources/Benchmarks/Cpp/CppDequeBenchmarks.swift @@ -0,0 +1,170 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +import CollectionsBenchmark +import CppBenchmarks + +internal class CppDeque { + var ptr: UnsafeMutableRawPointer? + + init(_ input: [Int]) { + self.ptr = input.withUnsafeBufferPointer { buffer in + cpp_deque_create(buffer.baseAddress, buffer.count) + } + } + + deinit { + destroy() + } + + func destroy() { + if let ptr = ptr { + cpp_deque_destroy(ptr) + } + ptr = nil + } +} + +extension Benchmark { + internal mutating func _addCppDequeBenchmarks() { + self.addSimple( + title: "std::deque push_back from integer range", + input: Int.self + ) { count in + cpp_deque_from_int_range(count) + } + + self.addSimple( + title: "std::deque constructor from buffer", + input: [Int].self + ) { input in + input.withUnsafeBufferPointer { buffer in + cpp_deque_from_int_buffer(buffer.baseAddress, buffer.count) + } + } + + self.add( + title: "std::deque sequential iteration", + input: [Int].self + ) { input in + let deque = CppDeque(input) + return { timer in + cpp_deque_iterate(deque.ptr) + } + } + + self.add( + title: "std::deque random-access offset lookups (operator [])", + input: ([Int], [Int]).self + ) { input, lookups in + let vector = CppDeque(input) + return { timer in + lookups.withUnsafeBufferPointer { buffer in + cpp_deque_lookups_subscript(vector.ptr, buffer.baseAddress, buffer.count) + } + } + } + + self.add( + title: "std::deque at, random offsets", + input: ([Int], [Int]).self + ) { input, lookups in + let deque = CppDeque(input) + return { timer in + lookups.withUnsafeBufferPointer { buffer in + cpp_deque_lookups_at(deque.ptr, buffer.baseAddress, buffer.count) + } + } + } + + self.addSimple( + title: "std::deque push_back", + input: [Int].self + ) { input in + input.withUnsafeBufferPointer { buffer in + cpp_deque_append_integers(buffer.baseAddress, buffer.count) + } + } + + self.addSimple( + title: "std::deque push_front", + input: [Int].self + ) { input in + input.withUnsafeBufferPointer { buffer in + cpp_deque_prepend_integers(buffer.baseAddress, buffer.count) + } + } + + self.addSimple( + title: "std::deque random insertions", + input: Insertions.self + ) { insertions in + insertions.values.withUnsafeBufferPointer { buffer in + cpp_deque_random_insertions(buffer.baseAddress, buffer.count) + } + } + + self.add( + title: "std::deque pop_back", + input: Int.self + ) { size in + return { timer in + let deque = CppDeque(Array(0 ..< size)) + timer.measure { + cpp_deque_pop_back(deque.ptr) + } + deque.destroy() + } + } + + self.add( + title: "std::deque pop_front", + input: Int.self + ) { size in + return { timer in + let deque = CppDeque(Array(0 ..< size)) + timer.measure { + cpp_deque_pop_front(deque.ptr) + } + deque.destroy() + } + } + + self.add( + title: "std::deque random removals", + input: Insertions.self + ) { insertions in + let removals = Array(insertions.values.reversed()) + return { timer in + let deque = CppDeque(Array(0 ..< removals.count)) + timer.measure { + removals.withUnsafeBufferPointer { buffer in + cpp_deque_random_removals(deque.ptr, buffer.baseAddress, buffer.count) + } + } + deque.destroy() + } + } + + self.add( + title: "std::deque sort", + input: [Int].self + ) { input in + return { timer in + let deque = CppDeque(input) + timer.measure { + cpp_deque_sort(deque.ptr) + } + deque.destroy() + } + } + } +} diff --git a/Benchmarks/Sources/Benchmarks/Cpp/CppMapBenchmarks.swift b/Benchmarks/Sources/Benchmarks/Cpp/CppMapBenchmarks.swift new file mode 100644 index 000000000..b548c60d2 --- /dev/null +++ b/Benchmarks/Sources/Benchmarks/Cpp/CppMapBenchmarks.swift @@ -0,0 +1,59 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +import CollectionsBenchmark +import CppBenchmarks + +internal class CppMap { + var ptr: UnsafeMutableRawPointer? + + init(_ input: [Int]) { + self.ptr = input.withUnsafeBufferPointer { buffer in + cpp_map_create(buffer.baseAddress, buffer.count) + } + } + + deinit { + destroy() + } + + func destroy() { + if let ptr = ptr { + cpp_map_destroy(ptr) + } + ptr = nil + } +} + +extension Benchmark { + internal mutating func _addCppMapBenchmarks() { + self.addSimple( + title: "std::map insert", + input: [Int].self + ) { input in + input.withUnsafeBufferPointer { buffer in + cpp_map_insert_integers(buffer.baseAddress, buffer.count) + } + } + + self.add( + title: "std::map successful find", + input: ([Int], [Int]).self + ) { input, lookups in + let map = CppMap(input) + return { timer in + lookups.withUnsafeBufferPointer { buffer in + cpp_map_lookups(map.ptr, buffer.baseAddress, buffer.count) + } + } + } + } +} diff --git a/Benchmarks/Sources/Benchmarks/Cpp/CppPriorityQueueBenchmarks.swift b/Benchmarks/Sources/Benchmarks/Cpp/CppPriorityQueueBenchmarks.swift new file mode 100644 index 000000000..46ee34ae3 --- /dev/null +++ b/Benchmarks/Sources/Benchmarks/Cpp/CppPriorityQueueBenchmarks.swift @@ -0,0 +1,96 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +import CollectionsBenchmark +import CppBenchmarks + +internal class CppPriorityQueue { + var ptr: UnsafeMutableRawPointer? + + init(_ input: [Int]) { + self.ptr = input.withUnsafeBufferPointer { buffer in + cpp_priority_queue_create(buffer.baseAddress, buffer.count) + } + } + + convenience init() { + self.init([]) + } + + deinit { + destroy() + } + + func destroy() { + if let ptr = ptr { + cpp_priority_queue_destroy(ptr) + } + ptr = nil + } + + func push(_ value: Int) { + cpp_priority_queue_push(ptr, value) + } + + func push(_ values: [Int]) { + values.withUnsafeBufferPointer { buffer in + cpp_priority_queue_push_loop(ptr, buffer.baseAddress, buffer.count) + } + } + + func pop() -> Int { + cpp_priority_queue_pop(ptr) + } + + func popAll() { + cpp_priority_queue_pop_all(ptr) + } +} + +extension Benchmark { + internal mutating func _addCppPriorityQueueBenchmarks() { + self.addSimple( + title: "std::priority_queue construct from buffer", + input: [Int].self + ) { input in + let pq = CppPriorityQueue(input) + blackHole(pq) + } + + self.add( + title: "std::priority_queue push", + input: [Int].self + ) { input in + return { timer in + let pq = CppPriorityQueue() + timer.measure { + pq.push(input) + } + blackHole(pq) + pq.destroy() + } + } + + self.add( + title: "std::priority_queue pop", + input: [Int].self + ) { input in + return { timer in + let pq = CppPriorityQueue(input) + timer.measure { + pq.popAll() + } + blackHole(pq) + pq.destroy() + } + } + } +} diff --git a/Benchmarks/Sources/Benchmarks/Cpp/CppUnorderedMapBenchmarks.swift b/Benchmarks/Sources/Benchmarks/Cpp/CppUnorderedMapBenchmarks.swift new file mode 100644 index 000000000..36c8e8f64 --- /dev/null +++ b/Benchmarks/Sources/Benchmarks/Cpp/CppUnorderedMapBenchmarks.swift @@ -0,0 +1,153 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +import CollectionsBenchmark +import CppBenchmarks + +internal class CppUnorderedMap { + var ptr: UnsafeMutableRawPointer? + + init(_ input: [Int]) { + self.ptr = input.withUnsafeBufferPointer { buffer in + cpp_unordered_map_create(buffer.baseAddress, buffer.count) + } + } + + deinit { + destroy() + } + + func destroy() { + if let ptr = ptr { + cpp_unordered_map_destroy(ptr) + } + ptr = nil + } +} + +extension Benchmark { + internal mutating func _addCppUnorderedMapBenchmarks() { + self.addSimple( + title: "std::unordered_map insert from integer range", + input: Int.self + ) { count in + cpp_unordered_map_from_int_range(count) + } + + self.add( + title: "std::unordered_map sequential iteration", + input: [Int].self + ) { input in + let map = CppUnorderedMap(input) + return { timer in + cpp_unordered_map_iterate(map.ptr) + } + } + + self.add( + title: "std::unordered_map successful find", + input: ([Int], [Int]).self + ) { input, lookups in + let map = CppUnorderedMap(input) + return { timer in + lookups.withUnsafeBufferPointer { buffer in + cpp_unordered_map_lookups(map.ptr, buffer.baseAddress, buffer.count, true) + } + } + } + + self.add( + title: "std::unordered_map unsuccessful find", + input: ([Int], [Int]).self + ) { input, lookups in + let map = CppUnorderedMap(input) + let lookups = lookups.map { $0 + input.count } + return { timer in + lookups.withUnsafeBufferPointer { buffer in + cpp_unordered_map_lookups(map.ptr, buffer.baseAddress, buffer.count, false) + } + } + } + + self.add( + title: "std::unordered_map subscript, existing key", + input: ([Int], [Int]).self + ) { input, lookups in + let map = CppUnorderedMap(input) + return { timer in + lookups.withUnsafeBufferPointer { buffer in + cpp_unordered_map_subscript(map.ptr, buffer.baseAddress, buffer.count) + } + } + } + + self.add( + title: "std::unordered_map subscript, new key", + input: ([Int], [Int]).self + ) { input, lookups in + let map = CppUnorderedMap(input) + let lookups = lookups.map { $0 + input.count } + return { timer in + lookups.withUnsafeBufferPointer { buffer in + cpp_unordered_map_subscript(map.ptr, buffer.baseAddress, buffer.count) + } + } + } + + self.addSimple( + title: "std::unordered_map insert", + input: [Int].self + ) { input in + input.withUnsafeBufferPointer { buffer in + cpp_unordered_map_insert_integers(buffer.baseAddress, buffer.count, false) + } + } + + self.addSimple( + title: "std::unordered_map insert, reserving capacity", + input: [Int].self + ) { input in + input.withUnsafeBufferPointer { buffer in + cpp_unordered_map_insert_integers(buffer.baseAddress, buffer.count, true) + } + } + + self.add( + title: "std::unordered_map erase existing", + input: ([Int], [Int]).self + ) { input, removals in + return { timer in + let map = CppUnorderedMap(input) + timer.measure { + removals.withUnsafeBufferPointer { buffer in + cpp_unordered_map_removals(map.ptr, buffer.baseAddress, buffer.count) + } + } + map.destroy() + } + } + + self.add( + title: "std::unordered_map erase missing", + input: ([Int], [Int]).self + ) { input, removals in + return { timer in + let map = CppUnorderedMap(input.map { input.count + $0 }) + timer.measure { + removals.withUnsafeBufferPointer { buffer in + cpp_unordered_map_removals(map.ptr, buffer.baseAddress, buffer.count) + } + } + map.destroy() + } + } + } +} diff --git a/Benchmarks/Sources/Benchmarks/Cpp/CppUnorderedSetBenchmarks.swift b/Benchmarks/Sources/Benchmarks/Cpp/CppUnorderedSetBenchmarks.swift new file mode 100644 index 000000000..40ff6a908 --- /dev/null +++ b/Benchmarks/Sources/Benchmarks/Cpp/CppUnorderedSetBenchmarks.swift @@ -0,0 +1,122 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +import CollectionsBenchmark +import CppBenchmarks + +internal class CppUnorderedSet { + var ptr: UnsafeMutableRawPointer? + + init(_ input: [Int]) { + self.ptr = input.withUnsafeBufferPointer { buffer in + cpp_unordered_set_create(buffer.baseAddress, buffer.count) + } + } + + deinit { + destroy() + } + + func destroy() { + if let ptr = ptr { + cpp_unordered_set_destroy(ptr) + } + ptr = nil + } +} + +extension Benchmark { + internal mutating func _addCppUnorderedSetBenchmarks() { + self.addSimple( + title: "std::unordered_set insert from integer range", + input: Int.self + ) { count in + cpp_unordered_set_from_int_range(count) + } + + self.addSimple( + title: "std::unordered_set constructor from buffer", + input: [Int].self + ) { input in + input.withUnsafeBufferPointer { buffer in + cpp_unordered_set_from_int_buffer(buffer.baseAddress, buffer.count) + } + } + + self.add( + title: "std::unordered_set sequential iteration", + input: [Int].self + ) { input in + let set = CppUnorderedSet(input) + return { timer in + cpp_unordered_set_iterate(set.ptr) + } + } + + self.add( + title: "std::unordered_set successful find", + input: ([Int], [Int]).self + ) { input, lookups in + let set = CppUnorderedSet(input) + return { timer in + lookups.withUnsafeBufferPointer { buffer in + cpp_unordered_set_lookups(set.ptr, buffer.baseAddress, buffer.count, true) + } + } + } + + self.add( + title: "std::unordered_set unsuccessful find", + input: ([Int], [Int]).self + ) { input, lookups in + let set = CppUnorderedSet(input) + let lookups = lookups.map { $0 + input.count } + return { timer in + lookups.withUnsafeBufferPointer { buffer in + cpp_unordered_set_lookups(set.ptr, buffer.baseAddress, buffer.count, false) + } + } + } + + self.addSimple( + title: "std::unordered_set insert", + input: [Int].self + ) { input in + input.withUnsafeBufferPointer { buffer in + cpp_unordered_set_insert_integers(buffer.baseAddress, buffer.count, false) + } + } + + self.addSimple( + title: "std::unordered_set insert, reserving capacity", + input: [Int].self + ) { input in + input.withUnsafeBufferPointer { buffer in + cpp_unordered_set_insert_integers(buffer.baseAddress, buffer.count, true) + } + } + + self.add( + title: "std::unordered_set erase", + input: ([Int], [Int]).self + ) { input, removals in + return { timer in + let set = CppUnorderedSet(input) + timer.measure { + removals.withUnsafeBufferPointer { buffer in + cpp_unordered_set_removals(set.ptr, buffer.baseAddress, buffer.count) + } + } + set.destroy() + } + } + } +} diff --git a/Benchmarks/Sources/Benchmarks/Cpp/CppVectorBenchmarks.swift b/Benchmarks/Sources/Benchmarks/Cpp/CppVectorBenchmarks.swift new file mode 100644 index 000000000..ded888381 --- /dev/null +++ b/Benchmarks/Sources/Benchmarks/Cpp/CppVectorBenchmarks.swift @@ -0,0 +1,188 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +import CollectionsBenchmark +import CppBenchmarks + +internal class CppVector { + var ptr: UnsafeMutableRawPointer? + + init(_ input: [Int]) { + self.ptr = input.withUnsafeBufferPointer { buffer in + cpp_vector_create(buffer.baseAddress, buffer.count) + } + } + + deinit { + destroy() + } + + func destroy() { + if let ptr = ptr { + cpp_vector_destroy(ptr) + } + ptr = nil + } +} + +extension Benchmark { + internal mutating func _addCppVectorBenchmarks() { + self.addSimple( + title: "std::vector push_back from integer range", + input: Int.self + ) { count in + cpp_vector_from_int_range(count) + } + + self.addSimple( + title: "std::vector constructor from buffer", + input: [Int].self + ) { input in + input.withUnsafeBufferPointer { buffer in + cpp_vector_from_int_buffer(buffer.baseAddress, buffer.count) + } + } + + self.add( + title: "std::vector sequential iteration", + input: [Int].self + ) { input in + let vector = CppVector(input) + return { timer in + cpp_vector_iterate(vector.ptr) + } + } + + self.add( + title: "std::vector random-access offset lookups (operator [])", + input: ([Int], [Int]).self + ) { input, lookups in + let vector = CppVector(input) + return { timer in + lookups.withUnsafeBufferPointer { buffer in + cpp_vector_lookups_subscript(vector.ptr, buffer.baseAddress, buffer.count) + } + } + } + + self.add( + title: "std::vector random-access offset lookups (at)", + input: ([Int], [Int]).self + ) { input, lookups in + let deque = CppVector(input) + return { timer in + lookups.withUnsafeBufferPointer { buffer in + cpp_vector_lookups_at(deque.ptr, buffer.baseAddress, buffer.count) + } + } + } + + self.addSimple( + title: "std::vector push_back", + input: [Int].self + ) { input in + input.withUnsafeBufferPointer { buffer in + cpp_vector_append_integers(buffer.baseAddress, buffer.count, false) + } + } + + self.addSimple( + title: "std::vector push_back, reserving capacity", + input: [Int].self + ) { input in + input.withUnsafeBufferPointer { buffer in + cpp_vector_append_integers(buffer.baseAddress, buffer.count, true) + } + } + + self.addSimple( + title: "std::vector insert at front", + input: [Int].self + ) { input in + input.withUnsafeBufferPointer { buffer in + cpp_vector_prepend_integers(buffer.baseAddress, buffer.count, false) + } + } + + self.addSimple( + title: "std::vector insert at front, reserving capacity", + input: [Int].self + ) { input in + input.withUnsafeBufferPointer { buffer in + cpp_vector_prepend_integers(buffer.baseAddress, buffer.count, true) + } + } + + self.addSimple( + title: "std::vector random insertions", + input: Insertions.self + ) { insertions in + insertions.values.withUnsafeBufferPointer { buffer in + cpp_vector_random_insertions(buffer.baseAddress, buffer.count, false) + } + } + + self.add( + title: "std::vector pop_back", + input: Int.self + ) { size in + return { timer in + let vector = CppVector(Array(0 ..< size)) + timer.measure { + cpp_vector_pop_back(vector.ptr) + } + vector.destroy() + } + } + + self.add( + title: "std::vector erase first", + input: Int.self + ) { size in + return { timer in + let vector = CppVector(Array(0 ..< size)) + timer.measure { + cpp_vector_pop_front(vector.ptr) + } + vector.destroy() + } + } + + self.add( + title: "std::vector random removals", + input: Insertions.self + ) { insertions in + let removals = Array(insertions.values.reversed()) + return { timer in + let vector = CppVector(Array(0 ..< removals.count)) + timer.measure { + removals.withUnsafeBufferPointer { buffer in + cpp_vector_random_removals(vector.ptr, buffer.baseAddress, buffer.count) + } + } + vector.destroy() + } + } + + self.add( + title: "std::vector sort", + input: [Int].self + ) { input in + return { timer in + let vector = CppVector(input) + timer.measure { + cpp_vector_sort(vector.ptr) + } + vector.destroy() + } + } + } +} diff --git a/Benchmarks/Sources/Benchmarks/Cpp/CppVectorBoolBenchmarks.swift b/Benchmarks/Sources/Benchmarks/Cpp/CppVectorBoolBenchmarks.swift new file mode 100644 index 000000000..1b575cd2b --- /dev/null +++ b/Benchmarks/Sources/Benchmarks/Cpp/CppVectorBoolBenchmarks.swift @@ -0,0 +1,237 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +import CollectionsBenchmark +import CppBenchmarks + +internal class CppVectorBool { + var ptr: UnsafeMutableRawPointer? + + init(repeating value: Bool, count: Int) { + self.ptr = cpp_vector_bool_create_repeating(count, value) + } + + init(count: Int, trueBits: ArraySlice) { + self.ptr = cpp_vector_bool_create_repeating(count, false) + trueBits.withUnsafeBufferPointer { buffer in + cpp_vector_bool_set_indices_subscript( + self.ptr, + buffer.baseAddress, + buffer.count) + } + withExtendedLifetime(self) {} + } + + deinit { + destroy() + } + + func destroy() { + if let ptr = ptr { + cpp_vector_bool_destroy(ptr) + } + ptr = nil + } +} + +extension Array where Element == Bool { + init(count: Int, trueBits: S) where S.Element == Int { + self.init(repeating: false, count: count) + for index in trueBits { + self[index] = true + } + } +} + +extension Benchmark { + internal mutating func _addCppVectorBoolBenchmarks() { + self.addSimple( + title: "std::vector create from integer buffer (subscript)", + input: [Int].self + ) { input in + let trueCount = input.count / 2 + let v = CppVectorBool(repeating: false, count: input.count) + input.withUnsafeBufferPointer { buffer in + cpp_vector_bool_set_indices_subscript( + v.ptr, + buffer.baseAddress, + trueCount) + } + v.destroy() + } + + self.addSimple( + title: "std::vector create from integer buffer (at)", + input: [Int].self + ) { input in + let trueCount = input.count / 2 + let v = CppVectorBool(repeating: false, count: input.count) + input.withUnsafeBufferPointer { buffer in + cpp_vector_bool_set_indices_at( + v.ptr, + buffer.baseAddress, + trueCount) + } + v.destroy() + } + + self.add( + title: "std::vector const_iterator", + input: [Int].self + ) { input in + let trueCount = input.count / 2 + let v = CppVectorBool(count: input.count, trueBits: input[.. find true bits", + input: [Int].self + ) { input in + let trueCount = input.count / 2 + let v = CppVectorBool(count: input.count, trueBits: input[.. count true bits", + input: [Int].self + ) { input in + let trueCount = input.count / 2 + let v = CppVectorBool(count: input.count, trueBits: input[.. random-access offset lookups (subscript)", + input: ([Int], [Int]).self + ) { input, lookups in + let trueCount = input.count / 2 + let v = CppVectorBool(count: input.count, trueBits: input[.. random-access offset lookups (at)", + input: ([Int], [Int]).self + ) { input, lookups in + let trueCount = input.count / 2 + let v = CppVectorBool(count: input.count, trueBits: input[.. set bits to true (subscript)", + input: [Int].self + ) { input in + let v = CppVectorBool(repeating: false, count: input.count) + return { timer in + input.withUnsafeBufferPointer { buffer in + cpp_vector_bool_set_indices_subscript(v.ptr, buffer.baseAddress, buffer.count) + } + } + } + + self.add( + title: "std::vector set bits to true (at)", + input: [Int].self + ) { input in + let v = CppVectorBool(repeating: false, count: input.count) + return { timer in + input.withUnsafeBufferPointer { buffer in + cpp_vector_bool_set_indices_at(v.ptr, buffer.baseAddress, buffer.count) + } + } + } + + self.add( + title: "std::vector set bits to false (subscript)", + input: [Int].self + ) { input in + let v = CppVectorBool(repeating: true, count: input.count) + return { timer in + input.withUnsafeBufferPointer { buffer in + cpp_vector_bool_reset_indices_subscript(v.ptr, buffer.baseAddress, buffer.count) + } + } + } + + self.add( + title: "std::vector set bits to false (at)", + input: [Int].self + ) { input in + let v = CppVectorBool(repeating: true, count: input.count) + return { timer in + input.withUnsafeBufferPointer { buffer in + cpp_vector_bool_reset_indices_at(v.ptr, buffer.baseAddress, buffer.count) + } + } + } + + self.add( + title: "std::vector push_back", + input: [Int].self + ) { input in + let trueCount = input.count / 2 + let bools = Array(count: input.count, trueBits: input[.. push_back, reserving capacity", + input: [Int].self + ) { input in + let trueCount = input.count / 2 + let bools = Array(count: input.count, trueBits: input[.. pop_back", + input: [Int].self + ) { input in + return { timer in + let trueCount = input.count / 2 + let v = CppVectorBool(count: input.count, trueBits: input[.. Bool { + left.v0 == right.v0 + } + + func hash(into hasher: inout Hasher) { + hasher.combine(v0) + } +} + +extension Benchmark { + public mutating func registerCustomGenerators() { + self.registerInputGenerator(for: [String].self) { size in + (0 ..< size).map { String($0) }.shuffled() + } + + self.registerInputGenerator(for: ([String], [String]).self) { size in + let items = (0 ..< size).map { String($0) } + return (items.shuffled(), items.shuffled()) + } + + self.registerInputGenerator(for: [Large].self) { size in + (0 ..< size).map { Large($0) }.shuffled() + } + + self.registerInputGenerator(for: ([Large], [Large]).self) { size in + let items = (0 ..< size).map { Large($0) } + return (items.shuffled(), items.shuffled()) + } + } +} diff --git a/Benchmarks/Benchmarks/DequeBenchmarks.swift b/Benchmarks/Sources/Benchmarks/DequeBenchmarks.swift similarity index 93% rename from Benchmarks/Benchmarks/DequeBenchmarks.swift rename to Benchmarks/Sources/Benchmarks/DequeBenchmarks.swift index 39894e492..83e51af40 100644 --- a/Benchmarks/Benchmarks/DequeBenchmarks.swift +++ b/Benchmarks/Sources/Benchmarks/DequeBenchmarks.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 @@ -40,7 +40,7 @@ extension Benchmark { } self.add( - title: "Deque sequential iteration (contiguous)", + title: "Deque sequential iteration (contiguous, iterator)", input: [Int].self ) { input in let deque = Deque(input) @@ -52,7 +52,7 @@ extension Benchmark { } self.add( - title: "Deque sequential iteration (discontiguous)", + title: "Deque sequential iteration (discontiguous, iterator)", input: [Int].self ) { input in let deque = Deque(discontiguous: input) @@ -63,6 +63,30 @@ extension Benchmark { } } + self.add( + title: "Deque sequential iteration (contiguous, indices)", + input: [Int].self + ) { input in + let deque = Deque(input) + return { timer in + for i in deque.indices { + blackHole(deque[i]) + } + } + } + + self.add( + title: "Deque sequential iteration (discontiguous, indices)", + input: [Int].self + ) { input in + let deque = Deque(discontiguous: input) + return { timer in + for i in deque.indices { + blackHole(deque[i]) + } + } + } + self.add( title: "Deque subscript get, random offsets (contiguous)", input: ([Int], [Int]).self @@ -488,7 +512,7 @@ extension Benchmark { } self.add( - title: "Deque equality different instance", + title: "Deque equality, unique", input: Int.self ) { size in let left = Deque(0 ..< size) @@ -501,7 +525,7 @@ extension Benchmark { } self.add( - title: "Deque equality same instance", + title: "Deque equality, shared", input: Int.self ) { size in let left = Deque(0 ..< size) @@ -512,5 +536,6 @@ extension Benchmark { } } } + } } diff --git a/Benchmarks/Benchmarks/DictionaryBenchmarks.swift b/Benchmarks/Sources/Benchmarks/DictionaryBenchmarks.swift similarity index 76% rename from Benchmarks/Benchmarks/DictionaryBenchmarks.swift rename to Benchmarks/Sources/Benchmarks/DictionaryBenchmarks.swift index 8388fe610..61e60c17c 100644 --- a/Benchmarks/Benchmarks/DictionaryBenchmarks.swift +++ b/Benchmarks/Sources/Benchmarks/DictionaryBenchmarks.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 @@ -59,6 +59,31 @@ extension Benchmark { } } + self.add( + title: "Dictionary sequential iteration, indices", + input: [Int].self + ) { input in + let d = Dictionary(uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + return { timer in + for i in d.indices { + blackHole(d[i]) + } + } + } + + self.add( + title: "Dictionary indexing subscript", + input: ([Int], [Int]).self + ) { input, lookups in + let d = Dictionary(uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + let indices = lookups.map { d.index(forKey: $0)! } + return { timer in + for i in indices { + blackHole(d[i]) + } + } + } + self.add( title: "Dictionary subscript, successful lookups", input: ([Int], [Int]).self @@ -134,7 +159,7 @@ extension Benchmark { } self.addSimple( - title: "Dictionary subscript, insert", + title: "Dictionary subscript, insert, unique", input: [Int].self ) { input in var d: [Int: Int] = [:] @@ -145,6 +170,20 @@ extension Benchmark { blackHole(d) } + self.addSimple( + title: "Dictionary subscript, insert, shared", + input: [Int].self + ) { input in + var d: [Int: Int] = [:] + for i in input { + let copy = d + d[i] = 2 * i + blackHole((copy, d)) + } + precondition(d.count == input.count) + blackHole(d) + } + self.addSimple( title: "Dictionary subscript, insert, reserving capacity", input: [Int].self @@ -159,14 +198,32 @@ extension Benchmark { } self.add( - title: "Dictionary subscript, remove existing", + title: "Dictionary subscript, remove existing, unique", + input: ([Int], [Int]).self + ) { input, lookups in + return { timer in + var d = Dictionary(uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + timer.measure { + for i in lookups { + d[i] = nil + } + } + precondition(d.isEmpty) + blackHole(d) + } + } + + self.add( + title: "Dictionary subscript, remove existing, shared", input: ([Int], [Int]).self ) { input, lookups in return { timer in var d = Dictionary(uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) timer.measure { for i in lookups { + let copy = d d[i] = nil + blackHole((copy, d)) } } precondition(d.isEmpty) @@ -191,6 +248,19 @@ extension Benchmark { } } + self.add( + title: "Dictionary subscript(position:)", + input: ([Int], [Int]).self + ) { input, lookups in + let d = Dictionary(uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + let indices = lookups.map { d.index(forKey: $0)! } + return { timer in + for i in indices { + blackHole(d[i]) + } + } + } + self.add( title: "Dictionary defaulted subscript, successful lookups", input: ([Int], [Int]).self @@ -337,6 +407,34 @@ extension Benchmark { blackHole(d) } } - + + self.add( + title: "Dictionary equality, unique", + input: [Int].self + ) { input in + let keysAndValues = input.map { ($0, 2 * $0) } + let left = Dictionary(uniqueKeysWithValues: keysAndValues) + let right = Dictionary(uniqueKeysWithValues: keysAndValues) + return { timer in + timer.measure { + precondition(left == right) + } + } + } + + self.add( + title: "Dictionary equality, shared", + input: [Int].self + ) { input in + let keysAndValues = input.map { ($0, 2 * $0) } + let left = Dictionary(uniqueKeysWithValues: keysAndValues) + let right = left + return { timer in + timer.measure { + precondition(left == right) + } + } + } + } } diff --git a/Benchmarks/Sources/Benchmarks/Foundation/CFBinaryHeapBenchmarks.swift b/Benchmarks/Sources/Benchmarks/Foundation/CFBinaryHeapBenchmarks.swift new file mode 100644 index 000000000..f517b38c6 --- /dev/null +++ b/Benchmarks/Sources/Benchmarks/Foundation/CFBinaryHeapBenchmarks.swift @@ -0,0 +1,103 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +import CollectionsBenchmark + +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +import Foundation + +extension CFBinaryHeap { + internal static func _create(capacity: Int) -> CFBinaryHeap { + var callbacks = CFBinaryHeapCallBacks( + version: 0, + retain: nil, + release: nil, + copyDescription: { value in + let result = "\(Int(bitPattern: value))" as NSString + return Unmanaged.passRetained(result) + }, + compare: { left, right, context in + let left = Int(bitPattern: left) + let right = Int(bitPattern: right) + if left == right { return .compareEqualTo } + if left < right { return .compareLessThan } + return .compareGreaterThan + }) + return CFBinaryHeapCreate(kCFAllocatorDefault, capacity, &callbacks, nil) + } +} +#endif + +extension Benchmark { + internal mutating func _addCFBinaryHeapBenchmarks() { + self.add( + title: "CFBinaryHeapAddValue", + input: [Int].self + ) { input in +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) + return { timer in + let heap = CFBinaryHeap._create(capacity: 0) + timer.measure { + for value in input { + CFBinaryHeapAddValue(heap, UnsafeRawPointer(bitPattern: value)) + } + } + blackHole(heap) + } +#else + // CFBinaryHeap isn't available + return nil +#endif + } + + self.add( + title: "CFBinaryHeapAddValue, reserving capacity", + input: [Int].self + ) { input in +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) + return { timer in + let heap = CFBinaryHeap._create(capacity: input.count) + timer.measure { + for value in input { + CFBinaryHeapAddValue(heap, UnsafeRawPointer(bitPattern: value)) + } + } + blackHole(heap) + } +#else + return nil +#endif + } + + self.add( + title: "CFBinaryHeapRemoveMinimumValue", + input: [Int].self + ) { input in +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) + return { timer in + let heap = CFBinaryHeap._create(capacity: input.count) + for value in input { + CFBinaryHeapAddValue(heap, UnsafeRawPointer(bitPattern: value)) + } + timer.measure { + for _ in 0 ..< input.count { + blackHole(CFBinaryHeapGetMinimum(heap)) + CFBinaryHeapRemoveMinimumValue(heap) + } + } + blackHole(heap) + } +#else + return nil +#endif + } + } +} diff --git a/Benchmarks/Sources/Benchmarks/Foundation/CFBitVectorBenchmarks.swift b/Benchmarks/Sources/Benchmarks/Foundation/CFBitVectorBenchmarks.swift new file mode 100644 index 000000000..10766ddb6 --- /dev/null +++ b/Benchmarks/Sources/Benchmarks/Foundation/CFBitVectorBenchmarks.swift @@ -0,0 +1,161 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +import CollectionsBenchmark + +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +import Foundation + +extension CFMutableBitVector { + static func _create( + count: Int, + trueBits: S + ) -> CFMutableBitVector where S.Element == Int { + let bits = CFBitVectorCreateMutable(nil, count)! + CFBitVectorSetCount(bits, count) + for index in trueBits { + CFBitVectorSetBitAtIndex(bits, index, 1) + } + return bits + } +} +#endif + +extension Benchmark { + internal mutating func _addCFBitVectorBenchmarks() { + self.add( + title: "CFBitVector create from integer buffer", + input: [Int].self + ) { input in +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) + return { timer in + let trueCount = input.count / 2 + let bits = CFMutableBitVector._create( + count: input.count, + trueBits: input[.. ChartLibrary { - let url = Bundle.module.url(forResource: "Library", withExtension: "json")! - return try ChartLibrary.load(from: url) + public mutating func addFoundationBenchmarks() { + _addCFBinaryHeapBenchmarks() + _addCFBitVectorBenchmarks() } } diff --git a/Benchmarks/Sources/Benchmarks/HeapBenchmarks.swift b/Benchmarks/Sources/Benchmarks/HeapBenchmarks.swift new file mode 100644 index 000000000..0b273cfa0 --- /dev/null +++ b/Benchmarks/Sources/Benchmarks/HeapBenchmarks.swift @@ -0,0 +1,88 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +import CollectionsBenchmark +import HeapModule +import CppBenchmarks + +extension Benchmark { + public mutating func addHeapBenchmarks() { + self.addSimple( + title: "Heap init from range", + input: Int.self + ) { size in + blackHole(Heap(0.. init from buffer", + input: [Int].self + ) { input in + blackHole(Heap(input)) + } + + self.addSimple( + title: "Heap insert", + input: [Int].self + ) { input in + var queue = Heap() + for i in input { + queue.insert(i) + } + precondition(queue.count == input.count) + blackHole(queue) + } + + self.add( + title: "Heap insert(contentsOf:)", + input: ([Int], [Int]).self + ) { (existing, new) in + return { timer in + var queue = Heap(existing) + queue.insert(contentsOf: new) + precondition(queue.count == existing.count + new.count) + blackHole(queue) + } + } + + self.add( + title: "Heap popMax", + input: [Int].self + ) { input in + return { timer in + var queue = Heap(input) + timer.measure { + while let max = queue.popMax() { + blackHole(max) + } + } + precondition(queue.isEmpty) + blackHole(queue) + } + } + + self.add( + title: "Heap popMin", + input: [Int].self + ) { input in + return { timer in + var queue = Heap(input) + timer.measure { + while let min = queue.popMin() { + blackHole(min) + } + } + precondition(queue.isEmpty) + blackHole(queue) + } + } + } +} diff --git a/Benchmarks/Benchmarks/Kalimba.swift b/Benchmarks/Sources/Benchmarks/Kalimba.swift similarity index 95% rename from Benchmarks/Benchmarks/Kalimba.swift rename to Benchmarks/Sources/Benchmarks/Kalimba.swift index 5da2e92ce..70ce261dc 100644 --- a/Benchmarks/Benchmarks/Kalimba.swift +++ b/Benchmarks/Sources/Benchmarks/Kalimba.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 diff --git a/Benchmarks/Benchmarks/OrderedDictionaryBenchmarks.swift b/Benchmarks/Sources/Benchmarks/OrderedDictionaryBenchmarks.swift similarity index 87% rename from Benchmarks/Benchmarks/OrderedDictionaryBenchmarks.swift rename to Benchmarks/Sources/Benchmarks/OrderedDictionaryBenchmarks.swift index 636d70288..3a14bf96d 100644 --- a/Benchmarks/Benchmarks/OrderedDictionaryBenchmarks.swift +++ b/Benchmarks/Sources/Benchmarks/OrderedDictionaryBenchmarks.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 @@ -85,6 +85,31 @@ extension Benchmark { } } + self.add( + title: "OrderedDictionary sequential iteration, indices", + input: [Int].self + ) { input in + let d = OrderedDictionary(uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + return { timer in + for i in d.elements.indices { + blackHole(d.elements[i]) + } + } + } + + self.add( + title: "OrderedDictionary indexing subscript", + input: ([Int], [Int]).self + ) { input, lookups in + let d = OrderedDictionary(uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + let indices = lookups.map { d.index(forKey: $0)! } + return { timer in + for i in indices { + blackHole(d.elements[indices[i]]) // uses `elements` random-access collection view + } + } + } + self.add( title: "OrderedDictionary subscript, successful lookups", input: ([Int], [Int]).self @@ -165,12 +190,26 @@ extension Benchmark { } self.addSimple( - title: "OrderedDictionary subscript, append", + title: "OrderedDictionary subscript, append, unique", + input: [Int].self + ) { input in + var d: OrderedDictionary = [:] + for i in input { + d[i] = 2 * i + } + precondition(d.count == input.count) + blackHole(d) + } + + self.addSimple( + title: "OrderedDictionary subscript, append, shared", input: [Int].self ) { input in var d: OrderedDictionary = [:] for i in input { + let copy = d d[i] = 2 * i + blackHole((copy, d)) } precondition(d.count == input.count) blackHole(d) @@ -190,7 +229,7 @@ extension Benchmark { } self.add( - title: "OrderedDictionary subscript, remove existing", + title: "OrderedDictionary subscript, remove existing, unique", input: ([Int], [Int]).self ) { input, lookups in return { timer in @@ -206,6 +245,24 @@ extension Benchmark { } } + self.add( + title: "OrderedDictionary subscript, remove existing, shared", + input: ([Int], [Int]).self + ) { input, lookups in + return { timer in + var d = OrderedDictionary(uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + timer.measure { + for i in lookups { + let copy = d + d[i] = nil + blackHole((copy, d)) + } + } + precondition(d.isEmpty) + blackHole(d) + } + } + self.add( title: "OrderedDictionary subscript, remove missing", input: ([Int], [Int]).self @@ -484,7 +541,7 @@ extension Benchmark { } self.add( - title: "OrderedDictionary equality different instance", + title: "OrderedDictionary equality, unique", input: [Int].self ) { input in let keysAndValues = input.map { ($0, 2 * $0) } @@ -498,7 +555,7 @@ extension Benchmark { } self.add( - title: "OrderedDictionary equality same instance", + title: "OrderedDictionary equality, shared", input: [Int].self ) { input in let keysAndValues = input.map { ($0, 2 * $0) } @@ -512,7 +569,7 @@ extension Benchmark { } self.add( - title: "OrderedDictionary.Values equality different instance", + title: "OrderedDictionary.Values equality, unique", input: [Int].self ) { input in let keysAndValues = input.map { ($0, 2 * $0) } @@ -526,7 +583,7 @@ extension Benchmark { } self.add( - title: "OrderedDictionary.Values equality same instance", + title: "OrderedDictionary.Values equality, shared", input: [Int].self ) { input in let keysAndValues = input.map { ($0, 2 * $0) } @@ -538,5 +595,6 @@ extension Benchmark { } } } + } } diff --git a/Benchmarks/Benchmarks/OrderedSetBenchmarks.swift b/Benchmarks/Sources/Benchmarks/OrderedSetBenchmarks.swift similarity index 94% rename from Benchmarks/Benchmarks/OrderedSetBenchmarks.swift rename to Benchmarks/Sources/Benchmarks/OrderedSetBenchmarks.swift index a31ed314d..fa9cf15cf 100644 --- a/Benchmarks/Benchmarks/OrderedSetBenchmarks.swift +++ b/Benchmarks/Sources/Benchmarks/OrderedSetBenchmarks.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 @@ -269,6 +269,25 @@ extension Benchmark { } } + for percent in [0, 25, 50, 75, 100] { + self.add( + title: "OrderedSet filter keeping \(percent)%", + input: [Int].self + ) { input in + let set = OrderedSet(input) + return { timer in + var r: OrderedSet! + timer.measure { + r = set.filter { $0 % 100 < percent } + } + let div = input.count / 100 + let rem = input.count % 100 + precondition(r.count == percent * div + Swift.min(rem, percent)) + blackHole(r) + } + } + } + let overlaps: [(String, (Int) -> Int)] = [ ("0%", { c in c }), ("25%", { c in 3 * c / 4 }), @@ -538,7 +557,7 @@ extension Benchmark { } self.add( - title: "OrderedSet equality different instance", + title: "OrderedSet equality, unique", input: Int.self ) { size in return { timer in @@ -551,7 +570,7 @@ extension Benchmark { } self.add( - title: "OrderedSet equality same instance", + title: "OrderedSet equality, shared", input: Int.self ) { size in return { timer in @@ -564,7 +583,7 @@ extension Benchmark { } self.add( - title: "OrderedSet.SubSequence equality different instance", + title: "OrderedSet.SubSequence equality, unique", input: Int.self ) { size in return { timer in @@ -577,7 +596,7 @@ extension Benchmark { } self.add( - title: "OrderedSet.SubSequence equality same instance", + title: "OrderedSet.SubSequence equality, shared", input: Int.self ) { size in return { timer in diff --git a/Benchmarks/Benchmarks/SetBenchmarks.swift b/Benchmarks/Sources/Benchmarks/SetBenchmarks.swift similarity index 80% rename from Benchmarks/Benchmarks/SetBenchmarks.swift rename to Benchmarks/Sources/Benchmarks/SetBenchmarks.swift index 3a853b6ab..93389eb42 100644 --- a/Benchmarks/Benchmarks/SetBenchmarks.swift +++ b/Benchmarks/Sources/Benchmarks/SetBenchmarks.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 @@ -61,6 +61,18 @@ extension Benchmark { } } + self.add( + title: "Set sequential iteration, indices", + input: Int.self + ) { size in + let set = Set(0 ..< size) + return { timer in + for i in set.indices { + blackHole(set[i]) + } + } + } + self.add( title: "Set successful contains", input: ([Int], [Int]).self @@ -111,6 +123,59 @@ extension Benchmark { blackHole(set) } + self.addSimple( + title: "Set insert, shared", + input: [Int].self + ) { input in + var set: Set = [] + for i in input { + let copy = set + set.insert(i) + blackHole(copy) + } + precondition(set.count == input.count) + blackHole(set) + } + + self.add( + title: "Set insert one + subtract, shared", + input: [Int].self + ) { input in + let original = Set(input) + let newMember = input.count + return { timer in + var copy = original + copy.insert(newMember) + let diff = copy.subtracting(original) + precondition(diff.count == 1 && diff.first == newMember) + blackHole(copy) + } + } + + self.addSimple( + title: "Set model diffing", + input: Int.self + ) { input in + typealias Model = Set + + var _state: Model = [] // Private + func updateState( + with model: Model + ) -> (insertions: Model, removals: Model) { + let insertions = model.subtracting(_state) + let removals = _state.subtracting(model) + _state = model + return (insertions, removals) + } + + var model: Model = [] + for i in 0 ..< input { + model.insert(i) + let r = updateState(with: model) + precondition(r.insertions.count == 1 && r.removals.count == 0) + } + } + self.add( title: "Set remove", input: ([Int], [Int]).self @@ -125,6 +190,22 @@ extension Benchmark { } } + self.add( + title: "Set remove, shared", + input: ([Int], [Int]).self + ) { input, removals in + return { timer in + var set = Set(input) + for i in removals { + let copy = set + set.remove(i) + blackHole(copy) + } + precondition(set.isEmpty) + blackHole(set) + } + } + let overlaps: [(String, (Int) -> Int)] = [ ("0%", { c in c }), ("25%", { c in 3 * c / 4 }), @@ -392,6 +473,32 @@ extension Benchmark { } } } - + + self.add( + title: "Set equality, unique", + input: Int.self + ) { size in + return { timer in + let left = Set(0 ..< size) + let right = Set(0 ..< size) + timer.measure { + precondition(left == right) + } + } + } + + self.add( + title: "Set equality, shared", + input: Int.self + ) { size in + return { timer in + let left = Set(0 ..< size) + let right = left + timer.measure { + precondition(left == right) + } + } + } + } } diff --git a/Benchmarks/Sources/Benchmarks/ShareableDictionaryBenchmarks.swift b/Benchmarks/Sources/Benchmarks/ShareableDictionaryBenchmarks.swift new file mode 100644 index 000000000..1aa7e25a9 --- /dev/null +++ b/Benchmarks/Sources/Benchmarks/ShareableDictionaryBenchmarks.swift @@ -0,0 +1,532 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +import CollectionsBenchmark +import HashTreeCollections + +extension Benchmark { + public mutating func addTreeDictionaryBenchmarks() { + self.add( + title: "TreeDictionary init(uniqueKeysWithValues:)", + input: [Int].self + ) { input in + let keysAndValues = input.map { ($0, 2 * $0) } + return { timer in + blackHole(TreeDictionary(uniqueKeysWithValues: keysAndValues)) + } + } + + self.add( + title: "TreeDictionary sequential iteration", + input: [Int].self + ) { input in + let d = TreeDictionary(uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + return { timer in + for item in d { + blackHole(item) + } + } + } + + self.add( + title: "TreeDictionary.Keys sequential iteration", + input: [Int].self + ) { input in + let d = TreeDictionary(uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + return { timer in + for item in d.keys { + blackHole(item) + } + } + } + + self.add( + title: "TreeDictionary.Values sequential iteration", + input: [Int].self + ) { input in + let d = TreeDictionary(uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + return { timer in + for item in d.values { + blackHole(item) + } + } + } + + self.add( + title: "TreeDictionary sequential iteration, indices", + input: [Int].self + ) { input in + let d = TreeDictionary(uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + return { timer in + for i in d.indices { + blackHole(d[i]) + } + } + } + + self.add( + title: "TreeDictionary striding, 10 steps", + input: [Int].self + ) { input in + let d = TreeDictionary( + uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + let steps = stride(from: 0, through: 10 * d.count, by: d.count) + .map { $0 / 10 } + return { timer in + var i = d.startIndex + for j in 1 ..< steps.count { + let distance = steps[j] - steps[j - 1] + i = identity(d.index(i, offsetBy: distance)) + } + precondition(i == d.endIndex) + blackHole(i) + } + } + + self.add( + title: "TreeDictionary indexing subscript", + input: ([Int], [Int]).self + ) { input, lookups in + let d = TreeDictionary(uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + let indices = lookups.map { d.index(forKey: $0)! } + return { timer in + for i in indices { + blackHole(d[i]) + } + } + } + + self.add( + title: "TreeDictionary subscript, successful lookups", + input: ([Int], [Int]).self + ) { input, lookups in + let d = TreeDictionary(uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + return { timer in + for i in lookups { + precondition(d[i] == 2 * i) + } + } + } + + self.add( + title: "TreeDictionary subscript, unsuccessful lookups", + input: ([Int], [Int]).self + ) { input, lookups in + let d = TreeDictionary(uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + let c = input.count + return { timer in + for i in lookups { + precondition(d[i + c] == nil) + } + } + } + + self.add( + title: "TreeDictionary subscript, noop setter", + input: ([Int], [Int]).self + ) { input, lookups in + return { timer in + var d = TreeDictionary(uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + let c = input.count + timer.measure { + for i in lookups { + d[i + c] = nil + } + } + precondition(d.count == input.count) + blackHole(d) + } + } + + self.add( + title: "TreeDictionary subscript, set existing", + input: ([Int], [Int]).self + ) { input, lookups in + return { timer in + var d = TreeDictionary(uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + timer.measure { + for i in lookups { + d[i] = 0 + } + } + precondition(d.count == input.count) + blackHole(d) + } + } + + self.add( + title: "TreeDictionary subscript, _modify", + input: ([Int], [Int]).self + ) { input, lookups in + return { timer in + var d = TreeDictionary(uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + timer.measure { + for i in lookups { + d[i]! *= 2 + } + } + precondition(d.count == input.count) + blackHole(d) + } + } + + self.addSimple( + title: "TreeDictionary subscript, insert, unique", + input: [Int].self + ) { input in + var d: TreeDictionary = [:] + for i in input { + d[i] = 2 * i + } + precondition(d.count == input.count) + blackHole(d) + } + + self.addSimple( + title: "TreeDictionary subscript, insert, shared", + input: [Int].self + ) { input in + var d: TreeDictionary = [:] + for i in input { + let copy = d + d[i] = 2 * i + blackHole((copy, d)) + } + precondition(d.count == input.count) + blackHole(d) + } + + self.add( + title: "TreeDictionary subscript, remove existing, unique", + input: ([Int], [Int]).self + ) { input, lookups in + return { timer in + var d = TreeDictionary(uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + timer.measure { + for i in lookups { + d[i] = nil + } + } + precondition(d.isEmpty) + blackHole(d) + } + } + + self.add( + title: "TreeDictionary subscript, remove existing, shared", + input: ([Int], [Int]).self + ) { input, lookups in + return { timer in + var d = TreeDictionary(uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + timer.measure { + for i in lookups { + let copy = d + d[i] = nil + blackHole((copy, d)) + } + } + precondition(d.isEmpty) + blackHole(d) + } + } + + self.add( + title: "TreeDictionary subscript, remove missing", + input: ([Int], [Int]).self + ) { input, lookups in + return { timer in + var d = TreeDictionary(uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + let c = input.count + timer.measure { + for i in lookups { + d[i + c] = nil + } + } + precondition(d.count == input.count) + blackHole(d) + } + } + + self.add( + title: "TreeDictionary defaulted subscript, successful lookups", + input: ([Int], [Int]).self + ) { input, lookups in + let d = TreeDictionary(uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + return { timer in + for i in lookups { + precondition(d[i, default: -1] != -1) + } + } + } + + self.add( + title: "TreeDictionary defaulted subscript, unsuccessful lookups", + input: ([Int], [Int]).self + ) { input, lookups in + let d = TreeDictionary(uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + return { timer in + let c = d.count + for i in lookups { + precondition(d[i + c, default: -1] == -1) + } + } + } + + self.add( + title: "TreeDictionary defaulted subscript, _modify existing", + input: [Int].self + ) { input in + return { timer in + var d = TreeDictionary(uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + timer.measure { + for i in input { + d[i, default: -1] *= 2 + } + } + precondition(d.count == input.count) + blackHole(d) + } + } + + self.add( + title: "TreeDictionary defaulted subscript, _modify missing", + input: ([Int], [Int]).self + ) { input, lookups in + return { timer in + var d = TreeDictionary(uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + let c = input.count + timer.measure { + for i in lookups { + d[c + i, default: -1] *= 2 + } + } + precondition(d.count == 2 * input.count) + blackHole(d) + } + } + + self.add( + title: "TreeDictionary successful index(forKey:)", + input: ([Int], [Int]).self + ) { input, lookups in + let d = TreeDictionary(uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + return { timer in + for i in lookups { + precondition(d.index(forKey: i) != nil) + } + } + } + + self.add( + title: "TreeDictionary unsuccessful index(forKey:)", + input: ([Int], [Int]).self + ) { input, lookups in + let d = TreeDictionary(uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + return { timer in + for i in lookups { + precondition(d.index(forKey: lookups.count + i) == nil) + } + } + } + + self.add( + title: "TreeDictionary updateValue(_:forKey:), existing", + input: ([Int], [Int]).self + ) { input, lookups in + return { timer in + var d = TreeDictionary(uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + timer.measure { + for i in lookups { + d.updateValue(0, forKey: i) + } + } + precondition(d.count == input.count) + blackHole(d) + } + } + + self.add( + title: "TreeDictionary updateValue(_:forKey:), insert", + input: ([Int], [Int]).self + ) { input, lookups in + return { timer in + var d = TreeDictionary(uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + timer.measure { + for i in lookups { + d.updateValue(0, forKey: input.count + i) + } + } + precondition(d.count == 2 * input.count) + blackHole(d) + } + } + + self.add( + title: "TreeDictionary random removals (existing keys)", + input: ([Int], [Int]).self + ) { input, lookups in + return { timer in + var d = TreeDictionary(uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + timer.measure { + for i in lookups { + precondition(d.removeValue(forKey: i) != nil) + } + } + precondition(d.count == 0) + blackHole(d) + } + } + + self.add( + title: "TreeDictionary random removals (missing keys)", + input: ([Int], [Int]).self + ) { input, lookups in + return { timer in + let c = input.count + var d = TreeDictionary(uniqueKeysWithValues: input.lazy.map { (c + $0, 2 * $0) }) + timer.measure { + for i in lookups { + precondition(d.removeValue(forKey: i) == nil) + } + } + precondition(d.count == input.count) + blackHole(d) + } + } + + self.add( + title: "TreeDictionary subscript, insert, unique", + input: [Large].self + ) { input in + return { timer in + var d: TreeDictionary = [:] + timer.measure { + for value in input { + d[value] = value + } + } + precondition(d.count == input.count) + blackHole(d) + } + } + + self.add( + title: "TreeDictionary subscript, insert, shared", + input: [Large].self + ) { input in + return { timer in + var d: TreeDictionary = [:] + timer.measure { + for value in input { + let copy = d + d[value] = value + blackHole((copy, d)) + } + } + precondition(d.count == input.count) + blackHole(d) + } + } + + self.add( + title: "TreeDictionary subscript, remove existing, unique", + input: ([Large], [Large]).self + ) { input, lookups in + return { timer in + var d = TreeDictionary( + uniqueKeysWithValues: input.lazy.map { ($0, $0) }) + timer.measure { + for key in lookups { + d[key] = nil + } + } + precondition(d.isEmpty) + blackHole(d) + } + } + + self.add( + title: "TreeDictionary subscript, remove existing, shared", + input: ([Large], [Large]).self + ) { input, lookups in + return { timer in + var d = TreeDictionary( + uniqueKeysWithValues: input.lazy.map { ($0, $0) }) + timer.measure { + for key in lookups { + let copy = d + d[key] = nil + blackHole((copy, d)) + } + } + precondition(d.isEmpty) + blackHole(d) + } + } + + self.add( + title: "TreeDictionary equality, unique", + input: [Int].self + ) { input in + let keysAndValues = input.map { ($0, 2 * $0) } + let left = TreeDictionary(uniqueKeysWithValues: keysAndValues) + let right = TreeDictionary(uniqueKeysWithValues: keysAndValues) + return { timer in + timer.measure { + precondition(left == right) + } + } + } + + self.add( + title: "TreeDictionary equality, shared", + input: [Int].self + ) { input in + let keysAndValues = input.map { ($0, 2 * $0) } + let left = TreeDictionary(uniqueKeysWithValues: keysAndValues) + let right = left + return { timer in + timer.measure { + precondition(left == right) + } + } + } + + self.add( + title: "TreeDictionary.Keys equality, unique", + input: [Int].self + ) { input in + let keysAndValues = input.map { ($0, 2 * $0) } + let left = TreeDictionary(uniqueKeysWithValues: keysAndValues) + let right = TreeDictionary(uniqueKeysWithValues: keysAndValues) + return { timer in + timer.measure { + precondition(left.keys == right.keys) + } + } + } + + self.add( + title: "TreeDictionary.Keys equality, shared", + input: [Int].self + ) { input in + let keysAndValues = input.map { ($0, 2 * $0) } + let left = TreeDictionary(uniqueKeysWithValues: keysAndValues) + let right = left + return { timer in + timer.measure { + precondition(left.keys == right.keys) + } + } + } + + } +} diff --git a/Benchmarks/Sources/Benchmarks/ShareableSetBenchmarks.swift b/Benchmarks/Sources/Benchmarks/ShareableSetBenchmarks.swift new file mode 100644 index 000000000..9807c9975 --- /dev/null +++ b/Benchmarks/Sources/Benchmarks/ShareableSetBenchmarks.swift @@ -0,0 +1,477 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +import CollectionsBenchmark +import HashTreeCollections + +extension Benchmark { + public mutating func addTreeSetBenchmarks() { + self.addSimple( + title: "TreeSet init from range", + input: Int.self + ) { size in + blackHole(TreeSet(0 ..< size)) + } + + self.addSimple( + title: "TreeSet init from unsafe buffer", + input: [Int].self + ) { input in + input.withUnsafeBufferPointer { buffer in + blackHole(TreeSet(buffer)) + } + } + + self.add( + title: "TreeSet sequential iteration", + input: Int.self + ) { size in + let set = TreeSet(0 ..< size) + return { timer in + for i in set { + blackHole(i) + } + } + } + + self.add( + title: "TreeSet sequential iteration, indices", + input: Int.self + ) { size in + let set = TreeSet(0 ..< size) + return { timer in + for i in set.indices { + blackHole(set[i]) + } + } + } + + self.add( + title: "TreeSet successful contains", + input: ([Int], [Int]).self + ) { input, lookups in + let set = TreeSet(input) + return { timer in + for i in lookups { + precondition(set.contains(i)) + } + } + } + + self.add( + title: "TreeSet unsuccessful contains", + input: ([Int], [Int]).self + ) { input, lookups in + let set = TreeSet(input) + let lookups = lookups.map { $0 + input.count } + return { timer in + for i in lookups { + precondition(!set.contains(i)) + } + } + } + + self.addSimple( + title: "TreeSet insert", + input: [Int].self + ) { input in + var set: TreeSet = [] + for i in input { + set.insert(i) + } + precondition(set.count == input.count) + blackHole(set) + } + + self.addSimple( + title: "TreeSet insert, shared", + input: [Int].self + ) { input in + var set: TreeSet = [] + for i in input { + let copy = set + set.insert(i) + blackHole(copy) + } + precondition(set.count == input.count) + blackHole(set) + } + + self.addSimple( + title: "TreeSet model diffing", + input: Int.self + ) { input in + typealias Model = TreeSet + + var _state: Model = [] // Private + func updateState( + with model: Model + ) -> (insertions: Model, removals: Model) { + let insertions = model.subtracting(_state) + let removals = _state.subtracting(model) + _state = model + return (insertions, removals) + } + + var model: Model = [] + for i in 0 ..< input { + model.insert(i) + let r = updateState(with: model) + precondition(r.insertions.count == 1 && r.removals.count == 0) + } + } + + self.add( + title: "TreeSet remove", + input: ([Int], [Int]).self + ) { input, removals in + return { timer in + var set = TreeSet(input) + for i in removals { + set.remove(i) + } + precondition(set.isEmpty) + blackHole(set) + } + } + + self.add( + title: "TreeSet remove, shared", + input: ([Int], [Int]).self + ) { input, removals in + return { timer in + var set = TreeSet(input) + for i in removals { + let copy = set + set.remove(i) + blackHole(copy) + } + precondition(set.isEmpty) + blackHole(set) + } + } + + let overlaps: [(String, (Int) -> Int)] = [ + ("0%", { c in c }), + ("25%", { c in 3 * c / 4 }), + ("50%", { c in c / 2 }), + ("75%", { c in c / 4 }), + ("100%", { c in 0 }), + ] + + // SetAlgebra operations with Self + do { + func makeB( + _ a: TreeSet, _ range: Range, shared: Bool + ) -> TreeSet { + guard shared else { + return TreeSet(range) + } + var b = a + b.subtract(0 ..< range.lowerBound) + b.formUnion(range) + return b + } + + for (percentage, start) in overlaps { + for shared in [false, true] { + let qualifier = "\(percentage) overlap, \(shared ? "shared" : "distinct")" + + self.add( + title: "TreeSet union with Self (\(qualifier))", + input: [Int].self + ) { input in + let start = start(input.count) + let a = TreeSet(input) + let b = makeB(a, start ..< start + input.count, shared: shared) + return { timer in + blackHole(a.union(identity(b))) + } + } + + self.add( + title: "TreeSet intersection with Self (\(qualifier))", + input: [Int].self + ) { input in + let start = start(input.count) + let a = TreeSet(input) + let b = makeB(a, start ..< start + input.count, shared: shared) + return { timer in + blackHole(a.intersection(identity(b))) + } + } + + self.add( + title: "TreeSet symmetricDifference with Self (\(qualifier))", + input: [Int].self + ) { input in + let start = start(input.count) + let a = TreeSet(input) + let b = makeB(a, start ..< start + input.count, shared: shared) + return { timer in + blackHole(a.symmetricDifference(identity(b))) + } + } + + self.add( + title: "TreeSet subtracting Self (\(qualifier))", + input: [Int].self + ) { input in + let start = start(input.count) + let a = TreeSet(input) + let b = makeB(a, start ..< start + input.count, shared: shared) + return { timer in + blackHole(a.subtracting(identity(b))) + } + } + } + } + } + + // SetAlgebra operations with Array + do { + for (percentage, start) in overlaps { + self.add( + title: "TreeSet union with Array (\(percentage) overlap)", + input: [Int].self + ) { input in + let start = start(input.count) + let a = TreeSet(input) + let b = Array(start ..< start + input.count) + return { timer in + blackHole(a.union(identity(b))) + } + } + } + + for (percentage, start) in overlaps { + self.add( + title: "TreeSet intersection with Array (\(percentage) overlap)", + input: [Int].self + ) { input in + let start = start(input.count) + let a = TreeSet(input) + let b = Array(start ..< start + input.count) + return { timer in + blackHole(a.intersection(identity(b))) + } + } + } + + for (percentage, start) in overlaps { + self.add( + title: "TreeSet symmetricDifference with Array (\(percentage) overlap)", + input: [Int].self + ) { input in + let start = start(input.count) + let a = TreeSet(input) + let b = Array(start ..< start + input.count) + return { timer in + blackHole(a.symmetricDifference(identity(b))) + } + } + } + + for (percentage, start) in overlaps { + self.add( + title: "TreeSet subtracting Array (\(percentage) overlap)", + input: [Int].self + ) { input in + let start = start(input.count) + let a = TreeSet(input) + let b = Array(start ..< start + input.count) + return { timer in + blackHole(a.subtracting(identity(b))) + } + } + } + } + + // SetAlgebra mutations with Self + do { + func makeB( + _ a: TreeSet, _ range: Range, shared: Bool + ) -> TreeSet { + guard shared else { + return TreeSet(range) + } + var b = a + b.subtract(0 ..< range.lowerBound) + b.formUnion(range) + return b + } + + for (percentage, start) in overlaps { + for shared in [false, true] { + let qualifier = "\(percentage) overlap, \(shared ? "shared" : "distinct")" + + self.add( + title: "TreeSet formUnion with Self (\(qualifier))", + input: [Int].self + ) { input in + let start = start(input.count) + return { timer in + var a = TreeSet(input) + let b = makeB(a, start ..< start + input.count, shared: shared) + timer.measure { + a.formUnion(identity(b)) + } + blackHole(a) + } + } + + self.add( + title: "TreeSet formIntersection with Self (\(qualifier))", + input: [Int].self + ) { input in + let start = start(input.count) + return { timer in + var a = TreeSet(input) + let b = makeB(a, start ..< start + input.count, shared: shared) + timer.measure { + a.formIntersection(identity(b)) + } + blackHole(a) + } + } + + self.add( + title: "TreeSet formSymmetricDifference with Self (\(qualifier))", + input: [Int].self + ) { input in + let start = start(input.count) + return { timer in + var a = TreeSet(input) + let b = makeB(a, start ..< start + input.count, shared: shared) + timer.measure { + a.formSymmetricDifference(identity(b)) + } + blackHole(a) + } + } + + self.add( + title: "TreeSet subtract Self (\(qualifier))", + input: [Int].self + ) { input in + let start = start(input.count) + return { timer in + var a = TreeSet(input) + let b = makeB(a, start ..< start + input.count, shared: shared) + timer.measure { + a.subtract(identity(b)) + } + blackHole(a) + } + } + } + } + } + + // SetAlgebra mutations with Array + do { + for (percentage, start) in overlaps { + self.add( + title: "TreeSet formUnion with Array (\(percentage) overlap)", + input: [Int].self + ) { input in + let start = start(input.count) + let b = Array(start ..< start + input.count) + return { timer in + var a = TreeSet(input) + timer.measure { + a.formUnion(identity(b)) + } + blackHole(a) + } + } + } + + for (percentage, start) in overlaps { + self.add( + title: "TreeSet formIntersection with Array (\(percentage) overlap)", + input: [Int].self + ) { input in + let start = start(input.count) + let b = Array(start ..< start + input.count) + return { timer in + var a = TreeSet(input) + timer.measure { + a.formIntersection(identity(b)) + } + blackHole(a) + } + } + } + + for (percentage, start) in overlaps { + self.add( + title: "TreeSet formSymmetricDifference with Array (\(percentage) overlap)", + input: [Int].self + ) { input in + let start = start(input.count) + let b = Array(start ..< start + input.count) + return { timer in + var a = TreeSet(input) + timer.measure { + a.formSymmetricDifference(identity(b)) + } + blackHole(a) + } + } + } + + for (percentage, start) in overlaps { + self.add( + title: "TreeSet subtract Array (\(percentage) overlap)", + input: [Int].self + ) { input in + let start = start(input.count) + let b = Array(start ..< start + input.count) + return { timer in + var a = TreeSet(input) + timer.measure { + a.subtract(identity(b)) + } + blackHole(a) + } + } + } + } + + self.add( + title: "TreeSet equality, unique", + input: Int.self + ) { size in + return { timer in + let left = TreeSet(0 ..< size) + let right = TreeSet(0 ..< size) + timer.measure { + precondition(left == right) + } + } + } + + self.add( + title: "TreeSet equality, shared", + input: Int.self + ) { size in + return { timer in + let left = TreeSet(0 ..< size) + let right = left + timer.measure { + precondition(left == right) + } + } + } + + } +} diff --git a/Benchmarks/CppBenchmarks/include/DequeBenchmarks.h b/Benchmarks/Sources/CppBenchmarks/include/DequeBenchmarks.h similarity index 96% rename from Benchmarks/CppBenchmarks/include/DequeBenchmarks.h rename to Benchmarks/Sources/CppBenchmarks/include/DequeBenchmarks.h index 1aaae6c59..b5419e5b0 100644 --- a/Benchmarks/CppBenchmarks/include/DequeBenchmarks.h +++ b/Benchmarks/Sources/CppBenchmarks/include/DequeBenchmarks.h @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 diff --git a/Benchmarks/CppBenchmarks/include/Hashing.h b/Benchmarks/Sources/CppBenchmarks/include/Hashing.h similarity index 91% rename from Benchmarks/CppBenchmarks/include/Hashing.h rename to Benchmarks/Sources/CppBenchmarks/include/Hashing.h index 5d85e7b7b..a9e11434a 100644 --- a/Benchmarks/CppBenchmarks/include/Hashing.h +++ b/Benchmarks/Sources/CppBenchmarks/include/Hashing.h @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 diff --git a/Benchmarks/Sources/CppBenchmarks/include/MapBenchmarks.h b/Benchmarks/Sources/CppBenchmarks/include/MapBenchmarks.h new file mode 100644 index 000000000..6579a39f7 --- /dev/null +++ b/Benchmarks/Sources/CppBenchmarks/include/MapBenchmarks.h @@ -0,0 +1,40 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#ifndef CPPBENCHMARKS_MAP_BENCHMARKS_H +#define CPPBENCHMARKS_MAP_BENCHMARKS_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/// Create a std::map, populating it with data from the supplied buffer. +/// Returns an opaque pointer to the created instance. +extern void *cpp_map_create(const intptr_t *start, size_t count); + +/// Destroys an ordered map previously returned by `cpp_map_create`. +extern void cpp_map_destroy(void *ptr); + +extern void cpp_map_insert_integers(const intptr_t *start, size_t count); + +extern void cpp_map_lookups(void *ptr, const intptr_t *start, size_t count); +extern void cpp_map_subscript(void *ptr, const intptr_t *start, size_t count); + +#ifdef __cplusplus +} +#endif + + +#endif /* CPPBENCHMARKS_MAP_BENCHMARKS_H */ diff --git a/Benchmarks/Sources/CppBenchmarks/include/PriorityQueueBenchmarks.h b/Benchmarks/Sources/CppBenchmarks/include/PriorityQueueBenchmarks.h new file mode 100644 index 000000000..dc1274602 --- /dev/null +++ b/Benchmarks/Sources/CppBenchmarks/include/PriorityQueueBenchmarks.h @@ -0,0 +1,47 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#ifndef CPPBENCHMARKS_PRIORITY_QUEUE_BENCHMARKS_H +#define CPPBENCHMARKS_PRIORITY_QUEUE_BENCHMARKS_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/// Create a `std::priority_queue`, populating it with data from the +/// supplied buffer. Returns an opaque pointer to the created instance. +extern void *cpp_priority_queue_create(const intptr_t *start, size_t count); + +/// Destroys a priority queue previously returned by `cpp_priority_queue_create`. +extern void cpp_priority_queue_destroy(void *ptr); + +/// Push a value to a priority queue. +extern void cpp_priority_queue_push(void *ptr, intptr_t value); + +/// Loop through the supplied buffer, pushing each value to the queue. +extern void cpp_priority_queue_push_loop(void *ptr, const intptr_t *start, size_t count); + +/// Remove and return the top value off of a priority queue. +extern intptr_t cpp_priority_queue_pop(void *ptr); + +/// Remove and discard all values in a priority queue one by one in a loop. +extern void cpp_priority_queue_pop_all(void *ptr); + +#ifdef __cplusplus +} +#endif + + +#endif /* CPPBENCHMARKS_PRIORITY_QUEUE_BENCHMARKS_H */ diff --git a/Benchmarks/CppBenchmarks/include/UnorderedMapBenchmarks.h b/Benchmarks/Sources/CppBenchmarks/include/UnorderedMapBenchmarks.h similarity index 95% rename from Benchmarks/CppBenchmarks/include/UnorderedMapBenchmarks.h rename to Benchmarks/Sources/CppBenchmarks/include/UnorderedMapBenchmarks.h index e368f79fa..0f16d14c8 100644 --- a/Benchmarks/CppBenchmarks/include/UnorderedMapBenchmarks.h +++ b/Benchmarks/Sources/CppBenchmarks/include/UnorderedMapBenchmarks.h @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 diff --git a/Benchmarks/CppBenchmarks/include/UnorderedSetBenchmarks.h b/Benchmarks/Sources/CppBenchmarks/include/UnorderedSetBenchmarks.h similarity index 95% rename from Benchmarks/CppBenchmarks/include/UnorderedSetBenchmarks.h rename to Benchmarks/Sources/CppBenchmarks/include/UnorderedSetBenchmarks.h index fb627d447..d635f8417 100644 --- a/Benchmarks/CppBenchmarks/include/UnorderedSetBenchmarks.h +++ b/Benchmarks/Sources/CppBenchmarks/include/UnorderedSetBenchmarks.h @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 diff --git a/Benchmarks/CppBenchmarks/src/utils.h b/Benchmarks/Sources/CppBenchmarks/include/Utils.h similarity index 89% rename from Benchmarks/CppBenchmarks/src/utils.h rename to Benchmarks/Sources/CppBenchmarks/include/Utils.h index 9109a0936..07a90f302 100644 --- a/Benchmarks/CppBenchmarks/src/utils.h +++ b/Benchmarks/Sources/CppBenchmarks/include/Utils.h @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 @@ -14,6 +14,8 @@ #include +#ifdef __cplusplus + // FIXME: Is putting this in a separate compilation unit enough to make // sure the function call is always emitted? @@ -35,4 +37,6 @@ static inline const T *identity(const T *value) { return static_cast(_identity(value)); } + +#endif /* __cplusplus */ #endif /* BLACK_HOLE_H */ diff --git a/Benchmarks/CppBenchmarks/include/VectorBenchmarks.h b/Benchmarks/Sources/CppBenchmarks/include/VectorBenchmarks.h similarity index 96% rename from Benchmarks/CppBenchmarks/include/VectorBenchmarks.h rename to Benchmarks/Sources/CppBenchmarks/include/VectorBenchmarks.h index 6a4a99b1f..143c450a3 100644 --- a/Benchmarks/CppBenchmarks/include/VectorBenchmarks.h +++ b/Benchmarks/Sources/CppBenchmarks/include/VectorBenchmarks.h @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 diff --git a/Benchmarks/Sources/CppBenchmarks/include/VectorBoolBenchmarks.h b/Benchmarks/Sources/CppBenchmarks/include/VectorBoolBenchmarks.h new file mode 100644 index 000000000..371579967 --- /dev/null +++ b/Benchmarks/Sources/CppBenchmarks/include/VectorBoolBenchmarks.h @@ -0,0 +1,67 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#ifndef CPPBENCHMARKS_VECTOR_BOOL_BENCHMARKS_H +#define CPPBENCHMARKS_VECTOR_BOOL_BENCHMARKS_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/// Create a `std::vector` of the specified size, filling it with the +/// given value. Returns an opaque pointer to the created instance. +extern void *cpp_vector_bool_create_repeating(size_t count, bool value); + +/// Destroy a vector previously returned by `cpp_vector_bool_create`. +extern void cpp_vector_bool_destroy(void *ptr); + +extern void cpp_vector_bool_push_back(const bool *start, size_t count, bool reserve); + +extern void cpp_vector_bool_pop_back(void *ptr, size_t count); + +/// Set bits indexed by a buffer of integers to true, using the unchecked subscript. +extern void cpp_vector_bool_set_indices_subscript(void *ptr, const intptr_t *start, size_t count); + +/// Set bits indexed by a buffer of integers to true, using the checked `at` method. +extern void cpp_vector_bool_set_indices_at(void *ptr, const intptr_t *start, size_t count); + +/// Set bits indexed by a buffer of integers to false, using the unchecked subscript. +extern void cpp_vector_bool_reset_indices_subscript(void *ptr, const intptr_t *start, size_t count); + +/// Set bits indexed by a buffer of integers to false, using the checked `at` method. +extern void cpp_vector_bool_reset_indices_at(void *ptr, const intptr_t *start, size_t count); + +/// Retrieve all bits indexed by a buffer of integers, using the unchecked subscript. +extern void cpp_vector_bool_lookups_subscript(void *ptr, const intptr_t *start, size_t count); + +/// Retrieve all bits indexed by a buffer of integers, using the checked `at` method. +extern void cpp_vector_bool_lookups_at(void *ptr, const intptr_t *start, size_t count); + +/// Iterate through all the bits in a `vector`. +extern void cpp_vector_bool_iterate(void *ptr); + +/// Use `std::find` to visit every true bit in a `vector`, returning +/// the number of true bits found. +extern size_t cpp_vector_bool_find_true_bits(void *ptr); + +/// Use `std::count` to return a count of every true bit in a `vector`. +extern size_t cpp_vector_bool_count_true_bits(void *ptr); + +#ifdef __cplusplus +} +#endif + + +#endif /* CPPBENCHMARKS_VECTOR_BOOL_BENCHMARKS_H */ diff --git a/Benchmarks/CppBenchmarks/include/module.modulemap b/Benchmarks/Sources/CppBenchmarks/include/module.modulemap similarity index 61% rename from Benchmarks/CppBenchmarks/include/module.modulemap rename to Benchmarks/Sources/CppBenchmarks/include/module.modulemap index 13fe2d394..65f516c5c 100644 --- a/Benchmarks/CppBenchmarks/include/module.modulemap +++ b/Benchmarks/Sources/CppBenchmarks/include/module.modulemap @@ -1,9 +1,12 @@ module CppBenchmarks { + header "Utils.h" header "Hashing.h" header "VectorBenchmarks.h" header "DequeBenchmarks.h" header "UnorderedSetBenchmarks.h" header "UnorderedMapBenchmarks.h" + header "MapBenchmarks.h" + header "PriorityQueueBenchmarks.h" + header "VectorBoolBenchmarks.h" export * } - diff --git a/Benchmarks/CppBenchmarks/src/CustomHash.h b/Benchmarks/Sources/CppBenchmarks/src/CustomHash.h similarity index 91% rename from Benchmarks/CppBenchmarks/src/CustomHash.h rename to Benchmarks/Sources/CppBenchmarks/src/CustomHash.h index f99beb611..d0c4707d2 100644 --- a/Benchmarks/CppBenchmarks/src/CustomHash.h +++ b/Benchmarks/Sources/CppBenchmarks/src/CustomHash.h @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 diff --git a/Benchmarks/CppBenchmarks/src/DequeBenchmarks.cpp b/Benchmarks/Sources/CppBenchmarks/src/DequeBenchmarks.cpp similarity index 97% rename from Benchmarks/CppBenchmarks/src/DequeBenchmarks.cpp rename to Benchmarks/Sources/CppBenchmarks/src/DequeBenchmarks.cpp index dc76accae..79349a6ac 100644 --- a/Benchmarks/CppBenchmarks/src/DequeBenchmarks.cpp +++ b/Benchmarks/Sources/CppBenchmarks/src/DequeBenchmarks.cpp @@ -2,17 +2,17 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 // //===----------------------------------------------------------------------===// -#include "DequeBenchmarks.h" #include #include -#include "utils.h" +#include "DequeBenchmarks.h" +#include "Utils.h" void * cpp_deque_create(const intptr_t *start, size_t count) diff --git a/Benchmarks/CppBenchmarks/src/Hashing.cpp b/Benchmarks/Sources/CppBenchmarks/src/Hashing.cpp similarity index 90% rename from Benchmarks/CppBenchmarks/src/Hashing.cpp rename to Benchmarks/Sources/CppBenchmarks/src/Hashing.cpp index cdbac9d6b..eaf37d13f 100644 --- a/Benchmarks/CppBenchmarks/src/Hashing.cpp +++ b/Benchmarks/Sources/CppBenchmarks/src/Hashing.cpp @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 @@ -11,7 +11,7 @@ #import "Hashing.h" #import "CustomHash.h" -#import "utils.h" +#import "Utils.h" cpp_hash_fn custom_hash_fn; diff --git a/Benchmarks/Sources/CppBenchmarks/src/MapBenchmarks.cpp b/Benchmarks/Sources/CppBenchmarks/src/MapBenchmarks.cpp new file mode 100644 index 000000000..94f27bb60 --- /dev/null +++ b/Benchmarks/Sources/CppBenchmarks/src/MapBenchmarks.cpp @@ -0,0 +1,70 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#include "MapBenchmarks.h" +#include +#include +#include "Utils.h" + +typedef std::map custom_map; + +void * +cpp_map_create(const intptr_t *start, size_t count) +{ + auto map = new custom_map(); + for (size_t i = 0; i < count; ++i) { + map->insert({start[i], 2 * start[i]}); + } + return map; +} + +void +cpp_map_destroy(void *ptr) +{ + delete static_cast(ptr); +} + +void +cpp_map_insert_integers(const intptr_t *start, size_t count) +{ + auto map = custom_map(); + auto end = start + count; + for (auto p = start; p != end; ++p) { + auto v = *identity(p); + map.insert({ v, 2 * v }); + } + black_hole(&map); +} + +__attribute__((noinline)) +auto find(custom_map* map, intptr_t value) +{ + return map->find(value); +} + +void +cpp_map_lookups(void *ptr, const intptr_t *start, size_t count) +{ + auto map = static_cast(ptr); + for (auto it = start; it < start + count; ++it) { + auto isCorrect = find(map, *it)->second == *it * 2; + if (!isCorrect) { abort(); } + } +} + +void +cpp_map_subscript(void *ptr, const intptr_t *start, size_t count) +{ + auto map = static_cast(ptr); + for (auto it = start; it < start + count; ++it) { + black_hole((*map)[*it]); + } +} diff --git a/Benchmarks/Sources/CppBenchmarks/src/PriorityQueueBenchmarks.cpp b/Benchmarks/Sources/CppBenchmarks/src/PriorityQueueBenchmarks.cpp new file mode 100644 index 000000000..da0364a8c --- /dev/null +++ b/Benchmarks/Sources/CppBenchmarks/src/PriorityQueueBenchmarks.cpp @@ -0,0 +1,63 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#include +#include "PriorityQueueBenchmarks.h" +#include "Utils.h" + +typedef std::priority_queue pqueue; + +void * +cpp_priority_queue_create(const intptr_t *start, size_t count) +{ + auto pq = new pqueue(start, start + count); + return pq; +} + +void +cpp_priority_queue_destroy(void *ptr) +{ + delete static_cast(ptr); +} + +void +cpp_priority_queue_push(void *ptr, intptr_t value) +{ + auto pq = static_cast(ptr); + pq->push(value); +} + +void +cpp_priority_queue_push_loop(void *ptr, const intptr_t *start, size_t count) +{ + auto pq = static_cast(ptr); + for (auto p = start; p < start + count; ++p) { + pq->push(*p); + } +} + +intptr_t cpp_priority_queue_pop(void *ptr) +{ + auto pq = static_cast(ptr); + auto result = pq->top(); + pq->pop(); + return result; +} + +void +cpp_priority_queue_pop_all(void *ptr) +{ + auto pq = static_cast(ptr); + while (!pq->empty()) { + black_hole(pq->top()); + pq->pop(); + } +} diff --git a/Benchmarks/CppBenchmarks/src/UnorderedMapBenchmarks.cpp b/Benchmarks/Sources/CppBenchmarks/src/UnorderedMapBenchmarks.cpp similarity index 96% rename from Benchmarks/CppBenchmarks/src/UnorderedMapBenchmarks.cpp rename to Benchmarks/Sources/CppBenchmarks/src/UnorderedMapBenchmarks.cpp index 17f4f95a8..e23a40615 100644 --- a/Benchmarks/CppBenchmarks/src/UnorderedMapBenchmarks.cpp +++ b/Benchmarks/Sources/CppBenchmarks/src/UnorderedMapBenchmarks.cpp @@ -2,19 +2,19 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 // //===----------------------------------------------------------------------===// -#include "UnorderedMapBenchmarks.h" #include #include #include +#include "UnorderedMapBenchmarks.h" #include "CustomHash.h" -#include "utils.h" +#include "Utils.h" typedef std::unordered_map custom_map; diff --git a/Benchmarks/CppBenchmarks/src/UnorderedSetBenchmarks.cpp b/Benchmarks/Sources/CppBenchmarks/src/UnorderedSetBenchmarks.cpp similarity index 95% rename from Benchmarks/CppBenchmarks/src/UnorderedSetBenchmarks.cpp rename to Benchmarks/Sources/CppBenchmarks/src/UnorderedSetBenchmarks.cpp index 625523c10..2da393571 100644 --- a/Benchmarks/CppBenchmarks/src/UnorderedSetBenchmarks.cpp +++ b/Benchmarks/Sources/CppBenchmarks/src/UnorderedSetBenchmarks.cpp @@ -2,20 +2,20 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 // //===----------------------------------------------------------------------===// -#include "UnorderedSetBenchmarks.h" #include #include #include #include +#include "UnorderedSetBenchmarks.h" #include "CustomHash.h" -#include "utils.h" +#include "Utils.h" typedef std::unordered_set custom_set; diff --git a/Benchmarks/CppBenchmarks/src/utils.cpp b/Benchmarks/Sources/CppBenchmarks/src/Utils.cpp similarity index 87% rename from Benchmarks/CppBenchmarks/src/utils.cpp rename to Benchmarks/Sources/CppBenchmarks/src/Utils.cpp index 1e2c542c2..39ea67943 100644 --- a/Benchmarks/CppBenchmarks/src/utils.cpp +++ b/Benchmarks/Sources/CppBenchmarks/src/Utils.cpp @@ -2,14 +2,14 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 // //===----------------------------------------------------------------------===// -#include "utils.h" +#include "Utils.h" void black_hole(intptr_t value) diff --git a/Benchmarks/CppBenchmarks/src/VectorBenchmarks.cpp b/Benchmarks/Sources/CppBenchmarks/src/VectorBenchmarks.cpp similarity index 97% rename from Benchmarks/CppBenchmarks/src/VectorBenchmarks.cpp rename to Benchmarks/Sources/CppBenchmarks/src/VectorBenchmarks.cpp index cd93965b6..9cb39fc60 100644 --- a/Benchmarks/CppBenchmarks/src/VectorBenchmarks.cpp +++ b/Benchmarks/Sources/CppBenchmarks/src/VectorBenchmarks.cpp @@ -2,17 +2,17 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 // //===----------------------------------------------------------------------===// -#include "VectorBenchmarks.h" #include #include -#include "utils.h" +#include "VectorBenchmarks.h" +#include "Utils.h" void * cpp_vector_create(const intptr_t *start, size_t count) diff --git a/Benchmarks/Sources/CppBenchmarks/src/VectorBoolBenchmarks.cpp b/Benchmarks/Sources/CppBenchmarks/src/VectorBoolBenchmarks.cpp new file mode 100644 index 000000000..dca3fbfec --- /dev/null +++ b/Benchmarks/Sources/CppBenchmarks/src/VectorBoolBenchmarks.cpp @@ -0,0 +1,147 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#include +#include +#include "VectorBoolBenchmarks.h" +#include "Utils.h" + +void * +cpp_vector_bool_create_repeating(size_t count, bool value) +{ + auto v = new std::vector(count, value); + return v; +} + +void +cpp_vector_bool_destroy(void *ptr) +{ + delete static_cast *>(ptr); +} + +void +cpp_vector_bool_push_back(const bool *start, size_t count, bool reserve) +{ + auto v = std::vector(); + + if (reserve) { + v.reserve(v.size() + count); + } + + for (auto p = start; p < start + count; ++p) { + v.push_back(*p); + } + + black_hole(&v); +} + +void +cpp_vector_bool_pop_back(void *ptr, size_t count) +{ + auto &v = *static_cast *>(ptr); + + for (size_t i = 0; i < count; ++i) { + v.pop_back(); + } +} + +void +cpp_vector_bool_set_indices_subscript(void *ptr, const intptr_t *start, size_t count) +{ + auto &v = *static_cast *>(ptr); + + for (auto p = start; p < start + count; ++p) { + v[*p] = true; + } +} + +void +cpp_vector_bool_set_indices_at(void *ptr, const intptr_t *start, size_t count) +{ + auto &v = *static_cast *>(ptr); + + for (auto p = start; p < start + count; ++p) { + v.at(*p) = true; + } +} + +void +cpp_vector_bool_reset_indices_subscript(void *ptr, const intptr_t *start, size_t count) +{ + auto &v = *static_cast *>(ptr); + + for (auto p = start; p < start + count; ++p) { + v[*p] = false; + } +} + +void +cpp_vector_bool_reset_indices_at(void *ptr, const intptr_t *start, size_t count) +{ + auto &v = *static_cast *>(ptr); + + for (auto p = start; p < start + count; ++p) { + v.at(*p) = false; + } +} + +void +cpp_vector_bool_lookups_subscript(void *ptr, const intptr_t *start, size_t count) +{ + auto &v = *static_cast *>(ptr); + + for (auto p = start; p < start + count; ++p) { + black_hole(v[*p]); + } +} + +void +cpp_vector_bool_lookups_at(void *ptr, const intptr_t *start, size_t count) +{ + auto &v = *static_cast *>(ptr); + + for (auto p = start; p < start + count; ++p) { + black_hole(v.at(*p)); + } +} + +void +cpp_vector_bool_iterate(void *ptr) +{ + auto &v = *static_cast *>(ptr); + + for (auto it = v.cbegin(); it != v.cend(); ++it) { + black_hole(*it); + } +} + +size_t +cpp_vector_bool_find_true_bits(void *ptr) +{ + auto &v = *static_cast *>(ptr); + + size_t count = 0; + for (auto it = std::find(v.cbegin(), v.cend(), true); + it != v.cend(); + it = std::find(it, v.cend(), true)) { + ++count; + ++it; + } + return count; +} + +size_t +cpp_vector_bool_count_true_bits(void *ptr) +{ + auto &v = *static_cast *>(ptr); + + return std::count(v.cbegin(), v.cend(), true); +} diff --git a/Benchmarks/benchmark-tool/main.swift b/Benchmarks/Sources/benchmark-tool/main.swift similarity index 54% rename from Benchmarks/benchmark-tool/main.swift rename to Benchmarks/Sources/benchmark-tool/main.swift index ddfed857b..c264f7063 100644 --- a/Benchmarks/benchmark-tool/main.swift +++ b/Benchmarks/Sources/benchmark-tool/main.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 @@ -11,16 +11,34 @@ import CollectionsBenchmark import Benchmarks +import DequeModule + +if Deque._isConsistencyCheckingEnabled { + complain(""" + *** INTERNAL CONSISTENCY CHECKING IS ENABLED *** + + Performance guarantees aren't valid in this configuration, + and benchmarking data will be largely useless. Proceed at + your own risk. + + """) +} var benchmark = Benchmark(title: "Collection Benchmarks") +benchmark.registerCustomGenerators() benchmark.addArrayBenchmarks() benchmark.addSetBenchmarks() benchmark.addDictionaryBenchmarks() +benchmark.addTreeDictionaryBenchmarks() benchmark.addDequeBenchmarks() benchmark.addOrderedSetBenchmarks() benchmark.addOrderedDictionaryBenchmarks() +benchmark.addHeapBenchmarks() +benchmark.addBitSetBenchmarks() +benchmark.addTreeSetBenchmarks() benchmark.addCppBenchmarks() - -benchmark.chartLibrary = try benchmark.loadReferenceLibrary() +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +benchmark.addFoundationBenchmarks() +#endif benchmark.main() diff --git a/Benchmarks/Sources/memory-benchmark/DictionaryStatistics.swift b/Benchmarks/Sources/memory-benchmark/DictionaryStatistics.swift new file mode 100644 index 000000000..b46cd42c5 --- /dev/null +++ b/Benchmarks/Sources/memory-benchmark/DictionaryStatistics.swift @@ -0,0 +1,62 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +struct DictionaryStatistics { + /// The sum of all storage within the hash table that is available for + /// item storage, measured in bytes. This does account for the maximum + /// load factor. + var capacityBytes: Int = 0 + + /// The number of bytes of storage currently used for storing items. + var itemBytes: Int = 0 + + /// The number of bytes currently available in storage for storing items. + var freeBytes: Int = 0 + + /// An estimate of the actual memory occupied by this hash table. + /// This includes not only storage space available for items, + /// but also the memory taken up by the object header and the hash table + /// occupation bitmap. + var grossBytes: Int = 0 + + /// An estimate of how efficiently this data structure manages memory. + /// This is a value between 0 and 1 -- the ratio between how much space + /// the actual stored data occupies and the overall number of bytes allocated + /// for the entire data structure. (`itemBytes / grossBytes`) + var memoryEfficiency: Double { + guard grossBytes > 0 else { return 1 } + return Double(itemBytes) / Double(grossBytes) + } +} + +extension Dictionary { + var statistics: DictionaryStatistics { + // Note: This logic is based on the Dictionary ABI. It may be off by a few + // bytes due to not accounting for padding bytes between storage regions. + // The gross bytes reported also do not include extra memory that was + // allocated by malloc but not actually used for Dictionary storage. + var stats = DictionaryStatistics() + let keyStride = MemoryLayout.stride + let valueStride = MemoryLayout.stride + stats.capacityBytes = self.capacity * (keyStride + valueStride) + stats.itemBytes = self.count * (keyStride + valueStride) + stats.freeBytes = stats.capacityBytes - stats.itemBytes + + let bucketCount = self.capacity._roundUpToPowerOfTwo() + let bitmapBitcount = (bucketCount + UInt.bitWidth - 1) + + let objectHeaderBits = 2 * Int.bitWidth + let ivarBits = 5 * Int.bitWidth + 64 + stats.grossBytes = (objectHeaderBits + ivarBits + bitmapBitcount) / 8 + stats.grossBytes += bucketCount * keyStride + bucketCount * valueStride + return stats + } +} diff --git a/Benchmarks/Sources/memory-benchmark/MemoryBenchmarks.swift b/Benchmarks/Sources/memory-benchmark/MemoryBenchmarks.swift new file mode 100644 index 000000000..0bdb54fbf --- /dev/null +++ b/Benchmarks/Sources/memory-benchmark/MemoryBenchmarks.swift @@ -0,0 +1,80 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +import ArgumentParser +import CollectionsBenchmark +import Collections + +@main +struct MemoryBenchmarks: ParsableCommand { + static var configuration: CommandConfiguration { + CommandConfiguration( + commandName: "memory-statistics", + abstract: "A utility for running memory benchmarks for collection types.") + } + + @OptionGroup + var sizes: Benchmark.Options.SizeSelection + + mutating func run() throws { + let sizes = try self.sizes.resolveSizes() + + var i = 0 + + var d: Dictionary = [:] + var pd: TreeDictionary = [:] + + print(""" + Size,"Dictionary",\ + "TreeDictionary",\ + "average node size",\ + "average item depth" + """) + + var sumd: Double = 0 + var sump: Double = 0 + for size in sizes { + while i < size.rawValue { + let key = "key \(i)" + let value = "value \(i)" + d[key] = value + pd[key] = value + i += 1 + } + + let dstats = d.statistics + let pstats = pd._statistics + print(""" + \(size.rawValue),\ + \(dstats.memoryEfficiency),\ + \(pstats.memoryEfficiency),\ + \(pstats.averageNodeSize),\ + \(pstats.averageItemDepth) + """) + sumd += dstats.memoryEfficiency + sump += pstats.memoryEfficiency + } + + let pstats = pd._statistics + complain(""" + Averages: + Dictionary: \(sumd / Double(sizes.count)) + TreeDictionary: \(sump / Double(sizes.count)) + + TreeDictionary at 1M items: + average node size: \(pstats.averageNodeSize) + average item depth: \(pstats.averageItemDepth) + average lookup chain length: \(pstats.averageLookupChainLength) + """) + } +} + + diff --git a/CMakeLists.txt b/CMakeLists.txt index 6661eea42..98d73f801 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ #[[ This source file is part of the Swift Collections Open Source Project -Copyright (c) 2021 Apple Inc. and the Swift project authors +Copyright (c) 2021 - 2024 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 @@ -20,6 +20,9 @@ list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules) set(CMAKE_Swift_MODULE_DIRECTORY ${CMAKE_BINARY_DIR}/swift) set(CMAKE_Swift_COMPILE_OPTIONS_MSVC_RUNTIME_LIBRARY MultiThreadedDLL) +set(CMAKE_Swift_MODULE_DIRECTORY ${CMAKE_BINARY_DIR}/swift) +set(CMAKE_Swift_COMPILE_OPTIONS_MSVC_RUNTIME_LIBRARY MultiThreadedDLL) + set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) diff --git a/Documentation/Announcement-benchmarks/generate-results.sh b/Documentation/Announcement-benchmarks/generate-results.sh index 49baf8961..dcb79bbda 100755 --- a/Documentation/Announcement-benchmarks/generate-results.sh +++ b/Documentation/Announcement-benchmarks/generate-results.sh @@ -3,7 +3,7 @@ # # This source file is part of the Swift Collections open source project # -# Copyright (c) 2021 Apple Inc. and the Swift project authors +# Copyright (c) 2021 - 2024 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 diff --git a/Documentation/Heap.md b/Documentation/Heap.md new file mode 100644 index 000000000..2037472d3 --- /dev/null +++ b/Documentation/Heap.md @@ -0,0 +1,153 @@ +# Heap + +A partially-ordered tree of elements with performant insertion and removal operations. + +## Declaration + +```swift +public struct Heap +``` + +## Overview + +Array-backed [binary heaps](https://en.wikipedia.org/wiki/Heap_(data_structure)) provide performant lookups (`O(1)`) of the smallest or largest element (depending on whether it's a min-heap or a max-heap, respectively) as well as insertion and removal (`O(log n)`). Heaps are commonly used as the backing storage for a priority queue. + +A variant on this, the [min-max heap](https://en.wikipedia.org/wiki/Min-max_heap), allows for performant lookups and removal of both the smallest **and** largest elements by interleaving min and max levels in the backing array. `Heap` is an implementation of a min-max heap. + +### Initialization + +There are a couple of options for initializing a `Heap`. To create an empty `Heap`, call `init()`: + +```swift +var heap = Heap() +``` + +You can also create a `Heap` from an existing sequence in linear time: + +```swift +var heap = Heap((1...).prefix(20)) +``` + +Finally, a `Heap` can be created from an array literal: + +```swift +var heap: Heap = [0.1, 0.6, 1.0, 0.15, 0.42] +``` + +### Insertion + +#### Of a single element + +To insert an element into a `Heap`, call `insert(_:)`: + +```swift +var heap = Heap() +heap.insert(6) +heap.insert(2) +``` + +This works by adding the new element into the end of the backing array and then bubbling it up to where it belongs in the heap. + +#### Of a sequence of elements + +You can also insert a sequence of elements into a `Heap`: + +```swift +var heap = Heap(0 ..< 10) +heap.insert(contentsOf: (20 ... 100).shuffled()) +heap.insert(contentsOf: [-5, -6, -8, -12, -3]) +``` + +### Lookup + +As mentioned earlier, the smallest and largest elements can be queried in constant time: + +```swift +var heap = Heap(1 ... 20) +let min = heap.min // 1 +let max = heap.max // 20 +``` + +In a min-max heap, the smallest element is stored at index 0 in the backing array; the largest element is stored at either index 1 or index 2, the first max level in the heap (so to look up the largest, we compare the two and return the larger one). + +We also expose a read-only view into the backing array, should somebody need that. + +```swift +let heap = Heap((1...100).shuffled()) +for val in heap.unordered { + ... +} +``` + +> Note: The elements aren't _arbitrarily_ ordered (it is, after all, a heap). However, no guarantees are given as to the ordering of the elements or that this won't change in future versions of the library. + +### Removal + +Removal has logarithmic complexity, and removing both the smallest and largest elements is supported: + +```swift +var heap = Heap((1...20).shuffled()) +var heap2 = heap + +while let min = heap.popMin() { + print("Next smallest element:", min) +} + +while let max = heap2.popMax() { + print("Next largest element:", max) +} +``` + +To remove the smallest element, we remove and return the element at index 0. The element at the end of the backing array is put in its place at index 0, and then we trickle it down to where it belongs in the heap. To remove the largest element, we do the same except the index is whatever the index of the largest element is (see above) instead of 0. + +We also have non-optional flavors that assume the heap isn't empty, `removeMin()` and `removeMax()`. + +### Iteration + +`Heap` itself doesn't conform to `Sequence` because of the potential confusion around which direction it should iterate (largest-to-smallest? smallest-to-largest?). + +### Performance + +| Operation | Complexity | +|-----------|------------| +| Insert | O(log n) | +| Get smallest element | O(1) | +| Get largest element | O(1) | +| Remove smallest element | O(log n) | +| Remove largest element | O(log n) | + +In all of the above, `n` is the number of elements in the heap. + +![Heap performance graph](Images/Heap%20Performance.png) + +The above graph was generated in release mode on a MacBook Pro (16-inch, 2019) with a 2.3 GHz 8-Core Intel Core i9 using the benchmarks defined in the `swift-collections-benchmark` target. + +## Implementation Details + +The implementation is based on the min-max heap data structure as introduced by [Atkinson et al. 1986]. + +[Atkinson et al. 1986]: https://doi.org/10.1145/6617.6621 + +Min-max heaps are complete binary trees represented implicitly as an array of their elements. Each node at an even level in the tree is less than or equal to all its descendants, while each node at an odd level in the tree is greater or equal to all of its descendants. + +``` +// Min-max heap: +level 0 (min): ┌────── A ──────┐ +level 1 (max): ┌── J ──┐ ┌── G ──┐ +level 2 (min): ┌ D ┐ ┌ B F C +level 3 (max): I E H + +// Array representation: +["A", "J", "G", "D", "B", "F", "C", "I", "E", "H"] +``` + +By the min-max property above, the root node is an on even level, so its value ("A" in this example) must be the minimum of the entire heap. Its two children are on an odd level, so they hold the maximum value for their respective subtrees; it follows that one of them holds the maximum value for the whole tree -- in this case, "J". Accessing the minimum and maximum values in the heap can therefore be done in O(1) comparisons. + +Mutations of the heap (insertions, removals) must ensure that items remain arranged in a way that maintain the min-max property. Inserting a single new element or removing the current minimum/maximum can be done by rearranging items on a single path in the tree; accordingly, these operations execute O(log(`count`)) comparisons/swaps. + +--- + +M.D. Atkinson, J.-R. Sack, N. Santoro, T. Strothotte. +"Min-Max Heaps and Generalized Priority Queues." +*Communications of the ACM*, vol. 29, no. 10, Oct. 1986., pp. 996-1000, +doi:[10.1145/6617.6621](https://doi.org/10.1145/6617.6621) diff --git a/Documentation/Images/Heap Performance.png b/Documentation/Images/Heap Performance.png new file mode 100644 index 000000000..544474b12 Binary files /dev/null and b/Documentation/Images/Heap Performance.png differ diff --git a/Documentation/Internals/README.md b/Documentation/Internals/README.md index a5594c390..c84306614 100644 --- a/Documentation/Internals/README.md +++ b/Documentation/Internals/README.md @@ -10,13 +10,13 @@ For more information on our benchmarking tool, please see its dedicated package, ## Test Support Library -The package comes with a rich test support library in the [Sources/_CollectionsTestSupport](./Sources/_CollectionsTestSupport) directory. These were loosely adapted from the contents of the `StdlibUnittest*` modules in the [Swift compiler repository](https://github.com/apple/swift/tree/main/stdlib/private), with some custom additions. +The package comes with a rich test support library in the [Sources/_CollectionsTestSupport](../../Sources/_CollectionsTestSupport) directory. These were loosely adapted from the contents of the `StdlibUnittest*` modules in the [Swift compiler repository](https://github.com/apple/swift/tree/main/stdlib/private), with some custom additions. These components would likely be of interest to the wider Swift community, but they aren't yet stable enough (or documented enough) to publish them. Accordingly, these testing helpers are currently considered implementation details of this package, and are subject to change at whim. The test support library currently provides the following functionality: -- [`AssertionContexts`](./Sources/CollectionsTestSupport/AssertionContexts): Custom test assertions with support for keeping track of nested context information, including stopping execution when the current context matches a particular value. (Useful for debugging combinatorial tests.) +- [`AssertionContexts`](../../Sources/_CollectionsTestSupport/AssertionContexts): Custom test assertions with support for keeping track of nested context information, including stopping execution when the current context matches a particular value. (Useful for debugging combinatorial tests.)
Click here for a short demonstration @@ -62,7 +62,7 @@ The test support library currently provides the following functionality:
-- [`Combinatorics`](./Sources/CollectionsTestSupport/AssertionContexts/Combinatorics.swift): Basic support for exhaustive combinatorial testing. This allows us to easily verify that a collection operation works correctly on all possible instances up to a certain size, including behavioral variations such as unique/shared storage. +- [`Combinatorics`](../../Sources/_CollectionsTestSupport/AssertionContexts/Combinatorics.swift): Basic support for exhaustive combinatorial testing. This allows us to easily verify that a collection operation works correctly on all possible instances up to a certain size, including behavioral variations such as unique/shared storage. This file also contains a basic set of functions for executing the same test code for all subsets or all permutations of a given collection, with each iteration registered in the current test context, for easy reproduction of failed cases.
Click here for an example @@ -84,35 +84,35 @@ The test support library currently provides the following functionality: } } ``` - +
- -- [`ConformanceCheckers`](./Sources/CollectionsTestSupport/ConformanceCheckers): A set of generic, semi-automated protocol conformance tests for some Standard Library protocols. These can be used to easily validate the custom protocol conformances provided by this package. These checks aren't (can't be) complete -- but when used correctly, they are able to detect most accidental mistakes. + +- [`ConformanceCheckers`](../../Sources/_CollectionsTestSupport/ConformanceCheckers): A set of generic, semi-automated protocol conformance tests for some Standard Library protocols. These can be used to easily validate the custom protocol conformances provided by this package. These checks aren't (can't be) complete -- but when used correctly, they are able to detect most accidental mistakes. We currently have conformance checkers for the following protocols: - - [`Sequence`](./Sources/CollectionsTestSupport/ConformanceCheckers/CheckSequence.swift) - - [`Collection`](./Sources/CollectionsTestSupport/ConformanceCheckers/CheckCollection.swift) - - [`BidirectionalCollection`](./Sources/CollectionsTestSupport/ConformanceCheckers/CheckBidirectionalCollection.swift) - - [`Equatable`](./Sources/CollectionsTestSupport/ConformanceCheckers/CheckEquatable.swift) - - [`Hashable`](./Sources/CollectionsTestSupport/ConformanceCheckers/CheckHashable.swift) - - [`Comparable`](./Sources/CollectionsTestSupport/ConformanceCheckers/CheckComparable.swift) - -- [`MinimalTypes`](./Sources/CollectionsTestSupport/MinimalTypes): Minimally conforming implementations for standard protocols. These types conform to various standard protocols by implementing the requirements in as narrow-minded way as possible -- sometimes going to extreme lengths to, say, implement collection index invalidation logic in the most unhelpful way possible. - - - [`MinimalSequence`](./Sources/CollectionsTestSupport/MinimalTypes/MinimalSequence.swift) - - [`MinimalCollection`](./Sources/CollectionsTestSupport/MinimalTypes/MinimalCollection.swift) - - [`MinimalBidirectionalCollection`](./Sources/CollectionsTestSupport/MinimalTypes/MinimalBidirectionalCollection.swift) - - [`MinimalRandomAccessCollection`](./Sources/CollectionsTestSupport/MinimalTypes/MinimalRandomAccessCollection.swift) - - [`MinimalMutableRandomAccessCollection`](./Sources/CollectionsTestSupport/MinimalTypes/MinimalMutableRandomAccessCollection.swift) - - [`MinimalRangeReplaceableRandomAccessCollection`](./Sources/CollectionsTestSupport/MinimalTypes/MinimalRangeReplaceableRandomAccessCollection.swift) - - [`MinimalMutableRangeReplaceableRandomAccessCollection`](./Sources/CollectionsTestSupport/MinimalTypes/MinimalMutableRangeReplaceableRandomAccessCollection.swift) - - [`MinimalIterator`](./Sources/CollectionsTestSupport/MinimalTypes/MinimalIterator.swift) - - [`MinimalIndex`](./Sources/CollectionsTestSupport/MinimalTypes/MinimalIndex.swift) - - [`MinimalEncoder`](./Sources/CollectionsTestSupport/MinimalTypes/MinimalEncoder.swift) - - [`MinimalDecoder`](./Sources/CollectionsTestSupport/MinimalTypes/MinimalDecoder.swift) - -- [`Utilities`](./Sources/CollectionsTestSupport/Utilities): Utility types. Wrapper types for boxed values, a simple deterministic random number generator, and a lifetime tracker for catching simple memory management issues such as memory leaks. (The [Address Sanitizer][asan] can be used to catch more serious problems.) + - [`Sequence`](../../Sources/_CollectionsTestSupport/ConformanceCheckers/CheckSequence.swift) + - [`Collection`](../../Sources/_CollectionsTestSupport/ConformanceCheckers/CheckCollection.swift) + - [`BidirectionalCollection`](../../Sources/_CollectionsTestSupport/ConformanceCheckers/CheckBidirectionalCollection.swift) + - [`Equatable`](../../Sources/_CollectionsTestSupport/ConformanceCheckers/CheckEquatable.swift) + - [`Hashable`](../../Sources/_CollectionsTestSupport/ConformanceCheckers/CheckHashable.swift) + - [`Comparable`](../../Sources/_CollectionsTestSupport/ConformanceCheckers/CheckComparable.swift) + +- [`MinimalTypes`](../../Sources/_CollectionsTestSupport/MinimalTypes): Minimally conforming implementations for standard protocols. These types conform to various standard protocols by implementing the requirements in as narrow-minded way as possible -- sometimes going to extreme lengths to, say, implement collection index invalidation logic in the most unhelpful way possible. + + - [`MinimalSequence`](../../Sources/_CollectionsTestSupport/MinimalTypes/MinimalSequence.swift) + - [`MinimalCollection`](../../Sources/_CollectionsTestSupport/MinimalTypes/MinimalCollection.swift) + - [`MinimalBidirectionalCollection`](../../Sources/_CollectionsTestSupport/MinimalTypes/MinimalBidirectionalCollection.swift) + - [`MinimalRandomAccessCollection`](../../Sources/_CollectionsTestSupport/MinimalTypes/MinimalRandomAccessCollection.swift) + - [`MinimalMutableRandomAccessCollection`](../../Sources/_CollectionsTestSupport/MinimalTypes/MinimalMutableRandomAccessCollection.swift) + - [`MinimalRangeReplaceableRandomAccessCollection`](../../Sources/_CollectionsTestSupport/MinimalTypes/MinimalRangeReplaceableRandomAccessCollection.swift) + - [`MinimalMutableRangeReplaceableRandomAccessCollection`](../../Sources/_CollectionsTestSupport/MinimalTypes/MinimalMutableRangeReplaceableRandomAccessCollection.swift) + - [`MinimalIterator`](../../Sources/_CollectionsTestSupport/MinimalTypes/MinimalIterator.swift) + - [`MinimalIndex`](../../Sources/_CollectionsTestSupport/MinimalTypes/MinimalIndex.swift) + - [`MinimalEncoder`](../../Sources/_CollectionsTestSupport/MinimalTypes/MinimalEncoder.swift) + - [`MinimalDecoder`](../../Sources/_CollectionsTestSupport/MinimalTypes/MinimalDecoder.swift) + +- [`Utilities`](../../Sources/_CollectionsTestSupport/Utilities): Utility types. Wrapper types for boxed values, a simple deterministic random number generator, and a lifetime tracker for catching simple memory management issues such as memory leaks. (The [Address Sanitizer][asan] can be used to catch more serious problems.) [asan]: https://developer.apple.com/documentation/xcode/diagnosing_memory_thread_and_crash_issues_early?language=objc diff --git a/Documentation/Reviews/2022-10-31.ShareableHashedCollections/ShareableHashedCollections.md b/Documentation/Reviews/2022-10-31.ShareableHashedCollections/ShareableHashedCollections.md new file mode 100644 index 000000000..11feb6185 --- /dev/null +++ b/Documentation/Reviews/2022-10-31.ShareableHashedCollections/ShareableHashedCollections.md @@ -0,0 +1,349 @@ +# Shareable Hashed Collections Module + +- Authors: [Michael Steindorfer](https://github.com/msteindorfer), [Karoy Lorentey](https://forums.swift.org/u/lorentey) +- Implementation: https://github.com/apple/swift-collections/tree/release/1.1/Sources/ShareableHashedCollections + +## Table of contents + +* [Introduction](#introduction) +* [Motivation](#motivation) +* [Proposed solution](#proposed-solution) + - [`ShareableSet`](#shareableset) + - [`ShareableDictionary`](#shareabledictionary) +* [Detailed design](#detailed-design) + +## Introduction + +`ShareableHashedCollections` is a new module in Swift Collections, containing new hashed collection types that store their elements in a prefix tree structure, based on their hash values. The new types are tree-based equivalents to the standard `Set` and `Dictionary`; they're called `ShareableSet` and `ShareableDictionary`. + +Like the standard hashed collections, `ShareableSet` and `ShareableDictionary` are unordered collections of unique items. However, the new collections are optimizing for making mutations of shared copies as efficient as practical, both by making such changes faster, but, just as importantly, by letting mutated copies of the same collection value share as much of their structure as possible, saving a considerable amount of memory. + +## Motivation + +Well-behaved Swift collection types are typically expected to implement the copy-on-write optimization: in this scheme, making copies of collection values is an O(1) operation, but the first time one of the copies gets mutated, its storage needs to be made unique. + +In the case of the standard collection types, the only way to make a collection value's storage unique is to copy the entire collection into newly allocated storage. This maintains the illusion of value semantics, but such all-or-nothing copying can lead to undesirable spikes in complexity. Inserting an item will usually take constant time, but if we aren't careful about making copies, it _sometimes_ jumps to linear complexity, which can easily render the code unusably slow, especially when the operation is called inside a loop. + +As a toy example, consider a case of a system where we want to allow the user to arbitrarily mutate a set of integers, then periodically (say, every time we want to refresh the screen) we compare the current set to the one we previously saw, and do something based on the differences. To keep things simple, let's say we are just inserting a sequence of integers one by one, and we want to update the display state after every insertion: + +```swift +typealias Model = Set + +var _state: Model // Private +func updateState( + with model: Model +) -> (insertions: Set, removals: Set) { + let insertions = model.subtracting(_state) + let removals = _state.subtracting(model) + _state = model + return (insertions, removals) +} + +let c = 1_000_000 +var model: Model = [] +for i in 0 ..< c { + model.insert(i) + let r = updateState(with: model) + precondition(r.insertions.count == 1 && r.removals.count = 0) +} +``` + +(Of course, ideally the set would remember precisely what items got inserted/removed in it since the last time a snapshot was made, so calculating the difference would be a trivial operation. But if we aren't able to maintain such a change log, this might be a reasonably pragmatic fallback approach.) + +With the standard `Set` type, having to keep a copy of the model around is inconvenient: it not only makes the first mutation after the `updateState` call slow, but it also roughly doubles the memory that we need to use, as the new version of the model will need to have its own, completely independent storage. Even worse, calculating the differences between the current model and its previous snapshot is quite expensive: we don't have any information about what changed, so the `subtracting` operations need to carefully compare each item one by one -- there aren't any useful shortcuts. + +Overall, the model update and the diffing operations all take time that's proportional to the number of items in the set, making it difficult to update the display state with high enough frequency once the set grows above a certain size. + +This all-or-nothing copy-on-write behavior can be eliminated by organizing the contents of hashed collections into a prefix tree of hash values rather than a single flat hash table. `ShareableSet` and `ShareableDictionary` use tree nodes that contain at most 32 items -- each of which can either be a direct key-value pair, or a reference to a child node containing more items. The nodes themselves work like tiny hash tables themselves: each node along the path from the root to any given item handles 5 bits (2^5 == 32) worth of data from the item's hash value. New nodes get created whenever a new item's hash value matches another's up to the depth currently covered by its path; such collisions are resolved by allocating a new node that looks at 5 more bits' worth of information until the items can be distinguished or we exhaust all available bits in the hash value. (Special collision nodes handle items that fall in the latter case.) + +A key aspect of this structure is that individual nodes can be reference counted, so they can be easily shared across different instances of the same collection type. Copies of collection values can be made just as easily as with the flat hash table -- we just need to make a copy of the reference to the root node, incrementing its reference count. Furthermore, when we need to insert a new item (or remove an existing one) from such a copy, we only need to make copies of the nodes along its path: all other nodes can continue to be linked into both trees. + +This boosts the performance of shared mutations from linear to logarithmic time, bringing along a corresponding improvement to memory use -- two set values that only diverged slightly are still expected to share most of their storage. + +Better, the trees are able to organically grow or shrink their storage to fit their contents: we don't need to think about reserving capacity, and we don't need to worry about temporary spikes in a collection's size inducing a corresponding increase in its size that lingers long after the collection is restored to a more typical size. + +Sharing storage nodes between collection instances also brings about additional benefits: when comparing or combining two sets (or dictionaries), we can easily detect shared nodes and we can typically handle their entire subtree as a single unit, instead of having to individually compare/combine their items. For example, when subtracting one set from another, nodes that are shared between them can simply be skipped when building the result, without even looking at any of their contained items. + +For example, simply changing the model type from `Set` to `ShareableSet` leads to an algorithmic improvement to running time: + +```swift +typealias Model = ShareableSet + +... // Same code as before +``` + +For a million items, the code that uses `Set` would have taken roughly five hours to run; the variant that uses `ShareableCode` runs in about two seconds! + +Plotting the average time spent over one iteration of the loop on a log-log chart, we get the following result: + +![](ShareableSet-model-diffing.png) + +Over the entire spread of item counts from 1 to a million, `Set` goes from 100ns to >10ms spent in each iteration -- an increase of five orders of magnitude, corresponding to a linear growth rate. Meanwhile `PeristentSet`'s growth is kept nicely logarithmic: its 8-10x growth factor is far removed from the growth rate of its input data. + +For this particular use case, hash-array mapped prefix trees proved to be a far better data structure than a flat hash table. Of course, this does not mean that `ShareableSet` is going to always be a better choice than `Set` -- as always, it depends on the use case. + +Maintaining the prefix tree involves a lot more bookkeeping than a flat hash table, including having to descend through multiple nodes when looking up items or iterating through the collection: + +![](ShareableSet-sequential%20iteration.png) + +Not having to think about reserving capacity also means that the tree cannot preallocate nodes in advance, even if we know exactly how many items we will need to insert -- it needs to organically allocate/deallocate nodes as needed. The more complicated structure therefore leads to worse expected behavior (by a constant factor) when the use case cannot exercise the benefits of node sharing, such as when we only mutate a single dictionary in place, without ever making a copy of it. + +![](ShareableSet-insert.png) + +(Still, certain use cases might still accept the constant-factor slowdown in exchange for the freedom of not having to worry about memory management.) + +## Proposed solution + +We propose to introduce a new Swift Collections module, called `ShareableHashedCollections`, with two top-level public types, `ShareableSet` and `ShareableDictionary`. + +### `ShareableSet` + +`ShareableSet` is a type with one generic argument, `Element`, required to be `Hashable`. Like the standard `Set`, `ShareableSet` is a `SetAlgebra` that is a forward-only, unordered `Collection` with an opaque index type. It is `Hashable` and conditionally `Sendable` and `Codable`, depending on its `Element`: + +```swift +struct ShareableSet + : Sequence, Collection, + SetAlgebra, + Equatable, Hashable, + CustomStringConvertible, CustomDebugStringConvertible, CustomReflectable, + ExpressibleByArrayLiteral +{} + +extension ShareableSet: Sendable where Element: Sendable {} +extension ShareableSet: Decodable where Element: Decodable {} +extension ShareableSet: Encodable where Element: Encodable {} +``` + +Internally, `ShareableSet` organizes its elements into a hash-array mapped prefix tree. To speed up indexing operations in such a structure, each index in a `ShareableSet` value contains a direct storage reference to the node within the tree that contains the element addressed. This means that unlike with `Set`, `ShareableSet` indices need to get invalidated on every mutation. + +#### Creating a Set + +```swift +init() +init(S) +init(`Self`) +init(ShareableDictionary.Keys) +``` + +#### Finding Elements + +```swift +func contains(Element) -> Bool +func firstIndex(of: Element) -> Index? +func lastIndex(of: Element) -> Index? +``` + +#### Adding and Updating Elements + +```swift +mutating func insert(Element) -> (inserted: Bool, memberAfterInsert: Element) +mutating func update(with: Element) -> Element? +mutating func update(Element, at: Index) -> Element +``` + +#### Removing Elements + +```swift +mutating func remove(Element) -> Element? +mutating func remove(at: Index) -> Element +func filter((Element) throws -> Bool) rethrows -> ShareableSet +mutating func removeAll(where shouldBeRemoved: (Element) throws -> Bool) rethrows +``` + +#### Combining Sets + +All the standard combining operations (intersection, union, subtraction and symmetric difference) are supported, in both non-mutating and mutating forms. `SetAlgebra` only requires the ability to combine one set instance with another, but `ShareableSet` follows the tradition established by `Set` in providing additional overloads to each operation that allow combining a set with additional types, including arbitrary sequences. + +```swift +func intersection(`Self`) -> ShareableSet +func intersection(ShareableDictionary.Keys) -> ShareableSet +func intersection(S) -> ShareableSet + +func union(`Self`) -> ShareableSet +func union(ShareableDictionary.Keys) -> ShareableSet +func union(S) -> ShareableSet + +func subtracting(`Self`) -> ShareableSet +func subtracting(ShareableDictionary.Keys) -> ShareableSet +func subtracting(S) -> ShareableSet + +func symmetricDifference(`Self`) -> ShareableSet +func symmetricDifference(ShareableDictionary.Keys) -> ShareableSet +func symmetricDifference(S) -> ShareableSet + +mutating func formIntersection(`Self`) +mutating func formIntersection(ShareableDictionary.Keys) +mutating func formIntersection(S) + +mutating func formUnion(`Self`) +mutating func formUnion(ShareableDictionary.Keys) +mutating func formUnion(S) + +mutating func subtract(`Self`) +mutating func subtract(ShareableDictionary.Keys) +mutating func subtract(S) + +mutating func formSymmetricDifference(`Self`) +mutating func formSymmetricDifference(ShareableDictionary.Keys) +mutating func formSymmetricDifference(S) +``` + +#### Comparing Sets + +`ShareableSet` supports all standard set comparisons (subset tests, superset tests, disjunctness test), including the customary overloads established by `Set`. As an additional extension, the `isEqualSet` family of member functions generalize the standard `==` operation to support checking whether a `ShareableSet` consists of exactly the same members as an arbitrary sequence. Like `==`, the `isEqualSet` functions ignore element ordering and duplicates (if any). + +```swift +static func == (`Self`, `Self`) -> Bool +func isEqualSet(to: `Self`) -> Bool +func isEqualSet(to: ShareableDictionary.Keys) -> Bool +func isEqualSet(to: S) -> Bool + +func isSubset(of: `Self`) -> Bool +func isSubset(of: ShareableDictionary.Keys) -> Bool +func isSubset(of: S) -> Bool + +func isSuperset(of: `Self`) -> Bool +func isSuperset(of: ShareableDictionary.Keys) -> Bool +func isSuperset(of: S) -> Bool + +func isStrictSubset(of: `Self`) -> Bool +func isStrictSubset(of: ShareableDictionary.Keys) -> Bool +func isStrictSubset(of: S) -> Bool + +func isStrictSuperset(of: `Self`) -> Bool +func isStrictSuperset(of: ShareableDictionary.Keys) -> Bool +func isStrictSuperset(of: S) -> Bool + +func isDisjoint(with: `Self`) -> Bool +func isDisjoint(with: ShareableDictionary.Keys) -> Bool +func isDisjoint(with: S) -> Bool +``` + +### `ShareableDictionary` + +`ShareableDictionary` is a type with two generic arguments, `Key` and `Value`, of which `Key` is required to be `Hashable`. Like the standard `Dictionary`, it implements a forward-only, unordered `Collection` of key-value pairs, with custom members providing efficient support for retrieving the value of any given key. `ShareableDictionary` is conditionally `Sendable`, `Codable`, `Equatable` and `Hashable`, depending on its `Key` and `Value` types. + +```swift +struct ShareableDictionary + : Sequence, Collection, + CustomStringConvertible, CustomDebugStringConvertible, CustomReflectable, + ExpressibleByDictionaryLiteral +{} + +extension ShareableDictionary: Sendable where Key: Sendable, Value: Sendable {} +extension ShareableDictionary: Equatable where Value: Equatable {} +extension ShareableDictionary: Hashable where Value: Hashable {} +extension ShareableDictionary: Decodable where Key: Decodable, Value: Decodable {} +extension ShareableDictionary: Encodable where Key: Encodable, Value: Encodable {} +``` + +Internally, `ShareableDictionary` organizes its elements into a hash-array mapped prefix tree. To speed up indexing operations in such a structure, each index in a `ShareableDictionary` value contains a direct storage reference to the node within the tree that contains the element addressed. This means that unlike with `Dictionary`, `ShareableDictionary` indices are invalidated on every mutation, including mutations that only affect a value within the dictionary. + +#### Collection Views + +`ShareableDictionary` provides the customary dictionary views, `keys` and `values`. These are collection types that are projections of the dictionary itself, with elements that match only the keys or values of the dictionary, respectively. The `Keys` view is notable in that it provides operations for subtracting and intersecting the keys of two dictionaries, allowing for easy detection of inserted and removed items between two snapshots of the same dictionary. Because `ShareableDictionary` needs to invalidate indices on every mutation, its `Values` view is not a `MutableCollection`. + +```swift +ShareableDictionary.Keys +ShareableDictionary.Values + +var keys: Keys +var values: Values + +extension ShareableDictionary.Keys { + func contains(Element) -> Bool + + func intersection(ShareableSet) -> Self + func intersection(ShareableDictionary.Keys) -> Self + + func subtracting(ShareableSet) -> Self + func subtracting(ShareableDictionary.Keys) -> Self +} +``` + +#### Creating a Dictionary + +```swift +init() +init(ShareableDictionary) +init(Dictionary) +init(uniqueKeysWithValues: S) +init(S, uniquingKeysWith: (Value, Value) throws -> Value) rethrows +init(grouping: S, by: (S.Element) throws -> Key) rethrows +init(keys: ShareableSet, valueGenerator: (Key) throws -> Value) rethrows +``` + +#### Inspecting a Dictionary + +```swift +var isEmpty: Bool +var count: Int +``` + +#### Accessing Keys and Values + +```swift +subscript(Key) -> Value? +subscript(Key, default _: () -> Value) -> Value +func index(forKey: Key) -> Index? +``` + +#### Adding or Updating Keys and Values + +Beyond the standard `updateValue(_:forKey:)` method, `ShareableDictionary` also provides additional `updateValue` variants that take closure arguments. These provide a more straightforward way to perform in-place mutations on dictionary values (compared to mutating values through the corresponding subscript operation.) `ShareableDictionary` also provides the standard `merge` and `merging` operations for combining dictionary values. + +```swift +mutating func updateValue(Value, forKey: Key) -> Value? +mutating func updateValue(forKey: Key, with: (inout Value?) throws -> R) rethrows -> R +mutating func updateValue(forKey: Key, default: () -> Value, with: (inout Value) throws -> R) rethrows -> R + +mutating func merge(`Self`, uniquingKeysWith: (Value, Value) throws -> Value) rethrows +mutating func merge(S, uniquingKeysWith: (Value, Value) throws -> Value) rethrows + +func merging(`Self`, uniquingKeysWith: (Value, Value) throws -> Value) rethrows -> ShareableDictionary +func merging(S, uniquingKeysWith: (Value, Value) throws -> Value) rethrows -> ShareableDictionary +``` + +#### Removing Keys and Values + +```swift +mutating func removeValue(forKey: Key) -> Value? +mutating func remove(at: Index) -> Element +func filter((Element) throws -> Bool) rethrows -> ShareableDictionary +mutating func removeAll(where shouldBeRemoved: (Element) throws -> Bool) rethrows +``` + +#### Comparing Dictionaries + +```swift +static func == (`Self`, `Self`) -> Bool +``` + +#### Transforming a Dictionary + +```swift +func mapValues((Value) throws -> T) rethrows -> ShareableDictionary +func compactMapValues((Value) throws -> T?) rethrows -> ShareableDictionary +``` + +## Detailed design + +For a precise list of all public APIs in the `ShareableHashedCollections` module, see the DocC documentation that is (temporarily) available at: + +[https://lorentey.github.io/swift-collections/ShareableHashedCollections/documentation/shareablehashedcollections/](https://lorentey.github.io/swift-collections/ShareableHashedCollections/documentation/shareablehashedcollections/) + +Alternatively, you can browse the proposed implementation at: + +[https://github.com/apple/swift-collections/tree/release/1.1/Sources/ShareableHashedCollections](https://github.com/apple/swift-collections/tree/release/1.1/Sources/ShareableHashedCollections) + +## Alternatives Considered + +### Naming + +The initial version of this document proposed to name the new types `PersistentSet` and `PersistentDictionary`, after the term of art "persistent data structure". Based on review feedback, we changed this to the current names. The new names eliminate confusion about the word "persistent", while also preserving the following important properties: + +1. The name succinctly labels the primary feature that distinguishes them from existing collection types in this package and the Standard Library. In our case, this feature is that these types are pretty good at mutating copied values. because they can continue to share parts of their storage with their copies after such mutations. +2. The meaning is still at least _somewhat_ understandable to folks who are familiar with this feature in, say, a purely functional context. (There is a bit of handwaving here -- the precise term "shareable" have never been applied to data structures in this (or from what I can tell, any other) sense. However, pointing out the possibility of shared storage seems to be striking a chord that other options have failed to match.) + +Another widespread preexisting alternative term for such data structures is "immutable"; unfortunately, it is not applicable here. + diff --git a/Documentation/Reviews/2022-10-31.ShareableHashedCollections/ShareableSet-insert.png b/Documentation/Reviews/2022-10-31.ShareableHashedCollections/ShareableSet-insert.png new file mode 100644 index 000000000..450a6749e Binary files /dev/null and b/Documentation/Reviews/2022-10-31.ShareableHashedCollections/ShareableSet-insert.png differ diff --git a/Documentation/Reviews/2022-10-31.ShareableHashedCollections/ShareableSet-model-diffing.png b/Documentation/Reviews/2022-10-31.ShareableHashedCollections/ShareableSet-model-diffing.png new file mode 100644 index 000000000..62cb390a0 Binary files /dev/null and b/Documentation/Reviews/2022-10-31.ShareableHashedCollections/ShareableSet-model-diffing.png differ diff --git a/Documentation/Reviews/2022-10-31.ShareableHashedCollections/ShareableSet-sequential iteration.png b/Documentation/Reviews/2022-10-31.ShareableHashedCollections/ShareableSet-sequential iteration.png new file mode 100644 index 000000000..1bfe32d68 Binary files /dev/null and b/Documentation/Reviews/2022-10-31.ShareableHashedCollections/ShareableSet-sequential iteration.png differ diff --git a/Package.swift b/Package.swift index 04ea57660..f4e09a05b 100644 --- a/Package.swift +++ b/Package.swift @@ -1,9 +1,9 @@ -// swift-tools-version:5.3 +// swift-tools-version:5.7 //===----------------------------------------------------------------------===// // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 @@ -17,7 +17,7 @@ import PackageDescription // from the package manager command line: // // swift build -Xswiftc -DCOLLECTIONS_INTERNAL_CHECKS -var settings: [SwiftSetting]? = [ +var defines: [String] = [ // Enables internal consistency checks at the end of initializers and // mutating operations. This can have very significant overhead, so enabling @@ -26,7 +26,7 @@ var settings: [SwiftSetting]? = [ // This is mostly useful while debugging an issue with the implementation of // the hash table itself. This setting should never be enabled in production // code. -// .define("COLLECTIONS_INTERNAL_CHECKS"), +// "COLLECTIONS_INTERNAL_CHECKS", // Hashing collections provided by this package usually seed their hash // function with the address of the memory location of their storage, @@ -40,66 +40,271 @@ var settings: [SwiftSetting]? = [ // This is mostly useful while debugging an issue with the implementation of // the hash table itself. This setting should never be enabled in production // code. -// .define("COLLECTIONS_DETERMINISTIC_HASHING"), +// "COLLECTIONS_DETERMINISTIC_HASHING", + // Enables randomized testing of some data structure implementations. + "COLLECTIONS_RANDOMIZED_TESTING", + + // Enable this to build the sources as a single, large module. + // This removes the distinct modules for each data structure, instead + // putting them all directly into the `Collections` module. + // Note: This is a source-incompatible variation of the default configuration. +// "COLLECTIONS_SINGLE_MODULE", ] -// Prevent SPM 5.3 from throwing an error on empty settings arrays. -// (This has been fixed in 5.4.) -if settings?.isEmpty == true { settings = nil } +var _settings: [SwiftSetting] = defines.map { .define($0) } -let package = Package( - name: "swift-collections", - products: [ +struct CustomTarget { + enum Kind { + case exported + case hidden + case test + case testSupport + } + + var kind: Kind + var name: String + var dependencies: [Target.Dependency] + var directory: String + var exclude: [String] +} + +extension CustomTarget.Kind { + func path(for name: String) -> String { + switch self { + case .exported, .hidden: return "Sources/\(name)" + case .test, .testSupport: return "Tests/\(name)" + } + } + + var isTest: Bool { + switch self { + case .exported, .hidden: return false + case .test, .testSupport: return true + } + } +} + +extension CustomTarget { + static func target( + kind: Kind, + name: String, + dependencies: [Target.Dependency] = [], + directory: String? = nil, + exclude: [String] = [] + ) -> CustomTarget { + CustomTarget( + kind: kind, + name: name, + dependencies: dependencies, + directory: directory ?? name, + exclude: exclude) + } + + func toTarget() -> Target { + var linkerSettings: [LinkerSetting] = [] + if kind == .testSupport { + linkerSettings.append( + .linkedFramework("XCTest", .when(platforms: [.macOS, .iOS, .watchOS, .tvOS]))) + } + switch kind { + case .exported, .hidden, .testSupport: + return Target.target( + name: name, + dependencies: dependencies, + path: kind.path(for: directory), + exclude: exclude, + swiftSettings: _settings, + linkerSettings: linkerSettings) + case .test: + return Target.testTarget( + name: name, + dependencies: dependencies, + path: kind.path(for: directory), + exclude: exclude, + swiftSettings: _settings, + linkerSettings: linkerSettings) + } + } +} + +extension Array where Element == CustomTarget { + func toMonolithicTarget( + name: String, + linkerSettings: [LinkerSetting] = [] + ) -> Target { + let targets = self.filter { !$0.kind.isTest } + return Target.target( + name: name, + path: "Sources", + exclude: [ + "CMakeLists.txt", + "BitCollections/BitCollections.docc", + "Collections/Collections.docc", + "DequeModule/DequeModule.docc", + "HashTreeCollections/HashTreeCollections.docc", + "HeapModule/HeapModule.docc", + "OrderedCollections/OrderedCollections.docc", + ] + targets.flatMap { t in + t.exclude.map { "\(t.name)/\($0)" } + }, + sources: targets.map { "\($0.directory)" }, + swiftSettings: _settings, + linkerSettings: linkerSettings) + } + + func toMonolithicTestTarget( + name: String, + dependencies: [Target.Dependency] = [], + linkerSettings: [LinkerSetting] = [] + ) -> Target { + let targets = self.filter { $0.kind.isTest } + return Target.testTarget( + name: name, + dependencies: dependencies, + path: "Tests", + exclude: [ + "README.md", + ] + targets.flatMap { t in + t.exclude.map { "\(t.name)/\($0)" } + }, + sources: targets.map { "\($0.name)" }, + swiftSettings: _settings, + linkerSettings: linkerSettings) + } +} + +let targets: [CustomTarget] = [ + .target( + kind: .testSupport, + name: "_CollectionsTestSupport", + dependencies: ["_CollectionsUtilities"]), + .target( + kind: .test, + name: "CollectionsTestSupportTests", + dependencies: ["_CollectionsTestSupport"]), + .target( + kind: .hidden, + name: "_CollectionsUtilities", + exclude: [ + "CMakeLists.txt", + "Compatibility/UnsafeMutableBufferPointer+SE-0370.swift.gyb", + "Compatibility/UnsafeMutablePointer+SE-0370.swift.gyb", + "Compatibility/UnsafeRawPointer extensions.swift.gyb", + "Debugging.swift.gyb", + "Descriptions.swift.gyb", + "IntegerTricks/FixedWidthInteger+roundUpToPowerOfTwo.swift.gyb", + "IntegerTricks/Integer rank.swift.gyb", + "IntegerTricks/UInt+first and last set bit.swift.gyb", + "IntegerTricks/UInt+reversed.swift.gyb", + "RandomAccessCollection+Offsets.swift.gyb", + "Specialize.swift.gyb", + "UnsafeBitSet/_UnsafeBitSet+Index.swift.gyb", + "UnsafeBitSet/_UnsafeBitSet+_Word.swift.gyb", + "UnsafeBitSet/_UnsafeBitSet.swift.gyb", + "UnsafeBufferPointer+Extras.swift.gyb", + "UnsafeMutableBufferPointer+Extras.swift.gyb", + ]), + + .target( + kind: .exported, + name: "BitCollections", + dependencies: ["_CollectionsUtilities"], + exclude: ["CMakeLists.txt"]), + .target( + kind: .test, + name: "BitCollectionsTests", + dependencies: [ + "BitCollections", "_CollectionsTestSupport", "OrderedCollections" + ]), + + .target( + kind: .exported, + name: "DequeModule", + dependencies: ["_CollectionsUtilities"], + exclude: ["CMakeLists.txt"]), + .target( + kind: .test, + name: "DequeTests", + dependencies: ["DequeModule", "_CollectionsTestSupport"]), + + .target( + kind: .exported, + name: "HashTreeCollections", + dependencies: ["_CollectionsUtilities"], + exclude: ["CMakeLists.txt"]), + .target( + kind: .test, + name: "HashTreeCollectionsTests", + dependencies: ["HashTreeCollections", "_CollectionsTestSupport"]), + + .target( + kind: .exported, + name: "HeapModule", + dependencies: ["_CollectionsUtilities"], + exclude: ["CMakeLists.txt"]), + .target( + kind: .test, + name: "HeapTests", + dependencies: ["HeapModule", "_CollectionsTestSupport"]), + + .target( + kind: .exported, + name: "OrderedCollections", + dependencies: ["_CollectionsUtilities"], + exclude: ["CMakeLists.txt"]), + .target( + kind: .test, + name: "OrderedCollectionsTests", + dependencies: ["OrderedCollections", "_CollectionsTestSupport"]), + + .target( + kind: .exported, + name: "_RopeModule", + dependencies: ["_CollectionsUtilities"], + directory: "RopeModule", + exclude: ["CMakeLists.txt"]), + .target( + kind: .test, + name: "RopeModuleTests", + dependencies: ["_RopeModule", "_CollectionsTestSupport"]), + + .target( + kind: .exported, + name: "Collections", + dependencies: [ + "BitCollections", + "DequeModule", + "HashTreeCollections", + "HeapModule", + "OrderedCollections", + "_RopeModule", + ], + exclude: ["CMakeLists.txt"]) +] + +var _products: [Product] = [] +var _targets: [Target] = [] +if defines.contains("COLLECTIONS_SINGLE_MODULE") { + _products = [ .library(name: "Collections", targets: ["Collections"]), - .library(name: "DequeModule", targets: ["DequeModule"]), - .library(name: "OrderedCollections", targets: ["OrderedCollections"]), - ], - targets: [ - .target( - name: "Collections", - dependencies: [ - "DequeModule", - "OrderedCollections", - ], - path: "Sources/Collections", - exclude: ["CMakeLists.txt", "Collections.docc"], - swiftSettings: settings), - - // Testing support module - .target( - name: "_CollectionsTestSupport", - dependencies: [], - swiftSettings: settings, - linkerSettings: [ - .linkedFramework( - "XCTest", - .when(platforms: [.macOS, .iOS, .watchOS, .tvOS])), - ] - ), - .testTarget( - name: "CollectionsTestSupportTests", - dependencies: ["_CollectionsTestSupport"], - swiftSettings: settings), - - // Deque - .target( - name: "DequeModule", - exclude: ["CMakeLists.txt", "DequeModule.docc"], - swiftSettings: settings), - .testTarget( - name: "DequeTests", - dependencies: ["DequeModule", "_CollectionsTestSupport"], - swiftSettings: settings), - - // OrderedSet, OrderedDictionary - .target( - name: "OrderedCollections", - exclude: ["CMakeLists.txt", "OrderedCollections.docc"], - swiftSettings: settings), - .testTarget( - name: "OrderedCollectionsTests", - dependencies: ["OrderedCollections", "_CollectionsTestSupport"], - swiftSettings: settings), ] + _targets = [ + targets.toMonolithicTarget(name: "Collections"), + targets.toMonolithicTestTarget( + name: "CollectionsTests", + dependencies: ["Collections"]), + ] +} else { + _products = targets.compactMap { t in + guard t.kind == .exported else { return nil } + return .library(name: t.name, targets: [t.name]) + } + _targets = targets.map { $0.toTarget() } +} + +let package = Package( + name: "swift-collections", + products: _products, + targets: _targets ) diff --git a/Package@swift-5.5.swift b/Package@swift-5.5.swift deleted file mode 100644 index 7deefc6b5..000000000 --- a/Package@swift-5.5.swift +++ /dev/null @@ -1,100 +0,0 @@ -// swift-tools-version:5.5 -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Collections open source project -// -// Copyright (c) 2021-2022 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 -// -//===----------------------------------------------------------------------===// - -import PackageDescription - -// This package recognizes the conditional compilation flags listed below. -// To use enable them, uncomment the corresponding lines or define them -// from the package manager command line: -// -// swift build -Xswiftc -DCOLLECTIONS_INTERNAL_CHECKS -var settings: [SwiftSetting] = [ - - // Enables internal consistency checks at the end of initializers and - // mutating operations. This can have very significant overhead, so enabling - // this setting invalidates all documented performance guarantees. - // - // This is mostly useful while debugging an issue with the implementation of - // the hash table itself. This setting should never be enabled in production - // code. -// .define("COLLECTIONS_INTERNAL_CHECKS"), - - // Hashing collections provided by this package usually seed their hash - // function with the address of the memory location of their storage, - // to prevent some common hash table merge/copy operations from regressing to - // quadratic behavior. This setting turns off this mechanism, seeding - // the hash function with the table's size instead. - // - // When used in conjunction with the SWIFT_DETERMINISTIC_HASHING environment - // variable, this enables reproducible hashing behavior. - // - // This is mostly useful while debugging an issue with the implementation of - // the hash table itself. This setting should never be enabled in production - // code. -// .define("COLLECTIONS_DETERMINISTIC_HASHING"), -] - -let package = Package( - name: "swift-collections", - products: [ - .library(name: "Collections", targets: ["Collections"]), - .library(name: "DequeModule", targets: ["DequeModule"]), - .library(name: "OrderedCollections", targets: ["OrderedCollections"]), - ], - targets: [ - .target( - name: "Collections", - dependencies: [ - "DequeModule", - "OrderedCollections", - ], - path: "Sources/Collections", - exclude: ["CMakeLists.txt"], - swiftSettings: settings), - - // Testing support module - .target( - name: "_CollectionsTestSupport", - dependencies: [], - swiftSettings: settings, - linkerSettings: [ - .linkedFramework( - "XCTest", - .when(platforms: [.macOS, .iOS, .watchOS, .tvOS])), - ] - ), - .testTarget( - name: "CollectionsTestSupportTests", - dependencies: ["_CollectionsTestSupport"], - swiftSettings: settings), - - // Deque - .target( - name: "DequeModule", - exclude: ["CMakeLists.txt"], - swiftSettings: settings), - .testTarget( - name: "DequeTests", - dependencies: ["DequeModule", "_CollectionsTestSupport"], - swiftSettings: settings), - - // OrderedSet, OrderedDictionary - .target( - name: "OrderedCollections", - exclude: ["CMakeLists.txt"], - swiftSettings: settings), - .testTarget( - name: "OrderedCollectionsTests", - dependencies: ["OrderedCollections", "_CollectionsTestSupport"], - swiftSettings: settings), - ] -) diff --git a/README.md b/README.md index 0353cd5cb..c7b54497e 100644 --- a/README.md +++ b/README.md @@ -10,15 +10,33 @@ Read more about the package, and the intent behind it, in the [announcement on s The package currently provides the following implementations: +- [`BitSet`][BitSet] and [`BitArray`][BitArray], dynamic bit collections. + - [`Deque`][Deque], a double-ended queue backed by a ring buffer. Deques are range-replaceable, mutable, random-access collections. +- [`Heap`][Heap], a min-max heap backed by an array, suitable for use as a priority queue. + - [`OrderedSet`][OrderedSet], a variant of the standard `Set` where the order of items is well-defined and items can be arbitrarily reordered. Uses a `ContiguousArray` as its backing store, augmented by a separate hash table of bit packed offsets into it. - [`OrderedDictionary`][OrderedDictionary], an ordered variant of the standard `Dictionary`, providing similar benefits. +- [`TreeSet`][TreeSet] and [`TreeDictionary`][TreeDictionary], persistent hashed collections implementing Compressed Hash-Array Mapped Prefix Trees (CHAMP). These work similar to the standard `Set` and `Dictionary`, but they excel at use cases that mutate shared copies, offering dramatic memory savings and radical time improvements. + +[BitSet]: Documentation/BitSet.md +[BitArray]: Documentation/BitArray.md [Deque]: Documentation/Deque.md +[Heap]: Documentation/Heap.md [OrderedSet]: Documentation/OrderedSet.md [OrderedDictionary]: Documentation/OrderedDictionary.md +[TreeSet]: Documentation/TreeSet.md +[TreeDictionary]: Documentation/TreeDictionary.md + +The following data structures are currently under development but they aren't ready for inclusion in a tagged release: + +- [`SortedSet` and `SortedDictionary`](https://github.com/apple/swift-collections/pull/65), sorted collections backed by in-memory persistent b-trees. +- [`SparseSet`](https://github.com/apple/swift-collections/pull/80), a constant time set construct, trading off memory for speed. + +[Heap]: Documentation/Heap.md Swift Collections uses the same modularization approach as [**Swift Numerics**](https://github.com/apple/swift-numerics): it provides a standalone module for each thematic group of data structures it implements. For instance, if you only need a double-ended queue type, you can pull in only that by importing `DequeModule`. `OrderedSet` and `OrderedDictionary` share much of the same underlying implementation, so they are provided by a single module, called `OrderedCollections`. However, there is also a top-level `Collections` module that gives you every collection type with a single import statement: @@ -37,7 +55,7 @@ The Swift Collections package is source stable. The version numbers follow [Sema [semver]: https://semver.org -The public API of version 1.0 of the `swift-collections` package consists of non-underscored declarations that are marked `public` in the `Collections`, `DequeModule` and `OrderedCollections` modules. +The public API of version 1.1 of the `swift-collections` package consists of non-underscored declarations that are marked `public` in the `Collections`, `BitCollections`, `DequeModule`, `HeapModule`, `OrderedCollections` and `HashTreeCollections` modules. Interfaces that aren't part of the public API may continue to change in any release, including patch releases. If you have a use case that requires using underscored APIs, please [submit a Feature Request][enhancement] describing it! We'd like the public interface to be as useful as possible -- although preferably without compromising safety or limiting future evolution. @@ -53,14 +71,24 @@ Note that contents of the `Tests`, `Utils` and `Benchmarks` subdirectories aren' Future minor versions of the package may update these rules as needed. -We'd like this package to quickly embrace Swift language and toolchain improvements that are relevant to its mandate. Accordingly, from time to time, we expect that new versions of this package will require clients to upgrade to a more recent Swift toolchain release. (This allows the package to make use of new language/stdlib features, build on compiler bug fixes, and adopt new package manager functionality as soon as they are available.) Requiring a new Swift release will only need a minor version bump. +We'd like this package to quickly embrace Swift language and toolchain improvements that are relevant to its mandate. Accordingly, from time to time, new versions of this package require clients to upgrade to a more recent Swift toolchain release. (This allows the package to make use of new language/stdlib features, build on compiler bug fixes, and adopt new package manager functionality as soon as they are available.) Patch (i.e., bugfix) releases will not increase the required toolchain version, but any minor (i.e., new feature) release may do so. + +The following table maps existing package releases to their minimum required Swift toolchain release: + +| Package version | Swift version | Xcode release | +| ----------------------- | --------------- | ------------- | +| swift-collections 1.0.x | >= Swift 5.3.2 | >= Xcode 12.4 | +| swift-collections 1.1.x | >= Swift 5.7.2 | >= Xcode 14.2 | + +(Note: the package has no minimum deployment target, so while it does require clients to use a recent Swift toolchain to build it, the code itself is able to run on any OS release that supports running Swift code.) + ## Using **Swift Collections** in your project To use this package in a SwiftPM project, you need to set it up as a package dependency: ```swift -// swift-tools-version:5.6 +// swift-tools-version:5.9 import PackageDescription let package = Package( @@ -68,7 +96,7 @@ let package = Package( dependencies: [ .package( url: "https://github.com/apple/swift-collections.git", - .upToNextMajor(from: "1.0.3") // or `.upToNextMinor + .upToNextMinor(from: "1.1.0") // or `.upToNextMajor ) ], targets: [ @@ -92,7 +120,7 @@ If you find something that looks like a bug, please open a [Bug Report][bugrepor ### Working on the package -We have some basic [documentation on package internals](./Documentation/Development/Internals/) that will help you get started. +We have some basic [documentation on package internals](./Documentation/Internals/README.md) that will help you get started. By submitting a pull request, you represent that you have the right to license your contribution to Apple and the community, and agree by submitting the patch that your contributions are licensed under the [Swift License](https://swift.org/LICENSE.txt), a copy of which is [provided in this repository](LICENSE.txt). @@ -117,11 +145,20 @@ By submitting a pull request, you represent that you have the right to license y #### Proposing the addition of a new data structure -1. Start a topic on the [forum], explaining why you believe it would be important to implement the data structure. This way we can figure out if it would be right for the package, discuss implementation strategies, and plan to allocate capacity to help. -2. When maintainers agreed to your implementation plan, start work on it, and submit a PR with your implementation as soon as you have something that's ready to show! We'd love to get involved as early as you like. -3. Participate in the review discussion, and adapt the code accordingly. Sometimes we may need to go through several revisions! This is fine -- it makes the end result that much better. -3. When there is a consensus that the feature is ready, and the implementation is fully tested and documented, the PR will be merged by a maintainer. -4. Celebrate! You've achieved something great! +We intend this package to collect generally useful data structures -- the ones that ought to be within easy reach of every Swift engineer's basic toolbox. The implementations we ship need to be of the highest technical quality, polished to the same shine as anything that gets included in the Swift Standard Library. (The only real differences are that this package isn't under the formal Swift Evolution process, and its code isn't ABI stable.) + +Accordingly, adding a new data structure to this package is not an easy or quick process, and not all useful data structures are going to be a good fit. + +If you have an idea for a data structure that might make a good addition to this package, please start a topic on the [forum], explaining why you believe it would be important to implement it. This way we can figure out if it would be right for the package, discuss implementation strategies, and plan to allocate capacity to help. + +Not all data structures will reach a high enough level of usefulness to ship in this package -- those that have a more limited audience might work better as a standalone package. Of course, reasonable people might disagree on the importance of including any particular data structure; but at the end of the day, the decision whether to take an implementation is up to the maintainers of this package. + +If maintainers have agreed that your implementation would likely make a good addition, then it's time to start work on it. Submit a PR with your implementation as soon as you have something that's ready to show! We'd love to get involved as early as you like. Historically, the best additions resulted from close work between the contributor and a package maintainer. + +Participate in the review discussion, and adapt code accordingly. Sometimes we may need to go through several revisions over multiple months! This is fine -- it makes the end result that much better. When there is a consensus that the feature is ready, and the implementation is fully tested and documented, the PR will be merged by a maintainer. This is good time for a small celebration -- merging is a good indicator that the addition will ship at some point. + +Historically, PRs adding a new data structure have typically been merged to a new feature branch rather than directly to a release branch or `main`, and there was an extended amount of time between the initial merge and the tag that shipped the new feature. Nobody likes to wait, but getting a new data structure implementation from a state that was ready to merge to a state that's ready to ship is actually quite difficult work, and it takes maintainer time and effort that needs to be scheduled in advance. The closer an implementation is to the coding conventions and performance baseline of the Standard Library, the shorter this wait is likely to become, and the fewer changes there will be between merging and shipping. + ### Code of Conduct @@ -134,4 +171,3 @@ Like all Swift.org projects, we would like the Swift Collections project to fost The current code owner of this package is Karoy Lorentey ([@lorentey](https://github.com/lorentey)). You can contact him [on the Swift forums](https://forums.swift.org/u/lorentey/summary), or by writing an email to klorentey at apple dot com. (Please keep it related to this project.) In case of moderation issues, you can also directly contact a member of the [Swift Core Team](https://swift.org/community/#community-structure). - diff --git a/Sources/BitCollections/BitArray/BitArray+BitwiseOperations.swift b/Sources/BitCollections/BitArray/BitArray+BitwiseOperations.swift new file mode 100644 index 000000000..2db5d9615 --- /dev/null +++ b/Sources/BitCollections/BitArray/BitArray+BitwiseOperations.swift @@ -0,0 +1,194 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if false +// FIXME: Bitwise operators disabled for now. I have two concerns: +// 1. We need to support bitwise operations over slices of bit arrays, not just +// whole arrays. +// 2. We need to put in-place mutations as the primary operation, and they +// need to avoid copy-on-write copies unless absolutely necessary. +// +// It seems unlikely that the operator syntax will survive these points. +// We have five (5!) separate cases: +// +// foo |= bar +// foo[i ..< j] |= bar +// foo |= bar[u ..< v] +// foo[i ..< j] |= bar[u ..< v] +// foo[i ..< j] |= foo[k ..< l] +// +// The last one where the array is ORed with itself is particularly problematic +// -- like memcpy, these operations can easily support overlapping inputs, but +// it doesn't seem likely we can implement that with this nice slicing syntax, +// unless we are okay with forcing a CoW copy. (Which we aren't.) +// +// Even ignoring that, I would not like to end up with four overloads for each +// operator, especially not for such niche operations. So we'll entirely disable +// these for now, to prevent any shipping API from interfering with an eventual +// redesign. (This is an active area of experimentation, as it will potentially +// also affect our non-copyable container design.) + +extension BitArray { + /// Stores the result of performing a bitwise OR operation on two + /// equal-sized bit arrays in the left-hand-side variable. + /// + /// - Parameter left: A bit array. + /// - Parameter right: Another bit array of the same size. + /// - Complexity: O(left.count) + public static func |=(left: inout Self, right: Self) { + precondition(left.count == right.count) + left._update { target in + right._read { source in + for i in 0 ..< target._words.count { + target._mutableWords[i].formUnion(source._words[i]) + } + } + } + left._checkInvariants() + } + + /// Returns the result of performing a bitwise OR operation on two + /// equal-sized bit arrays. + /// + /// - Parameter left: A bit array. + /// - Parameter right: Another bit array of the same size. + /// - Returns: The bitwise OR of `left` and `right`. + /// - Complexity: O(left.count) + public static func |(left: Self, right: Self) -> Self { + precondition(left.count == right.count) + var result = left + result |= right + return result + } + + /// Stores the result of performing a bitwise AND operation on two given + /// equal-sized bit arrays in the left-hand-side variable. + /// + /// - Parameter left: A bit array. + /// - Parameter right: Another bit array of the same size. + /// - Complexity: O(left.count) + public static func &=(left: inout Self, right: Self) { + precondition(left.count == right.count) + left._update { target in + right._read { source in + for i in 0 ..< target._words.count { + target._mutableWords[i].formIntersection(source._words[i]) + } + } + } + left._checkInvariants() + } + + /// Returns the result of performing a bitwise AND operation on two + /// equal-sized bit arrays. + /// + /// - Parameter left: A bit array. + /// - Parameter right: Another bit array of the same size. + /// - Returns: The bitwise AND of `left` and `right`. + /// - Complexity: O(left.count) + public static func &(left: Self, right: Self) -> Self { + precondition(left.count == right.count) + var result = left + result &= right + return result + } + + /// Stores the result of performing a bitwise XOR operation on two given + /// equal-sized bit arrays in the left-hand-side variable. + /// + /// - Parameter left: A bit array. + /// - Parameter right: Another bit array of the same size. + /// - Complexity: O(left.count) + public static func ^=(left: inout Self, right: Self) { + precondition(left.count == right.count) + left._update { target in + right._read { source in + for i in 0 ..< target._words.count { + target._mutableWords[i].formSymmetricDifference(source._words[i]) + } + } + } + left._checkInvariants() + } + + /// Returns the result of performing a bitwise XOR operation on two + /// equal-sized bit arrays. + /// + /// - Parameter left: A bit array. + /// - Parameter right: Another bit array of the same size. + /// - Returns: The bitwise XOR of `left` and `right`. + /// - Complexity: O(left.count) + public static func ^(left: Self, right: Self) -> Self { + precondition(left.count == right.count) + var result = left + result ^= right + return result + } +} + +extension BitArray { + /// Returns the complement of the given bit array. + /// + /// - Parameter value: A bit array. + /// - Returns: A bit array constructed by flipping each bit in `value`. + /// flipped. + /// - Complexity: O(value.count) + public static prefix func ~(value: Self) -> Self { + var result = value + result.toggleAll() + return result + } +} +#endif + +extension BitArray { + public mutating func toggleAll() { + _update { handle in + let w = handle._mutableWords + for i in 0 ..< handle._words.count { + w[i].formComplement() + } + let p = handle.end + if p.bit > 0 { + w[p.word].subtract(_Word(upTo: p.bit).complement()) + } + } + _checkInvariants() + } + + public mutating func toggleAll(in range: Range) { + precondition(range.upperBound <= count, "Range out of bounds") + guard !range.isEmpty else { return } + _update { handle in + let words = handle._mutableWords + let start = _BitPosition(range.lowerBound) + let end = _BitPosition(range.upperBound) + if start.word == end.word { + let bits = _Word(from: start.bit, to: end.bit) + words[start.word].formSymmetricDifference(bits) + return + } + words[start.word].formSymmetricDifference( + _Word(upTo: start.bit).complement()) + for i in stride(from: start.word + 1, to: end.word, by: 1) { + words[i].formComplement() + } + if end.bit > 0 { + words[end.word].formSymmetricDifference(_Word(upTo: end.bit)) + } + } + } + + @inlinable + public mutating func toggleAll(in range: some RangeExpression) { + toggleAll(in: range.relative(to: self)) + } +} diff --git a/Sources/BitCollections/BitArray/BitArray+ChunkedBitsIterators.swift b/Sources/BitCollections/BitArray/BitArray+ChunkedBitsIterators.swift new file mode 100644 index 000000000..6f1e1f015 --- /dev/null +++ b/Sources/BitCollections/BitArray/BitArray+ChunkedBitsIterators.swift @@ -0,0 +1,100 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +internal struct _ChunkedBitsForwardIterator { + internal typealias _BitPosition = _UnsafeBitSet.Index + + internal let words: UnsafeBufferPointer<_Word> + internal let end: _BitPosition + internal var position: _BitPosition + + internal init( + words: UnsafeBufferPointer<_Word>, + range: Range + ) { + assert(range.lowerBound >= 0) + assert(range.upperBound <= words.count * _Word.capacity) + self.words = words + self.end = _BitPosition(range.upperBound) + self.position = _BitPosition(range.lowerBound) + } + + mutating func next() -> (bits: _Word, count: UInt)? { + guard position < end else { return nil } + let (w, b) = position.split + if w == end.word { + position = end + return ( + bits: words[w] + .intersection(_Word(upTo: end.bit)) + .shiftedDown(by: b), + count: end.bit - b) + } + let c = UInt(_Word.capacity) - b + position.value += c + return (bits: words[w].shiftedDown(by: b), count: c) + } +} + +internal struct _ChunkedBitsBackwardIterator { + internal typealias _BitPosition = _UnsafeBitSet.Index + + internal let words: UnsafeBufferPointer<_Word> + internal let start: _BitPosition + internal var position: _BitPosition + + internal init( + words: UnsafeBufferPointer<_Word>, + range: Range + ) { + assert(range.lowerBound >= 0) + assert(range.upperBound <= words.count * _Word.capacity) + self.words = words + self.start = _BitPosition(range.lowerBound) + self.position = _BitPosition(range.upperBound) + } + + internal mutating func next() -> (bits: _Word, count: UInt)? { + guard position > start else { return nil } + let (w, b) = position.endSplit + if w == start.word { + position = start + return ( + bits: words[w] + .intersection(_Word(upTo: b)) + .shiftedDown(by: start.bit), + count: b - start.bit) + } + let c = b + position.value -= c + return (bits: words[w].intersection(_Word(upTo: b)), count: c) + } +} + +extension IteratorProtocol where Element == Bool { + mutating func _nextChunk( + maximumCount: UInt = UInt(_Word.capacity) + ) -> (bits: _Word, count: UInt) { + assert(maximumCount <= _Word.capacity) + var bits = _Word.empty + var c: UInt = 0 + while let v = next() { + if v { bits.insert(c) } + c += 1 + if c == maximumCount { break } + } + return (bits, c) + } +} diff --git a/Sources/BitCollections/BitArray/BitArray+Codable.swift b/Sources/BitCollections/BitArray/BitArray+Codable.swift new file mode 100644 index 000000000..18c765ede --- /dev/null +++ b/Sources/BitCollections/BitArray/BitArray+Codable.swift @@ -0,0 +1,73 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 BitArray: Codable { + /// Encodes this bit array into the given encoder. + /// + /// Bit arrays are encoded as an unkeyed container of `UInt64` values, + /// representing the total number of bits in the array, followed by + /// UInt64-sized pieces of the underlying bitmap. + /// + /// - Parameter encoder: The encoder to write data to. + public func encode(to encoder: Encoder) throws { + var container = encoder.unkeyedContainer() + try container.encode(UInt64(truncatingIfNeeded: _count)) + try _storage._encodeAsUInt64(to: &container) + } + + /// Creates a new bit array by decoding from the given decoder. + /// + /// Bit arrays are encoded as an unkeyed container of `UInt64` values, + /// representing the total number of bits in the array, followed by + /// UInt64-sized pieces of the underlying bitmap. + /// + /// - Parameter decoder: The decoder to read data from. + public init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + guard let count = UInt(exactly: try container.decode(UInt64.self)) else { + let context = DecodingError.Context( + codingPath: container.codingPath, + debugDescription: "Bit Array too long") + throw DecodingError.dataCorrupted(context) + } + var words = try [_Word]( + _fromUInt64: &container, + reservingCount: container.count.map { Swift.max(1, $0) - 1 }) + if _Word.capacity < UInt64.bitWidth, + count <= (words.count - 1) * _Word.capacity + { + let last = words.removeLast() + guard last.isEmpty else { + let context = DecodingError.Context( + codingPath: container.codingPath, + debugDescription: "Unexpected bits after end") + throw DecodingError.dataCorrupted(context) + } + } + guard + count <= words.count * _Word.capacity, + count > (words.count - 1) * _Word.capacity + else { + let context = DecodingError.Context( + codingPath: container.codingPath, + debugDescription: "Decoded words don't match expected count") + throw DecodingError.dataCorrupted(context) + } + let bit = _BitPosition(count).bit + if bit > 0, !words.last!.subtracting(_Word(upTo: bit)).isEmpty { + let context = DecodingError.Context( + codingPath: container.codingPath, + debugDescription: "Unexpected bits after end") + throw DecodingError.dataCorrupted(context) + } + self.init(_storage: words, count: count) + } +} diff --git a/Sources/BitCollections/BitArray/BitArray+Collection.swift b/Sources/BitCollections/BitArray/BitArray+Collection.swift new file mode 100644 index 000000000..93e63cdd2 --- /dev/null +++ b/Sources/BitCollections/BitArray/BitArray+Collection.swift @@ -0,0 +1,188 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 BitArray: Sequence { + /// The Boolean type representing the bit array's elements. + public typealias Element = Bool + /// The iterator type for a bit array. + public typealias Iterator = IndexingIterator +} + +extension BitArray: RandomAccessCollection, MutableCollection { + /// The type representing a position in a bit array. + public typealias Index = Int + + /// A collection representing a contiguous subrange of this collection's + /// elements. The subsequence shares indices with the original collection. + /// + /// The subsequence type for bit arrays is the default `Slice`. + public typealias SubSequence = Slice + + /// A type that represents the indices that are valid for subscripting the + /// collection, in ascending order. + public typealias Indices = Range + + /// The number of elements in the bit array. + /// + /// - Complexity: O(1) + @inlinable + public var count: Int { + Int(_count) + } + + /// The position of the first element in a nonempty bit array, or `endIndex` + /// if the array is empty. + /// + /// - Complexity: O(1) + @inlinable @inline(__always) + public var startIndex: Int { 0 } + + /// The collection’s “past the end” position--that is, the position one step + /// after the last valid subscript argument. + /// + /// - Complexity: O(1) + @inlinable @inline(__always) + public var endIndex: Int { count } + + /// Returns the position immediately after the given index. + /// + /// - Parameter `index`: A valid index of the bit set. `index` must be less than `endIndex`. + /// + /// - Returns: The valid index immediately after `index`. + /// + /// - Complexity: O(1) + @inlinable @inline(__always) + public func index(after i: Int) -> Int { i + 1 } + + /// Returns the position immediately before the given index. + /// + /// - Parameter `index`: A valid index of the bit set. `index` must be greater + /// than `startIndex`. + /// + /// - Returns: The valid index immediately before `index`. + /// + /// - Complexity: O(1) + @inlinable @inline(__always) + public func index(before i: Int) -> Int { i - 1 } + + /// Replaces the given index with its successor. + /// + /// - Parameter i: A valid index of the collection. `i` must be less than + /// `endIndex`. + /// + /// - Complexity: O(1) + @inlinable @inline(__always) + public func formIndex(after i: inout Int) { + i += 1 + } + + /// Replaces the given index with its predecessor. + /// + /// - Parameter i: A valid index of the collection. `i` must be greater than + /// `startIndex`. + /// + /// - Complexity: O(1) + @inlinable @inline(__always) + public func formIndex(before i: inout Int) { + i -= 1 + } + + /// Returns an index that is the specified distance from the given index. + /// + /// The value passed as `distance` must not offset `i` beyond the bounds of + /// the collection. + /// + /// - Parameters: + /// - i: A valid index of the collection. + /// - distance: The distance to offset `i`. + /// - Returns: An index offset by `distance` from the index `i`. If + /// `distance` is positive, this is the same value as the result of + /// `distance` calls to `index(after:)`. If `distance` is negative, this + /// is the same value as the result of `abs(distance)` calls to + /// `index(before:)`. + /// + /// - Complexity: O(1) + @inlinable @inline(__always) + public func index(_ i: Int, offsetBy distance: Int) -> Int { + i + distance + } + + /// Returns an index that is the specified distance from the given index, + /// unless that distance is beyond a given limiting index. + /// + /// The value passed as `distance` must not offset `i` beyond the bounds of + /// the collection, unless the index passed as `limit` prevents offsetting + /// beyond those bounds. + /// + /// - Parameters: + /// - i: A valid index of the collection. + /// - distance: The distance to offset `i`. + /// - limit: A valid index of the collection to use as a limit. If + /// `distance > 0`, a limit that is less than `i` has no effect. + /// Likewise, if `distance < 0`, a limit that is greater than `i` has no + /// effect. + /// - Returns: An index offset by `distance` from the index `i`, unless that + /// index would be beyond `limit` in the direction of movement. In that + /// case, the method returns `nil`. + /// + /// - Complexity: O(1) + @inlinable + public func index( + _ i: Index, offsetBy distance: Int, limitedBy limit: Index + ) -> Index? { + let l = self.distance(from: i, to: limit) + if distance > 0 ? l >= 0 && l < distance : l <= 0 && distance < l { + return nil + } + return index(i, offsetBy: distance) + } + + /// Returns the distance between two indices. + /// + /// - Parameters: + /// - start: A valid index of the collection. + /// - end: Another valid index of the collection. If `end` is equal to + /// `start`, the result is zero. + /// - Returns: The distance between `start` and `end`. + /// + /// - Complexity: O(1) + @inlinable @inline(__always) + public func distance(from start: Int, to end: Int) -> Int { + end - start + } + + /// Accesses the element at the specified position. + /// + /// You can subscript a collection with any valid index other than the + /// collection's end index. The end index refers to the position one past + /// the last element of a collection, so it doesn't correspond with an + /// element. + /// + /// - Parameter position: The position of the element to access. `position` + /// must be a valid index of the collection that is not equal to the + /// `endIndex` property. + /// + /// - Complexity: O(1) + public subscript(position: Int) -> Bool { + get { + precondition(position >= 0 && position < _count, "Index out of bounds") + return _read { handle in + handle[position] + } + } + set { + precondition(position >= 0 && position < _count, "Index out of bounds") + return _update { handle in + handle[position] = newValue + } + } + } +} diff --git a/Sources/BitCollections/BitArray/BitArray+Copy.swift b/Sources/BitCollections/BitArray/BitArray+Copy.swift new file mode 100644 index 000000000..b261a2d9f --- /dev/null +++ b/Sources/BitCollections/BitArray/BitArray+Copy.swift @@ -0,0 +1,176 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension BitArray { + internal mutating func _copy( + from range: Range, + to target: Int + ) { + _update { handle in + handle.copy(from: range, to: target) + } + } + + internal mutating func _copy( + from range: Range, + in source: UnsafeBufferPointer<_Word>, + to target: Int + ) { + _update { handle in + handle.copy(from: range, in: source, to: target) + } + } + + internal mutating func _copy( + from source: BitArray, + to target: Int + ) { + _copy(from: source[...], to: target) + } + + internal mutating func _copy( + from source: BitArray.SubSequence, + to target: Int + ) { + let range = source._bounds + source.base._storage.withUnsafeBufferPointer { words in + self._copy(from: range, in: words, to: target) + } + } + + internal mutating func _copy( + from source: S, + to range: Range + ) where S.Element == Bool { + _update { $0.copy(from: source, to: range) } + } +} + +extension BitArray._UnsafeHandle { + internal mutating func _copy( + bits: _Word, count: UInt, to target: _BitPosition + ) { + assert(count <= _Word.capacity) + assert(count == _Word.capacity || bits.shiftedDown(by: count).isEmpty) + assert(target.value + count <= _count) + let start = target.split + let end = _BitPosition(target.value + count).endSplit + let words = _mutableWords + if start.word == end.word { + let mask = _Word(from: start.bit, to: end.bit) + words[start.word].formIntersection(mask.complement()) + words[start.word].formUnion(bits.shiftedUp(by: start.bit)) + return + } + assert(start.word + 1 == end.word) + words[start.word].formIntersection(_Word(upTo: start.bit)) + words[start.word].formUnion(bits.shiftedUp(by: start.bit)) + words[end.word].formIntersection(_Word(upTo: end.bit).complement()) + words[end.word].formUnion( + bits.shiftedDown(by: UInt(_Word.capacity) &- start.bit)) + } + + internal mutating func copy( + from range: Range, + to target: Int + ) { + assert( + range.lowerBound >= 0 && range.upperBound <= self.count, + "Source range out of bounds") + copy(from: range, in: _words, to: target) + } + + internal mutating func copy( + from range: Range, + in source: UnsafeBufferPointer<_Word>, + to target: Int + ) { + ensureMutable() + assert( + range.lowerBound >= 0 && range.upperBound <= source.count * _Word.capacity, + "Source range out of bounds") + assert( + target >= 0 && target + range.count <= count, + "Target out of bounds") + guard !range.isEmpty else { return } + + func goForward() -> Bool { + let target = _BitPosition(target).split + let lowerSource = _BitPosition(range.lowerBound).split + let upperSource = _BitPosition(range.upperBound).endSplit + + + let targetPtr = _words._ptr(at: target.word) + let lowerSourcePtr = source._ptr(at: lowerSource.word) + let upperSourcePtr = source._ptr(at: upperSource.word) + + if targetPtr < lowerSourcePtr || targetPtr > upperSourcePtr { + return true + } + if targetPtr == lowerSourcePtr, target.bit < lowerSource.bit { + return true + } + if targetPtr == upperSourcePtr, target.bit >= upperSource.bit { + return true + } + return false + } + + if goForward() { + // Copy forward from a disjoint or following overlapping range. + var src = _ChunkedBitsForwardIterator(words: source, range: range) + var dst = _BitPosition(target) + while let (bits, count) = src.next() { + _copy(bits: bits, count: count, to: dst) + dst.value += count + } + assert(dst.value == target + range.count) + } else { + // Copy backward from a non-following overlapping range. + var src = _ChunkedBitsBackwardIterator(words: source, range: range) + var dst = _BitPosition(target + range.count) + while let (bits, count) = src.next() { + dst.value -= count + _copy(bits: bits, count: count, to: dst) + } + assert(dst.value == target) + } + } +} + +extension BitArray._UnsafeHandle { + internal mutating func copy( + from source: some Sequence, + to range: Range + ) { + assert(range.lowerBound >= 0 && range.upperBound <= self.count) + var pos = _BitPosition(range.lowerBound) + var it = source.makeIterator() + if pos.bit > 0 { + let (bits, count) = it._nextChunk( + maximumCount: UInt(_Word.capacity) - pos.bit) + _copy(bits: bits, count: count, to: pos) + pos.value += count + } + while true { + let (bits, count) = it._nextChunk() + guard count > 0 else { break } + assert(pos.bit == 0) + _copy(bits: bits, count: count, to: pos) + pos.value += count + } + precondition(pos.value == range.upperBound) + } +} diff --git a/Sources/BitCollections/BitArray/BitArray+CustomReflectable.swift b/Sources/BitCollections/BitArray/BitArray+CustomReflectable.swift new file mode 100644 index 000000000..e3bdae418 --- /dev/null +++ b/Sources/BitCollections/BitArray/BitArray+CustomReflectable.swift @@ -0,0 +1,17 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 BitArray: CustomReflectable { + /// The custom mirror for this instance. + public var customMirror: Mirror { + Mirror(self, unlabeledChildren: self, displayStyle: .collection) + } +} diff --git a/Sources/BitCollections/BitArray/BitArray+Descriptions.swift b/Sources/BitCollections/BitArray/BitArray+Descriptions.swift new file mode 100644 index 000000000..97e44433b --- /dev/null +++ b/Sources/BitCollections/BitArray/BitArray+Descriptions.swift @@ -0,0 +1,85 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 UInt8 { + @inline(__always) + internal static var _ascii0: Self { 48 } + + @inline(__always) + internal static var _ascii1: Self { 49 } + + @inline(__always) + internal static var _asciiLT: Self { 60 } + + @inline(__always) + internal static var _asciiGT: Self { 62 } +} + +extension BitArray: CustomStringConvertible { + /// A textual representation of this instance. + /// Bit arrays print themselves as a string of binary bits, with the + /// highest-indexed elements appearing first, as in the binary representation + /// of integers. The digits are surrounded by angle brackets, so that the + /// notation is non-ambigous even for empty bit arrays: + /// + /// let bits: BitArray = [false, false, false, true, true] + /// print(bits) // "<11000>" + /// + /// let empty = BitArray() + /// print(empty) // "<>" + public var description: String { + _bitString + } +} + +extension BitArray: CustomDebugStringConvertible { + /// A textual representation of this instance. + /// Bit arrays print themselves as a string of binary bits, with the + /// highest-indexed elements appearing first, as in the binary representation + /// of integers: + /// + /// let bits: BitArray = [false, false, false, true, true] + /// print(bits) // "<11000>" + /// + /// The digits are surrounded by angle brackets to serve as visual delimiters. + /// (So that empty bit arrays still have a non-empty description.) + public var debugDescription: String { + description + } +} + +extension BitArray { + internal var _bitString: String { + guard !isEmpty else { return "<>" } + var result: String + if #available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) { + result = String(unsafeUninitializedCapacity: self.count + 2) { target in + target.initializeElement(at: count + 1, to: ._asciiGT) + var i = count + for v in self { + target.initializeElement(at: i, to: v ? ._ascii1 : ._ascii0) + i &-= 1 + } + assert(i == 0) + target.initializeElement(at: 0, to: ._asciiLT) + return count + 2 + } + } else { + result = "<" + result.reserveCapacity(self.count + 2) + for v in self.reversed() { + result.append(v ? "1" : "0") + } + result.append(">") + } + return result + } +} diff --git a/Sources/BitCollections/BitArray/BitArray+Equatable.swift b/Sources/BitCollections/BitArray/BitArray+Equatable.swift new file mode 100644 index 000000000..a045b322f --- /dev/null +++ b/Sources/BitCollections/BitArray/BitArray+Equatable.swift @@ -0,0 +1,25 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 BitArray: Equatable { + /// Returns a Boolean value indicating whether two values are equal. + /// + /// Equality is the inverse of inequality. For any values `a` and `b`, + /// `a == b` implies that `a != b` is `false`. + /// + /// - Parameter lhs: A value to compare. + /// - Parameter rhs: Another value to compare. + /// - Complexity: O(left.count) + public static func ==(left: Self, right: Self) -> Bool { + guard left._count == right._count else { return false } + return left._storage == right._storage + } +} diff --git a/Sources/BitCollections/BitArray/BitArray+ExpressibleByArrayLiteral.swift b/Sources/BitCollections/BitArray/BitArray+ExpressibleByArrayLiteral.swift new file mode 100644 index 000000000..7bfc836f2 --- /dev/null +++ b/Sources/BitCollections/BitArray/BitArray+ExpressibleByArrayLiteral.swift @@ -0,0 +1,18 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 BitArray: ExpressibleByArrayLiteral { + /// Creates an instance initialized with the given elements. + @inlinable + public init(arrayLiteral elements: Bool...) { + self.init(elements) + } +} diff --git a/Sources/BitCollections/BitArray/BitArray+ExpressibleByStringLiteral.swift b/Sources/BitCollections/BitArray/BitArray+ExpressibleByStringLiteral.swift new file mode 100644 index 000000000..db7b96379 --- /dev/null +++ b/Sources/BitCollections/BitArray/BitArray+ExpressibleByStringLiteral.swift @@ -0,0 +1,21 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 BitArray: ExpressibleByStringLiteral { + /// Creates an instance initialized with the given elements. + @inlinable + public init(stringLiteral value: String) { + guard let bits = Self(value) else { + fatalError("Invalid bit array literal") + } + self = bits + } +} diff --git a/Sources/BitCollections/BitArray/BitArray+Extras.swift b/Sources/BitCollections/BitArray/BitArray+Extras.swift new file mode 100644 index 000000000..6a41c875e --- /dev/null +++ b/Sources/BitCollections/BitArray/BitArray+Extras.swift @@ -0,0 +1,51 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 BitArray { + /// Set the count of this bit array to `count`, by either truncating it or + /// extending it by appending the specified Boolean value (`false` by default). + /// + /// - Parameter count: The desired count of the bit array on return. + /// - Parameter padding: The value to append the array if the array is + /// currently too short. + public mutating func truncateOrExtend( + toCount count: Int, + with padding: Bool = false + ) { + precondition(count >= 0, "Negative count") + if count < _count { + _removeLast(self.count - count) + } else if count > _count { + _extend(by: count - self.count, with: padding) + } + } +} + +extension BitArray { + public mutating func insert( + repeating value: Bool, + count: Int, + at index: Int + ) { + precondition(count >= 0, "Can't insert a negative number of values") + precondition(index >= 0 && index <= count, "Index out of bounds") + guard count > 0 else { return } + _extend(by: count, with: false) + _copy(from: index ..< self.count - count, to: index + count) + fill(in: index ..< index + count, with: value) + } + + public mutating func append(repeating value: Bool, count: Int) { + precondition(count >= 0, "Can't append a negative number of values") + guard count > 0 else { return } + _extend(by: count, with: value) + } +} diff --git a/Sources/BitCollections/BitArray/BitArray+Fill.swift b/Sources/BitCollections/BitArray/BitArray+Fill.swift new file mode 100644 index 000000000..65c18e18c --- /dev/null +++ b/Sources/BitCollections/BitArray/BitArray+Fill.swift @@ -0,0 +1,46 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 BitArray { + /// Set every bit of this array to `value` (`true` by default). + /// + /// - Parameter value: The Boolean value to which to set the array's elements. + public mutating func fill(with value: Bool = true) { + fill(in: Range(uncheckedBounds: (0, count)), with: value) + } + + /// Set every bit of this array within the specified range to `value` + /// (`true` by default). + /// + /// - Parameter range: The range whose elements to overwrite. + /// - Parameter value: The Boolean value to which to set the array's elements. + public mutating func fill(in range: Range, with value: Bool = true) { + _update { handle in + if value { + handle.fill(in: range) + } else { + handle.clear(in: range) + } + } + } + + /// Set every bit of this array within the specified range to `value` + /// (`true` by default). + /// + /// - Parameter range: The range whose elements to overwrite. + /// - Parameter value: The Boolean value to which to set the array's elements. + public mutating func fill( + in range: some RangeExpression, + with value: Bool = true + ) { + fill(in: range.relative(to: self), with: value) + } +} diff --git a/Sources/BitCollections/BitArray/BitArray+Hashable.swift b/Sources/BitCollections/BitArray/BitArray+Hashable.swift new file mode 100644 index 000000000..dada9bcdd --- /dev/null +++ b/Sources/BitCollections/BitArray/BitArray+Hashable.swift @@ -0,0 +1,24 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 BitArray: Hashable { + /// Hashes the essential components of this value by feeding them into the + /// given hasher. + /// + /// - Parameter hasher: The hasher to use when combining the components + /// of this instance. + public func hash(into hasher: inout Hasher) { + hasher.combine(_count) + for word in _storage { + hasher.combine(word) + } + } +} diff --git a/Sources/BitCollections/BitArray/BitArray+Initializers.swift b/Sources/BitCollections/BitArray/BitArray+Initializers.swift new file mode 100644 index 000000000..4bf604b41 --- /dev/null +++ b/Sources/BitCollections/BitArray/BitArray+Initializers.swift @@ -0,0 +1,271 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 BitArray { + /// Initialize a bit array from a bit set. + /// + /// The result contains exactly as many bits as the maximum item in + /// the source set, plus one. If the set is empty, the result will + /// be empty, too. + /// + /// BitArray([] as BitSet) // (empty) + /// BitArray([0] as BitSet) // 1 + /// BitArray([1] as BitSet) // 10 + /// BitArray([1, 2, 4] as BitSet) // 1011 + /// BitArray([7] as BitSet) // 1000000 + /// + /// - Complexity: O(1) + public init(_ set: BitSet) { + guard let l = set.last else { self.init(); return } + self.init(_storage: set._storage, count: l + 1) + } + + /// Initialize a bit array from the binary representation of an integer. + /// The result will contain exactly `value.bitWidth` bits. + /// + /// BitArray(bitPattern: 3 as UInt8) // 00000011 + /// BitArray(bitPattern: 42 as Int8) // 00101010 + /// BitArray(bitPattern: -1 as Int8) // 11111111 + /// BitArray(bitPattern: 3 as Int16) // 0000000000000111 + /// BitArray(bitPattern: 42 as Int16) // 0000000000101010 + /// BitArray(bitPattern: -1 as Int16) // 1111111111111111 + /// BitArray(bitPattern: 3 as Int) // 0000000000...0000000111 + /// BitArray(bitPattern: 42 as Int) // 0000000000...0000101010 + /// BitArray(bitPattern: -1 as Int) // 1111111111...1111111111 + /// + /// - Complexity: O(value.bitWidth) + public init(bitPattern value: some BinaryInteger) { + var words = value.words.map { _Word($0) } + let count = value.bitWidth + if words.isEmpty { + precondition(count == 0, "Inconsistent bitWidth") + } else { + let (w, b) = _UnsafeHandle._BitPosition(count).endSplit + precondition(words.count == w + 1, "Inconsistent bitWidth") + words[w].formIntersection(_Word(upTo: b)) + } + self.init(_storage: words, count: count) + } + + /// Creates a new, empty bit array with preallocated space for at least the + /// specified number of elements. + public init(minimumCapacity: Int) { + self.init() + reserveCapacity(minimumCapacity) + } +} + +extension BinaryInteger { + @inlinable + internal static func _convert( + _ source: BitArray + ) -> (value: Self, isNegative: Bool) { + var value: Self = .zero + let isNegative = source._foreachTwosComplementWordDownward( + isSigned: Self.isSigned + ) { _, word in + value <<= UInt.bitWidth + value |= Self(truncatingIfNeeded: word) + return true + } + return (value, isNegative) + } + + /// Creates a new instance by truncating or extending the bits in the given + /// bit array, as needed. The bit at position 0 in `source` will correspond + /// to the least-significant bit in the result. + /// + /// If `Self` is an unsigned integer type, then the result will contain as + /// many bits from `source` it can accommodate, truncating off any extras. + /// + /// UInt8(truncatingIfNeeded: "" as BitArray) // 0 + /// UInt8(truncatingIfNeeded: "0" as BitArray) // 0 + /// UInt8(truncatingIfNeeded: "1" as BitArray) // 1 + /// UInt8(truncatingIfNeeded: "11" as BitArray) // 3 + /// UInt8(truncatingIfNeeded: "11111111" as BitArray) // 255 + /// UInt8(truncatingIfNeeded: "1100000001" as BitArray) // 1 + /// UInt8(truncatingIfNeeded: "1100000101" as BitArray) // 5 + /// + /// If `Self` is a signed integer type, then the contents of the bit array + /// are interpreted to be a two's complement representation of a signed + /// integer value, with the last bit in the array representing the sign of + /// the result. + /// + /// Int8(truncatingIfNeeded: "" as BitArray) // 0 + /// Int8(truncatingIfNeeded: "0" as BitArray) // 0 + /// Int8(truncatingIfNeeded: "1" as BitArray) // -1 + /// Int8(truncatingIfNeeded: "01" as BitArray) // 1 + /// Int8(truncatingIfNeeded: "101" as BitArray) // -3 + /// Int8(truncatingIfNeeded: "0101" as BitArray) // 5 + /// + /// Int8(truncatingIfNeeded: "00000001" as BitArray) // 1 + /// Int8(truncatingIfNeeded: "00000101" as BitArray) // 5 + /// Int8(truncatingIfNeeded: "01111111" as BitArray) // 127 + /// Int8(truncatingIfNeeded: "10000000" as BitArray) // -128 + /// Int8(truncatingIfNeeded: "11111111" as BitArray) // -1 + /// + /// Int8(truncatingIfNeeded: "000011111111" as BitArray) // -1 + /// Int8(truncatingIfNeeded: "111100000000" as BitArray) // 0 + /// Int8(truncatingIfNeeded: "111100000001" as BitArray) // 1 + @inlinable + public init(truncatingIfNeeded source: BitArray) { + self = Self._convert(source).value + } + + /// Creates a new instance from the bits in the given bit array, if the + /// corresponding integer value can be represented exactly. + /// If the value is not representable exactly, then the result is `nil`. + /// + /// If `Self` is an unsigned integer type, then the contents of the bit array + /// are interpreted to be the binary representation of a nonnegative + /// integer value. The bit array is allowed to contain bits in unrepresentable + /// positions, as long as they are all cleared. + /// + /// UInt8(exactly: "" as BitArray) // 0 + /// UInt8(exactly: "0" as BitArray) // 0 + /// UInt8(exactly: "1" as BitArray) // 1 + /// UInt8(exactly: "10" as BitArray) // 2 + /// UInt8(exactly: "00000000" as BitArray) // 0 + /// UInt8(exactly: "11111111" as BitArray) // 255 + /// UInt8(exactly: "0000000000000" as BitArray) // 0 + /// UInt8(exactly: "0000011111111" as BitArray) // 255 + /// UInt8(exactly: "0000100000000" as BitArray) // nil + /// UInt8(exactly: "1111111111111" as BitArray) // nil + /// + /// If `Self` is a signed integer type, then the contents of the bit array + /// are interpreted to be a two's complement representation of a signed + /// integer value, with the last bit in the array representing the sign of + /// the result. + /// + /// Int8(exactly: "" as BitArray) // 0 + /// Int8(exactly: "0" as BitArray) // 0 + /// Int8(exactly: "1" as BitArray) // -1 + /// Int8(exactly: "01" as BitArray) // 1 + /// Int8(exactly: "101" as BitArray) // -3 + /// Int8(exactly: "0101" as BitArray) // 5 + /// + /// Int8(exactly: "00000001" as BitArray) // 1 + /// Int8(exactly: "00000101" as BitArray) // 5 + /// Int8(exactly: "01111111" as BitArray) // 127 + /// Int8(exactly: "10000000" as BitArray) // -128 + /// Int8(exactly: "11111111" as BitArray) // -1 + /// + /// Int8(exactly: "00000000000" as BitArray) // 0 + /// Int8(exactly: "00001111111" as BitArray) // 127 + /// Int8(exactly: "00010000000" as BitArray) // nil + /// Int8(exactly: "11101111111" as BitArray) // nil + /// Int8(exactly: "11110000000" as BitArray) // -128 + /// Int8(exactly: "11111111111" as BitArray) // -1 + @inlinable + public init?(exactly source: BitArray) { + let (value, isNegative) = Self._convert(source) + guard isNegative == (value < 0) else { return nil } + let words = value.words + var equal = true + _ = source._foreachTwosComplementWordDownward(isSigned: Self.isSigned) { i, word in + assert(equal) + let w = ( + i < words.count ? words[i] + : isNegative ? UInt.max + : UInt.zero) + equal = (w == word) + return equal + } + guard equal else { return nil } + self = value + } + + /// Creates a new instance from the bits in the given bit array, if the + /// corresponding integer value can be represented exactly. + /// If the value is not representable exactly, then a runtime error will + /// occur. + /// + /// If `Self` is an unsigned integer type, then the contents of the bit array + /// are interpreted to be the binary representation of a nonnegative + /// integer value. The bit array is allowed to contain bits in unrepresentable + /// positions, as long as they are all cleared. + /// + /// UInt8("" as BitArray) // 0 + /// UInt8("0" as BitArray) // 0 + /// UInt8("1" as BitArray) // 1 + /// UInt8("10" as BitArray) // 2 + /// UInt8("00000000" as BitArray) // 0 + /// UInt8("11111111" as BitArray) // 255 + /// UInt8("0000000000000" as BitArray) // 0 + /// UInt8("0000011111111" as BitArray) // 255 + /// UInt8("0000100000000" as BitArray) // ERROR + /// UInt8("1111111111111" as BitArray) // ERROR + /// + /// If `Self` is a signed integer type, then the contents of the bit array + /// are interpreted to be a two's complement representation of a signed + /// integer value, with the last bit in the array representing the sign of + /// the result. + /// + /// Int8("" as BitArray) // 0 + /// Int8("0" as BitArray) // 0 + /// Int8("1" as BitArray) // -1 + /// Int8("01" as BitArray) // 1 + /// Int8("101" as BitArray) // -3 + /// Int8("0101" as BitArray) // 5 + /// + /// Int8("00000001" as BitArray) // 1 + /// Int8("00000101" as BitArray) // 5 + /// Int8("01111111" as BitArray) // 127 + /// Int8("10000000" as BitArray) // -128 + /// Int8("11111111" as BitArray) // -1 + /// + /// Int8("00000000000" as BitArray) // 0 + /// Int8("00001111111" as BitArray) // 127 + /// Int8("00010000000" as BitArray) // ERROR + /// Int8("11101111111" as BitArray) // ERROR + /// Int8("11110000000" as BitArray) // -128 + /// Int8("11111111111" as BitArray) // -1 + @inlinable + public init(_ source: BitArray) { + guard let value = Self(exactly: source) else { + fatalError(""" + BitArray value cannot be converted to \(Self.self) because it is \ + outside the representable range + """) + } + self = value + } +} + +extension BitArray { + @usableFromInline + internal func _foreachTwosComplementWordDownward( + isSigned: Bool, + body: (Int, UInt) -> Bool + ) -> Bool { + self._read { + guard $0._words.count > 0 else { return false } + + var isNegative = false + let end = $0.end.endSplit + assert(end.bit > 0) + let last = $0._words[end.word] + if isSigned, last.contains(end.bit - 1) { + // Sign extend last word + isNegative = true + if !body(end.word, last.union(_Word(upTo: end.bit).complement()).value) { + return isNegative + } + } else if !body(end.word, last.value) { + return isNegative + } + for i in stride(from: end.word - 1, through: 0, by: -1) { + if !body(i, $0._words[i].value) { return isNegative } + } + return isNegative + } + } +} diff --git a/Sources/BitCollections/BitArray/BitArray+Invariants.swift b/Sources/BitCollections/BitArray/BitArray+Invariants.swift new file mode 100644 index 000000000..4c97b0707 --- /dev/null +++ b/Sources/BitCollections/BitArray/BitArray+Invariants.swift @@ -0,0 +1,42 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension BitArray { + /// True if consistency checking is enabled in the implementation of this + /// type, false otherwise. + /// + /// Documented performance promises are null and void when this property + /// returns true -- for example, operations that are documented to take + /// O(1) time might take O(*n*) time, or worse. + public static var _isConsistencyCheckingEnabled: Bool { + _isCollectionsInternalCheckingEnabled + } + +#if COLLECTIONS_INTERNAL_CHECKS + @inline(never) + @_effects(releasenone) + public func _checkInvariants() { + precondition(_count <= _storage.count * _Word.capacity) + precondition(_count > (_storage.count - 1) * _Word.capacity) + let p = _BitPosition(_count).split + if p.bit > 0 { + precondition(_storage.last!.subtracting(_Word(upTo: p.bit)) == .empty) + } + } +#else + @inline(__always) @inlinable + public func _checkInvariants() {} +#endif // COLLECTIONS_INTERNAL_CHECKS +} diff --git a/Sources/BitCollections/BitArray/BitArray+LosslessStringConvertible.swift b/Sources/BitCollections/BitArray/BitArray+LosslessStringConvertible.swift new file mode 100644 index 000000000..f87e7050d --- /dev/null +++ b/Sources/BitCollections/BitArray/BitArray+LosslessStringConvertible.swift @@ -0,0 +1,64 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 BitArray: LosslessStringConvertible { + /// Initializes a new bit array from a string representation. + /// + /// The string given is interpreted as if it was the binary representation + /// of an integer value, consisting of the digits `0` and `1`, with the + /// highest digits appearing first. + /// + /// BitArray("") // [] + /// BitArray("001") // [true, false, false] + /// BitArray("1110") // [false, true, true, true] + /// BitArray("42") // nil + /// BitArray("Foo") // nil + /// + /// To accept the display format used by `description`, the digits in the + /// input string are also allowed to be surrounded by a single pair of ASCII + /// angle brackets: + /// + /// let bits: BitArray = [false, false, true, true] + /// let string = bits.description // "<1100>" + /// let sameBits = BitArray(string)! // [false, false, true, true] + /// + public init?(_ description: String) { + var digits = description[...] + if digits.utf8.first == ._asciiLT, digits.utf8.last == ._asciiGT { + digits = digits.dropFirst().dropLast() + } + let bits: BitArray? = digits.utf8.withContiguousStorageIfAvailable { buffer in + Self(_utf8Digits: buffer) + } ?? Self(_utf8Digits: description.utf8) + guard let bits = bits else { + return nil + } + self = bits + } + + internal init?(_utf8Digits utf8: some Collection) { + let c = utf8.count + self.init(repeating: false, count: c) + var i = c &- 1 + let success = _update { handle in + for byte in utf8 { + if byte == ._ascii1 { + handle.set(at: i) + } else { + guard byte == ._ascii0 else { return false } + } + i &-= 1 + } + return true + } + guard success else { return nil } + } +} diff --git a/Sources/BitCollections/BitArray/BitArray+RandomBits.swift b/Sources/BitCollections/BitArray/BitArray+RandomBits.swift new file mode 100644 index 000000000..f59ef36aa --- /dev/null +++ b/Sources/BitCollections/BitArray/BitArray+RandomBits.swift @@ -0,0 +1,38 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 BitArray { + /// Create and return a new bit array consisting of `count` random bits, + /// using the system random number generator. + /// + /// - Parameter count: The number of random bits to generate. + public static func randomBits(count: Int) -> BitArray { + var rng = SystemRandomNumberGenerator() + return randomBits(count: count, using: &rng) + } + + /// Create and return a new bit array consisting of `count` random bits, + /// using the given random number generator. + /// + /// - Parameter count: The number of random bits to generate. + /// - Parameter rng: The random number generator to use. + public static func randomBits( + count: Int, + using rng: inout some RandomNumberGenerator + ) -> BitArray { + precondition(count >= 0, "Invalid count") + guard count > 0 else { return BitArray() } + let (w, b) = _BitPosition(count).endSplit + var words = (0 ... w).map { _ in _Word(rng.next() as UInt) } + words[w].formIntersection(_Word(upTo: b)) + return BitArray(_storage: words, count: count) + } +} diff --git a/Sources/BitCollections/BitArray/BitArray+RangeReplaceableCollection.swift b/Sources/BitCollections/BitArray/BitArray+RangeReplaceableCollection.swift new file mode 100644 index 000000000..1141b840c --- /dev/null +++ b/Sources/BitCollections/BitArray/BitArray+RangeReplaceableCollection.swift @@ -0,0 +1,551 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension BitArray: RangeReplaceableCollection {} + +extension BitArray { + /// Prepares the bit array to store the specified number of bits. + /// + /// If you are adding a known number of elements to an array, use this + /// method to avoid multiple reallocations. + /// + /// - Parameter n: The requested number of bits to store. + public mutating func reserveCapacity(_ n: Int) { + let wordCount = _Word.wordCount(forBitCount: UInt(n)) + _storage.reserveCapacity(wordCount) + } + + /// Creates a new, empty bit array. + /// + /// - Complexity: O(1) + public init() { + self.init(_storage: [], count: 0) + } + + /// Creates a new bit array containing the specified number of a single, + /// repeated Boolean value. + /// + /// - Parameters: + /// - repeatedValue: The Boolean value to repeat. + /// - count: The number of times to repeat the value passed in the + /// `repeating` parameter. `count` must be zero or greater. + public init(repeating repeatedValue: Bool, count: Int) { + let wordCount = _Word.wordCount(forBitCount: UInt(count)) + var storage: [_Word] = .init( + repeating: repeatedValue ? .allBits : .empty, count: wordCount) + if repeatedValue, _BitPosition(count).bit != 0 { + // Clear upper bits of last word. + storage[wordCount - 1] = _Word(upTo: _BitPosition(count).bit) + } + self.init(_storage: storage, count: count) + } +} + +extension BitArray { + /// Creates a new bit array containing the Boolean values in a sequence. + /// + /// - Parameter elements: The sequence of elements for the new collection. + /// `elements` must be finite. + /// - Complexity: O(*count*) where *count* is the number of values in + /// `elements`. + @inlinable + public init(_ elements: some Sequence) { + defer { _checkInvariants() } + if let elements = _specialize(elements, for: BitArray.self) { + self.init(elements) + return + } + if let elements = _specialize(elements, for: BitArray.SubSequence.self) { + self.init(elements) + return + } + self.init() + self.reserveCapacity(elements.underestimatedCount) + self.append(contentsOf: elements) + } + + // Specializations + + /// Creates a new bit array containing the Boolean values in a sequence. + /// + /// - Parameter elements: The sequence of elements for the new collection. + /// `elements` must be finite. + /// - Complexity: O(*count*) where *count* is the number of values in + /// `elements`. + @inlinable + public init(_ elements: BitArray) { + self = elements + } + + /// Creates a new bit array containing the Boolean values in a sequence. + /// + /// - Parameter elements: The sequence of elements for the new collection. + /// `elements` must be finite. + /// - Complexity: O(*count*) where *count* is the number of values in + /// `elements`. + public init(_ elements: BitArray.SubSequence) { + let wordCount = _Word.wordCount(forBitCount: UInt(elements.count)) + let storage = Array(repeating: _Word.empty, count: wordCount) + self.init(_storage: storage, count: elements.count) + self._copy(from: elements, to: 0) + _checkInvariants() + } +} + +extension BitArray { + internal mutating func _prepareForReplaceSubrange( + _ range: Range, replacementCount c: Int + ) { + precondition(range.lowerBound >= 0 && range.upperBound <= self.count) + + let origCount = self.count + if range.count < c { + _extend(by: c - range.count) + } + + _copy(from: range.upperBound ..< origCount, to: range.lowerBound + c) + + if c < range.count { + _removeLast(range.count - c) + } + } + + /// Replaces the specified subrange of bits with the values in the given + /// collection. + /// + /// This method has the effect of removing the specified range of elements + /// from the collection and inserting the new elements at the same location. + /// The number of new elements need not match the number of elements being + /// removed. + /// + /// If you pass a zero-length range as the `range` parameter, this method + /// inserts the elements of `newElements` at `range.startIndex`. Calling + /// the `insert(contentsOf:at:)` method instead is preferred. + /// + /// Likewise, if you pass a zero-length collection as the `newElements` + /// parameter, this method removes the elements in the given subrange + /// without replacement. Calling the `removeSubrange(_:)` method instead is + /// preferred. + /// + /// - Parameters: + /// - range: The subrange of the collection to replace. The bounds of + /// the range must be valid indices of the collection. + /// - newElements: The new elements to add to the collection. + /// + /// - Complexity: O(*n* + *m*), where *n* is length of this collection and + /// *m* is the length of `newElements`. If the call to this method simply + /// appends the contents of `newElements` to the collection, this method is + /// equivalent to `append(contentsOf:)`. + public mutating func replaceSubrange( + _ range: Range, + with newElements: __owned some Collection + ) { + let c = newElements.count + _prepareForReplaceSubrange(range, replacementCount: c) + if let newElements = _specialize(newElements, for: BitArray.self) { + _copy(from: newElements, to: range.lowerBound) + } else if let newElements = _specialize( + newElements, for: BitArray.SubSequence.self + ) { + _copy(from: newElements, to: range.lowerBound) + } else { + _copy(from: newElements, to: range.lowerBound ..< range.lowerBound + c) + } + _checkInvariants() + } + + /// Replaces the specified subrange of bits with the values in the given + /// collection. + /// + /// This method has the effect of removing the specified range of elements + /// from the collection and inserting the new elements at the same location. + /// The number of new elements need not match the number of elements being + /// removed. + /// + /// If you pass a zero-length range as the `range` parameter, this method + /// inserts the elements of `newElements` at `range.startIndex`. Calling + /// the `insert(contentsOf:at:)` method instead is preferred. + /// + /// Likewise, if you pass a zero-length collection as the `newElements` + /// parameter, this method removes the elements in the given subrange + /// without replacement. Calling the `removeSubrange(_:)` method instead is + /// preferred. + /// + /// - Parameters: + /// - range: The subrange of the collection to replace. The bounds of + /// the range must be valid indices of the collection. + /// - newElements: The new elements to add to the collection. + /// + /// - Complexity: O(*n* + *m*), where *n* is length of this collection and + /// *m* is the length of `newElements`. If the call to this method simply + /// appends the contents of `newElements` to the collection, this method is + /// equivalent to `append(contentsOf:)`. + public mutating func replaceSubrange( + _ range: Range, + with newElements: __owned BitArray + ) { + replaceSubrange(range, with: newElements[...]) + } + + /// Replaces the specified subrange of bits with the values in the given + /// collection. + /// + /// This method has the effect of removing the specified range of elements + /// from the collection and inserting the new elements at the same location. + /// The number of new elements need not match the number of elements being + /// removed. + /// + /// If you pass a zero-length range as the `range` parameter, this method + /// inserts the elements of `newElements` at `range.startIndex`. Calling + /// the `insert(contentsOf:at:)` method instead is preferred. + /// + /// Likewise, if you pass a zero-length collection as the `newElements` + /// parameter, this method removes the elements in the given subrange + /// without replacement. Calling the `removeSubrange(_:)` method instead is + /// preferred. + /// + /// - Parameters: + /// - range: The subrange of the collection to replace. The bounds of + /// the range must be valid indices of the collection. + /// - newElements: The new elements to add to the collection. + /// + /// - Complexity: O(*n* + *m*), where *n* is length of this collection and + /// *m* is the length of `newElements`. If the call to this method simply + /// appends the contents of `newElements` to the collection, this method is + /// equivalent to `append(contentsOf:)`. + public mutating func replaceSubrange( + _ range: Range, + with newElements: __owned BitArray.SubSequence + ) { + _prepareForReplaceSubrange(range, replacementCount: newElements.count) + _copy(from: newElements, to: range.lowerBound) + _checkInvariants() + } +} + +extension BitArray { + /// Adds a new element to the end of the bit array. + /// + /// - Parameter newElement: The element to append to the bit array. + /// + /// - Complexity: Amortized O(1), averaged over many calls to `append(_:)` + /// on the same bit array. + public mutating func append(_ newElement: Bool) { + let (word, bit) = _BitPosition(_count).split + if bit == 0 { + _storage.append(_Word.empty) + } + _count += 1 + if newElement { + _update { handle in + handle._mutableWords[word].value |= 1 &<< bit + } + } + _checkInvariants() + } + + /// Adds the elements of a sequence or collection to the end of this + /// bit array. + /// + /// The collection being appended to allocates any additional necessary + /// storage to hold the new elements. + /// + /// - Parameter newElements: The elements to append to the bit array. + /// + /// - Complexity: O(*m*), where *m* is the length of `newElements`, if + /// `self` is the only copy of this bit array. Otherwise O(`count` + *m*). + public mutating func append( + contentsOf newElements: __owned some Sequence + ) { + if let newElements = _specialize(newElements, for: BitArray.self) { + self.append(contentsOf: newElements) + return + } + if let newElements = _specialize( + newElements, for: BitArray.SubSequence.self + ) { + self.append(contentsOf: newElements) + return + } + var it = newElements.makeIterator() + var pos = _BitPosition(_count) + if pos.bit > 0 { + let (bits, count) = it._nextChunk( + maximumCount: UInt(_Word.capacity) - pos.bit) + guard count > 0 else { return } + _count += count + _update { $0._copy(bits: bits, count: count, to: pos) } + pos.value += count + } + while true { + let (bits, count) = it._nextChunk() + guard count > 0 else { break } + assert(pos.bit == 0) + _storage.append(.empty) + _count += count + _update { $0._copy(bits: bits, count: count, to: pos) } + pos.value += count + } + _checkInvariants() + } + + /// Adds the elements of a sequence or collection to the end of this + /// bit array. + /// + /// The collection being appended to allocates any additional necessary + /// storage to hold the new elements. + /// + /// - Parameter newElements: The elements to append to the bit array. + /// + /// - Complexity: O(*m*), where *m* is the length of `newElements`, if + /// `self` is the only copy of this bit array. Otherwise O(`count` + *m*). + public mutating func append(contentsOf newElements: BitArray) { + _extend(by: newElements.count) + _copy(from: newElements, to: count - newElements.count) + _checkInvariants() + } + + /// Adds the elements of a sequence or collection to the end of this + /// bit array. + /// + /// The collection being appended to allocates any additional necessary + /// storage to hold the new elements. + /// + /// - Parameter newElements: The elements to append to the bit array. + /// + /// - Complexity: O(*m*), where *m* is the length of `newElements`, if + /// `self` is the only copy of this bit array. Otherwise O(`count` + *m*). + public mutating func append(contentsOf newElements: BitArray.SubSequence) { + _extend(by: newElements.count) + _copy(from: newElements, to: count - newElements.count) + _checkInvariants() + } +} + +extension BitArray { + /// Inserts a new element into the bit array at the specified position. + /// + /// The new element is inserted before the element currently at the + /// specified index. If you pass the bit array's `endIndex` as + /// the `index` parameter, then the new element is appended to the + /// collection. + /// + /// var bits = [false, true, true, false, true] + /// bits.insert(true, at: 3) + /// bits.insert(false, at: numbers.endIndex) + /// + /// print(bits) + /// // Prints "[false, true, true, true, false, true, false]" + /// + /// - Parameter newElement: The new element to insert into the bit array. + /// - Parameter i: The position at which to insert the new element. + /// `index` must be a valid index into the bit array. + /// + /// - Complexity: O(`count` - i), if `self` is the only copy of this bit + /// array. Otherwise O(`count`). + public mutating func insert(_ newElement: Bool, at i: Int) { + if _BitPosition(_count).bit == 0 { + _storage.append(_Word.empty) + } + let c = count + _count += 1 + _update { handle in + handle.copy(from: i ..< c, to: i + 1) + handle[i] = newElement + } + _checkInvariants() + } + + /// Inserts the elements of a collection into the bit array at the specified + /// position. + /// + /// The new elements are inserted before the element currently at the + /// specified index. If you pass the collection's `endIndex` property as the + /// `index` parameter, the new elements are appended to the collection. + /// + /// - Parameter newElements: The new elements to insert into the bit array. + /// - Parameter i: The position at which to insert the new elements. `index` + /// must be a valid index of the collection. + /// + /// - Complexity: O(`self.count` + `newElements.count`). + /// If `i == endIndex`, this method is equivalent to `append(contentsOf:)`. + public mutating func insert( + contentsOf newElements: __owned some Collection, + at i: Int + ) { + precondition(i >= 0 && i <= count) + let c = newElements.count + guard c > 0 else { return } + _extend(by: c) + _copy(from: i ..< count - c, to: i + c) + + if let newElements = _specialize(newElements, for: BitArray.self) { + _copy(from: newElements, to: i) + } else if let newElements = _specialize( + newElements, for: BitArray.SubSequence.self + ) { + _copy(from: newElements, to: i) + } else { + _copy(from: newElements, to: i ..< i + c) + } + + _checkInvariants() + } + + /// Inserts the elements of a collection into the bit array at the specified + /// position. + /// + /// The new elements are inserted before the element currently at the + /// specified index. If you pass the collection's `endIndex` property as the + /// `index` parameter, the new elements are appended to the collection. + /// + /// - Parameter newElements: The new elements to insert into the bit array. + /// - Parameter i: The position at which to insert the new elements. `index` + /// must be a valid index of the collection. + /// + /// - Complexity: O(`self.count` + `newElements.count`). + /// If `i == endIndex`, this method is equivalent to `append(contentsOf:)`. + public mutating func insert( + contentsOf newElements: __owned BitArray, + at i: Int + ) { + insert(contentsOf: newElements[...], at: i) + } + + /// Inserts the elements of a collection into the bit array at the specified + /// position. + /// + /// The new elements are inserted before the element currently at the + /// specified index. If you pass the collection's `endIndex` property as the + /// `index` parameter, the new elements are appended to the collection. + /// + /// - Parameter newElements: The new elements to insert into the bit array. + /// - Parameter i: The position at which to insert the new elements. `index` + /// must be a valid index of the collection. + /// + /// - Complexity: O(`self.count` + `newElements.count`). + /// If `i == endIndex`, this method is equivalent to `append(contentsOf:)`. + public mutating func insert( + contentsOf newElements: __owned BitArray.SubSequence, + at i: Int + ) { + let c = newElements.count + guard c > 0 else { return } + _extend(by: c) + _copy(from: i ..< count - c, to: i + c) + _copy(from: newElements, to: i) + _checkInvariants() + } +} + +extension BitArray { + /// Removes and returns the element at the specified position. + /// + /// All the elements following the specified position are moved to close the + /// gap. + /// + /// - Parameter i: The position of the element to remove. `index` must be + /// a valid index of the collection that is not equal to the collection's + /// end index. + /// - Returns: The removed element. + /// + /// - Complexity: O(`count`) + @discardableResult + public mutating func remove(at i: Int) -> Bool { + let result = self[i] + _copy(from: i + 1 ..< count, to: i) + _removeLast() + _checkInvariants() + return result + } + + /// Removes the specified subrange of elements from the collection. + /// + /// - Parameter range: The subrange of the collection to remove. The bounds + /// of the range must be valid indices of the collection. + /// + /// - Complexity: O(`count`) + public mutating func removeSubrange(_ range: Range) { + precondition( + range.lowerBound >= 0 && range.upperBound <= count, + "Bounds out of range") + _copy( + from: Range(uncheckedBounds: (range.upperBound, count)), + to: range.lowerBound) + _removeLast(range.count) + _checkInvariants() + } + + public mutating func _customRemoveLast() -> Bool? { + precondition(_count > 0) + let result = self[count - 1] + _removeLast() + _checkInvariants() + return result + } + + public mutating func _customRemoveLast(_ n: Int) -> Bool { + precondition(n >= 0 && n <= count) + _removeLast(n) + _checkInvariants() + return true + } + + /// Removes and returns the first element of the bit array. + /// + /// The bit array must not be empty. + /// + /// - Returns: The removed element. + /// + /// - Complexity: O(`count`) + @discardableResult + public mutating func removeFirst() -> Bool { + precondition(_count > 0) + let result = self[0] + _copy(from: 1 ..< count, to: 0) + _removeLast() + _checkInvariants() + return result + } + + /// Removes the specified number of elements from the beginning of the + /// bit array. + /// + /// - Parameter k: The number of elements to remove from the bit array. + /// `k` must be greater than or equal to zero and must not exceed the + /// number of elements in the bit array. + /// + /// - Complexity: O(`count`) + public mutating func removeFirst(_ k: Int) { + precondition(k >= 0 && k <= _count) + _copy(from: k ..< count, to: 0) + _removeLast(k) + _checkInvariants() + } + + /// Removes all elements from the bit array. + /// + /// - Parameter keepCapacity: Pass `true` to request that the collection + /// avoid releasing its storage. Retaining the collection's storage can + /// be a useful optimization when you're planning to grow the collection + /// again. The default value is `false`. + /// + /// - Complexity: O(`count`) + public mutating func removeAll(keepingCapacity keepCapacity: Bool = false) { + _storage.removeAll(keepingCapacity: keepCapacity) + _count = 0 + _checkInvariants() + } +} diff --git a/Sources/BitCollections/BitArray/BitArray+Shifts.swift b/Sources/BitCollections/BitArray/BitArray+Shifts.swift new file mode 100644 index 000000000..1a4808fc3 --- /dev/null +++ b/Sources/BitCollections/BitArray/BitArray+Shifts.swift @@ -0,0 +1,162 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 BitArray { + /// Shift the bits in this array by the specified number of places to the + /// left (towards the end of the array), by inserting `amount` false values + /// at the beginning. + /// + /// If `amount` is negative, this is equivalent to shifting `-amount` + /// places to the right. + /// + /// var bits: BitArray = "1110110" + /// bits.maskingShiftLeft(by: 2) + /// // bits is now 111011000 + /// bits.maskingShiftLeft(by: -4) + /// // bits is now 11101 + /// bits.maskingShiftLeft(by: 8) + /// // bits is now 111010000000 + public mutating func resizingShiftLeft(by amount: Int) { + guard amount != 0 else { return } + if amount > 0 { + _resizingShiftLeft(by: amount) + } else { + _resizingShiftRight(by: -amount) + } + } + + /// Shift the bits in this array by the specified number of places to the + /// right (towards the start of the array), by removing `amount` existing + /// values from the front of the array. + /// + /// If `amount` is negative, then this is equivalent to shifting `-amount` + /// places to the left. If amount is greater than or equal to `count`, + /// then the resulting bit array will be empty. + /// + /// var bits: BitArray = "1110110" + /// bits.maskingShiftRight(by: 2) + /// // bits is now 11101 + /// bits.maskingShiftRight(by: -4) + /// // bits is now 111010000 + /// bits.maskingShiftRight(by: 10) + /// // bits is now empty + /// + /// If `amount` is between 0 and `count`, then this has the same effect as + /// `removeFirst(amount)`. + public mutating func resizingShiftRight(by amount: Int) { + guard amount != 0 else { return } + if amount > 0 { + _resizingShiftRight(by: amount) + } else { + _resizingShiftLeft(by: -amount) + } + } + + internal mutating func _resizingShiftLeft(by amount: Int) { + assert(amount > 0) + _extend(by: amount, with: false) + maskingShiftLeft(by: amount) + } + + internal mutating func _resizingShiftRight(by amount: Int) { + assert(amount > 0) + guard amount < count else { + self = .init() + return + } + self._copy(from: Range(uncheckedBounds: (amount, count)), to: 0) + self._removeLast(count &- amount) + } +} + +extension BitArray { + // FIXME: Add maskingShiftRight(by:in:) and maskingShiftLeft(by:in:) for shifting slices. + + /// Shift the bits in this array by the specified number of places to the + /// left (towards the end of the array), without changing + /// its count. + /// + /// Values that are shifted off the array are discarded. Values that are + /// shifted in are all set to false. + /// + /// If `amount` is negative, this is equivalent to shifting `-amount` + /// places to the right. If `amount` is greater than or equal to `count`, + /// then all values are set to false. + /// + /// var bits: BitArray = "1110110" + /// bits.maskingShiftLeft(by: 2) + /// // bits is now 1011000 + /// bits.maskingShiftLeft(by: -4) + /// // bits is now 0000101 + /// bits.maskingShiftLeft(by: 8) + /// // bits is now 0000000 + public mutating func maskingShiftLeft(by amount: Int) { + guard amount != 0 else { return } + _update { + if amount > 0 { + $0._maskingShiftLeft(by: amount) + } else { + $0._maskingShiftRight(by: -amount) + } + } + } + + /// Shift the bits in this array by the specified number of places to the + /// right (towards the beginning of the array), without changing + /// its count. + /// + /// Values that are shifted off the array are discarded. Values that are + /// shifted in are all set to false. + /// + /// If `amount` is negative, this is equivalent to shifting `-amount` + /// places to the left. If `amount` is greater than or equal to `count`, + /// then all values are set to false. + /// + /// var bits: BitArray = "1110110" + /// bits.maskingShiftRight(by: 2) + /// // bits is now 0011101 + /// bits.maskingShiftRight(by: -3) + /// // bits is now 1101000 + /// bits.maskingShiftRight(by: 8) + /// // bits is now 0000000 + public mutating func maskingShiftRight(by amount: Int) { + guard amount != 0 else { return } + _update { + if amount > 0 { + $0._maskingShiftRight(by: amount) + } else { + $0._maskingShiftLeft(by: -amount) + } + } + } +} + +extension BitArray._UnsafeHandle { + internal mutating func _maskingShiftLeft(by amount: Int) { + assert(amount > 0) + let d = Swift.min(amount, self.count) + if d == amount { + let range = Range(uncheckedBounds: (0, self.count &- d)) + self.copy(from: range, to: d) + } + self.clear(in: Range(uncheckedBounds: (0, d))) + } + + internal mutating func _maskingShiftRight(by amount: Int) { + assert(amount > 0) + let d = Swift.min(amount, self.count) + if d == amount { + let range = Range(uncheckedBounds: (d, self.count)) + self.copy(from: range, to: 0) + } + self.clear(in: Range(uncheckedBounds: (self.count &- d, self.count))) + } +} diff --git a/Sources/BitCollections/BitArray/BitArray+Testing.swift b/Sources/BitCollections/BitArray/BitArray+Testing.swift new file mode 100644 index 000000000..bf0bcbf5b --- /dev/null +++ b/Sources/BitCollections/BitArray/BitArray+Testing.swift @@ -0,0 +1,17 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 BitArray { + @_spi(Testing) + public var _capacity: Int { + _storage.capacity * _Word.capacity + } +} diff --git a/Sources/BitCollections/BitArray/BitArray._UnsafeHandle.swift b/Sources/BitCollections/BitArray/BitArray._UnsafeHandle.swift new file mode 100644 index 000000000..680a70456 --- /dev/null +++ b/Sources/BitCollections/BitArray/BitArray._UnsafeHandle.swift @@ -0,0 +1,163 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension BitArray { + /// An unsafe-unowned bitarray view over `UInt` storage, providing bit array + /// primitives. + @usableFromInline + @frozen + internal struct _UnsafeHandle { + @usableFromInline + internal typealias _BitPosition = _UnsafeBitSet.Index + + @usableFromInline + internal let _words: UnsafeBufferPointer<_Word> + + @usableFromInline + internal var _count: UInt + +#if DEBUG + /// True when this handle does not support table mutations. + /// (This is only checked in debug builds.) + @usableFromInline + internal let _mutable: Bool +#endif + + @inline(__always) + internal func ensureMutable() { +#if DEBUG + assert(_mutable) +#endif + } + + internal var _mutableWords: UnsafeMutableBufferPointer<_Word> { + ensureMutable() + return UnsafeMutableBufferPointer(mutating: _words) + } + + @inlinable + @inline(__always) + internal init( + words: UnsafeBufferPointer<_Word>, + count: UInt, + mutable: Bool + ) { + assert(count <= words.count * _Word.capacity) + assert(count > (words.count - 1) * _Word.capacity) + self._words = words + self._count = count +#if DEBUG + self._mutable = mutable +#endif + } + + @inlinable + @inline(__always) + internal init( + words: UnsafeMutableBufferPointer<_Word>, + count: UInt, + mutable: Bool + ) { + self.init( + words: UnsafeBufferPointer(words), + count: count, + mutable: mutable) + } + } +} + +extension BitArray._UnsafeHandle { + internal var count: Int { + Int(_count) + } + + internal var end: _BitPosition { + _BitPosition(_count) + } + + internal func set(at position: Int) { + ensureMutable() + assert(position >= 0 && position < _count) + let (word, bit) = _BitPosition(UInt(position)).split + _mutableWords[word].insert(bit) + } + + internal func clear(at position: Int) { + ensureMutable() + assert(position >= 0 && position < _count) + let (word, bit) = _BitPosition(UInt(position)).split + _mutableWords[word].remove(bit) + } + + internal subscript(position: Int) -> Bool { + get { + assert(position >= 0 && position < _count) + let (word, bit) = _BitPosition(UInt(position)).split + return _words[word].contains(bit) + } + set { + ensureMutable() + assert(position >= 0 && position < _count) + let (word, bit) = _BitPosition(UInt(position)).split + if newValue { + _mutableWords[word].insert(bit) + } else { + _mutableWords[word].remove(bit) + } + } + } +} + +extension BitArray._UnsafeHandle { + internal mutating func fill(in range: Range) { + ensureMutable() + precondition( + range.lowerBound >= 0 && range.upperBound <= count, + "Range out of bounds") + guard range.count > 0 else { return } + let (lw, lb) = _BitPosition(range.lowerBound).split + let (uw, ub) = _BitPosition(range.upperBound).endSplit + let words = _mutableWords + guard lw != uw else { + words[lw].formUnion(_Word(from: lb, to: ub)) + return + } + words[lw].formUnion(_Word(upTo: lb).complement()) + for w in lw + 1 ..< uw { + words[w] = _Word.allBits + } + words[uw].formUnion(_Word(upTo: ub)) + } + + internal mutating func clear(in range: Range) { + ensureMutable() + precondition( + range.lowerBound >= 0 && range.upperBound <= count, + "Range out of bounds") + guard range.count > 0 else { return } + let (lw, lb) = _BitPosition(range.lowerBound).split + let (uw, ub) = _BitPosition(range.upperBound).endSplit + let words = _mutableWords + guard lw != uw else { + words[lw].subtract(_Word(from: lb, to: ub)) + return + } + words[lw].subtract(_Word(upTo: lb).complement()) + for w in lw + 1 ..< uw { + words[w] = _Word.empty + } + words[uw].subtract(_Word(upTo: ub)) + } +} diff --git a/Sources/BitCollections/BitArray/BitArray.swift b/Sources/BitCollections/BitArray/BitArray.swift new file mode 100644 index 000000000..956e23841 --- /dev/null +++ b/Sources/BitCollections/BitArray/BitArray.swift @@ -0,0 +1,124 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +/// An ordered, random-access collection of `Bool` values, implemented as an +/// uncompressed bitmap of as many bits as the count of the array. +/// +/// Bit arrays implement `RangeReplaceableCollection` and `MutableCollection` +/// and provide limited support for bitwise operations on same-sized arrays. +/// +/// See `BitSet` for an alternative form of the same underlying data +/// structure, treating it as a set of nonnegative integers corresponding to +/// `true` bits. +public struct BitArray { + @usableFromInline + internal typealias _BitPosition = _UnsafeBitSet.Index + + @usableFromInline + internal var _storage: [_Word] + + /// The number of bits in the bit array. This may less than the number of bits + /// in `_storage` if the last word isn't fully filled. + @usableFromInline + internal var _count: UInt + + @usableFromInline + internal init(_storage: [_Word], count: UInt) { + assert(count <= _storage.count * _Word.capacity) + assert(count > (_storage.count - 1) * _Word.capacity) + self._storage = _storage + self._count = count + } + + @inline(__always) + internal init(_storage: [_Word], count: Int) { + self.init(_storage: _storage, count: UInt(count)) + } +} + +extension BitArray: Sendable {} + +extension BitArray { + @inline(__always) + internal func _read( + _ body: (_UnsafeHandle) throws -> R + ) rethrows -> R { + try _storage.withUnsafeBufferPointer { words in + let handle = _UnsafeHandle( + words: words, count: _count, mutable: false) + return try body(handle) + } + } + + @inline(__always) + internal mutating func _update( + _ body: (inout _UnsafeHandle) throws -> R + ) rethrows -> R { + defer { + _checkInvariants() + } + return try _storage.withUnsafeMutableBufferPointer { words in + var handle = _UnsafeHandle(words: words, count: _count, mutable: true) + return try body(&handle) + } + } + + internal mutating func _removeLast() { + assert(_count > 0) + _count -= 1 + let bit = _BitPosition(_count).bit + if bit == 0 { + _storage.removeLast() + } else { + _storage[_storage.count - 1].remove(bit) + } + } + + internal mutating func _removeLast(_ n: Int) { + assert(n >= 0 && n <= _count) + guard n > 0 else { return } + let wordCount = _Word.wordCount(forBitCount: _count - UInt(n)) + if wordCount < _storage.count { + _storage.removeLast(_storage.count - wordCount) + } + _count -= UInt(n) + let (word, bit) = _BitPosition(_count).split + if bit > 0 { + _storage[word].formIntersection(_Word(upTo: bit)) + } + } + + internal mutating func _extend(by n: Int, with paddingBit: Bool = false) { + assert(n >= 0) + guard n > 0 else { return } + let newCount = _count + UInt(n) + let orig = _storage.count + let new = _Word.wordCount(forBitCount: newCount) + if paddingBit == false { + _storage.append(contentsOf: repeatElement(.empty, count: new - orig)) + } else { + let (w1, b1) = _BitPosition(_count).split + let (w2, b2) = _BitPosition(newCount).split + if w1 < _storage.count { + _storage[w1].formUnion(_Word(upTo: b1).complement()) + } + _storage.append(contentsOf: repeatElement(.allBits, count: new - orig)) + if w2 < _storage.count { + _storage[w2].formIntersection(_Word(upTo: b2)) + } + } + _count = newCount + } +} diff --git a/Sources/BitCollections/BitCollections.docc/BitCollections.md b/Sources/BitCollections/BitCollections.docc/BitCollections.md new file mode 100644 index 000000000..9ed49e208 --- /dev/null +++ b/Sources/BitCollections/BitCollections.docc/BitCollections.md @@ -0,0 +1,20 @@ +# ``BitCollections`` + +**Swift Collections** is an open-source package of data structure implementations for the Swift programming language. + +## Overview + + + +#### Additional Resources + +- [`Swift Collections` on GitHub](https://github.com/apple/swift-collections/) +- [`Swift Collections` on the Swift Forums](https://forums.swift.org/c/related-projects/collections/72) + + +## Topics + +### Structures + +- ``BitSet`` +- ``BitArray`` diff --git a/Sources/BitCollections/BitCollections.docc/Extensions/BitArray.md b/Sources/BitCollections/BitCollections.docc/Extensions/BitArray.md new file mode 100644 index 000000000..aabae3cb0 --- /dev/null +++ b/Sources/BitCollections/BitCollections.docc/Extensions/BitArray.md @@ -0,0 +1,82 @@ +# ``BitCollections/BitArray`` + + + + + +## Topics + +### Creating a Bit Array + +- ``init()`` +- ``init(minimumCapacity:)`` +- ``init(_:)-2y0wv`` +- ``init(repeating:count:)-4j5yd`` +- ``init(_:)-6ldyw`` +- ``init(_:)-4tksd`` +- ``init(_:)-765d2`` +- ``init(bitPattern:)`` +- ``randomBits(count:)`` +- ``randomBits(count:using:)`` + +### Accessing Elements + +- ``subscript(_:)-51ccj`` + +- ``first`` +- ``last`` + +### Adding Elements + +- ``append(_:)-8dqhn`` +- ``append(contentsOf:)-18dwf`` +- ``append(contentsOf:)-576q4`` +- ``append(contentsOf:)-8xkr8`` +- ``append(repeating:count:)`` +- ``insert(_:at:)-9t4hf`` +- ``insert(contentsOf:at:)-7e1xn`` +- ``insert(contentsOf:at:)-35dp3`` +- ``insert(contentsOf:at:)-1wsgw`` +- ``insert(repeating:count:at:)`` +- ``truncateOrExtend(toCount:with:)`` + +### Removing Elements + +- ``remove(at:)-7ij12`` +- ``removeAll(keepingCapacity:)-5tkge`` +- ``removeAll(where:)-7tv7z`` +- ``removeSubrange(_:)-86ou8`` +- ``removeSubrange(_:)-18qe7`` +- ``removeLast()`` +- ``removeLast(_:)`` +- ``removeFirst()-dcsp`` +- ``removeFirst(_:)-9nqlo`` +- ``popLast()`` + +### Replacing Elements + +- ``fill(in:with:)-1lrlg`` +- ``fill(in:with:)-8sf1b`` +- ``fill(with:)`` +- ``replaceSubrange(_:with:)-163u2`` +- ``replaceSubrange(_:with:)-875d8`` +- ``replaceSubrange(_:with:)-2i7lu`` +- ``replaceSubrange(_:with:)-b5ou`` + +### Bitwise Operations + +- ``toggleAll()`` +- ``toggleAll(in:)-3duwn`` +- ``toggleAll(in:)-5hfhl`` +- ``maskingShiftLeft(by:)`` +- ``maskingShiftRight(by:)`` +- ``resizingShiftLeft(by:)`` +- ``resizingShiftRight(by:)`` + + + + + + + + diff --git a/Sources/BitCollections/BitCollections.docc/Extensions/BitSet.Counted.md b/Sources/BitCollections/BitCollections.docc/Extensions/BitSet.Counted.md new file mode 100644 index 000000000..f7d6a6ba6 --- /dev/null +++ b/Sources/BitCollections/BitCollections.docc/Extensions/BitSet.Counted.md @@ -0,0 +1,125 @@ +# ``BitCollections/BitSet/Counted-swift.struct`` + + + + + +## Topics + +### Collection Views + +- ``uncounted`` + +### Creating a Set + +- ``init()`` +- ``init(reservingCapacity:)`` +- ``init(_:)-15cws`` +- ``init(_:)-38hho`` +- ``init(_:)-2of3i`` +- ``init(_:)-5fhls`` +- ``init(bitPattern:)`` +- ``init(words:)`` +- ``random(upTo:)`` +- ``random(upTo:using:)`` + +### Finding Elements + +- ``contains(_:)`` +- ``firstIndex(of:)`` +- ``lastIndex(of:)`` + +### Adding and Updating Elements + +- ``insert(_:)`` +- ``update(with:)`` + +### Removing Elements + +- ``filter(_:)`` +- ``remove(_:)`` +- ``remove(at:)`` + +### Sorted Set Operations + +- ``subscript(member:)`` +- ``subscript(members:)-5nkxk`` +- ``subscript(members:)-5xfq5`` +- ``min()`` +- ``max()`` +- ``sorted()`` + +### Binary Set Operations + +- ``intersection(_:)-1wfb5`` +- ``intersection(_:)-4evdp`` +- ``intersection(_:)-9rtcc`` +- ``intersection(_:)-13us`` + +- ``union(_:)-2okwt`` +- ``union(_:)-pwqf`` +- ``union(_:)-18u31`` +- ``union(_:)-8ysz9`` + +- ``subtracting(_:)-7u4tf`` +- ``subtracting(_:)-5vgml`` +- ``subtracting(_:)-6scy1`` +- ``subtracting(_:)-82loi`` + +- ``symmetricDifference(_:)-84e40`` +- ``symmetricDifference(_:)-3suo3`` +- ``symmetricDifference(_:)-7zx5q`` +- ``symmetricDifference(_:)-46ni1`` + +- ``formIntersection(_:)-49and`` +- ``formIntersection(_:)-49a0x`` +- ``formIntersection(_:)-79anv`` +- ``formIntersection(_:)-3zoc4`` + +- ``formUnion(_:)-c6a3`` +- ``formUnion(_:)-c5kv`` +- ``formUnion(_:)-2f05x`` +- ``formUnion(_:)-8kilf`` + +- ``subtract(_:)-2hzty`` +- ``subtract(_:)-2i1qq`` +- ``subtract(_:)-32jtb`` +- ``subtract(_:)-75xgt`` + +- ``formSymmetricDifference(_:)-6vskl`` +- ``formSymmetricDifference(_:)-6vs05`` +- ``formSymmetricDifference(_:)-d2kd`` +- ``formSymmetricDifference(_:)-54ghn`` + +### Binary Set Predicates + +- ``==(_:_:)`` +- ``isEqualSet(to:)-11031`` +- ``isEqualSet(to:)-1hvpp`` +- ``isEqualSet(to:)-1mvpq`` +- ``isEqualSet(to:)-878x1`` + +- ``isSubset(of:)-8iy8c`` +- ``isSubset(of:)-1r41b`` +- ``isSubset(of:)-1dz0p`` +- ``isSubset(of:)-3bq5m`` + +- ``isSuperset(of:)-48i5c`` +- ``isSuperset(of:)-10gu8`` +- ``isSuperset(of:)-8b7lq`` +- ``isSuperset(of:)-6slai`` + +- ``isStrictSubset(of:)-5ry1b`` +- ``isStrictSubset(of:)-2ndu3`` +- ``isStrictSubset(of:)-9iul0`` +- ``isStrictSubset(of:)-2pq1j`` + +- ``isStrictSuperset(of:)-9mgmd`` +- ``isStrictSuperset(of:)-6hw4t`` +- ``isStrictSuperset(of:)-1ya0j`` +- ``isStrictSuperset(of:)-4qt1e`` + +- ``isDisjoint(with:)-9wyku`` +- ``isDisjoint(with:)-5fww0`` +- ``isDisjoint(with:)-6p0t7`` +- ``isDisjoint(with:)-eujj`` diff --git a/Sources/BitCollections/BitCollections.docc/Extensions/BitSet.md b/Sources/BitCollections/BitCollections.docc/Extensions/BitSet.md new file mode 100644 index 000000000..4f34e223d --- /dev/null +++ b/Sources/BitCollections/BitCollections.docc/Extensions/BitSet.md @@ -0,0 +1,130 @@ +# ``BitCollections/BitSet`` + + + + + +## Topics + +### Creating a Bit Set + +- ``init()`` +- ``init(reservingCapacity:)`` +- ``init(_:)-15cws`` +- ``init(_:)-38hho`` +- ``init(_:)-2of3i`` +- ``init(_:)-5fhls`` +- ``init(bitPattern:)`` +- ``init(words:)`` +- ``random(upTo:)`` +- ``random(upTo:using:)`` + +### Finding Elements + +- ``contains(_:)`` +- ``firstIndex(of:)`` +- ``lastIndex(of:)`` + +### Adding and Updating Elements + +- ``insert(_:)`` +- ``update(with:)`` + +### Removing Elements + +- ``filter(_:)`` +- ``remove(_:)`` +- ``remove(at:)`` + +### Sorted Set Operations + +- ``subscript(member:)`` +- ``subscript(members:)-5nkxk`` +- ``subscript(members:)-5xfq5`` +- ``min()`` +- ``max()`` +- ``sorted()`` + +### Combining Sets + +- ``intersection(_:)-84q4u`` +- ``intersection(_:)-8hcl9`` +- ``intersection(_:)-7l8p3`` +- ``intersection(_:)-7kgi`` + +- ``union(_:)-5kqmx`` +- ``union(_:)-6mj8`` +- ``union(_:)-50wc4`` +- ``union(_:)-10had`` + +- ``subtracting(_:)-79e0o`` +- ``subtracting(_:)-7re82`` +- ``subtracting(_:)-7rn26`` +- ``subtracting(_:)-42s7d`` + +- ``symmetricDifference(_:)-55kqn`` +- ``symmetricDifference(_:)-5xt65`` +- ``symmetricDifference(_:)-91kh8`` +- ``symmetricDifference(_:)-79wfx`` + +- ``formIntersection(_:)-u07v`` +- ``formIntersection(_:)-87gjl`` +- ``formIntersection(_:)-9gffv`` +- ``formIntersection(_:)-8t2je`` + +- ``formUnion(_:)-72o7q`` +- ``formUnion(_:)-370hb`` +- ``formUnion(_:)-7tw8j`` +- ``formUnion(_:)-12ll3`` + +- ``subtract(_:)-9aabm`` +- ``subtract(_:)-1o083`` +- ``subtract(_:)-6kijg`` +- ``subtract(_:)-3pynh`` + +- ``formSymmetricDifference(_:)-2le2k`` +- ``formSymmetricDifference(_:)-5edyr`` +- ``formSymmetricDifference(_:)-7wole`` +- ``formSymmetricDifference(_:)-8vcnf`` + +### Comparing Sets + +- ``==(_:_:)`` +- ``isEqualSet(to:)-4xfa9`` +- ``isEqualSet(to:)-359ao`` +- ``isEqualSet(to:)-5ap6y`` +- ``isEqualSet(to:)-2dezf`` + +- ``isSubset(of:)-73apg`` +- ``isSubset(of:)-14xt1`` +- ``isSubset(of:)-4mj71`` +- ``isSubset(of:)-20wxs`` + +- ``isSuperset(of:)-1mfg2`` +- ``isSuperset(of:)-5adir`` +- ``isSuperset(of:)-4y68t`` +- ``isSuperset(of:)-2m7mj`` + +- ``isStrictSubset(of:)-8m1z6`` +- ``isStrictSubset(of:)-3y2l1`` +- ``isStrictSubset(of:)-97rky`` +- ``isStrictSubset(of:)-p3zj`` + +- ``isStrictSuperset(of:)-6e5gm`` +- ``isStrictSuperset(of:)-735zn`` +- ``isStrictSuperset(of:)-26acy`` +- ``isStrictSuperset(of:)-5jmxx`` + +- ``isDisjoint(with:)-2cdg6`` +- ``isDisjoint(with:)-3klxy`` +- ``isDisjoint(with:)-4uidy`` +- ``isDisjoint(with:)-78a8w`` + +### Memory Management + +- ``reserveCapacity(_:)`` + +### Collection Views + +- ``Counted-swift.struct`` +- ``counted-swift.property`` diff --git a/Sources/BitCollections/BitSet/BitSet+BidirectionalCollection.swift b/Sources/BitCollections/BitSet/BitSet+BidirectionalCollection.swift new file mode 100644 index 000000000..30a57cceb --- /dev/null +++ b/Sources/BitCollections/BitSet/BitSet+BidirectionalCollection.swift @@ -0,0 +1,267 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 BitSet: Sequence { + /// The type representing the bit set's elements. + /// Bit sets are collections of nonnegative integers. + public typealias Element = Int + + /// Returns the exact count of the bit set. + /// + /// - Complexity: O(1) + @inlinable + @inline(__always) + public var underestimatedCount: Int { + return count + } + + /// Returns an iterator over the elements of the bit set. + /// + /// - Complexity: O(1) + @inlinable + public func makeIterator() -> Iterator { + return Iterator(self) + } + + public func _customContainsEquatableElement( + _ element: Int + ) -> Bool? { + guard let element = UInt(exactly: element) else { return false } + return _contains(element) + } + + /// An iterator over the members of a bit set. + public struct Iterator: IteratorProtocol { + internal typealias _UnsafeHandle = BitSet._UnsafeHandle + + internal let bitset: BitSet + internal var index: Int + internal var word: _Word + + @usableFromInline + internal init(_ bitset: BitSet) { + self.bitset = bitset + self.index = 0 + self.word = bitset._read { handle in + guard handle.wordCount > 0 else { return .empty } + return handle._words[0] + } + } + + /// Advances to the next element and returns it, or `nil` if no next element + /// exists. + /// + /// Once `nil` has been returned, all subsequent calls return `nil`. + /// + /// - Complexity: + /// Each individual call has a worst case time complexity of O(*n*), + /// where *n* is largest element in the set, as each call needs to + /// search for the next `true` bit in the underlying storage. + /// However, each storage bit is only visited once, so iterating over the + /// entire set has the same O(*n*) complexity. + @_effects(releasenone) + public mutating func next() -> Int? { + if let bit = word.next() { + let i = _UnsafeHandle.Index(word: index, bit: bit) + return Int(truncatingIfNeeded: i.value) + } + return bitset._read { handle in + while (index + 1) < handle.wordCount { + index += 1 + word = handle._words[index] + if let bit = word.next() { + let i = _UnsafeHandle.Index(word: index, bit: bit) + return Int(truncatingIfNeeded: i.value) + } + } + return nil + } + } + } +} + +extension BitSet.Iterator: Sendable {} + +extension BitSet: Collection, BidirectionalCollection { + /// A Boolean value indicating whether the collection is empty. + /// + /// - Complexity: O(*min*) where *min* is the value of the first element. + /// (The complexity is O(1) if the set is empty.) + public var isEmpty: Bool { _storage.firstIndex { !$0.isEmpty } == nil } + + /// The number of elements in the bit set. + /// + /// - Complexity: O(*max*) where *max* is the value of the largest element. + /// (The complexity is O(1) if the set is empty.) + /// + /// - Note: `BitSet.Counted` is a variant of this type that keeps a running + /// total of its element count, for use cases that require an O(1) count. + public var count: Int { + return _read { $0.count } + } + + /// The position of the first element in a nonempty set, or `endIndex` + /// if the collection is empty. + /// + /// - Complexity: O(*min*) where *min* is the value of the first element. + public var startIndex: Index { + Index(_position: _read { $0.startIndex }) + } + + /// The collection’s “past the end” position--that is, the position one step + /// after the last valid subscript argument. + /// + /// - Complexity: O(1) + public var endIndex: Index { + Index(_position: .init(word: _storage.count, bit: 0)) + } + + /// Accesses the element at the specified position. + /// + /// You can subscript a collection with any valid index other than the + /// collection's end index. The end index refers to the position one past + /// the last element of a collection, so it doesn't correspond with an + /// element. + /// + /// - Parameter position: The position of the element to access. `position` + /// must be a valid index of the collection that is not equal to the + /// `endIndex` property. + /// + /// - Complexity: O(1) + public subscript(position: Index) -> Int { + let v = Int(bitPattern: position._value) + assert(contains(v)) + return v + } + + /// Returns the position immediately after the given index. + /// + /// - Parameter `index`: A valid index of the bit set. `index` must be less + /// than `endIndex`. + /// + /// - Returns: The valid index immediately after `index`. + /// + /// - Complexity: + /// O(*d*), where *d* is difference between the value of the member + /// addressed by `index` and the member following it in the set. + /// (Each call needs to search for the next `true` bit in the underlying + /// storage.) + public func index(after index: Index) -> Index { + _read { handle in + assert(handle._isReachable(index._position), "Invalid index") + let pos = handle.index(after: index._position) + return Index(_position: pos) + } + } + + /// Returns the position immediately before the given index. + /// + /// - Parameter `index`: A valid index of the bit set. + /// `index` must be greater than `startIndex`. + /// + /// - Returns: The preceding valid index immediately before `index`. + /// + /// - Complexity: + /// O(*d*), where *d* is difference between the value of the member + /// addressed by `index` and the member preceding it in the set. + /// (Each call needs to search for the next `true` bit in the underlying + /// storage.) + public func index(before index: Index) -> Index { + _read { handle in + assert(handle._isReachable(index._position), "Invalid index") + let pos = handle.index(before: index._position) + return Index(_position: pos) + } + } + + /// Returns the distance between two indices. + /// + /// - Parameters: + /// - start: A valid index of the collection. + /// - end: Another valid index of the collection. If `end` is equal to + /// `start`, the result is zero. + /// - Returns: The distance between `start` and `end`. + /// + /// - Complexity: O(*d*), where *d* is the difference of the values + /// addressed by the two input indices. + public func distance(from start: Index, to end: Index) -> Int { + _read { handle in + assert(handle._isReachable(start._position), "Invalid start index") + assert(handle._isReachable(end._position), "Invalid end index") + return handle.distance(from: start._position, to: end._position) + } + } + + /// Returns an index that is the specified distance from the given index. + /// + /// The value passed as `distance` must not offset `i` beyond the bounds of + /// the collection. + /// + /// - Parameters: + /// - i: A valid index of the collection. + /// - distance: The distance to offset `i`. + /// - Returns: An index offset by `distance` from the index `i`. If + /// `distance` is positive, this is the same value as the result of + /// `distance` calls to `index(after:)`. If `distance` is negative, this + /// is the same value as the result of `abs(distance)` calls to + /// `index(before:)`. + /// + /// - Complexity: O(*d*), where *d* is the difference of the values + /// addressed by `index` and the returned result. + public func index(_ index: Index, offsetBy distance: Int) -> Index { + _read { handle in + assert(handle._isReachable(index._position), "Invalid index") + let pos = handle.index(index._position, offsetBy: distance) + return Index(_position: pos) + } + } + + /// Returns an index that is the specified distance from the given index, + /// unless that distance is beyond a given limiting index. + /// + /// The value passed as `distance` must not offset `i` beyond the bounds of + /// the collection, unless the index passed as `limit` prevents offsetting + /// beyond those bounds. + /// + /// - Parameters: + /// - i: A valid index of the collection. + /// - distance: The distance to offset `i`. + /// - limit: A valid index of the collection to use as a limit. If + /// `distance > 0`, a limit that is less than `i` has no effect. + /// Likewise, if `distance < 0`, a limit that is greater than `i` has no + /// effect. + /// - Returns: An index offset by `distance` from the index `i`, unless that + /// index would be beyond `limit` in the direction of movement. In that + /// case, the method returns `nil`. + /// + /// - Complexity: O(*d*), where *d* is the difference of the values + /// addressed by `index` and the returned result. + public func index( + _ i: Index, offsetBy distance: Int, limitedBy limit: Index + ) -> Index? { + _read { handle in + assert(handle._isReachable(i._position), "Invalid index") + assert(handle._isReachable(limit._position), "Invalid limit index") + return handle.index( + i._position, offsetBy: distance, limitedBy: limit._position + ).map { Index(_position: $0) } + } + } + + public func _customIndexOfEquatableElement(_ element: Int) -> Index?? { + guard contains(element) else { return .some(nil) } + return Index(_value: UInt(bitPattern: element)) + } + + public func _customLastIndexOfEquatableElement(_ element: Int) -> Index?? { + _customIndexOfEquatableElement(element) + } +} diff --git a/Sources/BitCollections/BitSet/BitSet+Codable.swift b/Sources/BitCollections/BitSet/BitSet+Codable.swift new file mode 100644 index 000000000..8563193a3 --- /dev/null +++ b/Sources/BitCollections/BitSet/BitSet+Codable.swift @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 BitSet: Codable { + /// Encodes this bit set into the given encoder. + /// + /// Bit sets are encoded as an unkeyed container of `UInt64` values, + /// representing pieces of the underlying bitmap. + /// + /// - Parameter encoder: The encoder to write data to. + public func encode(to encoder: Encoder) throws { + var container = encoder.unkeyedContainer() + try _storage._encodeAsUInt64(to: &container) + } + + /// Creates a new bit set by decoding from the given decoder. + /// + /// Bit sets are encoded as an unkeyed container of `UInt64` values, + /// representing pieces of the underlying bitmap. + /// + /// - Parameter decoder: The decoder to read data from. + public init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + let words = try [_Word]( + _fromUInt64: &container, reservingCount: container.count) + self.init(_words: words) + } +} diff --git a/Sources/BitCollections/BitSet/BitSet+CustomDebugStringConvertible.swift b/Sources/BitCollections/BitSet/BitSet+CustomDebugStringConvertible.swift new file mode 100644 index 000000000..0a0ad7418 --- /dev/null +++ b/Sources/BitCollections/BitSet/BitSet+CustomDebugStringConvertible.swift @@ -0,0 +1,17 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 BitSet: CustomDebugStringConvertible { + /// A textual representation of this instance, suitable for debugging. + public var debugDescription: String { + description + } +} diff --git a/Sources/BitCollections/BitSet/BitSet+CustomReflectable.swift b/Sources/BitCollections/BitSet/BitSet+CustomReflectable.swift new file mode 100644 index 000000000..6827e9e62 --- /dev/null +++ b/Sources/BitCollections/BitSet/BitSet+CustomReflectable.swift @@ -0,0 +1,17 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 BitSet: CustomReflectable { + /// The custom mirror for this instance. + public var customMirror: Mirror { + Mirror(self, unlabeledChildren: self, displayStyle: .set) + } +} diff --git a/Sources/BitCollections/BitSet/BitSet+CustomStringConvertible.swift b/Sources/BitCollections/BitSet/BitSet+CustomStringConvertible.swift new file mode 100644 index 000000000..773043fcf --- /dev/null +++ b/Sources/BitCollections/BitSet/BitSet+CustomStringConvertible.swift @@ -0,0 +1,21 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension BitSet: CustomStringConvertible { + // A textual representation of this instance. + public var description: String { + _arrayDescription(for: self) + } +} diff --git a/Sources/BitCollections/BitSet/BitSet+Equatable.swift b/Sources/BitCollections/BitSet/BitSet+Equatable.swift new file mode 100644 index 000000000..1d821d6cb --- /dev/null +++ b/Sources/BitCollections/BitSet/BitSet+Equatable.swift @@ -0,0 +1,25 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 BitSet: Equatable { + /// Returns a Boolean value indicating whether two values are equal. Two + /// bit sets are considered equal if they contain the same elements. + /// + /// - Note: This simply forwards to the ``isEqualSet(to:)-4xfa9`` method. + /// That method has additional overloads that can be used to compare + /// bit sets with additional types. + /// + /// - Complexity: O(*max*), where *max* is value of the largest member of + /// either set. + public static func ==(left: Self, right: Self) -> Bool { + left.isEqualSet(to: right) + } +} diff --git a/Sources/BitCollections/BitSet/BitSet+ExpressibleByArrayLiteral.swift b/Sources/BitCollections/BitSet/BitSet+ExpressibleByArrayLiteral.swift new file mode 100644 index 000000000..7afc94118 --- /dev/null +++ b/Sources/BitCollections/BitSet/BitSet+ExpressibleByArrayLiteral.swift @@ -0,0 +1,27 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 BitSet: ExpressibleByArrayLiteral { + /// Creates a new bit set from the contents of an array literal. + /// + /// Do not call this initializer directly. It is used by the compiler when + /// you use an array literal. Instead, create a new bit set using an array + /// literal as its value by enclosing a comma-separated list of values in + /// square brackets. You can use an array literal anywhere a bit set is + /// expected by the type context. + /// + /// - Parameter elements: A variadic list of elements of the new set. + /// - Complexity: O(`elements.count`) + @inlinable + public init(arrayLiteral elements: Int...) { + self.init(elements) + } +} diff --git a/Sources/BitCollections/BitSet/BitSet+Extras.swift b/Sources/BitCollections/BitSet/BitSet+Extras.swift new file mode 100644 index 000000000..242b74829 --- /dev/null +++ b/Sources/BitCollections/BitSet/BitSet+Extras.swift @@ -0,0 +1,196 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension BitSet: _UniqueCollection {} + +extension BitSet { + /// Creates a new empty bit set with enough storage capacity to store values + /// up to the given maximum value without reallocating storage. + /// + /// - Parameter maximumValue: The desired maximum value. + public init(reservingCapacity maximumValue: Int) { + self.init() + self.reserveCapacity(maximumValue) + } + + /// Prepares the bit set to store the specified maximum value without + /// reallocating storage. + /// + /// - Parameter maximumValue: The desired maximum value. + public mutating func reserveCapacity(_ maximumValue: Int) { + let wc = _Word.wordCount(forBitCount: UInt(Swift.max(0, maximumValue)) + 1) + _storage.reserveCapacity(wc) + } +} + +extension BitSet { + /// A subscript operation for querying or updating membership in this + /// bit set as a boolean value. + /// + /// This is operation is a convenience shortcut for the `contains`, `insert` + /// and `remove` operations, enabling a uniform syntax that resembles the + /// corresponding `BitArray` subscript operation. + /// + /// var bits: BitSet = [1, 2, 3] + /// bits[member: 4] = true // equivalent to `bits.insert(4)` + /// bits[member: 2] = false // equivalent to `bits.remove(2)` + /// bits[member: 5].toggle() + /// + /// print(bits) // [1, 3, 4, 5] + /// print(bits[member: 4]) // true, equivalent to `bits.contains(4)` + /// print(bits[member: -4]) // false + /// print(bits[member: 10]) // false + /// + /// Note that unlike `BitArray`'s subscript, this operation may dynamically + /// resizes the underlying bitmap storage as needed. + /// + /// - Parameter member: An integer value. When setting membership via this + /// subscript, the value must be nonnegative. + /// - Returns: `true` if the bit set contains `member`, `false` otherwise. + /// - Complexity: O(1) + public subscript(member member: Int) -> Bool { + get { + contains(member) + } + set { + guard let member = UInt(exactly: member) else { + precondition(!newValue, "Can't insert a negative value to a BitSet") + return + } + if newValue { + _ensureCapacity(forValue: member) + } else if member >= _capacity { + return + } + _updateThenShrink { handle, shrink in + shrink = handle.update(member, to: newValue) + } + } + } + + /// Accesses the contiguous subrange of the collection’s elements that are + /// contained within a specific integer range. + /// + /// let bits: BitSet = [2, 5, 6, 8, 9] + /// let a = bits[members: 3..<7] // [5, 6] + /// let b = bits[members: 4...] // [5, 6, 8, 9] + /// let c = bits[members: ..<8] // [2, 5, 6] + /// + /// This enables you to easily find the closest set member to any integer + /// value. + /// + /// let firstMemberNotLessThanFive = bits[members: 5...].first // Optional(6) + /// let lastMemberBelowFive = bits[members: ..<5].last // Optional(2) + /// + /// - Complexity: Equivalent to two invocations of `index(after:)`. + public subscript(members bounds: Range) -> Slice { + let bounds: Range = _read { handle in + let bounds = bounds._clampedToUInt() + var lower = _UnsafeBitSet.Index(bounds.lowerBound) + if lower >= handle.endIndex { + lower = handle.endIndex + } else if !handle.contains(lower.value) { + lower = handle.index(after: lower) + } + assert(lower == handle.endIndex || handle.contains(lower.value)) + + var upper = _UnsafeBitSet.Index(bounds.upperBound) + if upper <= lower { + upper = lower + } else if upper >= handle.endIndex { + upper = handle.endIndex + } else if !handle.contains(upper.value) { + upper = handle.index(after: upper) + } + assert(upper == handle.endIndex || handle.contains(upper.value)) + assert(lower <= upper) + return Range( + uncheckedBounds: (Index(_position: lower), Index(_position: upper))) + } + return Slice(base: self, bounds: bounds) + } + + /// Accesses the contiguous subrange of the collection’s elements that are + /// contained within a specific integer range expression. + /// + /// let bits: BitSet = [2, 5, 6, 8, 9] + /// let a = bits[members: 3..<7] // [5, 6] + /// let b = bits[members: 4...] // [5, 6, 8, 9] + /// let c = bits[members: ..<8] // [2, 5, 6] + /// + /// This enables you to easily find the closest set member to any integer + /// value. + /// + /// let firstMemberNotLessThanFive = bits[members: 5...].first + /// // Optional(6) + /// + /// let lastMemberBelowFive = bits[members: ..<5].last + /// // Optional(2) + /// + /// - Complexity: Equivalent to two invocations of `index(after:)`. + public subscript(members bounds: some RangeExpression) -> Slice { + let bounds = bounds.relative(to: Int.min ..< Int.max) + return self[members: bounds] + } +} + +extension BitSet { + /// Removes and returns the element at the specified position. + /// + /// - Parameter i: The position of the element to remove. `index` must be + /// a valid index of the collection that is not equal to the collection's + /// end index. + /// + /// - Returns: The removed element. + /// + /// - Complexity: O(`1`) if the set is a unique value (with no live copies), + /// and the removed value is less than the largest value currently in the + /// set (named *max*). Otherwise the complexity is at worst O(*max*). + @discardableResult + public mutating func remove(at index: Index) -> Element { + let removed = _remove(index._value) + precondition(removed, "Invalid index") + return Int(bitPattern: index._value) + } + + /// Returns a new bit set containing the elements of the set that satisfy the + /// given predicate. + /// + /// In this example, `filter(_:)` is used to include only even members. + /// + /// let bits = BitSet(0 ..< 20) + /// let evens = bits.filter { $0.isMultiple(of: 2) } + /// + /// evens.isSubset(of: bits) // true + /// evens.contains(5) // false + /// + /// - Parameter isIncluded: A closure that takes an element as its argument + /// and returns a Boolean value indicating whether the element should be + /// included in the returned set. + /// - Returns: A set of the elements that `isIncluded` allows. + public func filter( + _ isIncluded: (Element) throws -> Bool + ) rethrows -> Self { + var words = [_Word](repeating: .empty, count: _storage.count) + try words.withUnsafeMutableBufferPointer { buffer in + var target = _UnsafeHandle(words: buffer, mutable: true) + for i in self { + guard try isIncluded(i) else { continue } + target.insert(UInt(i)) + } + } + return BitSet(_words: words) + } +} diff --git a/Sources/BitCollections/BitSet/BitSet+Hashable.swift b/Sources/BitCollections/BitSet/BitSet+Hashable.swift new file mode 100644 index 000000000..8beecf623 --- /dev/null +++ b/Sources/BitCollections/BitSet/BitSet+Hashable.swift @@ -0,0 +1,20 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 BitSet: Hashable { + /// Hashes the essential components of this value by feeding them into the + /// given hasher. + /// + /// Complexity: O(*max*) where *max* is the largest value stored in this set. + public func hash(into hasher: inout Hasher) { + hasher.combine(_storage) + } +} diff --git a/Sources/BitCollections/BitSet/BitSet+Initializers.swift b/Sources/BitCollections/BitSet/BitSet+Initializers.swift new file mode 100644 index 000000000..495135d3e --- /dev/null +++ b/Sources/BitCollections/BitSet/BitSet+Initializers.swift @@ -0,0 +1,217 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension BitSet { + /// Initializes a new, empty bit set. + /// + /// This is equivalent to initializing with an empty array literal. + /// For example: + /// + /// let set1 = BitSet() + /// print(set1.isEmpty) // true + /// + /// let set2: BitSet = [] + /// print(set2.isEmpty) // true + /// + /// - Complexity: O(1) + public init() { + self.init(_rawStorage: []) + } + + @usableFromInline + init(_words: [_Word]) { + self._storage = _words + _shrink() + _checkInvariants() + } + + /// Initialize a new bit set from the raw bits of the supplied sequence of + /// words. (The term "words" is used here to mean a sequence of `UInt` + /// values, as in the `words` property of `BinaryInteger`.) + /// + /// The resulting bit set will contain precisely those integers that + /// correspond to `true` bits within `words`. Bits are counted from least + /// to most significant within each word. + /// + /// let bits = BitSet(words: [5, 2]) + /// // bits is [0, 2, UInt.bitWidth + 1] + /// + /// - Complexity: O(`words.count`) + @inlinable + public init(words: some Sequence) { + self.init(_words: words.map { _Word($0) }) + } + + /// Initialize a new bit set from the raw bits of the supplied integer value. + /// + /// The resulting bit set will contain precisely those integers that + /// correspond to `true` bits within `x`. Bits are counted from least + /// to most significant. + /// + /// let bits = BitSet(bitPattern: 42) + /// // bits is [1, 3, 5] + /// + /// - Complexity: O(`x.bitWidth`) + @inlinable + public init(bitPattern x: some BinaryInteger) { + self.init(words: x.words) + } + + /// Initialize a new bit set from the storage bits of the given bit array. + /// The resulting bit set will contain exactly those integers that address + /// `true` elements in the array. + /// + /// Note that this conversion is lossy -- it discards the precise length of + /// the input array. + /// + /// - Complexity: O(`array.count`) + public init(_ array: BitArray) { + self.init(_words: array._storage) + } + + /// Create a new bit set containing the elements of a sequence. + /// + /// - Parameters: + /// - elements: The sequence of elements to turn into a bit set. + /// + /// - Complexity: O(*n*), where *n* is the number of elements in the sequence. + @inlinable + public init( + _ elements: __owned some Sequence + ) { + if let elements = _specialize(elements, for: BitSet.self) { + self = elements + return + } + if let elements = _specialize(elements, for: BitSet.Counted.self) { + self = elements._bits + return + } + self.init() + for value in elements { + self.insert(value) + } + } + + /// Create a new bit set containing all the nonnegative elements of a + /// sequence of integers. + /// + /// Items in `elements` that are negative are silently ignored. + /// + /// - Parameters: + /// - elements: The sequence of elements to turn into a bit set. + /// + /// - Complexity: O(*n*), where *n* is the number of elements in the sequence. + @inlinable + internal init( + _validMembersOf elements: __owned some Sequence + ) { + if let elements = _specialize(elements, for: BitSet.self) { + self = elements + return + } + if let elements = _specialize(elements, for: BitSet.Counted.self) { + self = elements._bits + return + } + if let elements = _specialize(elements, for: Range.self) { + let r = elements + self.init(_range: r._clampedToUInt()) + return + } + self.init() + for value in elements { + guard let value = UInt(exactly: value) else { continue } + self._insert(value) + } + } +} + +extension BitSet { + /// Create a new bit set containing the elements of a range of integers. + /// + /// - Parameters: + /// - range: The range to turn into a bit set. The range must not contain + /// negative values. + /// + /// - Complexity: O(`range.upperBound`) + public init(_ range: Range) { + guard let range = range._toUInt() else { + preconditionFailure("BitSet can only hold nonnegative integers") + } + self.init(_range: range) + } + + @usableFromInline + internal init(_range range: Range) { + _storage = [] + let lower = _UnsafeHandle.Index(range.lowerBound) + let upper = _UnsafeHandle.Index(range.upperBound) + if lower.word > 0 { + _storage.append(contentsOf: repeatElement(.empty, count: lower.word)) + } + if lower.word == upper.word { + _storage.append(_Word(from: lower.bit, to: upper.bit)) + } else { + _storage.append(_Word(upTo: lower.bit).complement()) + let filledWords = upper.word &- lower.word + if filledWords > 0 { + _storage.append( + contentsOf: repeatElement(.allBits, count: filledWords &- 1)) + } + _storage.append(_Word(upTo: upper.bit)) + } + _shrink() + _checkInvariants() + } +} + +extension BitSet { + internal init( + _combining handles: (_UnsafeHandle, _UnsafeHandle), + includingTail: Bool, + using function: (_Word, _Word) -> _Word + ) { + let w1 = handles.0._words + let w2 = handles.1._words + let capacity = ( + includingTail + ? Swift.max(w1.count, w2.count) + : Swift.min(w1.count, w2.count)) + _storage = Array(unsafeUninitializedCapacity: capacity) { buffer, count in + let sharedCount = Swift.min(w1.count, w2.count) + for w in 0 ..< sharedCount { + buffer.initializeElement(at: w, to: function(w1[w], w2[w])) + } + if includingTail { + if w1.count < w2.count { + for w in w1.count ..< w2.count { + buffer.initializeElement(at: w, to: function(_Word.empty, w2[w])) + } + } else { + for w in w2.count ..< w1.count { + buffer.initializeElement(at: w, to: function(w1[w], _Word.empty)) + } + } + } + // Adjust the word count based on results. + count = capacity + while count > 0, buffer[count - 1].isEmpty { + count -= 1 + } + } + _checkInvariants() + } +} diff --git a/Sources/BitCollections/BitSet/BitSet+Invariants.swift b/Sources/BitCollections/BitSet/BitSet+Invariants.swift new file mode 100644 index 000000000..693f5d183 --- /dev/null +++ b/Sources/BitCollections/BitSet/BitSet+Invariants.swift @@ -0,0 +1,41 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension BitSet { + /// True if consistency checking is enabled in the implementation of this + /// type, false otherwise. + /// + /// Documented performance promises are null and void when this property + /// returns true -- for example, operations that are documented to take + /// O(1) time might take O(*n*) time, or worse. + public static var _isConsistencyCheckingEnabled: Bool { + _isCollectionsInternalCheckingEnabled + } + +#if COLLECTIONS_INTERNAL_CHECKS + @inline(never) + @_effects(releasenone) + public func _checkInvariants() { + //let actualCount = _storage.reduce(into: 0) { $0 += $1.count } + //precondition(_count == actualCount, "Invalid count") + + precondition(_storage.isEmpty || !_storage.last!.isEmpty, + "Extraneous tail slot") + } +#else + @inline(__always) @inlinable + public func _checkInvariants() {} +#endif // COLLECTIONS_INTERNAL_CHECKS +} diff --git a/Sources/BitCollections/BitSet/BitSet+Random.swift b/Sources/BitCollections/BitSet/BitSet+Random.swift new file mode 100644 index 000000000..5e7977920 --- /dev/null +++ b/Sources/BitCollections/BitSet/BitSet+Random.swift @@ -0,0 +1,29 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 BitSet { + public static func random(upTo limit: Int) -> BitSet { + var rng = SystemRandomNumberGenerator() + return random(upTo: limit, using: &rng) + } + + public static func random( + upTo limit: Int, + using rng: inout some RandomNumberGenerator + ) -> BitSet { + precondition(limit >= 0, "Invalid limit value") + guard limit > 0 else { return BitSet() } + let (w, b) = _UnsafeHandle.Index(limit).endSplit + var words = (0 ... w).map { _ in _Word(rng.next() as UInt) } + words[w].formIntersection(_Word(upTo: b)) + return BitSet(_words: words) + } +} diff --git a/Sources/BitCollections/BitSet/BitSet+SetAlgebra basics.swift b/Sources/BitCollections/BitSet/BitSet+SetAlgebra basics.swift new file mode 100644 index 000000000..a7a0f344d --- /dev/null +++ b/Sources/BitCollections/BitSet/BitSet+SetAlgebra basics.swift @@ -0,0 +1,131 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 BitSet { + /// Returns a Boolean value that indicates whether the given element exists + /// in the set. + /// + /// - Parameter element: An element to look for in the set. + /// + /// - Returns: `true` if `member` exists in the set; otherwise, `false`. + /// + /// - Complexity: O(1) + @usableFromInline + internal func _contains(_ member: UInt) -> Bool { + _read { $0.contains(member) } + } + + /// Returns a Boolean value that indicates whether the given element exists + /// in the set. + /// + /// - Parameter element: An element to look for in the set. + /// + /// - Returns: `true` if `member` exists in the set; otherwise, `false`. + /// + /// - Complexity: O(1) + public func contains(_ member: Int) -> Bool { + guard let member = UInt(exactly: member) else { return false } + return _contains(member) + } +} + +extension BitSet { + /// Insert the given element in the set if it is not already present. + /// + /// If an element equal to `newMember` is already contained in the set, this + /// method has no effect. + /// + /// If `newMember` was not already a member, it gets inserted into the set. + /// + /// - Parameter newMember: An element to insert into the set. + /// + /// - Returns: True if `newMember` was not contained in the + /// set, false otherwise. + /// + /// - Complexity: O(1) if the set is a unique value (with no other copies), + /// and the inserted value is not greater than the largest value currently + /// contained in the set (named *max*). Otherwise the complexity is + /// O(max(`newMember`, *max*)). + @discardableResult + @usableFromInline + internal mutating func _insert(_ newMember: UInt) -> Bool { + _ensureCapacity(forValue: newMember) + return _update { $0.insert(newMember) } + } + + /// Inserts the given element in the set if it is not already present. + /// + /// If an element equal to `newMember` is already contained in the set, this + /// method has no effect. + /// + /// If `newMember` was not already a member, it gets inserted into the set. + /// + /// - Parameter newMember: An element to insert into the set. + /// + /// - Returns: `(true, newMember)` if `newMember` was not contained in the + /// set. If `newMember` was already contained in the set, the method + /// returns `(false, newMember)`. + /// + /// - Complexity: O(1) if the set is a unique value (with no other copies), + /// and the inserted value is not greater than the largest value currently + /// contained in the set (named *max*). Otherwise the complexity is + /// O(max(`newMember`, *max*)). + @discardableResult + public mutating func insert( + _ newMember: Int + ) -> (inserted: Bool, memberAfterInsert: Int) { + guard let i = UInt(exactly: newMember) else { + preconditionFailure("Value out of range") + } + return (_insert(i), newMember) + } + + /// Inserts the given element into the set unconditionally. + /// + /// - Parameter newMember: An element to insert into the set. + /// + /// - Returns: `newMember` if the set already contained it; otherwise, `nil`. + /// + /// - Complexity: O(1) if the set is a unique value (with no live copies), + /// and the inserted value is not greater than the largest value currently + /// contained in the set (named *max*). Otherwise the complexity is + /// O(max(`newMember`, *max*)). + @discardableResult + public mutating func update(with newMember: Int) -> Int? { + insert(newMember).inserted ? newMember : nil + } +} + +extension BitSet { + @discardableResult + @usableFromInline + internal mutating func _remove(_ member: UInt) -> Bool { + _updateThenShrink { handle, shrink in + shrink = handle.remove(member) + return shrink + } + } + + /// Removes the given element from the set. + /// + /// - Parameter member: The element of the set to remove. + /// + /// - Returns: `member` if it was contained in the set; otherwise, `nil`. + /// + /// - Complexity: O(`1`) if the set is a unique value (with no live copies), + /// and the removed value is less than the largest value currently in the + /// set (named *max*). Otherwise the complexity is at worst O(*max*). + @discardableResult + public mutating func remove(_ member: Int) -> Int? { + guard let m = UInt(exactly: member) else { return nil } + return _remove(m) ? member : nil + } +} diff --git a/Sources/BitCollections/BitSet/BitSet+SetAlgebra conformance.swift b/Sources/BitCollections/BitSet/BitSet+SetAlgebra conformance.swift new file mode 100644 index 000000000..c38cb9a10 --- /dev/null +++ b/Sources/BitCollections/BitSet/BitSet+SetAlgebra conformance.swift @@ -0,0 +1,12 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 BitSet: SetAlgebra {} diff --git a/Sources/BitCollections/BitSet/BitSet+SetAlgebra formIntersection.swift b/Sources/BitCollections/BitSet/BitSet+SetAlgebra formIntersection.swift new file mode 100644 index 000000000..daae4a90e --- /dev/null +++ b/Sources/BitCollections/BitSet/BitSet+SetAlgebra formIntersection.swift @@ -0,0 +1,99 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension BitSet { + /// Removes the elements of this set that aren't also in the given one. + /// + /// var set: BitSet = [1, 2, 3, 4] + /// let other: BitSet = [0, 2, 4, 6] + /// set.formIntersection(other) + /// // set is now [2, 4] + /// + /// - Parameter other: A bit set. + /// + /// - Complexity: O(*max*), where *max* is the largest item in either set. + public mutating func formIntersection(_ other: Self) { + other._read { source in + if source.wordCount < _storage.count { + self._storage.removeLast(_storage.count - source.wordCount) + } + _updateThenShrink { target, shrink in + target.combineSharedPrefix( + with: source, using: { $0.formIntersection($1) }) + } + } + } + + /// Removes the elements of this set that aren't also in the given one. + /// + /// var set: BitSet = [1, 2, 3, 4] + /// let other: BitSet.Counted = [0, 2, 4, 6] + /// set.formIntersection(other) + /// // set is now [2, 4] + /// + /// - Parameter other: A bit set. + /// + /// - Complexity: O(*max*), where *max* is the largest item in either set. + public mutating func formIntersection(_ other: BitSet.Counted) { + formIntersection(other._bits) + } + + /// Removes the elements of this set that aren't also in the given range. + /// + /// var set: BitSet = [1, 2, 3, 4] + /// set.formIntersection(-10 ..< 3) + /// // set is now [3, 4] + /// + /// - Parameter other: A range of integers. + /// + /// - Complexity: O(*max*), where *max* is the largest item in `self`. + public mutating func formIntersection(_ other: Range) { + let other = other._clampedToUInt() + guard let last = other.last else { + self = BitSet() + return + } + let lastWord = _UnsafeHandle.Index(last).word + if _storage.count - lastWord - 1 > 0 { + _storage.removeLast(_storage.count - lastWord - 1) + } + _updateThenShrink { handle, shrink in + handle.formIntersection(other) + } + } + + /// Removes the elements of this set that aren't also in the given sequence. + /// + /// var set: BitSet = [1, 2, 3, 4] + /// let other: Set = [6, 4, 2, 0] + /// set.formIntersection(other) + /// // set is now [2, 4] + /// + /// - Parameter other: A sequence of integers. + /// + /// - Complexity: O(*max*) + *k*, where *max* is the largest item in `self`, + /// and *k* is the complexity of iterating over all elements in `other`. + @inlinable + public mutating func formIntersection( + _ other: __owned some Sequence + ) { + if let other = _specialize(other, for: Range.self) { + formIntersection(other) + return + } + // Note: BitSet & BitSet.Counted are handled in the BitSet initializer below + formIntersection(BitSet(_validMembersOf: other)) + } +} diff --git a/Sources/BitCollections/BitSet/BitSet+SetAlgebra formSymmetricDifference.swift b/Sources/BitCollections/BitSet/BitSet+SetAlgebra formSymmetricDifference.swift new file mode 100644 index 000000000..5b78eeea2 --- /dev/null +++ b/Sources/BitCollections/BitSet/BitSet+SetAlgebra formSymmetricDifference.swift @@ -0,0 +1,98 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension BitSet { + /// Replace this set with the elements contained in this set or the given + /// set, but not both. + /// + /// var set: BitSet = [1, 2, 3, 4] + /// let other: BitSet = [0, 2, 4, 6] + /// set.formSymmetricDifference(other) + /// // set is now [0, 1, 3, 6] + /// + /// - Parameter other: Another set. + /// + /// - Complexity: O(*max*), where *max* is the largest item in either set. + public mutating func formSymmetricDifference(_ other: Self) { + _ensureCapacity(limit: other._capacity) + _updateThenShrink { target, shrink in + other._read { source in + target.combineSharedPrefix( + with: source, using: { $0.formSymmetricDifference($1) }) + } + } + } + + /// Replace this set with the elements contained in this set or the given + /// set, but not both. + /// + /// var set: BitSet = [1, 2, 3, 4] + /// let other: BitSet.Counted = [0, 2, 4, 6] + /// set.formSymmetricDifference(other) + /// // set is now [0, 1, 3, 6] + /// + /// - Parameter other: Another set. + /// + /// - Complexity: O(*max*), where *max* is the largest item in either set. + public mutating func formSymmetricDifference(_ other: BitSet.Counted) { + formSymmetricDifference(other._bits) + } + + /// Replace this set with the elements contained in this set or the given + /// range of integers, but not both. + /// + /// var set: BitSet = [1, 2, 3, 4] + /// set.formSymmetricDifference(3 ..< 7) + /// // set is now [1, 2, 5, 6] + /// + /// - Parameter other: A range of nonnegative integers. + /// + /// - Complexity: O(*max*), where *max* is the largest item in either input. + public mutating func formSymmetricDifference(_ other: Range) { + guard let other = other._toUInt() else { + preconditionFailure("Invalid range") + } + guard !other.isEmpty else { return } + _ensureCapacity(limit: other.upperBound) + _updateThenShrink { handle, shrink in + handle.formSymmetricDifference(other) + } + } + + /// Replace this set with the elements contained in this set or the given + /// sequence, but not both. + /// + /// var set: BitSet = [1, 2, 3, 4] + /// let other = [6, 4, 2, 0, 2, 0] + /// set.formSymmetricDifference(other) + /// // set is now [0, 1, 3, 6] + /// + /// - Parameter other: A sequence of nonnegative integers. + /// + /// - Complexity: O(*max*) + *k*, where *max* is the largest item in either + /// input, and *k* is the complexity of iterating over all elements in + /// `other`. + @inlinable + public mutating func formSymmetricDifference( + _ other: __owned some Sequence + ) { + if let other = _specialize(other, for: Range.self) { + formSymmetricDifference(other) + return + } + // Note: BitSet & BitSet.Counted are handled in the BitSet initializer below + formSymmetricDifference(BitSet(other)) + } +} diff --git a/Sources/BitCollections/BitSet/BitSet+SetAlgebra formUnion.swift b/Sources/BitCollections/BitSet/BitSet+SetAlgebra formUnion.swift new file mode 100644 index 000000000..993e6e3f3 --- /dev/null +++ b/Sources/BitCollections/BitSet/BitSet+SetAlgebra formUnion.swift @@ -0,0 +1,100 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension BitSet { + /// Adds the elements of the given set to this set. + /// + /// var set: BitSet = [1, 2, 3, 4] + /// let other: BitSet = [0, 2, 4, 6] + /// set.formUnion(other) + /// // `set` is now `[0, 1, 2, 3, 4, 6]` + /// + /// - Parameter other: The set of elements to insert. + /// + /// - Complexity: O(*max*), where *max* is the largest item in either input. + public mutating func formUnion(_ other: Self) { + _ensureCapacity(limit: other._capacity) + _update { target in + other._read { source in + target.combineSharedPrefix(with: source) { $0.formUnion($1) } + } + } + } + + /// Adds the elements of the given set to this set. + /// + /// var set: BitSet = [1, 2, 3, 4] + /// let other: BitSet.Counted = [0, 2, 4, 6] + /// set.formUnion(other) + /// // `set` is now `[0, 1, 2, 3, 4, 6]` + /// + /// - Parameter other: The set of elements to insert. + /// + /// - Complexity: O(*max*), where *max* is the largest item in either input. + public mutating func formUnion(_ other: BitSet.Counted) { + formUnion(other._bits) + } + + /// Adds the elements of the given range of integers to this set. + /// + /// var set: BitSet = [1, 2, 3, 4] + /// set.formUnion(3 ..< 7) + /// // `set` is now `[1, 2, 3, 4, 5, 6]` + /// + /// - Parameter other: A range of nonnegative integers. + /// + /// - Complexity: O(*max*), where *max* is the largest item in either input. + public mutating func formUnion(_ other: Range) { + guard let other = other._toUInt() else { + preconditionFailure("Invalid range") + } + guard !other.isEmpty else { return } + _ensureCapacity(limit: other.upperBound) + _update { handle in + handle.formUnion(other) + } + } + + /// Adds the elements of the given sequence to this set. + /// + /// var set: BitSet = [1, 2, 3, 4] + /// let other = [6, 4, 2, 0, 2, 0] + /// set.formUnion(other) + /// // `set` is now `[0, 1, 2, 3, 4, 6]` + /// + /// - Parameter other: A sequence of nonnegative integers. + /// + /// - Complexity: O(*max*) + *k*, where *max* is the largest item in either + /// input, and *k* is the complexity of iterating over all elements in + /// `other`. + @inlinable + public mutating func formUnion(_ other: __owned some Sequence) { + if let other = _specialize(other, for: BitSet.self) { + formUnion(other) + return + } + if let other = _specialize(other, for: BitSet.Counted.self) { + formUnion(other) + return + } + if let other = _specialize(other, for: Range.self) { + formUnion(other) + return + } + for value in other { + self.insert(value) + } + } +} diff --git a/Sources/BitCollections/BitSet/BitSet+SetAlgebra intersection.swift b/Sources/BitCollections/BitSet/BitSet+SetAlgebra intersection.swift new file mode 100644 index 000000000..b75f8d4e0 --- /dev/null +++ b/Sources/BitCollections/BitSet/BitSet+SetAlgebra intersection.swift @@ -0,0 +1,91 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension BitSet { + /// Returns a new bit set with the elements that are common to both this set + /// and the given set. + /// + /// let a: BitSet = [1, 2, 3, 4] + /// let b: BitSet = [6, 4, 2, 0] + /// let c = a.intersection(b) + /// // c is now [2, 4] + /// + /// - Parameter other: A bit set. + /// + /// - Complexity: O(*max*), where *max* is the largest item in either set. + public func intersection(_ other: Self) -> Self { + self._read { first in + other._read { second in + Self( + _combining: (first, second), + includingTail: false, + using: { $0.intersection($1) }) + } + } + } + + /// Returns a new bit set with the elements that are common to both this set + /// and the given set. + /// + /// let a: BitSet = [1, 2, 3, 4] + /// let b: BitSet.Counted = [6, 4, 2, 0] + /// let c = a.intersection(b) + /// // c is now [2, 4] + /// + /// - Parameter other: A bit set. + /// + /// - Complexity: O(*max*), where *max* is the largest item in either set. + public func intersection(_ other: BitSet.Counted) -> Self { + self.intersection(other._bits) + } + + /// Returns a new bit set with the elements that are common to both this set + /// and the given range of integers. + /// + /// let a: BitSet = [1, 2, 3, 4] + /// let c = a.intersection(-10 ..< 3) + /// // c is now [3, 4] + /// + /// - Parameter other: A range of integers. + /// + /// - Complexity: O(*max*) + *k*, where *max* is the largest item in `self`, + /// and *k* is the complexity of iterating over all elements in `other`. + public func intersection(_ other: Range) -> Self { + var result = self + result.formIntersection(other) + return result + } + + /// Returns a new bit set with the elements that are common to both this set + /// and the given sequence. + /// + /// let a: BitSet = [1, 2, 3, 4] + /// let b = [6, 4, 2, 0] + /// let c = a.intersection(b) + /// // c is now [2, 4] + /// + /// - Parameter other: A sequence of integer values. + /// + /// - Complexity: O(*max*) + *k*, where *max* is the largest item in `self`, + /// and *k* is the complexity of iterating over all elements in `other`. + @inlinable + public func intersection(_ other: __owned some Sequence) -> Self { + if let other = _specialize(other, for: Range.self) { + return intersection(other) + } + // Note: BitSet & BitSet.Counted are handled in the BitSet initializer below + return intersection(BitSet(_validMembersOf: other)) + } +} diff --git a/Sources/BitCollections/BitSet/BitSet+SetAlgebra isDisjoint.swift b/Sources/BitCollections/BitSet/BitSet+SetAlgebra isDisjoint.swift new file mode 100644 index 000000000..5817c0847 --- /dev/null +++ b/Sources/BitCollections/BitSet/BitSet+SetAlgebra isDisjoint.swift @@ -0,0 +1,106 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension BitSet { + /// Returns a Boolean value that indicates whether the set has no members in + /// common with the given set. + /// + /// let a: BitSet = [1, 2, 3, 4] + /// let b: BitSet = [5, 6] + /// a.isDisjoint(with: b) // true + /// + /// - Parameter other: Another bit set. + /// + /// - Returns: `true` if `self` has no elements in common with `other`; + /// otherwise, `false`. + /// + /// - Complexity: O(*max*), where *max* is the largest item in either input. + public func isDisjoint(with other: Self) -> Bool { + self._read { first in + other._read { second in + let w1 = first._words + let w2 = second._words + for i in 0 ..< Swift.min(w1.count, w2.count) { + if !w1[i].intersection(w2[i]).isEmpty { return false } + } + return true + } + } + } + + /// Returns a Boolean value that indicates whether a bit set has no members + /// in common with the given counted bit set. + /// + /// let a: BitSet = [1, 2, 3, 4] + /// let b: BitSet.Counted = [5, 6] + /// a.isDisjoint(with: b) // true + /// + /// - Parameter other: A counted bit set. + /// + /// - Returns: `true` if `self` has no elements in common with `other`; + /// otherwise, `false`. + /// + /// - Complexity: O(*max*), where *max* is the largest item in either input. + public func isDisjoint(with other: BitSet.Counted) -> Bool { + self.isDisjoint(with: other._bits) + } + + /// Returns a Boolean value that indicates whether the set has no members in + /// common with the given range of integers. + /// + /// let a: BitSet = [1, 2, 3, 4] + /// a.isDisjoint(with: -10 ..< 0) // true + /// + /// - Parameter other: A range of arbitrary integers. + /// + /// - Returns: `true` if `self` has no elements in common with `other`; + /// otherwise, `false`. + /// + /// - Complexity: O(*max*), where *max* is the largest item in `self`. + public func isDisjoint(with other: Range) -> Bool { + _read { $0.isDisjoint(with: other._clampedToUInt()) } + } + + /// Returns a Boolean value that indicates whether the set has no members in + /// common with the given sequence of integers. + /// + /// let a: BitSet = [1, 2, 3, 4] + /// let b: BitSet = [5, 6, -10, 42] + /// a.isDisjoint(with: b) // true + /// + /// - Parameter other: A sequence of arbitrary integers. + /// + /// - Returns: `true` if `self` has no elements in common with `other`; + /// otherwise, `false`. + /// + /// - Complexity: O(*max*) + *k*, where *max* is the largest item in `self`, + /// and *k* is the complexity of iterating over all elements in `other`. + @inlinable + public func isDisjoint(with other: some Sequence) -> Bool { + if let other = _specialize(other, for: BitSet.self) { + return self.isDisjoint(with: other) + } + if let other = _specialize(other, for: BitSet.Counted.self) { + return self.isDisjoint(with: other) + } + if let other = _specialize(other, for: Range.self) { + return self.isDisjoint(with: other) + } + for value in other { + guard !contains(value) else { return false } + } + return true + } +} diff --git a/Sources/BitCollections/BitSet/BitSet+SetAlgebra isEqualSet.swift b/Sources/BitCollections/BitSet/BitSet+SetAlgebra isEqualSet.swift new file mode 100644 index 000000000..712bef694 --- /dev/null +++ b/Sources/BitCollections/BitSet/BitSet+SetAlgebra isEqualSet.swift @@ -0,0 +1,108 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +// FIXME: These are non-standard extensions generalizing ==. +extension BitSet { + /// Returns a Boolean value indicating whether two bit sets are equal. Two + /// bit sets are considered equal if they contain the same elements. + /// + /// - Complexity: O(*max*), where *max* is value of the largest member of + /// either set. + public func isEqualSet(to other: Self) -> Bool { + self._storage == other._storage + } + + /// Returns a Boolean value indicating whether a bit set is equal to a counted + /// bit set, i.e., whether they contain the same values. + /// + /// - Complexity: O(*max*), where *max* is value of the largest member of + /// either set. + public func isEqualSet(to other: BitSet.Counted) -> Bool { + self.isEqualSet(to: other._bits) + } + + /// Returns a Boolean value indicating whether a bit set is equal to a range + /// of integers, i.e., whether they contain the same values. + /// + /// - Complexity: O(min(*max*, `other.upperBound`), where *max* is the largest + /// member of `self`. + public func isEqualSet(to other: Range) -> Bool { + guard let other = other._toUInt() else { return false } + return _read { $0.isEqualSet(to: other) } + } + + /// Returns a Boolean value indicating whether this bit set contains the same + /// elements as the given `other` sequence. + /// + /// Duplicate items in `other` do not prevent it from comparing equal to + /// `self`. + /// + /// let bits: BitSet = [0, 1, 5, 6] + /// let other = [5, 5, 0, 1, 1, 6, 5, 0, 1, 6, 6, 5] + /// + /// bits.isEqualSet(to: other) // true + /// + /// - Complexity: O(*n*), where *n* is the number of items in `other`. + public func isEqualSet(to other: some Sequence) -> Bool { + if let other = _specialize(other, for: BitSet.self) { + return isEqualSet(to: other) + } + if let other = _specialize(other, for: BitSet.Counted.self) { + return isEqualSet(to: other) + } + if let other = _specialize(other, for: Range.self) { + return isEqualSet(to: other) + } + + if self.isEmpty { + return other.allSatisfy { _ in false } + } + + if other is _UniqueCollection { + // We don't need to create a temporary set. + guard other.underestimatedCount <= self.count else { return false } + var seen = 0 + for item in other { + guard let item = UInt(exactly: item) else { return false } + guard self._contains(item) else { return false} + seen &+= 1 + } + precondition( + seen <= self.count, + // Otherwise other.underestimatedCount != other.count + "Invalid Collection '\(type(of: other))' (bad underestimatedCount)") + return seen == self.count + } + + var seen: BitSet? = BitSet(reservingCapacity: self.max()!) + var it = other.makeIterator() + while let item = it.next() { + guard let item = UInt(exactly: item) else { return false } + guard self._contains(item) else { return false} + seen!._insert(item) // Ignore dupes + if seen!.count == self.count { + // We've seen them all. Stop further accounting. + seen = nil + break + } + } + guard seen == nil else { return false } + while let item = it.next() { + guard let item = UInt(exactly: item) else { return false } + guard self._contains(item) else { return false} + } + return true + } +} diff --git a/Sources/BitCollections/BitSet/BitSet+SetAlgebra isStrictSubset.swift b/Sources/BitCollections/BitSet/BitSet+SetAlgebra isStrictSubset.swift new file mode 100644 index 000000000..1acf2509b --- /dev/null +++ b/Sources/BitCollections/BitSet/BitSet+SetAlgebra isStrictSubset.swift @@ -0,0 +1,159 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension BitSet { + /// Returns a Boolean value that indicates whether this bit set is a strict + /// subset of the given set. + /// + /// Set *A* is a strict subset of another set *B* if every member of *A* is + /// also a member of *B* and *B* contains at least one element that is not a + /// member of *A*. + /// + /// let a: BitSet = [1, 2, 3, 4] + /// let b: BitSet = [1, 2, 4] + /// let c: BitSet = [0, 1] + /// a.isStrictSubset(of: a) // false + /// b.isStrictSubset(of: a) // true + /// c.isStrictSubset(of: a) // false + /// + /// - Parameter other: Another bit set. + /// + /// - Returns: `true` if the set is a strict subset of `other`; + /// otherwise, `false`. + /// + /// - Complexity: O(*max*), where *max* is the largest item in `self`. + public func isStrictSubset(of other: Self) -> Bool { + self._read { first in + other._read { second in + let w1 = first._words + let w2 = second._words + if w1.count > w2.count { + return false + } + var strict = w1.count < w2.count + for i in 0 ..< w1.count { + if !w1[i].subtracting(w2[i]).isEmpty { + return false + } + strict = strict || w1[i] != w2[i] + } + return strict + } + } + } + + /// Returns a Boolean value that indicates whether this bit set is a strict + /// subset of the given set. + /// + /// Set *A* is a strict subset of another set *B* if every member of *A* is + /// also a member of *B* and *B* contains at least one element that is not a + /// member of *A*. + /// + /// - Parameter other: A counted bit set. + /// + /// - Returns: `true` if the set is a strict subset of `other`; + /// otherwise, `false`. + /// + /// - Complexity: O(*max*), where *max* is the largest item in `self`. + public func isStrictSubset(of other: BitSet.Counted) -> Bool { + isStrictSubset(of: other._bits) + } + + /// Returns a Boolean value that indicates whether this set is a strict + /// subset of the given set. + /// + /// Set *A* is a strict subset of another set *B* if every member of *A* is + /// also a member of *B* and *B* contains at least one element that is not a + /// member of *A*. + /// + /// let b: BitSet = [0, 1, 2] + /// let c: BitSet = [2, 3, 4] + /// b.isStrictSubset(of: -10 ..< 4) // true + /// c.isStrictSubset(of: -10 ..< 4) // false + /// + /// - Parameter other: An arbitrary range of integers. + /// + /// - Returns: `true` if the set is a strict subset of `other`; + /// otherwise, `false`. + /// + /// - Complexity: O(*max*), where *max* is the largest item in `self`. + public func isStrictSubset(of other: Range) -> Bool { + isSubset(of: other) && !isSuperset(of: other) + } + + /// Returns a Boolean value that indicates whether this bit set is a strict + /// subset of the values in a given sequence of integers. + /// + /// Set *A* is a strict subset of another set *B* if every member of *A* is + /// also a member of *B* and *B* contains at least one element that is not a + /// member of *A*. + /// + /// let a = [1, 2, 3, 4, -10] + /// let b: BitSet = [1, 2, 4] + /// let c: BitSet = [0, 1] + /// b.isStrictSubset(of: a) // true + /// c.isStrictSubset(of: a) // false + /// + /// - Parameter other: A sequence of arbitrary integers. + /// + /// - Returns: `true` if the set is a strict subset of `other`; + /// otherwise, `false`. + /// + /// - Complexity: O(*max*) + *k*, where *max* is the largest item in `self`, + /// and *k* is the complexity of iterating over all elements in `other`. + @inlinable + public func isStrictSubset(of other: some Sequence) -> Bool { + if let other = _specialize(other, for: BitSet.self) { + return isStrictSubset(of: other) + } + if let other = _specialize(other, for: BitSet.Counted.self) { + return isStrictSubset(of: other) + } + if let other = _specialize(other, for: Range.self) { + return isStrictSubset(of: other) + } + + if isEmpty { + var it = other.makeIterator() + return it.next() != nil + } + + let selfCount = self.count + return _UnsafeHandle.withTemporaryBitSet( + wordCount: _storage.count + ) { seen in + var strict = false + var it = other.makeIterator() + var c = 0 + while let i = it.next() { + guard self.contains(i) else { + strict = true + continue + } + if seen.insert(UInt(i)) { + c &+= 1 + if c == selfCount { + while !strict, let i = it.next() { + strict = !self.contains(i) + } + return strict + } + } + } + assert(c < selfCount) + return false + } + } +} diff --git a/Sources/BitCollections/BitSet/BitSet+SetAlgebra isStrictSuperset.swift b/Sources/BitCollections/BitSet/BitSet+SetAlgebra isStrictSuperset.swift new file mode 100644 index 000000000..4226f604a --- /dev/null +++ b/Sources/BitCollections/BitSet/BitSet+SetAlgebra isStrictSuperset.swift @@ -0,0 +1,123 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension BitSet { + /// Returns a Boolean value that indicates whether this set is a strict + /// superset of another set. + /// + /// Set *A* is a strict superset of another set *B* if every member of *B* is + /// also a member of *A* and *A* contains at least one element that is *not* + /// a member of *B*. + /// + /// let a: BitSet = [1, 2, 3, 4] + /// let b: BitSet = [1, 2, 4] + /// let c: BitSet = [0, 1] + /// a.isStrictSuperset(of: a) // false + /// a.isStrictSuperset(of: b) // true + /// a.isStrictSuperset(of: c) // false + /// + /// - Parameter other: Another bit set. + /// + /// - Returns: `true` if the set is a superset of `other`; otherwise, `false`. + /// + /// - Complexity: O(*max*), where *max* is the largest item in `other`. + public func isStrictSuperset(of other: Self) -> Bool { + other.isStrictSubset(of: self) + } + + /// Returns a Boolean value that indicates whether this set is a strict + /// superset of another set. + /// + /// Set *A* is a strict superset of another set *B* if every member of *B* is + /// also a member of *A* and *A* contains at least one element that is *not* + /// a member of *B*. + /// + /// - Parameter other: A counted bit set. + /// + /// - Returns: `true` if the set is a superset of `other`; otherwise, `false`. + /// + /// - Complexity: O(*max*), where *max* is the largest item in `other`. + public func isStrictSuperset(of other: BitSet.Counted) -> Bool { + other._bits.isStrictSubset(of: self) + } + + /// Returns a Boolean value that indicates whether this set is a superset of + /// a given range of integers. + /// + /// Set *A* is a superset of another set *B* if every member of *B* is also a + /// member of *A*. + /// + /// let a: BitSet = [0, 1, 2, 3, 4, 10] + /// a.isSuperset(of: 0 ..< 4) // true + /// a.isSuperset(of: -10 ..< 4) // false + /// + /// - Parameter other: An arbitrary range of integers. + /// + /// - Returns: `true` if the set is a subset of `other`; otherwise, `false`. + /// + /// - Complexity: O(`range.count`) + public func isStrictSuperset(of other: Range) -> Bool { + if other.isEmpty { return !isEmpty } + if isEmpty { return false } + if self[members: ..) -> Bool { + guard !isEmpty else { return false } + if let other = _specialize(other, for: BitSet.self) { + return isStrictSuperset(of: other) + } + if let other = _specialize(other, for: BitSet.Counted.self) { + return isStrictSuperset(of: other) + } + if let other = _specialize(other, for: Range.self) { + return isStrictSuperset(of: other) + } + return _UnsafeHandle.withTemporaryBitSet( + wordCount: _storage.count + ) { seen in + for i in other { + guard contains(i) else { return false } + seen.insert(UInt(i)) + } + return !_storage.elementsEqual(seen._words) + } + } +} diff --git a/Sources/BitCollections/BitSet/BitSet+SetAlgebra isSubset.swift b/Sources/BitCollections/BitSet/BitSet+SetAlgebra isSubset.swift new file mode 100644 index 000000000..da13d34fa --- /dev/null +++ b/Sources/BitCollections/BitSet/BitSet+SetAlgebra isSubset.swift @@ -0,0 +1,137 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension BitSet { + /// Returns a Boolean value that indicates whether this set is a subset of + /// the given set. + /// + /// Set *A* is a subset of another set *B* if every member of *A* is also a + /// member of *B*. + /// + /// let a: BitSet = [1, 2, 3, 4] + /// let b: BitSet = [1, 2, 4] + /// let c: BitSet = [0, 1] + /// a.isSubset(of: a) // true + /// b.isSubset(of: a) // true + /// c.isSubset(of: a) // false + /// + /// - Parameter other: Another bit set. + /// + /// - Returns: `true` if the set is a subset of `other`; otherwise, `false`. + /// + /// - Complexity: O(*max*), where *max* is the largest item in `self`. + public func isSubset(of other: Self) -> Bool { + self._read { first in + other._read { second in + let w1 = first._words + let w2 = second._words + if w1.count > w2.count { + return false + } + for i in 0 ..< w1.count { + if !w1[i].subtracting(w2[i]).isEmpty { + return false + } + } + return true + } + } + } + + /// Returns a Boolean value that indicates whether this set is a subset of + /// the given set. + /// + /// Set *A* is a subset of another set *B* if every member of *A* is also a + /// member of *B*. + /// + /// - Parameter other: A counted bit set. + /// + /// - Returns: `true` if the set is a subset of `other`; otherwise, `false`. + /// + /// - Complexity: O(*max*), where *max* is the largest item in `self`. + public func isSubset(of other: BitSet.Counted) -> Bool { + self.isSubset(of: other._bits) + } + + /// Returns a Boolean value that indicates whether this set is a subset of + /// the given range of integers. + /// + /// Set *A* is a subset of another set *B* if every member of *A* is also a + /// member of *B*. + /// + /// let b: BitSet = [0, 1, 2] + /// let c: BitSet = [2, 3, 4] + /// b.isSubset(of: -10 ..< 4) // true + /// c.isSubset(of: -10 ..< 4) // false + /// + /// - Parameter other: An arbitrary range of integers. + /// + /// - Returns: `true` if the set is a subset of `other`; otherwise, `false`. + /// + /// - Complexity: O(*max*), where *max* is the largest item in `self`. + public func isSubset(of other: Range) -> Bool { + _read { $0.isSubset(of: other._clampedToUInt()) } + } + + /// Returns a Boolean value that indicates whether this set is a subset of + /// the values in a given sequence of integers. + /// + /// Set *A* is a subset of another set *B* if every member of *A* is also a + /// member of *B*. + /// + /// let a = [1, 2, 3, 4, -10] + /// let b: BitSet = [1, 2, 4] + /// let c: BitSet = [0, 1] + /// b.isSubset(of: a) // true + /// c.isSubset(of: a) // false + /// + /// - Parameter other: A sequence of arbitrary integers. + /// + /// - Returns: `true` if the set is a subset of `other`; otherwise, `false`. + /// + /// - Complexity: O(*max*) + *k*, where *max* is the largest item in `self`, + /// and *k* is the complexity of iterating over all elements in `other`. + @inlinable + public func isSubset(of other: some Sequence) -> Bool { + if let other = _specialize(other, for: BitSet.self) { + return self.isSubset(of: other) + } + if let other = _specialize(other, for: BitSet.Counted.self) { + return self.isSubset(of: other) + } + if let other = _specialize(other, for: Range.self) { + return self.isSubset(of: other) + } + + var it = self.makeIterator() + guard let first = it.next() else { return true } + if let match = other._customContainsEquatableElement(first) { + // Fast path: the sequence has fast containment checks. + guard match else { return false } + while let item = it.next() { + guard other.contains(item) else { return false } + } + return true + } + + var t = self + for i in other { + guard let i = UInt(exactly: i) else { continue } + if t._remove(i), t.isEmpty { return true } + } + assert(!t.isEmpty) + return false + } +} diff --git a/Sources/BitCollections/BitSet/BitSet+SetAlgebra isSuperset.swift b/Sources/BitCollections/BitSet/BitSet+SetAlgebra isSuperset.swift new file mode 100644 index 000000000..91db1c63d --- /dev/null +++ b/Sources/BitCollections/BitSet/BitSet+SetAlgebra isSuperset.swift @@ -0,0 +1,111 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension BitSet { + /// Returns a Boolean value that indicates whether this set is a superset of + /// the given set. + /// + /// Set *A* is a superset of another set *B* if every member of *B* is also a + /// member of *A*. + /// + /// let a: BitSet = [1, 2, 3, 4] + /// let b: BitSet = [1, 2, 4] + /// let c: BitSet = [0, 1] + /// a.isSuperset(of: a) // true + /// a.isSuperset(of: b) // true + /// a.isSuperset(of: c) // false + /// + /// - Parameter other: Another bit set. + /// + /// - Returns: `true` if the set is a superset of `other`; otherwise, `false`. + /// + /// - Complexity: O(*max*), where *max* is the largest item in `other`. + public func isSuperset(of other: Self) -> Bool { + other.isSubset(of: self) + } + + /// Returns a Boolean value that indicates whether this set is a superset of + /// the given set. + /// + /// Set *A* is a superset of another set *B* if every member of *B* is also a + /// member of *A*. + /// + /// - Parameter other: A counted bit set. + /// + /// - Returns: `true` if the set is a superset of `other`; otherwise, `false`. + /// + /// - Complexity: O(*max*), where *max* is the largest item in `other`. + public func isSuperset(of other: BitSet.Counted) -> Bool { + isSuperset(of: other._bits) + } + + /// Returns a Boolean value that indicates whether this set is a superset of + /// a given range of integers. + /// + /// Set *A* is a superset of another set *B* if every member of *B* is also a + /// member of *A*. + /// + /// let a: BitSet = [0, 1, 2, 3, 4, 10] + /// a.isSuperset(of: 0 ..< 4) // true + /// a.isSuperset(of: -10 ..< 4) // false + /// + /// - Parameter other: An arbitrary range of integers. + /// + /// - Returns: `true` if the set is a subset of `other`; otherwise, `false`. + /// + /// - Complexity: O(`range.count`) + public func isSuperset(of other: Range) -> Bool { + if other.isEmpty { return true } + guard let r = other._toUInt() else { return false } + return _read { $0.isSuperset(of: r) } + } + + /// Returns a Boolean value that indicates whether this set is a superset of + /// the values in a given sequence of integers. + /// + /// Set *A* is a superset of another set *B* if every member of *B* is also a + /// member of *A*. + /// + /// let a = [1, 2, 3] + /// let b: BitSet = [0, 1, 2, 3, 4] + /// let c: BitSet = [0, 1, 2] + /// b.isSuperset(of: a) // true + /// c.isSuperset(of: a) // false + /// + /// - Parameter other: A sequence of arbitrary integers, some of whose members + /// may appear more than once. (Duplicate items are ignored.) + /// + /// - Returns: `true` if the set is a subset of `other`; otherwise, `false`. + /// + /// - Complexity: The same as the complexity of iterating over all elements + /// in `other`. + @inlinable + public func isSuperset(of other: some Sequence) -> Bool { + if let other = _specialize(other, for: BitSet.self) { + return self.isSuperset(of: other) + } + if let other = _specialize(other, for: BitSet.Counted.self) { + return self.isSuperset(of: other) + } + if let other = _specialize(other, for: Range.self) { + return self.isSuperset(of: other) + } + for i in other { + guard let i = UInt(exactly: i) else { return false } + if !_contains(i) { return false } + } + return true + } +} diff --git a/Sources/BitCollections/BitSet/BitSet+SetAlgebra subtract.swift b/Sources/BitCollections/BitSet/BitSet+SetAlgebra subtract.swift new file mode 100644 index 000000000..0243ab31a --- /dev/null +++ b/Sources/BitCollections/BitSet/BitSet+SetAlgebra subtract.swift @@ -0,0 +1,119 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension BitSet { + /// Removes the elements of the given bit set from this set. + /// + /// var set: BitSet = [1, 2, 3, 4] + /// let other: BitSet = [0, 2, 4, 6] + /// set.subtract(other) + /// // set is now [1, 3] + /// + /// - Parameter other: Another bit set. + /// + /// - Complexity: O(*max*), where *max* is the largest item in either input. + public mutating func subtract(_ other: Self) { + _updateThenShrink { target, shrink in + other._read { source in + target.combineSharedPrefix( + with: source, + using: { $0.subtract($1) } + ) + } + } + } + + /// Removes the elements of the given bit set from this set. + /// + /// var set: BitSet = [1, 2, 3, 4] + /// let other: BitSet.Counted = [0, 2, 4, 6] + /// set.subtract(other) + /// // set is now [1, 3] + /// + /// - Parameter other: Another bit set. + /// + /// - Complexity: O(*max*), where *max* is the largest item in either input. + public mutating func subtract(_ other: BitSet.Counted) { + subtract(other._bits) + } + + /// Removes the elements of the given range of integers from this set. + /// + /// var set: BitSet = [1, 2, 3, 4] + /// set.subtract(-10 ..< 3) + /// // set is now [3, 4] + /// + /// - Parameter other: A range of arbitrary integers. + /// + /// - Returns: A new set. + /// + /// - Complexity: O(*max*), where *max* is the largest item in self. + public mutating func subtract(_ other: Range) { + _subtract(other._clampedToUInt()) + } + + @usableFromInline + internal mutating func _subtract(_ other: Range) { + guard !other.isEmpty else { return } + _updateThenShrink { handle, shrink in + handle.subtract(other) + } + } + + /// Removes the elements of the given sequence of integers from this set. + /// + /// var set: BitSet = [1, 2, 3, 4] + /// let other = [6, 4, 2, 0, -2, -4] + /// set.subtract(other) + /// // set is now [1, 3] + /// + /// - Parameter other: A sequence of arbitrary integers. + /// + /// - Complexity: O(*max*) + *k*, where *max* is the largest item in `self`, + /// and *k* is the complexity of iterating over all elements in `other`. + @inlinable + public mutating func subtract(_ other: some Sequence) { + if let other = _specialize(other, for: BitSet.self) { + self.subtract(other) + return + } + if let other = _specialize(other, for: BitSet.Counted.self) { + self.subtract(other) + return + } + if let other = _specialize(other, for: Range.self) { + self.subtract(other) + return + } + var it = other.makeIterator() + _subtract { + while let value = it.next() { + if let value = UInt(exactly: value) { + return value + } + } + return nil + } + } + + @usableFromInline + internal mutating func _subtract(_ next: () -> UInt?) { + _updateThenShrink { handle, shrink in + while let value = next() { + handle.remove(value) + } + } + } +} diff --git a/Sources/BitCollections/BitSet/BitSet+SetAlgebra subtracting.swift b/Sources/BitCollections/BitSet/BitSet+SetAlgebra subtracting.swift new file mode 100644 index 000000000..9397d9103 --- /dev/null +++ b/Sources/BitCollections/BitSet/BitSet+SetAlgebra subtracting.swift @@ -0,0 +1,92 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension BitSet { + /// Returns a new set containing the elements of this set that do not occur + /// in the given other set. + /// + /// let set: BitSet = [1, 2, 3, 4] + /// let other: BitSet = [0, 2, 4, 6] + /// set.subtracting(other) // [1, 3] + /// + /// - Parameter other: Another bit set. + /// + /// - Returns: A new set. + /// + /// - Complexity: O(*max*), where *max* is the largest item in either input. + public func subtracting(_ other: Self) -> Self { + self._read { first in + other._read { second in + Self( + _combining: (first, second), + includingTail: true, + using: { $0.subtracting($1) }) + } + } + } + + /// Returns a new set containing the elements of this set that do not occur + /// in the given other set. + /// + /// let set: BitSet = [1, 2, 3, 4] + /// let other: BitSet.Counted = [0, 2, 4, 6] + /// set.subtracting(other) // [1, 3] + /// + /// - Parameter other: Another bit set. + /// + /// - Returns: A new set. + /// + /// - Complexity: O(*max*), where *max* is the largest item in either input. + public func subtracting(_ other: BitSet.Counted) -> Self { + subtracting(other._bits) + } + + /// Returns a new set containing the elements of this set that do not occur + /// in the given range of integers. + /// + /// let set: BitSet = [1, 2, 3, 4] + /// set.subtracting(-10 ..< 3) // [3, 4] + /// + /// - Parameter other: A range of arbitrary integers. + /// + /// - Returns: A new set. + /// + /// - Complexity: O(*max*), where *max* is the largest item in self. + public func subtracting(_ other: Range) -> Self { + var result = self + result.subtract(other) + return result + } + + /// Returns a new set containing the elements of this set that do not occur + /// in the given sequence of integers. + /// + /// let set: BitSet = [1, 2, 3, 4] + /// let other = [6, 4, 2, 0, -2, -4] + /// set.subtracting(other) // [1, 3] + /// + /// - Parameter other: A sequence of arbitrary integers. + /// + /// - Returns: A new set. + /// + /// - Complexity: O(*max*) + *k*, where *max* is the largest item in `self`, + /// and *k* is the complexity of iterating over all elements in `other`. + @inlinable + public func subtracting(_ other: __owned some Sequence) -> Self { + var result = self + result.subtract(other) + return result + } +} diff --git a/Sources/BitCollections/BitSet/BitSet+SetAlgebra symmetricDifference.swift b/Sources/BitCollections/BitSet/BitSet+SetAlgebra symmetricDifference.swift new file mode 100644 index 000000000..43b1e9d76 --- /dev/null +++ b/Sources/BitCollections/BitSet/BitSet+SetAlgebra symmetricDifference.swift @@ -0,0 +1,95 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension BitSet { + /// Returns a new bit set with the elements that are either in this set or in + /// `other`, but not in both. + /// + /// let set: BitSet = [1, 2, 3, 4] + /// let other: BitSet = [6, 4, 2, 0] + /// set.symmetricDifference(other) // [0, 1, 3, 6] + /// + /// - Parameter other: Another set. + /// + /// - Returns: A new set. + /// + /// - Complexity: O(*max*), where *max* is the largest item in either set. + public func symmetricDifference(_ other: Self) -> Self { + self._read { first in + other._read { second in + Self( + _combining: (first, second), + includingTail: true, + using: { $0.symmetricDifference($1) }) + } + } + } + + /// Returns a new bit set with the elements that are either in this set or in + /// `other`, but not in both. + /// + /// let set: BitSet = [1, 2, 3, 4] + /// let other: BitSet.Counted = [6, 4, 2, 0] + /// set.symmetricDifference(other) // [0, 1, 3, 6] + /// + /// - Parameter other: Another set. + /// + /// - Returns: A new set. + /// + /// - Complexity: O(*max*), where *max* is the largest item in either set. + public func symmetricDifference(_ other: Counted) -> Self { + symmetricDifference(other._bits) + } + + /// Returns a new bit set with the elements that are either in this set or in + /// `other`, but not in both. + /// + /// let set: BitSet = [1, 2, 3, 4] + /// set.formSymmetricDifference(3 ..< 7) // [1, 2, 5, 6] + /// + /// - Parameter other: A range of nonnegative integers. + /// + /// - Returns: A new set. + /// + /// - Complexity: O(*max*), where *max* is the largest item in either input. + public func symmetricDifference(_ other: Range) -> Self { + var result = self + result.formSymmetricDifference(other) + return result + } + + /// Returns a new bit set with the elements that are either in this set or in + /// `other`, but not in both. + /// + /// let set: BitSet = [1, 2, 3, 4] + /// let other = [6, 4, 2, 0, 2, 0] + /// set.formSymmetricDifference(other) // [0, 1, 3, 6] + /// + /// - Parameter other: A sequence of nonnegative integers. + /// + /// - Complexity: O(*max*) + *k*, where *max* is the largest item in either + /// input, and *k* is the complexity of iterating over all elements in + /// `other`. + @inlinable + public func symmetricDifference( + _ other: __owned some Sequence + ) -> Self { + if let other = _specialize(other, for: Range.self) { + return symmetricDifference(other) + } + // Note: BitSet & BitSet.Counted are handled in the BitSet initializer below + return symmetricDifference(BitSet(other)) + } +} diff --git a/Sources/BitCollections/BitSet/BitSet+SetAlgebra union.swift b/Sources/BitCollections/BitSet/BitSet+SetAlgebra union.swift new file mode 100644 index 000000000..58e25dbb6 --- /dev/null +++ b/Sources/BitCollections/BitSet/BitSet+SetAlgebra union.swift @@ -0,0 +1,92 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension BitSet { + /// Returns a new set with the elements of both this and the given set. + /// + /// let set: BitSet = [1, 2, 3, 4] + /// let other: BitSet = [0, 2, 4, 6] + /// set.union(other) // [0, 1, 2, 3, 4, 6] + /// + /// - Parameter other: The set of elements to insert. + /// + /// - Complexity: O(*max*), where *max* is the largest item in either input. + public func union(_ other: Self) -> Self { + self._read { first in + other._read { second in + Self( + _combining: (first, second), + includingTail: true, + using: { $0.union($1) }) + } + } + } + + /// Returns a new set with the elements of both this and the given set. + /// + /// let set: BitSet = [1, 2, 3, 4] + /// let other: BitSet = [0, 2, 4, 6] + /// set.union(other) // [0, 1, 2, 3, 4, 6] + /// + /// - Parameter other: The set of elements to insert. + /// + /// - Complexity: O(*max*), where *max* is the largest item in either input. + public func union(_ other: BitSet.Counted) -> Self { + union(other._bits) + } + + /// Returns a new set with the elements of both this set and the given + /// range of integers. + /// + /// let set: BitSet = [1, 2, 3, 4] + /// set.union(3 ..< 7) // [1, 2, 3, 4, 5, 6] + /// + /// - Parameter other: A range of nonnegative integers. + /// + /// - Complexity: O(*max*), where *max* is the largest item in either input. + public func union(_ other: Range) -> Self { + var result = self + result.formUnion(other) + return result + } + + /// Returns a new set with the elements of both this set and the given + /// sequence of integers. + /// + /// let set: BitSet = [1, 2, 3, 4] + /// let other = [6, 4, 2, 0, 2, 0] + /// set.union(other) // [0, 1, 2, 3, 4, 6] + /// + /// - Parameter other: A sequence of nonnegative integers. + /// + /// - Complexity: O(*max*) + *k*, where *max* is the largest item in either + /// input, and *k* is the complexity of iterating over all elements in + /// `other`. + @inlinable + public func union(_ other: __owned some Sequence) -> Self { + if let other = _specialize(other, for: BitSet.self) { + return union(other) + } + if let other = _specialize(other, for: BitSet.Counted.self) { + return union(other) + } + if let other = _specialize(other, for: Range.self) { + return union(other) + } + var result = self + result.formUnion(other) + return result + } +} diff --git a/Sources/BitCollections/BitSet/BitSet+Sorted Collection APIs.swift b/Sources/BitCollections/BitSet/BitSet+Sorted Collection APIs.swift new file mode 100644 index 000000000..513c8b253 --- /dev/null +++ b/Sources/BitCollections/BitSet/BitSet+Sorted Collection APIs.swift @@ -0,0 +1,49 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension BitSet: _SortedCollection { + /// Returns the current set (already sorted). + /// + /// - Complexity: O(1) + public func sorted() -> BitSet { self } + + /// Returns the minimum element in this set. + /// + /// Bit sets are sorted, so the minimum element is always at the first + /// position in the set. + /// + /// - Returns: The bit set's minimum element. If the sequence has no + /// elements, returns `nil`. + /// + /// - Complexity: O(1) + @warn_unqualified_access + public func min() -> Element? { + first + } + + /// Returns the maximum element in this set. + /// + /// Bit sets are sorted, so the maximum element is always at the last + /// position in the set. + /// + /// - Returns: The bit set's maximum element. If the sequence has no + /// elements, returns `nil`. + /// + /// - Complexity: O(1) + @warn_unqualified_access + public func max() -> Element? { + last + } +} diff --git a/Sources/BitCollections/BitSet/BitSet.Counted.swift b/Sources/BitCollections/BitSet/BitSet.Counted.swift new file mode 100644 index 000000000..d8435c6bb --- /dev/null +++ b/Sources/BitCollections/BitSet/BitSet.Counted.swift @@ -0,0 +1,1350 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension BitSet { + public struct Counted { + @usableFromInline + internal var _bits: BitSet + @usableFromInline + internal var _count: Int + + internal init(_bits: BitSet, count: Int) { + self._bits = _bits + self._count = count + _checkInvariants() + } + } +} + +extension BitSet.Counted: Sendable {} + +extension BitSet.Counted { +#if COLLECTIONS_INTERNAL_CHECKS + @inline(never) + @_effects(releasenone) + public func _checkInvariants() { + _bits._checkInvariants() + precondition(_count == _bits.count) + } +#else + @inline(__always) @inlinable + public func _checkInvariants() {} +#endif // COLLECTIONS_INTERNAL_CHECKS +} + +extension BitSet.Counted: _UniqueCollection {} + +extension BitSet.Counted { + public init() { + self.init(BitSet()) + } + + @inlinable + public init(words: some Sequence) { + self.init(BitSet(words: words)) + } + + @inlinable + public init(bitPattern x: some BinaryInteger) { + self.init(words: x.words) + } + + public init(_ array: BitArray) { + self.init(BitSet(array)) + } + + @inlinable + public init(_ elements: __owned some Sequence) { + self.init(BitSet(elements)) + } + + public init(_ range: Range) { + self.init(BitSet(range)) + } +} + +extension BitSet.Counted { + public init(_ bits: BitSet) { + self.init(_bits: bits, count: bits.count) + } + + public var uncounted: BitSet { + get { _bits } + @inline(__always) // https://github.com/apple/swift-collections/issues/164 + _modify { + defer { + _count = _bits.count + } + yield &_bits + } + } +} + +extension BitSet { + public init(_ bits: BitSet.Counted) { + self = bits._bits + } + + public var counted: BitSet.Counted { + get { + BitSet.Counted(_bits: self, count: self.count) + } + @inline(__always) // https://github.com/apple/swift-collections/issues/164 + _modify { + var value = BitSet.Counted(self) + self = [] + defer { self = value._bits } + yield &value + } + } +} + +extension BitSet.Counted: Sequence { + public typealias Iterator = BitSet.Iterator + + public var underestimatedCount: Int { + _count + } + + public func makeIterator() -> BitSet.Iterator { + _bits.makeIterator() + } + + public func _customContainsEquatableElement( + _ element: Int + ) -> Bool? { + _bits._customContainsEquatableElement(element) + } +} + +extension BitSet.Counted: BidirectionalCollection { + public typealias Index = BitSet.Index + + public var isEmpty: Bool { _count == 0 } + + public var count: Int { _count } + + public var startIndex: Index { _bits.startIndex } + public var endIndex: Index { _bits.endIndex } + + public subscript(position: Index) -> Int { + _bits[position] + } + + public func index(after index: Index) -> Index { + _bits.index(after: index) + } + + public func index(before index: Index) -> Index { + _bits.index(before: index) + } + + public func distance(from start: Index, to end: Index) -> Int { + _bits.distance(from: start, to: end) + } + + public func index(_ index: Index, offsetBy distance: Int) -> Index { + _bits.index(index, offsetBy: distance) + } + + public func index( + _ i: Index, offsetBy distance: Int, limitedBy limit: Index + ) -> Index? { + _bits.index(i, offsetBy: distance, limitedBy: limit) + } + + public func _customIndexOfEquatableElement(_ element: Int) -> Index?? { + _bits._customIndexOfEquatableElement(element) + } + + public func _customLastIndexOfEquatableElement(_ element: Int) -> Index?? { + _bits._customLastIndexOfEquatableElement(element) + } +} + + +extension BitSet.Counted: Codable { + public func encode(to encoder: Encoder) throws { + try _bits.encode(to: encoder) + } + + public init(from decoder: Decoder) throws { + self.init(try BitSet(from: decoder)) + } +} + +extension BitSet.Counted: CustomStringConvertible { + // A textual representation of this instance. + public var description: String { + _bits.description + } +} + +extension BitSet.Counted: CustomDebugStringConvertible { + /// A textual representation of this instance, suitable for debugging. + public var debugDescription: String { + description + } +} + +extension BitSet.Counted: CustomReflectable { + public var customMirror: Mirror { + _bits.customMirror + } +} + +extension BitSet.Counted: Equatable { + public static func ==(left: Self, right: Self) -> Bool { + guard left._count == right._count else { return false } + return left._bits == right._bits + } +} + +extension BitSet.Counted: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(_bits) + } +} + +extension BitSet.Counted: ExpressibleByArrayLiteral { + @inlinable + public init(arrayLiteral elements: Int...) { + let bits = BitSet(elements) + self.init(bits) + } +} + +extension BitSet.Counted { // Extras + public init(reservingCapacity maximumValue: Int) { + self.init(_bits: BitSet(reservingCapacity: maximumValue), count: 0) + } + + public mutating func reserveCapacity(_ maximumValue: Int) { + _bits.reserveCapacity(maximumValue) + } + + public subscript(member member: Int) -> Bool { + get { contains(member) } + set { + if newValue { + insert(member) + } else { + remove(member) + } + } + } + + public subscript(members bounds: Range) -> Slice { + _bits[members: bounds] + } + + public subscript(members bounds: some RangeExpression) -> Slice { + _bits[members: bounds] + } + + public mutating func remove(at index: Index) -> Int { + defer { self._count &-= 1 } + return _bits.remove(at: index) + } + + public func filter( + _ isIncluded: (Element) throws -> Bool + ) rethrows -> Self { + BitSet.Counted(try _bits.filter(isIncluded)) + } +} + +extension BitSet.Counted: _SortedCollection { + /// Returns the current set (already sorted). + /// + /// - Complexity: O(1) + public func sorted() -> BitSet.Counted { self } + + /// Returns the minimum element in this set. + /// + /// Bit sets are sorted, so the minimum element is always at the first + /// position in the set. + /// + /// - Returns: The bit set's minimum element. If the sequence has no + /// elements, returns `nil`. + /// + /// - Complexity: O(1) + @warn_unqualified_access + public func min() -> Element? { + first + } + + /// Returns the maximum element in this set. + /// + /// Bit sets are sorted, so the maximum element is always at the last + /// position in the set. + /// + /// - Returns: The bit set's maximum element. If the sequence has no + /// elements, returns `nil`. + /// + /// - Complexity: O(1) + @warn_unqualified_access + public func max() -> Element? { + last + } +} + +extension BitSet.Counted { + public static func random(upTo limit: Int) -> BitSet.Counted { + BitSet.Counted(BitSet.random(upTo: limit)) + } + + public static func random( + upTo limit: Int, + using rng: inout some RandomNumberGenerator + ) -> BitSet.Counted { + BitSet.Counted(BitSet.random(upTo: limit, using: &rng)) + } +} + +extension BitSet.Counted: SetAlgebra { + public func contains(_ member: Int) -> Bool { + _bits.contains(member) + } + + @discardableResult + public mutating func insert( + _ newMember: Int + ) -> (inserted: Bool, memberAfterInsert: Int) { + let r = _bits.insert(newMember) + if r.inserted { _count += 1 } + return r + } + + @discardableResult + public mutating func update(with newMember: Int) -> Int? { + _bits.update(with: newMember) + } + + @discardableResult + public mutating func remove(_ member: Int) -> Int? { + let old = _bits.remove(member) + if old != nil { _count -= 1 } + return old + } +} + +extension BitSet.Counted { + /// Returns a new set with the elements of both this and the given set. + /// + /// let set: BitSet.Counted = [1, 2, 3, 4] + /// let other: BitSet.Counted = [0, 2, 4, 6] + /// set.union(other) // [0, 1, 2, 3, 4, 6] + /// + /// - Parameter other: The set of elements to insert. + /// + /// - Complexity: O(*max*), where *max* is the largest item in either input. + public func union(_ other: BitSet.Counted) -> BitSet.Counted { + _bits.union(other).counted + } + + /// Returns a new set with the elements of both this and the given set. + /// + /// let set: BitSet.Counted = [1, 2, 3, 4] + /// let other: BitSet = [0, 2, 4, 6] + /// set.union(other) // [0, 1, 2, 3, 4, 6] + /// + /// - Parameter other: The set of elements to insert. + /// + /// - Complexity: O(*max*), where *max* is the largest item in either input. + public func union(_ other: BitSet) -> BitSet.Counted { + _bits.union(other).counted + } + + /// Returns a new set with the elements of both this set and the given + /// range of integers. + /// + /// let set: BitSet.Counted = [1, 2, 3, 4] + /// set.union(3 ..< 7) // [1, 2, 3, 4, 5, 6] + /// + /// - Parameter other: A range of nonnegative integers. + /// + /// - Complexity: O(*max*), where *max* is the largest item in either input. + public func union(_ other: Range) -> BitSet.Counted { + _bits.union(other).counted + } + + /// Returns a new set with the elements of both this set and the given + /// sequence of integers. + /// + /// let set: BitSet.Counted = [1, 2, 3, 4] + /// let other = [6, 4, 2, 0, 2, 0] + /// set.union(other) // [0, 1, 2, 3, 4, 6] + /// + /// - Parameter other: A sequence of nonnegative integers. + /// + /// - Complexity: O(*max*) + *k*, where *max* is the largest item in either + /// input, and *k* is the complexity of iterating over all elements in + /// `other`. + @inlinable + public func union( + _ other: __owned some Sequence + ) -> Self { + _bits.union(other).counted + } +} + +extension BitSet.Counted { + /// Returns a new bit set with the elements that are common to both this set + /// and the given set. + /// + /// let a: BitSet.Counted = [1, 2, 3, 4] + /// let b: BitSet.Counted = [6, 4, 2, 0] + /// let c = a.intersection(b) + /// // c is now [2, 4] + /// + /// - Parameter other: A bit set. + /// + /// - Complexity: O(*max*), where *max* is the largest item in either set. + public func intersection(_ other: BitSet.Counted) -> BitSet.Counted { + _bits.intersection(other).counted + } + + /// Returns a new bit set with the elements that are common to both this set + /// and the given set. + /// + /// let a: BitSet.Counted = [1, 2, 3, 4] + /// let b: BitSet = [6, 4, 2, 0] + /// let c = a.intersection(b) + /// // c is now [2, 4] + /// + /// - Parameter other: A bit set. + /// + /// - Complexity: O(*max*), where *max* is the largest item in either set. + public func intersection(_ other: BitSet) -> BitSet.Counted { + _bits.intersection(other).counted + } + + /// Returns a new bit set with the elements that are common to both this set + /// and the given range of integers. + /// + /// let a: BitSet.Counted = [1, 2, 3, 4] + /// let c = a.intersection(-10 ..< 3) + /// // c is now [3, 4] + /// + /// - Parameter other: A range of integers. + /// + /// - Complexity: O(*max*) + *k*, where *max* is the largest item in `self`, + /// and *k* is the complexity of iterating over all elements in `other`. + public func intersection(_ other: Range) -> BitSet.Counted { + _bits.intersection(other).counted + } + + /// Returns a new bit set with the elements that are common to both this set + /// and the given sequence. + /// + /// let a: BitSet.Counted = [1, 2, 3, 4] + /// let b = [6, 4, 2, 0] + /// let c = a.intersection(b) + /// // c is now [2, 4] + /// + /// - Parameter other: A sequence of integer values. + /// + /// - Complexity: O(*max*) + *k*, where *max* is the largest item in `self`, + /// and *k* is the complexity of iterating over all elements in `other`. + @inlinable + public func intersection( + _ other: __owned some Sequence + ) -> Self { + _bits.intersection(other).counted + } +} + +extension BitSet.Counted { + /// Returns a new bit set with the elements that are either in this set or in + /// `other`, but not in both. + /// + /// let set: BitSet.Counted = [1, 2, 3, 4] + /// let other: BitSet.Counted = [6, 4, 2, 0] + /// set.symmetricDifference(other) // [0, 1, 3, 6] + /// + /// - Parameter other: Another set. + /// + /// - Returns: A new set. + /// + /// - Complexity: O(*max*), where *max* is the largest item in either set. + public func symmetricDifference(_ other: BitSet.Counted) -> BitSet.Counted { + _bits.symmetricDifference(other).counted + } + + /// Returns a new bit set with the elements that are either in this set or in + /// `other`, but not in both. + /// + /// let set: BitSet.Counted = [1, 2, 3, 4] + /// let other: BitSet = [6, 4, 2, 0] + /// set.symmetricDifference(other) // [0, 1, 3, 6] + /// + /// - Parameter other: Another set. + /// + /// - Returns: A new set. + /// + /// - Complexity: O(*max*), where *max* is the largest item in either set. + public func symmetricDifference(_ other: BitSet) -> BitSet.Counted { + _bits.symmetricDifference(other).counted + } + + /// Returns a new bit set with the elements that are either in this set or in + /// `other`, but not in both. + /// + /// let set: BitSet.Counted = [1, 2, 3, 4] + /// set.formSymmetricDifference(3 ..< 7) // [1, 2, 5, 6] + /// + /// - Parameter other: A range of nonnegative integers. + /// + /// - Returns: A new set. + /// + /// - Complexity: O(*max*), where *max* is the largest item in either input. + public func symmetricDifference(_ other: Range) -> BitSet.Counted { + _bits.symmetricDifference(other).counted + } + + /// Returns a new bit set with the elements that are either in this set or in + /// `other`, but not in both. + /// + /// let set: BitSet.Counted = [1, 2, 3, 4] + /// let other = [6, 4, 2, 0, 2, 0] + /// set.formSymmetricDifference(other) // [0, 1, 3, 6] + /// + /// - Parameter other: A sequence of nonnegative integers. + /// + /// - Complexity: O(*max*) + *k*, where *max* is the largest item in either + /// input, and *k* is the complexity of iterating over all elements in + /// `other`. + @inlinable + public func symmetricDifference( + _ other: __owned some Sequence + ) -> Self { + _bits.symmetricDifference(other).counted + } +} + +extension BitSet.Counted { + /// Returns a new set containing the elements of this set that do not occur + /// in the given other set. + /// + /// let set: BitSet.Counted = [1, 2, 3, 4] + /// let other: BitSet.Counted = [0, 2, 4, 6] + /// set.subtracting(other) // [1, 3] + /// + /// - Parameter other: Another bit set. + /// + /// - Returns: A new set. + /// + /// - Complexity: O(*max*), where *max* is the largest item in either input. + public func subtracting(_ other: BitSet.Counted) -> BitSet.Counted { + _bits.subtracting(other).counted + } + + /// Returns a new set containing the elements of this set that do not occur + /// in the given other set. + /// + /// let set: BitSet.Counted = [1, 2, 3, 4] + /// let other: BitSet = [0, 2, 4, 6] + /// set.subtracting(other) // [1, 3] + /// + /// - Parameter other: Another bit set. + /// + /// - Returns: A new set. + /// + /// - Complexity: O(*max*), where *max* is the largest item in either input. + public func subtracting(_ other: BitSet) -> BitSet.Counted { + _bits.subtracting(other).counted + } + + /// Returns a new set containing the elements of this set that do not occur + /// in the given range of integers. + /// + /// let set: BitSet.Counted = [1, 2, 3, 4] + /// set.subtracting(-10 ..< 3) // [3, 4] + /// + /// - Parameter other: A range of arbitrary integers. + /// + /// - Returns: A new set. + /// + /// - Complexity: O(*max*), where *max* is the largest item in self. + public func subtracting(_ other: Range) -> BitSet.Counted { + _bits.subtracting(other).counted + } + + /// Returns a new set containing the elements of this set that do not occur + /// in the given sequence of integers. + /// + /// let set: BitSet.Counted = [1, 2, 3, 4] + /// let other = [6, 4, 2, 0, -2, -4] + /// set.subtracting(other) // [1, 3] + /// + /// - Parameter other: A sequence of arbitrary integers. + /// + /// - Returns: A new set. + /// + /// - Complexity: O(*max*) + *k*, where *max* is the largest item in `self`, + /// and *k* is the complexity of iterating over all elements in `other`. + @inlinable + public func subtracting( + _ other: __owned some Sequence + ) -> Self { + _bits.subtracting(other).counted + } +} + +extension BitSet.Counted { + /// Adds the elements of the given set to this set. + /// + /// var set: BitSet.Counted = [1, 2, 3, 4] + /// let other: BitSet.Counted = [0, 2, 4, 6] + /// set.formUnion(other) + /// // `set` is now `[0, 1, 2, 3, 4, 6]` + /// + /// - Parameter other: The set of elements to insert. + /// + /// - Complexity: O(*max*), where *max* is the largest item in either input. + public mutating func formUnion(_ other: BitSet.Counted) { + _bits.formUnion(other._bits) + _count = _bits.count + _checkInvariants() + } + + /// Adds the elements of the given set to this set. + /// + /// var set: BitSet.Counted = [1, 2, 3, 4] + /// let other: BitSet = [0, 2, 4, 6] + /// set.formUnion(other) + /// // `set` is now `[0, 1, 2, 3, 4, 6]` + /// + /// - Parameter other: The set of elements to insert. + /// + /// - Complexity: O(*max*), where *max* is the largest item in either input. + public mutating func formUnion(_ other: BitSet) { + _bits.formUnion(other) + _count = _bits.count + _checkInvariants() + } + + /// Adds the elements of the given range of integers to this set. + /// + /// var set: BitSet.Counted = [1, 2, 3, 4] + /// set.formUnion(3 ..< 7) + /// // `set` is now `[1, 2, 3, 4, 5, 6]` + /// + /// - Parameter other: A range of nonnegative integers. + /// + /// - Complexity: O(*max*), where *max* is the largest item in either input. + public mutating func formUnion(_ other: Range) { + _bits.formUnion(other) + _count = _bits.count + _checkInvariants() + } + + /// Adds the elements of the given sequence to this set. + /// + /// var set: BitSet.Counted = [1, 2, 3, 4] + /// let other = [6, 4, 2, 0, 2, 0] + /// set.formUnion(other) + /// // `set` is now `[0, 1, 2, 3, 4, 6]` + /// + /// - Parameter other: A sequence of nonnegative integers. + /// + /// - Complexity: O(*max*) + *k*, where *max* is the largest item in either + /// input, and *k* is the complexity of iterating over all elements in + /// `other`. + @inlinable + public mutating func formUnion( + _ other: __owned some Sequence + ) { + _bits.formUnion(other) + _count = _bits.count + _checkInvariants() + } +} + +extension BitSet.Counted { + /// Removes the elements of this set that aren't also in the given one. + /// + /// var set: BitSet.Counted = [1, 2, 3, 4] + /// let other: BitSet.Counted = [0, 2, 4, 6] + /// set.formIntersection(other) + /// // set is now [2, 4] + /// + /// - Parameter other: A bit set. + /// + /// - Complexity: O(*max*), where *max* is the largest item in either set. + public mutating func formIntersection(_ other: BitSet.Counted) { + _bits.formIntersection(other._bits) + _count = _bits.count + _checkInvariants() + } + + /// Removes the elements of this set that aren't also in the given one. + /// + /// var set: BitSet.Counted = [1, 2, 3, 4] + /// let other: BitSet = [0, 2, 4, 6] + /// set.formIntersection(other) + /// // set is now [2, 4] + /// + /// - Parameter other: A bit set. + /// + /// - Complexity: O(*max*), where *max* is the largest item in either set. + public mutating func formIntersection(_ other: BitSet) { + _bits.formIntersection(other) + _count = _bits.count + _checkInvariants() + } + + /// Removes the elements of this set that aren't also in the given range. + /// + /// var set: BitSet.Counted = [1, 2, 3, 4] + /// set.formIntersection(-10 ..< 3) + /// // set is now [3, 4] + /// + /// - Parameter other: A range of integers. + /// + /// - Complexity: O(*max*), where *max* is the largest item in `self`. + public mutating func formIntersection(_ other: Range) { + _bits.formIntersection(other) + _count = _bits.count + _checkInvariants() + } + + /// Removes the elements of this set that aren't also in the given sequence. + /// + /// var set: BitSet.Counted = [1, 2, 3, 4] + /// let other: Set = [6, 4, 2, 0] + /// set.formIntersection(other) + /// // set is now [2, 4] + /// + /// - Parameter other: A sequence of integers. + /// + /// - Complexity: O(*max*) + *k*, where *max* is the largest item in `self`, + /// and *k* is the complexity of iterating over all elements in `other`. + @inlinable + public mutating func formIntersection( + _ other: __owned some Sequence + ) { + _bits.formIntersection(other) + _count = _bits.count + _checkInvariants() + } +} + +extension BitSet.Counted { + /// Replace this set with the elements contained in this set or the given + /// set, but not both. + /// + /// var set: BitSet.Counted = [1, 2, 3, 4] + /// let other: BitSet.Counted = [0, 2, 4, 6] + /// set.formSymmetricDifference(other) + /// // set is now [0, 1, 3, 6] + /// + /// - Parameter other: Another set. + /// + /// - Complexity: O(*max*), where *max* is the largest item in either set. + public mutating func formSymmetricDifference(_ other: BitSet.Counted) { + _bits.formSymmetricDifference(other._bits) + _count = _bits.count + _checkInvariants() + } + + /// Replace this set with the elements contained in this set or the given + /// set, but not both. + /// + /// var set: BitSet.Counted = [1, 2, 3, 4] + /// let other: BitSet = [0, 2, 4, 6] + /// set.formSymmetricDifference(other) + /// // set is now [0, 1, 3, 6] + /// + /// - Parameter other: Another set. + /// + /// - Complexity: O(*max*), where *max* is the largest item in either set. + public mutating func formSymmetricDifference(_ other: BitSet) { + _bits.formSymmetricDifference(other) + _count = _bits.count + _checkInvariants() + } + + /// Replace this set with the elements contained in this set or the given + /// range of integers, but not both. + /// + /// var set: BitSet.Counted = [1, 2, 3, 4] + /// set.formSymmetricDifference(3 ..< 7) + /// // set is now [1, 2, 5, 6] + /// + /// - Parameter other: A range of nonnegative integers. + /// + /// - Complexity: O(*max*), where *max* is the largest item in either input. + public mutating func formSymmetricDifference(_ other: Range) { + _bits.formSymmetricDifference(other) + _count = _bits.count + _checkInvariants() + } + + /// Replace this set with the elements contained in this set or the given + /// sequence, but not both. + /// + /// var set: BitSet.Counted = [1, 2, 3, 4] + /// let other = [6, 4, 2, 0, 2, 0] + /// set.formSymmetricDifference(other) + /// // set is now [0, 1, 3, 6] + /// + /// - Parameter other: A sequence of nonnegative integers. + /// + /// - Complexity: O(*max*) + *k*, where *max* is the largest item in either + /// input, and *k* is the complexity of iterating over all elements in + /// `other`. + @inlinable + public mutating func formSymmetricDifference( + _ other: __owned some Sequence + ) { + _bits.formSymmetricDifference(other) + _count = _bits.count + _checkInvariants() + } +} + +extension BitSet.Counted { + /// Removes the elements of the given bit set from this set. + /// + /// var set: BitSet.Counted = [1, 2, 3, 4] + /// let other: BitSet.Counted = [0, 2, 4, 6] + /// set.subtract(other) + /// // set is now [1, 3] + /// + /// - Parameter other: Another bit set. + /// + /// - Complexity: O(*max*), where *max* is the largest item in either input. + public mutating func subtract(_ other: BitSet.Counted) { + _bits.subtract(other._bits) + _count = _bits.count + _checkInvariants() + } + + /// Removes the elements of the given bit set from this set. + /// + /// var set: BitSet.Counted = [1, 2, 3, 4] + /// let other: BitSet = [0, 2, 4, 6] + /// set.subtract(other) + /// // set is now [1, 3] + /// + /// - Parameter other: Another bit set. + /// + /// - Complexity: O(*max*), where *max* is the largest item in either input. + public mutating func subtract(_ other: BitSet) { + _bits.subtract(other) + _count = _bits.count + _checkInvariants() + } + + /// Removes the elements of the given range of integers from this set. + /// + /// var set: BitSet.Counted = [1, 2, 3, 4] + /// set.subtract(-10 ..< 3) + /// // set is now [3, 4] + /// + /// - Parameter other: A range of arbitrary integers. + /// + /// - Returns: A new set. + /// + /// - Complexity: O(*max*), where *max* is the largest item in self. + public mutating func subtract(_ other: Range) { + _bits.subtract(other) + _count = _bits.count + _checkInvariants() + } + + /// Removes the elements of the given sequence of integers from this set. + /// + /// var set: BitSet.Counted = [1, 2, 3, 4] + /// let other = [6, 4, 2, 0, -2, -4] + /// set.subtract(other) + /// // set is now [1, 3] + /// + /// - Parameter other: A sequence of arbitrary integers. + /// + /// - Complexity: O(*max*) + *k*, where *max* is the largest item in `self`, + /// and *k* is the complexity of iterating over all elements in `other`. + @inlinable + public mutating func subtract( + _ other: __owned some Sequence + ) { + _bits.subtract(other) + _count = _bits.count + _checkInvariants() + } +} + +extension BitSet.Counted { + /// Returns a Boolean value indicating whether two bit sets are equal. Two + /// bit sets are considered equal if they contain the same elements. + /// + /// - Complexity: O(*max*), where *max* is value of the largest member of + /// either set. + public func isEqualSet(to other: Self) -> Bool { + guard self.count == other.count else { return false } + return self._bits.isEqualSet(to: other._bits) + } + + /// Returns a Boolean value indicating whether a bit set is equal to a counted + /// bit set, i.e., whether they contain the same values. + /// + /// - Complexity: O(*max*), where *max* is value of the largest member of + /// either set. + public func isEqualSet(to other: BitSet) -> Bool { + self._bits.isEqualSet(to: other) + } + + /// Returns a Boolean value indicating whether a bit set is equal to a range + /// of integers, i.e., whether they contain the same values. + /// + /// - Complexity: O(min(*max*, `other.upperBound`), where *max* is the largest + /// member of `self`. + public func isEqualSet(to other: Range) -> Bool { + guard self.count == other.count else { return false } + return _bits.isEqualSet(to: other) + } + + /// Returns a Boolean value indicating whether this bit set contains the same + /// elements as the given `other` sequence. + /// + /// Duplicate items in `other` do not prevent it from comparing equal to + /// `self`. + /// + /// let bits: BitSet = [0, 1, 5, 6] + /// let other = [5, 5, 0, 1, 1, 6, 5, 0, 1, 6, 6, 5] + /// + /// bits.isEqualSet(to: other) // true + /// + /// - Complexity: O(*n*), where *n* is the number of items in `other`. + @inlinable + public func isEqualSet(to other: some Sequence) -> Bool { + guard self.count >= other.underestimatedCount else { return false } + return _bits.isEqualSet(to: other) + } +} + +extension BitSet.Counted { + /// Returns a Boolean value that indicates whether this set is a subset of + /// the given set. + /// + /// Set *A* is a subset of another set *B* if every member of *A* is also a + /// member of *B*. + /// + /// let a: BitSet.Counted = [1, 2, 3, 4] + /// let b: BitSet.Counted = [1, 2, 4] + /// let c: BitSet.Counted = [0, 1] + /// a.isSubset(of: a) // true + /// b.isSubset(of: a) // true + /// c.isSubset(of: a) // false + /// + /// - Parameter other: Another bit set. + /// + /// - Returns: `true` if the set is a subset of `other`; otherwise, `false`. + /// + /// - Complexity: O(*max*), where *max* is the largest item in `self`. + public func isSubset(of other: Self) -> Bool { + if self.count > other.count { return false } + return self._bits.isSubset(of: other._bits) + } + + /// Returns a Boolean value that indicates whether this set is a subset of + /// the given set. + /// + /// Set *A* is a subset of another set *B* if every member of *A* is also a + /// member of *B*. + /// + /// - Parameter other: Another bit set. + /// + /// - Returns: `true` if the set is a subset of `other`; otherwise, `false`. + /// + /// - Complexity: O(*max*), where *max* is the largest item in `self`. + public func isSubset(of other: BitSet) -> Bool { + self._bits.isSubset(of: other) + } + + /// Returns a Boolean value that indicates whether this set is a subset of + /// the given range of integers. + /// + /// Set *A* is a subset of another set *B* if every member of *A* is also a + /// member of *B*. + /// + /// let b: BitSet.Counted = [0, 1, 2] + /// let c: BitSet.Counted = [2, 3, 4] + /// b.isSubset(of: -10 ..< 4) // true + /// c.isSubset(of: -10 ..< 4) // false + /// + /// - Parameter other: An arbitrary range of integers. + /// + /// - Returns: `true` if the set is a subset of `other`; otherwise, `false`. + /// + /// - Complexity: O(*max*), where *max* is the largest item in `self`. + public func isSubset(of other: Range) -> Bool { + _bits.isSubset(of: other) + } + + /// Returns a Boolean value that indicates whether this set is a subset of + /// the values in a given sequence of integers. + /// + /// Set *A* is a subset of another set *B* if every member of *A* is also a + /// member of *B*. + /// + /// let a = [1, 2, 3, 4, -10] + /// let b: BitSet.Counted = [1, 2, 4] + /// let c: BitSet.Counted = [0, 1] + /// b.isSubset(of: a) // true + /// c.isSubset(of: a) // false + /// + /// - Parameter other: A sequence of arbitrary integers. + /// + /// - Returns: `true` if the set is a subset of `other`; otherwise, `false`. + /// + /// - Complexity: O(*max*) + *k*, where *max* is the largest item in `self`, + /// and *k* is the complexity of iterating over all elements in `other`. + @inlinable + public func isSubset(of other: some Sequence) -> Bool { + _bits.isSubset(of: other) + } +} + +extension BitSet.Counted { + /// Returns a Boolean value that indicates whether this set is a superset of + /// the given set. + /// + /// Set *A* is a superset of another set *B* if every member of *B* is also a + /// member of *A*. + /// + /// let a: BitSet.Counted = [1, 2, 3, 4] + /// let b: BitSet.Counted = [1, 2, 4] + /// let c: BitSet.Counted = [0, 1] + /// a.isSuperset(of: a) // true + /// a.isSuperset(of: b) // true + /// a.isSuperset(of: c) // false + /// + /// - Parameter other: Another bit set. + /// + /// - Returns: `true` if the set is a superset of `other`; otherwise, `false`. + /// + /// - Complexity: O(*max*), where *max* is the largest item in `other`. + public func isSuperset(of other: Self) -> Bool { + other.isSubset(of: self) + } + + /// Returns a Boolean value that indicates whether this set is a superset of + /// the given set. + /// + /// Set *A* is a superset of another set *B* if every member of *B* is also a + /// member of *A*. + /// + /// - Parameter other: Another bit set. + /// + /// - Returns: `true` if the set is a superset of `other`; otherwise, `false`. + /// + /// - Complexity: O(*max*), where *max* is the largest item in `other`. + public func isSuperset(of other: BitSet) -> Bool { + other.isSubset(of: self) + } + + /// Returns a Boolean value that indicates whether this set is a superset of + /// a given range of integers. + /// + /// Set *A* is a superset of another set *B* if every member of *B* is also a + /// member of *A*. + /// + /// let a: BitSet = [0, 1, 2, 3, 4, 10] + /// a.isSuperset(of: 0 ..< 4) // true + /// a.isSuperset(of: -10 ..< 4) // false + /// + /// - Parameter other: An arbitrary range of integers. + /// + /// - Returns: `true` if the set is a subset of `other`; otherwise, `false`. + /// + /// - Complexity: O(`range.count`) + public func isSuperset(of other: Range) -> Bool { + _bits.isSuperset(of: other) + } + + /// Returns a Boolean value that indicates whether this set is a superset of + /// the values in a given sequence of integers. + /// + /// Set *A* is a superset of another set *B* if every member of *B* is also a + /// member of *A*. + /// + /// let a = [1, 2, 3] + /// let b: BitSet = [0, 1, 2, 3, 4] + /// let c: BitSet = [0, 1, 2] + /// b.isSuperset(of: a) // true + /// c.isSuperset(of: a) // false + /// + /// - Parameter other: A sequence of arbitrary integers, some of whose members + /// may appear more than once. (Duplicate items are ignored.) + /// + /// - Returns: `true` if the set is a subset of `other`; otherwise, `false`. + /// + /// - Complexity: The same as the complexity of iterating over all elements + /// in `other`. + @inlinable + public func isSuperset(of other: some Sequence) -> Bool { + _bits.isSuperset(of: other) + } +} + +extension BitSet.Counted { + /// Returns a Boolean value that indicates whether this bit set is a strict + /// subset of the given set. + /// + /// Set *A* is a strict subset of another set *B* if every member of *A* is + /// also a member of *B* and *B* contains at least one element that is not a + /// member of *A*. + /// + /// let a: BitSet.Counted = [1, 2, 3, 4] + /// let b: BitSet.Counted = [1, 2, 4] + /// let c: BitSet.Counted = [0, 1] + /// a.isStrictSubset(of: a) // false + /// b.isStrictSubset(of: a) // true + /// c.isStrictSubset(of: a) // false + /// + /// - Parameter other: Another bit set. + /// + /// - Returns: `true` if the set is a strict subset of `other`; + /// otherwise, `false`. + /// + /// - Complexity: O(*max*), where *max* is the largest item in `self`. + public func isStrictSubset(of other: Self) -> Bool { + guard self.count < other.count else { return false } + return _bits.isStrictSubset(of: other._bits) + } + + /// Returns a Boolean value that indicates whether this bit set is a strict + /// subset of the given set. + /// + /// Set *A* is a strict subset of another set *B* if every member of *A* is + /// also a member of *B* and *B* contains at least one element that is not a + /// member of *A*. + /// + /// - Parameter other: Another bit set. + /// + /// - Returns: `true` if the set is a strict subset of `other`; + /// otherwise, `false`. + /// + /// - Complexity: O(*max*), where *max* is the largest item in `self`. + public func isStrictSubset(of other: BitSet) -> Bool { + _bits.isStrictSubset(of: other) + } + + /// Returns a Boolean value that indicates whether this set is a strict + /// subset of the given set. + /// + /// Set *A* is a strict subset of another set *B* if every member of *A* is + /// also a member of *B* and *B* contains at least one element that is not a + /// member of *A*. + /// + /// let b: BitSet.Counted = [0, 1, 2] + /// let c: BitSet.Counted = [2, 3, 4] + /// b.isStrictSubset(of: -10 ..< 4) // true + /// c.isStrictSubset(of: -10 ..< 4) // false + /// + /// - Parameter other: An arbitrary range of integers. + /// + /// - Returns: `true` if the set is a strict subset of `other`; + /// otherwise, `false`. + /// + /// - Complexity: O(*max*), where *max* is the largest item in `self`. + public func isStrictSubset(of other: Range) -> Bool { + guard self.count < other.count else { return false } + return _bits.isStrictSubset(of: other) + } + + /// Returns a Boolean value that indicates whether this bit set is a strict + /// subset of the values in a given sequence of integers. + /// + /// Set *A* is a strict subset of another set *B* if every member of *A* is + /// also a member of *B* and *B* contains at least one element that is not a + /// member of *A*. + /// + /// let a = [1, 2, 3, 4, -10] + /// let b: BitSet.Counted = [1, 2, 4] + /// let c: BitSet.Counted = [0, 1] + /// b.isStrictSubset(of: a) // true + /// c.isStrictSubset(of: a) // false + /// + /// - Parameter other: A sequence of arbitrary integers. + /// + /// - Returns: `true` if the set is a strict subset of `other`; + /// otherwise, `false`. + /// + /// - Complexity: O(*max*) + *k*, where *max* is the largest item in `self`, + /// and *k* is the complexity of iterating over all elements in `other`. + @inlinable + public func isStrictSubset(of other: some Sequence) -> Bool { + _bits.isStrictSubset(of: other) + } +} + +extension BitSet.Counted { + /// Returns a Boolean value that indicates whether this set is a strict + /// superset of another set. + /// + /// Set *A* is a strict superset of another set *B* if every member of *B* is + /// also a member of *A* and *A* contains at least one element that is *not* + /// a member of *B*. + /// + /// let a: BitSet.Counted = [1, 2, 3, 4] + /// let b: BitSet.Counted = [1, 2, 4] + /// let c: BitSet.Counted = [0, 1] + /// a.isStrictSuperset(of: a) // false + /// a.isStrictSuperset(of: b) // true + /// a.isStrictSuperset(of: c) // false + /// + /// - Parameter other: Another bit set. + /// + /// - Returns: `true` if the set is a superset of `other`; otherwise, `false`. + /// + /// - Complexity: O(*max*), where *max* is the largest item in `other`. + public func isStrictSuperset(of other: Self) -> Bool { + other.isStrictSubset(of: self) + } + + /// Returns a Boolean value that indicates whether this set is a strict + /// superset of another set. + /// + /// Set *A* is a strict superset of another set *B* if every member of *B* is + /// also a member of *A* and *A* contains at least one element that is *not* + /// a member of *B*. + /// + /// - Parameter other: Another bit set. + /// + /// - Returns: `true` if the set is a superset of `other`; otherwise, `false`. + /// + /// - Complexity: O(*max*), where *max* is the largest item in `other`. + public func isStrictSuperset(of other: BitSet) -> Bool { + other.isStrictSubset(of: self) + } + + /// Returns a Boolean value that indicates whether this set is a superset of + /// a given range of integers. + /// + /// Set *A* is a superset of another set *B* if every member of *B* is also a + /// member of *A*. + /// + /// let a: BitSet.Counted = [0, 1, 2, 3, 4, 10] + /// a.isSuperset(of: 0 ..< 4) // true + /// a.isSuperset(of: -10 ..< 4) // false + /// + /// - Parameter other: An arbitrary range of integers. + /// + /// - Returns: `true` if the set is a subset of `other`; otherwise, `false`. + /// + /// - Complexity: O(`range.count`) + public func isStrictSuperset(of other: Range) -> Bool { + _bits.isStrictSuperset(of: other) + } + + /// Returns a Boolean value that indicates whether this set is a superset of + /// the values in a given sequence of integers. + /// + /// Set *A* is a superset of another set *B* if every member of *B* is also a + /// member of *A*. + /// + /// let a = [1, 2, 3] + /// let b: BitSet.Counted = [0, 1, 2, 3, 4] + /// let c: BitSet.Counted = [0, 1, 2] + /// b.isSuperset(of: a) // true + /// c.isSuperset(of: a) // false + /// + /// - Parameter other: A sequence of arbitrary integers, some of whose members + /// may appear more than once. (Duplicate items are ignored.) + /// + /// - Returns: `true` if the set is a subset of `other`; otherwise, `false`. + /// + /// - Complexity: O(*max*) + *k*, where *max* is the largest item in `other`, + /// and *k* is the complexity of iterating over all elements in `other`. + @inlinable + public func isStrictSuperset(of other: some Sequence) -> Bool { + _bits.isStrictSuperset(of: other) + } +} + +extension BitSet.Counted { + /// Returns a Boolean value that indicates whether the set has no members in + /// common with the given set. + /// + /// let a: BitSet.Counted = [1, 2, 3, 4] + /// let b: BitSet.Counted = [5, 6] + /// a.isDisjoint(with: b) // true + /// + /// - Parameter other: Another bit set. + /// + /// - Returns: `true` if `self` has no elements in common with `other`; + /// otherwise, `false`. + /// + /// - Complexity: O(*max*), where *max* is the largest item in either input. + public func isDisjoint(with other: Self) -> Bool { + _bits.isDisjoint(with: other._bits) + } + + /// Returns a Boolean value that indicates whether the set has no members in + /// common with the given set. + /// + /// let a: BitSet.Counted = [1, 2, 3, 4] + /// let b: BitSet = [5, 6] + /// a.isDisjoint(with: b) // true + /// + /// - Parameter other: Another bit set. + /// + /// - Returns: `true` if `self` has no elements in common with `other`; + /// otherwise, `false`. + /// + /// - Complexity: O(*max*), where *max* is the largest item in either input. + public func isDisjoint(with other: BitSet) -> Bool { + _bits.isDisjoint(with: other) + } + + /// Returns a Boolean value that indicates whether the set has no members in + /// common with the given range of integers. + /// + /// let a: BitSet = [1, 2, 3, 4] + /// a.isDisjoint(with: -10 ..< 0) // true + /// + /// - Parameter other: A range of arbitrary integers. + /// + /// - Returns: `true` if `self` has no elements in common with `other`; + /// otherwise, `false`. + /// + /// - Complexity: O(*max*), where *max* is the largest item in `self`. + public func isDisjoint(with other: Range) -> Bool { + _bits.isDisjoint(with: other) + } + + /// Returns a Boolean value that indicates whether the set has no members in + /// common with the given sequence of integers. + /// + /// let a: BitSet = [1, 2, 3, 4] + /// let b: BitSet = [5, 6, -10, 42] + /// a.isDisjoint(with: b) // true + /// + /// - Parameter other: A sequence of arbitrary integers. + /// + /// - Returns: `true` if `self` has no elements in common with `other`; + /// otherwise, `false`. + /// + /// - Complexity: O(*max*) + *k*, where *max* is the largest item in `self`, + /// and *k* is the complexity of iterating over all elements in `other`. + @inlinable + public func isDisjoint(with other: some Sequence) -> Bool { + _bits.isDisjoint(with: other) + } +} diff --git a/Sources/BitCollections/BitSet/BitSet.Index.swift b/Sources/BitCollections/BitSet/BitSet.Index.swift new file mode 100644 index 000000000..45c28f8fe --- /dev/null +++ b/Sources/BitCollections/BitSet/BitSet.Index.swift @@ -0,0 +1,91 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension BitSet { + /// An opaque type that represents a position in a bit set. + /// + /// The elements of a bit set can be addressed simply by their value, + /// so the `Index` type could be defined to be `Int`, the same as `Element`. + /// However, `BitSet` uses an opaque wrapper instead, to prevent confusion: + /// it would otherwise be all too easy to accidentally use `i + 1` instead of + /// calling `index(after: i)`, and ending up with an invalid index. + @frozen + public struct Index { + @usableFromInline + var _value: UInt + + @inlinable + internal init(_value: UInt) { + self._value = _value + } + + @inlinable + internal init(_position: _UnsafeHandle.Index) { + self._value = _position.value + } + + @inline(__always) + internal var _position: _UnsafeHandle.Index { + _UnsafeHandle.Index(_value) + } + } +} + +extension BitSet.Index: Sendable {} + +extension BitSet.Index: CustomStringConvertible { + // A textual representation of this instance. + public var description: String { + "\(_value)" + } +} + +extension BitSet.Index: CustomDebugStringConvertible { + // A textual representation of this instance, suitable for debugging. + public var debugDescription: String { + description + } +} + +extension BitSet.Index: Equatable { + /// Returns a Boolean value indicating whether two values are equal. + /// + /// - Complexity: O(1) + @inlinable @inline(__always) + public static func ==(left: Self, right: Self) -> Bool { + left._value == right._value + } +} + +extension BitSet.Index: Comparable { + /// Returns a Boolean value indicating whether the first value is ordered + /// before the second. + /// + /// - Complexity: O(1) + @inlinable @inline(__always) + public static func < (left: Self, right: Self) -> Bool { + left._value < right._value + } +} + +extension BitSet.Index: Hashable { + /// Hashes the essential components of this value by feeding them to the given hasher. + /// + /// - Complexity: O(1) + @inlinable @inline(__always) + public func hash(into hasher: inout Hasher) { + hasher.combine(_value) + } +} diff --git a/Sources/BitCollections/BitSet/BitSet._UnsafeHandle.swift b/Sources/BitCollections/BitSet/BitSet._UnsafeHandle.swift new file mode 100644 index 000000000..5e717b49d --- /dev/null +++ b/Sources/BitCollections/BitSet/BitSet._UnsafeHandle.swift @@ -0,0 +1,250 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension BitSet { + @usableFromInline + internal typealias _UnsafeHandle = _UnsafeBitSet +} + +extension _UnsafeBitSet { + @inline(__always) + internal func _isReachable(_ index: Index) -> Bool { + index == endIndex || contains(index.value) + } +} + +extension _UnsafeBitSet { + internal func _emptySuffix() -> Int { + var i = wordCount - 1 + while i >= 0, _words[i].isEmpty { + i -= 1 + } + return wordCount - 1 - i + } +} + +extension _UnsafeBitSet { + @inlinable + public mutating func combineSharedPrefix( + with other: Self, + using function: (inout _Word, _Word) -> Void + ) { + ensureMutable() + let c = Swift.min(self.wordCount, other.wordCount) + for w in 0 ..< c { + function(&self._mutableWords[w], other._words[w]) + } + } +} + +extension _UnsafeBitSet { + @_effects(releasenone) + public mutating func formUnion(_ range: Range) { + ensureMutable() + let l = Index(range.lowerBound) + let u = Index(range.upperBound) + assert(u.value <= capacity) + + if l.word == u.word { + guard l.word < wordCount else { return } + let w = _Word(from: l.bit, to: u.bit) + _mutableWords[l.word].formUnion(w) + return + } + _mutableWords[l.word].formUnion(_Word(upTo: l.bit).complement()) + for w in l.word + 1 ..< u.word { + _mutableWords[w] = .allBits + } + if u.word < wordCount { + _mutableWords[u.word].formUnion(_Word(upTo: u.bit)) + } + } + + @_effects(releasenone) + public mutating func formIntersection(_ range: Range) { + ensureMutable() + let l = Index(Swift.min(range.lowerBound, capacity)) + let u = Index(Swift.min(range.upperBound, capacity)) + + for w in 0 ..< l.word { + _mutableWords[w] = .empty + } + + if l.word == u.word { + guard l.word < wordCount else { return } + + let w = _Word(from: l.bit, to: u.bit) + _mutableWords[l.word].formIntersection(w) + return + } + + _mutableWords[l.word].formIntersection(_Word(upTo: l.bit).complement()) + if u.word < wordCount { + _mutableWords[u.word].formIntersection(_Word(upTo: u.bit)) + _mutableWords[(u.word + 1)...].update(repeating: .empty) + } + } + + @_effects(releasenone) + public mutating func formSymmetricDifference(_ range: Range) { + ensureMutable() + let l = Index(range.lowerBound) + let u = Index(range.upperBound) + assert(u.value <= capacity) + + if l.word == u.word { + guard l.word < wordCount else { return } + let w = _Word(from: l.bit, to: u.bit) + _mutableWords[l.word].formSymmetricDifference(w) + return + } + _mutableWords[l.word] + .formSymmetricDifference(_Word(upTo: l.bit).complement()) + for w in l.word + 1 ..< u.word { + _mutableWords[w].formComplement() + } + if u.word < wordCount { + _mutableWords[u.word].formSymmetricDifference(_Word(upTo: u.bit)) + } + } + + @_effects(releasenone) + public mutating func subtract(_ range: Range) { + ensureMutable() + let l = Index(Swift.min(range.lowerBound, capacity)) + let u = Index(Swift.min(range.upperBound, capacity)) + + if l.word == u.word { + guard l.word < wordCount else { return } + let w = _Word(from: l.bit, to: u.bit) + _mutableWords[l.word].subtract(w) + return + } + + _mutableWords[l.word].subtract(_Word(upTo: l.bit).complement()) + _mutableWords[(l.word + 1) ..< u.word].update(repeating: .empty) + if u.word < wordCount { + _mutableWords[u.word].subtract(_Word(upTo: u.bit)) + } + } + + @_effects(releasenone) + public func isDisjoint(with range: Range) -> Bool { + if self.isEmpty { return true } + let lower = Index(Swift.min(range.lowerBound, capacity)) + let upper = Index(Swift.min(range.upperBound, capacity)) + if lower == upper { return true } + + if lower.word == upper.word { + guard lower.word < wordCount else { return true } + let w = _Word(from: lower.bit, to: upper.bit) + return _words[lower.word].intersection(w).isEmpty + } + + let lw = _Word(upTo: lower.bit).complement() + guard _words[lower.word].intersection(lw).isEmpty else { return false } + + for i in lower.word + 1 ..< upper.word { + guard _words[i].isEmpty else { return false } + } + if upper.word < wordCount { + let uw = _Word(upTo: upper.bit) + guard _words[upper.word].intersection(uw).isEmpty else { return false } + } + return true + } + + @_effects(releasenone) + public func isSubset(of range: Range) -> Bool { + guard !range.isEmpty else { return isEmpty } + guard !_words.isEmpty else { return true } + let r = range.clamped(to: 0 ..< UInt(capacity)) + + let lower = Index(r.lowerBound) + let upper = Index(r.upperBound) + + for w in 0 ..< lower.word { + guard _words[w].isEmpty else { return false } + } + + guard lower.word < wordCount else { return true } + + let lw = _Word(upTo: lower.bit) + guard _words[lower.word].intersection(lw).isEmpty else { return false } + + guard upper.word < wordCount else { return true } + + let hw = _Word(upTo: upper.bit).complement() + guard _words[upper.word].intersection(hw).isEmpty else { return false } + + return true + } + + @_effects(releasenone) + public func isSuperset(of range: Range) -> Bool { + guard !range.isEmpty else { return true } + let r = range.clamped(to: 0 ..< UInt(capacity)) + guard r == range else { return false } + + let lower = Index(range.lowerBound) + let upper = Index(range.upperBound) + + if lower.word == upper.word { + let w = _Word(from: lower.bit, to: upper.bit) + return _words[lower.word].intersection(w) == w + } + let lw = _Word(upTo: lower.bit).complement() + guard _words[lower.word].intersection(lw) == lw else { return false } + + for w in lower.word + 1 ..< upper.word { + guard _words[w].isFull else { return false } + } + + guard upper.word < wordCount else { return true } + let uw = _Word(upTo: upper.bit) + return _words[upper.word].intersection(uw) == uw + } + + @_effects(releasenone) + public func isEqualSet(to range: Range) -> Bool { + if range.isEmpty { return self.isEmpty } + let r = range.clamped(to: 0 ..< UInt(capacity)) + guard r == range else { return false } + + let lower = Index(range.lowerBound).split + let upper = Index(range.upperBound).endSplit + + guard upper.word == wordCount &- 1 else { return false } + + for w in 0 ..< lower.word { + guard _words[w].isEmpty else { return false } + } + + if lower.word == upper.word { + let w = _Word(from: lower.bit, to: upper.bit) + return _words[lower.word] == w + } + let lw = _Word(upTo: lower.bit).complement() + guard _words[lower.word] == lw else { return false } + + for w in lower.word + 1 ..< upper.word { + guard _words[w].isFull else { return false } + } + + let uw = _Word(upTo: upper.bit) + return _words[upper.word] == uw + } +} + diff --git a/Sources/BitCollections/BitSet/BitSet.swift b/Sources/BitCollections/BitSet/BitSet.swift new file mode 100644 index 000000000..79cd0d467 --- /dev/null +++ b/Sources/BitCollections/BitSet/BitSet.swift @@ -0,0 +1,101 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +/// A sorted collection of small nonnegative integers, implemented as an +/// uncompressed bitmap of as many bits as the value of the largest member. +/// +/// Bit sets implement `SetAlgebra` and provide efficient implementations +/// for set operations based on standard binary logic operations. +/// +/// See `BitArray` for an alternative form of the same underlying data +/// structure, treating it as a mutable random-access collection of `Bool` +/// values. +public struct BitSet { + @usableFromInline + internal var _storage: [_Word] + + @usableFromInline + init(_rawStorage storage: [_Word]) { + self._storage = storage + _checkInvariants() + } +} + +extension BitSet: Sendable {} + +extension BitSet { + @inline(__always) + internal func _read( + _ body: (_UnsafeHandle) throws -> R + ) rethrows -> R { + try _storage.withUnsafeBufferPointer { words in + let handle = _UnsafeHandle(words: words, mutable: false) + return try body(handle) + } + } + + @usableFromInline + internal var _capacity: UInt { + UInt(_storage.count) &* UInt(_Word.capacity) + } + + internal mutating func _ensureCapacity(limit capacity: UInt) { + let desired = _UnsafeHandle.wordCount(forCapacity: capacity) + guard _storage.count < desired else { return } + _storage.append( + contentsOf: repeatElement(.empty, count: desired - _storage.count)) + } + + internal mutating func _ensureCapacity(forValue value: UInt) { + let desiredWord = _UnsafeHandle.Index(value).word + guard desiredWord >= _storage.count else { return } + _storage.append( + contentsOf: + repeatElement(.empty, count: desiredWord - _storage.count + 1)) + } + + internal mutating func _shrink() { + let suffix = _read { $0._emptySuffix() } + if suffix > 0 { _storage.removeLast(suffix) } + } + + @inline(__always) + internal mutating func _update( + _ body: (inout _UnsafeHandle) throws -> R + ) rethrows -> R { + defer { + _checkInvariants() + } + return try _storage.withUnsafeMutableBufferPointer { words in + var handle = _UnsafeHandle(words: words, mutable: true) + return try body(&handle) + } + } + + @inline(__always) + internal mutating func _updateThenShrink( + _ body: (_ handle: inout _UnsafeHandle, _ shrink: inout Bool) throws -> R + ) rethrows -> R { + var shrink = true + defer { + if shrink { _shrink() } + _checkInvariants() + } + return try _storage.withUnsafeMutableBufferPointer { words in + var handle = _UnsafeHandle(words: words, mutable: true) + return try body(&handle, &shrink) + } + } +} diff --git a/Sources/BitCollections/CMakeLists.txt b/Sources/BitCollections/CMakeLists.txt new file mode 100644 index 000000000..25aedb54a --- /dev/null +++ b/Sources/BitCollections/CMakeLists.txt @@ -0,0 +1,74 @@ +#[[ +This source file is part of the Swift Collections Open Source Project + +Copyright (c) 2022 - 2024 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 +#]] + +add_library(BitCollections + "BitArray/BitArray+BitwiseOperations.swift" + "BitArray/BitArray+ChunkedBitsIterators.swift" + "BitArray/BitArray+Codable.swift" + "BitArray/BitArray+Collection.swift" + "BitArray/BitArray+Copy.swift" + "BitArray/BitArray+CustomReflectable.swift" + "BitArray/BitArray+Descriptions.swift" + "BitArray/BitArray+Equatable.swift" + "BitArray/BitArray+ExpressibleByArrayLiteral.swift" + "BitArray/BitArray+Extras.swift" + "BitArray/BitArray+Fill.swift" + "BitArray/BitArray+Hashable.swift" + "BitArray/BitArray+Initializers.swift" + "BitArray/BitArray+Invariants.swift" + "BitArray/BitArray+RandomBits.swift" + "BitArray/BitArray+RangeReplaceableCollection.swift" + "BitArray/BitArray+Testing.swift" + "BitArray/BitArray._UnsafeHandle.swift" + "BitArray/BitArray.swift" + "BitSet/BitSet+BidirectionalCollection.swift" + "BitSet/BitSet+Codable.swift" + "BitSet/BitSet+CustomDebugStringConvertible.swift" + "BitSet/BitSet+CustomReflectable.swift" + "BitSet/BitSet+CustomStringConvertible.swift" + "BitSet/BitSet+Equatable.swift" + "BitSet/BitSet+ExpressibleByArrayLiteral.swift" + "BitSet/BitSet+Extras.swift" + "BitSet/BitSet+Hashable.swift" + "BitSet/BitSet+Initializers.swift" + "BitSet/BitSet+Invariants.swift" + "BitSet/BitSet+Random.swift" + "BitSet/BitSet+SetAlgebra basics.swift" + "BitSet/BitSet+SetAlgebra conformance.swift" + "BitSet/BitSet+SetAlgebra formIntersection.swift" + "BitSet/BitSet+SetAlgebra formSymmetricDifference.swift" + "BitSet/BitSet+SetAlgebra formUnion.swift" + "BitSet/BitSet+SetAlgebra intersection.swift" + "BitSet/BitSet+SetAlgebra isDisjoint.swift" + "BitSet/BitSet+SetAlgebra isEqualSet.swift" + "BitSet/BitSet+SetAlgebra isStrictSubset.swift" + "BitSet/BitSet+SetAlgebra isStrictSuperset.swift" + "BitSet/BitSet+SetAlgebra isSubset.swift" + "BitSet/BitSet+SetAlgebra isSuperset.swift" + "BitSet/BitSet+SetAlgebra subtract.swift" + "BitSet/BitSet+SetAlgebra subtracting.swift" + "BitSet/BitSet+SetAlgebra symmetricDifference.swift" + "BitSet/BitSet+SetAlgebra union.swift" + "BitSet/BitSet+Sorted Collection APIs.swift" + "BitSet/BitSet.Counted.swift" + "BitSet/BitSet.Index.swift" + "BitSet/BitSet._UnsafeHandle.swift" + "BitSet/BitSet.swift" + "Shared/Range+Utilities.swift" + "Shared/Slice+Utilities.swift" + "Shared/UInt+Tricks.swift" + "Shared/_Word.swift" + ) +target_link_libraries(BitCollections PRIVATE + _CollectionsUtilities) +set_target_properties(BitCollections PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) + +_install_target(BitCollections) +set_property(GLOBAL APPEND PROPERTY SWIFT_COLLECTIONS_EXPORTS BitCollections) diff --git a/Sources/BitCollections/Shared/Range+Utilities.swift b/Sources/BitCollections/Shared/Range+Utilities.swift new file mode 100644 index 000000000..c18df4525 --- /dev/null +++ b/Sources/BitCollections/Shared/Range+Utilities.swift @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 Range where Bound: FixedWidthInteger { + @inlinable + internal func _clampedToUInt() -> Range { + if upperBound <= 0 { + return Range(uncheckedBounds: (0, 0)) + } + if lowerBound >= UInt.max { + return Range(uncheckedBounds: (UInt.max, UInt.max)) + } + let lower = lowerBound < 0 ? 0 : UInt(lowerBound) + let upper = upperBound > UInt.max ? UInt.max : UInt(upperBound) + return Range(uncheckedBounds: (lower, upper)) + } + + @inlinable + internal func _toUInt() -> Range? { + guard + let lower = UInt(exactly: lowerBound), + let upper = UInt(exactly: upperBound) + else { + return nil + } + return Range(uncheckedBounds: (lower: lower, upper: upper)) + } +} diff --git a/Sources/BitCollections/Shared/Slice+Utilities.swift b/Sources/BitCollections/Shared/Slice+Utilities.swift new file mode 100644 index 000000000..146b92585 --- /dev/null +++ b/Sources/BitCollections/Shared/Slice+Utilities.swift @@ -0,0 +1,16 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 Slice { + internal var _bounds: Range { + Range(uncheckedBounds: (startIndex, endIndex)) + } +} diff --git a/Sources/BitCollections/Shared/UInt+Tricks.swift b/Sources/BitCollections/Shared/UInt+Tricks.swift new file mode 100644 index 000000000..8040d031e --- /dev/null +++ b/Sources/BitCollections/Shared/UInt+Tricks.swift @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension UInt { + /// Returns the position of the `n`th set bit in `self`. + /// + /// - Parameter n: The to retrieve. This value is + /// decremented by the number of items found in this `self` towards the + /// value we're looking for. (If the function returns non-nil, then `n` + /// is set to `0` on return.) + /// - Returns: If this integer contains enough set bits to satisfy the + /// request, then this function returns the position of the bit found. + /// Otherwise it returns nil. + internal func _rank(ofBit n: inout UInt) -> UInt? { + let c = self.nonzeroBitCount + guard n < c else { + n &-= UInt(bitPattern: c) + return nil + } + let m = Int(bitPattern: n) + n = 0 + return _bit(ranked: m)! + } +} diff --git a/Sources/BitCollections/Shared/_Word.swift b/Sources/BitCollections/Shared/_Word.swift new file mode 100644 index 000000000..64b331c8f --- /dev/null +++ b/Sources/BitCollections/Shared/_Word.swift @@ -0,0 +1,72 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +@usableFromInline +internal typealias _Word = _UnsafeBitSet._Word + +extension Array where Element == _Word { + internal func _encodeAsUInt64( + to container: inout UnkeyedEncodingContainer + ) throws { + if _Word.capacity == 64 { + for word in self { + try container.encode(UInt64(truncatingIfNeeded: word.value)) + } + return + } + assert(_Word.capacity == 32, "Unsupported platform") + var first = true + var w: UInt64 = 0 + for word in self { + if first { + w = UInt64(truncatingIfNeeded: word.value) + first = false + } else { + w |= UInt64(truncatingIfNeeded: word.value) &<< 32 + try container.encode(w) + first = true + } + } + if !first { + try container.encode(w) + } + } + + internal init( + _fromUInt64 container: inout UnkeyedDecodingContainer, + reservingCount count: Int? = nil + ) throws { + self = [] + if Element.capacity == 64 { + if let c = count { + self.reserveCapacity(c) + } + while !container.isAtEnd { + let v = try container.decode(UInt64.self) + self.append(Element(UInt(truncatingIfNeeded: v))) + } + return + } + assert(Element.capacity == 32, "Unsupported platform") + if let c = count { + self.reserveCapacity(2 * c) + } + while !container.isAtEnd { + let v = try container.decode(UInt64.self) + self.append(Element(UInt(truncatingIfNeeded: v))) + self.append(Element(UInt(truncatingIfNeeded: v &>> 32))) + } + } +} diff --git a/Sources/CMakeLists.txt b/Sources/CMakeLists.txt index fa125f86c..2ab9a5942 100644 --- a/Sources/CMakeLists.txt +++ b/Sources/CMakeLists.txt @@ -1,12 +1,17 @@ #[[ This source file is part of the Swift Collections Open Source Project -Copyright (c) 2021 Apple Inc. and the Swift project authors +Copyright (c) 2021 - 2024 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 #]] +add_subdirectory(BitCollections) add_subdirectory(Collections) add_subdirectory(DequeModule) +add_subdirectory(HashTreeCollections) +add_subdirectory(HeapModule) add_subdirectory(OrderedCollections) +add_subdirectory(RopeModule) +add_subdirectory(_CollectionsUtilities) diff --git a/Sources/Collections/CMakeLists.txt b/Sources/Collections/CMakeLists.txt index aadba16a4..f8be93bd3 100644 --- a/Sources/Collections/CMakeLists.txt +++ b/Sources/Collections/CMakeLists.txt @@ -1,17 +1,21 @@ #[[ This source file is part of the Swift Collections Open Source Project -Copyright (c) 2021 Apple Inc. and the Swift project authors +Copyright (c) 2021 - 2024 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 #]] add_library(Collections - Collections.swift) + "Collections.swift") target_link_libraries(Collections PRIVATE + BitCollections DequeModule - OrderedCollections) + HeapModule + OrderedCollections + HashTreeCollections + ) set_target_properties(Collections PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) diff --git a/Sources/Collections/Collections.docc/Collections.md b/Sources/Collections/Collections.docc/Collections.md index 1d2cc0130..733249b7f 100644 --- a/Sources/Collections/Collections.docc/Collections.md +++ b/Sources/Collections/Collections.docc/Collections.md @@ -2,46 +2,37 @@ **Swift Collections** is an open-source package of data structure implementations for the Swift programming language. -The package currently provides the following implementations: +## Overview -- ``Deque``, a double-ended queue backed by a ring buffer. Deques are range-replaceable, mutable, random-access collections. -- ``OrderedSet``, a variant of the standard `Set` where the order of items is well-defined and items can be arbitrarily reordered. Uses a `ContiguousArray` as its backing store, augmented by a separate hash table of bit packed offsets into it. - -- ``OrderedDictionary``, an ordered variant of the standard `Dictionary`, providing similar benefits. - -## Modules - -This package provides separate products for each group of data structures it implements: - -- ``Collections``. This is an umbrella module that exports every other public module in the package. -- ``DequeModule``. Defines ``Deque``. -- ``OrderedCollections``. Defines the ordered collection types ``OrderedSet`` and ``OrderedDictionary``. - -If you aren't constrained by code size limitations, then importing ``Collections`` is the simplest way to start using the package. - -```swift -import Collections - -var deque: Deque = ["Ted", "Rebecca"] -deque.prepend("Keeley") - -let people = OrderedSet(deque) -people.contains("Rebecca") // true -``` #### Additional Resources - [`Swift Collections` on GitHub](https://github.com/apple/swift-collections/) - [`Swift Collections` on the Swift Forums](https://forums.swift.org/c/related-projects/collections/72) + ## Topics +### Bit Collections + +- ``BitSet`` +- ``BitArray`` + ### Deque Module - ``Deque`` +### Heap Module + +- ``Heap`` + ### Ordered Collections - ``OrderedSet`` - ``OrderedDictionary`` + +### Persistent Hashed Collections + +- ``TreeSet`` +- ``TreeDictionary`` diff --git a/Sources/Collections/Collections.docc/Extensions/BitArray.md b/Sources/Collections/Collections.docc/Extensions/BitArray.md new file mode 100644 index 000000000..96957cf6e --- /dev/null +++ b/Sources/Collections/Collections.docc/Extensions/BitArray.md @@ -0,0 +1,85 @@ +# ``Collections/BitArray`` + + + + + + + + +## Topics + +### Creating a Bit Array + +- ``init()`` +- ``init(minimumCapacity:)`` +- ``init(_:)-2y0wv`` +- ``init(repeating:count:)-4j5yd`` +- ``init(_:)-6ldyw`` +- ``init(_:)-4tksd`` +- ``init(_:)-765d2`` +- ``init(bitPattern:)`` +- ``randomBits(count:)`` +- ``randomBits(count:using:)`` + +### Accessing Elements + +- ``subscript(_:)-51ccj`` + +- ``first`` +- ``last`` + +### Adding Elements + +- ``append(_:)-8dqhn`` +- ``append(contentsOf:)-18dwf`` +- ``append(contentsOf:)-576q4`` +- ``append(contentsOf:)-8xkr8`` +- ``append(repeating:count:)`` +- ``insert(_:at:)-9t4hf`` +- ``insert(contentsOf:at:)-7e1xn`` +- ``insert(contentsOf:at:)-35dp3`` +- ``insert(contentsOf:at:)-1wsgw`` +- ``insert(repeating:count:at:)`` +- ``truncateOrExtend(toCount:with:)`` + +### Removing Elements + +- ``remove(at:)-7ij12`` +- ``removeAll(keepingCapacity:)-5tkge`` +- ``removeAll(where:)-7tv7z`` +- ``removeSubrange(_:)-86ou8`` +- ``removeSubrange(_:)-18qe7`` +- ``removeLast()`` +- ``removeLast(_:)`` +- ``removeFirst()-dcsp`` +- ``removeFirst(_:)-9nqlo`` +- ``popLast()`` + +### Replacing Elements + +- ``fill(in:with:)-1lrlg`` +- ``fill(in:with:)-8sf1b`` +- ``fill(with:)`` +- ``replaceSubrange(_:with:)-163u2`` +- ``replaceSubrange(_:with:)-875d8`` +- ``replaceSubrange(_:with:)-2i7lu`` +- ``replaceSubrange(_:with:)-b5ou`` + +### Bitwise Operations + +- ``toggleAll()`` +- ``toggleAll(in:)-3duwn`` +- ``toggleAll(in:)-5hfhl`` +- ``maskingShiftLeft(by:)`` +- ``maskingShiftRight(by:)`` +- ``resizingShiftLeft(by:)`` +- ``resizingShiftRight(by:)`` + + + + + + + + diff --git a/Sources/Collections/Collections.docc/Extensions/BitSet.Counted.md b/Sources/Collections/Collections.docc/Extensions/BitSet.Counted.md new file mode 100644 index 000000000..d5b23ba92 --- /dev/null +++ b/Sources/Collections/Collections.docc/Extensions/BitSet.Counted.md @@ -0,0 +1,128 @@ +# ``Collections/BitSet/Counted-swift.struct`` + + + + + + + + +## Topics + +### Collection Views + +- ``uncounted`` + +### Creating a Set + +- ``init()`` +- ``init(reservingCapacity:)`` +- ``init(_:)-15cws`` +- ``init(_:)-38hho`` +- ``init(_:)-2of3i`` +- ``init(_:)-5fhls`` +- ``init(bitPattern:)`` +- ``init(words:)`` +- ``random(upTo:)`` +- ``random(upTo:using:)`` + +### Finding Elements + +- ``contains(_:)`` +- ``firstIndex(of:)`` +- ``lastIndex(of:)`` + +### Adding and Updating Elements + +- ``insert(_:)`` +- ``update(with:)`` + +### Removing Elements + +- ``filter(_:)`` +- ``remove(_:)`` +- ``remove(at:)`` + +### Sorted Set Operations + +- ``subscript(member:)`` +- ``subscript(members:)-5nkxk`` +- ``subscript(members:)-5xfq5`` +- ``min()`` +- ``max()`` +- ``sorted()`` + +### Binary Set Operations + +- ``intersection(_:)-1wfb5`` +- ``intersection(_:)-4evdp`` +- ``intersection(_:)-9rtcc`` +- ``intersection(_:)-13us`` + +- ``union(_:)-2okwt`` +- ``union(_:)-pwqf`` +- ``union(_:)-18u31`` +- ``union(_:)-8ysz9`` + +- ``subtracting(_:)-7u4tf`` +- ``subtracting(_:)-5vgml`` +- ``subtracting(_:)-6scy1`` +- ``subtracting(_:)-82loi`` + +- ``symmetricDifference(_:)-84e40`` +- ``symmetricDifference(_:)-3suo3`` +- ``symmetricDifference(_:)-7zx5q`` +- ``symmetricDifference(_:)-46ni1`` + +- ``formIntersection(_:)-49and`` +- ``formIntersection(_:)-49a0x`` +- ``formIntersection(_:)-79anv`` +- ``formIntersection(_:)-3zoc4`` + +- ``formUnion(_:)-c6a3`` +- ``formUnion(_:)-c5kv`` +- ``formUnion(_:)-2f05x`` +- ``formUnion(_:)-8kilf`` + +- ``subtract(_:)-2hzty`` +- ``subtract(_:)-2i1qq`` +- ``subtract(_:)-32jtb`` +- ``subtract(_:)-75xgt`` + +- ``formSymmetricDifference(_:)-6vskl`` +- ``formSymmetricDifference(_:)-6vs05`` +- ``formSymmetricDifference(_:)-d2kd`` +- ``formSymmetricDifference(_:)-54ghn`` + +### Binary Set Predicates + +- ``==(_:_:)`` +- ``isEqualSet(to:)-11031`` +- ``isEqualSet(to:)-1hvpp`` +- ``isEqualSet(to:)-1mvpq`` +- ``isEqualSet(to:)-878x1`` + +- ``isSubset(of:)-8iy8c`` +- ``isSubset(of:)-1r41b`` +- ``isSubset(of:)-1dz0p`` +- ``isSubset(of:)-3bq5m`` + +- ``isSuperset(of:)-48i5c`` +- ``isSuperset(of:)-10gu8`` +- ``isSuperset(of:)-8b7lq`` +- ``isSuperset(of:)-6slai`` + +- ``isStrictSubset(of:)-5ry1b`` +- ``isStrictSubset(of:)-2ndu3`` +- ``isStrictSubset(of:)-9iul0`` +- ``isStrictSubset(of:)-2pq1j`` + +- ``isStrictSuperset(of:)-9mgmd`` +- ``isStrictSuperset(of:)-6hw4t`` +- ``isStrictSuperset(of:)-1ya0j`` +- ``isStrictSuperset(of:)-4qt1e`` + +- ``isDisjoint(with:)-9wyku`` +- ``isDisjoint(with:)-5fww0`` +- ``isDisjoint(with:)-6p0t7`` +- ``isDisjoint(with:)-eujj`` diff --git a/Sources/Collections/Collections.docc/Extensions/BitSet.md b/Sources/Collections/Collections.docc/Extensions/BitSet.md new file mode 100644 index 000000000..f65ddf220 --- /dev/null +++ b/Sources/Collections/Collections.docc/Extensions/BitSet.md @@ -0,0 +1,133 @@ +# ``Collections/BitSet`` + + + + + + + + +## Topics + +### Creating a Bit Set + +- ``init()`` +- ``init(reservingCapacity:)`` +- ``init(_:)-15cws`` +- ``init(_:)-38hho`` +- ``init(_:)-2of3i`` +- ``init(_:)-5fhls`` +- ``init(bitPattern:)`` +- ``init(words:)`` +- ``random(upTo:)`` +- ``random(upTo:using:)`` + +### Finding Elements + +- ``contains(_:)`` +- ``firstIndex(of:)`` +- ``lastIndex(of:)`` + +### Adding and Updating Elements + +- ``insert(_:)`` +- ``update(with:)`` + +### Removing Elements + +- ``filter(_:)`` +- ``remove(_:)`` +- ``remove(at:)`` + +### Sorted Set Operations + +- ``subscript(member:)`` +- ``subscript(members:)-5nkxk`` +- ``subscript(members:)-5xfq5`` +- ``min()`` +- ``max()`` +- ``sorted()`` + +### Combining Sets + +- ``intersection(_:)-84q4u`` +- ``intersection(_:)-8hcl9`` +- ``intersection(_:)-7l8p3`` +- ``intersection(_:)-7kgi`` + +- ``union(_:)-5kqmx`` +- ``union(_:)-6mj8`` +- ``union(_:)-50wc4`` +- ``union(_:)-10had`` + +- ``subtracting(_:)-79e0o`` +- ``subtracting(_:)-7re82`` +- ``subtracting(_:)-7rn26`` +- ``subtracting(_:)-42s7d`` + +- ``symmetricDifference(_:)-55kqn`` +- ``symmetricDifference(_:)-5xt65`` +- ``symmetricDifference(_:)-91kh8`` +- ``symmetricDifference(_:)-79wfx`` + +- ``formIntersection(_:)-u07v`` +- ``formIntersection(_:)-87gjl`` +- ``formIntersection(_:)-9gffv`` +- ``formIntersection(_:)-8t2je`` + +- ``formUnion(_:)-72o7q`` +- ``formUnion(_:)-370hb`` +- ``formUnion(_:)-7tw8j`` +- ``formUnion(_:)-12ll3`` + +- ``subtract(_:)-9aabm`` +- ``subtract(_:)-1o083`` +- ``subtract(_:)-6kijg`` +- ``subtract(_:)-3pynh`` + +- ``formSymmetricDifference(_:)-2le2k`` +- ``formSymmetricDifference(_:)-5edyr`` +- ``formSymmetricDifference(_:)-7wole`` +- ``formSymmetricDifference(_:)-8vcnf`` + +### Comparing Sets + +- ``==(_:_:)`` +- ``isEqualSet(to:)-4xfa9`` +- ``isEqualSet(to:)-359ao`` +- ``isEqualSet(to:)-5ap6y`` +- ``isEqualSet(to:)-2dezf`` + +- ``isSubset(of:)-73apg`` +- ``isSubset(of:)-14xt1`` +- ``isSubset(of:)-4mj71`` +- ``isSubset(of:)-20wxs`` + +- ``isSuperset(of:)-1mfg2`` +- ``isSuperset(of:)-5adir`` +- ``isSuperset(of:)-4y68t`` +- ``isSuperset(of:)-2m7mj`` + +- ``isStrictSubset(of:)-8m1z6`` +- ``isStrictSubset(of:)-3y2l1`` +- ``isStrictSubset(of:)-97rky`` +- ``isStrictSubset(of:)-p3zj`` + +- ``isStrictSuperset(of:)-6e5gm`` +- ``isStrictSuperset(of:)-735zn`` +- ``isStrictSuperset(of:)-26acy`` +- ``isStrictSuperset(of:)-5jmxx`` + +- ``isDisjoint(with:)-2cdg6`` +- ``isDisjoint(with:)-3klxy`` +- ``isDisjoint(with:)-4uidy`` +- ``isDisjoint(with:)-78a8w`` + +### Memory Management + +- ``reserveCapacity(_:)`` + +### Collection Views + +- ``Counted-swift.struct`` +- ``counted-swift.property`` diff --git a/Sources/Collections/Collections.docc/Extensions/Deque.md b/Sources/Collections/Collections.docc/Extensions/Deque.md index c65ac432a..b314f9559 100644 --- a/Sources/Collections/Collections.docc/Extensions/Deque.md +++ b/Sources/Collections/Collections.docc/Extensions/Deque.md @@ -3,65 +3,8 @@ -## Topics - -### Creating a Deque - -- ``init()`` -- ``init(_:)-8tyaw`` -- ``init(_:)-1tqf4`` -- ``init(minimumCapacity:)`` -- ``init(repeating:count:)-4v1gt`` -- ``init(unsafeUninitializedCapacity:initializingWith:)`` - -### Inspecting a Deque - -- ``count-8wcnm`` -- ``isEmpty`` - -### Manipulating Elements at the Front - -- ``first`` -- ``prepend(_:)`` -- ``prepend(contentsOf:)-96y15`` -- ``prepend(contentsOf:)-51zn6`` -- ``removeFirst()-1vdmt`` -- ``removeFirst(_:)-2vuji`` -- ``popFirst()`` -- ``prefix(_:)`` + -### Manipulating Elements at the End + -- ``last`` -- ``append(_:)-9h4m7`` -- ``append(contentsOf:)-8rqnl`` -- ``append(contentsOf:)-29aoh`` -- ``removeLast()`` -- ``removeLast(_:)`` -- ``popLast()`` -- ``suffix(_:)`` - -### Manipulating Elements Elsewhere - -- ``subscript(_:)-9nk44`` -- ``subscript(_:)-6ee8i`` -- ``subscript(_:)-1klky`` -- ``subscript(_:)-ejld`` -- ``insert(_:at:)-9hsp7`` -- ``insert(contentsOf:at:)-1d60f`` -- ``replaceSubrange(_:with:)-5rtzd`` -- ``remove(at:)-3imgi`` - -### Reordering Elements - -- ``swapAt(_:_:)-7910s`` -- ``sort()`` -- ``sort(by:)`` -- ``reverse()`` -- ``shuffle()`` -- ``shuffle(using:)`` -- ``partition(by:)-90y0t`` - -### Memory Management - -- ``reserveCapacity(_:)`` +## Topics diff --git a/Sources/Collections/Collections.docc/Extensions/Heap.md b/Sources/Collections/Collections.docc/Extensions/Heap.md new file mode 100644 index 000000000..8c0136f13 --- /dev/null +++ b/Sources/Collections/Collections.docc/Extensions/Heap.md @@ -0,0 +1,10 @@ +# ``Collections/Heap`` + + + + + + + + +## Topics diff --git a/Sources/Collections/Collections.docc/Extensions/OrderedSet.UnorderedView.md b/Sources/Collections/Collections.docc/Extensions/OrderedSet.UnorderedView.md index 6e2681dce..13404810b 100644 --- a/Sources/Collections/Collections.docc/Extensions/OrderedSet.UnorderedView.md +++ b/Sources/Collections/Collections.docc/Extensions/OrderedSet.UnorderedView.md @@ -3,6 +3,10 @@ + + + + ## Topics ### Binary Set Operations @@ -34,6 +38,8 @@ ### Binary Set Predicates - ``==(_:_:)`` +- ``isEqualSet(to:)-1szq`` +- ``isEqualSet(to:)-9djqq`` - ``isSubset(of:)-2dx31`` - ``isSubset(of:)-801lo`` diff --git a/Sources/Collections/Collections.docc/Extensions/OrderedSet.md b/Sources/Collections/Collections.docc/Extensions/OrderedSet.md index a47d51f19..a6ce70025 100644 --- a/Sources/Collections/Collections.docc/Extensions/OrderedSet.md +++ b/Sources/Collections/Collections.docc/Extensions/OrderedSet.md @@ -3,6 +3,10 @@ + + + + ## Topics ### Creating a Set @@ -33,8 +37,8 @@ ### Adding and Updating Elements - ``append(_:)`` -- ``insert(_:at:)`` - ``append(contentsOf:)`` +- ``insert(_:at:)`` - ``updateOrAppend(_:)`` - ``updateOrInsert(_:at:)`` - ``update(_:at:)`` @@ -90,6 +94,9 @@ ### Comparing Sets - ``==(_:_:)`` +- ``isEqualSet(to:)-6zqj7`` +- ``isEqualSet(to:)-34yz0`` +- ``isEqualSet(to:)-2bhxr`` - ``isSubset(of:)-ptij`` - ``isSubset(of:)-3mw6r`` diff --git a/Sources/Collections/Collections.docc/Extensions/TreeDictionary.md b/Sources/Collections/Collections.docc/Extensions/TreeDictionary.md new file mode 100644 index 000000000..23aeb80d7 --- /dev/null +++ b/Sources/Collections/Collections.docc/Extensions/TreeDictionary.md @@ -0,0 +1,86 @@ +# ``Collections/TreeDictionary`` + + + + + + + + +## Topics + +### Collection Views + +`TreeDictionary` provides the customary dictionary views, `keys` and +`values`. These are collection types that are projections of the dictionary +itself, with elements that match only the keys or values of the dictionary, +respectively. The `Keys` view is notable in that it provides operations for +subtracting and intersecting the keys of two dictionaries, allowing for easy +detection of inserted and removed items between two snapshots of the same +dictionary. Because `TreeDictionary` needs to invalidate indices on every +mutation, its `Values` view is not a `MutableCollection`. + +- ``Keys-swift.struct`` +- ``Values-swift.struct`` +- ``keys-swift.property`` +- ``values-swift.property`` + +### Creating a Dictionary + +- ``init()`` +- ``init(_:)-111p1`` +- ``init(_:)-9atjh`` +- ``init(uniqueKeysWithValues:)-2hosl`` +- ``init(uniqueKeysWithValues:)-92276`` +- ``init(_:uniquingKeysWith:)-6nofo`` +- ``init(_:uniquingKeysWith:)-99403`` +- ``init(grouping:by:)-a4ma`` +- ``init(grouping:by:)-4he86`` +- ``init(keys:valueGenerator:)`` + + +### Inspecting a Dictionary + +- ``isEmpty-6icj0`` +- ``count-ibl8`` + +### Accessing Keys and Values + +- ``subscript(_:)-8gx3j`` +- ``subscript(_:default:)`` +- ``index(forKey:)`` + +### Adding or Updating Keys and Values + +Beyond the standard `updateValue(_:forKey:)` method, `TreeDictionary` also +provides additional `updateValue` variants that take closure arguments. These +provide a more straightforward way to perform in-place mutations on dictionary +values (compared to mutating values through the corresponding subscript +operation.) `TreeDictionary` also provides the standard `merge` and +`merging` operations for combining dictionary values. + +- ``updateValue(_:forKey:)`` +- ``updateValue(forKey:with:)`` +- ``updateValue(forKey:default:with:)`` +- ``merge(_:uniquingKeysWith:)-59cm5`` +- ``merge(_:uniquingKeysWith:)-38axt`` +- ``merge(_:uniquingKeysWith:)-3s4cw`` +- ``merging(_:uniquingKeysWith:)-3khxe`` +- ``merging(_:uniquingKeysWith:)-1k63w`` +- ``merging(_:uniquingKeysWith:)-87wp7`` + +### Removing Keys and Values + +- ``removeValue(forKey:)`` +- ``remove(at:)`` +- ``filter(_:)`` + +### Comparing Dictionaries + +- ``==(_:_:)`` + +### Transforming a Dictionary + +- ``mapValues(_:)`` +- ``compactMapValues(_:)`` + diff --git a/Sources/Collections/Collections.docc/Extensions/TreeSet.md b/Sources/Collections/Collections.docc/Extensions/TreeSet.md new file mode 100644 index 000000000..931cbab25 --- /dev/null +++ b/Sources/Collections/Collections.docc/Extensions/TreeSet.md @@ -0,0 +1,174 @@ +# ``Collections/TreeSet`` + + + + +### Implementation Details + +`TreeSet` and `TreeDictionary` are based on a Swift adaptation +of the *Compressed Hash-Array Mapped Prefix Tree* (CHAMP) data structure. + +- Michael J Steindorfer and Jurgen J Vinju. Optimizing Hash-Array Mapped + Tries for Fast and Lean Immutable JVM Collections. In *Proc. + International Conference on Object-Oriented Programming, Systems, + Languages, and Applications,* pp. 783-800, 2015. + https://doi.org/10.1145/2814270.2814312 + +In this setup, the members of such a collection are organized into a tree +data structure based on their hash values. For example, assuming 16 bit hash +values sliced into 4-bit chunks, each node in the prefix tree would have +sixteen slots (one for each digit), each of which may contain a member, a +child node reference, or it may be empty. A `TreeSet` containing the +three items `Maximo`, `Julia` and `Don Pablo` (with hash values of `0x2B65`, +`0xA69F` and `0xADA1`, respectively) may be organized into a prefix tree of +two nodes: + +``` +┌0┬1┬2───────┬3┬4┬5┬6┬7┬8┬9┬A──┬B┬C┬D┬E┬F┐ +│ │ │ Maximo │ │ │ │ │ │ │ │ • │ │ │ │ │ │ +└─┴─┴────────┴─┴─┴─┴─┴─┴─┴─┴─┼─┴─┴─┴─┴─┴─┘ + ╎ + ╎ + ┌0┬1┬2┬3┬4┬5┬6───┴──┬7┬8┬9┬A┬B┬C┬D──────────┬E┬F┐ + │ │ │ │ │ │ │ Julia │ │ │ │ │ │ │ Don Pablo │ │ │ + └─┴─┴─┴─┴─┴─┴───────┴─┴─┴─┴─┴─┴─┴───────────┴─┴─┘ +``` + +The root node directly contains `Maximo`, because it is the only set member +whose hash value starts with `2`. However, the first digits of the hashes of +`Julia` and `Don Pablo` are both `A`, so these items reside in a separate +node, one level below the root. + +(To save space, nodes are actually stored in a more compact form, with just +enough space allocated to store their contents: empty slots do not take up +any room. Hence the term "compressed" in "Compressed Hash-Array Mapped +Prefix Tree".) + +The resulting tree structure lends itself well to sharing nodes across +multiple collection values. Inserting or removing an item in a completely +shared tree requires copying at most log(n) nodes -- every node along the +path to the item needs to be uniqued, but all other nodes can remain shared. +While the cost of copying this many nodes isn't trivial, it is dramatically +lower than the cost of having to copy the entire data structure, like the +standard `Set` has to do. + +When looking up a particular member, we descend from the root node, +following along the path specified by successive digits of the member's hash +value. As long as hash values are unique, we will either find the member +we're looking for, or we will know for sure that it does not exist in the +set. + +In practice, hash values aren't guaranteed to be unique though. Members with +conflicting hash values need to be collected in special collision nodes that +are able to grow as large as necessary to contain all colliding members that +share the same hash. Looking up a member in one of these nodes requires a +linear search, so it is crucial that such collisions do not happen often. + +As long as `Element` properly implements `Hashable`, lookup operations in a +`TreeSet` are expected to be able to decide whether the set contains a +particular item by looking at no more than a constant number of items on +average -- typically they will need to compare against just one member. + +## Topics + +### Creating a Set + +- ``init()`` +- ``init(_:)-2uun3`` +- ``init(_:)-714nu`` +- ``init(_:)-6lt4a`` + +### Finding Elements + +- ``contains(_:)`` +- ``firstIndex(of:)`` +- ``lastIndex(of:)`` + +### Adding and Updating Elements + +- ``insert(_:)`` +- ``update(with:)`` +- ``update(_:at:)`` + +### Removing Elements + +- ``remove(_:)`` +- ``remove(at:)`` +- ``filter(_:)`` +- ``removeAll(where:)`` + +### Combining Sets + +All the standard combining operations (intersection, union, subtraction and +symmetric difference) are supported, in both non-mutating and mutating forms. +`SetAlgebra` only requires the ability to combine one set instance with another, +but `TreeSet` follows the tradition established by `Set` in providing +additional overloads to each operation that allow combining a set with +additional types, including arbitrary sequences. + +- ``intersection(_:)-8ltpr`` +- ``intersection(_:)-9kwc0`` +- ``intersection(_:)-4u7ew`` + +- ``union(_:)-89jj2`` +- ``union(_:)-9yvze`` +- ``union(_:)-7p0m2`` + +- ``subtracting(_:)-cnsi`` +- ``subtracting(_:)-3yfac`` +- ``subtracting(_:)-90wrb`` + +- ``symmetricDifference(_:)-5bz4f`` +- ``symmetricDifference(_:)-6p8n5`` +- ``symmetricDifference(_:)-3qk9w`` + +- ``formIntersection(_:)-1zcar`` +- ``formIntersection(_:)-4xkf0`` +- ``formIntersection(_:)-6jb2z`` + +- ``formUnion(_:)-420zl`` +- ``formUnion(_:)-8zu6q`` +- ``formUnion(_:)-423id`` + +- ``subtract(_:)-49o9`` +- ``subtract(_:)-3ebkc`` +- ``subtract(_:)-87rhs`` + +- ``formSymmetricDifference(_:)-94f6x`` +- ``formSymmetricDifference(_:)-4x7vw`` +- ``formSymmetricDifference(_:)-6ypuy`` + +### Comparing Sets + +`TreeSet` supports all standard set comparisons (subset tests, superset +tests, disjunctness test), including the customary overloads established by +`Set`. As an additional extension, the `isEqualSet` family of member functions +generalize the standard `==` operation to support checking whether a +`TreeSet` consists of exactly the same members as an arbitrary sequence. +Like `==`, the `isEqualSet` functions ignore element ordering and duplicates (if +any). + +- ``==(_:_:)`` +- ``isEqualSet(to:)-4bc1i`` +- ``isEqualSet(to:)-7x4yi`` +- ``isEqualSet(to:)-44fkf`` + +- ``isSubset(of:)-2ktpu`` +- ``isSubset(of:)-5oufi`` +- ``isSubset(of:)-9tq5c`` + +- ``isSuperset(of:)-3zd41`` +- ``isSuperset(of:)-6xa75`` +- ``isSuperset(of:)-6vw4t`` + +- ``isStrictSubset(of:)-6xuil`` +- ``isStrictSubset(of:)-22f80`` +- ``isStrictSubset(of:)-5f78e`` + +- ``isStrictSuperset(of:)-4ryjr`` +- ``isStrictSuperset(of:)-3ephc`` +- ``isStrictSuperset(of:)-9ftlc`` + +- ``isDisjoint(with:)-4a9xa`` +- ``isDisjoint(with:)-12a64`` +- ``isDisjoint(with:)-5lvdr`` diff --git a/Sources/Collections/Collections.swift b/Sources/Collections/Collections.swift index 525b34281..33d5c8a1d 100644 --- a/Sources/Collections/Collections.swift +++ b/Sources/Collections/Collections.swift @@ -2,12 +2,19 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 // //===----------------------------------------------------------------------===// +#if !COLLECTIONS_SINGLE_MODULE +@_exported import BitCollections @_exported import DequeModule +@_exported import HashTreeCollections +@_exported import HeapModule @_exported import OrderedCollections +// Note: _RopeModule is very intentionally not reexported, as its contents +// aren't part of this package's stable API surface (yet). +#endif diff --git a/Sources/DequeModule/CMakeLists.txt b/Sources/DequeModule/CMakeLists.txt index 2006b241d..fa04ca91d 100644 --- a/Sources/DequeModule/CMakeLists.txt +++ b/Sources/DequeModule/CMakeLists.txt @@ -1,33 +1,33 @@ #[[ This source file is part of the Swift Collections Open Source Project -Copyright (c) 2021 Apple Inc. and the Swift project authors +Copyright (c) 2021 - 2024 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 #]] add_library(DequeModule - _DequeBuffer.swift - _DequeBufferHeader.swift - _DequeSlot.swift - _UnsafeWrappedBuffer.swift - Compatibility.swift - Deque._Storage.swift - Deque._UnsafeHandle.swift - Deque.swift - Deque+Codable.swift - Deque+Collection.swift - Deque+CustomDebugStringConvertible.swift - Deque+CustomReflectable.swift - Deque+CustomStringConvertible.swift - Deque+Equatable.swift - Deque+ExpressibleByArrayLiteral.swift - Deque+Extras.swift - Deque+Hashable.swift - Deque+Sendable.swift - Deque+Testing.swift - UnsafeMutableBufferPointer+Utilities.swift) + "Deque+Codable.swift" + "Deque+Collection.swift" + "Deque+CustomReflectable.swift" + "Deque+Descriptions.swift" + "Deque+Equatable.swift" + "Deque+ExpressibleByArrayLiteral.swift" + "Deque+Extras.swift" + "Deque+Hashable.swift" + "Deque+Sendable.swift" + "Deque+Testing.swift" + "Deque._Storage.swift" + "Deque._UnsafeHandle.swift" + "Deque.swift" + "_DequeBuffer.swift" + "_DequeBufferHeader.swift" + "_DequeSlot.swift" + "_UnsafeWrappedBuffer.swift" + ) +target_link_libraries(DequeModule PRIVATE + _CollectionsUtilities) set_target_properties(DequeModule PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) diff --git a/Sources/DequeModule/Compatibility.swift b/Sources/DequeModule/Compatibility.swift deleted file mode 100644 index 8ece8f6f8..000000000 --- a/Sources/DequeModule/Compatibility.swift +++ /dev/null @@ -1,62 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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( - _ body: (UnsafeBufferPointer) throws -> R - ) rethrows -> R? { - if Self.self == Array.self && Array._isWCSIABroken() { - return nil - } - - return try self.withContiguousStorageIfAvailable(body) - } -} diff --git a/Sources/DequeModule/Deque+Codable.swift b/Sources/DequeModule/Deque+Codable.swift index 2d98587c3..abef341ea 100644 --- a/Sources/DequeModule/Deque+Codable.swift +++ b/Sources/DequeModule/Deque+Codable.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 diff --git a/Sources/DequeModule/Deque+Collection.swift b/Sources/DequeModule/Deque+Collection.swift index 2b8b912a4..5eed96098 100644 --- a/Sources/DequeModule/Deque+Collection.swift +++ b/Sources/DequeModule/Deque+Collection.swift @@ -2,13 +2,17 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021-2023 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 // //===----------------------------------------------------------------------===// +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + extension Deque: Sequence { // Implementation note: we could also use the default `IndexingIterator` here. // This custom implementation performs direct storage access to eliminate any @@ -104,10 +108,10 @@ extension Deque: Sequence { _storage.read { source in let segments = source.segments() let c = segments.first.count - target[.. c1, let second = segments.second else { return (Iterator(_base: self, from: c1), c1) } let c2 = Swift.min(second.count, target.count - c1) - target[c1 ..< c1 + c2]._rebased()._initialize(from: second.prefix(c2)._rebased()) + target[c1 ..< c1 + c2].initializeAll(fromContentsOf: second.prefix(c2)) return (Iterator(_base: self, from: c1 + c2), c1 + c2) } } @@ -160,9 +164,7 @@ extension Deque: Sequence { } } -#if swift(>=5.5) extension Deque.Iterator: Sendable where Element: Sendable {} -#endif extension Deque: RandomAccessCollection { public typealias Index = Int @@ -542,10 +544,10 @@ extension Deque: RangeReplaceableCollection { /// items that need to be moved by shifting elements either before or after /// `subrange`. @inlinable - public mutating func replaceSubrange( + public mutating func replaceSubrange( _ subrange: Range, - with newElements: __owned C - ) where C.Element == Element { + with newElements: __owned some Collection + ) { precondition(subrange.lowerBound >= 0 && subrange.upperBound <= count, "Index range out of bounds") let removalCount = subrange.count let insertionCount = newElements.count @@ -602,7 +604,7 @@ extension Deque: RangeReplaceableCollection { /// /// - Complexity: O(*n*), where *n* is the number of elements in the sequence. @inlinable - public init(_ elements: S) where S.Element == Element { + public init(_ elements: some Sequence) { self.init() self.append(contentsOf: elements) } @@ -614,18 +616,18 @@ extension Deque: RangeReplaceableCollection { /// /// - Complexity: O(`elements.count`) @inlinable - public init(_ elements: C) where C.Element == Element { + public init(_ elements: some Collection) { let c = elements.count guard c > 0 else { _storage = _Storage(); return } self._storage = _Storage(minimumCapacity: c) _storage.update { handle in assert(handle.startSlot == .zero) let target = handle.mutableBuffer(for: .zero ..< _Slot(at: c)) - let done: Void? = elements._withContiguousStorageIfAvailable_SR14663 { source in - target._initialize(from: source) + let done: Void? = elements.withContiguousStorageIfAvailable { source in + target.initializeAll(fromContentsOf: source) } if done == nil { - target._initialize(from: elements) + target.initializeAll(fromContentsOf: elements) } handle.count = c } @@ -677,8 +679,8 @@ extension Deque: RangeReplaceableCollection { /// /// - Complexity: Amortized O(`newElements.count`). @inlinable - public mutating func append(contentsOf newElements: S) where S.Element == Element { - let done: Void? = newElements._withContiguousStorageIfAvailable_SR14663 { source in + public mutating func append(contentsOf newElements: some Sequence) { + let done: Void? = newElements.withContiguousStorageIfAvailable { source in _storage.ensureUnique(minimumCapacity: count + source.count) _storage.update { $0.uncheckedAppend(contentsOf: source) } } @@ -688,7 +690,7 @@ extension Deque: RangeReplaceableCollection { let underestimatedCount = newElements.underestimatedCount _storage.ensureUnique(minimumCapacity: count + underestimatedCount) - var it: S.Iterator = _storage.update { target in + var it = _storage.update { target in let gaps = target.availableSegments() let (it, copied) = gaps.initialize(fromSequencePrefix: newElements) target.count += copied @@ -719,8 +721,10 @@ extension Deque: RangeReplaceableCollection { /// /// - Complexity: Amortized O(`newElements.count`). @inlinable - public mutating func append(contentsOf newElements: C) where C.Element == Element { - let done: Void? = newElements._withContiguousStorageIfAvailable_SR14663 { source in + public mutating func append( + contentsOf newElements: some Collection + ) { + let done: Void? = newElements.withContiguousStorageIfAvailable { source in _storage.ensureUnique(minimumCapacity: count + source.count) _storage.update { $0.uncheckedAppend(contentsOf: source) } } @@ -789,9 +793,10 @@ extension Deque: RangeReplaceableCollection { /// inserting at the start or the end, this reduces the complexity to /// amortized O(1). @inlinable - public mutating func insert( - contentsOf newElements: __owned C, at index: Int - ) where C.Element == Element { + public mutating func insert( + contentsOf newElements: __owned some Collection, + at index: Int + ) { precondition(index >= 0 && index <= count, "Can't insert elements at an invalid index") let newCount = newElements.count diff --git a/Sources/DequeModule/Deque+CustomReflectable.swift b/Sources/DequeModule/Deque+CustomReflectable.swift index df9198562..472c2899d 100644 --- a/Sources/DequeModule/Deque+CustomReflectable.swift +++ b/Sources/DequeModule/Deque+CustomReflectable.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 diff --git a/Sources/DequeModule/Deque+CustomDebugStringConvertible.swift b/Sources/DequeModule/Deque+Descriptions.swift similarity index 62% rename from Sources/DequeModule/Deque+CustomDebugStringConvertible.swift rename to Sources/DequeModule/Deque+Descriptions.swift index 5a8e258fc..734f6c749 100644 --- a/Sources/DequeModule/Deque+CustomDebugStringConvertible.swift +++ b/Sources/DequeModule/Deque+Descriptions.swift @@ -2,27 +2,27 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 // //===----------------------------------------------------------------------===// +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension Deque: CustomStringConvertible { + /// A textual representation of this instance. + public var description: String { + _arrayDescription(for: self) + } +} + extension Deque: CustomDebugStringConvertible { /// A textual representation of this instance, suitable for debugging. public var debugDescription: String { - var result = "Deque<\(Element.self)>([" - var first = true - for item in self { - if first { - first = false - } else { - result += ", " - } - debugPrint(item, terminator: "", to: &result) - } - result += "])" - return result + description } } diff --git a/Sources/DequeModule/Deque+Equatable.swift b/Sources/DequeModule/Deque+Equatable.swift index 4def51959..4a068d7fa 100644 --- a/Sources/DequeModule/Deque+Equatable.swift +++ b/Sources/DequeModule/Deque+Equatable.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 @@ -26,7 +26,7 @@ extension Deque: Equatable where Element: Equatable { if lhsCount == 0 || left._storage.isIdentical(to: right._storage) { return true } - + return left.elementsEqual(right) } } diff --git a/Sources/DequeModule/Deque+ExpressibleByArrayLiteral.swift b/Sources/DequeModule/Deque+ExpressibleByArrayLiteral.swift index fd7125c90..8028938b1 100644 --- a/Sources/DequeModule/Deque+ExpressibleByArrayLiteral.swift +++ b/Sources/DequeModule/Deque+ExpressibleByArrayLiteral.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 diff --git a/Sources/DequeModule/Deque+Extras.swift b/Sources/DequeModule/Deque+Extras.swift index fd1c1487b..4d7d126f8 100644 --- a/Sources/DequeModule/Deque+Extras.swift +++ b/Sources/DequeModule/Deque+Extras.swift @@ -2,13 +2,17 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 // //===----------------------------------------------------------------------===// +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + extension Deque { /// Creates a deque with the specified capacity, then calls the given /// closure with a buffer covering the array's uninitialized memory. @@ -130,8 +134,10 @@ extension Deque { /// /// - SeeAlso: `append(contentsOf:)` @inlinable - public mutating func prepend(contentsOf newElements: C) where C.Element == Element { - let done: Void? = newElements._withContiguousStorageIfAvailable_SR14663 { source in + public mutating func prepend( + contentsOf newElements: some Collection + ) { + let done: Void? = newElements.withContiguousStorageIfAvailable { source in _storage.ensureUnique(minimumCapacity: count + source.count) _storage.update { $0.uncheckedPrepend(contentsOf: source) } } @@ -165,8 +171,8 @@ extension Deque { /// /// - SeeAlso: `append(contentsOf:)` @inlinable - public mutating func prepend(contentsOf newElements: S) where S.Element == Element { - let done: Void? = newElements._withContiguousStorageIfAvailable_SR14663 { source in + public mutating func prepend(contentsOf newElements: some Sequence) { + let done: Void? = newElements.withContiguousStorageIfAvailable { source in _storage.ensureUnique(minimumCapacity: count + source.count) _storage.update { $0.uncheckedPrepend(contentsOf: source) } } diff --git a/Sources/DequeModule/Deque+Hashable.swift b/Sources/DequeModule/Deque+Hashable.swift index de050c901..863688486 100644 --- a/Sources/DequeModule/Deque+Hashable.swift +++ b/Sources/DequeModule/Deque+Hashable.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 diff --git a/Sources/DequeModule/Deque+Sendable.swift b/Sources/DequeModule/Deque+Sendable.swift index 0a7da7f44..882e921e8 100644 --- a/Sources/DequeModule/Deque+Sendable.swift +++ b/Sources/DequeModule/Deque+Sendable.swift @@ -2,13 +2,11 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2022-2023 Apple Inc. and the Swift project authors +// Copyright (c) 2022 - 2024 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 // //===----------------------------------------------------------------------===// -#if swift(>=5.5) extension Deque: @unchecked Sendable where Element: Sendable {} -#endif diff --git a/Sources/DequeModule/Deque+Testing.swift b/Sources/DequeModule/Deque+Testing.swift index 00692981c..ea26e278c 100644 --- a/Sources/DequeModule/Deque+Testing.swift +++ b/Sources/DequeModule/Deque+Testing.swift @@ -2,17 +2,31 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 // //===----------------------------------------------------------------------===// +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + // This file contains exported but non-public entry points to support clear box // testing. extension Deque { + /// True if consistency checking is enabled in the implementation of this + /// type, false otherwise. + /// + /// Documented performance promises are null and void when this property + /// returns true -- for example, operations that are documented to take + /// O(1) time might take O(*n*) time, or worse. + public static var _isConsistencyCheckingEnabled: Bool { + _isCollectionsInternalCheckingEnabled + } + /// The maximum number of elements this deque is currently able to store /// without reallocating its storage buffer. /// @@ -42,11 +56,11 @@ extension Deque { /// as public to allow exhaustive input/output tests for `Deque`'s members. /// This isn't intended to be used outside of `Deque`'s own test target. @_spi(Testing) - public init( + public init( _capacity capacity: Int, startSlot: Int, - contents: S - ) where S.Element == Element { + contents: some Sequence + ) { let contents = ContiguousArray(contents) precondition(capacity >= 0) precondition(startSlot >= 0 && (startSlot < capacity || (capacity == 0 && startSlot == 0))) @@ -61,9 +75,9 @@ extension Deque { storage.update { target in let segments = target.mutableSegments() let c = segments.first.count - segments.first._initialize(from: source.prefix(c)._rebased()) + segments.first.initializeAll(fromContentsOf: source.prefix(c)) if let second = segments.second { - second._initialize(from: source.dropFirst(c)._rebased()) + second.initializeAll(fromContentsOf: source.dropFirst(c)) } } } diff --git a/Sources/DequeModule/Deque._Storage.swift b/Sources/DequeModule/Deque._Storage.swift index 839a1add5..f44e9c168 100644 --- a/Sources/DequeModule/Deque._Storage.swift +++ b/Sources/DequeModule/Deque._Storage.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 diff --git a/Sources/DequeModule/Deque._UnsafeHandle.swift b/Sources/DequeModule/Deque._UnsafeHandle.swift index ed32110d3..8302b635f 100644 --- a/Sources/DequeModule/Deque._UnsafeHandle.swift +++ b/Sources/DequeModule/Deque._UnsafeHandle.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 diff --git a/Sources/DequeModule/Deque.swift b/Sources/DequeModule/Deque.swift index 51d8643fb..9d6667a36 100644 --- a/Sources/DequeModule/Deque.swift +++ b/Sources/DequeModule/Deque.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 diff --git a/Sources/DequeModule/DequeModule.docc/DequeModule.md b/Sources/DequeModule/DequeModule.docc/DequeModule.md index 0746973eb..98ad67f44 100644 --- a/Sources/DequeModule/DequeModule.docc/DequeModule.md +++ b/Sources/DequeModule/DequeModule.docc/DequeModule.md @@ -1,3 +1,8 @@ # ``DequeModule`` -This module is dedicated to providing ``Deque``, a double-ended analogue of the standard `Array` type. +Summary + +## Overview + +Text + diff --git a/Sources/DequeModule/DequeModule.docc/Extensions/Deque.md b/Sources/DequeModule/DequeModule.docc/Extensions/Deque.md index 4d3a8362e..c262c77ff 100644 --- a/Sources/DequeModule/DequeModule.docc/Extensions/Deque.md +++ b/Sources/DequeModule/DequeModule.docc/Extensions/Deque.md @@ -1,64 +1,7 @@ # ``DequeModule/Deque`` -## Topics - -### Creating a Deque - -- ``init()`` -- ``init(_:)-8tyaw`` -- ``init(_:)-1tqf4`` -- ``init(minimumCapacity:)`` -- ``init(repeating:count:)-4v1gt`` -- ``init(unsafeUninitializedCapacity:initializingWith:)`` - -### Inspecting a Deque - -- ``count-8wcnm`` -- ``isEmpty`` - -### Manipulating Elements at the Front - -- ``first`` -- ``prepend(_:)`` -- ``prepend(contentsOf:)-96y15`` -- ``prepend(contentsOf:)-51zn6`` -- ``removeFirst()-1vdmt`` -- ``removeFirst(_:)-2vuji`` -- ``popFirst()`` -- ``prefix(_:)`` + -### Manipulating Elements at the End + -- ``last`` -- ``append(_:)-9h4m7`` -- ``append(contentsOf:)-8rqnl`` -- ``append(contentsOf:)-29aoh`` -- ``removeLast()`` -- ``removeLast(_:)`` -- ``popLast()`` -- ``suffix(_:)`` - -### Manipulating Elements Elsewhere - -- ``subscript(_:)-9nk44`` -- ``subscript(_:)-6ee8i`` -- ``subscript(_:)-1klky`` -- ``subscript(_:)-ejld`` -- ``insert(_:at:)-9hsp7`` -- ``insert(contentsOf:at:)-1d60f`` -- ``replaceSubrange(_:with:)-5rtzd`` -- ``remove(at:)-3imgi`` - -### Reordering Elements - -- ``swapAt(_:_:)-7910s`` -- ``sort()`` -- ``sort(by:)`` -- ``reverse()`` -- ``shuffle()`` -- ``shuffle(using:)`` -- ``partition(by:)-90y0t`` - -### Memory Management - -- ``reserveCapacity(_:)`` +## Topics diff --git a/Sources/DequeModule/UnsafeMutableBufferPointer+Utilities.swift b/Sources/DequeModule/UnsafeMutableBufferPointer+Utilities.swift deleted file mode 100644 index 95b2635aa..000000000 --- a/Sources/DequeModule/UnsafeMutableBufferPointer+Utilities.swift +++ /dev/null @@ -1,65 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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 Collection { - @inlinable - @inline(__always) - internal func _rebased() -> UnsafeBufferPointer - where Self == UnsafeBufferPointer.SubSequence { - .init(rebasing: self) - } -} - -extension Collection { - @inlinable - @inline(__always) - internal func _rebased() -> UnsafeMutableBufferPointer - where Self == UnsafeMutableBufferPointer.SubSequence { - .init(rebasing: self) - } -} - -extension UnsafeMutableBufferPointer { - @inlinable - @inline(__always) - internal func _initialize(from source: UnsafeBufferPointer) { - assert(source.count == count) - guard source.count > 0 else { return } - baseAddress!.initialize(from: source.baseAddress!, count: source.count) - } - - @inlinable - @inline(__always) - internal func _initialize( - from elements: C - ) where C.Element == Element { - assert(elements.count == count) - var (it, copied) = elements._copyContents(initializing: self) - precondition(copied == count) - precondition(it.next() == nil) - } - - @inlinable - @inline(__always) - internal func _deinitializeAll() { - guard count > 0 else { return } - baseAddress!.deinitialize(count: count) - } - - @inlinable - internal func _assign( - from replacement: C - ) where C.Element == Element { - guard self.count > 0 else { return } - self[0 ..< count]._rebased()._deinitializeAll() - _initialize(from: replacement) - } -} diff --git a/Sources/DequeModule/_DequeBuffer.swift b/Sources/DequeModule/_DequeBuffer.swift index bc0df278a..c7cd45bf4 100644 --- a/Sources/DequeModule/_DequeBuffer.swift +++ b/Sources/DequeModule/_DequeBuffer.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 diff --git a/Sources/DequeModule/_DequeBufferHeader.swift b/Sources/DequeModule/_DequeBufferHeader.swift index 37c0ff9c7..c45f756dd 100644 --- a/Sources/DequeModule/_DequeBufferHeader.swift +++ b/Sources/DequeModule/_DequeBufferHeader.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 diff --git a/Sources/DequeModule/_DequeSlot.swift b/Sources/DequeModule/_DequeSlot.swift index 321c2f878..2d281cb94 100644 --- a/Sources/DequeModule/_DequeSlot.swift +++ b/Sources/DequeModule/_DequeSlot.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 diff --git a/Sources/DequeModule/_UnsafeWrappedBuffer.swift b/Sources/DequeModule/_UnsafeWrappedBuffer.swift index 06a0a580c..b51a5caca 100644 --- a/Sources/DequeModule/_UnsafeWrappedBuffer.swift +++ b/Sources/DequeModule/_UnsafeWrappedBuffer.swift @@ -2,13 +2,17 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 // //===----------------------------------------------------------------------===// +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + @frozen @usableFromInline internal struct _UnsafeWrappedBuffer { @@ -72,6 +76,24 @@ internal struct _UnsafeMutableWrappedBuffer { assert(first.count > 0 || second == nil) } + @inlinable + @inline(__always) + internal init( + _ first: UnsafeMutableBufferPointer.SubSequence, + _ second: UnsafeMutableBufferPointer? = nil + ) { + self.init(UnsafeMutableBufferPointer(rebasing: first), second) + } + + @inlinable + @inline(__always) + internal init( + _ first: UnsafeMutableBufferPointer, + _ second: UnsafeMutableBufferPointer.SubSequence + ) { + self.init(first, UnsafeMutableBufferPointer(rebasing: second)) + } + @inlinable @inline(__always) internal init( @@ -113,9 +135,9 @@ extension _UnsafeMutableWrappedBuffer { return self } if n <= first.count { - return Self(first.prefix(n)._rebased()) + return Self(first.prefix(n)) } - return Self(first, second!.prefix(n - first.count)._rebased()) + return Self(first, second!.prefix(n - first.count)) } @inlinable @@ -125,20 +147,20 @@ extension _UnsafeMutableWrappedBuffer { return self } guard let second = second else { - return Self(first.suffix(n)._rebased()) + return Self(first.suffix(n)) } if n <= second.count { - return Self(second.suffix(n)._rebased()) + return Self(second.suffix(n)) } - return Self(first.suffix(n - second.count)._rebased(), second) + return Self(first.suffix(n - second.count), second) } } extension _UnsafeMutableWrappedBuffer { @inlinable internal func deinitialize() { - first._deinitializeAll() - second?._deinitializeAll() + first.deinitialize() + second?.deinitialize() } @inlinable @@ -177,7 +199,7 @@ extension _UnsafeMutableWrappedBuffer { // Note: Array._copyContents traps when not given enough space, so we // need to check if we have enough contiguous space available above. // - // FIXME: Add suppport for segmented (a.k.a. piecewise contiguous) + // FIXME: Add support for segmented (a.k.a. piecewise contiguous) // collections to the stdlib. var (it, copied) = elements._copyContents(initializing: first) if copied == first.count, let second = second { @@ -199,10 +221,10 @@ extension _UnsafeMutableWrappedBuffer { assert(self.count == elements.count) if let second = second { let wrap = elements.index(elements.startIndex, offsetBy: first.count) - first._initialize(from: elements[.. Bool { + left.path == right.path + } +} + +extension _AncestorHashSlots { + @inlinable @inline(__always) + internal static var empty: Self { Self(0) } +} + +extension _AncestorHashSlots { + /// Return or set the slot value at the specified level. + /// If this is used to mutate the collection, then the original value + /// on the given level must be zero. + @inlinable @inline(__always) + internal subscript(_ level: _HashLevel) -> _HashSlot { + get { + assert(level.shift < UInt.bitWidth) + return _HashSlot((path &>> level.shift) & _Bucket.bitMask) + } + set { + assert(newValue._value < _Bitmap.capacity) + assert(self[level] == .zero) + path |= (UInt(truncatingIfNeeded: newValue._value) &<< level.shift) + } + } + + @inlinable @inline(__always) + internal func appending(_ slot: _HashSlot, at level: _HashLevel) -> Self { + var result = self + result[level] = slot + return result + } + + /// Clear the slot at the specified level, by setting it to zero. + @inlinable + internal mutating func clear(_ level: _HashLevel) { + guard level.shift < UInt.bitWidth else { return } + path &= ~(_Bucket.bitMask &<< level.shift) + } + + /// Clear all slots at or below the specified level, by setting them to zero. + @inlinable + internal mutating func clear(atOrBelow level: _HashLevel) { + guard level.shift < UInt.bitWidth else { return } + path &= ~(UInt.max &<< level.shift) + } + + /// Truncate this path to the specified level. + /// Slots at or beyond the specified level are cleared. + @inlinable + internal func truncating(to level: _HashLevel) -> _AncestorHashSlots { + assert(level.shift <= UInt.bitWidth) + guard level.shift < UInt.bitWidth else { return self } + return _AncestorHashSlots(path & ((1 &<< level.shift) &- 1)) + } + + /// Returns true if this path contains non-zero slots at or beyond the + /// specified level, otherwise returns false. + @inlinable + internal func hasDataBelow(_ level: _HashLevel) -> Bool { + guard level.shift < UInt.bitWidth else { return false } + return (path &>> level.shift) != 0 + } + + /// Compares this path to `other` up to but not including the specified level. + /// Returns true if the path prefixes compare equal, otherwise returns false. + @inlinable + internal func isEqual(to other: Self, upTo level: _HashLevel) -> Bool { + if level.isAtRoot { return true } + if level.isAtBottom { return self == other } + let s = UInt(UInt.bitWidth) - level.shift + let v1 = self.path &<< s + let v2 = other.path &<< s + return v1 == v2 + } +} + diff --git a/Sources/HashTreeCollections/HashNode/_Bitmap.swift b/Sources/HashTreeCollections/HashNode/_Bitmap.swift new file mode 100644 index 000000000..e8e453fa2 --- /dev/null +++ b/Sources/HashTreeCollections/HashNode/_Bitmap.swift @@ -0,0 +1,221 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +/// A set of `_Bucket` values, represented by a 32-bit wide bitset. +@usableFromInline +@frozen +internal struct _Bitmap { + @usableFromInline + internal typealias Value = UInt32 + + @usableFromInline + internal var _value: Value + + @inlinable @inline(__always) + init(_value: Value) { + self._value = _value + } + + @inlinable @inline(__always) + init(bitPattern: Int) { + self._value = Value(bitPattern) + } + + @inlinable @inline(__always) + internal init(_ bucket: _Bucket) { + assert(bucket.value < Self.capacity) + _value = (1 &<< bucket.value) + } + + @inlinable @inline(__always) + internal init(_ bucket1: _Bucket, _ bucket2: _Bucket) { + assert(bucket1.value < Self.capacity && bucket2.value < Self.capacity) + assert(bucket1 != bucket2) + _value = (1 &<< bucket1.value) | (1 &<< bucket2.value) + } + + @inlinable + internal init(upTo bucket: _Bucket) { + assert(bucket.value < Self.capacity) + _value = (1 &<< bucket.value) &- 1 + } +} + +extension _Bitmap: Equatable { + @inlinable @inline(__always) + internal static func ==(left: Self, right: Self) -> Bool { + left._value == right._value + } +} + +extension _Bitmap: CustomStringConvertible { + @usableFromInline + internal var description: String { + let b = String(_value, radix: 2) + let bits = String(repeating: "0", count: _Bitmap.capacity - b.count) + b + return "\(String(bits.reversed())) (\(_value))" + } +} + +extension _Bitmap { + @inlinable @inline(__always) + internal static var empty: Self { .init(_value: 0) } + + @inlinable @inline(__always) + internal static var capacity: Int { Value.bitWidth } + + @inlinable @inline(__always) + internal static var bitWidth: Int { capacity.trailingZeroBitCount } + + @inlinable @inline(__always) + internal var count: Int { _value.nonzeroBitCount } + + @inlinable @inline(__always) + internal var capacity: Int { Value.bitWidth } + + @inlinable @inline(__always) + internal var isEmpty: Bool { _value == 0 } + + @inlinable @inline(__always) + internal var hasExactlyOneMember: Bool { + _value != 0 && _value & (_value &- 1) == 0 + } + + @inlinable @inline(__always) + internal var first: _Bucket? { + guard !isEmpty else { return nil } + return _Bucket( + _value: UInt8(truncatingIfNeeded: _value.trailingZeroBitCount)) + } + + @inlinable @inline(__always) + internal mutating func popFirst() -> _Bucket? { + guard let bucket = first else { return nil } + _value &= _value &- 1 // Clear lowest nonzero bit. + return bucket + } +} + +extension _Bitmap { + @inlinable @inline(__always) + internal func contains(_ bucket: _Bucket) -> Bool { + assert(bucket.value < capacity) + return _value & (1 &<< bucket.value) != 0 + } + + @inlinable @inline(__always) + internal mutating func insert(_ bucket: _Bucket) { + assert(bucket.value < capacity) + _value |= (1 &<< bucket.value) + } + + @inlinable @inline(__always) + internal func inserting(_ bucket: _Bucket) -> _Bitmap { + assert(bucket.value < capacity) + return _Bitmap(_value: _value | (1 &<< bucket.value)) + } + + @inlinable @inline(__always) + internal mutating func remove(_ bucket: _Bucket) { + assert(bucket.value < capacity) + _value &= ~(1 &<< bucket.value) + } + + @inlinable @inline(__always) + internal func removing(_ bucket: _Bucket) -> _Bitmap { + assert(bucket.value < capacity) + return _Bitmap(_value: _value & ~(1 &<< bucket.value)) + } + + @inlinable @inline(__always) + internal func slot(of bucket: _Bucket) -> _HashSlot { + _HashSlot(_value._rank(ofBit: bucket.value)) + } + + @inlinable @inline(__always) + internal func bucket(at slot: _HashSlot) -> _Bucket { + _Bucket(_value._bit(ranked: slot.value)!) + } +} + +extension _Bitmap { + @inlinable @inline(__always) + internal func isSubset(of other: Self) -> Bool { + _value & ~other._value == 0 + } + + @inlinable @inline(__always) + internal func isDisjoint(with other: Self) -> Bool { + _value & other._value == 0 + } + + @inlinable @inline(__always) + internal func union(_ other: Self) -> Self { + Self(_value: _value | other._value) + } + + @inlinable @inline(__always) + internal func intersection(_ other: Self) -> Self { + Self(_value: _value & other._value) + } + + @inlinable @inline(__always) + internal func symmetricDifference(_ other: Self) -> Self { + Self(_value: _value & other._value) + } + + @inlinable @inline(__always) + internal func subtracting(_ other: Self) -> Self { + Self(_value: _value & ~other._value) + } +} + +extension _Bitmap: Sequence { + @usableFromInline + internal typealias Element = (bucket: _Bucket, slot: _HashSlot) + + @usableFromInline + @frozen + internal struct Iterator: IteratorProtocol { + @usableFromInline + internal var bitmap: _Bitmap + + @usableFromInline + internal var slot: _HashSlot + + @inlinable + internal init(_ bitmap: _Bitmap) { + self.bitmap = bitmap + self.slot = .zero + } + + /// Return the index of the lowest set bit in this word, + /// and also destructively clear it. + @inlinable + internal mutating func next() -> Element? { + guard let bucket = bitmap.popFirst() else { return nil } + defer { slot = slot.next() } + return (bucket, slot) + } + } + + @inlinable + internal var underestimatedCount: Int { count } + + @inlinable + internal func makeIterator() -> Iterator { + Iterator(self) + } +} diff --git a/Sources/HashTreeCollections/HashNode/_Bucket.swift b/Sources/HashTreeCollections/HashNode/_Bucket.swift new file mode 100644 index 000000000..5f8a46f15 --- /dev/null +++ b/Sources/HashTreeCollections/HashNode/_Bucket.swift @@ -0,0 +1,71 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +/// Identifies an entry in the hash table inside a node. +/// (Internally, a number between 0 and 31.) +@usableFromInline +@frozen +internal struct _Bucket { + @usableFromInline + internal var _value: UInt8 + + @inlinable @inline(__always) + internal init(_value: UInt8) { + assert(_value < _Bitmap.capacity || _value == .max) + self._value = _value + } +} + +extension _Bucket { + @inlinable @inline(__always) + internal var value: UInt { UInt(truncatingIfNeeded: _value) } + + @inlinable @inline(__always) + internal init(_ value: UInt) { + assert(value < _Bitmap.capacity || value == .max) + self._value = UInt8(truncatingIfNeeded: value) + } +} + +extension _Bucket { + @inlinable @inline(__always) + static var bitWidth: Int { _Bitmap.capacity.trailingZeroBitCount } + + @inlinable @inline(__always) + static var bitMask: UInt { UInt(bitPattern: _Bitmap.capacity) &- 1 } + + @inlinable @inline(__always) + static var invalid: _Bucket { _Bucket(_value: .max) } + + @inlinable @inline(__always) + var isInvalid: Bool { _value == .max } +} + +extension _Bucket: Equatable { + @inlinable @inline(__always) + internal static func ==(left: Self, right: Self) -> Bool { + left._value == right._value + } +} + +extension _Bucket: Comparable { + @inlinable @inline(__always) + internal static func <(left: Self, right: Self) -> Bool { + left._value < right._value + } +} + +extension _Bucket: CustomStringConvertible { + @usableFromInline + internal var description: String { + String(_value, radix: _Bitmap.capacity) + } +} diff --git a/Sources/HashTreeCollections/HashNode/_Hash.swift b/Sources/HashTreeCollections/HashNode/_Hash.swift new file mode 100644 index 000000000..d43a16770 --- /dev/null +++ b/Sources/HashTreeCollections/HashNode/_Hash.swift @@ -0,0 +1,94 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +/// An abstract representation of a hash value. +@usableFromInline +@frozen +internal struct _Hash { + @usableFromInline + internal var value: UInt + + @inlinable + internal init(_ key: some Hashable) { + let hashValue = key._rawHashValue(seed: 0) + self.value = UInt(bitPattern: hashValue) + } + + @inlinable + internal init(_value: UInt) { + self.value = _value + } +} + +extension _Hash: Equatable { + @inlinable @inline(__always) + internal static func ==(left: Self, right: Self) -> Bool { + left.value == right.value + } +} + +extension _Hash: CustomStringConvertible { + @usableFromInline + internal var description: String { + // Print hash values in radix 32 & reversed, so that the path in the hash + // tree is readily visible. + let p = String(value, radix: _Bitmap.capacity, uppercase: true) + let c = _HashLevel.limit + let path = String(repeating: "0", count: Swift.max(0, c - p.count)) + p + return String(path.reversed()) + } +} + + +extension _Hash { + @inlinable @inline(__always) + internal static var bitWidth: Int { UInt.bitWidth } +} + +extension _Hash { + @inlinable + internal subscript(_ level: _HashLevel) -> _Bucket { + get { + assert(!level.isAtBottom) + return _Bucket((value &>> level.shift) & _Bucket.bitMask) + } + set { + let mask = _Bucket.bitMask &<< level.shift + self.value &= ~mask + self.value |= newValue.value &<< level.shift + } + } +} + +extension _Hash { + @inlinable + internal static var emptyPath: _Hash { + _Hash(_value: 0) + } + + @inlinable + internal func appending(_ bucket: _Bucket, at level: _HashLevel) -> Self { + assert(value >> level.shift == 0) + var copy = self + copy[level] = bucket + return copy + } + + @inlinable + internal func isEqual(to other: _Hash, upTo level: _HashLevel) -> Bool { + if level.isAtRoot { return true } + if level.isAtBottom { return self == other } + let s = UInt(UInt.bitWidth) - level.shift + let v1 = self.value &<< s + let v2 = self.value &<< s + return v1 == v2 + } +} diff --git a/Sources/HashTreeCollections/HashNode/_HashLevel.swift b/Sources/HashTreeCollections/HashNode/_HashLevel.swift new file mode 100644 index 000000000..2e3a21761 --- /dev/null +++ b/Sources/HashTreeCollections/HashNode/_HashLevel.swift @@ -0,0 +1,101 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +/// Identifies a particular level within the hash tree. +/// +/// Hash trees have a maximum depth of ⎡`UInt.bitWidth / _Bucket.bitWidth`⎤, so +/// the level always fits in an `UInt8` value. +@usableFromInline +@frozen +internal struct _HashLevel { + /// The bit position within a hash value that begins the hash slice that is + /// associated with this level. For collision nodes, this can be larger than + /// `UInt.bitWidth`. + @usableFromInline + internal var _shift: UInt8 + + @inlinable @inline(__always) + init(_shift: UInt8) { + self._shift = _shift + } + + @inlinable @inline(__always) + init(shift: UInt) { + assert(shift <= UInt8.max) + self._shift = UInt8(truncatingIfNeeded: shift) + } + + @inlinable @inline(__always) + init(depth: Int) { + assert(depth > 0 && depth < _HashLevel.limit) + self.init(shift: UInt(bitPattern: depth * _Bitmap.bitWidth)) + } +} + +extension _HashLevel { + @inlinable @inline(__always) + internal static var limit: Int { + (_Hash.bitWidth + _Bitmap.bitWidth - 1) / _Bitmap.bitWidth + } + + @inlinable @inline(__always) + internal static var _step: UInt8 { + UInt8(truncatingIfNeeded: _Bitmap.bitWidth) + } + + @inlinable @inline(__always) + internal static var top: _HashLevel { + _HashLevel(shift: 0) + } + + /// The bit position within a hash value that begins the hash slice that is + /// associated with this level. For collision nodes, this can be larger than + /// `UInt.bitWidth`. + @inlinable @inline(__always) + internal var shift: UInt { UInt(truncatingIfNeeded: _shift) } + + @inlinable @inline(__always) + internal var isAtRoot: Bool { _shift == 0 } + + @inlinable @inline(__always) + internal var isAtBottom: Bool { _shift >= UInt.bitWidth } + + @inlinable @inline(__always) + internal var depth: Int { + (Int(bitPattern: shift) + _Bitmap.bitWidth - 1) / _Bitmap.bitWidth + } + + @inlinable @inline(__always) + internal func descend() -> _HashLevel { + // FIXME: Consider returning nil when we run out of bits + _HashLevel(_shift: _shift &+ Self._step) + } + + @inlinable @inline(__always) + internal func ascend() -> _HashLevel { + assert(!isAtRoot) + return _HashLevel(_shift: _shift &- Self._step) + } +} + +extension _HashLevel: Equatable { + @inlinable @inline(__always) + internal static func ==(left: Self, right: Self) -> Bool { + left._shift == right._shift + } +} + +extension _HashLevel: Comparable { + @inlinable @inline(__always) + internal static func <(left: Self, right: Self) -> Bool { + left._shift < right._shift + } +} diff --git a/Sources/HashTreeCollections/HashNode/_HashNode+Builder.swift b/Sources/HashTreeCollections/HashNode/_HashNode+Builder.swift new file mode 100644 index 000000000..fc2341f1c --- /dev/null +++ b/Sources/HashTreeCollections/HashNode/_HashNode+Builder.swift @@ -0,0 +1,360 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 _HashNode { + @usableFromInline + @frozen + internal struct Builder { + @usableFromInline internal typealias Element = _HashNode.Element + + @usableFromInline + @frozen + internal enum Kind { + case empty + case item(Element, at: _Bucket) + case node(_HashNode) + case collisionNode(_HashNode) + } + + @usableFromInline + internal var level: _HashLevel + + @usableFromInline + internal var kind: Kind + + @inlinable + internal init(_ level: _HashLevel, _ kind: Kind) { + self.level = level + self.kind = kind + } + } +} + +extension _HashNode.Builder { + @usableFromInline + internal func dump() { + let head = "Builder(level: \(level.depth), kind: " + switch self.kind { + case .empty: + print(head + "empty)") + case .item(let item, at: let bucket): + print(head + "item(\(_HashNode._itemString(for: item)), at: \(bucket))") + case .node(let node): + print(head + "node)") + node.dump() + case .collisionNode(let node): + print(head + "collisionNode)") + node.dump() + } + } +} + +extension _HashNode.Builder { + @inlinable @inline(__always) + internal static func empty(_ level: _HashLevel) -> Self { + Self(level, .empty) + } + + @inlinable @inline(__always) + internal static func item( + _ level: _HashLevel, _ item: __owned Element, at bucket: _Bucket + ) -> Self { + Self(level, .item(item, at: bucket)) + } + + @inlinable @inline(__always) + internal static func node( + _ level: _HashLevel, _ node: __owned _HashNode + ) -> Self { + assert(!node.isCollisionNode) + return Self(level, .node(node)) + } + + @inlinable @inline(__always) + internal static func collisionNode( + _ level: _HashLevel, _ node: __owned _HashNode + ) -> Self { + assert(node.isCollisionNode) + return Self(level, .collisionNode(node)) + } +} + +extension _HashNode.Builder { + @inlinable + internal var count: Int { + switch kind { + case .empty: + return 0 + case .item: + return 1 + case .node(let node): + return node.count + case .collisionNode(let node): + return node.count + } + } + + @inlinable + internal var isEmpty: Bool { + guard case .empty = kind else { return false } + return true + } +} + +extension _HashNode.Builder { + @inlinable + internal init(_ level: _HashLevel, _ node: _HashNode) { + self.level = level + if node.count == 0 { + kind = .empty + } else if node.isCollisionNode { + assert(!node.hasSingletonItem) + kind = .collisionNode(node) + } else if node.hasSingletonItem { + kind = node.read { .item($0[item: .zero], at: $0.itemMap.first!) } + } else { + kind = .node(node) + } + } + + @inlinable + internal __consuming func finalize(_ level: _HashLevel) -> _HashNode { + assert(level.isAtRoot && self.level.isAtRoot) + switch kind { + case .empty: + return ._emptyNode() + case .item(let item, let bucket): + return ._regularNode(item, bucket) + case .node(let node): + return node + case .collisionNode(let node): + return node + } + } +} + +extension _HashNode { + @inlinable + internal mutating func applyReplacement( + _ level: _HashLevel, + _ replacement: Builder + ) -> Element? { + assert(level == replacement.level) + switch replacement.kind { + case .empty: + self = ._emptyNode() + case .node(let n), .collisionNode(let n): + self = n + case .item(let item, let bucket): + guard level.isAtRoot else { + self = ._emptyNode() + return item + } + self = ._regularNode(item, bucket) + } + return nil + } +} + +extension _HashNode.Builder { + @inlinable + internal mutating func addNewCollision( + _ level: _HashLevel, _ newItem: __owned Element, _ hash: _Hash + ) { + assert(level == self.level) + switch kind { + case .empty: + kind = .item(newItem, at: hash[level]) + case .item(let oldItem, at: let bucket): + assert(hash[level] == bucket) + let node = _HashNode._collisionNode(hash, oldItem, newItem) + kind = .collisionNode(node) + case .collisionNode(var node): + kind = .empty + assert(node.isCollisionNode) + assert(hash == node.collisionHash) + _ = node.ensureUniqueAndAppendCollision(isUnique: true, newItem) + kind = .collisionNode(node) + case .node: + fatalError() + } + } + + @inlinable + internal mutating func addNewItem( + _ level: _HashLevel, _ newItem: __owned Element, at newBucket: _Bucket + ) { + assert(level == self.level) + switch kind { + case .empty: + kind = .item(newItem, at: newBucket) + case .item(let oldItem, let oldBucket): + assert(oldBucket != newBucket) + let node = _HashNode._regularNode(oldItem, oldBucket, newItem, newBucket) + kind = .node(node) + case .node(var node): + kind = .empty + let isUnique = node.isUnique() + node.ensureUniqueAndInsertItem(isUnique: isUnique, newItem, at: newBucket) + kind = .node(node) + case .collisionNode(var node): + // Expansion + assert(!level.isAtBottom) + self.kind = .empty + node = _HashNode._regularNode( + newItem, newBucket, node, node.collisionHash[level]) + kind = .node(node) + } + } + + @inlinable + internal mutating func addNewChildNode( + _ level: _HashLevel, _ newChild: __owned _HashNode, at newBucket: _Bucket + ) { + assert(level == self.level) + switch self.kind { + case .empty: + if newChild.isCollisionNode { + // Compression + assert(!level.isAtBottom) + self.kind = .collisionNode(newChild) + } else { + self.kind = .node(._regularNode(newChild, newBucket)) + } + case let .item(oldItem, oldBucket): + let node = _HashNode._regularNode(oldItem, oldBucket, newChild, newBucket) + self.kind = .node(node) + case .node(var node): + self.kind = .empty + let isUnique = node.isUnique() + node.ensureUnique( + isUnique: isUnique, withFreeSpace: _HashNode.spaceForNewChild) + node.insertChild(newChild, newBucket) + self.kind = .node(node) + case .collisionNode(var node): + // Expansion + self.kind = .empty + assert(!level.isAtBottom) + node = _HashNode._regularNode( + node, node.collisionHash[level], newChild, newBucket) + self.kind = .node(node) + } + } + + @inlinable + internal mutating func addNewChildBranch( + _ level: _HashLevel, _ newChild: __owned Self, at newBucket: _Bucket + ) { + assert(level == self.level) + assert(newChild.level == self.level.descend()) + switch newChild.kind { + case .empty: + break + case .item(let newItem, _): + self.addNewItem(level, newItem, at: newBucket) + case .node(let newNode), .collisionNode(let newNode): + self.addNewChildNode(level, newNode, at: newBucket) + } + } + + @inlinable + internal static func childBranch( + _ level: _HashLevel, _ child: Self, at bucket: _Bucket + ) -> Self { + assert(child.level == level.descend()) + switch child.kind { + case .empty: + return self.empty(level) + case .item(let item, _): + return self.item(level, item, at: bucket) + case .node(let n): + return self.node(level, ._regularNode(n, bucket)) + case .collisionNode(let node): + // Compression + assert(!level.isAtBottom) + return self.collisionNode(level, node) + } + } +} + +extension _HashNode.Builder { + @inlinable + internal mutating func copyCollisions( + from source: _HashNode.UnsafeHandle, + upTo end: _HashSlot + ) { + assert(isEmpty) + assert(source.isCollisionNode) + assert(end < source.itemsEndSlot) + let h = source.collisionHash + for slot: _HashSlot in stride(from: .zero, to: end, by: 1) { + self.addNewCollision(self.level, source[item: slot], h) + } + } + + @inlinable + internal mutating func copyItems( + _ level: _HashLevel, + from source: _HashNode.UnsafeHandle, + upTo end: _Bucket + ) { + assert(level == self.level) + assert(isEmpty) + assert(!source.isCollisionNode) + for (b, s) in source.itemMap.intersection(_Bitmap(upTo: end)) { + self.addNewItem(level, source[item: s], at: b) + } + } + + @inlinable + internal mutating func copyItemsAndChildren( + _ level: _HashLevel, + from source: _HashNode.UnsafeHandle, + upTo end: _Bucket + ) { + assert(level == self.level) + assert(isEmpty) + assert(!source.isCollisionNode) + for (b, s) in source.itemMap { + self.addNewItem(level, source[item: s], at: b) + } + for (b, s) in source.childMap.intersection(_Bitmap(upTo: end)) { + self.addNewChildNode(level, source[child: s], at: b) + } + } +} + +extension _HashNode.Builder { + @inlinable + internal func mapValues( + _ transform: (Element) -> Value2 + ) -> _HashNode.Builder { + switch kind { + case .empty: + return .empty(level) + case let .item(item, at: bucket): + let value = transform(item) + return .item(level, (item.key, value), at: bucket) + case let .node(node): + return .node(level, node.mapValues(transform)) + case let .collisionNode(node): + return .collisionNode(level, node.mapValues(transform)) + } + } + + @inlinable + internal func mapValuesToVoid() -> _HashNode.Builder { + if Value.self == Void.self { + return unsafeBitCast(self, to: _HashNode.Builder.self) + } + return mapValues { _ in () } + } +} diff --git a/Sources/HashTreeCollections/HashNode/_HashNode+Debugging.swift b/Sources/HashTreeCollections/HashNode/_HashNode+Debugging.swift new file mode 100644 index 000000000..02f6c990c --- /dev/null +++ b/Sources/HashTreeCollections/HashNode/_HashNode+Debugging.swift @@ -0,0 +1,121 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension _HashNode { + @usableFromInline + internal func dump( + iterationOrder: Bool = false, + limit: Int = Int.max, + firstPrefix: String = "", + restPrefix: String = "", + depth: Int = 0 + ) { + read { + $0.dump( + iterationOrder: iterationOrder, + limit: limit, + extra: "count: \(count), ", + firstPrefix: firstPrefix, + restPrefix: restPrefix, + depth: depth) + } + } +} + +extension _HashNode.Storage { + @usableFromInline + final internal func dump(iterationOrder: Bool = false) { + UnsafeHandle.read(self) { $0.dump(iterationOrder: iterationOrder) } + } +} + +extension _HashNode { + internal static func _itemString(for item: Element) -> String { + let hash = _Hash(item.key).description + return "hash: \(hash), key: \(item.key), value: \(item.value)" + } +} +extension _HashNode.UnsafeHandle { + internal func _itemString(at slot: _HashSlot) -> String { + let item = self[item: slot] + return _HashNode._itemString(for: item) + } + + @usableFromInline + internal func dump( + iterationOrder: Bool = false, + limit: Int = .max, + extra: String = "", + firstPrefix: String = "", + restPrefix: String = "", + depth: Int = 0 + ) { + var firstPrefix = firstPrefix + var restPrefix = restPrefix + if iterationOrder && depth == 0 { + firstPrefix += "@" + restPrefix += "@" + } + if iterationOrder { + firstPrefix += " " + } + print(""" + \(firstPrefix)\(isCollisionNode ? "CollisionNode" : "Node")(\ + at: \(_addressString(for: _header)), \ + \(isCollisionNode ? "hash: \(collisionHash), " : "")\ + \(extra)\ + byteCapacity: \(byteCapacity), \ + freeBytes: \(bytesFree)) + """) + guard limit > 0 else { return } + if iterationOrder { + for slot in stride(from: .zero, to: itemsEndSlot, by: 1) { + print(" \(restPrefix)[\(slot)] \(_itemString(at: slot))") + } + for slot in stride(from: .zero, to: childrenEndSlot, by: 1) { + self[child: slot].dump( + iterationOrder: true, + limit: limit - 1, + firstPrefix: " \(restPrefix).\(slot)", + restPrefix: " \(restPrefix).\(slot)", + depth: depth + 1) + } + } + else if isCollisionNode { + for slot in stride(from: .zero, to: itemsEndSlot, by: 1) { + print("\(restPrefix)[\(slot)] \(_itemString(at: slot))") + } + } else { + var itemSlot: _HashSlot = .zero + var childSlot: _HashSlot = .zero + for b in 0 ..< UInt(_Bitmap.capacity) { + let bucket = _Bucket(b) + let bucketStr = "#\(String(b, radix: _Bitmap.capacity, uppercase: true))" + if itemMap.contains(bucket) { + print("\(restPrefix) \(bucketStr) \(_itemString(at: itemSlot))") + itemSlot = itemSlot.next() + } else if childMap.contains(bucket) { + self[child: childSlot].dump( + iterationOrder: false, + limit: limit - 1, + firstPrefix: "\(restPrefix) \(bucketStr) ", + restPrefix: "\(restPrefix) ", + depth: depth + 1) + childSlot = childSlot.next() + } + } + } + } +} diff --git a/Sources/HashTreeCollections/HashNode/_HashNode+Initializers.swift b/Sources/HashTreeCollections/HashNode/_HashNode+Initializers.swift new file mode 100644 index 000000000..e518eeea5 --- /dev/null +++ b/Sources/HashTreeCollections/HashNode/_HashNode+Initializers.swift @@ -0,0 +1,261 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension _HashNode { + @inlinable @inline(__always) + internal static func _emptyNode() -> _HashNode { + _HashNode(storage: _emptySingleton, count: 0) + } + + @inlinable + internal static func _collisionNode( + _ hash: _Hash, + _ item1: __owned Element, + _ item2: __owned Element + ) -> _HashNode { + let node = _HashNode.allocateCollision(count: 2, hash) { items in + items.initializeElement(at: 1, to: item1) + items.initializeElement(at: 0, to: item2) + }.node + node._invariantCheck() + return node + } + + @inlinable + internal static func _collisionNode( + _ hash: _Hash, + _ item1: __owned Element, + _ inserter2: (UnsafeMutablePointer) -> Void + ) -> _HashNode { + let node = _HashNode.allocateCollision(count: 2, hash) { items in + items.initializeElement(at: 1, to: item1) + inserter2(items.baseAddress.unsafelyUnwrapped) + }.node + node._invariantCheck() + return node + } + + @inlinable + internal static func _regularNode( + _ item: __owned Element, + _ bucket: _Bucket + ) -> _HashNode { + let r = _HashNode.allocate( + itemMap: _Bitmap(bucket), + childMap: .empty, + count: 1 + ) { children, items in + assert(items.count == 1 && children.count == 0) + items.initializeElement(at: 0, to: item) + } + r.node._invariantCheck() + return r.node + } + + @inlinable + internal static func _regularNode( + _ item1: __owned Element, + _ bucket1: _Bucket, + _ item2: __owned Element, + _ bucket2: _Bucket + ) -> _HashNode { + _regularNode( + item1, bucket1, + { $0.initialize(to: item2) }, bucket2).node + } + + @inlinable + internal static func _regularNode( + _ item1: __owned Element, + _ bucket1: _Bucket, + _ inserter2: (UnsafeMutablePointer) -> Void, + _ bucket2: _Bucket + ) -> (node: _HashNode, slot1: _HashSlot, slot2: _HashSlot) { + assert(bucket1 != bucket2) + let r = _HashNode.allocate( + itemMap: _Bitmap(bucket1, bucket2), + childMap: .empty, + count: 2 + ) { children, items -> (_HashSlot, _HashSlot) in + assert(items.count == 2 && children.count == 0) + let i1 = bucket1 < bucket2 ? 1 : 0 + let i2 = 1 &- i1 + items.initializeElement(at: i1, to: item1) + inserter2(items.baseAddress.unsafelyUnwrapped + i2) + return (_HashSlot(i2), _HashSlot(i1)) // Note: swapped + } + r.node._invariantCheck() + return (r.node, r.result.0, r.result.1) + } + + @inlinable + internal static func _regularNode( + _ child: __owned _HashNode, + _ bucket: _Bucket + ) -> _HashNode { + let r = _HashNode.allocate( + itemMap: .empty, + childMap: _Bitmap(bucket), + count: child.count + ) { children, items in + assert(items.count == 0 && children.count == 1) + children.initializeElement(at: 0, to: child) + } + r.node._invariantCheck() + return r.node + } + + @inlinable + internal static func _regularNode( + _ item: __owned Element, + _ itemBucket: _Bucket, + _ child: __owned _HashNode, + _ childBucket: _Bucket + ) -> _HashNode { + _regularNode( + { $0.initialize(to: item) }, itemBucket, + child, childBucket) + } + + @inlinable + internal static func _regularNode( + _ inserter: (UnsafeMutablePointer) -> Void, + _ itemBucket: _Bucket, + _ child: __owned _HashNode, + _ childBucket: _Bucket + ) -> _HashNode { + assert(itemBucket != childBucket) + let r = _HashNode.allocate( + itemMap: _Bitmap(itemBucket), + childMap: _Bitmap(childBucket), + count: child.count &+ 1 + ) { children, items in + assert(items.count == 1 && children.count == 1) + inserter(items.baseAddress.unsafelyUnwrapped) + children.initializeElement(at: 0, to: child) + } + r.node._invariantCheck() + return r.node + } + + @inlinable + internal static func _regularNode( + _ child1: __owned _HashNode, + _ child1Bucket: _Bucket, + _ child2: __owned _HashNode, + _ child2Bucket: _Bucket + ) -> _HashNode { + assert(child1Bucket != child2Bucket) + let r = _HashNode.allocate( + itemMap: .empty, + childMap: _Bitmap(child1Bucket, child2Bucket), + count: child1.count &+ child2.count + ) { children, items in + assert(items.count == 0 && children.count == 2) + children.initializeElement(at: 0, to: child1) + children.initializeElement(at: 1, to: child2) + } + r.node._invariantCheck() + return r.node + } +} + +extension _HashNode { + @inlinable + internal static func build( + level: _HashLevel, + item1: __owned Element, + _ hash1: _Hash, + item2 inserter2: (UnsafeMutablePointer) -> Void, + _ hash2: _Hash + ) -> (top: _HashNode, leaf: _UnmanagedHashNode, slot1: _HashSlot, slot2: _HashSlot) { + assert(hash1.isEqual(to: hash2, upTo: level.ascend())) + if hash1 == hash2 { + let top = _collisionNode(hash1, item1, inserter2) + return (top, top.unmanaged, _HashSlot(0), _HashSlot(1)) + } + let r = _build( + level: level, item1: item1, hash1, item2: inserter2, hash2) + return (r.top, r.leaf, r.slot1, r.slot2) + } + + @inlinable + internal static func _build( + level: _HashLevel, + item1: __owned Element, + _ hash1: _Hash, + item2 inserter2: (UnsafeMutablePointer) -> Void, + _ hash2: _Hash + ) -> (top: _HashNode, leaf: _UnmanagedHashNode, slot1: _HashSlot, slot2: _HashSlot) { + assert(hash1 != hash2) + let b1 = hash1[level] + let b2 = hash2[level] + guard b1 == b2 else { + let r = _regularNode(item1, b1, inserter2, b2) + return (r.node, r.node.unmanaged, r.slot1, r.slot2) + } + let r = _build( + level: level.descend(), + item1: item1, hash1, + item2: inserter2, hash2) + return (_regularNode(r.top, b1), r.leaf, r.slot1, r.slot2) + } + + @inlinable + internal static func build( + level: _HashLevel, + item1 inserter1: (UnsafeMutablePointer) -> Void, + _ hash1: _Hash, + child2: __owned _HashNode, + _ hash2: _Hash + ) -> (top: _HashNode, leaf: _UnmanagedHashNode, slot1: _HashSlot, slot2: _HashSlot) { + assert(child2.isCollisionNode) + assert(hash1 != hash2) + let b1 = hash1[level] + let b2 = hash2[level] + if b1 == b2 { + let node = build( + level: level.descend(), + item1: inserter1, hash1, + child2: child2, hash2) + return (_regularNode(node.top, b1), node.leaf, node.slot1, node.slot2) + } + let node = _regularNode(inserter1, hash1[level], child2, hash2[level]) + return (node, node.unmanaged, .zero, .zero) + } + + @inlinable + internal static func build( + level: _HashLevel, + child1: __owned _HashNode, + _ hash1: _Hash, + child2: __owned _HashNode, + _ hash2: _Hash + ) -> _HashNode { + assert(child1.isCollisionNode) + assert(child2.isCollisionNode) + assert(hash1 != hash2) + let b1 = hash1[level] + let b2 = hash2[level] + guard b1 == b2 else { + return _regularNode(child1, b1, child2, b2) + } + let node = build( + level: level.descend(), + child1: child1, hash1, + child2: child2, hash2) + return _regularNode(node, b1) + } +} diff --git a/Sources/HashTreeCollections/HashNode/_HashNode+Invariants.swift b/Sources/HashTreeCollections/HashNode/_HashNode+Invariants.swift new file mode 100644 index 000000000..6e44aabc1 --- /dev/null +++ b/Sources/HashTreeCollections/HashNode/_HashNode+Invariants.swift @@ -0,0 +1,107 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 _HashNodeHeader { +#if COLLECTIONS_INTERNAL_CHECKS + @usableFromInline @inline(never) + internal func _invariantCheck() { + precondition(bytesFree <= byteCapacity) + if isCollisionNode { + precondition(itemMap == childMap) + precondition(!itemMap.isEmpty) + } else { + precondition(itemMap.intersection(childMap).isEmpty) + } + } +#else + @inlinable @inline(__always) + internal func _invariantCheck() {} +#endif +} + +extension _HashNode { +#if COLLECTIONS_INTERNAL_CHECKS + @usableFromInline @inline(never) + internal func _invariantCheck() { + raw.storage.header._invariantCheck() + read { + let itemBytes = $0.itemCount * MemoryLayout.stride + + if $0.isCollisionNode { + let hashBytes = MemoryLayout<_Hash>.stride + assert($0.itemCount >= 1) + assert($0.childCount == 0) + assert(itemBytes + $0.bytesFree + hashBytes == $0.byteCapacity) + + assert($0.collisionHash == _Hash($0[item: .zero].key)) + } else { + let childBytes = $0.childCount * MemoryLayout<_HashNode>.stride + assert(itemBytes + $0.bytesFree + childBytes == $0.byteCapacity) + } + + let actualCount = $0.children.reduce($0.itemCount, { $0 + $1.count }) + assert(actualCount == self.count) + } + } +#else + @inlinable @inline(__always) + internal func _invariantCheck() {} +#endif + + @inlinable @inline(__always) + public func _fullInvariantCheck() { + self._fullInvariantCheck(.top, .emptyPath) + } + +#if COLLECTIONS_INTERNAL_CHECKS + @inlinable @inline(never) + internal func _fullInvariantCheck(_ level: _HashLevel, _ path: _Hash) { + _invariantCheck() + read { + precondition(level.isAtRoot || !hasSingletonItem) + precondition(!isAtrophied) + if $0.isCollisionNode { + precondition(count == $0.itemCount) + precondition(count > 0) + let hash = $0.collisionHash + precondition( + hash.isEqual(to: path, upTo: level), + "Misplaced collision node: \(path) isn't a prefix of \(hash)") + for item in $0.reverseItems { + precondition(_Hash(item.key) == hash) + } + } + var itemSlot: _HashSlot = .zero + var childSlot: _HashSlot = .zero + for b in 0 ..< UInt(_Bitmap.capacity) { + let bucket = _Bucket(b) + let path = path.appending(bucket, at: level) + if $0.itemMap.contains(bucket) { + let key = $0[item: itemSlot].key + let hash = _Hash(key) + precondition( + hash.isEqual(to: path, upTo: level.descend()), + "Misplaced key '\(key)': \(path) isn't a prefix of \(hash)") + itemSlot = itemSlot.next() + } + if $0.hasChildren && $0.childMap.contains(bucket) { + $0[child: childSlot]._fullInvariantCheck(level.descend(), path) + childSlot = childSlot.next() + } + } + } + } +#else + @inlinable @inline(__always) + internal func _fullInvariantCheck(_ level: _HashLevel, _ path: _Hash) {} +#endif +} + diff --git a/Sources/HashTreeCollections/HashNode/_HashNode+Lookups.swift b/Sources/HashTreeCollections/HashNode/_HashNode+Lookups.swift new file mode 100644 index 000000000..41e22f956 --- /dev/null +++ b/Sources/HashTreeCollections/HashNode/_HashNode+Lookups.swift @@ -0,0 +1,269 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +// MARK: Node-level lookup operations + +extension _HashNode { + @inlinable + internal func find( + _ level: _HashLevel, _ key: Key, _ hash: _Hash + ) -> (descend: Bool, slot: _HashSlot)? { + read { $0.find(level, key, hash) } + } +} + +extension _HashNode.UnsafeHandle { + @inlinable + internal func find( + _ level: _HashLevel, _ key: Key, _ hash: _Hash + ) -> (descend: Bool, slot: _HashSlot)? { + guard !isCollisionNode else { + let r = _findInCollision(level, key, hash) + guard r.code == 0 else { return nil } + return (false, r.slot) + } + let bucket = hash[level] + if itemMap.contains(bucket) { + let slot = itemMap.slot(of: bucket) + guard self[item: slot].key == key else { return nil } + return (false, slot) + } + if childMap.contains(bucket) { + let slot = childMap.slot(of: bucket) + return (true, slot) + } + return nil + } + + @inlinable @inline(never) + internal func _findInCollision( + _ level: _HashLevel, _ key: Key, _ hash: _Hash + ) -> (code: Int, slot: _HashSlot) { + assert(isCollisionNode) + if !level.isAtBottom { + if hash != self.collisionHash { return (2, .zero) } + } + // Note: this searches the items in reverse insertion order. + guard let slot = reverseItems.firstIndex(where: { $0.key == key }) + else { return (1, self.itemsEndSlot) } + return (0, _HashSlot(itemCount &- 1 &- slot)) + } +} + + +/// Represents the results of a lookup operation within a single node of a hash +/// tree. This enumeration captures all of the different cases that need to be +/// covered if we wanted to insert a new item into the tree. +/// +/// For simple read-only lookup operations (and removals) some of the cases are +/// equivalent: `.notFound`, .newCollision` and `expansion` all represent the +/// same logical outcome: the key we're looking for is not present in this +/// subtree. +@usableFromInline +@frozen +internal enum _FindResult { + /// The item we're looking for is stored directly in this node, at the + /// bucket / item slot identified in the payload. + /// + /// If the current node is a collision node, then the bucket value is + /// set to `_Bucket.invalid`. + case found(_Bucket, _HashSlot) + + /// The item we're looking for is not currently inside the subtree rooted at + /// this node. + /// + /// If we wanted to insert it, then its correct slot is within this node + /// at the specified bucket / item slot. (Which is currently empty.) + /// + /// When the node is a collision node, the `insertCollision` case is returned + /// instead of this one. + case insert(_Bucket, _HashSlot) + + /// The item we're looking for is not currently inside the subtree rooted at + /// this collision node. + /// + /// If we wanted to insert it, then it needs to be appended to the items + /// buffer. + case appendCollision + + /// The item we're looking for is not currently inside the subtree rooted at + /// this node. + /// + /// If we wanted to insert it, then it would need to be stored in this node + /// at the specified bucket / item slot. However, that bucket is already + /// occupied by another item, so the insertion would need to involve replacing + /// it with a new child node. + /// + /// (This case is never returned if the current node is a collision node.) + case spawnChild(_Bucket, _HashSlot) + + /// The item we're looking for is not in this subtree. + /// + /// However, the item doesn't belong in this subtree at all. This is an + /// irregular case that can only happen with (compressed) hash collision nodes + /// whose (otherwise empty) ancestors got eliminated, so they appear further + /// up in the tree than what their (logical) level would indicate. + /// + /// If we wanted to insert a new item with this key, then we'd need to create + /// (one or more) new parent nodes above this node, pushing this collision + /// node further down the tree. (This undoes the compression by expanding + /// the collision node's path, hence the name of the enum case.) + /// + /// (This case is never returned if the current node is a regular node.) + case expansion + + /// The item we're looking for is not directly stored in this node, but it + /// might be somewhere in the subtree rooted at the child at the given + /// bucket & slot. + /// + /// (This case is never returned if the current node is a collision node.) + case descend(_Bucket, _HashSlot) +} + +extension _HashNode { + @inlinable + internal func findForInsertion( + _ level: _HashLevel, _ key: Key, _ hash: _Hash + ) -> _FindResult { + read { $0.findForInsertion(level, key, hash) } + } +} + +extension _HashNode.UnsafeHandle { + @inlinable + internal func findForInsertion( + _ level: _HashLevel, _ key: Key, _ hash: _Hash + ) -> _FindResult { + guard !isCollisionNode else { + let r = _findInCollision(level, key, hash) + if r.code == 0 { + return .found(.invalid, r.slot) + } + if r.code == 1 { + return .appendCollision + } + assert(r.code == 2) + return .expansion + } + let bucket = hash[level] + if itemMap.contains(bucket) { + let slot = itemMap.slot(of: bucket) + if self[item: slot].key == key { + return .found(bucket, slot) + } + return .spawnChild(bucket, slot) + } + if childMap.contains(bucket) { + let slot = childMap.slot(of: bucket) + return .descend(bucket, slot) + } + let slot = itemMap.slot(of: bucket) + return .insert(bucket, slot) + } +} + +// MARK: Subtree-level lookup operations + +extension _HashNode { + @inlinable + internal func get(_ level: _HashLevel, _ key: Key, _ hash: _Hash) -> Value? { + var node = unmanaged + var level = level + while true { + let r = UnsafeHandle.read(node) { $0.find(level, key, hash) } + guard let r = r else { + return nil + } + guard r.descend else { + return UnsafeHandle.read(node) { $0[item: r.slot].value } + } + node = node.unmanagedChild(at: r.slot) + level = level.descend() + } + } +} + +extension _HashNode { + @inlinable + internal func containsKey( + _ level: _HashLevel, _ key: Key, _ hash: _Hash + ) -> Bool { + var node = unmanaged + var level = level + while true { + let r = UnsafeHandle.read(node) { $0.find(level, key, hash) } + guard let r = r else { return false } + guard r.descend else { return true } + node = node.unmanagedChild(at: r.slot) + level = level.descend() + } + } +} + +extension _HashNode { + @inlinable + internal func lookup( + _ level: _HashLevel, _ key: Key, _ hash: _Hash + ) -> (node: _UnmanagedHashNode, slot: _HashSlot)? { + var node = unmanaged + var level = level + while true { + let r = UnsafeHandle.read(node) { $0.find(level, key, hash) } + guard let r = r else { + return nil + } + guard r.descend else { + return (node, r.slot) + } + node = node.unmanagedChild(at: r.slot) + level = level.descend() + } + } +} + +extension _HashNode { + @inlinable + internal func position( + forKey key: Key, _ level: _HashLevel, _ hash: _Hash + ) -> Int? { + guard let r = find(level, key, hash) else { return nil } + guard r.descend else { return r.slot.value } + return read { h in + let children = h.children + let p = children[r.slot.value] + .position(forKey: key, level.descend(), hash) + guard let p = p else { return nil } + let c = h.itemCount &+ p + return children[.. Element { + assert(position >= 0 && position < self.count) + return read { + var itemsToSkip = position + let itemCount = $0.itemCount + if itemsToSkip < itemCount { + return $0[item: _HashSlot(itemsToSkip)] + } + itemsToSkip -= itemCount + let children = $0.children + for i in children.indices { + if itemsToSkip < children[i].count { + return children[i].item(position: itemsToSkip) + } + itemsToSkip -= children[i].count + } + fatalError("Inconsistent tree") + } + } +} diff --git a/Sources/HashTreeCollections/HashNode/_HashNode+Primitive Insertions.swift b/Sources/HashTreeCollections/HashNode/_HashNode+Primitive Insertions.swift new file mode 100644 index 000000000..723a33b7a --- /dev/null +++ b/Sources/HashTreeCollections/HashNode/_HashNode+Primitive Insertions.swift @@ -0,0 +1,117 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +// MARK: Node-level insertion operations + +extension _HashNode.UnsafeHandle { + /// Make room for a new item at `slot` corresponding to `bucket`. + /// There must be enough free space in the node to fit the new item. + /// + /// `itemMap` must not already reflect the insertion at the time this + /// function is called. This method does not update `itemMap`. + /// + /// - Returns: an unsafe mutable pointer to uninitialized memory that is + /// ready to store the new item. It is the caller's responsibility to + /// initialize this memory. + @inlinable + internal func _makeRoomForNewItem( + at slot: _HashSlot, _ bucket: _Bucket + ) -> UnsafeMutablePointer { + assertMutable() + let c = itemCount + assert(slot.value <= c) + + let stride = MemoryLayout.stride + assert(bytesFree >= stride) + bytesFree &-= stride + + let start = _memory + .advanced(by: byteCapacity &- (c &+ 1) &* stride) + .bindMemory(to: Element.self, capacity: 1) + + let prefix = c &- slot.value + start.moveInitialize(from: start + 1, count: prefix) + + if bucket.isInvalid { + assert(isCollisionNode) + collisionCount &+= 1 + } else { + assert(!itemMap.contains(bucket)) + assert(!childMap.contains(bucket)) + itemMap.insert(bucket) + assert(itemMap.slot(of: bucket) == slot) + } + + return start + prefix + } + + /// Insert `child` at `slot`. There must be enough free space in the node + /// to fit the new child. + /// + /// `childMap` must not yet reflect the insertion at the time this + /// function is called. This method does not update `childMap`. + @inlinable + internal func _insertChild(_ child: __owned _HashNode, at slot: _HashSlot) { + assertMutable() + assert(!isCollisionNode) + + let c = childMap.count + assert(slot.value <= c) + + let stride = MemoryLayout<_HashNode>.stride + assert(bytesFree >= stride) + bytesFree &-= stride + + _memory.bindMemory(to: _HashNode.self, capacity: c &+ 1) + let q = _childrenStart + slot.value + (q + 1).moveInitialize(from: q, count: c &- slot.value) + q.initialize(to: child) + } +} + +extension _HashNode { + @inlinable @inline(__always) + internal mutating func insertItem( + _ item: __owned Element, at bucket: _Bucket + ) { + let slot = read { $0.itemMap.slot(of: bucket) } + self.insertItem(item, at: slot, bucket) + } + + @inlinable @inline(__always) + internal mutating func insertItem( + _ item: __owned Element, at slot: _HashSlot, _ bucket: _Bucket + ) { + self.count &+= 1 + update { + let p = $0._makeRoomForNewItem(at: slot, bucket) + p.initialize(to: item) + } + } + + /// Insert `child` in `bucket`. There must be enough free space in the + /// node to fit the new child. + @inlinable + internal mutating func insertChild( + _ child: __owned _HashNode, _ bucket: _Bucket + ) { + count &+= child.count + update { + assert(!$0.isCollisionNode) + assert(!$0.itemMap.contains(bucket)) + assert(!$0.childMap.contains(bucket)) + + let slot = $0.childMap.slot(of: bucket) + $0._insertChild(child, at: slot) + $0.childMap.insert(bucket) + } + } +} diff --git a/Sources/HashTreeCollections/HashNode/_HashNode+Primitive Removals.swift b/Sources/HashTreeCollections/HashNode/_HashNode+Primitive Removals.swift new file mode 100644 index 000000000..f8861630c --- /dev/null +++ b/Sources/HashTreeCollections/HashNode/_HashNode+Primitive Removals.swift @@ -0,0 +1,151 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +// MARK: Node-level removal operations + +extension _HashNode.UnsafeHandle { + /// Remove and return the item at `slot`, increasing the amount of free + /// space available in the node. + /// + /// `itemMap` must not yet reflect the removal at the time this + /// function is called. This method does not update `itemMap`. + @inlinable + internal func _removeItem( + at slot: _HashSlot, + by remover: (UnsafeMutablePointer) -> R + ) -> R { + assertMutable() + let c = itemCount + assert(slot.value < c) + let stride = MemoryLayout.stride + bytesFree &+= stride + + let start = _memory + .advanced(by: byteCapacity &- stride &* c) + .assumingMemoryBound(to: Element.self) + + let prefix = c &- 1 &- slot.value + let q = start + prefix + defer { + (start + 1).moveInitialize(from: start, count: prefix) + } + return remover(q) + } + + /// Remove and return the child at `slot`, increasing the amount of free + /// space available in the node. + /// + /// `childMap` must not yet reflect the removal at the time this + /// function is called. This method does not update `childMap`. + @inlinable + internal func _removeChild(at slot: _HashSlot) -> _HashNode { + assertMutable() + assert(!isCollisionNode) + let count = childCount + assert(slot.value < count) + + bytesFree &+= MemoryLayout<_HashNode>.stride + + let q = _childrenStart + slot.value + let child = q.move() + q.moveInitialize(from: q + 1, count: count &- 1 &- slot.value) + return child + } +} + +extension _HashNode { + @inlinable + internal mutating func removeItem( + at bucket: _Bucket + ) -> Element { + let slot = read { $0.itemMap.slot(of: bucket) } + return removeItem(at: bucket, slot, by: { $0.move() }) + } + + @inlinable + internal mutating func removeItem( + at bucket: _Bucket, _ slot: _HashSlot + ) -> Element { + removeItem(at: bucket, slot, by: { $0.move() }) + } + + /// Remove the item at `slot`, increasing the amount of free + /// space available in the node. + /// + /// The closure `remove` is called to perform the deinitialization of the + /// storage slot corresponding to the item to be removed. + @inlinable + internal mutating func removeItem( + at bucket: _Bucket, _ slot: _HashSlot, + by remover: (UnsafeMutablePointer) -> R + ) -> R { + defer { _invariantCheck() } + assert(count > 0) + count &-= 1 + return update { + let old = $0._removeItem(at: slot, by: remover) + if $0.isCollisionNode { + assert(slot.value < $0.collisionCount) + $0.collisionCount &-= 1 + } else { + assert($0.itemMap.contains(bucket)) + assert($0.itemMap.slot(of: bucket) == slot) + $0.itemMap.remove(bucket) + } + return old + } + } + + @inlinable + internal mutating func removeChild( + at bucket: _Bucket, _ slot: _HashSlot + ) -> _HashNode { + assert(!isCollisionNode) + let child: _HashNode = update { + assert($0.childMap.contains(bucket)) + assert($0.childMap.slot(of: bucket) == slot) + let child = $0._removeChild(at: slot) + $0.childMap.remove(bucket) + return child + } + assert(self.count >= child.count) + self.count &-= child.count + return child + } + + @inlinable + internal mutating func removeSingletonItem() -> Element { + defer { _invariantCheck() } + assert(count == 1) + count = 0 + return update { + assert($0.hasSingletonItem) + let old = $0._removeItem(at: .zero) { $0.move() } + $0.clear() + return old + } + } + + @inlinable + internal mutating func removeSingletonChild() -> _HashNode { + defer { _invariantCheck() } + let child: _HashNode = update { + assert($0.hasSingletonChild) + let child = $0._removeChild(at: .zero) + $0.childMap = .empty + return child + } + assert(self.count == child.count) + self.count = 0 + return child + } +} + diff --git a/Sources/HashTreeCollections/HashNode/_HashNode+Primitive Replacement.swift b/Sources/HashTreeCollections/HashNode/_HashNode+Primitive Replacement.swift new file mode 100644 index 000000000..19813d796 --- /dev/null +++ b/Sources/HashTreeCollections/HashNode/_HashNode+Primitive Replacement.swift @@ -0,0 +1,90 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 _HashNode { + @inlinable + internal mutating func replaceItem( + at bucket: _Bucket, _ slot: _HashSlot, with item: __owned Element + ) { + update { + assert($0.isCollisionNode || $0.itemMap.contains(bucket)) + assert($0.isCollisionNode || slot == $0.itemMap.slot(of: bucket)) + assert(!$0.isCollisionNode || slot.value < $0.collisionCount) + $0[item: slot] = item + } + } + + @inlinable + internal mutating func replaceChild( + at bucket: _Bucket, with child: __owned _HashNode + ) -> Int { + let slot = read { $0.childMap.slot(of: bucket) } + return replaceChild(at: bucket, slot, with: child) + } + + @inlinable + internal mutating func replaceChild( + at bucket: _Bucket, _ slot: _HashSlot, with child: __owned _HashNode + ) -> Int { + let delta: Int = update { + assert(!$0.isCollisionNode) + assert($0.childMap.contains(bucket)) + assert($0.childMap.slot(of: bucket) == slot) + let p = $0.childPtr(at: slot) + let delta = child.count &- p.pointee.count + p.pointee = child + return delta + } + self.count &+= delta + return delta + } + + @inlinable + internal func replacingChild( + _ level: _HashLevel, + at bucket: _Bucket, + _ slot: _HashSlot, + with child: __owned Builder + ) -> Builder { + assert(child.level == level.descend()) + read { + assert(!$0.isCollisionNode) + assert($0.childMap.contains(bucket)) + assert(slot == $0.childMap.slot(of: bucket)) + } + switch child.kind { + case .empty: + return _removingChild(level, at: bucket, slot) + case let .item(item, _): + if hasSingletonChild { + return .item(level, item, at: bucket) + } + var node = self.copy(withFreeSpace: _HashNode.spaceForInlinedChild) + _ = node.removeChild(at: bucket, slot) + node.insertItem(item, at: bucket) + node._invariantCheck() + return .node(level, node) + case let .node(node): + var copy = self.copy() + _ = copy.replaceChild(at: bucket, slot, with: node) + return .node(level, copy) + case let .collisionNode(node): + if hasSingletonChild { + // Compression + assert(!level.isAtBottom) + return .collisionNode(level, node) + } + var copy = self.copy() + _ = copy.replaceChild(at: bucket, slot, with: node) + return .node(level, copy) + } + } +} diff --git a/Sources/HashTreeCollections/HashNode/_HashNode+Storage.swift b/Sources/HashTreeCollections/HashNode/_HashNode+Storage.swift new file mode 100644 index 000000000..c37e07ba5 --- /dev/null +++ b/Sources/HashTreeCollections/HashNode/_HashNode+Storage.swift @@ -0,0 +1,279 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +/// A base representation of a hash tree node, capturing functionality +/// independent of the `Key` and `Value` types. +@usableFromInline +internal typealias _RawHashStorage = ManagedBuffer<_HashNodeHeader, _RawHashNode> + +/// Type-punned storage for the singleton root node used in empty hash trees +/// (of all `Key` and `Value` types). +/// +/// `_HashNode` is carefully defined to use a `_RawHashStorage` reference as its +/// storage variable, so that this can work. (The only reason we need the +/// `_HashNode.Storage` subclass is to allow storage instances to properly +/// clean up after themselves in their `deinit` method.) +@usableFromInline +internal let _emptySingleton: _RawHashStorage = _RawHashStorage.create( + minimumCapacity: 0, + makingHeaderWith: { _ in _HashNodeHeader(byteCapacity: 0) }) + +extension _HashNode { + /// Instances of this class hold (tail-allocated) storage for individual + /// nodes in a hash tree. + @usableFromInline + internal final class Storage: _RawHashStorage { + @usableFromInline + internal typealias Element = (key: Key, value: Value) + + @usableFromInline + internal typealias UnsafeHandle = _HashNode.UnsafeHandle + + deinit { + UnsafeHandle.update(self) { handle in + handle.children.deinitialize() + handle.reverseItems.deinitialize() + } + } + } +} + +extension _HashNode.Storage { + @inlinable + internal static func allocate(byteCapacity: Int) -> _HashNode.Storage { + assert(byteCapacity >= 0) + + let itemStride = MemoryLayout.stride + let childStride = MemoryLayout<_HashNode>.stride + let unit = Swift.max(itemStride, childStride) + + // Round up request to nearest power-of-two number of units. + // We'll allow allocations of space that fits 0, 1, 2, 4, 8, 16 or 32 + // units. + var capacityInUnits = (byteCapacity &+ unit &- 1) / unit +#if false // Enable to set a larger minimum node size + if capacityInUnits != 0 { + capacityInUnits = Swift.max(capacityInUnits, 4) + } +#endif + var bytes = unit * capacityInUnits._roundUpToPowerOfTwo() + + let itemAlignment = MemoryLayout.alignment + let childAlignment = MemoryLayout<_HashNode>.alignment + if itemAlignment > childAlignment { + // Make sure we always have enough room to properly align trailing items. + bytes += itemAlignment - childAlignment + } + + let object = _HashNode.Storage.create( + minimumCapacity: (bytes &+ childStride &- 1) / childStride + ) { buffer in + _HashNodeHeader(byteCapacity: buffer.capacity * childStride) + } + + object.withUnsafeMutablePointers { header, elements in + let start = UnsafeRawPointer(elements) + let end = start + .advanced(by: Int(header.pointee.byteCapacity)) + .alignedDown(for: Element.self) + header.pointee._byteCapacity = UInt32(start.distance(to: end)) + header.pointee._bytesFree = header.pointee._byteCapacity + assert(byteCapacity <= header.pointee.byteCapacity) + } + return unsafeDowncast(object, to: _HashNode.Storage.self) + } +} + +extension _HashNode { + @inlinable @inline(__always) + internal static var spaceForNewItem: Int { + MemoryLayout.stride + } + + @inlinable @inline(__always) + internal static var spaceForNewChild: Int { + MemoryLayout<_HashNode>.stride + } + + @inlinable @inline(__always) + internal static var spaceForSpawningChild: Int { + Swift.max(0, spaceForNewChild - spaceForNewItem) + } + + @inlinable @inline(__always) + internal static var spaceForInlinedChild: Int { + Swift.max(0, spaceForNewItem - spaceForNewChild) + } + + @inlinable + internal mutating func isUnique() -> Bool { + isKnownUniquelyReferenced(&self.raw.storage) + } + + @inlinable + internal func hasFreeSpace(_ bytes: Int) -> Bool { + bytes <= self.raw.storage.header.bytesFree + } + + @inlinable + internal mutating func ensureUnique(isUnique: Bool) { + if !isUnique { + self = copy() + } + } + + @inlinable + internal mutating func ensureUnique( + isUnique: Bool, + withFreeSpace minimumFreeBytes: Int = 0 + ) { + if !isUnique { + self = copy(withFreeSpace: minimumFreeBytes) + } else if !hasFreeSpace(minimumFreeBytes) { + move(withFreeSpace: minimumFreeBytes) + } + } + + + @inlinable + internal static func allocate( + itemMap: _Bitmap, childMap: _Bitmap, + count: Int, + extraBytes: Int = 0, + initializingWith initializer: ( + UnsafeMutableBufferPointer<_HashNode>, UnsafeMutableBufferPointer + ) -> R + ) -> (node: _HashNode, result: R) { + assert(extraBytes >= 0) + assert(itemMap.isDisjoint(with: childMap)) // No collisions + let itemCount = itemMap.count + let childCount = childMap.count + + let itemStride = MemoryLayout.stride + let childStride = MemoryLayout<_HashNode>.stride + + let itemBytes = itemCount * itemStride + let childBytes = childCount * childStride + let occupiedBytes = itemBytes &+ childBytes + let storage = Storage.allocate( + byteCapacity: occupiedBytes &+ extraBytes) + var node = _HashNode(storage: storage, count: count) + let result: R = node.update { + $0.itemMap = itemMap + $0.childMap = childMap + + assert(occupiedBytes <= $0.bytesFree) + $0.bytesFree &-= occupiedBytes + + let childStart = $0._memory + .bindMemory(to: _HashNode.self, capacity: childCount) + let itemStart = ($0._memory + ($0.byteCapacity - itemBytes)) + .bindMemory(to: Element.self, capacity: itemCount) + + return initializer( + UnsafeMutableBufferPointer(start: childStart, count: childCount), + UnsafeMutableBufferPointer(start: itemStart, count: itemCount)) + } + return (node, result) + } + + @inlinable + internal static func allocateCollision( + count: Int, + _ hash: _Hash, + extraBytes: Int = 0, + initializingWith initializer: (UnsafeMutableBufferPointer) -> R + ) -> (node: _HashNode, result: R) { + assert(count >= 2) + assert(extraBytes >= 0) + let itemBytes = count * MemoryLayout.stride + let hashBytes = MemoryLayout<_Hash>.stride + let bytes = itemBytes &+ hashBytes + assert(MemoryLayout<_Hash>.alignment <= MemoryLayout<_RawHashNode>.alignment) + let storage = Storage.allocate(byteCapacity: bytes &+ extraBytes) + var node = _HashNode(storage: storage, count: count) + let result: R = node.update { + $0.itemMap = _Bitmap(bitPattern: count) + $0.childMap = $0.itemMap + assert(bytes <= $0.bytesFree) + $0.bytesFree &-= bytes + + $0._memory.storeBytes(of: hash, as: _Hash.self) + + let itemStart = ($0._memory + ($0.byteCapacity &- itemBytes)) + .bindMemory(to: Element.self, capacity: count) + + let items = UnsafeMutableBufferPointer(start: itemStart, count: count) + return initializer(items) + } + return (node, result) + } + + + @inlinable @inline(never) + internal func copy(withFreeSpace space: Int = 0) -> _HashNode { + assert(space >= 0) + + if isCollisionNode { + return read { src in + Self.allocateCollision( + count: self.count, self.collisionHash + ) { dstItems in + dstItems.initializeAll(fromContentsOf: src.reverseItems) + }.node + } + } + return read { src in + Self.allocate( + itemMap: src.itemMap, + childMap: src.childMap, + count: self.count, + extraBytes: space + ) { dstChildren, dstItems in + dstChildren.initializeAll(fromContentsOf: src.children) + dstItems.initializeAll(fromContentsOf: src.reverseItems) + }.node + } + } + + @inlinable @inline(never) + internal mutating func move(withFreeSpace space: Int = 0) { + assert(space >= 0) + let c = self.count + if isCollisionNode { + self = update { src in + Self.allocateCollision( + count: c, src.collisionHash + ) { dstItems in + dstItems.moveInitializeAll(fromContentsOf: src.reverseItems) + src.clear() + }.node + } + return + } + self = update { src in + Self.allocate( + itemMap: src.itemMap, + childMap: src.childMap, + count: c, + extraBytes: space + ) { dstChildren, dstItems in + dstChildren.moveInitializeAll(fromContentsOf: src.children) + dstItems.moveInitializeAll(fromContentsOf: src.reverseItems) + src.clear() + }.node + } + } +} diff --git a/Sources/HashTreeCollections/HashNode/_HashNode+Structural compactMapValues.swift b/Sources/HashTreeCollections/HashNode/_HashNode+Structural compactMapValues.swift new file mode 100644 index 000000000..b7c1901dd --- /dev/null +++ b/Sources/HashTreeCollections/HashNode/_HashNode+Structural compactMapValues.swift @@ -0,0 +1,46 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 _HashNode { + @inlinable + internal func compactMapValues( + _ level: _HashLevel, + _ transform: (Value) throws -> T? + ) rethrows -> _HashNode.Builder { + return try self.read { + var result: _HashNode.Builder = .empty(level) + + if isCollisionNode { + let items = $0.reverseItems + for i in items.indices { + if let v = try transform(items[i].value) { + result.addNewCollision(level, (items[i].key, v), $0.collisionHash) + } + } + return result + } + + for (bucket, slot) in $0.itemMap { + let p = $0.itemPtr(at: slot) + if let v = try transform(p.pointee.value) { + result.addNewItem(level, (p.pointee.key, v), at: bucket) + } + } + + for (bucket, slot) in $0.childMap { + let branch = try $0[child: slot] + .compactMapValues(level.descend(), transform) + result.addNewChildBranch(level, branch, at: bucket) + } + return result + } + } +} diff --git a/Sources/HashTreeCollections/HashNode/_HashNode+Structural filter.swift b/Sources/HashTreeCollections/HashNode/_HashNode+Structural filter.swift new file mode 100644 index 000000000..535fee20b --- /dev/null +++ b/Sources/HashTreeCollections/HashNode/_HashNode+Structural filter.swift @@ -0,0 +1,83 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 _HashNode { + @inlinable + internal func filter( + _ level: _HashLevel, + _ isIncluded: (Element) throws -> Bool + ) rethrows -> Builder? { + guard !isCollisionNode else { + return try _filter_slow(level, isIncluded) + } + return try self.read { + var result: Builder = .empty(level) + var removing = false // true if we need to remove something + + for (bucket, slot) in $0.itemMap { + let p = $0.itemPtr(at: slot) + let include = try isIncluded(p.pointee) + switch (include, removing) { + case (true, true): + result.addNewItem(level, p.pointee, at: bucket) + case (false, false): + removing = true + result.copyItems(level, from: $0, upTo: bucket) + default: + break + } + } + + for (bucket, slot) in $0.childMap { + let branch = try $0[child: slot].filter(level.descend(), isIncluded) + if let branch = branch { + assert(branch.count < self.count) + if !removing { + removing = true + result.copyItemsAndChildren(level, from: $0, upTo: bucket) + } + result.addNewChildBranch(level, branch, at: bucket) + } else if removing { + result.addNewChildNode(level, $0[child: slot], at: bucket) + } + } + + guard removing else { return nil } + return result + } + } + + @inlinable @inline(never) + internal func _filter_slow( + _ level: _HashLevel, + _ isIncluded: (Element) throws -> Bool + ) rethrows -> Builder? { + try self.read { + var result: Builder = .empty(level) + var removing = false + + for slot: _HashSlot in stride(from: .zero, to: $0.itemsEndSlot, by: 1) { + let p = $0.itemPtr(at: slot) + let include = try isIncluded(p.pointee) + if include, removing { + result.addNewCollision(level, p.pointee, $0.collisionHash) + } + else if !include, !removing { + removing = true + result.copyCollisions(from: $0, upTo: slot) + } + } + guard removing else { return nil } + assert(result.count < self.count) + return result + } + } +} diff --git a/Sources/HashTreeCollections/HashNode/_HashNode+Structural intersection.swift b/Sources/HashTreeCollections/HashNode/_HashNode+Structural intersection.swift new file mode 100644 index 000000000..a5255734a --- /dev/null +++ b/Sources/HashTreeCollections/HashNode/_HashNode+Structural intersection.swift @@ -0,0 +1,195 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 _HashNode { + @inlinable + internal func intersection( + _ level: _HashLevel, + _ other: _HashNode + ) -> _HashNode? { + assert(level.isAtRoot) + let builder = _intersection(level, other) + guard let builder = builder else { return nil } + let root = builder.finalize(.top) + root._fullInvariantCheck() + return root + } + + @inlinable + internal func _intersection( + _ level: _HashLevel, + _ other: _HashNode + ) -> Builder? { + if self.raw.storage === other.raw.storage { return nil } + + if self.isCollisionNode || other.isCollisionNode { + return _intersection_slow(level, other) + } + + return self.read { l in + other.read { r in + var result: Builder = .empty(level) + var removing = false + + for (bucket, lslot) in l.itemMap { + let lp = l.itemPtr(at: lslot) + let include: Bool + if r.itemMap.contains(bucket) { + let rslot = r.itemMap.slot(of: bucket) + include = (lp.pointee.key == r[item: rslot].key) + } + else if r.childMap.contains(bucket) { + let rslot = r.childMap.slot(of: bucket) + let h = _Hash(lp.pointee.key) + include = r[child: rslot] + .containsKey(level.descend(), lp.pointee.key, h) + } + else { include = false} + + if include, removing { + result.addNewItem(level, lp.pointee, at: bucket) + } + else if !include, !removing { + removing = true + result.copyItems(level, from: l, upTo: bucket) + } + } + + for (bucket, lslot) in l.childMap { + if r.itemMap.contains(bucket) { + if !removing { + removing = true + result.copyItemsAndChildren(level, from: l, upTo: bucket) + } + let rslot = r.itemMap.slot(of: bucket) + let rp = r.itemPtr(at: rslot) + let h = _Hash(rp.pointee.key) + let res = l[child: lslot].lookup(level.descend(), rp.pointee.key, h) + if let res = res { + let item = UnsafeHandle.read(res.node) { $0[item: res.slot] } + result.addNewItem(level, item, at: bucket) + } + } + else if r.childMap.contains(bucket) { + let rslot = r.childMap.slot(of: bucket) + let branch = l[child: lslot] + ._intersection(level.descend(), r[child: rslot]) + if let branch = branch { + assert(branch.count < self.count) + if !removing { + removing = true + result.copyItemsAndChildren(level, from: l, upTo: bucket) + } + result.addNewChildBranch(level, branch, at: bucket) + } else if removing { + result.addNewChildNode(level, l[child: lslot], at: bucket) + } + } + else if !removing { + removing = true + result.copyItemsAndChildren(level, from: l, upTo: bucket) + } + } + guard removing else { return nil } + return result + } + } + } + + @inlinable @inline(never) + internal func _intersection_slow( + _ level: _HashLevel, + _ other: _HashNode + ) -> Builder? { + let lc = self.isCollisionNode + let rc = other.isCollisionNode + if lc && rc { + return read { l in + other.read { r in + var result: Builder = .empty(level) + guard l.collisionHash == r.collisionHash else { return result } + + var removing = false + let ritems = r.reverseItems + for lslot: _HashSlot in stride(from: .zero, to: l.itemsEndSlot, by: 1) { + let lp = l.itemPtr(at: lslot) + let include = ritems.contains { $0.key == lp.pointee.key } + if include, removing { + result.addNewCollision(level, lp.pointee, l.collisionHash) + } + else if !include, !removing { + removing = true + result.copyCollisions(from: l, upTo: lslot) + } + } + guard removing else { return nil } + assert(result.count < self.count) + return result + } + } + } + + // One of the nodes must be on a compressed path. + assert(!level.isAtBottom) + + if lc { + // `self` is a collision node on a compressed path. The other tree might + // have the same set of collisions, just expanded a bit deeper. + return read { l in + other.read { r in + let bucket = l.collisionHash[level] + if r.itemMap.contains(bucket) { + let rslot = r.itemMap.slot(of: bucket) + let ritem = r.itemPtr(at: rslot) + let litems = l.reverseItems + let i = litems.firstIndex { $0.key == ritem.pointee.key } + guard let i = i else { return .empty(level) } + return .item(level, litems[i], at: l.collisionHash[level]) + } + if r.childMap.contains(bucket) { + let rslot = r.childMap.slot(of: bucket) + return _intersection(level.descend(), r[child: rslot]) + .map { .childBranch(level, $0, at: bucket) } + } + return .empty(level) + } + } + } + + assert(rc) + // `other` is a collision node on a compressed path. + return read { l in + other.read { r in + let bucket = r.collisionHash[level] + if l.itemMap.contains(bucket) { + let lslot = l.itemMap.slot(of: bucket) + let litem = l.itemPtr(at: lslot) + let ritems = r.reverseItems + let found = ritems.contains { $0.key == litem.pointee.key } + guard found else { return .empty(level) } + return .item(level, litem.pointee, at: bucket) + } + if l.childMap.contains(bucket) { + let lslot = l.childMap.slot(of: bucket) + let branch = l[child: lslot]._intersection(level.descend(), other) + guard let branch = branch else { + assert(l[child: lslot].isCollisionNode) + assert(l[child: lslot].collisionHash == r.collisionHash) + // Compression + return .collisionNode(level, l[child: lslot]) + } + return .childBranch(level, branch, at: bucket) + } + return .empty(level) + } + } + } +} diff --git a/Sources/HashTreeCollections/HashNode/_HashNode+Structural isDisjoint.swift b/Sources/HashTreeCollections/HashNode/_HashNode+Structural isDisjoint.swift new file mode 100644 index 000000000..a2d9609fc --- /dev/null +++ b/Sources/HashTreeCollections/HashNode/_HashNode+Structural isDisjoint.swift @@ -0,0 +1,111 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 _HashNode { + /// Returns true if `self` contains a disjoint set of keys than `other`. + /// Otherwise, returns false. + @inlinable @inline(never) + internal func isDisjoint( + _ level: _HashLevel, + with other: _HashNode + ) -> Bool { + if self.count == 0 || other.count == 0 { return true } + if self.raw.storage === other.raw.storage { return false } + + if self.isCollisionNode { + return _isDisjointCollision(level, with: other) + } + if other.isCollisionNode { + return other._isDisjointCollision(level, with: self) + } + + return self.read { l in + other.read { r in + let lmap = l.itemMap.union(l.childMap) + let rmap = r.itemMap.union(r.childMap) + if lmap.isDisjoint(with: rmap) { return true } + + for (bucket, _) in l.itemMap.intersection(r.itemMap) { + let lslot = l.itemMap.slot(of: bucket) + let rslot = r.itemMap.slot(of: bucket) + guard l[item: lslot].key != r[item: rslot].key else { return false } + } + for (bucket, _) in l.itemMap.intersection(r.childMap) { + let lslot = l.itemMap.slot(of: bucket) + let hash = _Hash(l[item: lslot].key) + let rslot = r.childMap.slot(of: bucket) + let found = r[child: rslot].containsKey( + level.descend(), + l[item: lslot].key, + hash) + if found { return false } + } + for (bucket, _) in l.childMap.intersection(r.itemMap) { + let lslot = l.childMap.slot(of: bucket) + let rslot = r.itemMap.slot(of: bucket) + let hash = _Hash(r[item: rslot].key) + let found = l[child: lslot].containsKey( + level.descend(), + r[item: rslot].key, + hash) + if found { return false } + } + for (bucket, _) in l.childMap.intersection(r.childMap) { + let lslot = l.childMap.slot(of: bucket) + let rslot = r.childMap.slot(of: bucket) + guard + l[child: lslot].isDisjoint(level.descend(), with: r[child: rslot]) + else { return false } + } + return true + } + } + } + + @inlinable @inline(never) + internal func _isDisjointCollision( + _ level: _HashLevel, + with other: _HashNode + ) -> Bool { + assert(isCollisionNode) + if other.isCollisionNode { + return read { l in + other.read { r in + guard l.collisionHash == r.collisionHash else { return true } + let litems = l.reverseItems + let ritems = r.reverseItems + return litems.allSatisfy { li in + !ritems.contains { ri in li.key == ri.key } + } + } + } + } + // `self` is on a compressed path. Try descending down by one level. + assert(!level.isAtBottom) + let bucket = self.collisionHash[level] + return other.read { r in + if r.childMap.contains(bucket) { + let slot = r.childMap.slot(of: bucket) + return isDisjoint(level.descend(), with: r[child: slot]) + } + if r.itemMap.contains(bucket) { + let rslot = r.itemMap.slot(of: bucket) + let p = r.itemPtr(at: rslot) + let hash = _Hash(p.pointee.key) + return read { l in + guard hash == l.collisionHash else { return true } + return !l.reverseItems.contains { $0.key == p.pointee.key } + } + } + return true + } + } +} diff --git a/Sources/HashTreeCollections/HashNode/_HashNode+Structural isEqualSet.swift b/Sources/HashTreeCollections/HashNode/_HashNode+Structural isEqualSet.swift new file mode 100644 index 000000000..bbf5d3ad5 --- /dev/null +++ b/Sources/HashTreeCollections/HashNode/_HashNode+Structural isEqualSet.swift @@ -0,0 +1,61 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +// TODO: `Equatable` needs more test coverage, apart from hash-collision smoke test +extension _HashNode { + @inlinable + internal func isEqualSet( + to other: _HashNode, + by areEquivalent: (Value, Value2) -> Bool + ) -> Bool { + if self.raw.storage === other.raw.storage { return true } + + guard self.count == other.count else { return false } + + if self.isCollisionNode { + guard other.isCollisionNode else { return false } + return self.read { lhs in + other.read { rhs in + guard lhs.collisionHash == rhs.collisionHash else { return false } + let l = lhs.reverseItems + let r = rhs.reverseItems + assert(l.count == r.count) // Already checked above + for i in l.indices { + let found = r.contains { + l[i].key == $0.key && areEquivalent(l[i].value, $0.value) + } + guard found else { return false } + } + return true + } + } + } + guard !other.isCollisionNode else { return false } + + return self.read { l in + other.read { r in + guard l.itemMap == r.itemMap else { return false } + guard l.childMap == r.childMap else { return false } + + guard l.reverseItems.elementsEqual( + r.reverseItems, + by: { $0.key == $1.key && areEquivalent($0.value, $1.value) }) + else { return false } + + let lc = l.children + let rc = r.children + return lc.elementsEqual( + rc, + by: { $0.isEqualSet(to: $1, by: areEquivalent) }) + } + } + } +} diff --git a/Sources/HashTreeCollections/HashNode/_HashNode+Structural isSubset.swift b/Sources/HashTreeCollections/HashNode/_HashNode+Structural isSubset.swift new file mode 100644 index 000000000..776d01b8e --- /dev/null +++ b/Sources/HashTreeCollections/HashNode/_HashNode+Structural isSubset.swift @@ -0,0 +1,89 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 _HashNode { + /// Returns true if `self` contains a subset of the keys in `other`. + /// Otherwise, returns false. + @inlinable @inline(never) + internal func isSubset( + _ level: _HashLevel, + of other: _HashNode + ) -> Bool { + guard self.count > 0 else { return true } + if self.raw.storage === other.raw.storage { return true } + guard self.count <= other.count else { return false } + + if self.isCollisionNode { + if other.isCollisionNode { + guard self.collisionHash == other.collisionHash else { return false } + return read { l in + other.read { r in + let li = l.reverseItems + let ri = r.reverseItems + return l.reverseItems.indices.allSatisfy { i in + ri.contains { $0.key == li[i].key } + } + } + } + } + // `self` is on a compressed path. Try to descend down by one level. + assert(!level.isAtBottom) + let bucket = self.collisionHash[level] + return other.read { + guard $0.childMap.contains(bucket) else { return false } + let slot = $0.childMap.slot(of: bucket) + return self.isSubset(level.descend(), of: $0[child: slot]) + } + } + if other.isCollisionNode { + return read { l in + guard level.isAtRoot, l.hasSingletonItem else { return false } + // Annoying special case: the root node may contain a single item + // that matches one in the collision node. + return other.read { r in + let hash = _Hash(l[item: .zero].key) + return r.find(level, l[item: .zero].key, hash) != nil + } + } + } + + return self.read { l in + other.read { r in + guard l.childMap.isSubset(of: r.childMap) else { return false } + guard l.itemMap.isSubset(of: r.itemMap.union(r.childMap)) else { + return false + } + for (bucket, lslot) in l.itemMap { + if r.itemMap.contains(bucket) { + let rslot = r.itemMap.slot(of: bucket) + guard l[item: lslot].key == r[item: rslot].key else { return false } + } else { + let hash = _Hash(l[item: lslot].key) + let rslot = r.childMap.slot(of: bucket) + guard + r[child: rslot].containsKey( + level.descend(), + l[item: lslot].key, + hash) + else { return false } + } + } + + for (bucket, lslot) in l.childMap { + let rslot = r.childMap.slot(of: bucket) + guard l[child: lslot].isSubset(level.descend(), of: r[child: rslot]) + else { return false } + } + return true + } + } + } +} diff --git a/Sources/HashTreeCollections/HashNode/_HashNode+Structural mapValues.swift b/Sources/HashTreeCollections/HashNode/_HashNode+Structural mapValues.swift new file mode 100644 index 000000000..4f54d28a4 --- /dev/null +++ b/Sources/HashTreeCollections/HashNode/_HashNode+Structural mapValues.swift @@ -0,0 +1,90 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension _HashNode { + @inlinable + internal func mapValues( + _ transform: (Element) throws -> T + ) rethrows -> _HashNode { + let c = self.count + return try read { source in + var result: _HashNode + if isCollisionNode { + result = _HashNode.allocateCollision( + count: c, source.collisionHash, + initializingWith: { _ in } + ).node + } else { + result = _HashNode.allocate( + itemMap: source.itemMap, + childMap: source.childMap, + count: c, + initializingWith: { _, _ in } + ).node + } + try result.update { target in + let sourceItems = source.reverseItems + let targetItems = target.reverseItems + assert(sourceItems.count == targetItems.count) + + let sourceChildren = source.children + let targetChildren = target.children + assert(sourceChildren.count == targetChildren.count) + + var i = 0 + var j = 0 + + var success = false + + defer { + if !success { + targetItems.prefix(i).deinitialize() + targetChildren.prefix(j).deinitialize() + target.clear() + } + } + + while i < targetItems.count { + let key = sourceItems[i].key + let value = try transform(sourceItems[i]) + targetItems.initializeElement(at: i, to: (key, value)) + i += 1 + } + while j < targetChildren.count { + let child = try sourceChildren[j].mapValues(transform) + targetChildren.initializeElement(at: j, to: child) + j += 1 + } + success = true + } + result._invariantCheck() + return result + } + } + + @inlinable + internal func mapValuesToVoid( + copy: Bool = false, extraBytes: Int = 0 + ) -> _HashNode { + if Value.self == Void.self { + let node = unsafeBitCast(self, to: _HashNode.self) + guard copy || !node.hasFreeSpace(extraBytes) else { return node } + return node.copy(withFreeSpace: extraBytes) + } + let node = mapValues { _ in () } + guard !node.hasFreeSpace(extraBytes) else { return node } + return node.copy(withFreeSpace: extraBytes) + } +} diff --git a/Sources/HashTreeCollections/HashNode/_HashNode+Structural merge.swift b/Sources/HashTreeCollections/HashNode/_HashNode+Structural merge.swift new file mode 100644 index 000000000..d793e14f9 --- /dev/null +++ b/Sources/HashTreeCollections/HashNode/_HashNode+Structural merge.swift @@ -0,0 +1,329 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 _HashNode { + /// - Returns: The number of new items added to `self`. + @inlinable + internal mutating func merge( + _ level: _HashLevel, + _ other: _HashNode, + _ combine: (Value, Value) throws -> Value + ) rethrows -> Int { + guard other.count > 0 else { return 0 } + guard self.count > 0 else { + self = other + return self.count + } + if level.isAtRoot, self.hasSingletonItem { + // In this special case, the root node may turn into a collision node + // during the merge process. Prevent this from causing issues below by + // handling it up front. + var copy = other + let delta = try self.read { l in + let lp = l.itemPtr(at: .zero) + let c = copy.count + let res = copy.updateValue( + level, forKey: lp.pointee.key, _Hash(lp.pointee.key) + ) { + $0.initialize(to: lp.pointee) + } + if !res.inserted { + try UnsafeHandle.update(res.leaf) { + let p = $0.itemPtr(at: res.slot) + p.pointee.value = try combine(lp.pointee.value, p.pointee.value) + } + } + return c - (res.inserted ? 0 : 1) + } + self = copy + return delta + } + + return try _merge(level, other, combine) + } + + @inlinable + internal mutating func _merge( + _ level: _HashLevel, + _ other: _HashNode, + _ combine: (Value, Value) throws -> Value + ) rethrows -> Int { + // Note: don't check storage identities -- we do need to merge the contents + // of identical nodes. + + if self.isCollisionNode || other.isCollisionNode { + return try _merge_slow(level, other, combine) + } + + return try other.read { r in + var isUnique = self.isUnique() + var delta = 0 + + let (originalItems, originalChildren) = self.read { + ($0.itemMap, $0.childMap) + } + + for (bucket, _) in originalItems { + assert(!isCollisionNode) + if r.itemMap.contains(bucket) { + let rslot = r.itemMap.slot(of: bucket) + let rp = r.itemPtr(at: rslot) + let lslot = self.read { $0.itemMap.slot(of: bucket) } + let conflict = self.read { $0[item: lslot].key == rp.pointee.key } + if conflict { + self.ensureUnique(isUnique: isUnique) + try self.update { + let p = $0.itemPtr(at: lslot) + p.pointee.value = try combine(p.pointee.value, rp.pointee.value) + } + } else { + _ = self.ensureUniqueAndSpawnChild( + isUnique: isUnique, + level: level, + replacing: bucket, + itemSlot: lslot, + newHash: _Hash(rp.pointee.key), + { $0.initialize(to: rp.pointee) }) + // If we hadn't handled the singleton root node case above, + // then this call would sometimes turn `self` into a collision + // node on a compressed path, causing mischief. + assert(!self.isCollisionNode) + delta &+= 1 + } + isUnique = true + } + else if r.childMap.contains(bucket) { + let rslot = r.childMap.slot(of: bucket) + let rp = r.childPtr(at: rslot) + + self.ensureUnique( + isUnique: isUnique, withFreeSpace: _HashNode.spaceForSpawningChild) + let item = self.removeItem(at: bucket) + delta &-= 1 + var child = rp.pointee + let r = child.updateValue( + level.descend(), forKey: item.key, _Hash(item.key) + ) { + $0.initialize(to: item) + } + if !r.inserted { + try UnsafeHandle.update(r.leaf) { + let p = $0.itemPtr(at: r.slot) + p.pointee.value = try combine(item.value, p.pointee.value) + } + } + self.insertChild(child, bucket) + isUnique = true + delta &+= child.count + } + } + + for (bucket, _) in originalChildren { + assert(!isCollisionNode) + let lslot = self.read { $0.childMap.slot(of: bucket) } + + if r.itemMap.contains(bucket) { + let rslot = r.itemMap.slot(of: bucket) + let rp = r.itemPtr(at: rslot) + self.ensureUnique(isUnique: isUnique) + let h = _Hash(rp.pointee.key) + let res = self.update { l in + l[child: lslot].updateValue( + level.descend(), forKey: rp.pointee.key, h + ) { + $0.initialize(to: rp.pointee) + } + } + if res.inserted { + self.count &+= 1 + delta &+= 1 + } else { + try UnsafeHandle.update(res.leaf) { + let p = $0.itemPtr(at: res.slot) + p.pointee.value = try combine(p.pointee.value, rp.pointee.value) + } + } + isUnique = true + } + else if r.childMap.contains(bucket) { + let rslot = r.childMap.slot(of: bucket) + self.ensureUnique(isUnique: isUnique) + let d = try self.update { l in + try l[child: lslot].merge( + level.descend(), + r[child: rslot], + combine) + } + self.count &+= d + delta &+= d + isUnique = true + } + } + + assert(!self.isCollisionNode) + + /// Add buckets in `other` that we haven't processed above. + let seen = self.read { l in l.itemMap.union(l.childMap) } + for (bucket, _) in r.itemMap.subtracting(seen) { + let rslot = r.itemMap.slot(of: bucket) + self.ensureUniqueAndInsertItem( + isUnique: isUnique, r[item: rslot], at: bucket) + delta &+= 1 + isUnique = true + } + for (bucket, _) in r.childMap.subtracting(seen) { + let rslot = r.childMap.slot(of: bucket) + self.ensureUnique( + isUnique: isUnique, withFreeSpace: _HashNode.spaceForNewChild) + self.insertChild(r[child: rslot], bucket) + delta &+= r[child: rslot].count + isUnique = true + } + + assert(isUnique) + return delta + } + } + + @inlinable @inline(never) + internal mutating func _merge_slow( + _ level: _HashLevel, + _ other: _HashNode, + _ combine: (Value, Value) throws -> Value + ) rethrows -> Int { + let lc = self.isCollisionNode + let rc = other.isCollisionNode + if lc && rc { + guard self.collisionHash == other.collisionHash else { + self = _HashNode.build( + level: level, + child1: self, self.collisionHash, + child2: other, other.collisionHash) + return other.count + } + return try other.read { r in + var isUnique = self.isUnique() + var delta = 0 + let originalItemCount = self.count + for rs: _HashSlot in stride(from: .zero, to: r.itemsEndSlot, by: 1) { + let rp = r.itemPtr(at: rs) + let lslot: _HashSlot? = self.read { l in + let litems = l.reverseItems + return litems + .suffix(originalItemCount) + .firstIndex { $0.key == rp.pointee.key } + .map { _HashSlot(litems.count &- 1 &- $0) } + } + if let lslot = lslot { + self.ensureUnique(isUnique: isUnique) + try self.update { + let p = $0.itemPtr(at: lslot) + p.pointee.value = try combine(p.pointee.value, rp.pointee.value) + } + } else { + _ = self.ensureUniqueAndAppendCollision( + isUnique: isUnique, rp.pointee) + delta &+= 1 + } + isUnique = true + } + return delta + } + } + + // One of the nodes must be on a compressed path. + assert(!level.isAtBottom) + + if lc { + // `self` is a collision node on a compressed path. The other tree might + // have the same set of collisions, just expanded a bit deeper. + return try other.read { r in + let bucket = self.collisionHash[level] + if r.itemMap.contains(bucket) { + let rslot = r.itemMap.slot(of: bucket) + let rp = r.itemPtr(at: rslot) + + let h = _Hash(rp.pointee.key) + let res = self.updateValue( + level.descend(), forKey: rp.pointee.key, h + ) { + $0.initialize(to: rp.pointee) + } + if !res.inserted { + try UnsafeHandle.update(res.leaf) { + let p = $0.itemPtr(at: res.slot) + p.pointee.value = try combine(p.pointee.value, rp.pointee.value) + } + } + self = other._copyNodeAndReplaceItemWithNewChild( + level: level, self, at: bucket, itemSlot: rslot) + return other.count - (res.inserted ? 0 : 1) + } + + if r.childMap.contains(bucket) { + let originalCount = self.count + let rslot = r.childMap.slot(of: bucket) + _ = try self._merge(level.descend(), r[child: rslot], combine) + var node = other.copy() + _ = node.replaceChild(at: bucket, rslot, with: self) + self = node + return self.count - originalCount + } + + var node = other.copy(withFreeSpace: _HashNode.spaceForNewChild) + node.insertChild(self, bucket) + self = node + return other.count + } + } + + assert(rc) + let isUnique = self.isUnique() + // `other` is a collision node on a compressed path. + return try other.read { r in + let bucket = r.collisionHash[level] + if self.read({ $0.itemMap.contains(bucket) }) { + self.ensureUnique( + isUnique: isUnique, withFreeSpace: _HashNode.spaceForSpawningChild) + let item = self.removeItem(at: bucket) + let h = _Hash(item.key) + var copy = other + let res = copy.updateValue(level.descend(), forKey: item.key, h) { + $0.initialize(to: item) + } + if !res.inserted { + try UnsafeHandle.update(res.leaf) { + let p = $0.itemPtr(at: res.slot) + p.pointee.value = try combine(item.value, p.pointee.value) + } + } + assert(self.count > 0) // Singleton case handled up front above + self.insertChild(copy, bucket) + return other.count - (res.inserted ? 0 : 1) + } + if self.read({ $0.childMap.contains(bucket) }) { + self.ensureUnique(isUnique: isUnique) + let delta: Int = try self.update { l in + let lslot = l.childMap.slot(of: bucket) + let lchild = l.childPtr(at: lslot) + return try lchild.pointee._merge(level.descend(), other, combine) + } + assert(delta >= 0) + self.count &+= delta + return delta + } + self.ensureUnique( + isUnique: isUnique, withFreeSpace: _HashNode.spaceForNewChild) + self.insertChild(other, bucket) + return other.count + } + } +} diff --git a/Sources/HashTreeCollections/HashNode/_HashNode+Structural subtracting.swift b/Sources/HashTreeCollections/HashNode/_HashNode+Structural subtracting.swift new file mode 100644 index 000000000..4e0386504 --- /dev/null +++ b/Sources/HashTreeCollections/HashNode/_HashNode+Structural subtracting.swift @@ -0,0 +1,199 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 _HashNode { + @inlinable + internal func subtracting( + _ level: _HashLevel, + _ other: _HashNode + ) -> _HashNode? { + assert(level.isAtRoot) + let builder = _subtracting(level, other) + guard let builder = builder else { return nil } + let root = builder.finalize(.top) + root._fullInvariantCheck() + return root + } + + @inlinable + internal func _subtracting( + _ level: _HashLevel, + _ other: _HashNode + ) -> Builder? { + if self.raw.storage === other.raw.storage { return .empty(level) } + + if self.isCollisionNode || other.isCollisionNode { + return _subtracting_slow(level, other) + } + + return self.read { l in + other.read { r in + var result: Builder = .empty(level) + var removing = false + + for (bucket, lslot) in l.itemMap { + let lp = l.itemPtr(at: lslot) + let include: Bool + if r.itemMap.contains(bucket) { + let rslot = r.itemMap.slot(of: bucket) + include = (lp.pointee.key != r[item: rslot].key) + } + else if r.childMap.contains(bucket) { + let rslot = r.childMap.slot(of: bucket) + let h = _Hash(lp.pointee.key) + include = !r[child: rslot] + .containsKey(level.descend(), lp.pointee.key, h) + } + else { + include = true + } + + if include, removing { + result.addNewItem(level, lp.pointee, at: bucket) + } + else if !include, !removing { + removing = true + result.copyItems(level, from: l, upTo: bucket) + } + } + + for (bucket, lslot) in l.childMap { + var done = false + if r.itemMap.contains(bucket) { + let rslot = r.itemMap.slot(of: bucket) + let rp = r.itemPtr(at: rslot) + let h = _Hash(rp.pointee.key) + let child = l[child: lslot] + .removing(level.descend(), rp.pointee.key, h)?.replacement + if let child = child { + assert(child.count < self.count) + if !removing { + removing = true + result.copyItemsAndChildren(level, from: l, upTo: bucket) + } + result.addNewChildBranch(level, child, at: bucket) + done = true + } + } + else if r.childMap.contains(bucket) { + let rslot = r.childMap.slot(of: bucket) + let child = l[child: lslot] + ._subtracting(level.descend(), r[child: rslot]) + if let child = child { + assert(child.count < self.count) + if !removing { + removing = true + result.copyItemsAndChildren(level, from: l, upTo: bucket) + } + result.addNewChildBranch(level, child, at: bucket) + done = true + } + } + if !done, removing { + result.addNewChildNode(level, l[child: lslot], at: bucket) + } + } + guard removing else { return nil } + return result + } + } + } + + @inlinable @inline(never) + internal func _subtracting_slow( + _ level: _HashLevel, + _ other: _HashNode + ) -> Builder? { + let lc = self.isCollisionNode + let rc = other.isCollisionNode + if lc && rc { + return read { l in + other.read { r in + guard l.collisionHash == r.collisionHash else { + return nil + } + var result: Builder = .empty(level) + var removing = false + + let ritems = r.reverseItems + for lslot: _HashSlot in stride(from: .zero, to: l.itemsEndSlot, by: 1) { + let lp = l.itemPtr(at: lslot) + let include = !ritems.contains { $0.key == lp.pointee.key } + if include, removing { + result.addNewCollision(level, lp.pointee, l.collisionHash) + } + else if !include, !removing { + removing = true + result.copyCollisions(from: l, upTo: lslot) + } + } + guard removing else { return nil } + assert(result.count < self.count) + return result + } + } + } + + // One of the nodes must be on a compressed path. + assert(!level.isAtBottom) + + if lc { + // `self` is a collision node on a compressed path. The other tree might + // have the same set of collisions, just expanded a bit deeper. + return read { l in + other.read { r in + let bucket = l.collisionHash[level] + if r.itemMap.contains(bucket) { + let rslot = r.itemMap.slot(of: bucket) + let ritem = r.itemPtr(at: rslot) + let h = _Hash(ritem.pointee.key) + let res = l.find(level, ritem.pointee.key, h) + guard let res = res else { return nil } + return self._removingItemFromLeaf(level, at: bucket, res.slot) + .replacement + } + else if r.childMap.contains(bucket) { + let rslot = r.childMap.slot(of: bucket) + return _subtracting(level.descend(), r[child: rslot]) + .map { .childBranch(level, $0, at: bucket) } + } + return nil + } + } + } + + assert(rc) + // `other` is a collision node on a compressed path. + return read { l in + other.read { r in + let bucket = r.collisionHash[level] + if l.itemMap.contains(bucket) { + let lslot = l.itemMap.slot(of: bucket) + let litem = l.itemPtr(at: lslot) + let h = _Hash(litem.pointee.key) + let res = r.find(level, litem.pointee.key, h) + if res == nil { return nil } + return self._removingItemFromLeaf(level, at: bucket, lslot) + .replacement + } + if l.childMap.contains(bucket) { + let lslot = l.childMap.slot(of: bucket) + let branch = l[child: lslot]._subtracting(level.descend(), other) + guard let branch = branch else { return nil } + var result = self._removingChild(level, at: bucket, lslot) + result.addNewChildBranch(level, branch, at: bucket) + return result + } + return nil + } + } + } +} diff --git a/Sources/HashTreeCollections/HashNode/_HashNode+Structural symmetricDifference.swift b/Sources/HashTreeCollections/HashNode/_HashNode+Structural symmetricDifference.swift new file mode 100644 index 000000000..089488f6b --- /dev/null +++ b/Sources/HashTreeCollections/HashNode/_HashNode+Structural symmetricDifference.swift @@ -0,0 +1,268 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 _HashNode { + @inlinable + internal func symmetricDifference( + _ level: _HashLevel, + _ other: _HashNode + ) -> _HashNode.Builder? { + guard self.count > 0 else { + return .init(level, other.mapValuesToVoid()) + } + guard other.count > 0 else { return nil } + return _symmetricDifference(level, other) + } + + @inlinable + internal func _symmetricDifference( + _ level: _HashLevel, + _ other: _HashNode + ) -> _HashNode.Builder { + typealias VoidNode = _HashNode + + assert(self.count > 0 && other.count > 0) + + if self.raw.storage === other.raw.storage { + return .empty(level) + } + if self.isCollisionNode || other.isCollisionNode { + return _symmetricDifference_slow(level, other) + } + + return self.read { l in + other.read { r in + var result: VoidNode.Builder = .empty(level) + + for (bucket, lslot) in l.itemMap { + let lp = l.itemPtr(at: lslot) + if r.itemMap.contains(bucket) { + let rslot = r.itemMap.slot(of: bucket) + let rp = r.itemPtr(at: rslot) + if lp.pointee.key != rp.pointee.key { + let h1 = _Hash(lp.pointee.key) + let h2 = _Hash(rp.pointee.key) + let child = VoidNode.build( + level: level.descend(), + item1: (lp.pointee.key, ()), h1, + item2: { $0.initialize(to: (rp.pointee.key, ())) }, h2) + result.addNewChildNode(level, child.top, at: bucket) + } + } + else if r.childMap.contains(bucket) { + let rslot = r.childMap.slot(of: bucket) + let rp = r.childPtr(at: rslot) + let h = _Hash(lp.pointee.key) + let child = rp.pointee + .removing(level.descend(), lp.pointee.key, h)?.replacement + if let child = child { + let child = child.mapValuesToVoid() + result.addNewChildBranch(level, child, at: bucket) + } + else { + var child2 = rp.pointee + .mapValuesToVoid( + copy: true, extraBytes: VoidNode.spaceForNewItem) + let r = child2.insert(level.descend(), (lp.pointee.key, ()), h) + assert(r.inserted) + result.addNewChildNode(level, child2, at: bucket) + } + } + else { + result.addNewItem(level, (lp.pointee.key, ()), at: bucket) + } + } + + for (bucket, lslot) in l.childMap { + let lp = l.childPtr(at: lslot) + if r.itemMap.contains(bucket) { + let rslot = r.itemMap.slot(of: bucket) + let rp = r.itemPtr(at: rslot) + let h = _Hash(rp.pointee.key) + let child = lp.pointee + .mapValuesToVoid() + .removing(level.descend(), rp.pointee.key, h)?.replacement + if let child = child { + result.addNewChildBranch(level, child, at: bucket) + } + else { + var child2 = lp.pointee.mapValuesToVoid( + copy: true, extraBytes: VoidNode.spaceForNewItem) + let r2 = child2.insert(level.descend(), (rp.pointee.key, ()), h) + assert(r2.inserted) + result.addNewChildNode(level, child2, at: bucket) + } + } + else if r.childMap.contains(bucket) { + let rslot = r.childMap.slot(of: bucket) + let b = l[child: lslot]._symmetricDifference( + level.descend(), r[child: rslot]) + result.addNewChildBranch(level, b, at: bucket) + } + else { + result.addNewChildNode( + level, lp.pointee.mapValuesToVoid(), at: bucket) + } + } + + let seen = l.itemMap.union(l.childMap) + for (bucket, rslot) in r.itemMap { + guard !seen.contains(bucket) else { continue } + result.addNewItem(level, (r[item: rslot].key, ()), at: bucket) + } + for (bucket, rslot) in r.childMap { + guard !seen.contains(bucket) else { continue } + result.addNewChildNode( + level, r[child: rslot].mapValuesToVoid(), at: bucket) + } + return result + } + } + } + + @inlinable @inline(never) + internal func _symmetricDifference_slow( + _ level: _HashLevel, + _ other: _HashNode + ) -> _HashNode.Builder { + switch (self.isCollisionNode, other.isCollisionNode) { + case (true, true): + return self._symmetricDifference_slow_both(level, other) + case (true, false): + return self._symmetricDifference_slow_left(level, other) + case (false, _): + return other._symmetricDifference_slow_left(level, self) + } + } + + @inlinable + internal func _symmetricDifference_slow_both( + _ level: _HashLevel, + _ other: _HashNode + ) -> _HashNode.Builder { + typealias VoidNode = _HashNode + return read { l in + other.read { r in + guard l.collisionHash == r.collisionHash else { + let node = VoidNode.build( + level: level, + child1: self.mapValuesToVoid(), l.collisionHash, + child2: other.mapValuesToVoid(), r.collisionHash) + return .node(level, node) + } + var result: VoidNode.Builder = .empty(level) + let ritems = r.reverseItems + for ls: _HashSlot in stride(from: .zero, to: l.itemsEndSlot, by: 1) { + let lp = l.itemPtr(at: ls) + let include = !ritems.contains(where: { $0.key == lp.pointee.key }) + if include { + result.addNewCollision(level, (lp.pointee.key, ()), l.collisionHash) + } + } + // FIXME: Consider remembering slots of shared items in r by + // caching them in a bitset. + let litems = l.reverseItems + for rs: _HashSlot in stride(from: .zero, to: r.itemsEndSlot, by: 1) { + let rp = r.itemPtr(at: rs) + let include = !litems.contains(where: { $0.key == rp.pointee.key }) + if include { + result.addNewCollision(level, (rp.pointee.key, ()), r.collisionHash) + } + } + return result + } + } + } + + @inlinable + internal func _symmetricDifference_slow_left( + _ level: _HashLevel, + _ other: _HashNode + ) -> _HashNode.Builder { + typealias VoidNode = _HashNode + // `self` is a collision node on a compressed path. The other tree might + // have the same set of collisions, just expanded a bit deeper. + return read { l in + other.read { r in + assert(l.isCollisionNode && !r.isCollisionNode) + let bucket = l.collisionHash[level] + if r.itemMap.contains(bucket) { + let rslot = r.itemMap.slot(of: bucket) + let rp = r.itemPtr(at: rslot) + let rh = _Hash(rp.pointee.key) + guard rh == l.collisionHash else { + var copy = other.mapValuesToVoid( + copy: true, extraBytes: VoidNode.spaceForSpawningChild) + let item = copy.removeItem(at: bucket, rslot) + let child = VoidNode.build( + level: level.descend(), + item1: { $0.initialize(to: (item.key, ())) }, rh, + child2: self.mapValuesToVoid(), l.collisionHash) + copy.insertChild(child.top, bucket) + return .node(level, copy) + } + let litems = l.reverseItems + if let li = litems.firstIndex(where: { $0.key == rp.pointee.key }) { + if l.itemCount == 2 { + var node = other.mapValuesToVoid(copy: true) + node.replaceItem( + at: bucket, rslot, + with: (litems[1 &- li].key, ())) + return .node(level, node) + } + let lslot = _HashSlot(litems.count &- 1 &- li) + var child = self.mapValuesToVoid(copy: true) + _ = child.removeItem(at: .invalid, lslot) + if other.hasSingletonItem { + // Compression + return .collisionNode(level, child) + } + var node = other.mapValuesToVoid( + copy: true, extraBytes: VoidNode.spaceForSpawningChild) + _ = node.removeItem(at: bucket, rslot) + node.insertChild(child, bucket) + return .node(level, node) + } + if other.hasSingletonItem { + // Compression + var copy = self.mapValuesToVoid( + copy: true, extraBytes: VoidNode.spaceForNewItem) + _ = copy.ensureUniqueAndAppendCollision( + isUnique: true, + (r[item: .zero].key, ())) + return .collisionNode(level, copy) + } + var node = other.mapValuesToVoid( + copy: true, extraBytes: VoidNode.spaceForSpawningChild) + let item = node.removeItem(at: bucket, rslot) + var child = self.mapValuesToVoid( + copy: true, extraBytes: VoidNode.spaceForNewItem) + _ = child.ensureUniqueAndAppendCollision( + isUnique: true, (item.key, ())) + node.insertChild(child, bucket) + return .node(level, node) + } + if r.childMap.contains(bucket) { + let rslot = r.childMap.slot(of: bucket) + let rp = r.childPtr(at: rslot) + let child = rp.pointee._symmetricDifference(level.descend(), self) + return other + .mapValuesToVoid() + .replacingChild(level, at: bucket, rslot, with: child) + } + var node = other + .mapValuesToVoid(copy: true, extraBytes: VoidNode.spaceForNewChild) + node.insertChild(self.mapValuesToVoid(), bucket) + return .node(level, node) + } + } + } +} diff --git a/Sources/HashTreeCollections/HashNode/_HashNode+Structural union.swift b/Sources/HashTreeCollections/HashNode/_HashNode+Structural union.swift new file mode 100644 index 000000000..88a850d7b --- /dev/null +++ b/Sources/HashTreeCollections/HashNode/_HashNode+Structural union.swift @@ -0,0 +1,257 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 _HashNode { + @inlinable + internal func union( + _ level: _HashLevel, + _ other: _HashNode + ) -> (copied: Bool, node: _HashNode) { + guard self.count > 0 else { return (true, other.mapValuesToVoid()) } + guard other.count > 0 else { return (false, self.mapValuesToVoid()) } + if level.isAtRoot, self.hasSingletonItem { + // In this special case, the root node may turn into a collision node + // during the merge process. Prevent this from causing issues below by + // handling it up front. + return self.read { l in + let lp = l.itemPtr(at: .zero) + var copy = other.mapValuesToVoid(copy: true) + let r = copy.updateValue( + level, forKey: lp.pointee.key, _Hash(lp.pointee.key) + ) { + $0.initialize(to: (lp.pointee.key, ())) + } + if !r.inserted { + UnsafeHandle.update(r.leaf) { + $0[item: r.slot] = lp.pointee + } + } + return (true, copy) + } + } + return _union(level, other) + } + + @inlinable + internal func _union( + _ level: _HashLevel, + _ other: _HashNode + ) -> (copied: Bool, node: _HashNode) { + if self.raw.storage === other.raw.storage { + return (false, self.mapValuesToVoid()) + } + + if self.isCollisionNode || other.isCollisionNode { + return _union_slow(level, other) + } + + return self.read { l in + other.read { r in + var node = self.mapValuesToVoid() + var copied = false + + for (bucket, lslot) in l.itemMap { + assert(!node.isCollisionNode) + if r.itemMap.contains(bucket) { + let rslot = r.itemMap.slot(of: bucket) + let lp = l.itemPtr(at: lslot) + let rp = r.itemPtr(at: rslot) + if lp.pointee.key != rp.pointee.key { + let slot = ( + copied + ? node.read { $0.itemMap.slot(of: bucket) } + : lslot) + _ = node.ensureUniqueAndSpawnChild( + isUnique: copied, + level: level, + replacing: bucket, + itemSlot: slot, + newHash: _Hash(rp.pointee.key), + { $0.initialize(to: (rp.pointee.key, ())) }) + // If we hadn't handled the singleton root node case above, + // then this call would sometimes turn `node` into a collision + // node on a compressed path, causing mischief. + assert(!node.isCollisionNode) + copied = true + } + } + else if r.childMap.contains(bucket) { + let rslot = r.childMap.slot(of: bucket) + let rp = r.childPtr(at: rslot) + + node.ensureUnique( + isUnique: copied, withFreeSpace: _HashNode.spaceForSpawningChild) + let item = node.removeItem(at: bucket) + let r = rp.pointee.mapValuesToVoid() + .inserting(level.descend(), (item.key, ()), _Hash(item.key)) + node.insertChild(r.node, bucket) + copied = true + } + } + + for (bucket, lslot) in l.childMap { + assert(!node.isCollisionNode) + if r.itemMap.contains(bucket) { + let rslot = r.itemMap.slot(of: bucket) + let rp = r.itemPtr(at: rslot) + let h = _Hash(rp.pointee.key) + let r = l[child: lslot].mapValuesToVoid() + .inserting(level.descend(), (rp.pointee.key, ()), h) + guard r.inserted else { + // Nothing to do + continue + } + node.ensureUnique(isUnique: copied) + let delta = node.replaceChild(at: bucket, with: r.node) + assert(delta == 1) + copied = true + } + else if r.childMap.contains(bucket) { + let rslot = r.childMap.slot(of: bucket) + let child = l[child: lslot]._union(level.descend(), r[child: rslot]) + guard child.copied else { + // Nothing to do + continue + } + node.ensureUnique(isUnique: copied) + let delta = node.replaceChild(at: bucket, with: child.node) + assert(delta > 0) // If we didn't add an item, why did we copy? + copied = true + } + } + + assert(!node.isCollisionNode) + + /// Add buckets in `other` that we haven't processed above. + let seen = l.itemMap.union(l.childMap) + + for (bucket, _) in r.itemMap.subtracting(seen) { + let rslot = r.itemMap.slot(of: bucket) + node.ensureUniqueAndInsertItem( + isUnique: copied, (r[item: rslot].key, ()), at: bucket) + copied = true + } + for (bucket, _) in r.childMap.subtracting(seen) { + let rslot = r.childMap.slot(of: bucket) + node.ensureUnique( + isUnique: copied, withFreeSpace: _HashNode.spaceForNewChild) + copied = true + node.insertChild(r[child: rslot].mapValuesToVoid(), bucket) + } + + return (copied, node) + } + } + } + + @inlinable @inline(never) + internal func _union_slow( + _ level: _HashLevel, + _ other: _HashNode + ) -> (copied: Bool, node: _HashNode) { + let lc = self.isCollisionNode + let rc = other.isCollisionNode + if lc && rc { + return read { l in + other.read { r in + guard l.collisionHash == r.collisionHash else { + let node = _HashNode.build( + level: level, + child1: self.mapValuesToVoid(), l.collisionHash, + child2: other.mapValuesToVoid(), r.collisionHash) + return (true, node) + } + var copied = false + var node = self.mapValuesToVoid() + let litems = l.reverseItems + for rs: _HashSlot in stride(from: .zero, to: r.itemsEndSlot, by: 1) { + let p = r.itemPtr(at: rs) + if !litems.contains(where: { $0.key == p.pointee.key }) { + _ = node.ensureUniqueAndAppendCollision( + isUnique: copied, (p.pointee.key, ())) + copied = true + } + } + return (copied, node) + } + } + } + + // One of the nodes must be on a compressed path. + assert(!level.isAtBottom) + + if lc { + // `self` is a collision node on a compressed path. The other tree might + // have the same set of collisions, just expanded a bit deeper. + return read { l in + other.read { r in + let bucket = l.collisionHash[level] + if r.itemMap.contains(bucket) { + let rslot = r.itemMap.slot(of: bucket) + let rp = r.itemPtr(at: rslot) + if + r.hasSingletonItem + && l.reverseItems.contains(where: { $0.key == rp.pointee.key }) + { + return (false, self.mapValuesToVoid()) + } + let node = other.mapValuesToVoid().copyNodeAndPushItemIntoNewChild( + level: level, self.mapValuesToVoid(), at: bucket, itemSlot: rslot) + return (true, node) + } + + if r.childMap.contains(bucket) { + let rslot = r.childMap.slot(of: bucket) + let res = self._union(level.descend(), r[child: rslot]) + var node = other.mapValuesToVoid(copy: true) + let delta = node.replaceChild(at: bucket, rslot, with: res.node) + assert(delta >= 0) + return (true, node) + } + + var node = other.mapValuesToVoid( + copy: true, extraBytes: _HashNode.spaceForNewChild) + node.insertChild(self.mapValuesToVoid(), bucket) + return (true, node) + } + } + } + + assert(rc) + // `other` is a collision node on a compressed path. + return read { l -> (copied: Bool, node: _HashNode) in + other.read { r -> (copied: Bool, node: _HashNode) in + let bucket = r.collisionHash[level] + if l.itemMap.contains(bucket) { + let lslot = l.itemMap.slot(of: bucket) + assert(!l.hasSingletonItem) // Handled up front above + let node = self.mapValuesToVoid().copyNodeAndPushItemIntoNewChild( + level: level, other.mapValuesToVoid(), at: bucket, itemSlot: lslot) + return (true, node) + } + if l.childMap.contains(bucket) { + let lslot = l.childMap.slot(of: bucket) + let child = l[child: lslot]._union(level.descend(), other) + guard child.copied else { return (false, self.mapValuesToVoid()) } + var node = self.mapValuesToVoid(copy: true) + let delta = node.replaceChild(at: bucket, lslot, with: child.node) + assert(delta > 0) // If we didn't add an item, why did we copy? + return (true, node) + } + + var node = self.mapValuesToVoid( + copy: true, extraBytes: _HashNode.spaceForNewChild) + node.insertChild(other.mapValuesToVoid(), bucket) + return (true, node) + } + } + } +} diff --git a/Sources/HashTreeCollections/HashNode/_HashNode+Subtree Insertions.swift b/Sources/HashTreeCollections/HashNode/_HashNode+Subtree Insertions.swift new file mode 100644 index 000000000..0f7e4367c --- /dev/null +++ b/Sources/HashTreeCollections/HashNode/_HashNode+Subtree Insertions.swift @@ -0,0 +1,578 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension _HashNode { + @inlinable + internal mutating func insert( + _ level: _HashLevel, + _ item: Element, + _ hash: _Hash + ) -> (inserted: Bool, leaf: _UnmanagedHashNode, slot: _HashSlot) { + insert(level, item.key, hash) { $0.initialize(to: item) } + } + + @inlinable + internal mutating func insert( + _ level: _HashLevel, + _ key: Key, + _ hash: _Hash, + _ inserter: (UnsafeMutablePointer) -> Void + ) -> (inserted: Bool, leaf: _UnmanagedHashNode, slot: _HashSlot) { + defer { _invariantCheck() } + let isUnique = self.isUnique() + if !isUnique { + let r = self.inserting(level, key, hash, inserter) + self = r.node + return (r.inserted, r.leaf, r.slot) + } + let r = findForInsertion(level, key, hash) + switch r { + case .found(_, let slot): + return (false, unmanaged, slot) + case .insert(let bucket, let slot): + ensureUniqueAndInsertItem( + isUnique: true, at: bucket, itemSlot: slot, inserter) + return (true, unmanaged, slot) + case .appendCollision: + let slot = ensureUniqueAndAppendCollision(isUnique: true, inserter) + return (true, unmanaged, slot) + case .spawnChild(let bucket, let slot): + let r = ensureUniqueAndSpawnChild( + isUnique: true, + level: level, + replacing: bucket, + itemSlot: slot, + newHash: hash, + inserter) + return (true, r.leaf, r.slot) + case .expansion: + let r = _HashNode.build( + level: level, + item1: inserter, hash, + child2: self, self.collisionHash) + self = r.top + return (true, r.leaf, r.slot1) + case .descend(_, let slot): + let r = update { + $0[child: slot].insert(level.descend(), key, hash, inserter) + } + if r.inserted { count &+= 1 } + return r + } + } + + @inlinable + internal func inserting( + _ level: _HashLevel, + _ item: __owned Element, + _ hash: _Hash + ) -> ( + inserted: Bool, node: _HashNode, leaf: _UnmanagedHashNode, slot: _HashSlot + ) { + inserting(level, item.key, hash, { $0.initialize(to: item) }) + } + + @inlinable + internal func inserting( + _ level: _HashLevel, + _ key: Key, + _ hash: _Hash, + _ inserter: (UnsafeMutablePointer) -> Void + ) -> ( + inserted: Bool, node: _HashNode, leaf: _UnmanagedHashNode, slot: _HashSlot + ) { + defer { _invariantCheck() } + let r = findForInsertion(level, key, hash) + switch r { + case .found(_, let slot): + return (false, self, unmanaged, slot) + case .insert(let bucket, let slot): + let node = copyNodeAndInsertItem(at: bucket, itemSlot: slot, inserter) + return (true, node, node.unmanaged, slot) + case .appendCollision: + let r = copyNodeAndAppendCollision(inserter) + return (true, r.node, r.node.unmanaged, r.slot) + case .spawnChild(let bucket, let slot): + let existingHash = read { _Hash($0[item: slot].key) } + let r = copyNodeAndSpawnChild( + level: level, + replacing: bucket, + itemSlot: slot, + existingHash: existingHash, + newHash: hash, + inserter) + return (true, r.node, r.leaf, r.slot) + case .expansion: + let r = _HashNode.build( + level: level, + item1: inserter, hash, + child2: self, self.collisionHash) + return (true, r.top, r.leaf, r.slot1) + case .descend(_, let slot): + let r = read { + $0[child: slot].inserting(level.descend(), key, hash, inserter) + } + guard r.inserted else { + return (false, self, r.leaf, r.slot) + } + var copy = self.copy() + copy.update { $0[child: slot] = r.node } + copy.count &+= 1 + return (true, copy, r.leaf, r.slot) + } + } + + @inlinable + internal mutating func updateValue( + _ level: _HashLevel, + forKey key: Key, + _ hash: _Hash, + _ inserter: (UnsafeMutablePointer) -> Void + ) -> (inserted: Bool, leaf: _UnmanagedHashNode, slot: _HashSlot) { + defer { _invariantCheck() } + let isUnique = self.isUnique() + let r = findForInsertion(level, key, hash) + switch r { + case .found(_, let slot): + ensureUnique(isUnique: isUnique) + return (false, unmanaged, slot) + case .insert(let bucket, let slot): + ensureUniqueAndInsertItem( + isUnique: isUnique, at: bucket, itemSlot: slot, inserter) + return (true, unmanaged, slot) + case .appendCollision: + let slot = ensureUniqueAndAppendCollision(isUnique: isUnique, inserter) + return (true, unmanaged, slot) + case .spawnChild(let bucket, let slot): + let r = ensureUniqueAndSpawnChild( + isUnique: isUnique, + level: level, + replacing: bucket, + itemSlot: slot, + newHash: hash, + inserter) + return (true, r.leaf, r.slot) + case .expansion: + let r = _HashNode.build( + level: level, + item1: inserter, hash, + child2: self, self.collisionHash) + self = r.top + return (true, r.leaf, r.slot1) + case .descend(_, let slot): + ensureUnique(isUnique: isUnique) + let r = update { + $0[child: slot].updateValue( + level.descend(), forKey: key, hash, inserter) + } + if r.inserted { count &+= 1 } + return r + } + } +} + +extension _HashNode { + @inlinable + internal mutating func ensureUniqueAndInsertItem( + isUnique: Bool, + _ item: Element, + at bucket: _Bucket + ) { + let slot = self.read { $0.itemMap.slot(of: bucket) } + ensureUniqueAndInsertItem( + isUnique: isUnique, + at: bucket, + itemSlot: slot + ) { + $0.initialize(to: item) + } + } + + @inlinable + internal mutating func ensureUniqueAndInsertItem( + isUnique: Bool, + at bucket: _Bucket, + itemSlot slot: _HashSlot, + _ inserter: (UnsafeMutablePointer) -> Void + ) { + assert(!isCollisionNode) + + if !isUnique { + self = copyNodeAndInsertItem(at: bucket, itemSlot: slot, inserter) + return + } + if !hasFreeSpace(Self.spaceForNewItem) { + resizeNodeAndInsertItem(at: bucket, itemSlot: slot, inserter) + return + } + // In-place insert. + update { + let p = $0._makeRoomForNewItem(at: slot, bucket) + inserter(p) + } + self.count &+= 1 + } + + @inlinable @inline(never) + internal func copyNodeAndInsertItem( + at bucket: _Bucket, + itemSlot slot: _HashSlot, + _ inserter: (UnsafeMutablePointer) -> Void + ) -> _HashNode { + assert(!isCollisionNode) + let c = self.count + return read { src in + assert(!src.itemMap.contains(bucket)) + assert(!src.childMap.contains(bucket)) + return Self.allocate( + itemMap: src.itemMap.inserting(bucket), + childMap: src.childMap, + count: c &+ 1 + ) { dstChildren, dstItems in + dstChildren.initializeAll(fromContentsOf: src.children) + + let srcItems = src.reverseItems + assert(dstItems.count == srcItems.count + 1) + dstItems.suffix(slot.value) + .initializeAll(fromContentsOf: srcItems.suffix(slot.value)) + let rest = srcItems.count &- slot.value + dstItems.prefix(rest) + .initializeAll(fromContentsOf: srcItems.prefix(rest)) + + inserter(dstItems.baseAddress! + rest) + }.node + } + } + + @inlinable @inline(never) + internal mutating func resizeNodeAndInsertItem( + at bucket: _Bucket, + itemSlot slot: _HashSlot, + _ inserter: (UnsafeMutablePointer) -> Void + ) { + assert(!isCollisionNode) + let c = self.count + self = update { src in + assert(!src.itemMap.contains(bucket)) + assert(!src.childMap.contains(bucket)) + return Self.allocate( + itemMap: src.itemMap.inserting(bucket), + childMap: src.childMap, + count: c &+ 1 + ) { dstChildren, dstItems in + dstChildren.moveInitializeAll(fromContentsOf: src.children) + + let srcItems = src.reverseItems + assert(dstItems.count == srcItems.count + 1) + dstItems.suffix(slot.value) + .moveInitializeAll(fromContentsOf: srcItems.suffix(slot.value)) + let rest = srcItems.count &- slot.value + dstItems.prefix(rest) + .moveInitializeAll(fromContentsOf: srcItems.prefix(rest)) + + inserter(dstItems.baseAddress! + rest) + + src.clear() + }.node + } + } +} + +extension _HashNode { + @inlinable + internal mutating func ensureUniqueAndAppendCollision( + isUnique: Bool, + _ item: Element + ) -> _HashSlot { + ensureUniqueAndAppendCollision(isUnique: isUnique) { + $0.initialize(to: item) + } + } + + @inlinable + internal mutating func ensureUniqueAndAppendCollision( + isUnique: Bool, + _ inserter: (UnsafeMutablePointer) -> Void + ) -> _HashSlot { + assert(isCollisionNode) + if !isUnique { + let r = copyNodeAndAppendCollision(inserter) + self = r.node + return r.slot + } + if !hasFreeSpace(Self.spaceForNewItem) { + return resizeNodeAndAppendCollision(inserter) + } + // In-place insert. + update { + let p = $0._makeRoomForNewItem(at: $0.itemsEndSlot, .invalid) + inserter(p) + } + self.count &+= 1 + return _HashSlot(self.count &- 1) + } + + @inlinable @inline(never) + internal func copyNodeAndAppendCollision( + _ inserter: (UnsafeMutablePointer) -> Void + ) -> (node: _HashNode, slot: _HashSlot) { + assert(isCollisionNode) + assert(self.count == read { $0.collisionCount }) + let c = self.count + let node = read { src in + Self.allocateCollision(count: c &+ 1, src.collisionHash) { dstItems in + let srcItems = src.reverseItems + assert(dstItems.count == srcItems.count + 1) + dstItems.dropFirst().initializeAll(fromContentsOf: srcItems) + inserter(dstItems.baseAddress!) + }.node + } + return (node, _HashSlot(c)) + } + + @inlinable @inline(never) + internal mutating func resizeNodeAndAppendCollision( + _ inserter: (UnsafeMutablePointer) -> Void + ) -> _HashSlot { + assert(isCollisionNode) + assert(self.count == read { $0.collisionCount }) + let c = self.count + self = update { src in + Self.allocateCollision(count: c &+ 1, src.collisionHash) { dstItems in + let srcItems = src.reverseItems + assert(dstItems.count == srcItems.count + 1) + dstItems.dropFirst().moveInitializeAll(fromContentsOf: srcItems) + inserter(dstItems.baseAddress!) + + src.clear() + }.node + } + return _HashSlot(c) + } +} + +extension _HashNode { + @inlinable + internal func _copyNodeAndReplaceItemWithNewChild( + level: _HashLevel, + _ newChild: __owned _HashNode, + at bucket: _Bucket, + itemSlot: _HashSlot + ) -> _HashNode { + let c = self.count + return read { src in + assert(!src.isCollisionNode) + assert(src.itemMap.contains(bucket)) + assert(!src.childMap.contains(bucket)) + assert(src.itemMap.slot(of: bucket) == itemSlot) + + if src.hasSingletonItem && newChild.isCollisionNode { + // Compression + return newChild + } + + let childSlot = src.childMap.slot(of: bucket) + return Self.allocate( + itemMap: src.itemMap.removing(bucket), + childMap: src.childMap.inserting(bucket), + count: c &+ newChild.count &- 1 + ) { dstChildren, dstItems in + let srcChildren = src.children + let srcItems = src.reverseItems + + // Initialize children. + dstChildren.prefix(childSlot.value) + .initializeAll(fromContentsOf: srcChildren.prefix(childSlot.value)) + let rest = srcChildren.count &- childSlot.value + dstChildren.suffix(rest) + .initializeAll(fromContentsOf: srcChildren.suffix(rest)) + + dstChildren.initializeElement(at: childSlot.value, to: newChild) + + // Initialize items. + dstItems.suffix(itemSlot.value) + .initializeAll(fromContentsOf: srcItems.suffix(itemSlot.value)) + let rest2 = dstItems.count &- itemSlot.value + dstItems.prefix(rest2) + .initializeAll(fromContentsOf: srcItems.prefix(rest2)) + }.node + } + } + + /// The item at `itemSlot` must have already been deinitialized by the time + /// this function is called. + @inlinable + internal mutating func _resizeNodeAndReplaceItemWithNewChild( + level: _HashLevel, + _ newChild: __owned _HashNode, + at bucket: _Bucket, + itemSlot: _HashSlot + ) { + let c = self.count + let node: _HashNode = update { src in + assert(!src.isCollisionNode) + assert(src.itemMap.contains(bucket)) + assert(!src.childMap.contains(bucket)) + assert(src.itemMap.slot(of: bucket) == itemSlot) + + let childSlot = src.childMap.slot(of: bucket) + return Self.allocate( + itemMap: src.itemMap.removing(bucket), + childMap: src.childMap.inserting(bucket), + count: c &+ newChild.count &- 1 + ) { dstChildren, dstItems in + let srcChildren = src.children + let srcItems = src.reverseItems + + // Initialize children. + dstChildren.prefix(childSlot.value) + .moveInitializeAll(fromContentsOf: srcChildren.prefix(childSlot.value)) + let rest = srcChildren.count &- childSlot.value + dstChildren.suffix(rest) + .moveInitializeAll(fromContentsOf: srcChildren.suffix(rest)) + + dstChildren.initializeElement(at: childSlot.value, to: newChild) + + // Initialize items. + dstItems.suffix(itemSlot.value) + .moveInitializeAll(fromContentsOf: srcItems.suffix(itemSlot.value)) + let rest2 = dstItems.count &- itemSlot.value + dstItems.prefix(rest2) + .moveInitializeAll(fromContentsOf: srcItems.prefix(rest2)) + + src.clear() + }.node + } + self = node + } +} + +extension _HashNode { + @inlinable @inline(never) + internal func copyNodeAndPushItemIntoNewChild( + level: _HashLevel, + _ newChild: __owned _HashNode, + at bucket: _Bucket, + itemSlot: _HashSlot + ) -> _HashNode { + assert(!isCollisionNode) + let item = read { $0[item: itemSlot] } + let hash = _Hash(item.key) + let r = newChild.inserting(level, item, hash) + return _copyNodeAndReplaceItemWithNewChild( + level: level, + r.node, + at: bucket, + itemSlot: itemSlot) + } +} + +extension _HashNode { + @inlinable + internal mutating func ensureUniqueAndSpawnChild( + isUnique: Bool, + level: _HashLevel, + replacing bucket: _Bucket, + itemSlot: _HashSlot, + newHash: _Hash, + _ inserter: (UnsafeMutablePointer) -> Void + ) -> (leaf: _UnmanagedHashNode, slot: _HashSlot) { + let existingHash = read { _Hash($0[item: itemSlot].key) } + assert(existingHash.isEqual(to: newHash, upTo: level)) + if newHash == existingHash, hasSingletonItem { + // Convert current node to a collision node. + self = _HashNode._collisionNode(newHash, read { $0[item: .zero] }, inserter) + return (unmanaged, _HashSlot(1)) + } + + if !isUnique { + let r = copyNodeAndSpawnChild( + level: level, + replacing: bucket, + itemSlot: itemSlot, + existingHash: existingHash, + newHash: newHash, + inserter) + self = r.node + return (r.leaf, r.slot) + } + if !hasFreeSpace(Self.spaceForSpawningChild) { + return resizeNodeAndSpawnChild( + level: level, + replacing: bucket, + itemSlot: itemSlot, + existingHash: existingHash, + newHash: newHash, + inserter) + } + + let existing = removeItem(at: bucket, itemSlot) + let r = _HashNode.build( + level: level.descend(), + item1: existing, existingHash, + item2: inserter, newHash) + insertChild(r.top, bucket) + return (r.leaf, r.slot2) + } + + @inlinable @inline(never) + internal func copyNodeAndSpawnChild( + level: _HashLevel, + replacing bucket: _Bucket, + itemSlot: _HashSlot, + existingHash: _Hash, + newHash: _Hash, + _ inserter: (UnsafeMutablePointer) -> Void + ) -> (node: _HashNode, leaf: _UnmanagedHashNode, slot: _HashSlot) { + let r = read { + _HashNode.build( + level: level.descend(), + item1: $0[item: itemSlot], existingHash, + item2: inserter, newHash) + } + let node = _copyNodeAndReplaceItemWithNewChild( + level: level, + r.top, + at: bucket, + itemSlot: itemSlot) + node._invariantCheck() + return (node, r.leaf, r.slot2) + } + + @inlinable @inline(never) + internal mutating func resizeNodeAndSpawnChild( + level: _HashLevel, + replacing bucket: _Bucket, + itemSlot: _HashSlot, + existingHash: _Hash, + newHash: _Hash, + _ inserter: (UnsafeMutablePointer) -> Void + ) -> (leaf: _UnmanagedHashNode, slot: _HashSlot) { + let r = update { + _HashNode.build( + level: level.descend(), + item1: $0.itemPtr(at: itemSlot).move(), existingHash, + item2: inserter, newHash) + } + _resizeNodeAndReplaceItemWithNewChild( + level: level, + r.top, + at: bucket, + itemSlot: itemSlot) + _invariantCheck() + return (r.leaf, r.slot2) + } +} + diff --git a/Sources/HashTreeCollections/HashNode/_HashNode+Subtree Modify.swift b/Sources/HashTreeCollections/HashNode/_HashNode+Subtree Modify.swift new file mode 100644 index 000000000..18ed7fa94 --- /dev/null +++ b/Sources/HashTreeCollections/HashNode/_HashNode+Subtree Modify.swift @@ -0,0 +1,272 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +// MARK: Subtree-level in-place mutation operations + +extension _HashNode { + @inlinable + internal mutating func ensureUnique( + level: _HashLevel, at path: _UnsafePath + ) -> (leaf: _UnmanagedHashNode, slot: _HashSlot) { + ensureUnique(isUnique: isUnique()) + guard level < path.level else { return (unmanaged, path.currentItemSlot) } + return update { + $0[child: path.childSlot(at: level)] + .ensureUnique(level: level.descend(), at: path) + } + } +} + +extension _HashNode { + @usableFromInline + @frozen + internal struct ValueUpdateState { + @usableFromInline + internal var key: Key + + @usableFromInline + internal var value: Value? + + @usableFromInline + internal let hash: _Hash + + @usableFromInline + internal var path: _UnsafePath + + @usableFromInline + internal var found: Bool + + @inlinable + internal init( + _ key: Key, + _ hash: _Hash, + _ path: _UnsafePath + ) { + self.key = key + self.value = nil + self.hash = hash + self.path = path + self.found = false + } + } + + @inlinable + internal mutating func prepareValueUpdate( + _ key: Key, + _ hash: _Hash + ) -> ValueUpdateState { + var state = ValueUpdateState(key, hash, _UnsafePath(root: raw)) + _prepareValueUpdate(&state) + return state + } + + @inlinable + internal mutating func _prepareValueUpdate( + _ state: inout ValueUpdateState + ) { + // This doesn't make room for a new item if the key doesn't already exist + // but it does ensure that all parent nodes along its eventual path are + // uniquely held. + // + // If the key already exists, we ensure uniqueness for its node and extract + // its item but otherwise leave the tree as it was. + let isUnique = self.isUnique() + let r = findForInsertion(state.path.level, state.key, state.hash) + switch r { + case .found(_, let slot): + ensureUnique(isUnique: isUnique) + state.path.node = unmanaged + state.path.selectItem(at: slot) + state.found = true + (state.key, state.value) = update { $0.itemPtr(at: slot).move() } + + + case .insert(_, let slot): + state.path.selectItem(at: slot) + + case .appendCollision: + state.path.selectItem(at: _HashSlot(self.count)) + + case .spawnChild(_, let slot): + state.path.selectItem(at: slot) + + case .expansion: + state.path.selectEnd() + + case .descend(_, let slot): + ensureUnique(isUnique: isUnique) + update { + let p = $0.childPtr(at: slot) + state.path.descendToChild(p.pointee.unmanaged, at: slot) + p.pointee._prepareValueUpdate(&state) + } + } + } + + @inlinable + internal mutating func finalizeValueUpdate( + _ state: __owned ValueUpdateState + ) { + switch (state.found, state.value != nil) { + case (true, true): + // Fast path: updating an existing value. + UnsafeHandle.update(state.path.node) { + $0.itemPtr(at: state.path.currentItemSlot) + .initialize(to: (state.key, state.value.unsafelyUnwrapped)) + } + case (true, false): + // Removal + let remainder = _finalizeRemoval(.top, state.hash, at: state.path) + assert(remainder == nil) + case (false, true): + // Insertion + let r = updateValue(.top, forKey: state.key, state.hash) { + $0.initialize(to: (state.key, state.value.unsafelyUnwrapped)) + } + assert(r.inserted) + case (false, false): + // Noop + break + } + } + + @inlinable + internal mutating func _finalizeRemoval( + _ level: _HashLevel, _ hash: _Hash, at path: _UnsafePath + ) -> Element? { + assert(isUnique()) + if level == path.level { + return _removeItemFromUniqueLeafNode( + level, at: hash[level], path.currentItemSlot, by: { _ in } + ).remainder + } + let slot = path.childSlot(at: level) + let remainder = update { + $0[child: slot]._finalizeRemoval(level.descend(), hash, at: path) + } + return _fixupUniqueAncestorAfterItemRemoval( + level, at: { _ in hash[level] }, slot, remainder: remainder) + } +} + +extension _HashNode { + @usableFromInline + @frozen + internal struct DefaultedValueUpdateState { + @usableFromInline + internal var item: Element + + @usableFromInline + internal var node: _UnmanagedHashNode + + @usableFromInline + internal var slot: _HashSlot + + @usableFromInline + internal var inserted: Bool + + @inlinable + internal init( + _ item: Element, + in node: _UnmanagedHashNode, + at slot: _HashSlot, + inserted: Bool + ) { + self.item = item + self.node = node + self.slot = slot + self.inserted = inserted + } + } + + @inlinable + internal mutating func prepareDefaultedValueUpdate( + _ level: _HashLevel, + _ key: Key, + _ defaultValue: () -> Value, + _ hash: _Hash + ) -> DefaultedValueUpdateState { + let isUnique = self.isUnique() + let r = findForInsertion(level, key, hash) + switch r { + case .found(_, let slot): + ensureUnique(isUnique: isUnique) + return DefaultedValueUpdateState( + update { $0.itemPtr(at: slot).move() }, + in: unmanaged, + at: slot, + inserted: false) + + case .insert(let bucket, let slot): + ensureUniqueAndInsertItem( + isUnique: isUnique, at: bucket, itemSlot: slot + ) { _ in } + return DefaultedValueUpdateState( + (key, defaultValue()), + in: unmanaged, + at: slot, + inserted: true) + + case .appendCollision: + let slot = ensureUniqueAndAppendCollision(isUnique: isUnique) { _ in } + return DefaultedValueUpdateState( + (key, defaultValue()), + in: unmanaged, + at: slot, + inserted: true) + + case .spawnChild(let bucket, let slot): + let r = ensureUniqueAndSpawnChild( + isUnique: isUnique, + level: level, + replacing: bucket, + itemSlot: slot, + newHash: hash) { _ in } + return DefaultedValueUpdateState( + (key, defaultValue()), + in: r.leaf, + at: r.slot, + inserted: true) + + case .expansion: + let r = _HashNode.build( + level: level, + item1: { _ in }, hash, + child2: self, self.collisionHash + ) + self = r.top + return DefaultedValueUpdateState( + (key, defaultValue()), + in: r.leaf, + at: r.slot1, + inserted: true) + + case .descend(_, let slot): + ensureUnique(isUnique: isUnique) + let res = update { + $0[child: slot].prepareDefaultedValueUpdate( + level.descend(), key, defaultValue, hash) + } + if res.inserted { count &+= 1 } + return res + } + } + + @inlinable + internal mutating func finalizeDefaultedValueUpdate( + _ state: __owned DefaultedValueUpdateState + ) { + UnsafeHandle.update(state.node) { + $0.itemPtr(at: state.slot).initialize(to: state.item) + } + } +} + diff --git a/Sources/HashTreeCollections/HashNode/_HashNode+Subtree Removals.swift b/Sources/HashTreeCollections/HashNode/_HashNode+Subtree Removals.swift new file mode 100644 index 000000000..5ae6cb384 --- /dev/null +++ b/Sources/HashTreeCollections/HashNode/_HashNode+Subtree Removals.swift @@ -0,0 +1,275 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +// MARK: Subtree-level removal operations + +extension _HashNode { + /// Remove the item with the specified key from this subtree and return it. + /// + /// This function may leave `self` containing a singleton item. + /// It is up to the caller to detect this situation & correct it when needed, + /// by inlining the remaining item into the parent node. + @inlinable + internal mutating func remove( + _ level: _HashLevel, _ key: Key, _ hash: _Hash + ) -> (removed: Element, remainder: Element?)? { + guard self.isUnique() else { + guard let r = removing(level, key, hash) else { return nil } + let remainder = self.applyReplacement(level, r.replacement) + return (r.removed, remainder) + } + guard let r = find(level, key, hash) else { return nil } + let bucket = hash[level] + guard r.descend else { + let r = _removeItemFromUniqueLeafNode(level, at: bucket, r.slot) { + $0.move() + } + return (r.result, r.remainder) + } + + let r2 = update { $0[child: r.slot].remove(level.descend(), key, hash) } + guard let r2 = r2 else { return nil } + let remainder = _fixupUniqueAncestorAfterItemRemoval( + level, + at: { _ in hash[level] }, + r.slot, + remainder: r2.remainder) + return (r2.removed, remainder) + } +} + +extension _HashNode { + @inlinable + internal func removing( + _ level: _HashLevel, _ key: Key, _ hash: _Hash + ) -> (removed: Element, replacement: Builder)? { + guard let r = find(level, key, hash) else { return nil } + let bucket = hash[level] + guard r.descend else { + return _removingItemFromLeaf(level, at: bucket, r.slot) + } + let r2 = read { $0[child: r.slot].removing(level.descend(), key, hash) } + guard let r2 = r2 else { return nil } + let replacement = self.replacingChild( + level, at: bucket, r.slot, with: r2.replacement) + return (r2.removed, replacement) + } +} + +extension _HashNode { + @inlinable + internal mutating func remove( + _ level: _HashLevel, at path: _UnsafePath + ) -> (removed: Element, remainder: Element?) { + defer { _invariantCheck() } + guard self.isUnique() else { + let r = removing(level, at: path) + let remainder = applyReplacement(level, r.replacement) + return (r.removed, remainder) + } + if level == path.level { + let slot = path.currentItemSlot + let bucket = read { $0.itemBucket(at: slot) } + let r = _removeItemFromUniqueLeafNode( + level, at: bucket, slot, by: { $0.move() }) + return (r.result, r.remainder) + } + let slot = path.childSlot(at: level) + let r = update { $0[child: slot].remove(level.descend(), at: path) } + let remainder = _fixupUniqueAncestorAfterItemRemoval( + level, + at: { $0.childMap.bucket(at: slot) }, + slot, + remainder: r.remainder) + return (r.removed, remainder) + } + + @inlinable + internal func removing( + _ level: _HashLevel, at path: _UnsafePath + ) -> (removed: Element, replacement: Builder) { + if level == path.level { + let slot = path.currentItemSlot + let bucket = read { $0.itemBucket(at: slot) } + return _removingItemFromLeaf(level, at: bucket, slot) + } + let slot = path.childSlot(at: level) + return read { + let bucket = $0.childMap.bucket(at: slot) + let r = $0[child: slot].removing(level.descend(), at: path) + return ( + r.removed, + self.replacingChild(level, at: bucket, slot, with: r.replacement)) + } + } +} + +extension _HashNode { + @inlinable + internal mutating func _removeItemFromUniqueLeafNode( + _ level: _HashLevel, + at bucket: _Bucket, + _ slot: _HashSlot, + by remover: (UnsafeMutablePointer) -> R + ) -> (result: R, remainder: Element?) { + assert(isUnique()) + let result = removeItem(at: bucket, slot, by: remover) + if isAtrophied { + self = removeSingletonChild() + } + if hasSingletonItem { + if level.isAtRoot { + if isCollisionNode { + _convertToRegularNode() + } + return (result, nil) + } + let item = removeSingletonItem() + return (result, item) + } + return (result, nil) + } + + @inlinable + internal func _removingItemFromLeaf( + _ level: _HashLevel, at bucket: _Bucket, _ slot: _HashSlot + ) -> (removed: Element, replacement: Builder) { + read { + if $0.isCollisionNode { + assert(slot.value < $0.collisionCount ) + + if $0.collisionCount == 2 { + // Node will evaporate + let remainder = _HashSlot(1 &- slot.value) + let bucket = $0.collisionHash[level] + return ( + removed: $0[item: slot], + replacement: .item(level, $0[item: remainder], at: bucket)) + } + + var node = self.copy() + let old = node.removeItem(at: bucket, slot) + node._invariantCheck() + return (old, .collisionNode(level, node)) + } + + assert($0.itemMap.contains(bucket)) + assert(slot == $0.itemMap.slot(of: bucket)) + + let willAtrophy = ( + !$0.isCollisionNode + && $0.itemMap.hasExactlyOneMember + && $0.childMap.hasExactlyOneMember + && $0[child: .zero].isCollisionNode) + if willAtrophy { + // Compression + let child = $0[child: .zero] + let old = $0[item: .zero] + return (old, .collisionNode(level, child)) + } + + if $0.itemMap.count == 2 && $0.childMap.isEmpty { + // Evaporating node + let remainder = _HashSlot(1 &- slot.value) + + var map = $0.itemMap + if remainder != .zero { _ = map.popFirst() } + let bucket = map.first! + + return ( + removed: $0[item: slot], + replacement: .item(level, $0[item: remainder], at: bucket)) + } + var node = self.copy() + let old = node.removeItem(at: bucket, slot) + node._invariantCheck() + return (old, .node(level, node)) + } + } +} + +extension _HashNode { + @inlinable + internal func _removingChild( + _ level: _HashLevel, at bucket: _Bucket, _ slot: _HashSlot + ) -> Builder { + read { + assert(!$0.isCollisionNode && $0.childMap.contains(bucket)) + let willAtrophy = ( + $0.itemMap.isEmpty + && $0.childCount == 2 + && $0[child: _HashSlot(1 &- slot.value)].isCollisionNode + ) + if willAtrophy { + // Compression + let child = $0[child: _HashSlot(1 &- slot.value)] + return .collisionNode(level, child) + } + if $0.itemMap.hasExactlyOneMember && $0.childMap.hasExactlyOneMember { + return .item(level, $0[item: .zero], at: $0.itemMap.first!) + } + if $0.hasSingletonChild { + // Evaporate node + return .empty(level) + } + + var node = self.copy() + _ = node.removeChild(at: bucket, slot) + node._invariantCheck() + return .node(level, node) + } + } +} + +extension _HashNode { + @inlinable + internal mutating func _fixupUniqueAncestorAfterItemRemoval( + _ level: _HashLevel, + at bucket: (UnsafeHandle) -> _Bucket, + _ childSlot: _HashSlot, + remainder: Element? + ) -> Element? { + assert(isUnique()) + count &-= 1 + if let remainder = remainder { + if hasSingletonChild, !level.isAtRoot { + self = ._emptyNode() + return remainder + } + // Child to be inlined has already been cleared, so we need to adjust + // the count manually. + assert(read { $0[child: childSlot].count == 0 }) + count &-= 1 + let bucket = read { bucket($0) } + ensureUnique(isUnique: true, withFreeSpace: Self.spaceForInlinedChild) + _ = self.removeChild(at: bucket, childSlot) + insertItem(remainder, at: bucket) + return nil + } + if isAtrophied { + self = removeSingletonChild() + } + return nil + } +} + +extension _HashNode { + @inlinable + internal mutating func _convertToRegularNode() { + assert(isCollisionNode && hasSingletonItem) + assert(isUnique()) + update { + $0.itemMap = _Bitmap($0.collisionHash[.top]) + $0.childMap = .empty + $0.bytesFree &+= MemoryLayout<_Hash>.stride + } + } +} diff --git a/Sources/HashTreeCollections/HashNode/_HashNode+UnsafeHandle.swift b/Sources/HashTreeCollections/HashNode/_HashNode+UnsafeHandle.swift new file mode 100644 index 000000000..ee0f8c64e --- /dev/null +++ b/Sources/HashTreeCollections/HashNode/_HashNode+UnsafeHandle.swift @@ -0,0 +1,295 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 _HashNode { + /// An unsafe view of the data stored inside a node in the hash tree, hiding + /// the mechanics of accessing storage from the code that uses it. + /// + /// Handles do not own the storage they access -- it is the client's + /// responsibility to ensure that handles (and any pointer values generated + /// by them) do not escape the closure call that received them. + /// + /// A handle can be either read-only or mutable, depending on the method used + /// to access it. In debug builds, methods that modify data trap at runtime if + /// they're called on a read-only view. + @usableFromInline + @frozen + internal struct UnsafeHandle { + @usableFromInline + internal typealias Element = (key: Key, value: Value) + + @usableFromInline + internal let _header: UnsafeMutablePointer<_HashNodeHeader> + + @usableFromInline + internal let _memory: UnsafeMutableRawPointer + + #if DEBUG + @usableFromInline + internal let _isMutable: Bool + #endif + + @inlinable + internal init( + _ header: UnsafeMutablePointer<_HashNodeHeader>, + _ memory: UnsafeMutableRawPointer, + isMutable: Bool + ) { + self._header = header + self._memory = memory + #if DEBUG + self._isMutable = isMutable + #endif + } + } +} + +extension _HashNode.UnsafeHandle { + @inlinable + @inline(__always) + func assertMutable() { +#if DEBUG + assert(_isMutable) +#endif + } +} + +extension _HashNode.UnsafeHandle { + @inlinable @inline(__always) + static func read( + _ node: _UnmanagedHashNode, + _ body: (Self) throws -> R + ) rethrows -> R { + try node.ref._withUnsafeGuaranteedRef { storage in + try storage.withUnsafeMutablePointers { header, elements in + try body(Self(header, UnsafeMutableRawPointer(elements), isMutable: false)) + } + } + } + + @inlinable @inline(__always) + static func read( + _ storage: _RawHashStorage, + _ body: (Self) throws -> R + ) rethrows -> R { + try storage.withUnsafeMutablePointers { header, elements in + try body(Self(header, UnsafeMutableRawPointer(elements), isMutable: false)) + } + } + + @inlinable @inline(__always) + static func update( + _ node: _UnmanagedHashNode, + _ body: (Self) throws -> R + ) rethrows -> R { + try node.ref._withUnsafeGuaranteedRef { storage in + try storage.withUnsafeMutablePointers { header, elements in + try body(Self(header, UnsafeMutableRawPointer(elements), isMutable: true)) + } + } + } + + @inlinable @inline(__always) + static func update( + _ storage: _RawHashStorage, + _ body: (Self) throws -> R + ) rethrows -> R { + try storage.withUnsafeMutablePointers { header, elements in + try body(Self(header, UnsafeMutableRawPointer(elements), isMutable: true)) + } + } +} + +extension _HashNode.UnsafeHandle { + @inlinable @inline(__always) + internal var itemMap: _Bitmap { + get { + _header.pointee.itemMap + } + nonmutating set { + assertMutable() + _header.pointee.itemMap = newValue + } + } + + @inlinable @inline(__always) + internal var childMap: _Bitmap { + get { + _header.pointee.childMap + } + nonmutating set { + assertMutable() + _header.pointee.childMap = newValue + } + } + + @inlinable @inline(__always) + internal var byteCapacity: Int { + _header.pointee.byteCapacity + } + + @inlinable @inline(__always) + internal var bytesFree: Int { + get { _header.pointee.bytesFree } + nonmutating set { + assertMutable() + _header.pointee.bytesFree = newValue + } + } + + @inlinable @inline(__always) + internal var isCollisionNode: Bool { + _header.pointee.isCollisionNode + } + + @inlinable @inline(__always) + internal var collisionCount: Int { + get { _header.pointee.collisionCount } + nonmutating set { + assertMutable() + _header.pointee.collisionCount = newValue + } + } + + @inlinable @inline(__always) + internal var collisionHash: _Hash { + get { + assert(isCollisionNode) + return _memory.load(as: _Hash.self) + } + nonmutating set { + assertMutable() + assert(isCollisionNode) + _memory.storeBytes(of: newValue, as: _Hash.self) + } + } + + @inlinable @inline(__always) + internal var _childrenStart: UnsafeMutablePointer<_HashNode> { + _memory.assumingMemoryBound(to: _HashNode.self) + } + + @inlinable @inline(__always) + internal var hasChildren: Bool { + _header.pointee.hasChildren + } + + @inlinable @inline(__always) + internal var childCount: Int { + _header.pointee.childCount + } + + @inlinable + internal func childBucket(at slot: _HashSlot) -> _Bucket { + guard !isCollisionNode else { return .invalid } + return childMap.bucket(at: slot) + } + + @inlinable @inline(__always) + internal var childrenEndSlot: _HashSlot { + _header.pointee.childrenEndSlot + } + + @inlinable + internal var children: UnsafeMutableBufferPointer<_HashNode> { + UnsafeMutableBufferPointer(start: _childrenStart, count: childCount) + } + + @inlinable + internal func childPtr(at slot: _HashSlot) -> UnsafeMutablePointer<_HashNode> { + assert(slot.value < childCount) + return _childrenStart + slot.value + } + + @inlinable + internal subscript(child slot: _HashSlot) -> _HashNode { + unsafeAddress { + UnsafePointer(childPtr(at: slot)) + } + nonmutating unsafeMutableAddress { + assertMutable() + return childPtr(at: slot) + } + } + + @inlinable + internal var _itemsEnd: UnsafeMutablePointer { + (_memory + _header.pointee.byteCapacity) + .assumingMemoryBound(to: Element.self) + } + + @inlinable @inline(__always) + internal var hasItems: Bool { + _header.pointee.hasItems + } + + @inlinable @inline(__always) + internal var itemCount: Int { + _header.pointee.itemCount + } + + @inlinable + internal func itemBucket(at slot: _HashSlot) -> _Bucket { + guard !isCollisionNode else { return .invalid } + return itemMap.bucket(at: slot) + } + + @inlinable @inline(__always) + internal var itemsEndSlot: _HashSlot { + _header.pointee.itemsEndSlot + } + + @inlinable + internal var reverseItems: UnsafeMutableBufferPointer { + let c = itemCount + return UnsafeMutableBufferPointer(start: _itemsEnd - c, count: c) + } + + @inlinable + internal func itemPtr(at slot: _HashSlot) -> UnsafeMutablePointer { + assert(slot.value <= itemCount) + return _itemsEnd.advanced(by: -1 &- slot.value) + } + + @inlinable + internal subscript(item slot: _HashSlot) -> Element { + unsafeAddress { + UnsafePointer(itemPtr(at: slot)) + } + nonmutating unsafeMutableAddress { + assertMutable() + return itemPtr(at: slot) + } + } + + @inlinable + internal func clear() { + assertMutable() + _header.pointee.clear() + } +} + +extension _HashNode.UnsafeHandle { + @inlinable + internal var hasSingletonItem: Bool { + _header.pointee.hasSingletonItem + } + + @inlinable + internal var hasSingletonChild: Bool { + _header.pointee.hasSingletonChild + } + + @inlinable + internal var isAtrophiedNode: Bool { + hasSingletonChild && self[child: .zero].isCollisionNode + } +} diff --git a/Sources/HashTreeCollections/HashNode/_HashNode.swift b/Sources/HashTreeCollections/HashNode/_HashNode.swift new file mode 100644 index 000000000..b5d9171a4 --- /dev/null +++ b/Sources/HashTreeCollections/HashNode/_HashNode.swift @@ -0,0 +1,138 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +/// A node in the hash tree, logically representing a hash table with +/// 32 buckets, corresponding to a 5-bit slice of a full hash value. +/// +/// Each bucket may store either a single key-value pair or a reference +/// to a child node that contains additional items. +/// +/// To save space, children and items are stored compressed into the two +/// ends of a single raw storage buffer, with the free space in between +/// available for use by either side. +/// +/// The storage is arranged as shown below. +/// +/// ┌───┬───┬───┬───┬───┬──────────────────┬───┬───┬───┬───┐ +/// │ 0 │ 1 │ 2 │ 3 │ 4 │→ free space ←│ 3 │ 2 │ 1 │ 0 │ +/// └───┴───┴───┴───┴───┴──────────────────┴───┴───┴───┴───┘ +/// children items +/// +/// Note that the region for items grows downwards from the end, so the item +/// at slot 0 is at the very end of the buffer. +/// +/// Two 32-bit bitmaps are used to associate each child and item with their +/// position in the hash table. The bucket occupied by the *n*th child in the +/// buffer above is identified by position of the *n*th true bit in the child +/// map, and the *n*th item's bucket corresponds to the *n*th true bit in the +/// items map. +@usableFromInline +@frozen +internal struct _HashNode { + // Warning: This struct must be kept layout-compatible with _RawHashNode. + // Do not add any new stored properties to this type. + // + // Swift guarantees that frozen structs with a single stored property will + // be layout-compatible with the type they are wrapping. + // + // (_RawHashNode is used as the Element type of the ManagedBuffer underlying + // node storage, and the memory is then rebound to `_HashNode` later. + // This will not work correctly unless `_HashNode` has the exact same alignment + // and stride as `RawNode`.) + + @usableFromInline + internal typealias Element = (key: Key, value: Value) + + @usableFromInline + internal var raw: _RawHashNode + + @inlinable + internal init(storage: _RawHashStorage, count: Int) { + self.raw = _RawHashNode(storage: storage, count: count) + } +} + +extension _HashNode { + @inlinable @inline(__always) + internal var count: Int { + get { raw.count } + set { raw.count = newValue } + } +} + +extension _HashNode { + @inlinable @inline(__always) + internal var unmanaged: _UnmanagedHashNode { + _UnmanagedHashNode(raw.storage) + } + + @inlinable @inline(__always) + internal func isIdentical(to other: _UnmanagedHashNode) -> Bool { + raw.isIdentical(to: other) + } +} + +extension _HashNode { + @inlinable @inline(__always) + internal func read( + _ body: (UnsafeHandle) throws -> R + ) rethrows -> R { + try UnsafeHandle.read(raw.storage, body) + } + + @inlinable @inline(__always) + internal mutating func update( + _ body: (UnsafeHandle) throws -> R + ) rethrows -> R { + try UnsafeHandle.update(raw.storage, body) + } +} + +// MARK: Shortcuts to reading header data + +extension _HashNode { + @inlinable + internal var isCollisionNode: Bool { + read { $0.isCollisionNode } + } + + @inlinable + internal var collisionHash: _Hash { + read { $0.collisionHash } + } + + @inlinable + internal var hasSingletonItem: Bool { + read { $0.hasSingletonItem } + } + + @inlinable + internal var hasSingletonChild: Bool { + read { $0.hasSingletonChild } + } + + @inlinable + internal var isAtrophied: Bool { + read { $0.isAtrophiedNode } + } +} + +extension _HashNode { + @inlinable + internal var initialVersionNumber: UInt { + // Ideally we would simply just generate a true random number, but the + // memory address of the root node is a reasonable substitute. + // Alternatively, we could use a per-thread counter along with a thread + // id, or some sort of global banks of atomic integer counters. + let address = Unmanaged.passUnretained(raw.storage).toOpaque() + return UInt(bitPattern: address) + } +} diff --git a/Sources/HashTreeCollections/HashNode/_HashNodeHeader.swift b/Sources/HashTreeCollections/HashNode/_HashNodeHeader.swift new file mode 100644 index 000000000..4d275e056 --- /dev/null +++ b/Sources/HashTreeCollections/HashNode/_HashNodeHeader.swift @@ -0,0 +1,135 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +/// The storage header in a hash tree node. This includes data about the +/// current size and capacity of the node's storage region, as well as +/// information about the currently occupied hash table buckets. +@usableFromInline +@frozen +internal struct _HashNodeHeader { + @usableFromInline + internal var itemMap: _Bitmap + + @usableFromInline + internal var childMap: _Bitmap + + @usableFromInline + internal var _byteCapacity: UInt32 + + @usableFromInline + internal var _bytesFree: UInt32 + + @inlinable + internal init(byteCapacity: Int) { + assert(byteCapacity >= 0 && byteCapacity <= UInt32.max) + self.itemMap = .empty + self.childMap = .empty + self._byteCapacity = UInt32(truncatingIfNeeded: byteCapacity) + self._bytesFree = self._byteCapacity + } +} + +extension _HashNodeHeader { + @inlinable @inline(__always) + internal var byteCapacity: Int { + get { Int(truncatingIfNeeded: _byteCapacity) } + } + + @inlinable + internal var bytesFree: Int { + @inline(__always) + get { Int(truncatingIfNeeded: _bytesFree) } + set { + assert(newValue >= 0 && newValue <= UInt32.max) + _bytesFree = UInt32(truncatingIfNeeded: newValue) + } + } +} + +extension _HashNodeHeader { + @inlinable @inline(__always) + internal var isEmpty: Bool { + return itemMap.isEmpty && childMap.isEmpty + } + + @inlinable @inline(__always) + internal var isCollisionNode: Bool { + !itemMap.isDisjoint(with: childMap) + } + + @inlinable @inline(__always) + internal var hasChildren: Bool { + itemMap != childMap && !childMap.isEmpty + } + + @inlinable @inline(__always) + internal var hasItems: Bool { + !itemMap.isEmpty + } + + @inlinable + internal var childCount: Int { + itemMap == childMap ? 0 : childMap.count + } + + @inlinable + internal var itemCount: Int { + (itemMap == childMap + ? Int(truncatingIfNeeded: itemMap._value) + : itemMap.count) + } + + @inlinable + internal var hasSingletonChild: Bool { + itemMap.isEmpty && childMap.hasExactlyOneMember + } + + @inlinable + internal var hasSingletonItem: Bool { + if itemMap == childMap { + return itemMap._value == 1 + } + return childMap.isEmpty && itemMap.hasExactlyOneMember + } + + @inlinable @inline(__always) + internal var childrenEndSlot: _HashSlot { + _HashSlot(childCount) + } + + @inlinable @inline(__always) + internal var itemsEndSlot: _HashSlot { + _HashSlot(itemCount) + } + + @inlinable + internal var collisionCount: Int { + get { + assert(isCollisionNode) + return Int(truncatingIfNeeded: itemMap._value) + } + set { + assert(isCollisionNode || childMap.isEmpty) + assert(newValue > 0 && newValue < _Bitmap.Value.max) + itemMap._value = _Bitmap.Value(truncatingIfNeeded: newValue) + childMap = itemMap + } + } +} + +extension _HashNodeHeader { + @inlinable + internal mutating func clear() { + itemMap = .empty + childMap = .empty + bytesFree = byteCapacity + } +} diff --git a/Sources/HashTreeCollections/HashNode/_HashSlot.swift b/Sources/HashTreeCollections/HashNode/_HashSlot.swift new file mode 100644 index 000000000..7ff8b3cfd --- /dev/null +++ b/Sources/HashTreeCollections/HashNode/_HashSlot.swift @@ -0,0 +1,111 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +/// Identifies a position within a contiguous storage region within a hash tree +/// node. Hash tree nodes have two storage regions, one for items and one for +/// children; the same `_HashSlot` type is used to refer to positions within both. +/// +/// We use the term "slot" to refer to internal storage entries, to avoid +/// confusion with terms that sometimes appear in public API, such as +/// "index", "position" or "offset". +@usableFromInline +@frozen +internal struct _HashSlot { + @usableFromInline + internal var _value: UInt32 + + @inlinable @inline(__always) + internal init(_ value: UInt32) { + self._value = value + } + + @inlinable @inline(__always) + internal init(_ value: UInt) { + assert(value <= UInt32.max) + self._value = UInt32(truncatingIfNeeded: value) + } + + @inlinable @inline(__always) + internal init(_ value: Int) { + assert(value >= 0 && value <= UInt32.max) + self._value = UInt32(truncatingIfNeeded: value) + } +} + +extension _HashSlot { + @inlinable @inline(__always) + internal static var zero: _HashSlot { _HashSlot(0) } +} + +extension _HashSlot { + @inlinable @inline(__always) + internal var value: Int { + Int(truncatingIfNeeded: _value) + } +} + +extension _HashSlot: Equatable { + @inlinable @inline(__always) + internal static func ==(left: Self, right: Self) -> Bool { + left._value == right._value + } +} + +extension _HashSlot: Comparable { + @inlinable @inline(__always) + internal static func <(left: Self, right: Self) -> Bool { + left._value < right._value + } +} + +extension _HashSlot: Hashable { + @inlinable + internal func hash(into hasher: inout Hasher) { + hasher.combine(_value) + } +} + +extension _HashSlot: CustomStringConvertible { + @usableFromInline + internal var description: String { + "\(_value)" + } +} + +extension _HashSlot: Strideable { + @inlinable @inline(__always) + internal func advanced(by n: Int) -> _HashSlot { + assert(n >= 0 || value + n >= 0) + return _HashSlot(_value &+ UInt32(truncatingIfNeeded: n)) + } + + @inlinable @inline(__always) + internal func distance(to other: _HashSlot) -> Int { + if self < other { + return Int(truncatingIfNeeded: other._value - self._value) + } + return -Int(truncatingIfNeeded: self._value - other._value) + } +} + +extension _HashSlot { + @inlinable @inline(__always) + internal func next() -> _HashSlot { + assert(_value < .max) + return _HashSlot(_value &+ 1) + } + + @inlinable @inline(__always) + internal func previous() -> _HashSlot { + assert(_value > 0) + return _HashSlot(_value &- 1) + } +} diff --git a/Sources/HashTreeCollections/HashNode/_HashStack.swift b/Sources/HashTreeCollections/HashNode/_HashStack.swift new file mode 100644 index 000000000..caefce013 --- /dev/null +++ b/Sources/HashTreeCollections/HashNode/_HashStack.swift @@ -0,0 +1,116 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +/// A fixed-size array of just enough size to hold an ancestor path in a +/// `TreeDictionary`. +@usableFromInline +@frozen +internal struct _HashStack { +#if arch(x86_64) || arch(arm64) + @inlinable + @inline(__always) + internal static var capacity: Int { 13 } + + // xxxxx xxxxx xxxxx xxxxx xxxxx xxxxx xxxxx xxxxx xxxxx xxxxx xxxxx xxxxx xxxx + @usableFromInline + internal var _contents: ( + Element, Element, Element, Element, + Element, Element, Element, Element, + Element, Element, Element, Element, + Element + ) +#else + @inlinable + @inline(__always) + internal static var capacity: Int { 7 } + + // xxxxx xxxxx xxxxx xxxxx xxxxx xxxxx xx + @usableFromInline + internal var _contents: ( + Element, Element, Element, Element, + Element, Element, Element + ) +#endif + + @usableFromInline + internal var _count: UInt8 + + @inlinable + internal init(filledWith value: Element) { + assert(Self.capacity == _HashLevel.limit) +#if arch(x86_64) || arch(arm64) + _contents = ( + value, value, value, value, + value, value, value, value, + value, value, value, value, + value + ) +#else + _contents = ( + value, value, value, value, + value, value, value + ) +#endif + self._count = 0 + } + + @inlinable + @inline(__always) + internal var capacity: Int { Self.capacity } + + @inlinable + @inline(__always) + internal var count: Int { Int(truncatingIfNeeded: _count) } + + @inlinable + @inline(__always) + internal var isEmpty: Bool { _count == 0 } + + @inlinable + subscript(level: UInt8) -> Element { + mutating get { + assert(level < _count) + return withUnsafeBytes(of: &_contents) { buffer in + // Homogeneous tuples are layout compatible with their element type + let start = buffer.baseAddress!.assumingMemoryBound(to: Element.self) + return start[Int(truncatingIfNeeded: level)] + } + } + set { + assert(level < capacity) + withUnsafeMutableBytes(of: &_contents) { buffer in + // Homogeneous tuples are layout compatible with their element type + let start = buffer.baseAddress!.assumingMemoryBound(to: Element.self) + start[Int(truncatingIfNeeded: level)] = newValue + } + } + } + + @inlinable + mutating func push(_ item: Element) { + assert(_count < capacity) + self[_count] = item + _count &+= 1 + } + + @inlinable + mutating func pop() -> Element { + assert(_count > 0) + defer { _count &-= 1 } + return self[_count &- 1] + } + + @inlinable + mutating func peek() -> Element { + assert(count > 0) + return self[_count &- 1] + } +} diff --git a/Sources/HashTreeCollections/HashNode/_HashTreeIterator.swift b/Sources/HashTreeCollections/HashNode/_HashTreeIterator.swift new file mode 100644 index 000000000..5c8bfc636 --- /dev/null +++ b/Sources/HashTreeCollections/HashNode/_HashTreeIterator.swift @@ -0,0 +1,131 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +@usableFromInline +@frozen +internal struct _HashTreeIterator { + @usableFromInline + internal struct _Opaque { + internal var ancestorSlots: _AncestorHashSlots + internal var ancestorNodes: _HashStack<_UnmanagedHashNode> + internal var level: _HashLevel + internal var isAtEnd: Bool + + @usableFromInline + @_effects(releasenone) + internal init(_ root: _UnmanagedHashNode) { + self.ancestorSlots = .empty + self.ancestorNodes = _HashStack(filledWith: root) + self.level = .top + self.isAtEnd = false + } + } + + @usableFromInline + internal let root: _RawHashStorage + + @usableFromInline + internal var node: _UnmanagedHashNode + + @usableFromInline + internal var slot: _HashSlot + + @usableFromInline + internal var endSlot: _HashSlot + + @usableFromInline + internal var _o: _Opaque + + @usableFromInline + @_effects(releasenone) + internal init(root: __shared _RawHashNode) { + self.root = root.storage + self.node = root.unmanaged + self.slot = .zero + self.endSlot = node.itemsEndSlot + self._o = _Opaque(self.node) + + if node.hasItems { return } + if node.hasChildren { + _descendToLeftmostItem(ofChildAtSlot: .zero) + } else { + self._o.isAtEnd = true + } + } +} + +extension _HashTreeIterator: IteratorProtocol { + @inlinable + internal mutating func next( + ) -> (node: _UnmanagedHashNode, slot: _HashSlot)? { + guard slot < endSlot else { + return _next() + } + defer { slot = slot.next() } + return (node, slot) + } + + @usableFromInline + @_effects(releasenone) + internal mutating func _next( + ) -> (node: _UnmanagedHashNode, slot: _HashSlot)? { + if _o.isAtEnd { return nil } + if node.hasChildren { + _descendToLeftmostItem(ofChildAtSlot: .zero) + slot = slot.next() + return (node, .zero) + } + while !_o.level.isAtRoot { + let nextChild = _ascend().next() + if nextChild < node.childrenEndSlot { + _descendToLeftmostItem(ofChildAtSlot: nextChild) + slot = slot.next() + return (node, .zero) + } + } + // At end + endSlot = node.itemsEndSlot + slot = endSlot + _o.isAtEnd = true + return nil + } +} + +extension _HashTreeIterator { + internal mutating func _descend(toChildSlot childSlot: _HashSlot) { + assert(childSlot < node.childrenEndSlot) + _o.ancestorSlots[_o.level] = childSlot + _o.ancestorNodes.push(node) + _o.level = _o.level.descend() + node = node.unmanagedChild(at: childSlot) + slot = .zero + endSlot = node.itemsEndSlot + } + + internal mutating func _ascend() -> _HashSlot { + assert(!_o.level.isAtRoot) + node = _o.ancestorNodes.pop() + _o.level = _o.level.ascend() + let childSlot = _o.ancestorSlots[_o.level] + _o.ancestorSlots.clear(_o.level) + return childSlot + } + + internal mutating func _descendToLeftmostItem( + ofChildAtSlot childSlot: _HashSlot + ) { + _descend(toChildSlot: childSlot) + while endSlot == .zero { + assert(node.hasChildren) + _descend(toChildSlot: .zero) + } + } +} diff --git a/Sources/HashTreeCollections/HashNode/_HashTreeStatistics.swift b/Sources/HashTreeCollections/HashNode/_HashTreeStatistics.swift new file mode 100644 index 000000000..85468af5f --- /dev/null +++ b/Sources/HashTreeCollections/HashNode/_HashTreeStatistics.swift @@ -0,0 +1,130 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +public struct _HashTreeStatistics { + /// The number of nodes in the tree. + public internal(set) var nodeCount: Int = 0 + + /// The number of collision nodes in the tree. + public internal(set) var collisionNodeCount: Int = 0 + + /// The number of elements within this tree. + public internal(set) var itemCount: Int = 0 + + /// The number of elements whose keys have colliding hashes in the tree. + public internal(set) var collisionCount: Int = 0 + + /// The number of key comparisons that need to be done due to hash collisions + /// when finding every key in the tree. + public internal(set) var _collisionChainCount: Int = 0 + + /// The maximum depth of the tree. + public internal(set) var maxItemDepth: Int = 0 + + internal var _sumItemDepth: Int = 0 + + /// The sum of all storage within the tree that is available for item storage, + /// measured in bytes. (This is storage is shared between actual + /// items and child references. Depending on alignment issues, not all of + /// this may be actually usable.) + public internal(set) var capacityBytes: Int = 0 + + /// The number of bytes of storage currently used for storing items. + public internal(set) var itemBytes: Int = 0 + + /// The number of bytes of storage currently used for storing child + /// references. + public internal(set) var childBytes: Int = 0 + + /// The number of bytes currently available for storage, summed over all + /// nodes in the tree. + public internal(set) var freeBytes: Int = 0 + + /// An estimate of the actual memory occupied by this tree. This includes + /// not only storage space for items & children, but also the memory taken up + /// by node headers and Swift's object headers. + public internal(set) var grossBytes: Int = 0 + + /// The average level of an item within this tree. + public var averageItemDepth: Double { + guard nodeCount > 0 else { return 0 } + return Double(_sumItemDepth) / Double(itemCount) + } + /// An estimate of how efficiently this data structure manages memory. + /// This is a value between 0 and 1 -- the ratio between how much space + /// the actual stored data occupies and the overall number of bytes allocated + /// for the entire data structure. (`itemBytes / grossBytes`) + public var memoryEfficiency: Double { + guard grossBytes > 0 else { return 1 } + return Double(itemBytes) / Double(grossBytes) + } + + public var averageNodeSize: Double { + guard nodeCount > 0 else { return 0 } + return Double(capacityBytes) / Double(nodeCount) + } + + /// The average number of keys that need to be compared within the tree + /// to find a member item. This is exactly 1 unless the tree contains hash + /// collisions. + public var averageLookupChainLength: Double { + guard itemCount > 0 else { return 1 } + return Double(itemCount + _collisionChainCount) / Double(itemCount) + } + + internal init() { + // Nothing to do + } +} + + +extension _HashNode { + internal func gatherStatistics( + _ level: _HashLevel, _ stats: inout _HashTreeStatistics + ) { + // The empty singleton does not count as a node and occupies no space. + if self.raw.storage === _emptySingleton { return } + + read { + stats.nodeCount += 1 + stats.itemCount += $0.itemCount + + if isCollisionNode { + stats.collisionNodeCount += 1 + stats.collisionCount += $0.itemCount + stats._collisionChainCount += $0.itemCount * ($0.itemCount - 1) / 2 + } + + let keyStride = MemoryLayout.stride + let valueStride = MemoryLayout.stride + + stats.maxItemDepth = Swift.max(stats.maxItemDepth, level.depth) + stats._sumItemDepth += (level.depth + 1) * $0.itemCount + stats.capacityBytes += $0.byteCapacity + stats.freeBytes += $0.bytesFree + stats.itemBytes += $0.itemCount * (keyStride + valueStride) + stats.childBytes += $0.childCount * MemoryLayout<_RawHashNode>.stride + + let objectHeaderSize = 2 * MemoryLayout.stride + + // Note: for simplicity, we assume that there is no padding between + // the object header and the storage header. + let start = _getUnsafePointerToStoredProperties(self.raw.storage) + let capacity = self.raw.storage.capacity + let end = $0._memory + capacity * MemoryLayout<_RawHashNode>.stride + stats.grossBytes += objectHeaderSize + (end - start) + + for child in $0.children { + child.gatherStatistics(level.descend(), &stats) + } + } + } +} diff --git a/Sources/HashTreeCollections/HashNode/_RawHashNode+UnsafeHandle.swift b/Sources/HashTreeCollections/HashNode/_RawHashNode+UnsafeHandle.swift new file mode 100644 index 000000000..70039a78c --- /dev/null +++ b/Sources/HashTreeCollections/HashNode/_RawHashNode+UnsafeHandle.swift @@ -0,0 +1,116 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 _RawHashNode { + /// An unsafe, non-generic view of the data stored inside a node in the + /// hash tree, hiding the mechanics of accessing storage from the code that + /// uses it. + /// + /// This is the non-generic equivalent of `_HashNode.UnsafeHandle`, sharing some + /// of its functionality, but it only provides read-only access to the tree + /// structure (incl. subtree counts) -- it doesn't provide any ways to mutate + /// the underlying data or to access user payload. + /// + /// Handles do not own the storage they access -- it is the client's + /// responsibility to ensure that handles (and any pointer values generated + /// by them) do not escape the closure call that received them. + @usableFromInline + @frozen + internal struct UnsafeHandle { + @usableFromInline + internal let _header: UnsafePointer<_HashNodeHeader> + + @usableFromInline + internal let _memory: UnsafeRawPointer + + @inlinable + internal init( + _ header: UnsafePointer<_HashNodeHeader>, + _ memory: UnsafeRawPointer + ) { + self._header = header + self._memory = memory + } + } +} + +extension _RawHashNode.UnsafeHandle { + @inlinable @inline(__always) + static func read( + _ node: _UnmanagedHashNode, + _ body: (Self) throws -> R + ) rethrows -> R { + try node.ref._withUnsafeGuaranteedRef { storage in + try storage.withUnsafeMutablePointers { header, elements in + try body(Self(header, UnsafeRawPointer(elements))) + } + } + } +} + +extension _RawHashNode.UnsafeHandle { + @inline(__always) + internal var isCollisionNode: Bool { + _header.pointee.isCollisionNode + } + + @inline(__always) + internal var collisionHash: _Hash { + assert(isCollisionNode) + return _memory.load(as: _Hash.self) + } + + @inline(__always) + internal var hasChildren: Bool { + _header.pointee.hasChildren + } + + @inline(__always) + internal var childCount: Int { + _header.pointee.childCount + } + + @inline(__always) + internal var childrenEndSlot: _HashSlot { + _header.pointee.childrenEndSlot + } + + @inline(__always) + internal var hasItems: Bool { + _header.pointee.hasItems + } + + @inline(__always) + internal var itemCount: Int { + _header.pointee.itemCount + } + + @inline(__always) + internal var itemsEndSlot: _HashSlot { + _header.pointee.itemsEndSlot + } + + @inline(__always) + internal var _childrenStart: UnsafePointer<_RawHashNode> { + _memory.assumingMemoryBound(to: _RawHashNode.self) + } + + internal subscript(child slot: _HashSlot) -> _RawHashNode { + unsafeAddress { + assert(slot < childrenEndSlot) + return _childrenStart + slot.value + } + } + + internal var children: UnsafeBufferPointer<_RawHashNode> { + UnsafeBufferPointer(start: _childrenStart, count: childCount) + } +} diff --git a/Sources/HashTreeCollections/HashNode/_RawHashNode.swift b/Sources/HashTreeCollections/HashNode/_RawHashNode.swift new file mode 100644 index 000000000..98a67cf67 --- /dev/null +++ b/Sources/HashTreeCollections/HashNode/_RawHashNode.swift @@ -0,0 +1,73 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +/// A type-erased node in a hash tree. This doesn't know about the user data +/// stored in the tree, but it has access to subtree counts and it can be used +/// to freely navigate within the tree structure. +/// +/// This construct is powerful enough to implement APIs such as `index(after:)`, +/// `distance(from:to:)`, `index(_:offsetBy:)` in non-generic code. +@usableFromInline +@frozen +internal struct _RawHashNode { + @usableFromInline + internal var storage: _RawHashStorage + + @usableFromInline + internal var count: Int + + @inlinable + internal init(storage: _RawHashStorage, count: Int) { + self.storage = storage + self.count = count + } +} + +extension _RawHashNode { + @inline(__always) + internal func read(_ body: (UnsafeHandle) -> R) -> R { + storage.withUnsafeMutablePointers { header, elements in + body(UnsafeHandle(header, UnsafeRawPointer(elements))) + } + } +} + +extension _RawHashNode { + @inlinable @inline(__always) + internal var unmanaged: _UnmanagedHashNode { + _UnmanagedHashNode(storage) + } + + @inlinable @inline(__always) + internal func isIdentical(to other: _UnmanagedHashNode) -> Bool { + other.ref.toOpaque() == Unmanaged.passUnretained(storage).toOpaque() + } +} + +extension _RawHashNode { + @usableFromInline + internal func validatePath(_ path: _UnsafePath) { + var l = _HashLevel.top + var n = self.unmanaged + while l < path.level { + let slot = path.ancestors[l] + precondition(slot < n.childrenEndSlot) + n = n.unmanagedChild(at: slot) + l = l.descend() + } + precondition(n == path.node) + if path._isItem { + precondition(path.nodeSlot < n.itemsEndSlot) + } else { + precondition(path.nodeSlot <= n.childrenEndSlot) + } + } +} diff --git a/Sources/HashTreeCollections/HashNode/_UnmanagedHashNode.swift b/Sources/HashTreeCollections/HashNode/_UnmanagedHashNode.swift new file mode 100644 index 000000000..cb3872f2b --- /dev/null +++ b/Sources/HashTreeCollections/HashNode/_UnmanagedHashNode.swift @@ -0,0 +1,109 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +/// An unsafe, unowned, type-erased reference to a hash tree node; essentially +/// just a lightweight wrapper around `Unmanaged<_RawHashStorage>`. +/// +/// Because such a reference may outlive the underlying object, use sites must +/// be extraordinarily careful to never dereference an invalid +/// `_UnmanagedHashNode`. Doing so results in undefined behavior. +@usableFromInline +@frozen +internal struct _UnmanagedHashNode { + @usableFromInline + internal var ref: Unmanaged<_RawHashStorage> + + @inlinable @inline(__always) + internal init(_ storage: _RawHashStorage) { + self.ref = .passUnretained(storage) + } +} + +extension _UnmanagedHashNode: Equatable { + /// Indicates whether two unmanaged node references are equal. + /// + /// This function is safe to call even if one or both of its arguments are + /// invalid -- however, it may incorrectly return true in this case. + /// (This can happen when a destroyed node's memory region is later reused for + /// a newly created node.) + @inlinable + internal static func ==(left: Self, right: Self) -> Bool { + left.ref.toOpaque() == right.ref.toOpaque() + } +} + +extension _UnmanagedHashNode: CustomStringConvertible { + @usableFromInline + internal var description: String { + _addressString(for: ref.toOpaque()) + } +} + +extension _UnmanagedHashNode { + @inlinable @inline(__always) + internal func withRaw(_ body: (_RawHashStorage) -> R) -> R { + ref._withUnsafeGuaranteedRef(body) + } + + @inline(__always) + internal func read(_ body: (_RawHashNode.UnsafeHandle) -> R) -> R { + ref._withUnsafeGuaranteedRef { storage in + storage.withUnsafeMutablePointers { header, elements in + body(_RawHashNode.UnsafeHandle(header, UnsafeRawPointer(elements))) + } + } + } + + @inlinable + internal var hasItems: Bool { + withRaw { $0.header.hasItems } + } + + @inlinable + internal var hasChildren: Bool { + withRaw { $0.header.hasChildren } + } + + @inlinable + internal var itemCount: Int { + withRaw { $0.header.itemCount } + } + + @inlinable + internal var childCount: Int { + withRaw { $0.header.childCount } + } + + @inlinable + internal var itemsEndSlot: _HashSlot { + withRaw { _HashSlot($0.header.itemCount) } + } + + @inlinable + internal var childrenEndSlot: _HashSlot { + withRaw { _HashSlot($0.header.childCount) } + } + + @inlinable + internal func unmanagedChild(at slot: _HashSlot) -> Self { + withRaw { raw in + assert(slot.value < raw.header.childCount) + return raw.withUnsafeMutablePointerToElements { p in + Self(p[slot.value].storage) + } + } + } +} + diff --git a/Sources/HashTreeCollections/HashNode/_UnsafePath.swift b/Sources/HashTreeCollections/HashNode/_UnsafePath.swift new file mode 100644 index 000000000..a434c15d3 --- /dev/null +++ b/Sources/HashTreeCollections/HashNode/_UnsafePath.swift @@ -0,0 +1,890 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +/// A non-owning, mutable construct representing a path to an item or child node +/// within a hash tree (or the virtual slot addressing the end of the +/// items or children region within a node). +/// +/// Path values provide mutating methods to freely navigate around in the tree, +/// including basics such as descending into a child node, ascending to a +/// parent or selecting a particular item within the current node; as well as +/// more complicated methods such as finding the next/previous item in a +/// preorder walk of the tree. +/// +/// Paths are, for the most part, represented by a series of slot values +/// identifying a particular branch within each level in the tree up to and +/// including the final node on the path. +/// +/// However, to speed up common operations, path values also include a single +/// `_UnmanagedHashNode` reference to their final node. This reference does not +/// keep the targeted node alive -- it is the use site's responsibility to +/// ensure that the path is still valid before calling most of its operations. +/// +/// Note: paths only have a direct reference to their final node. This means +/// that ascending to the parent node requires following the path from the root +/// node down. (Paths could also store references to every node alongside them +/// in a fixed-size array; this would speed up walking over the tree, but it +/// would considerably embiggen the size of the path construct.) +@usableFromInline +@frozen +internal struct _UnsafePath { + @usableFromInline + internal var ancestors: _AncestorHashSlots + + @usableFromInline + internal var node: _UnmanagedHashNode + + @usableFromInline + internal var nodeSlot: _HashSlot + + @usableFromInline + internal var level: _HashLevel + + @usableFromInline + internal var _isItem: Bool + + @inlinable + internal init(root: __shared _RawHashNode) { + self.level = .top + self.ancestors = .empty + self.node = root.unmanaged + self.nodeSlot = .zero + self._isItem = root.storage.header.hasItems + } +} + +extension _UnsafePath { + internal init( + _ level: _HashLevel, + _ ancestors: _AncestorHashSlots, + _ node: _UnmanagedHashNode, + childSlot: _HashSlot + ) { + assert(childSlot < node.childrenEndSlot) + self.level = level + self.ancestors = ancestors + self.node = node + self.nodeSlot = childSlot + self._isItem = false + } + + @inlinable + internal init( + _ level: _HashLevel, + _ ancestors: _AncestorHashSlots, + _ node: _UnmanagedHashNode, + itemSlot: _HashSlot + ) { + assert(itemSlot < node.itemsEndSlot) + self.level = level + self.ancestors = ancestors + self.node = node + self.nodeSlot = itemSlot + self._isItem = true + } +} + +extension _UnsafePath: Equatable { + @usableFromInline + @_effects(releasenone) + internal static func ==(left: Self, right: Self) -> Bool { + // Note: we don't compare nodes (node equality should follow from the rest) + left.level == right.level + && left.ancestors == right.ancestors + && left.nodeSlot == right.nodeSlot + && left._isItem == right._isItem + } +} + +extension _UnsafePath: Hashable { + @usableFromInline + @_effects(releasenone) + internal func hash(into hasher: inout Hasher) { + // Note: we don't hash nodes, as they aren't compared by ==, either. + hasher.combine(ancestors.path) + hasher.combine(nodeSlot) + hasher.combine(level._shift) + hasher.combine(_isItem) + } +} + +extension _UnsafePath: Comparable { + @usableFromInline + @_effects(releasenone) + internal static func <(left: Self, right: Self) -> Bool { + // This implements a total ordering across paths based on the slot + // sequences they contain, corresponding to a preorder walk of the tree. + // + // Paths addressing items within a node are ordered before paths addressing + // a child node within the same node. + + var level: _HashLevel = .top + while level < left.level, level < right.level { + let l = left.ancestors[level] + let r = right.ancestors[level] + guard l == r else { return l < r } + level = level.descend() + } + assert(level < left.level || !left.ancestors.hasDataBelow(level)) + assert(level < right.level || !right.ancestors.hasDataBelow(level)) + if level < right.level { + guard !left._isItem else { return true } + let l = left.nodeSlot + let r = right.ancestors[level] + return l < r + } + if level < left.level { + guard !right._isItem else { return false } + let l = left.ancestors[level] + let r = right.nodeSlot + return l < r + } + guard left._isItem == right._isItem else { return left._isItem } + return left.nodeSlot < right.nodeSlot + } +} + +extension _UnsafePath: CustomStringConvertible { + @usableFromInline + internal var description: String { + var d = "@" + var l: _HashLevel = .top + while l < self.level { + d += ".\(self.ancestors[l])" + l = l.descend() + } + if isPlaceholder { + d += ".end[\(self.nodeSlot)]" + } else if isOnItem { + d += "[\(self.nodeSlot)]" + } else if isOnChild { + d += ".\(self.nodeSlot)" + } else if isOnNodeEnd { + d += ".end(\(self.nodeSlot))" + } + return d + } +} + +extension _UnsafePath { + /// Returns true if this path addresses an item in the tree; otherwise returns + /// false. + /// + /// - Note: It is undefined behavior to call this on a path that is no longer + /// valid. + @inlinable @inline(__always) + internal var isOnItem: Bool { + // Note: this may be true even if nodeSlot == itemCount (insertion paths). + _isItem + } + + /// Returns true if this path addresses the position following a node's last + /// valid item. Such paths can represent the place of an item that might be + /// inserted later; they do not occur while simply iterating over existing + /// items. + /// + /// - Note: It is undefined behavior to call this on a path that is no longer + /// valid. + @inlinable + internal var isPlaceholder: Bool { + _isItem && nodeSlot.value == node.itemCount + } + + /// Returns true if this path addresses a node in the tree; otherwise returns + /// false. + /// + /// - Note: It is undefined behavior to call this on a path that is no longer + /// valid. + @inlinable + internal var isOnChild: Bool { + !_isItem && nodeSlot.value < node.childCount + } + + /// Returns true if this path addresses an empty slot within a node in a tree; + /// otherwise returns false. + /// + /// - Note: It is undefined behavior to call this on a path that is no longer + /// valid. + @inlinable + internal var isOnNodeEnd: Bool { + !_isItem && nodeSlot.value == node.childCount + } +} + +extension _UnsafePath { + /// Returns an unmanaged reference to the child node this path is currently + /// addressing. + /// + /// - Note: It is undefined behavior to call this on a path that is no longer + /// valid. + @inlinable + internal var currentChild: _UnmanagedHashNode { + assert(isOnChild) + return node.unmanagedChild(at: nodeSlot) + } + + /// Returns the chid slot in this path corresponding to the specified level. + /// + /// - Note: It is undefined behavior to call this on a path that is no longer + /// valid. + @inlinable + internal func childSlot(at level: _HashLevel) -> _HashSlot { + assert(level < self.level) + return ancestors[level] + } + /// Returns the slot of the currently addressed item. + /// + /// - Note: It is undefined behavior to call this on a path that is no longer + /// valid. + @inlinable @inline(__always) + internal var currentItemSlot: _HashSlot { + assert(isOnItem) + return nodeSlot + } +} + +extension _UnsafePath { + /// Positions this path on the item with the specified slot within its + /// current node. + /// + /// - Note: It is undefined behavior to call this on a path that is no longer + /// valid. + @inlinable + internal mutating func selectItem(at slot: _HashSlot) { + // As a special exception, this allows slot to equal the item count. + // This can happen for paths that address the position a new item might be + // inserted later. + assert(slot <= node.itemsEndSlot) + nodeSlot = slot + _isItem = true + } + + /// Positions this path on the child with the specified slot within its + /// current node, without descending into it. + /// + /// - Note: It is undefined behavior to call this on a path that is no longer + /// valid. + @inlinable + internal mutating func selectChild(at slot: _HashSlot) { + // As a special exception, this allows slot to equal the child count. + // This is equivalent to a call to `selectEnd()`. + assert(slot <= node.childrenEndSlot) + nodeSlot = slot + _isItem = false + } + + /// Positions this path on the empty slot at the end of its current node. + /// + /// - Note: It is undefined behavior to call this on a path that is no longer + /// valid. + @usableFromInline + @_effects(releasenone) + internal mutating func selectEnd() { + nodeSlot = node.childrenEndSlot + _isItem = false + } + + /// Descend onto the first path within the currently selected child. + /// (Either the first item if it exists, or the first child. If the child + /// is an empty node (which should not happen in a valid hash tree), then this + /// selects the empty slot at the end of it. + /// + /// - Note: It is undefined behavior to call this on a path that is no longer + /// valid. + @inlinable + internal mutating func descend() { + self.node = currentChild + self.ancestors[level] = nodeSlot + self.nodeSlot = .zero + self._isItem = node.hasItems + self.level = level.descend() + } + + /// Descend onto the first path within the currently selected child. + /// (Either the first item if it exists, or the first child. If the child + /// is an empty node (which should not happen in a valid hash tree), then this + /// selects the empty slot at the end of it. + /// + /// - Note: It is undefined behavior to call this on a path that is no longer + /// valid. + @inlinable + internal mutating func descendToChild( + _ child: _UnmanagedHashNode, at slot: _HashSlot + ) { + assert(slot < node.childrenEndSlot) + assert(child == node.unmanagedChild(at: slot)) + self.node = child + self.ancestors[level] = slot + self.nodeSlot = .zero + self._isItem = node.hasItems + self.level = level.descend() + } + + internal mutating func ascend( + to ancestor: _UnmanagedHashNode, at level: _HashLevel + ) { + guard level != self.level else { return } + assert(level < self.level) + self.level = level + self.node = ancestor + self.nodeSlot = ancestors[level] + self.ancestors.clear(atOrBelow: level) + self._isItem = false + } + + /// Ascend to the nearest ancestor for which the `test` predicate returns + /// true. Because paths do not contain references to every node on them, + /// you need to manually supply a valid reference to the root node. This + /// method visits every node between the root and the current final node on + /// the path. + /// + /// - Note: It is undefined behavior to call this on a path that is no longer + /// valid. + internal mutating func ascendToNearestAncestor( + under root: _RawHashNode, + where test: (_UnmanagedHashNode, _HashSlot) -> Bool + ) -> Bool { + if self.level.isAtRoot { return false } + var best: _UnsafePath? = nil + var n = root.unmanaged + var l: _HashLevel = .top + while l < self.level { + let slot = self.ancestors[l] + if test(n, slot) { + best = _UnsafePath( + l, self.ancestors.truncating(to: l), n, childSlot: slot) + } + n = n.unmanagedChild(at: slot) + l = l.descend() + } + guard let best = best else { return false } + self = best + return true + } +} + +extension _UnsafePath { + /// Given a path that is on an item, advance it to the next item within its + /// current node, and return true. If there is no next item, position the path + /// on the first child, and return false. If there is no children, position + /// the path on the node's end position, and return false. + /// + /// - Note: It is undefined behavior to call this on a path that is no longer + /// valid. + mutating func selectNextItem() -> Bool { + assert(isOnItem) + nodeSlot = nodeSlot.next() + if nodeSlot < node.itemsEndSlot { return true } + nodeSlot = .zero + _isItem = false + return false + } + + /// Given a path that is on a child node, advance it to the next child within + /// its current node, and return true. If there is no next child, position + /// the path on the node's end position, and return false. + /// + /// - Note: It is undefined behavior to call this on a path that is no longer + /// valid. + mutating func selectNextChild() -> Bool { + assert(!isOnItem) + let childrenEndSlot = node.childrenEndSlot + guard nodeSlot < childrenEndSlot else { return false } + nodeSlot = nodeSlot.next() + return nodeSlot < childrenEndSlot + } +} + +extension _UnsafePath { + /// If this path addresses a child node, descend into the leftmost item + /// within the subtree under it (i.e., the first item that would be visited + /// by a preorder walk within that subtree). Do nothing if the path addresses + /// an item or the end position. + /// + /// - Note: It is undefined behavior to call this on a path that is no longer + /// valid. + @usableFromInline + @_effects(releasenone) + internal mutating func descendToLeftMostItem() { + while isOnChild { + descend() + } + } + + /// Given a path addressing a child node, descend into the rightmost item + /// within the subtree under it (i.e., the last item that would be visited + /// by a preorder walk within that subtree). Do nothing if the path addresses + /// an item or the end position. + /// + /// - Note: It is undefined behavior to call this on a path that is no longer + /// valid. + internal mutating func descendToRightMostItem() { + assert(isOnChild) + while true { + descend() + let childrenEndSlot = node.childrenEndSlot + guard childrenEndSlot > .zero else { break } + selectChild(at: childrenEndSlot.previous()) + } + let itemsEndSlot = node.itemsEndSlot + assert(itemsEndSlot > .zero) + selectItem(at: itemsEndSlot.previous()) + } + + /// Find the next item in a preorder walk in the tree following the currently + /// addressed item, and return true. Return false and do nothing if the + /// path does not currently address an item. + @usableFromInline + @_effects(releasenone) + internal mutating func findSuccessorItem(under root: _RawHashNode) -> Bool { + guard isOnItem else { return false } + if selectNextItem() { return true } + if node.hasChildren { + descendToLeftMostItem() + assert(isOnItem) + return true + } + if ascendToNearestAncestor( + under: root, where: { $1.next() < $0.childrenEndSlot } + ) { + let r = selectNextChild() + assert(r) + descendToLeftMostItem() + assert(isOnItem) + return true + } + self = _UnsafePath(root: root) + self.selectEnd() + return true + } + + /// Find the previous item in a preorder walk in the tree preceding the + /// currently addressed position, and return true. + /// Return false if there is no previous item. + @usableFromInline + @_effects(releasenone) + internal mutating func findPredecessorItem(under root: _RawHashNode) -> Bool { + switch (isOnItem, nodeSlot > .zero) { + case (true, true): + selectItem(at: nodeSlot.previous()) + return true + case (false, true): + selectChild(at: nodeSlot.previous()) + descendToRightMostItem() + return true + case (false, false): + if node.hasItems { + selectItem(at: node.itemsEndSlot.previous()) + return true + } + case (true, false): + break + } + guard + ascendToNearestAncestor( + under: root, + where: { $0.hasItems || $1 > .zero }) + else { return false } + + if nodeSlot > .zero { + selectChild(at: nodeSlot.previous()) + descendToRightMostItem() + return true + } + if node.hasItems { + selectItem(at: node.itemsEndSlot.previous()) + return true + } + return false + } +} + +extension _RawHashNode { + /// Return the integer position of the item addressed by the given path + /// within a preorder walk of the tree. If the path addresses the end + /// position, then return the number of items in the tree. + /// + /// This method must only be called on the root node. + internal func preorderPosition( + _ level: _HashLevel, of path: _UnsafePath + ) -> Int { + if path.isOnNodeEnd { return count } + assert(path.isOnItem) + if level < path.level { + let childSlot = path.childSlot(at: level) + return read { + let prefix = $0.children[.. (found: Bool, remaining: Int) { + assert(position >= 0) + let top = node + let topLevel = level + var stop = false + var remaining = position + while !stop { + let itemCount = node.itemCount + if remaining < itemCount { + selectItem(at: _HashSlot(remaining)) + return (true, 0) + } + remaining -= itemCount + node.read { + let children = $0.children + for i in children.indices { + let c = children[i].count + if remaining < c { + descendToChild(children[i].unmanaged, at: _HashSlot(i)) + return + } + remaining &-= c + } + stop = true + } + } + ascend(to: top, at: topLevel) + selectEnd() + return (false, remaining) + } +} + +extension _RawHashNode { + /// Return the number of steps between two paths within a preorder walk of the + /// tree. The two paths must not address a child node. + /// + /// This method must only be called on the root node. + @usableFromInline + @_effects(releasenone) + internal func distance( + _ level: _HashLevel, from start: _UnsafePath, to end: _UnsafePath + ) -> Int { + assert(level.isAtRoot) + if start.isOnNodeEnd { + // Shortcut: distance from end. + return preorderPosition(level, of: end) - count + } + if end.isOnNodeEnd { + // Shortcut: distance to end. + return count - preorderPosition(level, of: start) + } + assert(start.isOnItem) + assert(end.isOnItem) + if start.level == end.level, start.ancestors == end.ancestors { + // Shortcut: the paths are under the same node. + precondition(start.node == end.node, "Internal index validation error") + return start.currentItemSlot.distance(to: end.currentItemSlot) + } + if + start.level < end.level, + start.ancestors.isEqual(to: end.ancestors, upTo: start.level) + { + // Shortcut: start's node is an ancestor of end's position. + return start.node._distance( + start.level, fromItemAt: start.currentItemSlot, to: end) + } + if start.ancestors.isEqual(to: end.ancestors, upTo: end.level) { + // Shortcut: end's node is an ancestor of start's position. + return -end.node._distance( + end.level, fromItemAt: end.currentItemSlot, to: start) + } + // No shortcuts -- the two paths are in different subtrees. + // Start descending from the root to look for the closest common + // ancestor. + if start < end { + return _distance(level, from: start, to: end) + } + return -_distance(level, from: end, to: start) + } + + internal func _distance( + _ level: _HashLevel, from start: _UnsafePath, to end: _UnsafePath + ) -> Int { + assert(start < end) + assert(level < start.level) + assert(level < end.level) + let slot1 = start.childSlot(at: level) + let slot2 = end.childSlot(at: level) + if slot1 == slot2 { + return read { + $0[child: slot1]._distance(level.descend(), from: start, to: end) + } + } + return read { + let children = $0.children + let d1 = children[slot1.value] + .preorderPosition(level.descend(), of: start) + let d2 = children[slot1.value &+ 1 ..< slot2.value] + .reduce(0) { $0 + $1.count } + let d3 = children[slot2.value] + .preorderPosition(level.descend(), of: end) + return (children[slot1.value].count - d1) + d2 + d3 + } + } +} + +extension _UnmanagedHashNode { + internal func _distance( + _ level: _HashLevel, fromItemAt start: _HashSlot, to end: _UnsafePath + ) -> Int { + read { + assert(start < $0.itemsEndSlot) + assert(level < end.level) + let childSlot = end.childSlot(at: level) + let children = $0.children + let prefix = children[.. _UnsafePath? { + var node = unmanaged + var level: _HashLevel = .top + var ancestors: _AncestorHashSlots = .empty + while true { + let r = UnsafeHandle.read(node) { $0.find(level, key, hash) } + guard let r = r else { break } + guard r.descend else { + return _UnsafePath(level, ancestors, node, itemSlot: r.slot) + } + node = node.unmanagedChild(at: r.slot) + ancestors[level] = r.slot + level = level.descend() + } + return nil + } +} + +extension _RawHashNode { + @usableFromInline + @_effects(releasenone) + internal func seek( + _ level: _HashLevel, + _ path: inout _UnsafePath, + offsetBy distance: Int, + limitedBy limit: _UnsafePath + ) -> (found: Bool, limited: Bool) { + assert(level.isAtRoot) + if (distance > 0 && limit < path) || (distance < 0 && limit > path) { + return (seek(level, &path, offsetBy: distance), false) + } + var d = distance + guard self._seek(level, &path, offsetBy: &d) else { + path = limit + return (distance >= 0 && d == 0 && limit.isOnNodeEnd, true) + } + let found = ( + distance == 0 + || (distance > 0 && path <= limit) + || (distance < 0 && path >= limit)) + return (found, true) + } + + @usableFromInline + @_effects(releasenone) + internal func seek( + _ level: _HashLevel, + _ path: inout _UnsafePath, + offsetBy distance: Int + ) -> Bool { + var d = distance + if self._seek(level, &path, offsetBy: &d) { + return true + } + if distance > 0, d == 0 { // endIndex + return true + } + return false + } + + internal func _seek( + _ level: _HashLevel, + _ path: inout _UnsafePath, + offsetBy distance: inout Int + ) -> Bool { + // This is a bit complicated, because we only have a direct reference to the + // final node on the path, and we want to avoid having to descend + // from the root down if the target item stays within the original node's + // subtree. So we first figure out the subtree situation, and only start the + // recursion if the target is outside of it. + assert(level.isAtRoot) + assert(path.isOnItem || path.isOnNodeEnd) + guard distance != 0 else { return true } + if distance > 0 { + if !path.isOnItem { return false } + // Try a local search within the subtree starting at the current node. + let slot = path.currentItemSlot + let r = path.findItemAtPreorderPosition(distance &+ slot.value) + if r.found { + assert(r.remaining == 0) + return true + } + assert(r.remaining >= 0 && r.remaining <= distance) + distance = r.remaining + + // Fall back to recursively descending from the root. + return _seekForward(level, by: &distance, fromSubtree: &path) + } + // distance < 0 + if !path.isOnNodeEnd { + // Shortcut: see if the destination item is within the same node. + // (Doing this here allows us to avoid having to descend from the root + // down only to figure this out.) + let slot = path.nodeSlot + distance &+= slot.value + if distance >= 0 { + path.selectItem(at: _HashSlot(distance)) + distance = 0 + return true + } + } + // Otherwise we need to visit ancestor nodes to find the item at the right + // position. We also do this when we start from the end index -- there + // will be no recursion in that case anyway. + return _seekBackward(level, by: &distance, fromSubtree: &path) + } + + /// Find the item at the given positive distance from the last item within the + /// subtree rooted at the current node in `path`. + internal func _seekForward( + _ level: _HashLevel, + by distance: inout Int, + fromSubtree path: inout _UnsafePath + ) -> Bool { + assert(distance >= 0) + assert(level <= path.level) + guard level < path.level else { + path.selectEnd() + return false + } + return read { + let children = $0.children + var i = path.childSlot(at: level).value + if children[i]._seekForward( + level.descend(), by: &distance, fromSubtree: &path + ) { + assert(distance == 0) + return true + } + path.ascend(to: unmanaged, at: level) + i &+= 1 + while i < children.endIndex { + let c = children[i].count + if distance < c { + path.descendToChild(children[i].unmanaged, at: _HashSlot(i)) + let r = path.findItemAtPreorderPosition(distance) + precondition(r.found, "Internal inconsistency: invalid node counts") + assert(r.remaining == 0) + distance = 0 + return true + } + distance &-= c + i &+= 1 + } + path.selectEnd() + return false + } + } + + /// Find the item at the given negative distance from the first item within the + /// subtree rooted at the current node in `path`. + internal func _seekBackward( + _ level: _HashLevel, + by distance: inout Int, + fromSubtree path: inout _UnsafePath + ) -> Bool { + assert(distance < 0) + assert(level <= path.level) + + return read { + let children = $0.children + var slot: _HashSlot + if level < path.level { + // We need to descend to the end of the path before we can start the + // search for real. + slot = path.childSlot(at: level) + if children[slot.value]._seekBackward( + level.descend(), by: &distance, fromSubtree: &path + ) { + // A deeper level has found the target item. + assert(distance == 0) + return true + } + // No luck yet -- ascend to this node and look through preceding data. + path.ascend(to: unmanaged, at: level) + } else if path.isOnNodeEnd { + // When we start from the root's end (the end index), we don't need + // to descend before starting to look at previous children. + assert(level.isAtRoot && path.node == self.unmanaged) + slot = path.node.childrenEndSlot + } else { // level == path.level + // The outermost caller has already gone as far back as possible + // within the original subtree. Return a level higher to actually + // start the rest of the search. + return false + } + + // Look through all preceding children for the target item. + while slot > .zero { + slot = slot.previous() + let c = children[slot.value].count + if c + distance >= 0 { + path.descendToChild(children[slot.value].unmanaged, at: slot) + let r = path.findItemAtPreorderPosition(c + distance) + precondition(r.found, "Internal inconsistency: invalid node counts") + distance = 0 + return true + } + distance += c + } + // See if the target is hiding somewhere in our immediate items. + distance &+= $0.itemCount + if distance >= 0 { + path.selectItem(at: _HashSlot(distance)) + distance = 0 + return true + } + // No joy -- we need to continue searching a level higher. + assert(distance < 0) + return false + } + } +} + diff --git a/Sources/HashTreeCollections/HashTreeCollections.docc/Extensions/TreeDictionary.md b/Sources/HashTreeCollections/HashTreeCollections.docc/Extensions/TreeDictionary.md new file mode 100644 index 000000000..091c4d6f9 --- /dev/null +++ b/Sources/HashTreeCollections/HashTreeCollections.docc/Extensions/TreeDictionary.md @@ -0,0 +1,83 @@ +# ``HashTreeCollections/TreeDictionary`` + + + + + +## Topics + +### Collection Views + +`TreeDictionary` provides the customary dictionary views, `keys` and +`values`. These are collection types that are projections of the dictionary +itself, with elements that match only the keys or values of the dictionary, +respectively. The `Keys` view is notable in that it provides operations for +subtracting and intersecting the keys of two dictionaries, allowing for easy +detection of inserted and removed items between two snapshots of the same +dictionary. Because `TreeDictionary` needs to invalidate indices on every +mutation, its `Values` view is not a `MutableCollection`. + +- ``Keys-swift.struct`` +- ``Values-swift.struct`` +- ``keys-swift.property`` +- ``values-swift.property`` + +### Creating a Dictionary + +- ``init()`` +- ``init(_:)-111p1`` +- ``init(_:)-9atjh`` +- ``init(uniqueKeysWithValues:)-2hosl`` +- ``init(uniqueKeysWithValues:)-92276`` +- ``init(_:uniquingKeysWith:)-6nofo`` +- ``init(_:uniquingKeysWith:)-99403`` +- ``init(grouping:by:)-a4ma`` +- ``init(grouping:by:)-4he86`` +- ``init(keys:valueGenerator:)`` + + +### Inspecting a Dictionary + +- ``isEmpty-6icj0`` +- ``count-ibl8`` + +### Accessing Keys and Values + +- ``subscript(_:)-8gx3j`` +- ``subscript(_:default:)`` +- ``index(forKey:)`` + +### Adding or Updating Keys and Values + +Beyond the standard `updateValue(_:forKey:)` method, `TreeDictionary` also +provides additional `updateValue` variants that take closure arguments. These +provide a more straightforward way to perform in-place mutations on dictionary +values (compared to mutating values through the corresponding subscript +operation.) `TreeDictionary` also provides the standard `merge` and +`merging` operations for combining dictionary values. + +- ``updateValue(_:forKey:)`` +- ``updateValue(forKey:with:)`` +- ``updateValue(forKey:default:with:)`` +- ``merge(_:uniquingKeysWith:)-59cm5`` +- ``merge(_:uniquingKeysWith:)-38axt`` +- ``merge(_:uniquingKeysWith:)-3s4cw`` +- ``merging(_:uniquingKeysWith:)-3khxe`` +- ``merging(_:uniquingKeysWith:)-1k63w`` +- ``merging(_:uniquingKeysWith:)-87wp7`` + +### Removing Keys and Values + +- ``removeValue(forKey:)`` +- ``remove(at:)`` +- ``filter(_:)`` + +### Comparing Dictionaries + +- ``==(_:_:)`` + +### Transforming a Dictionary + +- ``mapValues(_:)`` +- ``compactMapValues(_:)`` + diff --git a/Sources/HashTreeCollections/HashTreeCollections.docc/Extensions/TreeSet.md b/Sources/HashTreeCollections/HashTreeCollections.docc/Extensions/TreeSet.md new file mode 100644 index 000000000..3642fac7a --- /dev/null +++ b/Sources/HashTreeCollections/HashTreeCollections.docc/Extensions/TreeSet.md @@ -0,0 +1,171 @@ +# ``HashTreeCollections/TreeSet`` + +### Implementation Details + +`TreeSet` and `TreeDictionary` are based on a Swift adaptation +of the *Compressed Hash-Array Mapped Prefix Tree* (CHAMP) data structure. + +- Michael J Steindorfer and Jurgen J Vinju. Optimizing Hash-Array Mapped + Tries for Fast and Lean Immutable JVM Collections. In *Proc. + International Conference on Object-Oriented Programming, Systems, + Languages, and Applications,* pp. 783-800, 2015. + https://doi.org/10.1145/2814270.2814312 + +In this setup, the members of such a collection are organized into a tree +data structure based on their hash values. For example, assuming 16 bit hash +values sliced into 4-bit chunks, each node in the prefix tree would have +sixteen slots (one for each digit), each of which may contain a member, a +child node reference, or it may be empty. A `TreeSet` containing the +three items `Maximo`, `Julia` and `Don Pablo` (with hash values of `0x2B65`, +`0xA69F` and `0xADA1`, respectively) may be organized into a prefix tree of +two nodes: + +``` +┌0┬1┬2───────┬3┬4┬5┬6┬7┬8┬9┬A──┬B┬C┬D┬E┬F┐ +│ │ │ Maximo │ │ │ │ │ │ │ │ • │ │ │ │ │ │ +└─┴─┴────────┴─┴─┴─┴─┴─┴─┴─┴─┼─┴─┴─┴─┴─┴─┘ + ╎ + ╎ + ┌0┬1┬2┬3┬4┬5┬6───┴──┬7┬8┬9┬A┬B┬C┬D──────────┬E┬F┐ + │ │ │ │ │ │ │ Julia │ │ │ │ │ │ │ Don Pablo │ │ │ + └─┴─┴─┴─┴─┴─┴───────┴─┴─┴─┴─┴─┴─┴───────────┴─┴─┘ +``` + +The root node directly contains `Maximo`, because it is the only set member +whose hash value starts with `2`. However, the first digits of the hashes of +`Julia` and `Don Pablo` are both `A`, so these items reside in a separate +node, one level below the root. + +(To save space, nodes are actually stored in a more compact form, with just +enough space allocated to store their contents: empty slots do not take up +any room. Hence the term "compressed" in "Compressed Hash-Array Mapped +Prefix Tree".) + +The resulting tree structure lends itself well to sharing nodes across +multiple collection values. Inserting or removing an item in a completely +shared tree requires copying at most log(n) nodes -- every node along the +path to the item needs to be uniqued, but all other nodes can remain shared. +While the cost of copying this many nodes isn't trivial, it is dramatically +lower than the cost of having to copy the entire data structure, like the +standard `Set` has to do. + +When looking up a particular member, we descend from the root node, +following along the path specified by successive digits of the member's hash +value. As long as hash values are unique, we will either find the member +we're looking for, or we will know for sure that it does not exist in the +set. + +In practice, hash values aren't guaranteed to be unique though. Members with +conflicting hash values need to be collected in special collision nodes that +are able to grow as large as necessary to contain all colliding members that +share the same hash. Looking up a member in one of these nodes requires a +linear search, so it is crucial that such collisions do not happen often. + +As long as `Element` properly implements `Hashable`, lookup operations in a +`TreeSet` are expected to be able to decide whether the set contains a +particular item by looking at no more than a constant number of items on +average -- typically they will need to compare against just one member. + +## Topics + +### Creating a Set + +- ``init()`` +- ``init(_:)-2uun3`` +- ``init(_:)-714nu`` +- ``init(_:)-6lt4a`` + +### Finding Elements + +- ``contains(_:)`` +- ``firstIndex(of:)`` +- ``lastIndex(of:)`` + +### Adding and Updating Elements + +- ``insert(_:)`` +- ``update(with:)`` +- ``update(_:at:)`` + +### Removing Elements + +- ``remove(_:)`` +- ``remove(at:)`` +- ``filter(_:)`` +- ``removeAll(where:)`` + +### Combining Sets + +All the standard combining operations (intersection, union, subtraction and +symmetric difference) are supported, in both non-mutating and mutating forms. +`SetAlgebra` only requires the ability to combine one set instance with another, +but `TreeSet` follows the tradition established by `Set` in providing +additional overloads to each operation that allow combining a set with +additional types, including arbitrary sequences. + +- ``intersection(_:)-8ltpr`` +- ``intersection(_:)-9kwc0`` +- ``intersection(_:)-4u7ew`` + +- ``union(_:)-89jj2`` +- ``union(_:)-9yvze`` +- ``union(_:)-7p0m2`` + +- ``subtracting(_:)-cnsi`` +- ``subtracting(_:)-3yfac`` +- ``subtracting(_:)-90wrb`` + +- ``symmetricDifference(_:)-5bz4f`` +- ``symmetricDifference(_:)-6p8n5`` +- ``symmetricDifference(_:)-3qk9w`` + +- ``formIntersection(_:)-1zcar`` +- ``formIntersection(_:)-4xkf0`` +- ``formIntersection(_:)-6jb2z`` + +- ``formUnion(_:)-420zl`` +- ``formUnion(_:)-8zu6q`` +- ``formUnion(_:)-423id`` + +- ``subtract(_:)-49o9`` +- ``subtract(_:)-3ebkc`` +- ``subtract(_:)-87rhs`` + +- ``formSymmetricDifference(_:)-94f6x`` +- ``formSymmetricDifference(_:)-4x7vw`` +- ``formSymmetricDifference(_:)-6ypuy`` + +### Comparing Sets + +`TreeSet` supports all standard set comparisons (subset tests, superset +tests, disjunctness test), including the customary overloads established by +`Set`. As an additional extension, the `isEqualSet` family of member functions +generalize the standard `==` operation to support checking whether a +`TreeSet` consists of exactly the same members as an arbitrary sequence. +Like `==`, the `isEqualSet` functions ignore element ordering and duplicates (if +any). + +- ``==(_:_:)`` +- ``isEqualSet(to:)-4bc1i`` +- ``isEqualSet(to:)-7x4yi`` +- ``isEqualSet(to:)-44fkf`` + +- ``isSubset(of:)-2ktpu`` +- ``isSubset(of:)-5oufi`` +- ``isSubset(of:)-9tq5c`` + +- ``isSuperset(of:)-3zd41`` +- ``isSuperset(of:)-6xa75`` +- ``isSuperset(of:)-6vw4t`` + +- ``isStrictSubset(of:)-6xuil`` +- ``isStrictSubset(of:)-22f80`` +- ``isStrictSubset(of:)-5f78e`` + +- ``isStrictSuperset(of:)-4ryjr`` +- ``isStrictSuperset(of:)-3ephc`` +- ``isStrictSuperset(of:)-9ftlc`` + +- ``isDisjoint(with:)-4a9xa`` +- ``isDisjoint(with:)-12a64`` +- ``isDisjoint(with:)-5lvdr`` diff --git a/Sources/HashTreeCollections/HashTreeCollections.docc/HashTreeCollections.md b/Sources/HashTreeCollections/HashTreeCollections.docc/HashTreeCollections.md new file mode 100644 index 000000000..9d3a927b8 --- /dev/null +++ b/Sources/HashTreeCollections/HashTreeCollections.docc/HashTreeCollections.md @@ -0,0 +1,20 @@ +# ``HashTreeCollections`` + +**Swift Collections** is an open-source package of data structure implementations for the Swift programming language. + +## Overview + + + +#### Additional Resources + +- [`Swift Collections` on GitHub](https://github.com/apple/swift-collections/) +- [`Swift Collections` on the Swift Forums](https://forums.swift.org/c/related-projects/collections/72) + + +## Topics + +### Persistent Collections + +- ``TreeSet`` +- ``TreeDictionary`` diff --git a/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+Codable.swift b/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+Codable.swift new file mode 100644 index 000000000..d0dbc2f9f --- /dev/null +++ b/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+Codable.swift @@ -0,0 +1,186 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +// Code in this file is a slightly adapted variant of `Dictionary`'s `Codable` +// implementation in the Standard Library as of Swift 5.7. +// `TreeDictionary` therefore encodes/decodes itself exactly the same as +// `Dictionary`, and the two types can each decode data encoded by the other. + +/// A wrapper for dictionary keys which are Strings or Ints. +internal struct _DictionaryCodingKey: CodingKey { + internal let stringValue: String + internal let intValue: Int? + + internal init(stringValue: String) { + self.stringValue = stringValue + self.intValue = Int(stringValue) + } + + internal init(intValue: Int) { + self.stringValue = "\(intValue)" + self.intValue = intValue + } + + fileprivate init(codingKey: CodingKey) { + self.stringValue = codingKey.stringValue + self.intValue = codingKey.intValue + } +} + +extension TreeDictionary: Encodable +where Key: Encodable, Value: Encodable +{ + /// Encodes the elements of this dictionary into the given encoder. + /// + /// - Parameter encoder: The encoder to write data to. + public func encode(to encoder: Encoder) throws { + if Key.self == String.self { + // Since the keys are already Strings, we can use them as keys directly. + var container = encoder.container(keyedBy: _DictionaryCodingKey.self) + for (key, value) in self { + let codingKey = _DictionaryCodingKey(stringValue: key as! String) + try container.encode(value, forKey: codingKey) + } + return + } + if Key.self == Int.self { + // Since the keys are already Ints, we can use them as keys directly. + var container = encoder.container(keyedBy: _DictionaryCodingKey.self) + for (key, value) in self { + let codingKey = _DictionaryCodingKey(intValue: key as! Int) + try container.encode(value, forKey: codingKey) + } + return + } + if #available(macOS 12.3, iOS 15.4, watchOS 8.5, tvOS 15.4, *), + Key.self is CodingKeyRepresentable.Type { + // Since the keys are CodingKeyRepresentable, we can use the `codingKey` + // to create `_DictionaryCodingKey` instances. + var container = encoder.container(keyedBy: _DictionaryCodingKey.self) + for (key, value) in self { + let codingKey = (key as! CodingKeyRepresentable).codingKey + let dictionaryCodingKey = _DictionaryCodingKey(codingKey: codingKey) + try container.encode(value, forKey: dictionaryCodingKey) + } + return + } + // Keys are Encodable but not Strings or Ints, so we cannot arbitrarily + // convert to keys. We can encode as an array of alternating key-value + // pairs, though. + var container = encoder.unkeyedContainer() + for (key, value) in self { + try container.encode(key) + try container.encode(value) + } + } +} + +extension TreeDictionary: Decodable +where Key: Decodable, Value: Decodable +{ + /// Creates a new dictionary by decoding from the given decoder. + /// + /// This initializer throws an error if reading from the decoder fails, or + /// if the data read is corrupted or otherwise invalid. + /// + /// - Parameter decoder: The decoder to read data from. + public init(from decoder: Decoder) throws { + self.init() + + if Key.self == String.self { + // The keys are Strings, so we should be able to expect a keyed container. + let container = try decoder.container(keyedBy: _DictionaryCodingKey.self) + for key in container.allKeys { + let value = try container.decode(Value.self, forKey: key) + self[key.stringValue as! Key] = value + } + return + } + if Key.self == Int.self { + // The keys are Ints, so we should be able to expect a keyed container. + let container = try decoder.container(keyedBy: _DictionaryCodingKey.self) + for key in container.allKeys { + guard key.intValue != nil else { + // We provide stringValues for Int keys; if an encoder chooses not to + // use the actual intValues, we've encoded string keys. + // So on init, _DictionaryCodingKey tries to parse string keys as + // Ints. If that succeeds, then we would have had an intValue here. + // We don't, so this isn't a valid Int key. + var codingPath = decoder.codingPath + codingPath.append(key) + throw DecodingError.typeMismatch( + Int.self, + DecodingError.Context( + codingPath: codingPath, + debugDescription: "Expected Int key but found String key instead." + ) + ) + } + + let value = try container.decode(Value.self, forKey: key) + self[key.intValue! as! Key] = value + } + return + } + + if #available(macOS 12.3, iOS 15.4, watchOS 8.5, tvOS 15.4, *), + let keyType = Key.self as? CodingKeyRepresentable.Type { + // The keys are CodingKeyRepresentable, so we should be able to expect + // a keyed container. + let container = try decoder.container(keyedBy: _DictionaryCodingKey.self) + for codingKey in container.allKeys { + guard let key: Key = keyType.init(codingKey: codingKey) as? Key else { + throw DecodingError.dataCorruptedError( + forKey: codingKey, + in: container, + debugDescription: "Could not convert key to type \(Key.self)" + ) + } + let value: Value = try container.decode(Value.self, forKey: codingKey) + self[key] = value + } + return + } + + // We should have encoded as an array of alternating key-value pairs. + var container = try decoder.unkeyedContainer() + + // We're expecting to get pairs. If the container has a known count, it + // had better be even; no point in doing work if not. + if let count = container.count { + guard count % 2 == 0 else { + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Expected collection of key-value pairs; encountered odd-length array instead." + ) + ) + } + } + + while !container.isAtEnd { + let key = try container.decode(Key.self) + + guard !container.isAtEnd else { + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Unkeyed container reached end before value in key-value pair." + ) + ) + } + + let value = try container.decode(Value.self) + self[key] = value + } + } +} + diff --git a/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+Collection.swift b/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+Collection.swift new file mode 100644 index 000000000..ad73356af --- /dev/null +++ b/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+Collection.swift @@ -0,0 +1,303 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 TreeDictionary { + /// The position of an element in a persistent dictionary. + /// + /// An index in a persistent dictionary is a compact encoding of a path in + /// the underlying prefix tree. Such indices are valid until the tree + /// structure is changed; hence, indices are usually invalidated every time + /// the dictionary gets mutated. + @frozen + public struct Index { + @usableFromInline + internal let _root: _UnmanagedHashNode + + @usableFromInline + internal var _version: UInt + + @usableFromInline + internal var _path: _UnsafePath + + @inlinable @inline(__always) + internal init( + _root: _UnmanagedHashNode, version: UInt, path: _UnsafePath + ) { + self._root = _root + self._version = version + self._path = path + } + } +} + +extension TreeDictionary.Index: @unchecked Sendable +where Key: Sendable, Value: Sendable {} + +extension TreeDictionary.Index: Equatable { + /// Returns a Boolean value indicating whether two index values are equal. + /// + /// Note that comparing two indices that do not belong to the same tree + /// leads to a runtime error. + /// + /// - Complexity: O(1) + @inlinable + public static func ==(left: Self, right: Self) -> Bool { + precondition( + left._root == right._root && left._version == right._version, + "Indices from different dictionary values aren't comparable") + return left._path == right._path + } +} + +extension TreeDictionary.Index: Comparable { + /// Returns a Boolean value indicating whether the value of the first argument + /// is less than the second argument. + /// + /// Note that comparing two indices that do not belong to the same tree + /// leads to a runtime error. + /// + /// - Complexity: O(1) + public static func <(left: Self, right: Self) -> Bool { + precondition( + left._root == right._root && left._version == right._version, + "Indices from different dictionary values aren't comparable") + return left._path < right._path + } +} + +extension TreeDictionary.Index: Hashable { + /// Hashes the essential components of this value by feeding them into the + /// given hasher. + /// + /// - Complexity: O(1) + @inlinable + public func hash(into hasher: inout Hasher) { + hasher.combine(_path) + } +} + +extension TreeDictionary.Index: CustomStringConvertible { + // A textual representation of this instance. + public var description: String { + _path.description + } +} + +extension TreeDictionary.Index: CustomDebugStringConvertible { + /// A textual representation of this instance, suitable for debugging. + public var debugDescription: String { + description + } +} + +extension TreeDictionary: Collection { + /// A Boolean value indicating whether the collection is empty. + /// + /// - Complexity: O(1) + @inlinable + public var isEmpty: Bool { + _root.count == 0 + } + + /// The number of elements in the collection. + /// + /// - Complexity: O(1) + @inlinable + public var count: Int { + _root.count + } + + /// The position of the first element in a nonempty collection, or `endIndex` + /// if the collection is empty. + /// + /// - Complexity: O(1) + @inlinable + public var startIndex: Index { + var path = _UnsafePath(root: _root.raw) + path.descendToLeftMostItem() + return Index(_root: _root.unmanaged, version: _version, path: path) + } + + /// The collection’s “past the end” position—that is, the position one greater + /// than the last valid subscript argument. + /// + /// - Complexity: O(1) + @inlinable + public var endIndex: Index { + var path = _UnsafePath(root: _root.raw) + path.selectEnd() + return Index(_root: _root.unmanaged, version: _version, path: path) + } + + @inlinable @inline(__always) + internal func _isValid(_ i: Index) -> Bool { + _root.isIdentical(to: i._root) && i._version == self._version + } + + @inlinable @inline(__always) + internal mutating func _invalidateIndices() { + _version &+= 1 + } + + /// Accesses the key-value pair at the specified position. + /// + /// - Parameter position: The position of the element to access. `position` + /// must be a valid index of the collection that is not equal to + /// `endIndex`. + /// + /// - Complexity: O(1) + @inlinable + public subscript(i: Index) -> Element { + precondition(_isValid(i), "Invalid index") + precondition(i._path.isOnItem, "Can't get element at endIndex") + return _UnsafeHandle.read(i._path.node) { + $0[item: i._path.currentItemSlot] + } + } + + /// Replaces the given index with its successor. + /// + /// - Parameter i: A valid index of the collection. + /// `i` must be less than `endIndex`. + /// + /// - Complexity: O(1) + @inlinable + public func formIndex(after i: inout Index) { + precondition(_isValid(i), "Invalid index") + guard i._path.findSuccessorItem(under: _root.raw) else { + preconditionFailure("The end index has no successor") + } + } + + /// Returns the position immediately after the given index. + /// + /// - Parameter i: A valid index of the collection. + /// `i` must be less than `endIndex`. + /// + /// - Complexity: O(1) + @inlinable @inline(__always) + public func index(after i: Index) -> Index { + var i = i + formIndex(after: &i) + return i + } + + /// Returns the distance between two arbitrary valid indices in this + /// collection. + /// + /// - Parameter start: A valid index of the collection. + /// - Parameter end: Another valid index of the collection. + /// - Returns: The distance between `start` and `end`. + /// (The result can be negative, even though `TreeDictionary` is not + /// a bidirectional collection.) + /// - Complexity: O(log(`count`)) + @inlinable + public func distance(from start: Index, to end: Index) -> Int { + precondition(_isValid(start) && _isValid(end), "Invalid index") + return _root.raw.distance(.top, from: start._path, to: end._path) + } + + /// Returns an index that is the specified distance from the given index. + /// + /// The value passed as `distance` must not offset `i` beyond the bounds of + /// the collection. + /// + /// - Parameters: + /// - i: A valid index of the collection. + /// - distance: The distance to offset `i`. As a special exception, + /// `distance` is allowed to be negative even though + /// `TreeDictionary` isn't a bidirectional collection. + /// - Returns: An index offset by `distance` from the index `i`. If + /// `distance` is positive, this is the same value as the result of + /// `distance` calls to `index(after:)`. If distance is negative, then + /// `distance` calls to `index(after:)` on the returned value will be the + /// same as `start`. + /// + /// - Complexity: O(log(`distance`)) + @inlinable + public func index(_ i: Index, offsetBy distance: Int) -> Index { + precondition(_isValid(i), "Invalid index") + var i = i + let r = _root.raw.seek(.top, &i._path, offsetBy: distance) + precondition(r, "Index offset out of bounds") + return i + } + + /// Returns an index that is the specified distance from the given index, + /// unless that distance is beyond a given limiting index. + /// + /// The value passed as `distance` must not offset `i` beyond the bounds of + /// the collection, unless the index passed as `limit` prevents offsetting + /// beyond those bounds. + /// + /// - Parameters: + /// - i: A valid index of the collection. + /// - distance: The distance to offset `i`. As a special exception, + /// `distance` is allowed to be negative even though + /// `TreeDictionary` isn't a bidirectional collection. + /// - limit: A valid index of the collection to use as a limit. If + /// `distance > 0`, a limit that is less than `i` has no effect. + /// Likewise, if `distance < 0`, a limit that is greater than `i` has no + /// effect. + /// - Returns: An index offset by `distance` from the index `i`, unless that + /// index would be beyond `limit` in the direction of movement. In that + /// case, the method returns `nil`. + /// + /// - Complexity: O(log(`distance`)) + @inlinable + public func index( + _ i: Index, offsetBy distance: Int, limitedBy limit: Index + ) -> Index? { + precondition(_isValid(i), "Invalid index") + precondition(_isValid(limit), "Invalid limit index") + var i = i + let (found, limited) = _root.raw.seek( + .top, &i._path, offsetBy: distance, limitedBy: limit._path + ) + if found { return i } + precondition(limited, "Index offset out of bounds") + return nil + } +} + +#if false +// Note: Let's not do this. `BidirectionalCollection` would imply that +// the ordering of elements would be meaningful, which isn't true for +// `TreeDictionary`. +extension TreeDictionary: BidirectionalCollection { + /// Replaces the given index with its predecessor. + /// + /// - Parameter i: A valid index of the collection. + /// `i` must be greater than `startIndex`. + /// + /// - Complexity: O(1) + @inlinable + public func formIndex(before i: inout Index) { + precondition(_isValid(i), "Invalid index") + guard i._path.findPredecessorItem(under: _root.raw) else { + preconditionFailure("The start index has no predecessor") + } + } + + /// Returns the position immediately before the given index. + /// + /// - Parameter i: A valid index of the collection. + /// `i` must be greater than `startIndex`. + /// + /// - Complexity: O(1) + @inlinable @inline(__always) + public func index(before i: Index) -> Index { + var i = i + formIndex(before: &i) + return i + } +} +#endif diff --git a/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+CustomReflectable.swift b/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+CustomReflectable.swift new file mode 100644 index 000000000..046b7855e --- /dev/null +++ b/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+CustomReflectable.swift @@ -0,0 +1,17 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 TreeDictionary: CustomReflectable { + /// The custom mirror for this instance. + public var customMirror: Mirror { + Mirror(self, unlabeledChildren: self, displayStyle: .dictionary) + } +} diff --git a/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+Debugging.swift b/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+Debugging.swift new file mode 100644 index 000000000..9b8d78f1c --- /dev/null +++ b/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+Debugging.swift @@ -0,0 +1,45 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension TreeDictionary { + /// True if consistency checking is enabled in the implementation of this + /// type, false otherwise. + /// + /// Documented performance promises are null and void when this property + /// returns true -- for example, operations that are documented to take + /// O(1) time might take O(*n*) time, or worse. + public static var _isConsistencyCheckingEnabled: Bool { + _isCollectionsInternalCheckingEnabled + } + + @inlinable + public func _invariantCheck() { + _root._fullInvariantCheck() + } + + public func _dump(iterationOrder: Bool = false) { + _root.dump(iterationOrder: iterationOrder) + } + + public static var _maxDepth: Int { + _HashLevel.limit + } + + public var _statistics: _HashTreeStatistics { + var stats = _HashTreeStatistics() + _root.gatherStatistics(.top, &stats) + return stats + } +} diff --git a/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+Descriptions.swift b/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+Descriptions.swift new file mode 100644 index 000000000..987eda624 --- /dev/null +++ b/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+Descriptions.swift @@ -0,0 +1,28 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension TreeDictionary: CustomStringConvertible { + // A textual representation of this instance. + public var description: String { + _dictionaryDescription(for: self) + } +} + +extension TreeDictionary: CustomDebugStringConvertible { + /// A textual representation of this instance, suitable for debugging. + public var debugDescription: String { + description + } +} diff --git a/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+Equatable.swift b/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+Equatable.swift new file mode 100644 index 000000000..1fb1ec382 --- /dev/null +++ b/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+Equatable.swift @@ -0,0 +1,23 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2019 - 2024 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 TreeDictionary: Equatable where Value: Equatable { + /// Returns a Boolean value indicating whether two values are equal. + /// + /// Two persistent dictionaries are considered equal if they contain the same + /// key-value pairs, but not necessarily in the same order. + /// + /// - Complexity: O(`min(left.count, right.count)`) + @inlinable + public static func == (left: Self, right: Self) -> Bool { + left._root.isEqualSet(to: right._root, by: { $0 == $1 }) + } +} diff --git a/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+ExpressibleByDictionaryLiteral.swift b/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+ExpressibleByDictionaryLiteral.swift new file mode 100644 index 000000000..c41671885 --- /dev/null +++ b/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+ExpressibleByDictionaryLiteral.swift @@ -0,0 +1,35 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2019 - 2024 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 TreeDictionary: ExpressibleByDictionaryLiteral { + /// Creates a new dictionary from the contents of a dictionary literal. + /// + /// The literal must not contain duplicate elements. + /// + /// Do not call this initializer directly. It is used by the compiler when + /// you use a dictionary literal. Instead, create a new dictionary using a + /// dictionary literal as its value by enclosing a comma-separated list of + /// values in square brackets. You can use a dictionary literal anywhere a + /// persistent dictionary is expected by the type context. + /// + /// Like the standard `Dictionary`, persistent dictionaries do not preserve + /// the order of elements inside the array literal. + /// + /// - Parameter elements: A variadic list of elements of the new set. + /// + /// - Complexity: O(`elements.count`) if `Element` properly implements + /// hashing. + @inlinable + @inline(__always) + public init(dictionaryLiteral elements: (Key, Value)...) { + self.init(uniqueKeysWithValues: elements) + } +} diff --git a/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+Filter.swift b/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+Filter.swift new file mode 100644 index 000000000..9ed113221 --- /dev/null +++ b/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+Filter.swift @@ -0,0 +1,58 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 TreeDictionary { + /// Returns a new persistent dictionary containing the key-value pairs of this + /// dictionary that satisfy the given predicate. + /// + /// - Parameter isIncluded: A closure that takes a key-value pair as its + /// argument and returns a Boolean value indicating whether it should be + /// included in the returned dictionary. + /// + /// - Returns: A dictionary of the key-value pairs that `isIncluded` allows. + /// + /// - Complexity: O(`count`) + @inlinable + public func filter( + _ isIncluded: (Element) throws -> Bool + ) rethrows -> Self { + let result = try _root.filter(.top, isIncluded) + guard let result = result else { return self } + let r = TreeDictionary(_new: result.finalize(.top)) + r._invariantCheck() + return r + } + + /// Removes all the elements that satisfy the given predicate. + /// + /// Use this method to remove every element in the dictionary that meets + /// particular criteria. + /// This example removes all the odd valued items from a + /// dictionary mapping strings to numbers: + /// + /// var numbers: TreeDictionary = ["a": 5, "b": 6, "c": 7, "d": 8] + /// numbers.removeAll(where: { $0.value % 2 != 0 }) + /// // numbers == ["b": 6, "d": 8] + /// + /// - Parameter shouldBeRemoved: A closure that takes an element of the + /// dictionary as its argument and returns a Boolean value indicating + /// whether the element should be removed from the collection. + /// + /// - Complexity: O(`count`) + @inlinable + public mutating func removeAll( + where shouldBeRemoved: (Element) throws -> Bool + ) rethrows { + // FIXME: Implement in-place reductions + self = try filter { try !shouldBeRemoved($0) } + } + +} diff --git a/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+Hashable.swift b/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+Hashable.swift new file mode 100644 index 000000000..4cda8c416 --- /dev/null +++ b/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+Hashable.swift @@ -0,0 +1,28 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2019 - 2024 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 TreeDictionary: Hashable where Value: Hashable { + /// Hashes the essential components of this value by feeding them into the + /// given hasher. + /// + /// Complexity: O(`count`) + @inlinable + public func hash(into hasher: inout Hasher) { + var commutativeHash = 0 + for (key, value) in self { + var elementHasher = hasher + elementHasher.combine(key) + elementHasher.combine(value) + commutativeHash ^= elementHasher.finalize() + } + hasher.combine(commutativeHash) + } +} diff --git a/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+Initializers.swift b/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+Initializers.swift new file mode 100644 index 000000000..9fdb1b027 --- /dev/null +++ b/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+Initializers.swift @@ -0,0 +1,313 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension TreeDictionary { + /// Creates an empty dictionary. + /// + /// This initializer is equivalent to initializing with an empty dictionary + /// literal. + /// + /// - Complexity: O(1) + @inlinable + public init() { + self.init(_new: ._emptyNode()) + } + + /// Makes a copy of an existing persistent dictionary. + /// + /// - Complexity: O(1) + @inlinable + public init(_ other: TreeDictionary) { + self = other + } + + /// Creates a new persistent dictionary that contains the same key-value + /// pairs as the given `Dictionary` instance, although not necessarily + /// in the same order. + /// + /// - Complexity: O(`other.count`) + @inlinable + public init(_ other: Dictionary) { + self.init(_uniqueKeysWithValues: other) + } + + /// Creates a new persistent dictionary by associating the given persistent + /// set of keys with the values generated using the specified closure. + /// + /// - Complexity: O(`other.count`) + @inlinable + public init( + keys: TreeSet, + valueGenerator valueTransform: (Key) throws -> Value + ) rethrows { + // FIXME: This is a non-standard addition + let root = try keys._root.mapValues { try valueTransform($0.key) } + self.init(_new: root) + } + + /// Creates a new dictionary from the key-value pairs in the given sequence. + /// + /// You use this initializer to create a dictionary when you have a sequence + /// of key-value tuples with unique keys. Passing a sequence with duplicate + /// keys to this initializer results in a runtime error. If your + /// sequence might have duplicate keys, use the + /// `Dictionary(_:uniquingKeysWith:)` initializer instead. + /// + /// - Parameter keysAndValues: A sequence of key-value pairs to use for + /// the new dictionary. Every key in `keysAndValues` must be unique. + /// + /// - Returns: A new dictionary initialized with the elements of + /// `keysAndValues`. + /// + /// - Precondition: The sequence must not have duplicate keys. + /// + /// - Complexity: Expected O(*n*) on average, where *n* is the count if + /// key-value pairs, if `Key` properly implements hashing. + @inlinable + public init( + uniqueKeysWithValues keysAndValues: some Sequence<(Key, Value)> + ) { + self.init() + for item in keysAndValues { + let hash = _Hash(item.0) + let r = _root.insert(.top, item, hash) + precondition(r.inserted, "Duplicate key: '\(item.0)'") + } + _invariantCheck() + } + + /// Creates a new dictionary from the key-value pairs in the given sequence. + /// + /// You use this initializer to create a dictionary when you have a sequence + /// of key-value tuples with unique keys. Passing a sequence with duplicate + /// keys to this initializer results in a runtime error. If your + /// sequence might have duplicate keys, use the + /// `Dictionary(_:uniquingKeysWith:)` initializer instead. + /// + /// - Parameter keysAndValues: A sequence of key-value pairs to use for + /// the new dictionary. Every key in `keysAndValues` must be unique. + /// + /// - Returns: A new dictionary initialized with the elements of + /// `keysAndValues`. + /// + /// - Precondition: The sequence must not have duplicate keys. + /// + /// - Complexity: Expected O(*n*) on average, where *n* is the count if + /// key-value pairs, if `Key` properly implements hashing. + @_disfavoredOverload // https://github.com/apple/swift-collections/issues/125 + @inlinable + public init( + uniqueKeysWithValues keysAndValues: some Sequence + ) { + if let keysAndValues = _specialize(keysAndValues, for: Self.self) { + self = keysAndValues + return + } + if let keysAndValues = _specialize( + keysAndValues, for: Dictionary.self + ) { + self.init(keysAndValues) + return + } + self.init(_uniqueKeysWithValues: keysAndValues) + } + + @inlinable + internal init( + _uniqueKeysWithValues keysAndValues: some Sequence + ) { + self.init() + for item in keysAndValues { + let hash = _Hash(item.key) + let r = _root.insert(.top, item, hash) + precondition(r.inserted, "Duplicate key: '\(item.key)'") + } + _invariantCheck() + } + + /// Creates a new dictionary from the key-value pairs in the given sequence, + /// using a combining closure to determine the value for any duplicate keys. + /// + /// You use this initializer to create a dictionary when you have a sequence + /// of key-value tuples that might have duplicate keys. As the dictionary is + /// built, the initializer calls the `combine` closure with the current and + /// new values for any duplicate keys. Pass a closure as `combine` that + /// returns the value to use in the resulting dictionary: The closure can + /// choose between the two values, combine them to produce a new value, or + /// even throw an error. + /// + /// let pairsWithDuplicateKeys = [("a", 1), ("b", 2), ("a", 3), ("b", 4)] + /// + /// let firstValues = TreeDictionary( + /// pairsWithDuplicateKeys, + /// uniquingKeysWith: { (first, _) in first }) + /// // ["a": 1, "b": 2] + /// + /// let lastValues = TreeDictionary( + /// pairsWithDuplicateKeys, + /// uniquingKeysWith: { (_, last) in last }) + /// // ["a": 3, "b": 4] + /// + /// - Parameters: + /// - keysAndValues: A sequence of key-value pairs to use for the new + /// dictionary. + /// - combine: A closure that is called with the values for any duplicate + /// keys that are encountered. The closure returns the desired value for + /// the final dictionary. + /// + /// - Complexity: Expected O(*n*) on average, where *n* is the count of + /// key-value pairs, if `Key` properly implements hashing. + public init( + _ keysAndValues: some Sequence<(Key, Value)>, + uniquingKeysWith combine: (Value, Value) throws -> Value + ) rethrows { + self.init() + try self.merge(keysAndValues, uniquingKeysWith: combine) + } + + /// Creates a new dictionary from the key-value pairs in the given sequence, + /// using a combining closure to determine the value for any duplicate keys. + /// + /// You use this initializer to create a dictionary when you have a sequence + /// of key-value tuples that might have duplicate keys. As the dictionary is + /// built, the initializer calls the `combine` closure with the current and + /// new values for any duplicate keys. Pass a closure as `combine` that + /// returns the value to use in the resulting dictionary: The closure can + /// choose between the two values, combine them to produce a new value, or + /// even throw an error. + /// + /// let pairsWithDuplicateKeys = [("a", 1), ("b", 2), ("a", 3), ("b", 4)] + /// + /// let firstValues = TreeDictionary( + /// pairsWithDuplicateKeys, + /// uniquingKeysWith: { (first, _) in first }) + /// // ["a": 1, "b": 2] + /// + /// let lastValues = TreeDictionary( + /// pairsWithDuplicateKeys, + /// uniquingKeysWith: { (_, last) in last }) + /// // ["a": 3, "b": 4] + /// + /// - Parameters: + /// - keysAndValues: A sequence of key-value pairs to use for the new + /// dictionary. + /// - combine: A closure that is called with the values for any duplicate + /// keys that are encountered. The closure returns the desired value for + /// the final dictionary. + /// + /// - Complexity: Expected O(*n*) on average, where *n* is the count of + /// key-value pairs, if `Key` properly implements hashing. + @_disfavoredOverload // https://github.com/apple/swift-collections/issues/125 + public init( + _ keysAndValues: some Sequence, + uniquingKeysWith combine: (Value, Value) throws -> Value + ) rethrows { + try self.init( + keysAndValues.lazy.map { ($0.key, $0.value) }, + uniquingKeysWith: combine) + } +} + +extension TreeDictionary { + /// Creates a new dictionary whose keys are the groupings returned by the + /// given closure and whose values are arrays of the elements that returned + /// each key. + /// + /// The arrays in the "values" position of the new dictionary each contain at + /// least one element, with the elements in the same order as the source + /// sequence. + /// + /// The following example declares an array of names, and then creates a + /// dictionary from that array by grouping the names by first letter: + /// + /// let students = ["Kofi", "Abena", "Efua", "Kweku", "Akosua"] + /// let studentsByLetter = TreeDictionary(grouping: students, by: { $0.first! }) + /// // ["K": ["Kofi", "Kweku"], "A": ["Abena", "Akosua"], "E": ["Efua"]] + /// + /// The new `studentsByLetter` dictionary has three entries, with students' + /// names grouped by the keys `"E"`, `"K"`, and `"A"`. + /// + /// - Parameters: + /// - values: A sequence of values to group into a dictionary. + /// - keyForValue: A closure that returns a key for each element in + /// `values`. + /// + /// - Complexity: Expected O(*n*) on average, where *n* is the count of + /// values, if `Key` properly implements hashing. + @inlinable @inline(__always) + public init( + grouping values: S, + by keyForValue: (S.Element) throws -> Key + ) rethrows + where Value: RangeReplaceableCollection, Value.Element == S.Element + { + try self.init(_grouping: values, by: keyForValue) + } + + /// Creates a new dictionary whose keys are the groupings returned by the + /// given closure and whose values are arrays of the elements that returned + /// each key. + /// + /// The arrays in the "values" position of the new dictionary each contain at + /// least one element, with the elements in the same order as the source + /// sequence. + /// + /// The following example declares an array of names, and then creates a + /// dictionary from that array by grouping the names by first letter: + /// + /// let students = ["Kofi", "Abena", "Efua", "Kweku", "Akosua"] + /// let studentsByLetter = TreeDictionary(grouping: students, by: { $0.first! }) + /// // ["K": ["Kofi", "Kweku"], "A": ["Abena", "Akosua"], "E": ["Efua"]] + /// + /// The new `studentsByLetter` dictionary has three entries, with students' + /// names grouped by the keys `"E"`, `"K"`, and `"A"`. + /// + /// - Parameters: + /// - values: A sequence of values to group into a dictionary. + /// - keyForValue: A closure that returns a key for each element in + /// `values`. + /// + /// - Complexity: Expected O(*n*) on average, where *n* is the count of + /// values, if `Key` properly implements hashing. + @inlinable @inline(__always) + public init( + grouping values: S, + by keyForValue: (S.Element) throws -> Key + ) rethrows + where Value == [S.Element] + { + // Note: this extra overload is necessary to make type inference work + // for the `Value` type -- we want it to default to `[S.Element`]. + // (https://github.com/apple/swift-collections/issues/139) + try self.init(_grouping: values, by: keyForValue) + } + + @inlinable + internal init( + _grouping values: S, + by keyForValue: (S.Element) throws -> Key + ) rethrows + where Value: RangeReplaceableCollection, Value.Element == S.Element + { + self.init() + for value in values { + let key = try keyForValue(value) + self.updateValue(forKey: key, default: Value()) { array in + array.append(value) + } + } + _invariantCheck() + } +} diff --git a/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+Keys.swift b/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+Keys.swift new file mode 100644 index 000000000..7b6ac3ac0 --- /dev/null +++ b/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+Keys.swift @@ -0,0 +1,327 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension TreeDictionary { + /// A view of a persistent dictionary’s keys, as a standalone collection. + @frozen + public struct Keys { + @usableFromInline + internal typealias _Node = _HashNode + + @usableFromInline + internal var _base: TreeDictionary + + @inlinable + internal init(_base: TreeDictionary) { + self._base = _base + } + } + + /// A collection containing just the keys of the dictionary. + /// + /// - Complexity: O(1) + @inlinable + public var keys: Keys { + Keys(_base: self) + } +} + +extension TreeDictionary.Keys: Sendable +where Key: Sendable, Value: Sendable {} + +extension TreeDictionary.Keys: _UniqueCollection {} + +extension TreeDictionary.Keys: CustomStringConvertible { + // A textual representation of this instance. + public var description: String { + _arrayDescription(for: self) + } +} + +extension TreeDictionary.Keys: CustomDebugStringConvertible { + /// A textual representation of this instance, suitable for debugging. + public var debugDescription: String { + description + } +} + +extension TreeDictionary.Keys: Sequence { + /// The element type of the collection. + public typealias Element = Key + + /// The type that allows iteration over the elements of the keys view + /// of a persistent dictionary. + @frozen + public struct Iterator: IteratorProtocol { + public typealias Element = Key + + @usableFromInline + internal var _base: TreeDictionary.Iterator + + @inlinable + internal init(_base: TreeDictionary.Iterator) { + self._base = _base + } + + @inlinable + public mutating func next() -> Element? { + _base.next()?.key + } + } + + @inlinable + public func makeIterator() -> Iterator { + Iterator(_base: _base.makeIterator()) + } + + @inlinable + public func _customContainsEquatableElement( + _ element: Element + ) -> Bool? { + _base._root.containsKey(.top, element, _Hash(element)) + } + + /// Returns a Boolean value that indicates whether the given key exists + /// in the dictionary. + /// + /// - Parameter element: A key to look for in the dictionary/ + /// + /// - Returns: `true` if `element` exists in the set; otherwise, `false`. + /// + /// - Complexity: This operation is expected to perform O(1) hashing and + /// comparison operations on average, provided that `Element` implements + /// high-quality hashing. + @inlinable + public func contains(_ element: Element) -> Bool { + _base._root.containsKey(.top, element, _Hash(element)) + } +} + +extension TreeDictionary.Keys.Iterator: Sendable +where Key: Sendable, Value: Sendable {} + +extension TreeDictionary.Keys: Collection { + public typealias Index = TreeDictionary.Index + + @inlinable + public var isEmpty: Bool { _base.isEmpty } + + @inlinable + public var count: Int { _base.count } + + @inlinable + public var startIndex: Index { _base.startIndex } + + @inlinable + public var endIndex: Index { _base.endIndex } + + @inlinable + public subscript(index: Index) -> Element { + _base[index].key + } + + @inlinable + public func formIndex(after i: inout Index) { + _base.formIndex(after: &i) + } + + @inlinable + public func index(after i: Index) -> Index { + _base.index(after: i) + } + + @inlinable + public func index(_ i: Index, offsetBy distance: Int) -> Index { + _base.index(i, offsetBy: distance) + } + + @inlinable + public func index( + _ i: Index, offsetBy distance: Int, limitedBy limit: Index + ) -> Index? { + _base.index(i, offsetBy: distance, limitedBy: limit) + } + + @inlinable + public func distance(from start: Index, to end: Index) -> Int { + _base.distance(from: start, to: end) + } +} + +#if false +extension TreeDictionary.Keys: BidirectionalCollection { + // Note: Let's not do this. `BidirectionalCollection` would imply that + // the ordering of elements would be meaningful, which isn't true for + // `TreeDictionary.Keys`. + @inlinable + public func formIndex(before i: inout Index) { + _base.formIndex(before: &i) + } + + @inlinable + public func index(before i: Index) -> Index { + _base.index(before: i) + } +} +#endif + +extension TreeDictionary.Keys { + /// Returns a new keys view with the elements that are common to both this + /// view and the provided other one. + /// + /// var a: TreeDictionary = ["a": 1, "b": 2, "c": 3] + /// let b: TreeDictionary = ["b": 4, "d": 5, "e": 6] + /// let c = a.keys.intersection(b.keys) + /// // `c` is `["b"]` + /// + /// The result will only contain instances that were originally in `self`. + /// (This matters if equal members can be distinguished by comparing their + /// identities, or by some other means.) + /// + /// - Parameter other: The keys view of a persistent dictionary with the same + /// `Key` type. + /// + /// - Complexity: Expected complexity is O(`self.count`) in + /// the worst case, if `Element` properly implements hashing. + /// However, the implementation is careful to make the best use of + /// hash tree structure to minimize work when possible, e.g. by linking + /// parts of the input trees directly into the result. + public func intersection( + _ other: TreeDictionary.Keys + ) -> Self { + guard let r = _base._root.intersection(.top, other._base._root) else { + return self + } + let d = TreeDictionary(_new: r) + d._invariantCheck() + return d.keys + } + + /// Returns a new keys view with the elements that are common to both this + /// view and the provided persistent set. + /// + /// var a: TreeDictionary = ["a": 1, "b": 2, "c": 3] + /// let b: TreeSet = ["b", "d", "e"] + /// let c = a.keys.intersection(b) + /// // `c` is `["b"]` + /// + /// The result will only contain instances that were originally in `self`. + /// (This matters if equal members can be distinguished by comparing their + /// identities, or by some other means.) + /// + /// - Parameter other: A persistent set whose `Element` type is `Key`. + /// + /// - Complexity: Expected complexity is O(`self.count`) in + /// the worst case, if `Element` properly implements hashing. + /// However, the implementation is careful to make the best use of + /// hash tree structure to minimize work when possible, e.g. by linking + /// parts of the input trees directly into the result. + public func intersection(_ other: TreeSet) -> Self { + guard let r = _base._root.intersection(.top, other._root) else { + return self + } + let d = TreeDictionary(_new: r) + d._invariantCheck() + return d.keys + } + + /// Returns a new keys view containing the elements of `self` that do not + /// occur in the provided other one. + /// + /// var a: TreeDictionary = ["a": 1, "b": 2, "c": 3] + /// let b: TreeDictionary = ["b": 4, "d": 5, "e": 6] + /// let c = a.keys.subtracting(b.keys) + /// // `c` is some permutation of `["a", "c"]` + /// + /// - Parameter other: The keys view of a persistent dictionary with the same + /// `Key` type. + /// + /// - Complexity: Expected complexity is O(`self.count`) in + /// the worst case, if `Element` properly implements hashing. + /// However, the implementation is careful to make the best use of + /// hash tree structure to minimize work when possible, e.g. by linking + /// parts of the input trees directly into the result. + public func subtracting( + _ other: TreeDictionary.Keys + ) -> Self { + guard let r = _base._root.subtracting(.top, other._base._root) else { + return self + } + let d = TreeDictionary(_new: r) + d._invariantCheck() + return d.keys + } + + /// Returns a new keys view containing the elements of `self` that do not + /// occur in the provided persistent set. + /// + /// var a: TreeDictionary = ["a": 1, "b": 2, "c": 3] + /// let b: TreeSet = ["b", "d", "e"] + /// let c = a.keys.subtracting(b) + /// // `c` is some permutation of `["a", "c"]` + /// + /// - Parameter other: A persistent set whose `Element` type is `Key`. + /// + /// - Complexity: Expected complexity is O(`self.count`) in + /// the worst case, if `Element` properly implements hashing. + /// However, the implementation is careful to make the best use of + /// hash tree structure to minimize work when possible, e.g. by linking + /// parts of the input trees directly into the result. + public func subtracting(_ other: TreeSet) -> Self { + guard let r = _base._root.subtracting(.top, other._root) else { + return self + } + let d = TreeDictionary(_new: r) + d._invariantCheck() + return d.keys + } +} + +extension TreeDictionary.Keys: Equatable { + /// Returns a Boolean value indicating whether two values are equal. + /// + /// Equality is the inverse of inequality. For any values `a` and `b`, + /// `a == b` implies that `a != b` is `false`. + /// + /// - Parameter lhs: A value to compare. + /// - Parameter rhs: Another value to compare. + /// + /// - Complexity: Generally O(`count`), as long as`Element` properly + /// implements hashing. That said, the implementation is careful to take + /// every available shortcut to reduce complexity, e.g. by skipping + /// comparing elements in shared subtrees. + @inlinable + public static func == (left: Self, right: Self) -> Bool { + left._base._root.isEqualSet(to: right._base._root, by: { _, _ in true }) + } +} + +extension TreeDictionary.Keys: Hashable { + /// Hashes the essential components of this value by feeding them into the + /// given hasher. + /// + /// Complexity: O(`count`) + @inlinable + public func hash(into hasher: inout Hasher) { + let copy = hasher + let seed = copy.finalize() + + var hash = 0 + for member in self { + hash ^= member._rawHashValue(seed: seed) + } + hasher.combine(hash) + } +} diff --git a/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+MapValues.swift b/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+MapValues.swift new file mode 100644 index 000000000..65fd750c7 --- /dev/null +++ b/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+MapValues.swift @@ -0,0 +1,68 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 TreeDictionary { + /// Returns a new dictionary containing the keys of this dictionary with the + /// values transformed by the given closure. + /// + /// - Parameter transform: A closure that transforms a value. `transform` + /// accepts each value of the dictionary as its parameter and returns a + /// transformed value of the same or of a different type. + /// - Returns: A dictionary containing the keys and transformed values of + /// this dictionary. + /// + /// - Complexity: O(`count`) + @inlinable + public func mapValues( + _ transform: (Value) throws -> T + ) rethrows -> TreeDictionary { + let transformed = try _root.mapValues { try transform($0.value) } + let r = TreeDictionary(_new: transformed) + r._invariantCheck() + return r + } + + /// Returns a new dictionary containing only the key-value pairs that have + /// non-`nil` values as the result of transformation by the given closure. + /// + /// Use this method to receive a dictionary with non-optional values when + /// your transformation produces optional values. + /// + /// In this example, note the difference in the result of using `mapValues` + /// and `compactMapValues` with a transformation that returns an optional + /// `Int` value. + /// + /// let data: TreeDictionary = ["a": "1", "b": "three", "c": "///4///"] + /// + /// let m: [String: Int?] = data.mapValues { str in Int(str) } + /// // ["a": Optional(1), "b": nil, "c": nil] + /// + /// let c: [String: Int] = data.compactMapValues { str in Int(str) } + /// // ["a": 1] + /// + /// - Parameter transform: A closure that transforms a value. `transform` + /// accepts each value of the dictionary as its parameter and returns an + /// optional transformed value of the same or of a different type. + /// + /// - Returns: A dictionary containing the keys and non-`nil` transformed + /// values of this dictionary. + /// + /// - Complexity: O(`count`) + @inlinable + public func compactMapValues( + _ transform: (Value) throws -> T? + ) rethrows -> TreeDictionary { + let result = try _root.compactMapValues(.top, transform) + let d = TreeDictionary(_new: result.finalize(.top)) + d._invariantCheck() + return d + } +} diff --git a/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+Merge.swift b/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+Merge.swift new file mode 100644 index 000000000..0d73dd67d --- /dev/null +++ b/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+Merge.swift @@ -0,0 +1,267 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 TreeDictionary { + /// Merges the key-value pairs in the given sequence into the dictionary, + /// using a combining closure to determine the value for any duplicate keys. + /// + /// Use the `combine` closure to select a value to use in the updated + /// dictionary, or to combine existing and new values. As the key-value + /// pairs are merged with the dictionary, the `combine` closure is called + /// with the current and new values for any duplicate keys that are + /// encountered. + /// + /// This example shows how to choose the current or new values for any + /// duplicate keys: + /// + /// var dictionary: TreeDictionary = ["a": 1, "b": 2] + /// + /// // Keeping existing value for key "a": + /// dictionary.merge(zip(["a", "c"], [3, 4])) { (current, _) in current } + /// // ["a": 1, "b": 2, "c": 4] (in some order) + /// + /// // Taking the new value for key "a": + /// dictionary.merge(zip(["a", "d"], [5, 6])) { (_, new) in new } + /// // ["a": 5, "b": 2, "c": 4, "d": 6] (in some order) + /// + /// - Parameters: + /// - keysAndValues: A sequence of key-value pairs. + /// - combine: A closure that takes the current and new values for any + /// duplicate keys. The closure returns the desired value for the final + /// dictionary. + @inlinable + public mutating func merge( + _ keysAndValues: Self, + uniquingKeysWith combine: (Value, Value) throws -> Value + ) rethrows { + _invalidateIndices() + _ = try _root.merge(.top, keysAndValues._root, combine) + _invariantCheck() + } + + /// Merges the key-value pairs in the given sequence into the dictionary, + /// using a combining closure to determine the value for any duplicate keys. + /// + /// Use the `combine` closure to select a value to use in the updated + /// dictionary, or to combine existing and new values. As the key-value + /// pairs are merged with the dictionary, the `combine` closure is called + /// with the current and new values for any duplicate keys that are + /// encountered. + /// + /// This example shows how to choose the current or new values for any + /// duplicate keys: + /// + /// var dictionary: TreeDictionary = ["a": 1, "b": 2] + /// + /// // Keeping existing value for key "a": + /// dictionary.merge(zip(["a", "c"], [3, 4])) { (current, _) in current } + /// // ["a": 1, "b": 2, "c": 4] (in some order) + /// + /// // Taking the new value for key "a": + /// dictionary.merge(zip(["a", "d"], [5, 6])) { (_, new) in new } + /// // ["a": 5, "b": 2, "c": 4, "d": 6] (in some order) + /// + /// - Parameters: + /// - keysAndValues: A sequence of key-value pairs. + /// - combine: A closure that takes the current and new values for any + /// duplicate keys. The closure returns the desired value for the final + /// dictionary. + @inlinable + public mutating func merge( + _ keysAndValues: __owned some Sequence<(Key, Value)>, + uniquingKeysWith combine: (Value, Value) throws -> Value + ) rethrows { + for (key, value) in keysAndValues { + try self.updateValue(forKey: key) { target in + if let old = target { + target = try combine(old, value) + } else { + target = value + } + } + } + _invariantCheck() + } + + /// Merges the key-value pairs in the given sequence into the dictionary, + /// using a combining closure to determine the value for any duplicate keys. + /// + /// Use the `combine` closure to select a value to use in the updated + /// dictionary, or to combine existing and new values. As the key-value + /// pairs are merged with the dictionary, the `combine` closure is called + /// with the current and new values for any duplicate keys that are + /// encountered. + /// + /// This example shows how to choose the current or new values for any + /// duplicate keys: + /// + /// var dictionary: TreeDictionary = ["a": 1, "b": 2] + /// + /// // Keeping existing value for key "a": + /// dictionary.merge(zip(["a", "c"], [3, 4])) { (current, _) in current } + /// // ["a": 1, "b": 2, "c": 4] (in some order) + /// + /// // Taking the new value for key "a": + /// dictionary.merge(zip(["a", "d"], [5, 6])) { (_, new) in new } + /// // ["a": 5, "b": 2, "c": 4, "d": 6] (in some order) + /// + /// - Parameters: + /// - keysAndValues: A sequence of key-value pairs. + /// - combine: A closure that takes the current and new values for any + /// duplicate keys. The closure returns the desired value for the final + /// dictionary. + @_disfavoredOverload // https://github.com/apple/swift-collections/issues/125 + @inlinable + public mutating func merge( + _ keysAndValues: __owned some Sequence, + uniquingKeysWith combine: (Value, Value) throws -> Value + ) rethrows { + try merge( + keysAndValues.lazy.map { ($0.key, $0.value) }, + uniquingKeysWith: combine) + } + + /// Creates a dictionary by merging key-value pairs in a sequence into this + /// dictionary, using a combining closure to determine the value for + /// duplicate keys. + /// + /// Use the `combine` closure to select a value to use in the returned + /// dictionary, or to combine existing and new values. As the key-value + /// pairs are merged with the dictionary, the `combine` closure is called + /// with the current and new values for any duplicate keys that are + /// encountered. + /// + /// This example shows how to choose the current or new values for any + /// duplicate keys: + /// + /// let dictionary: OrderedDictionary = ["a": 1, "b": 2] + /// let newKeyValues = zip(["a", "b"], [3, 4]) + /// + /// let keepingCurrent = dictionary.merging(newKeyValues) { (current, _) in current } + /// // ["a": 1, "b": 2] + /// let replacingCurrent = dictionary.merging(newKeyValues) { (_, new) in new } + /// // ["a": 3, "b": 4] + /// + /// - Parameters: + /// - other: A sequence of key-value pairs. + /// - combine: A closure that takes the current and new values for any + /// duplicate keys. The closure returns the desired value for the final + /// dictionary. + /// + /// - Returns: A new dictionary with the combined keys and values of this + /// dictionary and `other`. The order of keys in the result dictionary + /// matches that of `self`, with additional key-value pairs (if any) + /// appended at the end in the order they appear in `other`. + /// + /// - Complexity: Expected to be O(`count` + *n*) on average, where *n* is the + /// number of elements in `keysAndValues`, if `Key` implements high-quality + /// hashing. + @inlinable + public func merging( + _ other: Self, + uniquingKeysWith combine: (Value, Value) throws -> Value + ) rethrows -> Self { + var copy = self + try copy.merge(other, uniquingKeysWith: combine) + return copy + } + + /// Creates a dictionary by merging key-value pairs in a sequence into this + /// dictionary, using a combining closure to determine the value for + /// duplicate keys. + /// + /// Use the `combine` closure to select a value to use in the returned + /// dictionary, or to combine existing and new values. As the key-value + /// pairs are merged with the dictionary, the `combine` closure is called + /// with the current and new values for any duplicate keys that are + /// encountered. + /// + /// This example shows how to choose the current or new values for any + /// duplicate keys: + /// + /// let dictionary: OrderedDictionary = ["a": 1, "b": 2] + /// let newKeyValues = zip(["a", "b"], [3, 4]) + /// + /// let keepingCurrent = dictionary.merging(newKeyValues) { (current, _) in current } + /// // ["a": 1, "b": 2] + /// let replacingCurrent = dictionary.merging(newKeyValues) { (_, new) in new } + /// // ["a": 3, "b": 4] + /// + /// - Parameters: + /// - other: A sequence of key-value pairs. + /// - combine: A closure that takes the current and new values for any + /// duplicate keys. The closure returns the desired value for the final + /// dictionary. + /// + /// - Returns: A new dictionary with the combined keys and values of this + /// dictionary and `other`. The order of keys in the result dictionary + /// matches that of `self`, with additional key-value pairs (if any) + /// appended at the end in the order they appear in `other`. + /// + /// - Complexity: Expected to be O(`count` + *n*) on average, where *n* is the + /// number of elements in `keysAndValues`, if `Key` implements high-quality + /// hashing. + @inlinable + public func merging( + _ other: __owned some Sequence<(Key, Value)>, + uniquingKeysWith combine: (Value, Value) throws -> Value + ) rethrows -> Self { + var copy = self + try copy.merge(other, uniquingKeysWith: combine) + return copy + } + + /// Creates a dictionary by merging key-value pairs in a sequence into this + /// dictionary, using a combining closure to determine the value for + /// duplicate keys. + /// + /// Use the `combine` closure to select a value to use in the returned + /// dictionary, or to combine existing and new values. As the key-value + /// pairs are merged with the dictionary, the `combine` closure is called + /// with the current and new values for any duplicate keys that are + /// encountered. + /// + /// This example shows how to choose the current or new values for any + /// duplicate keys: + /// + /// let dictionary: OrderedDictionary = ["a": 1, "b": 2] + /// let newKeyValues = zip(["a", "b"], [3, 4]) + /// + /// let keepingCurrent = dictionary.merging(newKeyValues) { (current, _) in current } + /// // ["a": 1, "b": 2] + /// let replacingCurrent = dictionary.merging(newKeyValues) { (_, new) in new } + /// // ["a": 3, "b": 4] + /// + /// - Parameters: + /// - other: A sequence of key-value pairs. + /// - combine: A closure that takes the current and new values for any + /// duplicate keys. The closure returns the desired value for the final + /// dictionary. + /// + /// - Returns: A new dictionary with the combined keys and values of this + /// dictionary and `other`. The order of keys in the result dictionary + /// matches that of `self`, with additional key-value pairs (if any) + /// appended at the end in the order they appear in `other`. + /// + /// - Complexity: Expected to be O(`count` + *n*) on average, where *n* is the + /// number of elements in `keysAndValues`, if `Key` implements high-quality + /// hashing. + @_disfavoredOverload // https://github.com/apple/swift-collections/issues/125 + @inlinable + public func merging( + _ other: __owned some Sequence, + uniquingKeysWith combine: (Value, Value) throws -> Value + ) rethrows -> Self { + var copy = self + try copy.merge(other, uniquingKeysWith: combine) + return copy + } +} diff --git a/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+Sendable.swift b/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+Sendable.swift new file mode 100644 index 000000000..148e281ca --- /dev/null +++ b/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+Sendable.swift @@ -0,0 +1,13 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 TreeDictionary: @unchecked Sendable +where Key: Sendable, Value: Sendable {} diff --git a/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+Sequence.swift b/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+Sequence.swift new file mode 100644 index 000000000..f9a40d2d8 --- /dev/null +++ b/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+Sequence.swift @@ -0,0 +1,73 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2019 - 2024 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 TreeDictionary: Sequence { + /// The element type of a dictionary: a tuple containing an individual + /// key-value pair. + public typealias Element = (key: Key, value: Value) + + /// The type that allows iteration over a persistent dictionary's elements. + @frozen + public struct Iterator { + // Fixed-stack iterator for traversing a hash tree. + // The iterator performs a pre-order traversal, with items at a node visited + // before any items within children. + + @usableFromInline + internal typealias _UnsafeHandle = _Node.UnsafeHandle + + @usableFromInline + internal var _it: _HashTreeIterator + + @inlinable + internal init(_root: _RawHashNode) { + self._it = _HashTreeIterator(root: _root) + } + } + + /// A value less than or equal to the number of elements in the sequence, + /// calculated nondestructively. + /// + /// - Complexity: O(1) + @inlinable + public var underestimatedCount: Int { + _root.count + } + + /// Returns an iterator over the elements of this collection. + /// + /// - Complexity: O(1) + @inlinable + public __consuming func makeIterator() -> Iterator { + return Iterator(_root: _root.raw) + } +} + +extension TreeDictionary.Iterator: @unchecked Sendable +where Key: Sendable, Value: Sendable {} + +extension TreeDictionary.Iterator: IteratorProtocol { + /// The element type of a dictionary: a tuple containing an individual + /// key-value pair. + public typealias Element = (key: Key, value: Value) + + /// Advances to the next element and returns it, or `nil` if no next + /// element exists. + /// + /// Once `nil` has been returned, all subsequent calls return `nil`. + /// + /// - Complexity: O(1) + @inlinable + public mutating func next() -> Element? { + guard let (node, slot) = _it.next() else { return nil } + return _UnsafeHandle.read(node) { $0[item: slot] } + } +} diff --git a/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+Values.swift b/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+Values.swift new file mode 100644 index 000000000..b70110e21 --- /dev/null +++ b/Sources/HashTreeCollections/TreeDictionary/TreeDictionary+Values.swift @@ -0,0 +1,164 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension TreeDictionary { + /// A view of a dictionary’s values. + @frozen + public struct Values { + @usableFromInline + internal typealias _Node = TreeDictionary._Node + + @usableFromInline + internal typealias _UnsafeHandle = _Node.UnsafeHandle + + @usableFromInline + internal var _base: TreeDictionary + + @inlinable + internal init(_base: TreeDictionary) { + self._base = _base + } + } + + /// A collection containing just the values of the dictionary. + @inlinable + public var values: Values { + // Note: this property is kept read only for now until we decide whether + // it's worth providing setters without a `MutableCollection` conformance. + get { + Values(_base: self) + } + } +} + +extension TreeDictionary.Values: Sendable +where Key: Sendable, Value: Sendable {} + +extension TreeDictionary.Values: CustomStringConvertible { + // A textual representation of this instance. + public var description: String { + _arrayDescription(for: self) + } +} + +extension TreeDictionary.Values: CustomDebugStringConvertible { + /// A textual representation of this instance, suitable for debugging. + public var debugDescription: String { + description + } +} + +extension TreeDictionary.Values: Sequence { + public typealias Element = Value + + @frozen + public struct Iterator: IteratorProtocol { + public typealias Element = Value + + @usableFromInline + internal var _base: TreeDictionary.Iterator + + @inlinable + internal init(_base: TreeDictionary.Iterator) { + self._base = _base + } + + @inlinable + public mutating func next() -> Element? { + _base.next()?.value + } + } + + @inlinable + public func makeIterator() -> Iterator { + Iterator(_base: _base.makeIterator()) + } +} + +extension TreeDictionary.Values.Iterator: Sendable +where Key: Sendable, Value: Sendable {} + +// Note: This cannot be a MutableCollection because its subscript setter +// needs to invalidate indices. +extension TreeDictionary.Values: Collection { + public typealias Index = TreeDictionary.Index + + @inlinable + public var isEmpty: Bool { _base.isEmpty } + + @inlinable + public var count: Int { _base.count } + + @inlinable + public var startIndex: Index { _base.startIndex } + + @inlinable + public var endIndex: Index { _base.endIndex } + + @inlinable + public subscript(index: Index) -> Element { + // The subscript is kept read only for now until we decide whether it's + // worth providing setters without a `MutableCollection` conformance. + // (With the current index implementation, mutating values must invalidate + // indices.) + get { + _base[index].value + } + } + + @inlinable + public func formIndex(after i: inout Index) { + _base.formIndex(after: &i) + } + + @inlinable + public func index(after i: Index) -> Index { + _base.index(after: i) + } + + @inlinable + public func index(_ i: Index, offsetBy distance: Int) -> Index { + _base.index(i, offsetBy: distance) + } + + @inlinable + public func index( + _ i: Index, offsetBy distance: Int, limitedBy limit: Index + ) -> Index? { + _base.index(i, offsetBy: distance, limitedBy: limit) + } + + @inlinable + public func distance(from start: Index, to end: Index) -> Int { + _base.distance(from: start, to: end) + } +} + +#if false +extension TreeDictionary.Values: BidirectionalCollection { + // Note: Let's not do this. `BidirectionalCollection` would imply that + // the ordering of elements would be meaningful, which isn't true for + // `TreeDictionary.Values`. + @inlinable + public func formIndex(before i: inout Index) { + _base.formIndex(before: &i) + } + + @inlinable + public func index(before i: Index) -> Index { + _base.index(before: i) + } +} +#endif diff --git a/Sources/HashTreeCollections/TreeDictionary/TreeDictionary.swift b/Sources/HashTreeCollections/TreeDictionary/TreeDictionary.swift new file mode 100644 index 000000000..5152e1bf5 --- /dev/null +++ b/Sources/HashTreeCollections/TreeDictionary/TreeDictionary.swift @@ -0,0 +1,552 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2019 - 2024 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 +// +//===----------------------------------------------------------------------===// + +/// An unordered collection of unique keys and associated values, optimized for +/// mutating shared copies and comparing different snapshots of the same +/// collection. +/// +/// `TreeDictionary` has the same functionality as a standard +/// `Dictionary`, and it largely implements the same APIs: both are hashed +/// collection types with convenient and efficient ways to look up the value +/// associated with a particular key, and both types are unordered, meaning that +/// neither type provides any guarantees about the ordering of their items. +/// +/// However, `TreeDictionary` is optimizing specifically for use cases +/// that need to mutate shared copies or to compare a dictionary value to one of +/// its older snapshots. To use a term from functional programming, +/// `TreeDictionary` implements a _persistent data structure_. +/// +/// The standard `Dictionary` stores its members in a single, flat hash table, +/// and it implements value semantics with all-or-nothing copy-on-write +/// behavior: every time a shared copy of a dictionary is mutated, the mutation +/// needs to make a full copy of the dictionary's storage. +/// `TreeDictionary` takes a different approach: it organizes its members +/// into a tree structure, the nodes of which can be freely shared across +/// collection values. When mutating a shared copy of a dictionary value, +/// `TreeDictionary` is able to simply link the unchanged parts of the +/// tree directly into the result, saving both time and memory. +/// +/// This structural sharing also makes it more efficient to compare mutated +/// dictionaries values to earlier versions of themselves. When comparing or +/// combining dictionaries, parts that are shared across both inputs can +/// typically be handled in constant time, leading to a dramatic performance +/// boost when the two inputs are still largely unchanged: +/// +/// var d = TreeDictionary( +/// uniqueKeysWithValues: (0 ..< 10_000).map { ($0, 2 * $0) }) +/// let copy = d +/// d[20_000] = 42 // Expected to be an O(log(n)) operation +/// let diff = d.keys.subtracting(copy.keys) // Also O(log(n))! +/// // `diff` now holds the single item 20_000. +/// +/// The tree structure also eliminates the need to reserve capacity in advance: +/// `TreeDictionary` creates, destroys and resizes individual nodes as +/// needed, always consuming just enough memory to store its contents. As of +/// Swift 5.9, the standard collection types never shrink their storage, so +/// temporary storage spikes can linger as unused but still allocated memory +/// long after the collection has shrunk back to its usual size. +/// +/// Of course, switching to a tree structure comes with some trade offs. In +/// particular, inserting new items, removing existing ones, and iterating over +/// a `TreeDictionary` is expected to be a constant factor slower than a +/// standard `Dictionary` -- allocating/deallocating nodes isn't free, and +/// navigating the tree structure requires more pointer dereferences than +/// accessing a flat hash table. However the algorithmic improvements above +/// usually more than make up for this, as long as the use case can make use of +/// them. +@frozen // Not really -- this package is not at all ABI stable +public struct TreeDictionary { + @usableFromInline + internal typealias _Node = _HashNode + + @usableFromInline + internal typealias _UnsafeHandle = _Node.UnsafeHandle + + @usableFromInline + var _root: _Node + + /// The version number of this instance, used for quick index validation. + /// This is initialized to a (very weakly) random value and it gets + /// incremented on every mutation that needs to invalidate indices. + @usableFromInline + var _version: UInt + + @inlinable + internal init(_root: _Node, version: UInt) { + self._root = _root + self._version = version + } + + @inlinable + internal init(_new: _Node) { + self.init(_root: _new, version: _new.initialVersionNumber) + } +} + +extension TreeDictionary { + /// Accesses the value associated with the given key for reading and writing. + /// + /// This *key-based* subscript returns the value for the given key if the key + /// is found in the dictionary, or `nil` if the key is not found. + /// + /// The following example creates a new dictionary and prints the value of a + /// key found in the dictionary (`"Coral"`) and a key not found in the + /// dictionary (`"Cerise"`). + /// + /// var hues: TreeDictionary = ["Heliotrope": 296, "Coral": 16, "Aquamarine": 156] + /// print(hues["Coral"]) + /// // Prints "Optional(16)" + /// print(hues["Cerise"]) + /// // Prints "nil" + /// + /// When you assign a value for a key and that key already exists, the + /// dictionary overwrites the existing value. If the dictionary doesn't + /// contain the key, the key and value are added as a new key-value pair. + /// + /// Here, the value for the key `"Coral"` is updated from `16` to `18` and a + /// new key-value pair is added for the key `"Cerise"`. + /// + /// hues["Coral"] = 18 + /// print(hues["Coral"]) + /// // Prints "Optional(18)" + /// + /// hues["Cerise"] = 330 + /// print(hues["Cerise"]) + /// // Prints "Optional(330)" + /// + /// If you assign `nil` as the value for the given key, the dictionary + /// removes that key and its associated value. + /// + /// In the following example, the key-value pair for the key `"Aquamarine"` + /// is removed from the dictionary by assigning `nil` to the key-based + /// subscript. + /// + /// hues["Aquamarine"] = nil + /// print(hues) + /// // Prints "["Coral": 18, "Heliotrope": 296, "Cerise": 330]" + /// + /// Updating the value of an existing key only modifies the value: it does not + /// change the key that is stored in the dictionary. (In some cases, equal + /// keys may be distinguishable from each other by identity comparison or + /// some other means.) + /// + /// Removing or updating an existing key-value pair or inserting a new + /// key-value pair invalidates all indices in the dictionary. Removing a + /// key that doesn't exist does not invalidate any indices. + /// + /// - Parameter key: The key to find in the dictionary. + /// + /// - Returns: The value associated with `key` if `key` is in the dictionary; + /// otherwise, `nil`. + /// + /// - Complexity: Looking up the value for a key is expected to traverse + /// O(log(`count`)) tree nodes and to do at most O(1) hashing/comparison + /// operations on the `Element` type, as long as `Element` properly + /// implements hashing. + /// + /// Updating the dictionary through this subscript is expected to copy at + /// most O(log(`count`)) existing members. + @inlinable + public subscript(key: Key) -> Value? { + get { + _root.get(.top, key, _Hash(key)) + } + set { + if let value = newValue { + _updateValue(value, forKey: key) + _invalidateIndices() + } else { + removeValue(forKey: key) + } + } + @inline(__always) // https://github.com/apple/swift-collections/issues/164 + _modify { + _invalidateIndices() + var state = _root.prepareValueUpdate(key, _Hash(key)) + defer { + _root.finalizeValueUpdate(state) + } + yield &state.value + } + } + + /// Accesses the value with the given key. If the dictionary doesn't contain + /// the given key, accesses the provided default value as if the key and + /// default value existed in the dictionary. + /// + /// Use this subscript when you want either the value for a particular key + /// or, when that key is not present in the dictionary, a default value. This + /// example uses the subscript with a message to use in case an HTTP response + /// code isn't recognized: + /// + /// var responseMessages: TreeDictionary = [ + /// 200: "OK", + /// 403: "Access forbidden", + /// 404: "File not found", + /// 500: "Internal server error"] + /// + /// let httpResponseCodes = [200, 403, 301] + /// for code in httpResponseCodes { + /// let message = responseMessages[code, default: "Unknown response"] + /// print("Response \(code): \(message)") + /// } + /// // Prints "Response 200: OK" + /// // Prints "Response 403: Access forbidden" + /// // Prints "Response 301: Unknown response" + /// + /// When a dictionary's `Value` type has value semantics, you can use this + /// subscript to perform in-place operations on values in the dictionary. + /// The following example uses this subscript while counting the occurrences + /// of each letter in a string: + /// + /// let message = "Hello, Elle!" + /// var letterCounts: TreeDictionary = [:] + /// for letter in message { + /// letterCounts[letter, default: 0] += 1 + /// } + /// // letterCounts == ["H": 1, "e": 2, "l": 4, "o": 1, ...] + /// + /// When `letterCounts[letter, defaultValue: 0] += 1` is executed with a + /// value of `letter` that isn't already a key in `letterCounts`, the + /// specified default value (`0`) is returned from the subscript, + /// incremented, and then added to the dictionary under that key. + /// + /// Updating the value of an existing key only modifies the value: it does not + /// change the key that is stored in the dictionary. (In some cases, equal + /// keys may be distinguishable from each other by identity comparison or + /// some other means.) + /// + /// Calling this method invalidates all existing indices in the dictionary. + /// + /// - Note: Do not use this subscript to modify dictionary values if the + /// dictionary's `Value` type is a class. In that case, the default value + /// and key are not written back to the dictionary after an operation. (For + /// a variant of this operation that supports this usecase, see + /// `updateValue(forKey:default:_:)`.) + /// + /// - Parameters: + /// - key: The key the look up in the dictionary. + /// - defaultValue: The default value to use if `key` doesn't exist in the + /// dictionary. + /// + /// - Returns: The value associated with `key` in the dictionary; otherwise, + /// `defaultValue`. + /// + /// - Complexity: Looking up the value for a key is expected to do at most + /// O(1) hashing/comparison operations on the `Element` type, as long as + /// `Element` properly implements hashing. + /// + /// Updating the dictionary through this subscript is expected to copy at + /// most O(log(`count`)) existing members. + @inlinable + public subscript( + key: Key, + default defaultValue: @autoclosure () -> Value + ) -> Value { + get { + _root.get(.top, key, _Hash(key)) ?? defaultValue() + } + set { + _updateValue(newValue, forKey: key) + _invalidateIndices() + } + @inline(__always) // https://github.com/apple/swift-collections/issues/164 + _modify { + _invalidateIndices() + var state = _root.prepareDefaultedValueUpdate( + .top, key, defaultValue, _Hash(key)) + defer { + _root.finalizeDefaultedValueUpdate(state) + } + yield &state.item.value + } + } + + /// Returns the index for the given key. + /// + /// If the given key is found in the dictionary, this method returns an index + /// into the dictionary that corresponds with the key-value pair. If the + /// key is not found, then this method returns `nil`. + /// + /// - Parameter key: The key to find in the dictionary. + /// + /// - Returns: The index for `key` and its associated value if `key` is in + /// the dictionary; otherwise, `nil`. + /// + /// - Complexity: This operation is expected to perform O(1) hashing and + /// comparison operations on average, provided that `Element` implements + /// high-quality hashing. + @inlinable + public func index(forKey key: Key) -> Index? { + guard let path = _root.path(to: key, _Hash(key)) + else { return nil } + return Index(_root: _root.unmanaged, version: _version, path: path) + } + + /// Updates the value stored in the dictionary for the given key, or appends a + /// new key-value pair if the key does not exist. + /// + /// Use this method instead of key-based subscripting when you need to know + /// whether the new value supplants the value of an existing key. If the + /// value of an existing key is updated, `updateValue(_:forKey:)` returns + /// the original value. + /// + /// var hues: TreeDictionary = [ + /// "Heliotrope": 296, + /// "Coral": 16, + /// "Aquamarine": 156] + /// + /// if let oldValue = hues.updateValue(18, forKey: "Coral") { + /// print("The old value of \(oldValue) was replaced with a new one.") + /// } + /// // Prints "The old value of 16 was replaced with a new one." + /// + /// If the given key is not present in the dictionary, this method appends the + /// key-value pair and returns `nil`. + /// + /// if let oldValue = hues.updateValue(330, forKey: "Cerise") { + /// print("The old value of \(oldValue) was replaced with a new one.") + /// } else { + /// print("No value was found in the dictionary for that key.") + /// } + /// // Prints "No value was found in the dictionary for that key." + /// + /// Updating the value of an existing key only modifies the value: it does not + /// change the key that is stored in the dictionary. (In some cases, equal + /// keys may be distinguishable from each other by identity comparison or + /// some other means.) + /// + /// Calling this method invalidates all existing indices in the dictionary. + /// + /// - Parameters: + /// - value: The new value to add to the dictionary. + /// - key: The key to associate with `value`. If `key` already exists in + /// the dictionary, `value` replaces the existing associated value. If + /// `key` isn't already a key of the dictionary, the `(key, value)` pair + /// is added. + /// + /// - Returns: The value that was replaced, or `nil` if a new key-value pair + /// was added. + /// + /// - Complexity: This operation is expected to copy at most O(log(`count`)) + /// existing members and to perform at most O(1) hashing/comparison + /// operations on the `Element` type, as long as `Element` properly + /// implements hashing. + @inlinable + @discardableResult + public mutating func updateValue( + _ value: __owned Value, forKey key: Key + ) -> Value? { + defer { _fixLifetime(self) } + let hash = _Hash(key) + let r = _root.updateValue(.top, forKey: key, hash) { + $0.initialize(to: (key, value)) + } + _invalidateIndices() + if r.inserted { return nil } + return _UnsafeHandle.update(r.leaf) { + let p = $0.itemPtr(at: r.slot) + let old = p.pointee.value + p.pointee.value = value + return old + } + } + + @inlinable + @discardableResult + internal mutating func _updateValue( + _ value: __owned Value, forKey key: Key + ) -> Bool { + defer { _fixLifetime(self) } + let hash = _Hash(key) + let r = _root.updateValue(.top, forKey: key, hash) { + $0.initialize(to: (key, value)) + } + if r.inserted { return true } + _UnsafeHandle.update(r.leaf) { + $0[item: r.slot].value = value + } + return false + } + + /// Calls `body` to directly update the current value of `key` in the + /// dictionary. + /// + /// You can use this method to perform in-place operations on values in the + /// dictionary, whether or not `Value` has value semantics. The following + /// example uses this method while counting the occurrences of each letter + /// in a string: + /// + /// let message = "Hello, Elle!" + /// var letterCounts: TreeDictionary = [:] + /// for letter in message { + /// letterCounts.updateValue(forKey: letter) { count in + /// if count == nil { + /// count = 1 + /// } else + /// count! += 1 + /// } + /// } + /// } + /// // letterCounts == ["H": 1, "e": 2, "l": 4, "o": 1, ...] + /// + /// Updating the value of an existing key only modifies the value: it does not + /// change the key that is stored in the dictionary. (In some cases, equal + /// keys may be distinguishable from each other by identity comparison or + /// some other means.) + /// + /// Removing or updating an existing key-value pair or inserting a new + /// key-value pair invalidates all indices in the dictionary. Removing a + /// key that doesn't exist does not invalidate any indices. + /// + /// - Parameters: + /// - key: The key whose value to look up. + /// - body: A function that performs an in-place mutation on the dictionary + /// value. If `key` exists in the dictionary, then `body` is called with + /// its current value; otherwise `body` is passed `nil`. + /// + /// - Returns: The return value of `body`. + /// + /// - Complexity: In addition to calling `body`, this operation is expected + /// to copy at most O(log(`count`)) existing members and to perform at + /// most O(1) hashing/comparison operations on the `Element` type, as long + /// as `Element` properly implements hashing. + @inlinable @inline(__always) + public mutating func updateValue( + forKey key: Key, + with body: (inout Value?) throws -> R + ) rethrows -> R { + try body(&self[key]) + } + + /// Ensures that the specified key exists in the dictionary (by inserting one + /// with the supplied default value if necessary), then calls `body` to update + /// it in place. + /// + /// You can use this method to perform in-place operations on values in the + /// dictionary, whether or not `Value` has value semantics. The following + /// example uses this method while counting the occurrences of each letter + /// in a string: + /// + /// let message = "Hello, Elle!" + /// var letterCounts: TreeDictionary = [:] + /// for letter in message { + /// letterCounts.updateValue(forKey: letter, default: 0) { count in + /// count += 1 + /// } + /// } + /// // letterCounts == ["H": 1, "e": 2, "l": 4, "o": 1, ...] + /// + /// Updating the value of an existing key only modifies the value: it does not + /// change the key that is stored in the dictionary. (In some cases, equal + /// keys may be distinguishable from each other by identity comparison or + /// some other means.) + /// + /// Calling this method invalidates all existing indices in the dictionary. + /// + /// - Parameters: + /// - key: The key to look up (or insert). If `key` does not already exist + /// in the dictionary, it is inserted with the supplied default value. + /// - defaultValue: The default value to insert if `key` doesn't exist in + /// the dictionary. + /// - body: A function that performs an in-place mutation on the dictionary + /// value. + /// + /// - Returns: The return value of `body`. + /// + /// - Complexity: In addition to calling `body`, this operation is expected + /// to copy at most O(log(`count`)) existing members and to perform at + /// most O(1) hashing/comparison operations on the `Element` type, as long + /// as `Element` properly implements hashing. + @inlinable + public mutating func updateValue( + forKey key: Key, + default defaultValue: @autoclosure () -> Value, + with body: (inout Value) throws -> R + ) rethrows -> R { + defer { _fixLifetime(self) } + let hash = _Hash(key) + let r = _root.updateValue(.top, forKey: key, hash) { + $0.initialize(to: (key, defaultValue())) + } + return try _UnsafeHandle.update(r.leaf) { + try body(&$0[item: r.slot].value) + } + } + + /// Removes the given key and its associated value from the dictionary. + /// + /// If the key is found in the dictionary, this method returns the key's + /// associated value, and invalidates all previously returned indices. + /// + /// var hues: TreeDictionary = [ + /// "Heliotrope": 296, + /// "Coral": 16, + /// "Aquamarine": 156] + /// if let value = hues.removeValue(forKey: "Coral") { + /// print("The value \(value) was removed.") + /// } + /// // Prints "The value 16 was removed." + /// + /// If the key isn't found in the dictionary, `removeValue(forKey:)` returns + /// `nil`. Removing a key that isn't in the dictionary does not invalidate + /// any indices. + /// + /// if let value = hues.removeValue(forKey: "Cerise") { + /// print("The value \(value) was removed.") + /// } else { + /// print("No value found for that key.") + /// } + /// // Prints "No value found for that key."" + /// + /// - Parameter key: The key to remove along with its associated value. + /// + /// - Returns: The value that was removed, or `nil` if the key was not + /// present in the dictionary. + /// + /// - Complexity: In addition to calling `body`, this operation is expected + /// to copy at most O(log(`count`)) existing members and to perform at + /// most O(1) hashing/comparison operations on the `Element` type, as long + /// as `Element` properly implements hashing. + @inlinable + @discardableResult + public mutating func removeValue(forKey key: Key) -> Value? { + guard let r = _root.remove(.top, key, _Hash(key)) else { return nil } + _invalidateIndices() + assert(r.remainder == nil) + _invariantCheck() + return r.removed.value + } + + /// Removes and returns the key-value pair at the specified index. + /// + /// Calling this method invalidates all existing indices in the dictionary. + /// + /// - Parameter index: The position of the element to remove. `index` must be + /// a valid index of the dictionary that is not equal to `endIndex`. + /// + /// - Returns: The removed key-value pair. + /// + /// - Complexity: This operation is expected to copy at most O(log(`count`)) + /// existing members and to perform at most O(1) hashing/comparison + /// operations on the `Element` type, as long as `Element` properly + /// implements hashing. + @inlinable + public mutating func remove(at index: Index) -> Element { + precondition(_isValid(index), "Invalid index") + precondition(index._path._isItem, "Can't remove item at end index") + _invalidateIndices() + let r = _root.remove(.top, at: index._path) + assert(r.remainder == nil) + return r.removed + } +} + diff --git a/Sources/HashTreeCollections/TreeSet/TreeSet+Codable.swift b/Sources/HashTreeCollections/TreeSet/TreeSet+Codable.swift new file mode 100644 index 000000000..1d8080269 --- /dev/null +++ b/Sources/HashTreeCollections/TreeSet/TreeSet+Codable.swift @@ -0,0 +1,46 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 TreeSet: Encodable where Element: Encodable { + /// Encodes the elements of this set into the given encoder. + /// + /// - Parameter encoder: The encoder to write data to. + @inlinable + public func encode(to encoder: Encoder) throws { + var container = encoder.unkeyedContainer() + try container.encode(contentsOf: self) + } +} + +extension TreeSet: Decodable where Element: Decodable { + /// Creates a new set by decoding from the given decoder. + /// + /// This initializer throws an error if reading from the decoder fails, or + /// if the decoded contents contain duplicate values. + /// + /// - Parameter decoder: The decoder to read data from. + @inlinable + public init(from decoder: Decoder) throws { + self.init() + + var container = try decoder.unkeyedContainer() + while !container.isAtEnd { + let element = try container.decode(Element.self) + let inserted = self._insert(element) + guard inserted else { + let context = DecodingError.Context( + codingPath: container.codingPath, + debugDescription: "Decoded elements aren't unique (first duplicate at offset \(self.count))") + throw DecodingError.dataCorrupted(context) + } + } + } +} diff --git a/Sources/HashTreeCollections/TreeSet/TreeSet+Collection.swift b/Sources/HashTreeCollections/TreeSet/TreeSet+Collection.swift new file mode 100644 index 000000000..c2fcd3e11 --- /dev/null +++ b/Sources/HashTreeCollections/TreeSet/TreeSet+Collection.swift @@ -0,0 +1,370 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 TreeSet { + /// The position of an element in a persistent set. + /// + /// An index in a persistent set is a compact encoding of a path in the + /// underlying prefix tree. Such indices are valid until the tree structure + /// is changed; hence, indices are usually invalidated every time the set + /// gets mutated. + @frozen + public struct Index { + @usableFromInline + internal let _root: _UnmanagedHashNode + + @usableFromInline + internal var _version: UInt + + @usableFromInline + internal var _path: _UnsafePath + + @inlinable @inline(__always) + internal init( + _root: _UnmanagedHashNode, version: UInt, path: _UnsafePath + ) { + self._root = _root + self._version = version + self._path = path + } + } +} + +extension TreeSet.Index: @unchecked Sendable +where Element: Sendable {} + +extension TreeSet.Index: Equatable { + /// Returns a Boolean value indicating whether two index values are equal. + /// + /// Note that comparing two indices that do not belong to the same tree + /// leads to a runtime error. + /// + /// - Complexity: O(1) + @inlinable + public static func ==(left: Self, right: Self) -> Bool { + precondition( + left._root == right._root && left._version == right._version, + "Indices from different set values aren't comparable") + return left._path == right._path + } +} + +extension TreeSet.Index: Comparable { + /// Returns a Boolean value indicating whether the value of the first argument + /// is less than the second argument. + /// + /// Note that comparing two indices that do not belong to the same tree + /// leads to a runtime error. + /// + /// - Complexity: O(1) + @inlinable + public static func <(left: Self, right: Self) -> Bool { + precondition( + left._root == right._root && left._version == right._version, + "Indices from different set values aren't comparable") + return left._path < right._path + } +} + +extension TreeSet.Index: Hashable { + /// Hashes the essential components of this value by feeding them into the + /// given hasher. + /// + /// - Complexity: O(1) + @inlinable + public func hash(into hasher: inout Hasher) { + hasher.combine(_path) + } +} + +extension TreeSet.Index: CustomStringConvertible { + // A textual representation of this instance. + public var description: String { + _path.description + } +} + +extension TreeSet.Index: CustomDebugStringConvertible { + /// A textual representation of this instance, suitable for debugging. + public var debugDescription: String { + description + } +} + +extension TreeSet: Collection { + /// A Boolean value indicating whether the collection is empty. + /// + /// - Complexity: O(1) + @inlinable + public var isEmpty: Bool { + _root.count == 0 + } + + /// The number of elements in the collection. + /// + /// - Complexity: O(1) + @inlinable + public var count: Int { + _root.count + } + + /// The position of the first element in a nonempty collection, or `endIndex` + /// if the collection is empty. + /// + /// - Complexity: O(1) + public var startIndex: Index { + var path = _UnsafePath(root: _root.raw) + path.descendToLeftMostItem() + return Index(_root: _root.unmanaged, version: _version, path: path) + } + + /// The collection’s “past the end” position—that is, the position one greater + /// than the last valid subscript argument. + /// + /// - Complexity: O(1) + @inlinable + public var endIndex: Index { + var path = _UnsafePath(root: _root.raw) + path.selectEnd() + return Index(_root: _root.unmanaged, version: _version, path: path) + } + + @inlinable @inline(__always) + internal func _isValid(_ i: Index) -> Bool { + _root.isIdentical(to: i._root) && i._version == self._version + } + + @inlinable @inline(__always) + internal mutating func _invalidateIndices() { + _version &+= 1 + } + + /// Accesses the key-value pair at the specified position. + /// + /// - Parameter position: The position of the element to access. `position` + /// must be a valid index of the collection that is not equal to + /// `endIndex`. + /// + /// - Complexity: O(1) + @inlinable + public subscript(position: Index) -> Element { + precondition(_isValid(position), "Invalid index") + precondition(position._path.isOnItem, "Can't get element at endIndex") + return _UnsafeHandle.read(position._path.node) { + $0[item: position._path.currentItemSlot].key + } + } + + /// Replaces the given index with its successor. + /// + /// - Parameter i: A valid index of the collection. + /// `i` must be less than `endIndex`. + /// + /// - Complexity: O(1) + @inlinable + public func formIndex(after i: inout Index) { + precondition(_isValid(i), "Invalid index") + guard i._path.findSuccessorItem(under: _root.raw) else { + preconditionFailure("The end index has no successor") + } + } + + /// Returns the position immediately after the given index. + /// + /// - Parameter i: A valid index of the collection. + /// `i` must be less than `endIndex`. + /// + /// - Complexity: O(1) + @inlinable @inline(__always) + public func index(after i: Index) -> Index { + var i = i + formIndex(after: &i) + return i + } + + /// Returns the distance between two arbitrary valid indices in this + /// collection. + /// + /// - Parameter start: A valid index of the collection. + /// - Parameter end: Another valid index of the collection. + /// - Returns: The distance between `start` and `end`. + /// (The result can be negative, even though `TreeSet` is not a + /// bidirectional collection.) + /// - Complexity: O(log(`count`)) + @inlinable + public func distance(from start: Index, to end: Index) -> Int { + precondition(_isValid(start) && _isValid(end), "Invalid index") + return _root.raw.distance(.top, from: start._path, to: end._path) + } + + /// Returns an index that is the specified distance from the given index. + /// + /// The value passed as `distance` must not offset `i` beyond the bounds of + /// the collection. + /// + /// - Parameters: + /// - i: A valid index of the collection. + /// - distance: The distance to offset `i`. As a special exception, + /// `distance` is allowed to be negative even though `TreeSet` + /// isn't a bidirectional collection. + /// - Returns: An index offset by `distance` from the index `i`. If + /// `distance` is positive, this is the same value as the result of + /// `distance` calls to `index(after:)`. If distance is negative, then + /// `distance` calls to `index(after:)` on the returned value will be the + /// same as `start`. + /// + /// - Complexity: O(log(`distance`)) + @inlinable + public func index(_ i: Index, offsetBy distance: Int) -> Index { + precondition(_isValid(i), "Invalid index") + var i = i + let r = _root.raw.seek(.top, &i._path, offsetBy: distance) + precondition(r, "Index offset out of bounds") + return i + } + + /// Returns an index that is the specified distance from the given index, + /// unless that distance is beyond a given limiting index. + /// + /// The value passed as `distance` must not offset `i` beyond the bounds of + /// the collection, unless the index passed as `limit` prevents offsetting + /// beyond those bounds. + /// + /// - Parameters: + /// - i: A valid index of the collection. + /// - distance: The distance to offset `i`. As a special exception, + /// `distance` is allowed to be negative even though `TreeSet` + /// isn't a bidirectional collection. + /// - limit: A valid index of the collection to use as a limit. If + /// `distance > 0`, a limit that is less than `i` has no effect. + /// Likewise, if `distance < 0`, a limit that is greater than `i` has no + /// effect. + /// - Returns: An index offset by `distance` from the index `i`, unless that + /// index would be beyond `limit` in the direction of movement. In that + /// case, the method returns `nil`. + /// + /// - Complexity: O(log(`distance`)) + @inlinable + public func index( + _ i: Index, offsetBy distance: Int, limitedBy limit: Index + ) -> Index? { + precondition(_isValid(i), "Invalid index") + precondition(_isValid(limit), "Invalid limit index") + var i = i + let (found, limited) = _root.raw.seek( + .top, &i._path, offsetBy: distance, limitedBy: limit._path + ) + if found { return i } + precondition(limited, "Index offset out of bounds") + return nil + } + + @inlinable @inline(__always) + public func _customIndexOfEquatableElement( + _ element: Element + ) -> Index?? { + _index(of: element) + } + + @inlinable @inline(__always) + public func _customLastIndexOfEquatableElement( + _ element: Element + ) -> Index?? { + _index(of: element) + } + + /// Returns the index of the specified member of the collection, or `nil` if + /// the value isn't a member. + /// + /// - Parameter element: An element to search for in the collection. + /// - Returns: The index where `element` is found. If `element` is not + /// found in the collection, returns `nil`. + /// + /// - Complexity: The expected complexity is O(1) hashing/comparison + /// operations, as long as `Element` properly implements `Hashable`. + @inlinable + public func firstIndex(of element: Element) -> Index? { + _index(of: element) + } + + /// Returns the index of the specified member of the collection, or `nil` if + /// the value isn't a member. + /// + /// - Parameter element: An element to search for in the collection. + /// - Returns: The index where `element` is found. If `element` is not + /// found in the collection, returns `nil`. + /// + /// - Complexity: The expected complexity is O(1) hashing/comparison + /// operations, as long as `Element` properly implements `Hashable`. + @inlinable + public func lastIndex(of element: Element) -> Index? { + _index(of: element) + } + + @inlinable + internal func _index(of element: Element) -> Index? { + let hash = _Hash(element) + guard let path = _root.path(to: element, hash) else { return nil } + return Index(_root: _root.unmanaged, version: _version, path: path) + } + + public func _failEarlyRangeCheck( + _ index: Index, bounds: Range + ) { + precondition(_isValid(index)) + } + + public func _failEarlyRangeCheck( + _ index: Index, bounds: ClosedRange + ) { + precondition(_isValid(index)) + } + + public func _failEarlyRangeCheck( + _ range: Range, bounds: Range + ) { + precondition(_isValid(range.lowerBound) && _isValid(range.upperBound)) + } +} + +#if false +// Note: Let's not do this. `BidirectionalCollection` would imply that +// the ordering of elements would be meaningful, which isn't true for +// `TreeSet`. +extension TreeSet: BidirectionalCollection { + /// Replaces the given index with its predecessor. + /// + /// - Parameter i: A valid index of the collection. + /// `i` must be greater than `startIndex`. + /// + /// - Complexity: O(1) + @inlinable + public func formIndex(before i: inout Index) { + precondition(_isValid(i), "Invalid index") + guard i._path.findPredecessorItem(under: _root.raw) else { + preconditionFailure("The start index has no predecessor") + } + } + + /// Returns the position immediately before the given index. + /// + /// - Parameter i: A valid index of the collection. + /// `i` must be greater than `startIndex`. + /// + /// - Complexity: O(1) + @inlinable @inline(__always) + public func index(before i: Index) -> Index { + var i = i + formIndex(before: &i) + return i + } +} +#endif diff --git a/Sources/HashTreeCollections/TreeSet/TreeSet+CustomReflectable.swift b/Sources/HashTreeCollections/TreeSet/TreeSet+CustomReflectable.swift new file mode 100644 index 000000000..e10bee5ad --- /dev/null +++ b/Sources/HashTreeCollections/TreeSet/TreeSet+CustomReflectable.swift @@ -0,0 +1,17 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 TreeSet: CustomReflectable { + /// The custom mirror for this instance. + public var customMirror: Mirror { + Mirror(self, unlabeledChildren: self, displayStyle: .set) + } +} diff --git a/Sources/HashTreeCollections/TreeSet/TreeSet+Debugging.swift b/Sources/HashTreeCollections/TreeSet/TreeSet+Debugging.swift new file mode 100644 index 000000000..a000f61d6 --- /dev/null +++ b/Sources/HashTreeCollections/TreeSet/TreeSet+Debugging.swift @@ -0,0 +1,45 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension TreeSet { + /// True if consistency checking is enabled in the implementation of this + /// type, false otherwise. + /// + /// Documented performance promises are null and void when this property + /// returns true -- for example, operations that are documented to take + /// O(1) time might take O(*n*) time, or worse. + public static var _isConsistencyCheckingEnabled: Bool { + _isCollectionsInternalCheckingEnabled + } + + @inlinable + public func _invariantCheck() { + _root._fullInvariantCheck() + } + + public func _dump(iterationOrder: Bool = false) { + _root.dump(iterationOrder: iterationOrder) + } + + public static var _maxDepth: Int { + _HashLevel.limit + } + + public var _statistics: _HashTreeStatistics { + var stats = _HashTreeStatistics() + _root.gatherStatistics(.top, &stats) + return stats + } +} diff --git a/Sources/DequeModule/Deque+CustomStringConvertible.swift b/Sources/HashTreeCollections/TreeSet/TreeSet+Descriptions.swift similarity index 53% rename from Sources/DequeModule/Deque+CustomStringConvertible.swift rename to Sources/HashTreeCollections/TreeSet/TreeSet+Descriptions.swift index 915c339b4..fd6a36a8a 100644 --- a/Sources/DequeModule/Deque+CustomStringConvertible.swift +++ b/Sources/HashTreeCollections/TreeSet/TreeSet+Descriptions.swift @@ -2,27 +2,27 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2022 - 2024 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 Deque: CustomStringConvertible { +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension TreeSet: CustomStringConvertible { /// A textual representation of this instance. public var description: String { - var result = "[" - var first = true - for item in self { - if first { - first = false - } else { - result += ", " - } - print(item, terminator: "", to: &result) - } - result += "]" - return result + _arrayDescription(for: self) + } +} + +extension TreeSet: CustomDebugStringConvertible { + /// A textual representation of this instance, suitable for debugging. + public var debugDescription: String { + description } } diff --git a/Sources/HashTreeCollections/TreeSet/TreeSet+Equatable.swift b/Sources/HashTreeCollections/TreeSet/TreeSet+Equatable.swift new file mode 100644 index 000000000..0d1022681 --- /dev/null +++ b/Sources/HashTreeCollections/TreeSet/TreeSet+Equatable.swift @@ -0,0 +1,31 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension TreeSet: Equatable { + /// Returns a Boolean value indicating whether two values are equal. + /// + /// Two persistent sets are considered equal if they contain the same + /// elements, but not necessarily in the same order. + /// + /// - Note: This simply forwards to the ``isEqualSet(to:)-4bc1i`` method. + /// That method has additional overloads that can be used to compare + /// persistent sets with additional types. + /// + /// - Complexity: O(`min(left.count, right.count)`) + @inlinable @inline(__always) + public static func == (left: Self, right: Self) -> Bool { + left.isEqualSet(to: right) + } +} diff --git a/Sources/HashTreeCollections/TreeSet/TreeSet+ExpressibleByArrayLiteral.swift b/Sources/HashTreeCollections/TreeSet/TreeSet+ExpressibleByArrayLiteral.swift new file mode 100644 index 000000000..ce74752cf --- /dev/null +++ b/Sources/HashTreeCollections/TreeSet/TreeSet+ExpressibleByArrayLiteral.swift @@ -0,0 +1,34 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 TreeSet: ExpressibleByArrayLiteral { + /// Creates a new set from the contents of an array literal. + /// + /// Duplicate elements in the literal are allowed, but the resulting + /// persistent set will only contain the first occurrence of each. + /// + /// Do not call this initializer directly. It is used by the compiler when + /// you use an array literal. Instead, create a new persistent set using an + /// array literal as its value by enclosing a comma-separated list of values + /// in square brackets. You can use an array literal anywhere a persistent set + /// is expected by the type context. + /// + /// Like the standard `Set`, persistent sets do not preserve the order of + /// elements inside the array literal. + /// + /// - Parameter elements: A variadic list of elements of the new set. + /// + /// - Complexity: O(`elements.count`) if `Element` properly implements + /// hashing. + public init(arrayLiteral elements: Element...) { + self.init(elements) + } +} diff --git a/Sources/HashTreeCollections/TreeSet/TreeSet+Extras.swift b/Sources/HashTreeCollections/TreeSet/TreeSet+Extras.swift new file mode 100644 index 000000000..9ed41206c --- /dev/null +++ b/Sources/HashTreeCollections/TreeSet/TreeSet+Extras.swift @@ -0,0 +1,71 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension TreeSet: _UniqueCollection {} + +extension TreeSet { + /// Removes the element at the given valid index. + /// + /// Calling this method invalidates all existing indices of the collection. + /// + /// - Parameter position: The index of the member to remove. `position` must + /// be a valid index of the set, and it must not be equal to the set’s + /// end index. + /// - Returns: The element that was removed from the set. + /// - Complexity: O(log(`count`)) if set storage might be shared; O(1) + /// otherwise. + @discardableResult + public mutating func remove(at position: Index) -> Element { + precondition(_isValid(position)) + _invalidateIndices() + let r = _root.remove(.top, at: position._path) + precondition(r.remainder == nil) + return r.removed.key + } + + /// Replace the member at the given index with a new value that compares equal + /// to it. + /// + /// This is useful when equal elements can be distinguished by identity + /// comparison or some other means. Updating a member through this method + /// does not require any hashing operations. + /// + /// Calling this method invalidates all existing indices of the collection. + /// + /// - Parameter item: The new value that should replace the original element. + /// `item` must compare equal to the original value. + /// + /// - Parameter index: The index of the element to be replaced. + /// + /// - Returns: The original element that was replaced. + /// + /// - Complexity: O(log(`count`)) if set storage might be shared; O(1) + /// otherwise. + public mutating func update(_ member: Element, at index: Index) -> Element { + defer { _fixLifetime(self) } + precondition(_isValid(index), "Invalid index") + precondition(index._path.isOnItem, "Can't get element at endIndex") + _invalidateIndices() + return _UnsafeHandle.update(index._path.node) { + let p = $0.itemPtr(at: index._path.currentItemSlot) + var old = member + precondition( + member == p.pointee.key, + "The replacement item must compare equal to the original") + swap(&p.pointee.key, &old) + return old + } + } +} diff --git a/Sources/HashTreeCollections/TreeSet/TreeSet+Filter.swift b/Sources/HashTreeCollections/TreeSet/TreeSet+Filter.swift new file mode 100644 index 000000000..1039d458f --- /dev/null +++ b/Sources/HashTreeCollections/TreeSet/TreeSet+Filter.swift @@ -0,0 +1,55 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 TreeSet { + /// Returns a new persistent set containing all the members of this persistent + /// set that satisfy the given predicate. + /// + /// - Parameter isIncluded: A closure that takes a value as its + /// argument and returns a Boolean value indicating whether the value + /// should be included in the returned set. + /// + /// - Returns: A set of the values that `isIncluded` allows. + /// + /// - Complexity: O(`count`) + @inlinable + public func filter( + _ isIncluded: (Element) throws -> Bool + ) rethrows -> Self { + let result = try _root.filter(.top) { try isIncluded($0.key) } + guard let result = result else { return self } + return TreeSet(_new: result.finalize(.top)) + } + + /// Removes all the elements that satisfy the given predicate. + /// + /// Use this method to remove every element in the set that meets + /// particular criteria. + /// This example removes all the odd values from a + /// set of numbers: + /// + /// var numbers: TreeSet = [5, 6, 7, 8, 9, 10, 11] + /// numbers.removeAll(where: { $0 % 2 != 0 }) + /// // numbers == [6, 8, 10] + /// + /// - Parameter shouldBeRemoved: A closure that takes an element of the + /// set as its argument and returns a Boolean value indicating + /// whether the element should be removed from the collection. + /// + /// - Complexity: O(`count`) + @inlinable + public mutating func removeAll( + where shouldBeRemoved: (Element) throws -> Bool + ) rethrows { + // FIXME: Implement in-place reductions + self = try filter { try !shouldBeRemoved($0) } + } +} diff --git a/Sources/HashTreeCollections/TreeSet/TreeSet+Hashable.swift b/Sources/HashTreeCollections/TreeSet/TreeSet+Hashable.swift new file mode 100644 index 000000000..e1148333e --- /dev/null +++ b/Sources/HashTreeCollections/TreeSet/TreeSet+Hashable.swift @@ -0,0 +1,28 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 TreeSet: Hashable { + /// Hashes the essential components of this value by feeding them into the + /// given hasher. + /// + /// Complexity: O(`count`) + @inlinable + public func hash(into hasher: inout Hasher) { + let copy = hasher + let seed = copy.finalize() + + var hash = 0 + for member in self { + hash ^= member._rawHashValue(seed: seed) + } + hasher.combine(hash) + } +} diff --git a/Sources/HashTreeCollections/TreeSet/TreeSet+Sendable.swift b/Sources/HashTreeCollections/TreeSet/TreeSet+Sendable.swift new file mode 100644 index 000000000..8b94642ef --- /dev/null +++ b/Sources/HashTreeCollections/TreeSet/TreeSet+Sendable.swift @@ -0,0 +1,12 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 TreeSet: @unchecked Sendable where Element: Sendable {} diff --git a/Sources/HashTreeCollections/TreeSet/TreeSet+Sequence.swift b/Sources/HashTreeCollections/TreeSet/TreeSet+Sequence.swift new file mode 100644 index 000000000..1b26d9540 --- /dev/null +++ b/Sources/HashTreeCollections/TreeSet/TreeSet+Sequence.swift @@ -0,0 +1,53 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 TreeSet: Sequence { + /// An iterator over the members of a `TreeSet`. + @frozen + public struct Iterator: IteratorProtocol { + @usableFromInline + internal typealias _UnsafeHandle = _Node.UnsafeHandle + + @usableFromInline + internal var _it: _HashTreeIterator + + @inlinable + internal init(_root: _RawHashNode) { + _it = _HashTreeIterator(root: _root) + } + + /// Advances to the next element and returns it, or `nil` if no next element + /// exists. + /// + /// Once `nil` has been returned, all subsequent calls return `nil`. + /// + /// - Complexity: O(1) + @inlinable + public mutating func next() -> Element? { + guard let (node, slot) = _it.next() else { return nil } + return _UnsafeHandle.read(node) { $0[item: slot].key } + } + } + + /// Returns an iterator over the members of the set. + @inlinable + public func makeIterator() -> Iterator { + Iterator(_root: _root.raw) + } + + @inlinable + public func _customContainsEquatableElement(_ element: Element) -> Bool? { + _root.containsKey(.top, element, _Hash(element)) + } +} + +extension TreeSet.Iterator: @unchecked Sendable +where Element: Sendable {} diff --git a/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra Initializers.swift b/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra Initializers.swift new file mode 100644 index 000000000..69cdac453 --- /dev/null +++ b/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra Initializers.swift @@ -0,0 +1,73 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension TreeSet { + /// Creates an empty set. + /// + /// This initializer is equivalent to initializing with an empty array + /// literal. + /// + /// - Complexity: O(1) + @inlinable + public init() { + self.init(_new: ._emptyNode()) + } + + /// Creates a new set from a finite sequence of items. + /// + /// - Parameter items: The elements to use as members of the new set. + /// The sequence is allowed to contain duplicate elements, but only + /// the first duplicate instance is preserved in the result. + /// + /// - Complexity: This operation is expected to perform O(*n*) + /// hashing and equality comparisons on average (where *n* + /// is the number of elements in the sequence), provided that + /// `Element` properly implements hashing. + @inlinable + public init(_ items: __owned some Sequence) { + if let items = _specialize(items, for: Self.self) { + self = items + return + } + self.init() + for item in items { + self._insert(item) + } + } + + /// Creates a new set from a an existing set. This is functionally the same as + /// copying the value of `items` into a new variable. + /// + /// - Parameter items: The elements to use as members of the new set. + /// + /// - Complexity: O(1) + @inlinable + public init(_ items: __owned Self) { + self = items + } + + /// Creates a new persistent set from the keys view of an existing persistent + /// dictionary. + /// + /// - Parameter items: The elements to use as members of the new set. + /// + /// - Complexity: O(*items.count*) + @inlinable + public init( + _ item: __owned TreeDictionary.Keys + ) { + self.init(_new: item._base._root.mapValues { _ in () }) + } +} diff --git a/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra basics.swift b/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra basics.swift new file mode 100644 index 000000000..a44705814 --- /dev/null +++ b/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra basics.swift @@ -0,0 +1,134 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension TreeSet: SetAlgebra { + /// Returns a Boolean value that indicates whether the given element exists + /// in the set. + /// + /// - Parameter element: An element to look for in the set. + /// + /// - Returns: `true` if `element` exists in the set; otherwise, `false`. + /// + /// - Complexity: This operation is expected to perform O(1) hashing and + /// comparison operations on average, provided that `Element` implements + /// high-quality hashing. + @inlinable + public func contains(_ item: Element) -> Bool { + _root.containsKey(.top, item, _Hash(item)) + } + + /// Insert a new member to this set, if the set doesn't already contain it. + /// + /// Inserting a new member invalidates all existing indices of the collection. + /// + /// - Parameter newMember: The element to insert into the set. + /// + /// - Returns: `(true, newMember)` if `newMember` was not contained in the + /// set. If an element equal to `newMember` was already contained in the + /// set, the method returns `(false, oldMember)`, where `oldMember` is the + /// element that was equal to `newMember`. In some cases, `oldMember` may + /// be distinguishable from `newMember` by identity comparison or some + /// other means. + /// + /// - Complexity: This operation is expected to copy at most O(log(`count`)) + /// existing members and to perform at most O(1) hashing/comparison + /// operations on the `Element` type, as long as `Element` properly + /// implements hashing. + @discardableResult + @inlinable + public mutating func insert( + _ newMember: __owned Element + ) -> (inserted: Bool, memberAfterInsert: Element) { + defer { _fixLifetime(self) } + let hash = _Hash(newMember) + let r = _root.insert(.top, (newMember, ()), hash) + if r.inserted { + _invalidateIndices() + return (true, newMember) + } + return _UnsafeHandle.read(r.leaf) { + (false, $0[item: r.slot].key) + } + } + + @discardableResult + @inlinable + internal mutating func _insert(_ newMember: __owned Element) -> Bool { + let hash = _Hash(newMember) + let r = _root.insert(.top, (newMember, ()), hash) + return r.inserted + } + + /// Removes the given element from the set. + /// + /// Removing an existing member invalidates all existing indices of the + /// collection. + /// + /// - Parameter member: The element of the set to remove. + /// + /// - Returns: The element equal to `member` if `member` is contained in the + /// set; otherwise, `nil`. In some cases, the returned element may be + /// distinguishable from `newMember` by identity comparison or some other + /// means. + /// + /// - Complexity: This operation is expected to copy at most O(log(`count`)) + /// existing members and to perform at most O(1) hashing/comparison + /// operations on the `Element` type, as long as `Element` properly + /// implements hashing. + @discardableResult + @inlinable + public mutating func remove(_ member: Element) -> Element? { + let hash = _Hash(member) + guard let r = _root.remove(.top, member, hash) else { return nil } + _invalidateIndices() + assert(r.remainder == nil) + return r.removed.key + } + + /// Inserts the given element into the set unconditionally. + /// + /// If an element equal to `newMember` is already contained in the set, + /// `newMember` replaces the existing element. + /// + /// If `newMember` was not already a member, it gets inserted. + /// + /// - Parameter newMember: An element to insert into the set. + /// + /// - Returns: The original member equal to `newMember` if the set already + /// contained such a member; otherwise, `nil`. In some cases, the returned + /// element may be distinguishable from `newMember` by identity comparison + /// or some other means. + /// + /// - Complexity: This operation is expected to copy at most O(log(`count`)) + /// existing members and to perform at most O(1) hashing/comparison + /// operations on the `Element` type, as long as `Element` properly + /// implements hashing. + @discardableResult + @inlinable + public mutating func update(with newMember: __owned Element) -> Element? { + defer { _fixLifetime(self) } + let hash = _Hash(newMember) + let r = _root.updateValue(.top, forKey: newMember, hash) { + $0.initialize(to: (newMember, ())) + } + if r.inserted { return nil } + return _UnsafeHandle.update(r.leaf) { + let p = $0.itemPtr(at: r.slot) + let old = p.move().key + p.initialize(to: (newMember, ())) + return old + } + } +} diff --git a/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra formIntersection.swift b/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra formIntersection.swift new file mode 100644 index 000000000..8e6a8dc55 --- /dev/null +++ b/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra formIntersection.swift @@ -0,0 +1,76 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 TreeSet { + /// Removes the elements of this set that aren't also in the given one. + /// + /// var a: TreeSet = [1, 2, 3, 4] + /// let b: TreeSet = [0, 2, 4, 6] + /// a.formIntersection(b) + /// // `a` is now some permutation of `[2, 4]` + /// + /// The result will only contain instances that were originally in `self`. + /// (This matters if equal members can be distinguished by comparing their + /// identities, or by some other means.) + /// + /// - Parameter other: An arbitrary set of elements. + /// + /// - Complexity: Expected complexity is O(`self.count` + `other.count`) in + /// the worst case, if `Element` properly implements hashing. + /// However, the implementation is careful to make the best use of + /// hash tree structure to minimize work when possible, e.g. by linking + /// parts of the input trees directly into the result. + @inlinable + public mutating func formIntersection(_ other: Self) { + // FIXME: Implement in-place reductions + self = intersection(other) + } + + /// Removes the elements of this set that aren't also in the given keys view + /// of a persistent dictionary. + /// + /// The result will only contain instances that were originally in `self`. + /// (This matters if equal members can be distinguished by comparing their + /// identities, or by some other means.) + /// + /// - Parameter other: The keys view of a persistent dictionary. + /// + /// - Complexity: Expected complexity is O(`self.count` + `other.count`) in + /// the worst case, if `Element` properly implements hashing. + /// However, the implementation is careful to make the best use of + /// hash tree structure to minimize work when possible, e.g. by linking + /// parts of the input trees directly into the result. + @inlinable + public mutating func formIntersection( + _ other: TreeDictionary.Keys + ) { + // FIXME: Implement in-place reductions + self = intersection(other) + } + + /// Removes the elements of this set that aren't also in the given sequence. + /// + /// var a: TreeSet = [1, 2, 3, 4] + /// let b = [0, 2, 4, 6] + /// a.formIntersection(b) + /// // `a` is now some permutation of `[2, 4]` + /// + /// The result will only contain instances that were originally in `self`. + /// (This matters if equal members can be distinguished by comparing their + /// identities, or by some other means.) + /// + /// - Parameter other: An arbitrary finite sequence of items, + /// possibly containing duplicate values. + @inlinable + public mutating func formIntersection(_ other: some Sequence) { + self = intersection(other) + } +} diff --git a/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra formSymmetricDifference.swift b/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra formSymmetricDifference.swift new file mode 100644 index 000000000..22b8c74b2 --- /dev/null +++ b/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra formSymmetricDifference.swift @@ -0,0 +1,71 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 TreeSet { + /// Replace this set with the elements contained in this set or the given + /// set, but not both. + /// + /// var a: TreeSet = [1, 2, 3, 4] + /// let b: TreeSet = [0, 2, 4, 6] + /// a.formSymmetricDifference(b) + /// // `a` is now some permutation of `[0, 1, 3, 6]` + /// + /// - Parameter other: An arbitrary set of elements. + /// + /// - Complexity: Expected complexity is O(`self.count` + `other.count`) in + /// the worst case, if `Element` properly implements hashing. + /// However, the implementation is careful to make the best use of + /// hash tree structure to minimize work when possible, e.g. by linking + /// parts of the input trees directly into the result. + @inlinable + public mutating func formSymmetricDifference(_ other: __owned Self) { + self = symmetricDifference(other) + } + + /// Replace this set with the elements contained in this set or the given + /// keys view of a persistent dictionary, but not both. + /// + /// var a: TreeSet = [1, 2, 3, 4] + /// let b: TreeDictionary = [0: "a", 2: "b", 4: "c", 6: "d"] + /// a.formSymmetricDifference(b.keys) + /// // `a` is now some permutation of `[0, 1, 3, 6]` + /// + /// - Parameter other: An arbitrary set of elements. + /// + /// - Complexity: Expected complexity is O(`self.count` + `other.count`) in + /// the worst case, if `Element` properly implements hashing. + /// However, the implementation is careful to make the best use of + /// hash tree structure to minimize work when possible, e.g. by linking + /// parts of the input trees directly into the result. + @inlinable + public mutating func formSymmetricDifference( + _ other: __owned TreeDictionary.Keys + ) { + self = symmetricDifference(other) + } + + /// Replace this set with the elements contained in this set or the given + /// sequence, but not both. + /// + /// var a: TreeSet = [1, 2, 3, 4] + /// let b = [0, 2, 4, 6, 2, 4, 6] + /// a.formSymmetricDifference(b) + /// // `a` is now some permutation of `[0, 1, 3, 6]` + /// + /// - Parameter other: A finite sequence of elements, possibly containing + /// duplicate values. + @inlinable + public mutating func formSymmetricDifference( + _ other: __owned some Sequence + ) { + self = symmetricDifference(other) + } +} diff --git a/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra formUnion.swift b/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra formUnion.swift new file mode 100644 index 000000000..c4c14471e --- /dev/null +++ b/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra formUnion.swift @@ -0,0 +1,92 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 TreeSet { + /// Adds the elements of the given set to this set. + /// + /// var a: TreeSet = [1, 2, 3, 4] + /// let b: TreeSet = [0, 2, 4, 6] + /// a.formUnion(b) + /// // `a` is now some permutation of `[0, 1, 2, 3, 4, 6]` + /// + /// For values that are members of both sets, this operation preserves the + /// instances that were originally in `self`. (This matters if equal members + /// can be distinguished by comparing their identities, or by some other + /// means.) + /// + /// - Parameter other: The set of elements to insert. + /// + /// - Complexity: Expected complexity is O(`self.count` + `other.count`) in + /// the worst case, if `Element` properly implements hashing. + /// However, the implementation is careful to make the best use of + /// hash tree structure to minimize work when possible, e.g. by linking + /// parts of the input trees directly into the result. + @inlinable + public mutating func formUnion(_ other: __owned Self) { + self = union(other) + } + + /// Adds the elements of the given keys view of a persistent dictionary + /// to this set. + /// + /// var a: TreeSet = [1, 2, 3, 4] + /// let b: TreeDictionary = [0: "a", 2: "b", 4: "c", 6: "d"] + /// a.formUnion(b.keys) + /// // `a` is now some permutation of `[0, 1, 2, 3, 4, 6]` + /// + /// For values that are members of both inputs, this operation preserves the + /// instances that were originally in `self`. (This matters if equal members + /// can be distinguished by comparing their identities, or by some other + /// means.) + /// + /// - Parameter other: The keys view of a persistent dictionary. + /// + /// - Complexity: Expected complexity is O(`self.count` + `other.count`) in + /// the worst case, if `Element` properly implements hashing. + /// However, the implementation is careful to make the best use of + /// hash tree structure to minimize work when possible, e.g. by linking + /// parts of the input trees directly into the result. + @inlinable + public mutating func formUnion( + _ other: __owned TreeDictionary.Keys + ) { + self = union(other) + } + + /// Adds the elements of the given sequence to this set. + /// + /// var a: TreeSet = [1, 2, 3, 4] + /// let b = [0, 2, 4, 6, 0, 2] + /// a.formUnion(b) + /// // `a` is now some permutation of `[0, 1, 2, 3, 4, 6]` + /// + /// For values that are members of both inputs, this operation preserves the + /// instances that were originally in `self`. (This matters if equal members + /// can be distinguished by comparing their identities, or by some other + /// means.) + /// + /// If some of the values that are missing from `self` have multiple copies + /// in `other`, then the result of this function always contains the first + /// instances in the sequence -- the second and subsequent copies are ignored. + /// + /// - Parameter other: An arbitrary finite sequence of items, + /// possibly containing duplicate values. + /// + /// - Complexity: Expected complexity is O(`self.count` + `other.count`) in + /// the worst case, if `Element` properly implements hashing. + /// However, the implementation is careful to make the best use of + /// hash tree structure to minimize work when possible, e.g. by linking + /// parts of the input trees directly into the result. + @inlinable + public mutating func formUnion(_ other: __owned some Sequence) { + self = union(other) + } +} diff --git a/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra intersection.swift b/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra intersection.swift new file mode 100644 index 000000000..4235d50ee --- /dev/null +++ b/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra intersection.swift @@ -0,0 +1,113 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension TreeSet { + /// Returns a new set with the elements that are common to both this set and + /// the provided other one. + /// + /// var a: TreeSet = [1, 2, 3, 4] + /// let b: TreeSet = [0, 2, 4, 6] + /// let c = a.intersection(b) + /// // `c` is some permutation of `[2, 4]` + /// + /// The result will only contain instances that were originally in `self`. + /// (This matters if equal members can be distinguished by comparing their + /// identities, or by some other means.) + /// + /// - Parameter other: An arbitrary set of elements. + /// + /// - Complexity: Expected complexity is O(`self.count`) in + /// the worst case, if `Element` properly implements hashing. + /// However, the implementation is careful to make the best use of + /// hash tree structure to minimize work when possible, e.g. by linking + /// parts of the input trees directly into the result. + @inlinable @inline(__always) + public func intersection(_ other: Self) -> Self { + _intersection(other._root) + } + + /// Returns a new set with the elements that are common to both this set and + /// the provided keys view of a persistent dictionary. + /// + /// var a: TreeSet = [1, 2, 3, 4] + /// let b: TreeDictionary = [0: "a", 2: "b", 4: "c", 6: "d"] + /// let c = a.intersection(b.keys) + /// // `c` is some permutation of `[2, 4]` + /// + /// The result will only contain instances that were originally in `self`. + /// (This matters if equal members can be distinguished by comparing their + /// identities, or by some other means.) + /// + /// - Parameter other: The keys view of a persistent dictionary. + /// + /// - Complexity: Expected complexity is O(`self.count`) in + /// the worst case, if `Element` properly implements hashing. + /// However, the implementation is careful to make the best use of + /// hash tree structure to minimize work when possible, e.g. by linking + /// parts of the input trees directly into the result. + @inlinable @inline(__always) + public func intersection( + _ other: TreeDictionary.Keys + ) -> Self { + _intersection(other._base._root) + } + + @inlinable + internal func _intersection(_ other: _HashNode) -> Self { + guard let r = _root.intersection(.top, other) else { return self } + return Self(_new: r) + } + + /// Returns a new set with the elements that are common to both this set and + /// the provided sequence. + /// + /// var a: TreeSet = [1, 2, 3, 4] + /// let b = [0, 2, 4, 6] + /// let c = a.intersection(b) + /// // `c` is some permutation of `[2, 4]` + /// + /// The result will only contain instances that were originally in `self`. + /// (This matters if equal members can be distinguished by comparing their + /// identities, or by some other means.) + /// + /// - Parameter other: An arbitrary finite sequence of items, + /// possibly containing duplicate values. + @inlinable + public func intersection( + _ other: some Sequence + ) -> Self { + if let other = _specialize(other, for: Self.self) { + return intersection(other) + } + + guard let first = self.first else { return Self() } + if other._customContainsEquatableElement(first) != nil { + // Fast path: the sequence has fast containment checks. + return self.filter { other.contains($0) } + } + + var result: _Node = ._emptyNode() + for item in other { + let hash = _Hash(item) + if let r = self._root.lookup(.top, item, hash) { + let itemInSelf = _UnsafeHandle.read(r.node) { $0[item: r.slot] } + _ = result.updateValue(.top, forKey: itemInSelf.key, hash) { + $0.initialize(to: itemInSelf) + } + } + } + return Self(_new: result) + } +} diff --git a/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra isDisjoint.swift b/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra isDisjoint.swift new file mode 100644 index 000000000..e7d8f77d7 --- /dev/null +++ b/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra isDisjoint.swift @@ -0,0 +1,80 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension TreeSet { + /// Returns a Boolean value that indicates whether the set has no members in + /// common with the given set. + /// + /// let a: TreeSet = [1, 2, 3, 4] + /// let b: TreeSet = [5, 6] + /// a.isDisjoint(with: b) // true + /// + /// - Parameter other: Another set. + /// + /// - Returns: `true` if `self` has no elements in common with `other`; + /// otherwise, `false`. + /// + /// - Complexity: Expected to be O(min(`self.count`, `other.count`)) on + /// average, if `Element` implements high-quality hashing. + @inlinable + public func isDisjoint(with other: Self) -> Bool { + self._root.isDisjoint(.top, with: other._root) + } + + /// Returns a Boolean value that indicates whether the set has no members in + /// common with the given set. + /// + /// let a: TreeSet = [1, 2, 3, 4] + /// let b: TreeSet = [5, 6] + /// a.isDisjoint(with: b) // true + /// + /// - Parameter other: The keys view of a persistent dictionary. + /// + /// - Returns: `true` if `self` has no elements in common with `other`; + /// otherwise, `false`. + /// + /// - Complexity: Expected to be O(min(`self.count`, `other.count`)) on + /// average, if `Element` implements high-quality hashing. + @inlinable + public func isDisjoint( + with other: TreeDictionary.Keys + ) -> Bool { + self._root.isDisjoint(.top, with: other._base._root) + } + + /// Returns a Boolean value that indicates whether the set has no members in + /// common with the given set. + /// + /// let a: TreeSet = [1, 2, 3, 4] + /// let b: TreeSet = [5, 6] + /// a.isDisjoint(with: b) // true + /// + /// - Parameter other: A finite sequence of elements, some of which may + /// appear more than once. + /// + /// - Returns: `true` if `self` has no elements in common with `other`; + /// otherwise, `false`. + /// + /// - Complexity: In the worst case, this makes O(*n*) calls to + /// `self.contains`, where *n* is the length of the sequence. + @inlinable + public func isDisjoint(with other: some Sequence) -> Bool { + guard !self.isEmpty else { return true } + if let other = _specialize(other, for: Self.self) { + return isDisjoint(with: other) + } + return other.allSatisfy { !self.contains($0) } + } +} diff --git a/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra isEqualSet.swift b/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra isEqualSet.swift new file mode 100644 index 000000000..874b36d70 --- /dev/null +++ b/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra isEqualSet.swift @@ -0,0 +1,123 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +// FIXME: These are non-standard extensions generalizing ==. +extension TreeSet { + /// Returns a Boolean value indicating whether persistent sets are equal. Two + /// persistent sets are considered equal if they contain the same elements, + /// but not necessarily in the same order. + /// + /// - Parameter other: Another set. + /// + /// - Returns: `true` if the set is equal to `other`; otherwise, `false`. + /// + /// - Complexity: Generally O(`count`), as long as`Element` properly + /// implements hashing. That said, the implementation is careful to take + /// every available shortcut to reduce complexity, e.g. by skipping + /// comparing elements in shared subtrees. + @inlinable + public func isEqualSet(to other: Self) -> Bool { + _root.isEqualSet(to: other._root, by: { _, _ in true }) + } + + /// Returns a Boolean value indicating whether a persistent set compares equal + /// to the given keys view of a persistent dictionary. The two input + /// collections are considered equal if they contain the same elements, + /// but not necessarily in the same order. + /// + /// - Parameter other: The keys view of a persistent dictionary. + /// + /// - Returns: `true` if the set contains exactly the same members as `other`; + /// otherwise, `false`. + /// + /// - Complexity: Generally O(`count`), as long as`Element` properly + /// implements hashing. That said, the implementation is careful to take + /// every available shortcut to reduce complexity, e.g. by skipping + /// comparing elements in shared subtrees. + @inlinable + public func isEqualSet( + to other: TreeDictionary.Keys + ) -> Bool { + _root.isEqualSet(to: other._base._root, by: { _, _ in true }) + } + + /// Returns a Boolean value indicating whether this persistent set contains + /// the same elements as the given `other` sequence, but not necessarily + /// in the same order. + /// + /// Duplicate items in `other` do not prevent it from comparing equal to + /// `self`. + /// + /// let this: TreeSet = [0, 1, 5, 6] + /// let that = [5, 5, 0, 1, 1, 6, 5, 0, 1, 6, 6, 5] + /// + /// this.isEqualSet(to: that) // true + /// + /// - Parameter other: The keys view of a persistent dictionary. + /// + /// - Returns: `true` if the set contains exactly the same members as `other`; + /// otherwise, `false`. This function does not consider the order of + /// elements and it ignores duplicate items in `other`. + /// + /// - Complexity: Generally O(*n*), where *n* is the number of items in + /// `other`, as long as`Element` properly implements hashing. + /// That said, the implementation is careful to take + /// every available shortcut to reduce complexity, e.g. by skipping + /// comparing elements in shared subtrees. + @inlinable + public func isEqualSet(to other: some Sequence) -> Bool { + if let other = _specialize(other, for: Self.self) { + return isEqualSet(to: other) + } + + if self.isEmpty { + return other.allSatisfy { _ in false } + } + + if other is _UniqueCollection { + // We don't need to create a temporary set. + guard other.underestimatedCount <= self.count else { return false } + var seen = 0 + for item in other { + guard self.contains(item) else { return false } + seen &+= 1 + } + precondition( + seen <= self.count, + // Otherwise other.underestimatedCount != other.count + "Invalid Collection '\(type(of: other))' (bad underestimatedCount)") + return seen == self.count + } + + // FIXME: Would making this a BitSet of seen positions be better? + var seen: _Node? = ._emptyNode() + var it = other.makeIterator() + while let item = it.next() { + let hash = _Hash(item) + guard self._root.containsKey(.top, item, hash) else { return false } + _ = seen!.insert(.top, (item, ()), hash) // Ignore dupes + if seen!.count == self.count { + // We've seen them all. Stop further accounting. + seen = nil + break + } + } + guard seen == nil else { return false } + while let item = it.next() { + guard self.contains(item) else { return false } + } + return true + } +} diff --git a/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra isStrictSubset.swift b/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra isStrictSubset.swift new file mode 100644 index 000000000..821d81402 --- /dev/null +++ b/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra isStrictSubset.swift @@ -0,0 +1,140 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension TreeSet { + /// Returns a Boolean value that indicates whether the set is a strict subset + /// of the given set. + /// + /// Set *A* is a strict subset of another set *B* if every member of *A* is + /// also a member of *B* and *B* contains at least one element that is not a + /// member of *A*. (Ignoring the order the elements appear in the sets.) + /// + /// let a: TreeSet = [1, 2, 3, 4] + /// let b: TreeSet = [4, 2, 1] + /// b.isStrictSubset(of: a) // true + /// + /// - Parameter other: Another set. + /// + /// - Returns: `true` if `self` is a strict subset of `other`; otherwise, + /// `false`. + /// + /// - Complexity: Expected to be O(`self.count`) on average, if `Element` + /// implements high-quality hashing. The implementation is careful to make + /// the best use of hash tree structure to minimize work when possible, + /// e.g. by skipping over parts of the input trees. + @inlinable + public func isStrictSubset(of other: Self) -> Bool { + guard self.count < other.count else { return false } + return isSubset(of: other) + } + + /// Returns a Boolean value that indicates whether the set is a strict subset + /// of the given set. + /// + /// Set *A* is a strict subset of another set *B* if every member of *A* is + /// also a member of *B* and *B* contains at least one element that is not a + /// member of *A*. (Ignoring the order the elements appear in the sets.) + /// + /// let a: TreeSet = [1, 2, 3, 4] + /// let b: TreeSet = [4, 2, 1] + /// b.isStrictSubset(of: a) // true + /// + /// - Parameter other: The keys view of a persistent dictionary. + /// + /// - Returns: `true` if `self` is a strict subset of `other`; otherwise, + /// `false`. + /// + /// - Complexity: Expected to be O(`self.count`) on average, if `Element` + /// implements high-quality hashing. The implementation is careful to make + /// the best use of hash tree structure to minimize work when possible, + /// e.g. by skipping over parts of the input trees. + @inlinable + public func isStrictSubset( + of other: TreeDictionary.Keys + ) -> Bool { + guard self.count < other.count else { return false } + return isSubset(of: other) + } + + /// Returns a Boolean value that indicates whether the set is a strict subset + /// of the given set. + /// + /// Set *A* is a strict subset of another set *B* if every member of *A* is + /// also a member of *B* and *B* contains at least one element that is not a + /// member of *A*. (Ignoring the order the elements appear in the sets.) + /// + /// let a: TreeSet = [1, 2, 3, 4] + /// let b: TreeSet = [4, 2, 1] + /// b.isStrictSubset(of: a) // true + /// + /// - Parameter other: A sequence of elements, some of whose elements may + /// appear more than once. + /// + /// - Returns: `true` if `self` is a strict subset of `other`; otherwise, + /// `false`. + /// + /// - Complexity: In the worst case, this makes O(*n*) calls to + /// `self.contains` (where *n* is the number of elements in `other`), + /// and it constructs a temporary persistent set containing every + /// element of the sequence. + @inlinable + public func isStrictSubset(of other: some Sequence) -> Bool { + if let other = _specialize(other, for: Self.self) { + return isStrictSubset(of: other) + } + + do { + var it = self.makeIterator() + guard let first = it.next() else { + return other.contains(where: { _ in true }) + } + if let match = other._customContainsEquatableElement(first) { + // Fast path: the sequence has fast containment checks. + guard match else { return false } + while let item = it.next() { + guard other.contains(item) else { return false } + } + return !other.allSatisfy { self.contains($0) } + } + } + + // FIXME: Would making this a BitSet of seen positions be better? + var seen: _Node = ._emptyNode() + var doneCollecting = false + var isStrict = false + var it = other.makeIterator() + while let item = it.next() { + let hash = _Hash(item) + if self._root.containsKey(.top, item, hash) { + if + !doneCollecting, + seen.insert(.top, (item, ()), hash).inserted, + seen.count == self.count + { + if isStrict { return true } + // Stop collecting seen items -- we just need to decide + // strictness now. + seen = ._emptyNode() + doneCollecting = true + } + } else { + isStrict = true + if doneCollecting { return true } + } + } + + return false + } +} diff --git a/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra isStrictSuperset.swift b/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra isStrictSuperset.swift new file mode 100644 index 000000000..4bc74f645 --- /dev/null +++ b/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra isStrictSuperset.swift @@ -0,0 +1,125 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension TreeSet { + /// Returns a Boolean value that indicates whether the set is a strict + /// superset of the given set. + /// + /// Set *A* is a strict superset of another set *B* if every member of *B* is + /// also a member of *A* and *A* contains at least one element that is *not* + /// a member of *B*. (Ignoring the order the elements appear in the sets.) + /// + /// let a: TreeSet = [1, 2, 3, 4] + /// let b: TreeSet = [4, 2, 1] + /// a.isStrictSuperset(of: b.unordered) // true + /// + /// - Parameter other: Another set. + /// + /// - Returns: `true` if `self` is a strict superset of `other`; otherwise, + /// `false`. + /// + /// - Complexity: Expected to be O(`other.count`) on average, if `Element` + /// implements high-quality hashing. The implementation is careful to make + /// the best use of hash tree structure to minimize work when possible, + /// e.g. by skipping over parts of the input trees. + @inlinable + public func isStrictSuperset(of other: Self) -> Bool { + guard self.count > other.count else { return false } + return other._root.isSubset(.top, of: self._root) + } + + /// Returns a Boolean value that indicates whether the set is a strict + /// superset of the given set. + /// + /// Set *A* is a strict subset of another set *B* if every member of *A* is + /// also a member of *B* and *B* contains at least one element that is not a + /// member of *A*. (Ignoring the order the elements appear in the sets.) + /// + /// let a: TreeSet = [1, 2, 3, 4] + /// let b: TreeSet = [4, 2, 1] + /// a.isStrictSuperset(of: b.unordered) // true + /// + /// - Parameter other: The keys view of a persistent dictionary. + /// + /// - Returns: `true` if `self` is a strict subset of `other`; otherwise, + /// `false`. + /// + /// - Complexity: Expected to be O(`other.count`) on average, if `Element` + /// implements high-quality hashing. The implementation is careful to make + /// the best use of hash tree structure to minimize work when possible, + /// e.g. by skipping over parts of the input trees. + @inlinable + public func isStrictSuperset( + of other: TreeDictionary.Keys + ) -> Bool { + guard self.count > other.count else { return false } + return other._base._root.isSubset(.top, of: self._root) + } + + /// Returns a Boolean value that indicates whether the set is a strict + /// superset of the given set. + /// + /// Set *A* is a strict subset of another set *B* if every member of *A* is + /// also a member of *B* and *B* contains at least one element that is not a + /// member of *A*. (Ignoring the order the elements appear in the sets.) + /// + /// let a: TreeSet = [1, 2, 3, 4] + /// let b: TreeSet = [4, 2, 1] + /// a.isStrictSuperset(of: b.unordered) // true + /// + /// - Parameter other: A sequence of elements, some of whose elements may + /// appear more than once. (Duplicate items are ignored.) + /// + /// - Returns: `true` if `self` is a strict subset of `other`; otherwise, + /// `false`. + /// + /// - Complexity: In the worst case, this makes O(*n*) calls to + /// `self.contains` (where *n* is the number of elements in `other`), + /// and it constructs a temporary persistent set containing every + /// element of the sequence. + @inlinable + public func isStrictSuperset(of other: some Sequence) -> Bool { + if let other = _specialize(other, for: Self.self) { + return isStrictSuperset(of: other) + } + + var it = self.makeIterator() + guard let first = it.next() else { return false } + if let match = other._customContainsEquatableElement(first) { + // Fast path: the sequence has fast containment checks. + guard other.allSatisfy({ self.contains($0) }) else { return false } + guard match else { return true } + while let item = it.next() { + guard other.contains(item) else { return true } + } + return false + } + + // FIXME: Would making this a BitSet of seen positions be better? + var seen: _Node = ._emptyNode() + for item in other { + let hash = _Hash(item) + guard self._root.containsKey(.top, item, hash) else { return false } + if + seen.insert(.top, (item, ()), hash).inserted, + seen.count == self.count + { + return false + } + } + assert(seen.count < self.count) + return true + } +} diff --git a/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra isSubset.swift b/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra isSubset.swift new file mode 100644 index 000000000..95adf35e7 --- /dev/null +++ b/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra isSubset.swift @@ -0,0 +1,113 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension TreeSet { + /// Returns a Boolean value that indicates whether this set is a subset of + /// the given set. + /// + /// Set *A* is a subset of another set *B* if every member of *A* is also a + /// member of *B*, ignoring the order they appear in the two sets. + /// + /// let a: TreeSet = [1, 2, 3, 4] + /// let b: TreeSet = [4, 2, 1] + /// b.isSubset(of: a) // true + /// + /// - Parameter other: Another set. + /// + /// - Returns: `true` if the set is a subset of `other`; otherwise, `false`. + /// + /// - Complexity: Expected to be O(`self.count`) on average, if `Element` + /// implements high-quality hashing. The implementation is careful to make + /// the best use of hash tree structure to minimize work when possible, + /// e.g. by skipping over parts of the input trees. + @inlinable + public func isSubset(of other: Self) -> Bool { + self._root.isSubset(.top, of: other._root) + } + + /// Returns a Boolean value that indicates whether this set is a subset of + /// the given set. + /// + /// Set *A* is a subset of another set *B* if every member of *A* is also a + /// member of *B*, ignoring the order they appear in the two sets. + /// + /// let a: TreeSet = [1, 2, 3, 4] + /// let b: TreeSet = [4, 2, 1] + /// b.isSubset(of: a) // true + /// + /// - Parameter other: The keys view of a persistent dictionary. + /// + /// - Returns: `true` if the set is a subset of `other`; otherwise, `false`. + /// + /// - Complexity: Expected to be O(`self.count`) on average, if `Element` + /// implements high-quality hashing. The implementation is careful to make + /// the best use of hash tree structure to minimize work when possible, + /// e.g. by skipping over parts of the input trees. + @inlinable + public func isSubset( + of other: TreeDictionary.Keys + ) -> Bool { + self._root.isSubset(.top, of: other._base._root) + } + + /// Returns a Boolean value that indicates whether this set is a subset of + /// the given sequence. + /// + /// Set *A* is a subset of another set *B* if every member of *A* is also a + /// member of *B*, ignoring the order they appear in the two sets. + /// + /// let a: TreeSet = [1, 2, 3, 4] + /// let b: TreeSet = [4, 2, 1] + /// b.isSubset(of: a) // true + /// + /// - Parameter other: A sequence of elements, some of whose elements may + /// appear more than once. + /// + /// - Returns: `true` if the set is a subset of `other`; otherwise, `false`. + /// + /// - Complexity: In the worst case, this makes O(*n*) calls to + /// `self.contains` (where *n* is the number of elements in `other`), + /// and it constructs a temporary persistent set containing every + /// element of the sequence. + @inlinable + public func isSubset(of other: some Sequence) -> Bool { + if let other = _specialize(other, for: Self.self) { + return isSubset(of: other) + } + + var it = self.makeIterator() + guard let first = it.next() else { return true } + if let match = other._customContainsEquatableElement(first) { + // Fast path: the sequence has fast containment checks. + guard match else { return false } + while let item = it.next() { + guard other.contains(item) else { return false } + } + return true + } + + // FIXME: Would making this a BitSet of seen positions be better? + var seen: _Node = ._emptyNode() + for item in other { + let hash = _Hash(item) + guard _root.containsKey(.top, item, hash) else { continue } + guard seen.insert(.top, (item, ()), hash).inserted else { continue } + if seen.count == self.count { + return true + } + } + return false + } +} diff --git a/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra isSuperset.swift b/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra isSuperset.swift new file mode 100644 index 000000000..49659d564 --- /dev/null +++ b/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra isSuperset.swift @@ -0,0 +1,90 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension TreeSet { + /// Returns a Boolean value that indicates whether this set is a superset of + /// the given set. + /// + /// Set *A* is a superset of another set *B* if every member of *B* is also a + /// member of *A*, ignoring the order they appear in the two sets. + /// + /// let a: TreeSet = [1, 2, 3, 4] + /// let b: TreeSet = [4, 2, 1] + /// a.isSuperset(of: b) // true + /// + /// - Parameter other: Another set. + /// + /// - Returns: `true` if the set is a subset of `other`; otherwise, `false`. + /// + /// - Complexity: Expected to be O(`other.count`) on average, if `Element` + /// implements high-quality hashing. The implementation is careful to make + /// the best use of hash tree structure to minimize work when possible, + /// e.g. by skipping over parts of the input trees. + @inlinable + public func isSuperset(of other: Self) -> Bool { + other._root.isSubset(.top, of: self._root) + } + + /// Returns a Boolean value that indicates whether this set is a superset of + /// the given set. + /// + /// Set *A* is a superset of another set *B* if every member of *B* is also a + /// member of *A*, ignoring the order they appear in the two sets. + /// + /// let a: TreeSet = [1, 2, 3, 4] + /// let b: TreeSet = [4, 2, 1] + /// a.isSuperset(of: b) // true + /// + /// - Parameter other: The keys view of a persistent dictionary. + /// + /// - Returns: `true` if the set is a subset of `other`; otherwise, `false`. + /// + /// - Complexity: Expected to be O(`other.count`) on average, if `Element` + /// implements high-quality hashing. The implementation is careful to make + /// the best use of hash tree structure to minimize work when possible, + /// e.g. by skipping over parts of the input trees. + @inlinable + public func isSuperset( + of other: TreeDictionary.Keys + ) -> Bool { + other._base._root.isSubset(.top, of: self._root) + } + + /// Returns a Boolean value that indicates whether this set is a superset of + /// the given set. + /// + /// Set *A* is a superset of another set *B* if every member of *B* is also a + /// member of *A*, ignoring the order they appear in the two sets. + /// + /// let a: TreeSet = [1, 2, 3, 4] + /// let b: TreeSet = [4, 2, 1] + /// a.isSuperset(of: b) // true + /// + /// - Parameter other: A sequence of elements, some of whose elements may + /// appear more than once. (Duplicate items are ignored.) + /// + /// - Returns: `true` if the set is a subset of `other`; otherwise, `false`. + /// + /// - Complexity: O(*n*) calls to `self.contains`, where *n* is the number + /// of elements in `other`. + @inlinable + public func isSuperset(of other: some Sequence) -> Bool { + if let other = _specialize(other, for: Self.self) { + return isSuperset(of: other) + } + + return other.allSatisfy { self.contains($0) } + } +} diff --git a/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra subtract.swift b/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra subtract.swift new file mode 100644 index 000000000..e4803f261 --- /dev/null +++ b/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra subtract.swift @@ -0,0 +1,71 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 TreeSet { + /// Removes the elements of the given set from this set. + /// + /// var a: TreeSet = [1, 2, 3, 4] + /// let b: TreeSet = [0, 2, 4, 6] + /// a.subtract(b) + /// // `a` is now some permutation of `[1, 3]` + /// + /// - Parameter other: An arbitrary set of elements. + /// + /// - Complexity: Expected complexity is O(`self.count` + `other.count`) in + /// the worst case, if `Element` properly implements hashing. + /// However, the implementation is careful to make the best use of + /// hash tree structure to minimize work when possible, e.g. by linking + /// parts of the input trees directly into the result. + @inlinable + public mutating func subtract(_ other: Self) { + // FIXME: Implement in-place reductions + self = subtracting(other) + } + + /// Removes the elements of the given keys view of a persistent dictionary + /// from this set. + /// + /// var a: TreeSet = [1, 2, 3, 4] + /// let b: TreeDictionary = [0: "a", 2: "b", 4: "c", 6: "d"] + /// a.subtract(b.keys) + /// // `a` is now some permutation of `[1, 3]` + /// + /// - Parameter other: The keys view of a persistent dictionary. + /// + /// - Complexity: Expected complexity is O(`self.count` + `other.count`) in + /// the worst case, if `Element` properly implements hashing. + /// However, the implementation is careful to make the best use of + /// hash tree structure to minimize work when possible, e.g. by linking + /// parts of the input trees directly into the result. + @inlinable + public mutating func subtract( + _ other: TreeDictionary.Keys + ) { + // FIXME: Implement in-place reductions + self = subtracting(other) + } + + /// Removes the elements of the given sequence from this set. + /// + /// var a: TreeSet = [1, 2, 3, 4] + /// let b = [0, 2, 4, 6] + /// a.subtract(b) + /// // `a` is now some permutation of `[1, 3]` + /// + /// - Parameter other: An arbitrary finite sequence. + /// + /// - Complexity: O(*n*) where *n* is the number of elements in `other`, + /// as long as `Element` properly implements hashing. + @inlinable + public mutating func subtract(_ other: some Sequence) { + self = subtracting(other) + } +} diff --git a/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra subtracting.swift b/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra subtracting.swift new file mode 100644 index 000000000..975dff00d --- /dev/null +++ b/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra subtracting.swift @@ -0,0 +1,98 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension TreeSet { + /// Returns a new set containing the elements of this set that do not occur + /// in the given other set. + /// + /// var a: TreeSet = [1, 2, 3, 4] + /// let b: TreeSet = [0, 2, 4, 6] + /// let c = a.subtracting(b) + /// // `c` is some permutation of `[1, 3]` + /// + /// - Parameter other: An arbitrary set of elements. + /// + /// - Complexity: Expected complexity is O(`self.count`) in + /// the worst case, if `Element` properly implements hashing. + /// However, the implementation is careful to make the best use of + /// hash tree structure to minimize work when possible, e.g. by linking + /// parts of the input trees directly into the result. + @inlinable + public __consuming func subtracting(_ other: Self) -> Self { + _subtracting(other._root) + } + + /// Returns a new set containing the elements of this set that do not occur + /// in the given keys view of a persistent dictionary. + /// + /// var a: TreeSet = [1, 2, 3, 4] + /// let b: TreeDictionary = [0: "a", 2: "b", 4: "c", 6: "d"] + /// let c = a.subtracting(b.keys) + /// // `c` is some permutation of `[1, 3]` + /// + /// - Parameter other: The keys view of a persistent dictionary. + /// + /// - Complexity: Expected complexity is O(`self.count`) in + /// the worst case, if `Element` properly implements hashing. + /// However, the implementation is careful to make the best use of + /// hash tree structure to minimize work when possible, e.g. by linking + /// parts of the input trees directly into the result. + @inlinable + public __consuming func subtracting( + _ other: TreeDictionary.Keys + ) -> Self { + _subtracting(other._base._root) + } + + @inlinable + internal __consuming func _subtracting( + _ other: _HashNode + ) -> Self { + guard let r = _root.subtracting(.top, other) else { return self } + return Self(_new: r) + } + + /// Returns a new set containing the elements of this set that do not occur + /// in the given sequence. + /// + /// var a: TreeSet = [1, 2, 3, 4] + /// let b = [0, 2, 4, 6] + /// let c = a.subtracting(b) + /// // `c` is some permutation of `[1, 3]` + /// + /// - Parameter other: An arbitrary finite sequence. + /// + /// - Complexity: O(*n*) where *n* is the number of elements in `other`, + /// as long as `Element` properly implements hashing. + @inlinable + public __consuming func subtracting(_ other: some Sequence) -> Self { + if let other = _specialize(other, for: Self.self) { + return subtracting(other) + } + + guard let first = self.first else { return Self() } + if other._customContainsEquatableElement(first) != nil { + // Fast path: the sequence has fast containment checks. + return self.filter { !other.contains($0) } + } + + var root = self._root + for item in other { + let hash = _Hash(item) + _ = root.remove(.top, item, hash) + } + return Self(_new: root) + } +} diff --git a/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra symmetricDifference.swift b/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra symmetricDifference.swift new file mode 100644 index 000000000..80ab38ce3 --- /dev/null +++ b/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra symmetricDifference.swift @@ -0,0 +1,115 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension TreeSet { + /// Returns a new set with the elements that are either in this set or in + /// `other`, but not in both. + /// + /// var a: TreeSet = [1, 2, 3, 4] + /// let b: TreeSet = [0, 2, 4, 6] + /// let c = a.symmetricDifference(b) + /// // `c` is some permutation of `[0, 1, 3, 6]` + /// + /// - Parameter other: An arbitrary set of elements. + /// + /// - Complexity: Expected complexity is O(`self.count` + `other.count`) in + /// the worst case, if `Element` properly implements hashing. + /// However, the implementation is careful to make the best use of + /// hash tree structure to minimize work when possible, e.g. by linking + /// parts of the input trees directly into the result. + @inlinable + public func symmetricDifference(_ other: __owned Self) -> Self { + let branch = _root.symmetricDifference(.top, other._root) + guard let branch = branch else { return self } + let root = branch.finalize(.top) + root._fullInvariantCheck() + return TreeSet(_new: root) + } + + /// Returns a new set with the elements that are either in this set or in + /// `other`, but not in both. + /// + /// var a: TreeSet = [1, 2, 3, 4] + /// let b: TreeDictionary = [0: "a", 2: "b", 4: "c", 6: "d"] + /// let c = a.symmetricDifference(b.keys) + /// // `c` is some permutation of `[0, 1, 3, 6]` + /// + /// - Parameter other: An arbitrary set of elements. + /// + /// - Complexity: Expected complexity is O(`self.count` + `other.count`) in + /// the worst case, if `Element` properly implements hashing. + /// However, the implementation is careful to make the best use of + /// hash tree structure to minimize work when possible, e.g. by linking + /// parts of the input trees directly into the result. + @inlinable + public func symmetricDifference( + _ other: __owned TreeDictionary.Keys + ) -> Self { + let branch = _root.symmetricDifference(.top, other._base._root) + guard let branch = branch else { return self } + let root = branch.finalize(.top) + root._fullInvariantCheck() + return TreeSet(_new: root) + } + + /// Returns a new set with the elements that are either in this set or in + /// the given sequence, but not in both. + /// + /// var a: TreeSet = [1, 2, 3, 4] + /// let b = [0, 2, 4, 6, 6, 2] + /// let c = a.symmetricDifference(b) + /// // `c` is some permutation of `[0, 1, 3, 6]` + /// + /// In case the sequence contains duplicate elements, only the first instance + /// matters -- the second and subsequent instances are ignored by this method. + /// + /// - Parameter other: A finite sequence of elements, possibly containing + /// duplicates. + /// + /// - Complexity: Expected complexity is O(`self.count` + `other.count`) in + /// the worst case, if `Element` properly implements hashing. + /// However, the implementation is careful to make the best use of + /// hash tree structure to minimize work when possible, e.g. by linking + /// parts of the input trees directly into the result. + @inlinable + public func symmetricDifference( + _ other: __owned some Sequence + ) -> Self { + if let other = _specialize(other, for: Self.self) { + return symmetricDifference(other) + } + + if other is _UniqueCollection { + // Fast path: we can do a simple in-place loop. + var root = self._root + for item in other { + let hash = _Hash(item) + var state = root.prepareValueUpdate(item, hash) + if state.found { + state.value = nil + } else { + state.value = () + } + root.finalizeValueUpdate(state) + } + return Self(_new: root) + } + + // If `other` may contain duplicates, we need to be more + // careful (and slower). + let other = TreeSet(other) + return self.symmetricDifference(other) + } +} diff --git a/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra union.swift b/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra union.swift new file mode 100644 index 000000000..ba60e400e --- /dev/null +++ b/Sources/HashTreeCollections/TreeSet/TreeSet+SetAlgebra union.swift @@ -0,0 +1,110 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension TreeSet { + /// Returns a new set with the elements of both this and the given set. + /// + /// var a: TreeSet = [1, 2, 3, 4] + /// let b: TreeSet = [0, 2, 4, 6] + /// let c = a.union(b) + /// // `c` is some permutation of `[0, 1, 2, 3, 4, 6]` + /// + /// For values that are members of both sets, the result set contains the + /// instances that were originally in `self`. (This matters if equal members + /// can be distinguished by comparing their identities, or by some other + /// means.) + /// + /// - Parameter other: The set of elements to insert. + /// + /// - Complexity: Expected complexity is O(`self.count` + `other.count`) in + /// the worst case, if `Element` properly implements hashing. + /// However, the implementation is careful to make the best use of + /// hash tree structure to minimize work when possible, e.g. by linking + /// parts of the input trees directly into the result. + @inlinable + public func union(_ other: __owned Self) -> Self { + let r = _root.union(.top, other._root) + guard r.copied else { return self } + r.node._fullInvariantCheck() + return TreeSet(_new: r.node) + } + + /// Returns a new set with the elements of both this set and the given + /// keys view of a persistent dictionary. + /// + /// var a: TreeSet = [1, 2, 3, 4] + /// let b: TreeDictionary = [0: "a", 2: "b", 4: "c", 6: "d"] + /// let c = a.union(b) + /// // `c` is some permutation of `[0, 1, 2, 3, 4, 6]` + /// + /// For values that are members of both inputs, the result set contains the + /// instances that were originally in `self`. (This matters if equal members + /// can be distinguished by comparing their identities, or by some other + /// means.) + /// + /// - Parameter other: The keys view of a persistent dictionary. + /// + /// - Complexity: Expected complexity is O(`self.count` + `other.count`) in + /// the worst case, if `Element` properly implements hashing. + /// However, the implementation is careful to make the best use of + /// hash tree structure to minimize work when possible, e.g. by linking + /// parts of the input trees directly into the result. + @inlinable + public func union( + _ other: __owned TreeDictionary.Keys + ) -> Self { + let r = _root.union(.top, other._base._root) + guard r.copied else { return self } + r.node._fullInvariantCheck() + return TreeSet(_new: r.node) + } + + /// Returns a new set with the elements of both this set and the given + /// sequence. + /// + /// var a: TreeSet = [1, 2, 3, 4] + /// let b = [0, 2, 4, 6, 0, 2] + /// let c = a.union(b) + /// // `c` is some permutation of `[0, 1, 2, 3, 4, 6]` + /// + /// For values that are members of both inputs, the result set contains the + /// instances that were originally in `self`. (This matters if equal members + /// can be distinguished by comparing their identities, or by some other + /// means.) + /// + /// If some of the values that are missing from `self` have multiple copies + /// in `other`, then the result of this function always contains the first + /// instances in the sequence -- the second and subsequent copies are ignored. + /// + /// - Parameter other: An arbitrary finite sequence of items, + /// possibly containing duplicate values. + /// + /// - Complexity: Expected complexity is O(*n*) in + /// the worst case, where *n* is the number of items in `other`, + /// as long as `Element` properly implements hashing. + @inlinable + public func union(_ other: __owned some Sequence) -> Self { + if let other = _specialize(other, for: Self.self) { + return union(other) + } + + var root = self._root + for item in other { + let hash = _Hash(item) + _ = root.insert(.top, (item, ()), hash) + } + return Self(_new: root) + } +} diff --git a/Sources/HashTreeCollections/TreeSet/TreeSet.swift b/Sources/HashTreeCollections/TreeSet/TreeSet.swift new file mode 100644 index 000000000..7cde0ba82 --- /dev/null +++ b/Sources/HashTreeCollections/TreeSet/TreeSet.swift @@ -0,0 +1,84 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +/// An unordered collection of unique elements, optimized for mutating shared +/// copies and comparing different snapshots of the same collection. +/// +/// `TreeSet` has the same functionality as a standard `Set`, and +/// it largely implements the same APIs: both are hashed collection +/// types that conform to `SetAlgebra`, and both are unordered -- neither type +/// provides any guarantees about the ordering of their members. +/// +/// However, `TreeSet` is optimizing specifically for use cases that +/// need to mutate shared copies or to compare a set value to one of its older +/// snapshots. To use a term from functional programming, +/// `TreeSet` implements a _persistent data structure_. +/// +/// The standard `Set` stores its members in a single, flat hash table, and it +/// implements value semantics with all-or-nothing copy-on-write behavior: every +/// time a shared copy of a set is mutated, the mutation needs to make a full +/// copy of the set's storage. `TreeSet` takes a different approach: it +/// organizes its members into a tree structure, the nodes of which can be +/// freely shared across collection values. When mutating a shared copy of a set +/// value, `TreeSet` is able to simply link the unchanged parts of the +/// tree directly into the result, saving both time and memory. +/// +/// This structural sharing also makes it more efficient to compare mutated set +/// values to earlier versions of themselves. When comparing or combining sets, +/// parts that are shared across both inputs can typically be handled in +/// constant time, leading to a dramatic performance boost when the two inputs +/// are still largely unchanged: +/// +/// var set = TreeSet(0 ..< 10_000) +/// let copy = set +/// set.insert(20_000) // Expected to be an O(log(n)) operation +/// let diff = set.subtracting(copy) // Also O(log(n))! +/// // `diff` now holds the single item 20_000. +/// +/// The tree structure also eliminates the need to reserve capacity in advance: +/// `TreeSet` creates, destroys and resizes individual nodes as needed, +/// always consuming just enough memory to store its contents. As of Swift 5.9, +/// the standard collection types never shrink their storage, so temporary +/// storage spikes can linger as unused but still allocated memory long after +/// the collection has shrunk back to its usual size. +/// +/// Of course, switching to a tree structure comes with some trade offs. In +/// particular, inserting new items, removing existing ones, and iterating over +/// a `TreeSet` is expected to be a constant factor slower than a standard +/// `Set` -- allocating/deallocating nodes isn't free, and navigating the tree +/// structure requires more pointer dereferences than accessing a flat hash +/// table. However the algorithmic improvements above usually more than make up +/// for this, as long as the use case can make use of them. +@frozen // Not really -- this package is not at all ABI stable +public struct TreeSet { + @usableFromInline + internal typealias _Node = _HashNode + + @usableFromInline + internal typealias _UnsafeHandle = _Node.UnsafeHandle + + @usableFromInline + internal var _root: _Node + + @usableFromInline + internal var _version: UInt + + @inlinable + internal init(_root: _Node, version: UInt) { + self._root = _root + self._version = version + } + + @inlinable + internal init(_new: _Node) { + self.init(_root: _new, version: _new.initialVersionNumber) + } +} diff --git a/Sources/HeapModule/CMakeLists.txt b/Sources/HeapModule/CMakeLists.txt new file mode 100644 index 000000000..2fce017e0 --- /dev/null +++ b/Sources/HeapModule/CMakeLists.txt @@ -0,0 +1,23 @@ +#[[ +This source file is part of the Swift Collections Open Source Project + +Copyright (c) 2021 - 2024 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 +#]] + +add_library(HeapModule + "_HeapNode.swift" + "Heap.swift" + "Heap+Descriptions.swift" + "Heap+ExpressibleByArrayLiteral.swift" + "Heap+Invariants.swift" + "Heap+UnsafeHandle.swift") +target_link_libraries(HeapModule PRIVATE + _CollectionsUtilities) +set_target_properties(HeapModule PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) + +_install_target(HeapModule) +set_property(GLOBAL APPEND PROPERTY SWIFT_COLLECTIONS_EXPORTS HeapModule) diff --git a/Sources/HeapModule/Heap+Descriptions.swift b/Sources/HeapModule/Heap+Descriptions.swift new file mode 100644 index 000000000..7a26ea02c --- /dev/null +++ b/Sources/HeapModule/Heap+Descriptions.swift @@ -0,0 +1,35 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 Heap: CustomStringConvertible { + /// A textual representation of this instance. + public var description: String { + "<\(count) item\(count == 1 ? "" : "s") @\(_idString)>" + } + + internal var _idString: String { + // "<32 items @0x2787abcf>" + _storage.withUnsafeBytes { + guard let p = $0.baseAddress else { + return "nil" + } + return String(UInt(bitPattern: p), radix: 16) + } + } +} + +extension Heap: CustomDebugStringConvertible { + /// A textual representation of this instance, suitable for debugging. + public var debugDescription: String { + description + } +} + diff --git a/Sources/HeapModule/Heap+ExpressibleByArrayLiteral.swift b/Sources/HeapModule/Heap+ExpressibleByArrayLiteral.swift new file mode 100644 index 000000000..b9999b5e8 --- /dev/null +++ b/Sources/HeapModule/Heap+ExpressibleByArrayLiteral.swift @@ -0,0 +1,25 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 Heap: ExpressibleByArrayLiteral { + /// Creates a new heap from the contents of an array literal. + /// + /// **Do not call this initializer directly.** It is used by the compiler when + /// you use an array literal. Instead, create a new heap using an array + /// literal as its value by enclosing a comma-separated list of values in + /// square brackets. You can use an array literal anywhere a heap is expected + /// by the type context. + /// + /// - Parameter elements: A variadic list of elements of the new heap. + public init(arrayLiteral elements: Element...) { + self.init(elements) + } +} diff --git a/Sources/HeapModule/Heap+Invariants.swift b/Sources/HeapModule/Heap+Invariants.swift new file mode 100644 index 000000000..94904ba91 --- /dev/null +++ b/Sources/HeapModule/Heap+Invariants.swift @@ -0,0 +1,71 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension Heap { + /// True if consistency checking is enabled in the implementation of this + /// type, false otherwise. + /// + /// Documented performance promises are null and void when this property + /// returns true -- for example, operations that are documented to take + /// O(1) time might take O(*n*) time, or worse. + public static var _isConsistencyCheckingEnabled: Bool { + _isCollectionsInternalCheckingEnabled + } + + #if COLLECTIONS_INTERNAL_CHECKS + /// Visits each item in the heap in depth-first order, verifying that the + /// contents satisfy the min-max heap property. + @inlinable + @inline(never) + internal func _checkInvariants() { + guard count > 1 else { return } + _checkInvariants(node: .root, min: nil, max: nil) + } + + @inlinable + internal func _checkInvariants(node: _HeapNode, min: Element?, max: Element?) { + let value = _storage[node.offset] + if let min = min { + precondition(value >= min, + "Element \(value) at \(node) is less than min \(min)") + } + if let max = max { + precondition(value <= max, + "Element \(value) at \(node) is greater than max \(max)") + } + let left = node.leftChild() + let right = node.rightChild() + if node.isMinLevel { + if left.offset < count { + _checkInvariants(node: left, min: value, max: max) + } + if right.offset < count { + _checkInvariants(node: right, min: value, max: max) + } + } else { + if left.offset < count { + _checkInvariants(node: left, min: min, max: value) + } + if right.offset < count { + _checkInvariants(node: right, min: min, max: value) + } + } + } + #else + @inlinable + @inline(__always) + public func _checkInvariants() {} + #endif // COLLECTIONS_INTERNAL_CHECKS +} diff --git a/Sources/HeapModule/Heap+UnsafeHandle.swift b/Sources/HeapModule/Heap+UnsafeHandle.swift new file mode 100644 index 000000000..b7d8ceebb --- /dev/null +++ b/Sources/HeapModule/Heap+UnsafeHandle.swift @@ -0,0 +1,382 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 Heap { + @usableFromInline @frozen + struct _UnsafeHandle { + @usableFromInline + var buffer: UnsafeMutableBufferPointer + + @inlinable @inline(__always) + init(_ buffer: UnsafeMutableBufferPointer) { + self.buffer = buffer + } + } + + @inlinable @inline(__always) + mutating func _update(_ body: (_UnsafeHandle) -> R) -> R { + _storage.withUnsafeMutableBufferPointer { buffer in + body(_UnsafeHandle(buffer)) + } + } +} + +extension Heap._UnsafeHandle { + @inlinable @inline(__always) + internal var count: Int { + buffer.count + } + + @inlinable + subscript(node: _HeapNode) -> Element { + @inline(__always) + get { + buffer[node.offset] + } + @inline(__always) + nonmutating _modify { + yield &buffer[node.offset] + } + } + + @inlinable @inline(__always) + internal func ptr(to node: _HeapNode) -> UnsafeMutablePointer { + assert(node.offset < count) + return buffer.baseAddress! + node.offset + } + + /// Move the value at the specified node out of the buffer, leaving it + /// uninitialized. + @inlinable @inline(__always) + internal func extract(_ node: _HeapNode) -> Element { + ptr(to: node).move() + } + + @inlinable @inline(__always) + internal func initialize(_ node: _HeapNode, to value: __owned Element) { + ptr(to: node).initialize(to: value) + } + + /// Swaps the elements in the heap at the given indices. + @inlinable @inline(__always) + internal func swapAt(_ i: _HeapNode, _ j: _HeapNode) { + buffer.swapAt(i.offset, j.offset) + } + + /// Swaps the element at the given node with the supplied value. + @inlinable @inline(__always) + internal func swapAt(_ i: _HeapNode, with value: inout Element) { + let p = buffer.baseAddress.unsafelyUnwrapped + i.offset + swap(&p.pointee, &value) + } + + + @inlinable @inline(__always) + internal func minValue(_ a: _HeapNode, _ b: _HeapNode) -> _HeapNode { + self[a] < self[b] ? a : b + } + + @inlinable @inline(__always) + internal func maxValue(_ a: _HeapNode, _ b: _HeapNode) -> _HeapNode { + self[a] < self[b] ? b : a + } +} + +extension Heap._UnsafeHandle { + @inlinable + internal func bubbleUp(_ node: _HeapNode) { + guard !node.isRoot else { return } + + let parent = node.parent() + + var node = node + if (node.isMinLevel && self[node] > self[parent]) + || (!node.isMinLevel && self[node] < self[parent]){ + swapAt(node, parent) + node = parent + } + + if node.isMinLevel { + while let grandparent = node.grandParent(), + self[node] < self[grandparent] { + swapAt(node, grandparent) + node = grandparent + } + } else { + while let grandparent = node.grandParent(), + self[node] > self[grandparent] { + swapAt(node, grandparent) + node = grandparent + } + } + } +} + +extension Heap._UnsafeHandle { + /// Sink the item at `node` to its correct position in the heap. + /// The given node must be minimum-ordered. + @inlinable + internal func trickleDownMin(_ node: _HeapNode) { + assert(node.isMinLevel) + var node = node + var value = extract(node) + _trickleDownMin(node: &node, value: &value) + initialize(node, to: value) + } + + @inlinable @inline(__always) + internal func _trickleDownMin(node: inout _HeapNode, value: inout Element) { + // Note: `_HeapNode` is quite the useless abstraction here, as we don't need + // to look at its `level` property, and we need to move sideways amongst + // siblings/cousins in the tree, for which we don't have direct operations. + // Luckily, all the `_HeapNode` business gets optimized away, so this only + // affects the readability of the code, not its performance. + // The alternative would be to reintroduce offset-based parent/child + // navigation methods, which seems less palatable. + + var gc0 = node.firstGrandchild() + while gc0.offset &+ 3 < count { + // Invariant: buffer slot at `node` is uninitialized + + // We have four grandchildren, so we don't need to compare children. + let gc1 = _HeapNode(offset: gc0.offset &+ 1, level: gc0.level) + let minA = minValue(gc0, gc1) + + let gc2 = _HeapNode(offset: gc0.offset &+ 2, level: gc0.level) + let gc3 = _HeapNode(offset: gc0.offset &+ 3, level: gc0.level) + let minB = minValue(gc2, gc3) + + let min = minValue(minA, minB) + guard self[min] < value else { + return // We're done -- `node` is a good place for `value`. + } + + initialize(node, to: extract(min)) + node = min + gc0 = node.firstGrandchild() + + let parent = min.parent() + if self[parent] < value { + swapAt(parent, with: &value) + } + } + + // At this point, we don't have a full complement of grandchildren, but + // we haven't finished sinking the item. + + let c0 = node.leftChild() + if c0.offset >= count { + return // No more descendants to consider. + } + let min = _minDescendant(c0: c0, gc0: gc0) + guard self[min] < value else { + return // We're done. + } + + initialize(node, to: extract(min)) + node = min + + if min < gc0 { return } + + // If `min` was a grandchild, check the parent. + let parent = min.parent() + if self[parent] < value { + initialize(node, to: extract(parent)) + node = parent + } + } + + /// Returns the node holding the minimal item amongst the children & + /// grandchildren of a node in the tree. The parent node is not specified; + /// instead, this function takes the nodes corresponding to its first child + /// (`c0`) and first grandchild (`gc0`). + /// + /// There must be at least one child, but there must not be a full complement + /// of 4 grandchildren. (Other cases are handled directly above.) + /// + /// This method is an implementation detail of `trickleDownMin`. Do not call + /// it directly. + @inlinable + internal func _minDescendant(c0: _HeapNode, gc0: _HeapNode) -> _HeapNode { + assert(c0.offset < count) + assert(gc0.offset + 3 >= count) + + if gc0.offset < count { + if gc0.offset &+ 2 < count { + // We have three grandchildren. We don't need to compare direct children. + let gc1 = _HeapNode(offset: gc0.offset &+ 1, level: gc0.level) + let gc2 = _HeapNode(offset: gc0.offset &+ 2, level: gc0.level) + return minValue(minValue(gc0, gc1), gc2) + } + + let c1 = _HeapNode(offset: c0.offset &+ 1, level: c0.level) + let m = minValue(c1, gc0) + if gc0.offset &+ 1 < count { + // Two grandchildren. + let gc1 = _HeapNode(offset: gc0.offset &+ 1, level: gc0.level) + return minValue(m, gc1) + } + + // One grandchild. + return m + } + + let c1 = _HeapNode(offset: c0.offset &+ 1, level: c0.level) + if c1.offset < count { + return minValue(c0, c1) + } + + return c0 + } + + /// Sink the item at `node` to its correct position in the heap. + /// The given node must be maximum-ordered. + @inlinable + internal func trickleDownMax(_ node: _HeapNode) { + assert(!node.isMinLevel) + var node = node + var value = extract(node) + + _trickleDownMax(node: &node, value: &value) + initialize(node, to: value) + } + + @inlinable @inline(__always) + internal func _trickleDownMax(node: inout _HeapNode, value: inout Element) { + // See note on `_HeapNode` in `_trickleDownMin` above. + + var gc0 = node.firstGrandchild() + while gc0.offset &+ 3 < count { + // Invariant: buffer slot at `node` is uninitialized + + // We have four grandchildren, so we don't need to compare children. + let gc1 = _HeapNode(offset: gc0.offset &+ 1, level: gc0.level) + let maxA = maxValue(gc0, gc1) + + let gc2 = _HeapNode(offset: gc0.offset &+ 2, level: gc0.level) + let gc3 = _HeapNode(offset: gc0.offset &+ 3, level: gc0.level) + let maxB = maxValue(gc2, gc3) + + let max = maxValue(maxA, maxB) + guard value < self[max] else { + return // We're done -- `node` is a good place for `value`. + } + + initialize(node, to: extract(max)) + node = max + gc0 = node.firstGrandchild() + + let parent = max.parent() + if value < self[parent] { + swapAt(parent, with: &value) + } + } + + // At this point, we don't have a full complement of grandchildren, but + // we haven't finished sinking the item. + + let c0 = node.leftChild() + if c0.offset >= count { + return // No more descendants to consider. + } + let max = _maxDescendant(c0: c0, gc0: gc0) + guard value < self[max] else { + return // We're done. + } + + initialize(node, to: extract(max)) + node = max + + if max < gc0 { return } + + // If `max` was a grandchild, check the parent. + let parent = max.parent() + if value < self[parent] { + initialize(node, to: extract(parent)) + node = parent + } + } + + /// Returns the node holding the maximal item amongst the children & + /// grandchildren of a node in the tree. The parent node is not specified; + /// instead, this function takes the nodes corresponding to its first child + /// (`c0`) and first grandchild (`gc0`). + /// + /// There must be at least one child, but there must not be a full complement + /// of 4 grandchildren. (Other cases are handled directly above.) + /// + /// This method is an implementation detail of `trickleDownMax`. Do not call + /// it directly. + @inlinable + internal func _maxDescendant(c0: _HeapNode, gc0: _HeapNode) -> _HeapNode { + assert(c0.offset < count) + assert(gc0.offset + 3 >= count) + + if gc0.offset < count { + if gc0.offset &+ 2 < count { + // We have three grandchildren. We don't need to compare direct children. + let gc1 = _HeapNode(offset: gc0.offset &+ 1, level: gc0.level) + let gc2 = _HeapNode(offset: gc0.offset &+ 2, level: gc0.level) + return maxValue(maxValue(gc0, gc1), gc2) + } + + let c1 = _HeapNode(offset: c0.offset &+ 1, level: c0.level) + let m = maxValue(c1, gc0) + if gc0.offset &+ 1 < count { + // Two grandchildren. + let gc1 = _HeapNode(offset: gc0.offset &+ 1, level: gc0.level) + return maxValue(m, gc1) + } + + // One grandchild. + return m + } + + let c1 = _HeapNode(offset: c0.offset &+ 1, level: c0.level) + if c1.offset < count { + return maxValue(c0, c1) + } + + return c0 + } +} + +extension Heap._UnsafeHandle { + @inlinable + internal func heapify() { + // This is Floyd's linear-time heap construction algorithm. + // (https://en.wikipedia.org/wiki/Heapsort#Floyd's_heap_construction). + // + // FIXME: See if a more cache friendly algorithm would be faster. + + let limit = count / 2 // The first offset without a left child + var level = _HeapNode.level(forOffset: limit &- 1) + while level >= 0 { + let nodes = _HeapNode.allNodes(onLevel: level, limit: limit) + _heapify(level, nodes) + level &-= 1 + } + } + + @inlinable + internal func _heapify(_ level: Int, _ nodes: ClosedRange<_HeapNode>?) { + guard let nodes = nodes else { return } + if _HeapNode.isMinLevel(level) { + nodes._forEach { node in + trickleDownMin(node) + } + } else { + nodes._forEach { node in + trickleDownMax(node) + } + } + } +} diff --git a/Sources/HeapModule/Heap.swift b/Sources/HeapModule/Heap.swift new file mode 100644 index 000000000..7c60a6745 --- /dev/null +++ b/Sources/HeapModule/Heap.swift @@ -0,0 +1,373 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + + +/// A container type implementing a double-ended priority queue. +/// `Heap` is a container of `Comparable` elements that provides immediate +/// access to its minimal and maximal members, and supports removing these items +/// or inserting arbitrary new items in (amortized) logarithmic complexity. +/// +/// var queue: Heap = [3, 4, 1, 2] +/// queue.insert(0) +/// print(queue.min) // 0 +/// print(queue.popMax()) // 4 +/// print(queue.max) // 3 +/// +/// `Heap` implements the min-max heap data structure, based on +/// [Atkinson et al. 1986]. +/// +/// [Atkinson et al. 1986]: https://doi.org/10.1145/6617.6621 +/// +/// > M.D. Atkinson, J.-R. Sack, N. Santoro, T. Strothotte. +/// "Min-Max Heaps and Generalized Priority Queues." +/// *Communications of the ACM*, vol. 29, no. 10, Oct. 1986., pp. 996-1000, +/// doi:[10.1145/6617.6621](https://doi.org/10.1145/6617.6621) +/// +/// To efficiently implement these operations, a min-max heap arranges its items +/// into a complete binary tree, maintaining a specific invariant across levels, +/// called the "min-max heap property": each node at an even level in the tree +/// must be less than or equal to all its descendants, while each node at an odd +/// level in the tree must be greater than or equal to all of its descendants. +/// To achieve a compact representation, this tree is stored in breadth-first +/// order inside a single contiguous array value. +/// +/// Unlike most container types, `Heap` doesn't provide a direct way to iterate +/// over the elements it contains -- it isn't a `Sequence` (nor a `Collection`). +/// This is because the order of items in a heap is unspecified and unstable: +/// it may vary between heaps that contain the same set of items, and it may +/// sometimes change in between versions of this library. In particular, the +/// items are (almost) never expected to be in sorted order. +/// +/// For cases where you do need to access the contents of a heap directly and +/// you don't care about their (lack of) order, you can do so by invoking the +/// `unordered` view. This read-only view gives you direct access to the +/// underlying array value: +/// +/// for item in queue.unordered { +/// ... +/// } +/// +/// The name `unordered` highlights the lack of ordering guarantees on the +/// contents, and it helps avoid relying on any particular order. +@frozen +public struct Heap { + @usableFromInline + internal var _storage: ContiguousArray + + /// Creates an empty heap. + @inlinable + public init() { + _storage = [] + } +} + +extension Heap: Sendable where Element: Sendable {} + +extension Heap { + /// A Boolean value indicating whether or not the heap is empty. + /// + /// - Complexity: O(1) + @inlinable @inline(__always) + public var isEmpty: Bool { + _storage.isEmpty + } + + /// The number of elements in the heap. + /// + /// - Complexity: O(1) + @inlinable @inline(__always) + public var count: Int { + _storage.count + } + + /// A read-only view into the underlying array. + /// + /// Note: The elements aren't _arbitrarily_ ordered (it is, after all, a + /// heap). However, no guarantees are given as to the ordering of the elements + /// or that this won't change in future versions of the library. + /// + /// - Complexity: O(1) + @inlinable + public var unordered: [Element] { + Array(_storage) + } + + /// Creates an empty heap with preallocated space for at least the + /// specified number of elements. + /// + /// Use this initializer to avoid intermediate reallocations of a heap's + /// storage when you know in advance how many elements you'll insert into it + /// after creation. + /// + /// - Parameter minimumCapacity: The minimum number of elements that the newly + /// created heap should be able to store without reallocating its storage. + /// + /// - Complexity: O(1) allocations + @inlinable + public init(minimumCapacity: Int) { + self.init() + self.reserveCapacity(minimumCapacity) + } + + /// Reserves enough space to store the specified number of elements. + /// + /// If you are adding a known number of elements to a heap, use this method + /// to avoid multiple reallocations. This method ensures that the heap has + /// unique, mutable, contiguous storage, with space allocated for at least + /// the requested number of elements. + /// + /// For performance reasons, the size of the newly allocated storage might be + /// greater than the requested capacity. + /// + /// - Parameter minimumCapacity: The minimum number of elements that the + /// resulting heap should be able to store without reallocating its storage. + /// + /// - Complexity: O(`count`) + @inlinable + public mutating func reserveCapacity(_ minimumCapacity: Int) { + _storage.reserveCapacity(minimumCapacity) + } + + /// Inserts the given element into the heap. + /// + /// - Complexity: O(log(`count`)) element comparisons + @inlinable + public mutating func insert(_ element: Element) { + _storage.append(element) + + _update { handle in + handle.bubbleUp(_HeapNode(offset: handle.count - 1)) + } + _checkInvariants() + } + + /// Returns the element with the lowest priority, if available. + /// + /// - Complexity: O(1) + @inlinable + public var min: Element? { + _storage.first + } + + /// Returns the element with the highest priority, if available. + /// + /// - Complexity: O(1) + @inlinable + public var max: Element? { + _storage.withUnsafeBufferPointer { buffer in + guard buffer.count > 2 else { + // If count is 0, `last` will return `nil` + // If count is 1, the last (and only) item is the max + // If count is 2, the last item is the max (as it's the only item in the + // first max level) + return buffer.last + } + // We have at least 3 items -- return the larger of the two in the first + // max level + return Swift.max(buffer[1], buffer[2]) + } + } + + /// Removes and returns the element with the lowest priority, if available. + /// + /// - Complexity: O(log(`count`)) element comparisons + @inlinable + public mutating func popMin() -> Element? { + guard _storage.count > 0 else { return nil } + + var removed = _storage.removeLast() + + if _storage.count > 0 { + _update { handle in + let minNode = _HeapNode.root + handle.swapAt(minNode, with: &removed) + handle.trickleDownMin(minNode) + } + } + + _checkInvariants() + return removed + } + + /// Removes and returns the element with the highest priority, if available. + /// + /// - Complexity: O(log(`count`)) element comparisons + @inlinable + public mutating func popMax() -> Element? { + guard _storage.count > 2 else { return _storage.popLast() } + + var removed = _storage.removeLast() + + _update { handle in + if handle.count == 2 { + if handle[.leftMax] > removed { + handle.swapAt(.leftMax, with: &removed) + } + } else { + let maxNode = handle.maxValue(.rightMax, .leftMax) + handle.swapAt(maxNode, with: &removed) + handle.trickleDownMax(maxNode) + } + } + + _checkInvariants() + return removed + } + + /// Removes and returns the element with the lowest priority. + /// + /// The heap *must not* be empty. + /// + /// - Complexity: O(log(`count`)) element comparisons + @inlinable + @discardableResult + public mutating func removeMin() -> Element { + return popMin()! + } + + /// Removes and returns the element with the highest priority. + /// + /// The heap *must not* be empty. + /// + /// - Complexity: O(log(`count`)) element comparisons + @inlinable + @discardableResult + public mutating func removeMax() -> Element { + return popMax()! + } + + /// Replaces the minimum value in the heap with the given replacement, + /// then updates heap contents to reflect the change. + /// + /// The heap must not be empty. + /// + /// - Parameter replacement: The value that is to replace the current + /// minimum value. + /// - Returns: The original minimum value before the replacement. + /// + /// - Complexity: O(log(`count`)) element comparisons + @inlinable + @discardableResult + public mutating func replaceMin(with replacement: Element) -> Element { + precondition(!isEmpty, "No element to replace") + + var removed = replacement + _update { handle in + let minNode = _HeapNode.root + handle.swapAt(minNode, with: &removed) + handle.trickleDownMin(minNode) + } + _checkInvariants() + return removed + } + + /// Replaces the maximum value in the heap with the given replacement, + /// then updates heap contents to reflect the change. + /// + /// The heap must not be empty. + /// + /// - Parameter replacement: The value that is to replace the current maximum + /// value. + /// - Returns: The original maximum value before the replacement. + /// + /// - Complexity: O(log(`count`)) element comparisons + @inlinable + @discardableResult + public mutating func replaceMax(with replacement: Element) -> Element { + precondition(!isEmpty, "No element to replace") + + var removed = replacement + _update { handle in + switch handle.count { + case 1: + handle.swapAt(.root, with: &removed) + case 2: + handle.swapAt(.leftMax, with: &removed) + handle.bubbleUp(.leftMax) + default: + let maxNode = handle.maxValue(.leftMax, .rightMax) + handle.swapAt(maxNode, with: &removed) + handle.bubbleUp(maxNode) // This must happen first + handle.trickleDownMax(maxNode) // Either new element or dethroned min + } + } + _checkInvariants() + return removed + } +} + +// MARK: - + +extension Heap { + /// Initializes a heap from a sequence. + /// + /// - Complexity: O(*n*), where *n* is the number of items in `elements`. + @inlinable + public init(_ elements: some Sequence) { + _storage = ContiguousArray(elements) + guard _storage.count > 1 else { return } + + _update { handle in + handle.heapify() + } + _checkInvariants() + } + + /// Inserts the elements in the given sequence into the heap. + /// + /// - Parameter newElements: The new elements to insert into the heap. + /// + /// - Complexity: O(`count` + *k*), where *k* is the length of `newElements`. + @inlinable + public mutating func insert( + contentsOf newElements: some Sequence + ) { + let origCount = self.count + if origCount == 0 { + self = Self(newElements) + return + } + defer { _checkInvariants() } + _storage.append(contentsOf: newElements) + let newCount = self.count + + guard newCount > origCount, newCount > 1 else { + // If we didn't append, or the result is too small to violate heapness, + // then we have nothing else to dp. + return + } + + // Otherwise we can either insert items one by one, or we can run Floyd's + // algorithm to re-heapify our entire storage from scratch. + // + // If n is the original count, and k is the number of items we need to + // append, then Floyd's costs O(n + k) comparisons/swaps, while + // the naive loop costs k * log(n + k) -- so we expect that Floyd will + // be cheaper whenever k is "large enough" relative to n. + // + // Floyd's algorithm has a worst-case upper complexity bound of 2 * (n + k), + // so one simple heuristic is to use it whenever k * log(n + k) exceeds + // that. + // + // FIXME: Write a benchmark to verify this heuristic. + let heuristicLimit = 2 * newCount / newCount._binaryLogarithm() + let useFloyd = (newCount - origCount) < heuristicLimit + _update { handle in + if useFloyd { + handle.heapify() + } else { + for offset in origCount ..< handle.count { + handle.bubbleUp(_HeapNode(offset: offset)) + } + } + } + } +} diff --git a/Sources/HeapModule/HeapModule.docc/Extensions/Heap.md b/Sources/HeapModule/HeapModule.docc/Extensions/Heap.md new file mode 100644 index 000000000..bc647cdc3 --- /dev/null +++ b/Sources/HeapModule/HeapModule.docc/Extensions/Heap.md @@ -0,0 +1,7 @@ +# ``HeapModule/Heap`` + + + + + +## Topics diff --git a/Sources/HeapModule/HeapModule.docc/HeapModule.md b/Sources/HeapModule/HeapModule.docc/HeapModule.md new file mode 100644 index 000000000..5523f52b8 --- /dev/null +++ b/Sources/HeapModule/HeapModule.docc/HeapModule.md @@ -0,0 +1,13 @@ +# ``HeapModule`` + +Summary + +## Overview + +Text + +## Topics + +### Structures + +- ``Heap`` diff --git a/Sources/HeapModule/_HeapNode.swift b/Sources/HeapModule/_HeapNode.swift new file mode 100644 index 000000000..7556aef15 --- /dev/null +++ b/Sources/HeapModule/_HeapNode.swift @@ -0,0 +1,174 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +@usableFromInline @frozen +internal struct _HeapNode { + @usableFromInline + internal var offset: Int + + @usableFromInline + internal var level: Int + + @inlinable + internal init(offset: Int, level: Int) { + assert(offset >= 0) +#if COLLECTIONS_INTERNAL_CHECKS + assert(level == Self.level(forOffset: offset)) +#endif + self.offset = offset + self.level = level + } + + @inlinable + internal init(offset: Int) { + self.init(offset: offset, level: Self.level(forOffset: offset)) + } +} + +extension _HeapNode: Comparable { + @inlinable @inline(__always) + internal static func ==(left: Self, right: Self) -> Bool { + left.offset == right.offset + } + + @inlinable @inline(__always) + internal static func <(left: Self, right: Self) -> Bool { + left.offset < right.offset + } +} + +extension _HeapNode: CustomStringConvertible { + @usableFromInline + internal var description: String { + "(offset: \(offset), level: \(level))" + } +} + +extension _HeapNode { + @inlinable @inline(__always) + internal static func level(forOffset offset: Int) -> Int { + (offset &+ 1)._binaryLogarithm() + } + + @inlinable @inline(__always) + internal static func firstNode(onLevel level: Int) -> _HeapNode { + assert(level >= 0) + return _HeapNode(offset: (1 &<< level) &- 1, level: level) + } + + @inlinable @inline(__always) + internal static func lastNode(onLevel level: Int) -> _HeapNode { + assert(level >= 0) + return _HeapNode(offset: (1 &<< (level &+ 1)) &- 2, level: level) + } + + @inlinable @inline(__always) + internal static func isMinLevel(_ level: Int) -> Bool { + level & 0b1 == 0 + } +} + +extension _HeapNode { + /// The root node in the heap. + @inlinable @inline(__always) + internal static var root: Self { + Self.init(offset: 0, level: 0) + } + + /// The first max node in the heap. (I.e., the left child of the root.) + @inlinable @inline(__always) + internal static var leftMax: Self { + Self.init(offset: 1, level: 1) + } + + /// The second max node in the heap. (I.e., the right child of the root.) + @inlinable @inline(__always) + internal static var rightMax: Self { + Self.init(offset: 2, level: 1) + } + + @inlinable @inline(__always) + internal var isMinLevel: Bool { + Self.isMinLevel(level) + } + + @inlinable @inline(__always) + internal var isRoot: Bool { + offset == 0 + } +} + +extension _HeapNode { + /// Returns the parent of this index, or `nil` if the index has no parent + /// (i.e. when this is the root index). + @inlinable @inline(__always) + internal func parent() -> Self { + assert(!isRoot) + return Self(offset: (offset &- 1) / 2, level: level &- 1) + } + + /// Returns the grandparent of this index, or `nil` if the index has + /// no grandparent. + @inlinable @inline(__always) + internal func grandParent() -> Self? { + guard offset > 2 else { return nil } + return Self(offset: (offset &- 3) / 4, level: level &- 2) + } + + /// Returns the left child of this node. + @inlinable @inline(__always) + internal func leftChild() -> Self { + Self(offset: offset &* 2 &+ 1, level: level &+ 1) + } + + /// Returns the right child of this node. + @inlinable @inline(__always) + internal func rightChild() -> Self { + Self(offset: offset &* 2 &+ 2, level: level &+ 1) + } + + @inlinable @inline(__always) + internal func firstGrandchild() -> Self { + Self(offset: offset &* 4 &+ 3, level: level &+ 2) + } + + @inlinable @inline(__always) + internal func lastGrandchild() -> Self { + Self(offset: offset &* 4 &+ 6, level: level &+ 2) + } + + @inlinable + internal static func allNodes( + onLevel level: Int, + limit: Int + ) -> ClosedRange? { + let first = Self.firstNode(onLevel: level) + guard first.offset < limit else { return nil } + var last = self.lastNode(onLevel: level) + if last.offset >= limit { + last.offset = limit &- 1 + } + return ClosedRange(uncheckedBounds: (first, last)) + } +} + +extension ClosedRange where Bound == _HeapNode { + @inlinable @inline(__always) + internal func _forEach(_ body: (_HeapNode) -> Void) { + assert( + isEmpty || _HeapNode.level(forOffset: upperBound.offset) == lowerBound.level) + var node = self.lowerBound + while node.offset <= self.upperBound.offset { + body(node) + node.offset &+= 1 + } + } +} diff --git a/Sources/OrderedCollections/CMakeLists.txt b/Sources/OrderedCollections/CMakeLists.txt index 2c0cf8eb2..44706b8f1 100644 --- a/Sources/OrderedCollections/CMakeLists.txt +++ b/Sources/OrderedCollections/CMakeLists.txt @@ -1,7 +1,7 @@ #[[ This source file is part of the Swift Collections Open Source Project -Copyright (c) 2021 Apple Inc. and the Swift project authors +Copyright (c) 2021 - 2024 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 @@ -16,13 +16,12 @@ add_library(OrderedCollections "HashTable/_Hashtable+Header.swift" "HashTable/_HashTable+Testing.swift" "HashTable/_HashTable+UnsafeHandle.swift" - "OrderedDictionary/OrderedDictionary.swift" "OrderedDictionary/OrderedDictionary+Codable.swift" - "OrderedDictionary/OrderedDictionary+CustomDebugStringConvertible.swift" "OrderedDictionary/OrderedDictionary+CustomReflectable.swift" - "OrderedDictionary/OrderedDictionary+CustomStringConvertible.swift" - "OrderedDictionary/OrderedDictionary+Elements+SubSequence.swift" + "OrderedDictionary/OrderedDictionary+Deprecations.swift" + "OrderedDictionary/OrderedDictionary+Descriptions.swift" + "OrderedDictionary/OrderedDictionary+Elements.SubSequence.swift" "OrderedDictionary/OrderedDictionary+Elements.swift" "OrderedDictionary/OrderedDictionary+Equatable.swift" "OrderedDictionary/OrderedDictionary+ExpressibleByDictionaryLiteral.swift" @@ -31,16 +30,13 @@ add_library(OrderedCollections "OrderedDictionary/OrderedDictionary+Invariants.swift" "OrderedDictionary/OrderedDictionary+Partial MutableCollection.swift" "OrderedDictionary/OrderedDictionary+Partial RangeReplaceableCollection.swift" + "OrderedDictionary/OrderedDictionary+Sendable.swift" "OrderedDictionary/OrderedDictionary+Sequence.swift" "OrderedDictionary/OrderedDictionary+Sendable.swift" "OrderedDictionary/OrderedDictionary+Values.swift" - "OrderedDictionary/OrderedDictionary+Deprecations.swift" - - "OrderedSet/OrderedSet.swift" "OrderedSet/OrderedSet+Codable.swift" - "OrderedSet/OrderedSet+CustomDebugStringConvertible.swift" "OrderedSet/OrderedSet+CustomReflectable.swift" - "OrderedSet/OrderedSet+CustomStringConvertible.swift" + "OrderedSet/OrderedSet+Descriptions.swift" "OrderedSet/OrderedSet+Diffing.swift" "OrderedSet/OrderedSet+Equatable.swift" "OrderedSet/OrderedSet+ExpressibleByArrayLiteral.swift" @@ -50,9 +46,21 @@ add_library(OrderedCollections "OrderedSet/OrderedSet+Invariants.swift" "OrderedSet/OrderedSet+Partial MutableCollection.swift" "OrderedSet/OrderedSet+Partial RangeReplaceableCollection.swift" + "OrderedSet/OrderedSet+Partial SetAlgebra formIntersection.swift" + "OrderedSet/OrderedSet+Partial SetAlgebra formSymmetricDifference.swift" + "OrderedSet/OrderedSet+Partial SetAlgebra formUnion.swift" + "OrderedSet/OrderedSet+Partial SetAlgebra intersection.swift" + "OrderedSet/OrderedSet+Partial SetAlgebra isDisjoint.swift" + "OrderedSet/OrderedSet+Partial SetAlgebra isEqualSet.swift" + "OrderedSet/OrderedSet+Partial SetAlgebra isStrictSubset.swift" + "OrderedSet/OrderedSet+Partial SetAlgebra isStrictSuperset.swift" + "OrderedSet/OrderedSet+Partial SetAlgebra isSubset.swift" + "OrderedSet/OrderedSet+Partial SetAlgebra isSuperset.swift" + "OrderedSet/OrderedSet+Partial SetAlgebra subtract.swift" + "OrderedSet/OrderedSet+Partial SetAlgebra subtracting.swift" + "OrderedSet/OrderedSet+Partial SetAlgebra symmetricDifference.swift" + "OrderedSet/OrderedSet+Partial SetAlgebra union.swift" "OrderedSet/OrderedSet+Partial SetAlgebra+Basics.swift" - "OrderedSet/OrderedSet+Partial SetAlgebra+Operations.swift" - "OrderedSet/OrderedSet+Partial SetAlgebra+Predicates.swift" "OrderedSet/OrderedSet+RandomAccessCollection.swift" "OrderedSet/OrderedSet+ReserveCapacity.swift" "OrderedSet/OrderedSet+Sendable.swift" @@ -60,9 +68,11 @@ add_library(OrderedCollections "OrderedSet/OrderedSet+Testing.swift" "OrderedSet/OrderedSet+UnorderedView.swift" "OrderedSet/OrderedSet+UnstableInternals.swift" - - "Utilities/_UnsafeBitset.swift" - "Utilities/RandomAccessCollection+Offsets.swift") + "OrderedSet/OrderedSet.swift" + "Utilities/_UnsafeBitSet.swift" + ) +target_link_libraries(OrderedCollections PRIVATE + _CollectionsUtilities) set_target_properties(OrderedCollections PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) diff --git a/Sources/OrderedCollections/HashTable/_HashTable+Bucket.swift b/Sources/OrderedCollections/HashTable/_HashTable+Bucket.swift index b912fe831..203a06062 100644 --- a/Sources/OrderedCollections/HashTable/_HashTable+Bucket.swift +++ b/Sources/OrderedCollections/HashTable/_HashTable+Bucket.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 diff --git a/Sources/OrderedCollections/HashTable/_HashTable+BucketIterator.swift b/Sources/OrderedCollections/HashTable/_HashTable+BucketIterator.swift index c4a62485e..6ed5165d7 100644 --- a/Sources/OrderedCollections/HashTable/_HashTable+BucketIterator.swift +++ b/Sources/OrderedCollections/HashTable/_HashTable+BucketIterator.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 @@ -14,7 +14,7 @@ extension _HashTable { /// table. This is a convenient tool for implementing linear probing. /// /// Beyond merely providing bucket values, bucket iterators can also tell - /// you their current oposition within the hash table, and (for mutable hash + /// you their current opposition within the hash table, and (for mutable hash /// tables) they allow you update the value of the currently visited bucket. /// (This is useful when implementing simple insertions, for example.) /// diff --git a/Sources/OrderedCollections/HashTable/_HashTable+Constants.swift b/Sources/OrderedCollections/HashTable/_HashTable+Constants.swift index 84ef2dd58..49fcf935d 100644 --- a/Sources/OrderedCollections/HashTable/_HashTable+Constants.swift +++ b/Sources/OrderedCollections/HashTable/_HashTable+Constants.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 diff --git a/Sources/OrderedCollections/HashTable/_HashTable+CustomStringConvertible.swift b/Sources/OrderedCollections/HashTable/_HashTable+CustomStringConvertible.swift index 2c4663124..68904713a 100644 --- a/Sources/OrderedCollections/HashTable/_HashTable+CustomStringConvertible.swift +++ b/Sources/OrderedCollections/HashTable/_HashTable+CustomStringConvertible.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 diff --git a/Sources/OrderedCollections/HashTable/_HashTable+Testing.swift b/Sources/OrderedCollections/HashTable/_HashTable+Testing.swift index f27006fa1..f552352ee 100644 --- a/Sources/OrderedCollections/HashTable/_HashTable+Testing.swift +++ b/Sources/OrderedCollections/HashTable/_HashTable+Testing.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 @@ -10,6 +10,7 @@ //===----------------------------------------------------------------------===// extension _HashTable.Bucket: CustomStringConvertible { + // A textual representation of this instance. public var description: String { "Bucket(@\(offset))"} } diff --git a/Sources/OrderedCollections/HashTable/_HashTable+UnsafeHandle.swift b/Sources/OrderedCollections/HashTable/_HashTable+UnsafeHandle.swift index d2cb19576..40d833154 100644 --- a/Sources/OrderedCollections/HashTable/_HashTable+UnsafeHandle.swift +++ b/Sources/OrderedCollections/HashTable/_HashTable+UnsafeHandle.swift @@ -2,13 +2,17 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 // //===----------------------------------------------------------------------===// +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + @usableFromInline internal typealias _UnsafeHashTable = _HashTable.UnsafeHandle @@ -484,11 +488,7 @@ extension _UnsafeHashTable { @usableFromInline internal func clear() { assertMutable() - #if swift(>=5.8) _buckets.update(repeating: 0, count: wordCount) - #else - _buckets.assign(repeating: 0, count: wordCount) - #endif } } diff --git a/Sources/OrderedCollections/HashTable/_HashTable.swift b/Sources/OrderedCollections/HashTable/_HashTable.swift index 319a65ff5..0f3bf5e12 100644 --- a/Sources/OrderedCollections/HashTable/_HashTable.swift +++ b/Sources/OrderedCollections/HashTable/_HashTable.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 diff --git a/Sources/OrderedCollections/HashTable/_Hashtable+Header.swift b/Sources/OrderedCollections/HashTable/_Hashtable+Header.swift index 93ceb2467..7ad708965 100644 --- a/Sources/OrderedCollections/HashTable/_Hashtable+Header.swift +++ b/Sources/OrderedCollections/HashTable/_Hashtable+Header.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 diff --git a/Sources/OrderedCollections/OrderedCollections.docc/Extensions/OrderedSet.UnorderedView.md b/Sources/OrderedCollections/OrderedCollections.docc/Extensions/OrderedSet.UnorderedView.md index ebc2c8892..659d1eb69 100644 --- a/Sources/OrderedCollections/OrderedCollections.docc/Extensions/OrderedSet.UnorderedView.md +++ b/Sources/OrderedCollections/OrderedCollections.docc/Extensions/OrderedSet.UnorderedView.md @@ -1,5 +1,9 @@ # ``OrderedCollections/OrderedSet/UnorderedView`` + + + + ## Topics ### Binary Set Operations @@ -31,6 +35,8 @@ ### Binary Set Predicates - ``==(_:_:)`` +- ``isEqualSet(to:)-1szq`` +- ``isEqualSet(to:)-9djqq`` - ``isSubset(of:)-2dx31`` - ``isSubset(of:)-801lo`` diff --git a/Sources/OrderedCollections/OrderedCollections.docc/Extensions/OrderedSet.md b/Sources/OrderedCollections/OrderedCollections.docc/Extensions/OrderedSet.md index eac28040b..a7d389944 100644 --- a/Sources/OrderedCollections/OrderedCollections.docc/Extensions/OrderedSet.md +++ b/Sources/OrderedCollections/OrderedCollections.docc/Extensions/OrderedSet.md @@ -1,5 +1,9 @@ # ``OrderedCollections/OrderedSet`` + + + + ## Topics ### Creating a Set @@ -30,8 +34,8 @@ ### Adding and Updating Elements - ``append(_:)`` -- ``insert(_:at:)`` - ``append(contentsOf:)`` +- ``insert(_:at:)`` - ``updateOrAppend(_:)`` - ``updateOrInsert(_:at:)`` - ``update(_:at:)`` @@ -87,6 +91,9 @@ ### Comparing Sets - ``==(_:_:)`` +- ``isEqualSet(to:)-6zqj7`` +- ``isEqualSet(to:)-34yz0`` +- ``isEqualSet(to:)-2bhxr`` - ``isSubset(of:)-ptij`` - ``isSubset(of:)-3mw6r`` diff --git a/Sources/OrderedCollections/OrderedCollections.docc/OrderedCollections.md b/Sources/OrderedCollections/OrderedCollections.docc/OrderedCollections.md index dcc7288a0..a15dedd92 100644 --- a/Sources/OrderedCollections/OrderedCollections.docc/OrderedCollections.md +++ b/Sources/OrderedCollections/OrderedCollections.docc/OrderedCollections.md @@ -1,6 +1,16 @@ # ``OrderedCollections`` -The `OrderedCollections` module provides hashed collection types that work like the standard `Set` and `Dictionary`, but maintain their elements in a particular user-specified order. +**Swift Collections** is an open-source package of data structure implementations for the Swift programming language. + +## Overview + + + +#### Additional Resources + +- [`Swift Collections` on GitHub](https://github.com/apple/swift-collections/) +- [`Swift Collections` on the Swift Forums](https://forums.swift.org/c/related-projects/collections/72) + ## Topics diff --git a/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Codable.swift b/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Codable.swift index 61a9808cf..af8c7eda0 100644 --- a/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Codable.swift +++ b/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Codable.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 diff --git a/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+CustomDebugStringConvertible.swift b/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+CustomDebugStringConvertible.swift deleted file mode 100644 index 4bc122a30..000000000 --- a/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+CustomDebugStringConvertible.swift +++ /dev/null @@ -1,44 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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 OrderedDictionary: CustomDebugStringConvertible { - /// A textual representation of this instance, suitable for debugging. - public var debugDescription: String { - _debugDescription(typeName: _debugTypeName()) - } - - internal func _debugTypeName() -> String { - "OrderedDictionary<\(Key.self), \(Value.self)>" - } - - internal func _debugDescription(typeName: String) -> String { - var result = "\(typeName)(" - if isEmpty { - result += "[:]" - } else { - result += "[" - var first = true - for (key, value) in self { - if first { - first = false - } else { - result += ", " - } - debugPrint(key, terminator: "", to: &result) - result += ": " - debugPrint(value, terminator: "", to: &result) - } - result += "]" - } - result += ")" - return result - } -} diff --git a/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+CustomReflectable.swift b/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+CustomReflectable.swift index 82a5d37cf..04296d763 100644 --- a/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+CustomReflectable.swift +++ b/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+CustomReflectable.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 diff --git a/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Deprecations.swift b/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Deprecations.swift index a8e531d8e..697760dee 100644 --- a/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Deprecations.swift +++ b/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Deprecations.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 diff --git a/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+CustomStringConvertible.swift b/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Descriptions.swift similarity index 57% rename from Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+CustomStringConvertible.swift rename to Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Descriptions.swift index eb40ebab1..6caad1406 100644 --- a/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+CustomStringConvertible.swift +++ b/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Descriptions.swift @@ -2,28 +2,27 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 // //===----------------------------------------------------------------------===// +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + extension OrderedDictionary: CustomStringConvertible { /// A textual representation of this instance. public var description: String { - if isEmpty { return "[:]" } - var result = "[" - var first = true - for (key, value) in self { - if first { - first = false - } else { - result += ", " - } - result += "\(key): \(value)" - } - result += "]" - return result + _dictionaryDescription(for: self.elements) + } +} + +extension OrderedDictionary: CustomDebugStringConvertible { + /// A textual representation of this instance, suitable for debugging. + public var debugDescription: String { + description } } diff --git a/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Elements+SubSequence.swift b/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Elements.SubSequence.swift similarity index 95% rename from Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Elements+SubSequence.swift rename to Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Elements.SubSequence.swift index 8bf05eec6..18a8172a0 100644 --- a/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Elements+SubSequence.swift +++ b/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Elements.SubSequence.swift @@ -2,13 +2,17 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021-2023 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 // //===----------------------------------------------------------------------===// +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + extension OrderedDictionary.Elements { /// A collection that represents a contiguous slice of an ordered dictionary. /// @@ -30,10 +34,22 @@ extension OrderedDictionary.Elements { } } -#if swift(>=5.5) extension OrderedDictionary.Elements.SubSequence: Sendable where Key: Sendable, Value: Sendable {} -#endif + +extension OrderedDictionary.Elements.SubSequence: CustomStringConvertible { + // A textual representation of this instance. + public var description: String { + _dictionaryDescription(for: self) + } +} + +extension OrderedDictionary.Elements.SubSequence: CustomDebugStringConvertible { + /// A textual representation of this instance, suitable for debugging. + public var debugDescription: String { + description + } +} extension OrderedDictionary.Elements.SubSequence { /// A read-only collection view containing the keys in this slice. @@ -128,10 +144,8 @@ extension OrderedDictionary.Elements.SubSequence: Sequence { } } -#if swift(>=5.5) extension OrderedDictionary.Elements.SubSequence.Iterator: Sendable where Key: Sendable, Value: Sendable {} -#endif extension OrderedDictionary.Elements.SubSequence: RandomAccessCollection { /// The index type for an ordered dictionary: `Int`. diff --git a/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Elements.swift b/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Elements.swift index 62538f933..e4cb08a69 100644 --- a/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Elements.swift +++ b/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Elements.swift @@ -2,13 +2,17 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021-2023 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 // //===----------------------------------------------------------------------===// +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + extension OrderedDictionary { /// A view of the contents of an ordered dictionary as a random-access /// collection. @@ -25,10 +29,8 @@ extension OrderedDictionary { } } -#if swift(>=5.5) extension OrderedDictionary.Elements: Sendable where Key: Sendable, Value: Sendable {} -#endif extension OrderedDictionary { /// A view of the contents of this dictionary as a random-access collection. @@ -332,15 +334,16 @@ extension OrderedDictionary.Elements: RandomAccessCollection { } extension OrderedDictionary.Elements: CustomStringConvertible { + // A textual representation of this instance. public var description: String { _base.description } } extension OrderedDictionary.Elements: CustomDebugStringConvertible { + /// A textual representation of this instance, suitable for debugging. public var debugDescription: String { - _base._debugDescription( - typeName: "OrderedDictionary<\(Key.self), \(Value.self)>.Elements") + description } } @@ -508,8 +511,8 @@ extension OrderedDictionary.Elements { /// change when your program is compiled using a different version of /// Swift. @inlinable - public mutating func shuffle( - using generator: inout T + public mutating func shuffle( + using generator: inout some RandomNumberGenerator ) { _base.shuffle(using: &generator) } @@ -582,9 +585,9 @@ extension OrderedDictionary.Elements { /// /// - Complexity: O(`count`) @inlinable - public mutating func removeSubrange( - _ bounds: R - ) where R.Bound == Int { + public mutating func removeSubrange( + _ bounds: some RangeExpression + ) { _base.removeSubrange(bounds) } diff --git a/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Equatable.swift b/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Equatable.swift index 93ed08c63..40d29310c 100644 --- a/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Equatable.swift +++ b/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Equatable.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 diff --git a/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+ExpressibleByDictionaryLiteral.swift b/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+ExpressibleByDictionaryLiteral.swift index 8c1ed0396..d32835900 100644 --- a/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+ExpressibleByDictionaryLiteral.swift +++ b/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+ExpressibleByDictionaryLiteral.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 diff --git a/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Hashable.swift b/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Hashable.swift index 40375df8a..1ac8972cf 100644 --- a/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Hashable.swift +++ b/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Hashable.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 diff --git a/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Initializers.swift b/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Initializers.swift index 93352ef71..6fbfbc4fe 100644 --- a/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Initializers.swift +++ b/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Initializers.swift @@ -2,13 +2,17 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 // //===----------------------------------------------------------------------===// +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + extension OrderedDictionary { /// Creates an empty dictionary. /// @@ -77,10 +81,12 @@ extension OrderedDictionary { /// key-value pairs, if `Key` implements high-quality hashing. @_disfavoredOverload // https://github.com/apple/swift-collections/issues/125 @inlinable - public init( - uniqueKeysWithValues keysAndValues: S - ) where S.Element == (key: Key, value: Value) { - if S.self == Dictionary.self { + public init( + uniqueKeysWithValues keysAndValues: some Sequence<(key: Key, value: Value)> + ) { + if let keysAndValues = _specialize( + keysAndValues, for: Dictionary.self + ) { self.init(_uncheckedUniqueKeysWithValues: keysAndValues) return } @@ -113,9 +119,9 @@ extension OrderedDictionary { /// - Complexity: Expected O(*n*) on average, where *n* is the count if /// key-value pairs, if `Key` implements high-quality hashing. @inlinable - public init( - uniqueKeysWithValues keysAndValues: S - ) where S.Element == (Key, Value) { + public init( + uniqueKeysWithValues keysAndValues: some Sequence<(Key, Value)> + ) { self.init() reserveCapacity(keysAndValues.underestimatedCount) for (key, value) in keysAndValues { @@ -148,10 +154,10 @@ extension OrderedDictionary { /// - Complexity: Expected O(*n*) on average, where *n* is the count if /// key-value pairs, if `Key` implements high-quality hashing. @inlinable - public init( - uniqueKeys keys: Keys, - values: Values - ) where Keys.Element == Key, Values.Element == Value { + public init( + uniqueKeys keys: some Sequence, + values: some Sequence + ) { let keys = ContiguousArray(keys) let values = ContiguousArray(values) precondition(keys.count == values.count, @@ -199,10 +205,10 @@ extension OrderedDictionary { @_disfavoredOverload // https://github.com/apple/swift-collections/issues/125 @inlinable @inline(__always) - public init( - _ keysAndValues: S, + public init( + _ keysAndValues: some Sequence<(key: Key, value: Value)>, uniquingKeysWith combine: (Value, Value) throws -> Value - ) rethrows where S.Element == (key: Key, value: Value) { + ) rethrows { self.init() try self.merge(keysAndValues, uniquingKeysWith: combine) } @@ -241,10 +247,10 @@ extension OrderedDictionary { /// key-value pairs, if `Key` implements high-quality hashing. @inlinable @inline(__always) - public init( - _ keysAndValues: S, + public init( + _ keysAndValues: some Sequence<(Key, Value)>, uniquingKeysWith combine: (Value, Value) throws -> Value - ) rethrows where S.Element == (Key, Value) { + ) rethrows { self.init() try self.merge(keysAndValues, uniquingKeysWith: combine) } @@ -337,9 +343,10 @@ extension OrderedDictionary { extension OrderedDictionary { @inlinable - internal init( - _uncheckedUniqueKeysWithValues keysAndValues: S - ) where S.Element == (key: Key, value: Value) { + internal init( + _uncheckedUniqueKeysWithValues keysAndValues: + some Sequence<(key: Key, value: Value)> + ) { self.init() reserveCapacity(keysAndValues.underestimatedCount) for (key, value) in keysAndValues { @@ -372,9 +379,10 @@ extension OrderedDictionary { /// key-value pairs, if `Key` implements high-quality hashing. @_disfavoredOverload // https://github.com/apple/swift-collections/issues/125 @inlinable - public init( - uncheckedUniqueKeysWithValues keysAndValues: S - ) where S.Element == (key: Key, value: Value) { + public init( + uncheckedUniqueKeysWithValues keysAndValues: + some Sequence<(key: Key, value: Value)> + ) { #if DEBUG self.init(uniqueKeysWithValues: keysAndValues) #else @@ -404,9 +412,9 @@ extension OrderedDictionary { /// - Complexity: Expected O(*n*) on average, where *n* is the count if /// key-value pairs, if `Key` implements high-quality hashing. @inlinable - public init( - uncheckedUniqueKeysWithValues keysAndValues: S - ) where S.Element == (Key, Value) { + public init( + uncheckedUniqueKeysWithValues keysAndValues: some Sequence<(Key, Value)> + ) { // Add tuple labels let keysAndValues = keysAndValues.lazy.map { (key: $0.0, value: $0.1) } self.init(uncheckedUniqueKeysWithValues: keysAndValues) @@ -439,10 +447,10 @@ extension OrderedDictionary { /// key-value pairs, if `Key` implements high-quality hashing. @inlinable @inline(__always) - public init( - uncheckedUniqueKeys keys: Keys, - values: Values - ) where Keys.Element == Key, Values.Element == Value { + public init( + uncheckedUniqueKeys keys: some Sequence, + values: some Sequence + ) { #if DEBUG self.init(uniqueKeys: keys, values: values) #else diff --git a/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Invariants.swift b/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Invariants.swift index fa3d01fe0..da0d6f534 100644 --- a/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Invariants.swift +++ b/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Invariants.swift @@ -2,14 +2,28 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 // //===----------------------------------------------------------------------===// +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + extension OrderedDictionary { + /// True if consistency checking is enabled in the implementation of this + /// type, false otherwise. + /// + /// Documented performance promises are null and void when this property + /// returns true -- for example, operations that are documented to take + /// O(1) time might take O(*n*) time, or worse. + public static var _isConsistencyCheckingEnabled: Bool { + _isCollectionsInternalCheckingEnabled + } + #if COLLECTIONS_INTERNAL_CHECKS @inline(never) @_effects(releasenone) public func _checkInvariants() { diff --git a/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Partial MutableCollection.swift b/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Partial MutableCollection.swift index 35970a7f3..9350259db 100644 --- a/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Partial MutableCollection.swift +++ b/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Partial MutableCollection.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 @@ -160,8 +160,8 @@ extension OrderedDictionary { /// change when your program is compiled using a different version of /// Swift. @inlinable - public mutating func shuffle( - using generator: inout T + public mutating func shuffle( + using generator: inout some RandomNumberGenerator ) { guard count > 1 else { return } var keys = self._keys.elements diff --git a/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Partial RangeReplaceableCollection.swift b/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Partial RangeReplaceableCollection.swift index bc5791bab..603319bfd 100644 --- a/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Partial RangeReplaceableCollection.swift +++ b/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Partial RangeReplaceableCollection.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 @@ -48,7 +48,7 @@ extension OrderedDictionary { _values.removeAll(keepingCapacity: keepCapacity) } - /// Removes and returns the element at the specified position. + /// Removes and returns the key-value pair at the specified index. /// /// All the elements following the specified position are moved to close the /// resulting gap. @@ -93,9 +93,9 @@ extension OrderedDictionary { /// /// - Complexity: O(`count`) @inlinable - public mutating func removeSubrange( - _ bounds: R - ) where R.Bound == Int { + public mutating func removeSubrange( + _ bounds: some RangeExpression + ) { removeSubrange(bounds.relative(to: elements)) } diff --git a/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Sendable.swift b/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Sendable.swift index dbf372323..f0582b605 100644 --- a/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Sendable.swift +++ b/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Sendable.swift @@ -2,14 +2,12 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2022-2023 Apple Inc. and the Swift project authors +// Copyright (c) 2022 - 2024 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 // //===----------------------------------------------------------------------===// -#if swift(>=5.5) extension OrderedDictionary: @unchecked Sendable where Key: Sendable, Value: Sendable {} -#endif diff --git a/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Sequence.swift b/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Sequence.swift index a6dd2324c..bb79aa780 100644 --- a/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Sequence.swift +++ b/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Sequence.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021-2023 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 @@ -62,7 +62,5 @@ extension OrderedDictionary: Sequence { } } -#if swift(>=5.5) extension OrderedDictionary.Iterator: Sendable where Key: Sendable, Value: Sendable {} -#endif diff --git a/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Values.swift b/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Values.swift index 6a63fbdb9..2964d9836 100644 --- a/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Values.swift +++ b/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary+Values.swift @@ -2,13 +2,17 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021-2023 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 // //===----------------------------------------------------------------------===// +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + extension OrderedDictionary { /// A view of an ordered dictionary's values as a standalone collection. @frozen @@ -24,10 +28,22 @@ extension OrderedDictionary { } } -#if swift(>=5.5) extension OrderedDictionary.Values: Sendable where Key: Sendable, Value: Sendable {} -#endif + +extension OrderedDictionary.Values: CustomStringConvertible { + // A textual representation of this instance. + public var description: String { + _arrayDescription(for: self) + } +} + +extension OrderedDictionary.Values: CustomDebugStringConvertible { + /// A textual representation of this instance, suitable for debugging. + public var debugDescription: String { + description + } +} extension OrderedDictionary.Values { /// A read-only view of the contents of this collection as an array value. diff --git a/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary.swift b/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary.swift index 6095dfa6f..2f13e942a 100644 --- a/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary.swift +++ b/Sources/OrderedCollections/OrderedDictionary/OrderedDictionary.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 @@ -79,7 +79,7 @@ /// /// If the `Value` type implements reference semantics, or when you need to /// perform a series of individual mutations on the values, the closure-based -/// `updateValue(forKey:default:_:)` method provides an easier-to-use +/// ``updateValue(forKey:default:with:)`` method provides an easier-to-use /// alternative to the defaulted key-based subscript. /// /// let text = "short string" @@ -99,8 +99,8 @@ /// ``init(grouping:by:)-6mahw``), methods for merging one dictionary with /// another (``merge(_:uniquingKeysWith:)-6ka2i``, /// ``merging(_:uniquingKeysWith:)-4z49c``), filtering dictionary entries -/// (``filter(_:)``), transforming values (``mapValues(_:)``), -/// and a combination of these two (``compactMapValues(_:)``). +/// (``filter(_:)``), transforming values (``mapValues(_:)``), and a combination +/// of these two (``compactMapValues(_:)``). /// /// ### Sequence and Collection Operations /// @@ -115,11 +115,26 @@ /// responses.elements[0] // `(200, "OK")` (index-based subscript) /// /// Because ordered dictionaries need to maintain unique keys, neither -/// `OrderedDictionary` nor its ``elements-swift.property`` view can conform to -/// the full `MutableCollection` or `RangeReplaceableCollection` protocols. -/// However, they are able to partially implement requirements: they support -/// mutations that merely change the order of elements, or just remove a subset -/// of existing members. +/// `OrderedDictionary` nor its `elements` view can conform to the full +/// `MutableCollection` or `RangeReplaceableCollection` protocols. +/// However, `OrderedDictioanr` is still able to implement some of the +/// requirements of these protocols. In particular, it supports permutation +/// operations from `MutableCollection`: +/// +/// - ``swapAt(_:_:)`` +/// - ``partition(by:)`` +/// - ``sort()``, ``sort(by:)`` +/// - ``shuffle()``, ``shuffle(using:)`` +/// - ``reverse()`` +/// +/// It also supports removal operations from `RangeReplaceableCollection`: +/// +/// - ``removeAll(keepingCapacity:)`` +/// - ``remove(at:)`` +/// - ``removeSubrange(_:)-512n3``, ``removeSubrange(_:)-8rmzx`` +/// - ``removeLast()``, ``removeLast(_:)`` +/// - ``removeFirst()``, ``removeFirst(_:)`` +/// - ``removeAll(where:)`` /// /// `OrderedDictionary` also implements ``reserveCapacity(_:)`` from /// `RangeReplaceableCollection`, to allow for efficient insertion of a known @@ -132,8 +147,8 @@ /// ``values-swift.property`` properties that provide lightweight views into /// the corresponding parts of the dictionary. /// -/// The ``keys`` collection is of type `OrderedSet`, containing all the -/// keys in the original dictionary. +/// The ``keys`` collection is of type ``OrderedSet``, containing all the keys +/// in the original dictionary. /// /// let d: OrderedDictionary = [2: "two", 1: "one", 0: "zero"] /// d.keys // [2, 1, 0] as OrderedSet @@ -143,8 +158,8 @@ /// copied out and then mutated if desired. (Such mutations won't affect the /// original dictionary value.) /// -/// The ``values-swift.property`` property returns a mutable random-access -/// collection of the values in the dictionary: +/// The ``values-swift.property`` collection is a mutable random-access +/// ordered collection of the values in the dictionary: /// /// d.values // "two", "one", "zero" /// d.values[2] = "nada" @@ -153,40 +168,37 @@ /// // `d` is now [2: "nada", 1: "one", 0: "two"] /// /// Both views store their contents in regular `Array` values, accessible -/// through their own `elements` property. +/// through their ``elements-swift.property`` property. /// /// ## Performance /// -/// Like the standard `Dictionary` type, the performance of hashing operations -/// in `OrderedDictionary` is highly sensitive to the quality of hashing -/// implemented by the `Key` type. Failing to correctly implement hashing can -/// easily lead to unacceptable performance, with the severity of the effect -/// increasing with the size of the hash table. -/// -/// In particular, if a certain set of keys all produce the same hash value, -/// then hash table lookups regress to searching an element in an unsorted -/// array, i.e., a linear operation. To ensure hashed collection types exhibit -/// their target performance, it is important to ensure that such collisions -/// cannot be induced merely by adding a particular list of keys to the -/// dictionary. -/// -/// The easiest way to achieve this is to make sure `Key` implements hashing -/// following `Hashable`'s documented best practices. The conformance must -/// implement the `hash(into:)` requirement, and every bit of information that -/// is compared in `==` needs to be combined into the supplied `Hasher` value. -/// When used correctly, `Hasher` produces high-quality, randomly seeded hash -/// values that prevent repeatable hash collisions. +/// An ordered dictionary consists of an ``OrderedSet`` of keys, alongside a +/// regular `Array` value that contains their associated values. +/// The performance characteristics of `OrderedDictionary` are mostly dictated +/// by this setup. /// -/// When `Key` correctly conforms to `Hashable`, key-based lookups in an ordered -/// dictionary is expected to take O(1) equality checks on average. Hash -/// collisions can still occur organically, so the worst-case lookup performance -/// is technically still O(*n*) (where *n* is the size of the dictionary); -/// however, long lookup chains are unlikely to occur in practice. +/// - Looking up a member in an ordered dictionary is expected to execute +/// a constant number of hashing and equality check operations, just like +/// the standard `Dictionary`. +/// - `OrderedDictionary` is also able to append new items at the end of the +/// dictionary with an expected amortized complexity of O(1), similar to +/// inserting new items into `Dictionary`. +/// - Unfortunately, removing or inserting items at the start or middle of an +/// `OrderedDictionary` has linear complexity, making these significantly +/// slower than `Dictionary`. +/// - Storing keys and values outside of the hash table makes +/// `OrderedDictionary` more memory efficient than most alternative +/// ordered dictionary representations. It can sometimes also be more memory +/// efficient than the standard `Dictionary`, despote the additional +/// functionality of preserving element ordering. /// -/// ## Implementation Details +/// Like all hashed data structures, ordered dictionaries are extremely +/// sensitive to the quality of the `Key` type's `Hashable` conformance. +/// All complexity guarantees are null and void if `Key` implements `Hashable` +/// incorrectly. /// -/// An ordered dictionary consists of an ordered set of keys, alongside a -/// regular `Array` value that contains their associated values. +/// See ``OrderedSet`` for a more detailed discussion of these performance +/// characteristics. @frozen public struct OrderedDictionary { @usableFromInline @@ -207,7 +219,7 @@ public struct OrderedDictionary { } extension OrderedDictionary { - /// A read-only collection view for the keys contained in this dictionary, as + /// A read-only ordered collection view for the keys contained in this dictionary, as /// an `OrderedSet`. /// /// - Complexity: O(1) @@ -215,7 +227,7 @@ extension OrderedDictionary { @inline(__always) public var keys: OrderedSet { _keys } - /// A mutable collection view containing the values in this dictionary. + /// A mutable collection view containing the ordered values in this dictionary. /// /// - Complexity: O(1) @inlinable @@ -436,7 +448,7 @@ extension OrderedDictionary { /// of each letter in a string: /// /// let message = "Hello, Elle!" - /// var letterCounts: [Character: Int] = [:] + /// var letterCounts: OrderedDictionary = [:] /// for letter in message { /// letterCounts[letter, default: 0] += 1 /// } @@ -647,7 +659,7 @@ extension OrderedDictionary { /// in a string: /// /// let message = "Hello, Elle!" - /// var letterCounts: [Character: Int] = [:] + /// var letterCounts: OrderedDictionary = [:] /// for letter in message { /// letterCounts.updateValue(forKey: letter, default: 0) { count in /// count += 1 @@ -807,10 +819,10 @@ extension OrderedDictionary { /// elements in `keysAndValues`, if `Key` implements high-quality hashing. @_disfavoredOverload // https://github.com/apple/swift-collections/issues/125 @inlinable - public mutating func merge( - _ keysAndValues: __owned S, + public mutating func merge( + _ keysAndValues: __owned some Sequence<(key: Key, value: Value)>, uniquingKeysWith combine: (Value, Value) throws -> Value - ) rethrows where S.Element == (key: Key, value: Value) { + ) rethrows { for (key, value) in keysAndValues { let (index, bucket) = _keys._find(key) if let index = index { @@ -857,10 +869,10 @@ extension OrderedDictionary { /// - Complexity: Expected to be O(*n*) on average, where *n* is the number of /// elements in `keysAndValues`, if `Key` implements high-quality hashing. @inlinable - public mutating func merge( - _ keysAndValues: __owned S, + public mutating func merge( + _ keysAndValues: __owned some Sequence<(Key, Value)>, uniquingKeysWith combine: (Value, Value) throws -> Value - ) rethrows where S.Element == (Key, Value) { + ) rethrows { let mapped: LazyMapSequence = keysAndValues.lazy.map { (key: $0.0, value: $0.1) } try merge(mapped, uniquingKeysWith: combine) @@ -903,10 +915,10 @@ extension OrderedDictionary { /// hashing. @_disfavoredOverload // https://github.com/apple/swift-collections/issues/125 @inlinable - public __consuming func merging( - _ other: __owned S, + public __consuming func merging( + _ other: __owned some Sequence<(key: Key, value: Value)>, uniquingKeysWith combine: (Value, Value) throws -> Value - ) rethrows -> Self where S.Element == (key: Key, value: Value) { + ) rethrows -> Self { var copy = self try copy.merge(other, uniquingKeysWith: combine) return copy @@ -948,10 +960,10 @@ extension OrderedDictionary { /// number of elements in `keysAndValues`, if `Key` implements high-quality /// hashing. @inlinable - public __consuming func merging( - _ other: __owned S, + public __consuming func merging( + _ other: __owned some Sequence<(Key, Value)>, uniquingKeysWith combine: (Value, Value) throws -> Value - ) rethrows -> Self where S.Element == (Key, Value) { + ) rethrows -> Self { var copy = self try copy.merge(other, uniquingKeysWith: combine) return copy diff --git a/Sources/OrderedCollections/OrderedSet/OrderedSet+Codable.swift b/Sources/OrderedCollections/OrderedSet/OrderedSet+Codable.swift index 365d225f9..e0999c082 100644 --- a/Sources/OrderedCollections/OrderedSet/OrderedSet+Codable.swift +++ b/Sources/OrderedCollections/OrderedSet/OrderedSet+Codable.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 diff --git a/Sources/OrderedCollections/OrderedSet/OrderedSet+CustomDebugStringConvertible.swift b/Sources/OrderedCollections/OrderedSet/OrderedSet+CustomDebugStringConvertible.swift deleted file mode 100644 index effbe7d3a..000000000 --- a/Sources/OrderedCollections/OrderedSet/OrderedSet+CustomDebugStringConvertible.swift +++ /dev/null @@ -1,36 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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 OrderedSet: CustomDebugStringConvertible { - /// A textual representation of this instance, suitable for debugging. - public var debugDescription: String { - _debugDescription(typeName: _debugTypeName()) - } - - internal func _debugTypeName() -> String { - "OrderedSet<\(Element.self)>" - } - - internal func _debugDescription(typeName: String) -> String { - var result = "\(typeName)([" - var first = true - for item in self { - if first { - first = false - } else { - result += ", " - } - debugPrint(item, terminator: "", to: &result) - } - result += "])" - return result - } -} diff --git a/Sources/OrderedCollections/OrderedSet/OrderedSet+CustomReflectable.swift b/Sources/OrderedCollections/OrderedSet/OrderedSet+CustomReflectable.swift index 8b0546285..8eb8415b2 100644 --- a/Sources/OrderedCollections/OrderedSet/OrderedSet+CustomReflectable.swift +++ b/Sources/OrderedCollections/OrderedSet/OrderedSet+CustomReflectable.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 @@ -12,6 +12,6 @@ extension OrderedSet: CustomReflectable { /// The custom mirror for this instance. public var customMirror: Mirror { - Mirror(self, unlabeledChildren: _elements, displayStyle: .collection) + Mirror(self, unlabeledChildren: _elements, displayStyle: .set) } } diff --git a/Sources/OrderedCollections/OrderedSet/OrderedSet+CustomStringConvertible.swift b/Sources/OrderedCollections/OrderedSet/OrderedSet+Descriptions.swift similarity index 58% rename from Sources/OrderedCollections/OrderedSet/OrderedSet+CustomStringConvertible.swift rename to Sources/OrderedCollections/OrderedSet/OrderedSet+Descriptions.swift index 2678ed60a..725970b89 100644 --- a/Sources/OrderedCollections/OrderedSet/OrderedSet+CustomStringConvertible.swift +++ b/Sources/OrderedCollections/OrderedSet/OrderedSet+Descriptions.swift @@ -2,27 +2,27 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 // //===----------------------------------------------------------------------===// +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + extension OrderedSet: CustomStringConvertible { /// A textual representation of this instance. public var description: String { - var result = "[" - var first = true - for item in self { - if first { - first = false - } else { - result += ", " - } - print(item, terminator: "", to: &result) - } - result += "]" - return result + _arrayDescription(for: self) + } +} + +extension OrderedSet: CustomDebugStringConvertible { + /// A textual representation of this instance, suitable for debugging. + public var debugDescription: String { + description } } diff --git a/Sources/OrderedCollections/OrderedSet/OrderedSet+Diffing.swift b/Sources/OrderedCollections/OrderedSet/OrderedSet+Diffing.swift index 95b60b5e4..008d2664e 100644 --- a/Sources/OrderedCollections/OrderedSet/OrderedSet+Diffing.swift +++ b/Sources/OrderedCollections/OrderedSet/OrderedSet+Diffing.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 diff --git a/Sources/OrderedCollections/OrderedSet/OrderedSet+Equatable.swift b/Sources/OrderedCollections/OrderedSet/OrderedSet+Equatable.swift index 472b0937c..c81b2387f 100644 --- a/Sources/OrderedCollections/OrderedSet/OrderedSet+Equatable.swift +++ b/Sources/OrderedCollections/OrderedSet/OrderedSet+Equatable.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 @@ -15,6 +15,11 @@ extension OrderedSet: Equatable { /// Two ordered sets are considered equal if they contain the same /// elements in the same order. /// + /// - Note: This operator implements different behavior than the + /// `isEqualSet(to:)` method -- the latter implements an unordered + /// comparison, to match the behavior of members like `isSubset(of:)`, + /// `isStrictSuperset(of:)` etc. + /// /// - Complexity: O(`min(left.count, right.count)`) @inlinable public static func ==(left: Self, right: Self) -> Bool { diff --git a/Sources/OrderedCollections/OrderedSet/OrderedSet+ExpressibleByArrayLiteral.swift b/Sources/OrderedCollections/OrderedSet/OrderedSet+ExpressibleByArrayLiteral.swift index 1d1d6dece..96176cd08 100644 --- a/Sources/OrderedCollections/OrderedSet/OrderedSet+ExpressibleByArrayLiteral.swift +++ b/Sources/OrderedCollections/OrderedSet/OrderedSet+ExpressibleByArrayLiteral.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 diff --git a/Sources/OrderedCollections/OrderedSet/OrderedSet+Hashable.swift b/Sources/OrderedCollections/OrderedSet/OrderedSet+Hashable.swift index 3acba7206..c14fc9082 100644 --- a/Sources/OrderedCollections/OrderedSet/OrderedSet+Hashable.swift +++ b/Sources/OrderedCollections/OrderedSet/OrderedSet+Hashable.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 diff --git a/Sources/OrderedCollections/OrderedSet/OrderedSet+Initializers.swift b/Sources/OrderedCollections/OrderedSet/OrderedSet+Initializers.swift index e9b68df6e..83cfe08ca 100644 --- a/Sources/OrderedCollections/OrderedSet/OrderedSet+Initializers.swift +++ b/Sources/OrderedCollections/OrderedSet/OrderedSet+Initializers.swift @@ -2,13 +2,17 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 // //===----------------------------------------------------------------------===// +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + extension OrderedSet { /// Creates a set with the contents of the given sequence, which /// must not include duplicate elements. @@ -28,8 +32,7 @@ extension OrderedSet { /// high-quality hashing. @inlinable @inline(__always) - public init(uncheckedUniqueElements elements: S) - where S.Element == Element { + public init(uncheckedUniqueElements elements: some Sequence) { let elements = ContiguousArray(elements) #if DEBUG let (table, firstDupe) = _HashTable.create(untilFirstDuplicateIn: elements) @@ -57,18 +60,18 @@ extension OrderedSet { /// is the number of elements in the sequence), provided that /// `Element` properly implements hashing. @inlinable - public init(_ elements: S) where S.Element == Element { - if S.self == Self.self { - self = elements as! Self + public init(_ elements: some Sequence) { + if let elements = _specialize(elements, for: Self.self) { + self = elements return } // Fast paths for when we know elements are all unique - if S.self == Set.self || S.self == SubSequence.self { + if elements is _UniqueCollection { self.init(uncheckedUniqueElements: elements) return } - self.init() + self.init(minimumCapacity: elements.underestimatedCount) append(contentsOf: elements) } @@ -132,9 +135,7 @@ extension OrderedSet { /// in the sequence), provided that `Element` implements /// high-quality hashing. @inlinable - public init( - _ elements: C - ) where C.Element == Element { + public init(_ elements: some RandomAccessCollection) { // This code is careful not to copy storage if `C` is an Array // or ContiguousArray and the elements are already unique. let (table, firstDupe) = _HashTable.create( diff --git a/Sources/OrderedCollections/OrderedSet/OrderedSet+Insertions.swift b/Sources/OrderedCollections/OrderedSet/OrderedSet+Insertions.swift index 6b054366a..e6d960d26 100644 --- a/Sources/OrderedCollections/OrderedSet/OrderedSet+Insertions.swift +++ b/Sources/OrderedCollections/OrderedSet/OrderedSet+Insertions.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 @@ -62,7 +62,9 @@ extension OrderedSet { @inlinable @discardableResult - internal mutating func _append(_ item: Element) -> (inserted: Bool, index: Int) { + internal mutating func _append( + _ item: Element + ) -> (inserted: Bool, index: Int) { let (index, bucket) = _find(item) if let index = index { return (false, index) } _appendNew(item, in: bucket) @@ -102,9 +104,9 @@ extension OrderedSet { /// hash, and compare operations on the `Element` type, if it implements /// high-quality hashing. @inlinable - public mutating func append( - contentsOf elements: S - ) where S.Element == Element { + public mutating func append( + contentsOf elements: some Sequence + ) { for item in elements { _append(item) } diff --git a/Sources/OrderedCollections/OrderedSet/OrderedSet+Invariants.swift b/Sources/OrderedCollections/OrderedSet/OrderedSet+Invariants.swift index 51c853b7c..8189a1f7f 100644 --- a/Sources/OrderedCollections/OrderedSet/OrderedSet+Invariants.swift +++ b/Sources/OrderedCollections/OrderedSet/OrderedSet+Invariants.swift @@ -2,14 +2,28 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 // //===----------------------------------------------------------------------===// +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + extension OrderedSet { + /// True if consistency checking is enabled in the implementation of this + /// type, false otherwise. + /// + /// Documented performance promises are null and void when this property + /// returns true -- for example, operations that are documented to take + /// O(1) time might take O(*n*) time, or worse. + public static var _isConsistencyCheckingEnabled: Bool { + _isCollectionsInternalCheckingEnabled + } + #if COLLECTIONS_INTERNAL_CHECKS @inlinable @inline(never) @_effects(releasenone) diff --git a/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial MutableCollection.swift b/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial MutableCollection.swift index 953b8a7aa..42376cc3f 100644 --- a/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial MutableCollection.swift +++ b/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial MutableCollection.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 @@ -11,6 +11,10 @@ // The parts of MutableCollection that OrderedSet is able to implement. +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + extension OrderedSet { /// Exchanges the values at the specified indices of the set. /// @@ -328,8 +332,8 @@ extension OrderedSet { /// change when your program is compiled using a different version of /// Swift. @inlinable - public mutating func shuffle( - using generator: inout T + public mutating func shuffle( + using generator: inout some RandomNumberGenerator ) { _elements.shuffle(using: &generator) _regenerateExistingHashTable() diff --git a/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial RangeReplaceableCollection.swift b/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial RangeReplaceableCollection.swift index fd8750d08..bb8900f16 100644 --- a/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial RangeReplaceableCollection.swift +++ b/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial RangeReplaceableCollection.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 @@ -111,9 +111,7 @@ extension OrderedSet { /// /// - Complexity: O(`count`) @inlinable - public mutating func removeSubrange( - _ bounds: R - ) where R.Bound == Int { + public mutating func removeSubrange(_ bounds: some RangeExpression) { removeSubrange(bounds.relative(to: self)) } diff --git a/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra formIntersection.swift b/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra formIntersection.swift new file mode 100644 index 000000000..c0306c0a7 --- /dev/null +++ b/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra formIntersection.swift @@ -0,0 +1,85 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +// `OrderedSet` does not directly conform to `SetAlgebra` because its definition +// of equality conflicts with `SetAlgebra` requirements. However, it still +// implements most `SetAlgebra` requirements (except `insert`, which is replaced +// by `append`). +// +// `OrderedSet` also provides an `unordered` view that explicitly conforms to +// `SetAlgebra`. That view implements `Equatable` by ignoring element order, +// so it can satisfy `SetAlgebra` requirements. + +extension OrderedSet { + /// Removes the elements of this set that aren't also in the given one. + /// + /// var set: OrderedSet = [1, 2, 3, 4] + /// let other: OrderedSet = [6, 4, 2, 0] + /// set.formIntersection(other) + /// // set is now [2, 4] + /// + /// The result will only contain instances that were originally in `self`. + /// (This matters if equal members can be distinguished by comparing their + /// identities, or by some other means.) + /// + /// - Parameter other: A set of elements. + /// + /// - Complexity: Expected to be O(`self.count`) on average, if `Element` + /// implements high-quality hashing. + @inlinable + public mutating func formIntersection(_ other: Self) { + self = self.intersection(other) + } + + // Generalizations + + /// Removes the elements of this set that aren't also in the given one. + /// + /// var set: OrderedSet = [1, 2, 3, 4] + /// let other: OrderedSet = [6, 4, 2, 0] + /// set.formIntersection(other) + /// // set is now [2, 4] + /// + /// The result will only contain instances that were originally in `self`. + /// (This matters if equal members can be distinguished by comparing their + /// identities, or by some other means.) + /// + /// - Parameter other: A set of elements. + /// + /// - Complexity: Expected to be O(`self.count`) on average, if `Element` + /// implements high-quality hashing. + @inlinable + @inline(__always) + public mutating func formIntersection(_ other: UnorderedView) { + formIntersection(other._base) + } + + /// Removes the elements of this set that aren't also in the given sequence. + /// + /// var set: OrderedSet = [1, 2, 3, 4] + /// set.formIntersection([6, 4, 2, 0] as Array) + /// // set is now [2, 4] + /// + /// The result will only contain instances that were originally in `self`. + /// (This matters if equal members can be distinguished by comparing their + /// identities, or by some other means.) + /// + /// - Parameter other: A finite sequence of elements. + /// + /// - Complexity: Expected to be O(*n*) on average where *n* is the number of + /// elements in `other`, if `Element` implements high-quality hashing. + @inlinable + public mutating func formIntersection( + _ other: some Sequence + ) { + self = self.intersection(other) + } +} diff --git a/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra formSymmetricDifference.swift b/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra formSymmetricDifference.swift new file mode 100644 index 000000000..9ab3353bc --- /dev/null +++ b/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra formSymmetricDifference.swift @@ -0,0 +1,87 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +// `OrderedSet` does not directly conform to `SetAlgebra` because its definition +// of equality conflicts with `SetAlgebra` requirements. However, it still +// implements most `SetAlgebra` requirements (except `insert`, which is replaced +// by `append`). +// +// `OrderedSet` also provides an `unordered` view that explicitly conforms to +// `SetAlgebra`. That view implements `Equatable` by ignoring element order, +// so it can satisfy `SetAlgebra` requirements. + +extension OrderedSet { + /// Replace this set with the elements contained in this set or the given + /// set, but not both. + /// + /// On return, `self` contains elements originally from `self` followed by + /// elements in `other`, in the same order they appeared in the input values. + /// + /// var set: OrderedSet = [1, 2, 3, 4] + /// let other: OrderedSet = [6, 4, 2, 0] + /// set.formSymmetricDifference(other) + /// // set is now [1, 3, 6, 0] + /// + /// - Parameter other: Another set. + /// + /// - Complexity: Expected to be O(`self.count + other.count`) on average, if + /// `Element` implements high-quality hashing. + @inlinable + public mutating func formSymmetricDifference(_ other: __owned Self) { + self = self.symmetricDifference(other) + } + + // Generalizations + + /// Replace this set with the elements contained in this set or the given + /// set, but not both. + /// + /// On return, `self` contains elements originally from `self` followed by + /// elements in `other`, in the same order they appeared in the input values. + /// + /// var set: OrderedSet = [1, 2, 3, 4] + /// let other: OrderedSet = [6, 4, 2, 0] + /// set.formSymmetricDifference(other.unordered) + /// // set is now [1, 3, 6, 0] + /// + /// - Parameter other: Another set. + /// + /// - Complexity: Expected to be O(`self.count + other.count`) on average, if + /// `Element` implements high-quality hashing. + @inlinable + @inline(__always) + public mutating func formSymmetricDifference(_ other: __owned UnorderedView) { + formSymmetricDifference(other._base) + } + + /// Replace this set with the elements contained in this set or the given + /// sequence, but not both. + /// + /// On return, `self` contains elements originally from `self` followed by + /// elements in `other`, in the same order they first appeared in the input + /// values. + /// + /// var set: OrderedSet = [1, 2, 3, 4] + /// set.formSymmetricDifference([6, 4, 2, 0] as Array) + /// // set is now [1, 3, 6, 0] + /// + /// - Parameter other: A finite sequence of elements. + /// + /// - Complexity: Expected to be O(`self.count` + *n*) on average where *n* is + /// the number of elements in `other`, if `Element` implements high-quality + /// hashing. + @inlinable + public mutating func formSymmetricDifference( + _ other: __owned some Sequence + ) { + self = self.symmetricDifference(other) + } +} diff --git a/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra formUnion.swift b/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra formUnion.swift new file mode 100644 index 000000000..bd3a8a6f4 --- /dev/null +++ b/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra formUnion.swift @@ -0,0 +1,103 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +// `OrderedSet` does not directly conform to `SetAlgebra` because its definition +// of equality conflicts with `SetAlgebra` requirements. However, it still +// implements most `SetAlgebra` requirements (except `insert`, which is replaced +// by `append`). +// +// `OrderedSet` also provides an `unordered` view that explicitly conforms to +// `SetAlgebra`. That view implements `Equatable` by ignoring element order, +// so it can satisfy `SetAlgebra` requirements. + +extension OrderedSet { + /// Adds the elements of the given set to this set. + /// + /// Members of `other` that aren't already in `self` get appended to the end + /// of the set, in the order they appear in `other`. + /// + /// var a: OrderedSet = [1, 2, 3, 4] + /// let b: OrderedSet = [0, 2, 4, 6] + /// a.formUnion(b) + /// // `a` is now `[1, 2, 3, 4, 0, 6]` + /// + /// For values that are members of both sets, this operation preserves the + /// instances that were originally in `self`. (This matters if equal members + /// can be distinguished by comparing their identities, or by some other + /// means.) + /// + /// - Parameter other: The set of elements to insert. + /// + /// - Complexity: Expected to be O(`other.count`) on average, if `Element` + /// implements high-quality hashing. + @inlinable + public mutating func formUnion(_ other: __owned Self) { + append(contentsOf: other) + } + + // Generalizations + + /// Adds the elements of the given set to this set. + /// + /// Members of `other` that aren't already in `self` get appended to the end + /// of the set, in the order they appear in `other`. + /// + /// let a: OrderedSet = [1, 2, 3, 4] + /// let b: OrderedSet = [0, 2, 4, 6] + /// a.formUnion(b.unordered) + /// // a is now [1, 2, 3, 4, 0, 6] + /// + /// For values that are members of both inputs, this operation preserves the + /// instances that were originally in `self`. (This matters if equal members + /// can be distinguished by comparing their identities, or by some other + /// means.) + /// + /// - Parameter other: The set of elements to add. + /// + /// - Complexity: Expected to be O(`self.count` + `other.count`) on average, + /// if `Element` implements high-quality hashing. + @inlinable + @inline(__always) + public mutating func formUnion(_ other: __owned UnorderedView) { + formUnion(other._base) + } + + /// Adds the elements of the given sequence to this set. + /// + /// Members of `other` that aren't already in `self` get appended to the end + /// of the set, in the order they appear in `other`. + /// + /// let a: OrderedSet = [1, 2, 3, 4] + /// let b: Array = [0, 2, 4, 6] + /// a.formUnion(b) + /// // a is now [1, 2, 3, 4, 0, 6] + /// + /// For values that are members of both inputs, this operation preserves the + /// instances that were originally in `self`. (This matters if equal members + /// can be distinguished by comparing their identities, or by some other + /// means.) + /// + /// If some of the values that are missing from `self` have multiple copies + /// in `other`, then the result of this function always contains the first + /// instances in the sequence -- the second and subsequent copies are ignored. + /// + /// - Parameter other: A finite sequence of elements. + /// + /// - Complexity: Expected to be O(`self.count` + `other.count`) on average, + /// if `Element` implements high-quality hashing. + @inlinable + public mutating func formUnion( + _ other: __owned some Sequence + ) { + append(contentsOf: other) + } +} + diff --git a/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra intersection.swift b/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra intersection.swift new file mode 100644 index 000000000..8ed990c9b --- /dev/null +++ b/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra intersection.swift @@ -0,0 +1,110 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +// `OrderedSet` does not directly conform to `SetAlgebra` because its definition +// of equality conflicts with `SetAlgebra` requirements. However, it still +// implements most `SetAlgebra` requirements (except `insert`, which is replaced +// by `append`). +// +// `OrderedSet` also provides an `unordered` view that explicitly conforms to +// `SetAlgebra`. That view implements `Equatable` by ignoring element order, +// so it can satisfy `SetAlgebra` requirements. + +extension OrderedSet { + /// Returns a new set with the elements that are common to both this set and + /// the provided other one, in the order they appear in `self`. + /// + /// let set: OrderedSet = [1, 2, 3, 4] + /// let other: OrderedSet = [6, 4, 2, 0] + /// set.intersection(other) // [2, 4] + /// + /// The result will only contain instances that were originally in `self`. + /// (This matters if equal members can be distinguished by comparing their + /// identities, or by some other means.) + /// + /// - Parameter other: Another set. + /// + /// - Returns: A new set. + /// + /// - Complexity: Expected to be O(`self.count`) on average, if `Element` + /// implements high-quality hashing. + @inlinable + public __consuming func intersection(_ other: Self) -> Self { + var result = Self() + for item in self { + if other.contains(item) { + result._appendNew(item) + } + } + result._checkInvariants() + return result + } + + // Generalizations + + /// Returns a new set with the elements that are common to both this set and + /// the provided other one, in the order they appear in `self`. + /// + /// let set: OrderedSet = [1, 2, 3, 4] + /// let other: OrderedSet = [6, 4, 2, 0] + /// set.intersection(other) // [2, 4] + /// + /// The result will only contain instances that were originally in `self`. + /// (This matters if equal members can be distinguished by comparing their + /// identities, or by some other means.) + /// + /// - Parameter other: Another set. + /// + /// - Returns: A new set. + /// + /// - Complexity: Expected to be O(`self.count`) on average, if `Element` + /// implements high-quality hashing. + @inlinable + @inline(__always) + public __consuming func intersection(_ other: UnorderedView) -> Self { + intersection(other._base) + } + + /// Returns a new set with the elements that are common to both this set and + /// the provided sequence, in the order they appear in `self`. + /// + /// let set: OrderedSet = [1, 2, 3, 4] + /// set.intersection([6, 4, 2, 0] as Array) // [2, 4] + /// + /// The result will only contain instances that were originally in `self`. + /// (This matters if equal members can be distinguished by comparing their + /// identities, or by some other means.) + /// + /// - Parameter other: A finite sequence of elements. + /// + /// - Returns: A new set. + /// + /// - Complexity: Expected to be O(*n*) on average where *n* is the number of + /// elements in `other`, if `Element` implements high-quality hashing. + @inlinable + public __consuming func intersection( + _ other: some Sequence + ) -> Self { + _UnsafeBitSet.withTemporaryBitSet(capacity: self.count) { bitset in + for item in other { + if let index = self._find_inlined(item).index { + bitset.insert(index) + } + } + return self._extractSubset(using: bitset) + } + } +} + diff --git a/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra isDisjoint.swift b/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra isDisjoint.swift new file mode 100644 index 000000000..1603008a3 --- /dev/null +++ b/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra isDisjoint.swift @@ -0,0 +1,130 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +// `OrderedSet` does not directly conform to `SetAlgebra` because its definition +// of equality conflicts with `SetAlgebra` requirements. However, it still +// implements most `SetAlgebra` requirements (except `insert`, which is replaced +// by `append`). +// +// `OrderedSet` also provides an `unordered` view that explicitly conforms to +// `SetAlgebra`. That view implements `Equatable` by ignoring element order, +// so it can satisfy `SetAlgebra` requirements. + +extension OrderedSet { + /// Returns a Boolean value that indicates whether the set has no members in + /// common with the given set. + /// + /// let a: OrderedSet = [1, 2, 3, 4] + /// let b: OrderedSet = [5, 6] + /// a.isDisjoint(with: b) // true + /// + /// - Parameter other: Another set. + /// + /// - Returns: `true` if `self` has no elements in common with `other`; + /// otherwise, `false`. + /// + /// - Complexity: Expected to be O(min(`self.count`, `other.count`)) on + /// average, if `Element` implements high-quality hashing. + @inlinable + public func isDisjoint(with other: Self) -> Bool { + guard !self.isEmpty && !other.isEmpty else { return true } + if self.count <= other.count { + for item in self { + if other.contains(item) { return false } + } + } else { + for item in other { + if self.contains(item) { return false } + } + } + return true + } + + // Generalizations + + /// Returns a Boolean value that indicates whether the set has no members in + /// common with the given set. + /// + /// let a: OrderedSet = [1, 2, 3, 4] + /// let b: OrderedSet = [5, 6] + /// a.isDisjoint(with: b.unordered) // true + /// + /// - Parameter other: Another set. + /// + /// - Returns: `true` if `self` has no elements in common with `other`; + /// otherwise, `false`. + /// + /// - Complexity: Expected to be O(min(`self.count`, `other.count`)) on + /// average, if `Element` implements high-quality hashing. + @inlinable + @inline(__always) + public func isDisjoint(with other: UnorderedView) -> Bool { + isDisjoint(with: other._base) + } + + /// Returns a Boolean value that indicates whether the set has no members in + /// common with the given set. + /// + /// let a: OrderedSet = [1, 2, 3, 4] + /// let b: Set = [5, 6] + /// a.isDisjoint(with: b) // true + /// + /// - Parameter other: Another set. + /// + /// - Returns: `true` if `self` has no elements in common with `other`; + /// otherwise, `false`. + /// + /// - Complexity: Expected to be O(min(`self.count`, `other.count`)) on + /// average, if `Element` implements high-quality hashing. + @inlinable + public func isDisjoint(with other: Set) -> Bool { + guard !self.isEmpty && !other.isEmpty else { return true } + if self.count <= other.count { + for item in self { + if other.contains(item) { return false } + } + } else { + for item in other { + if self.contains(item) { return false } + } + } + return true + } + + /// Returns a Boolean value that indicates whether the set has no members in + /// common with the given sequence. + /// + /// let a: OrderedSet = [1, 2, 3, 4] + /// let b: Array = [5, 6] + /// a.isDisjoint(with: b) // true + /// + /// - Parameter other: A finite sequence of elements. + /// + /// - Returns: `true` if `self` has no elements in common with `other`; + /// otherwise, `false`. + /// + /// - Complexity: Expected to be O(*n*) on average, where *n* is the number of + /// elements in `other`, if `Element` implements high-quality hashing. + @inlinable + public func isDisjoint( + with other: some Sequence + ) -> Bool { + guard !self.isEmpty else { return true } + for item in other { + if self.contains(item) { return false } + } + return true + } +} diff --git a/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra isEqualSet.swift b/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra isEqualSet.swift new file mode 100644 index 000000000..665e4b7b1 --- /dev/null +++ b/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra isEqualSet.swift @@ -0,0 +1,84 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension OrderedSet { + /// Returns a Boolean value indicating whether two set values contain the + /// same elements, but not necessarily in the same order. + /// + /// - Note: This member implements different behavior than the `==(_:_:)` + /// operator -- the latter implements an ordered comparison, matching + /// the stricter concept of equality expected of an ordered collection + /// type. + /// + /// - Complexity: O(`min(left.count, right.count)`), as long as`Element` + /// properly implements hashing. + public func isEqualSet(to other: Self) -> Bool { + self.unordered == other.unordered + } + + /// Returns a Boolean value indicating whether two set values contain the + /// same elements, but not necessarily in the same order. + /// + /// - Complexity: O(`min(left.count, right.count)`), as long as`Element` + /// properly implements hashing. + public func isEqualSet(to other: UnorderedView) -> Bool { + self.unordered == other + } + + /// Returns a Boolean value indicating whether an ordered set contains the + /// same values as a given sequence, but not necessarily in the same + /// order. + /// + /// Duplicate items in `other` do not prevent it from comparing equal to + /// `self`. + /// + /// - Complexity: O(*n*), where *n* is the number of items in + /// `other`, as long as`Element` properly implements hashing. + public func isEqualSet(to other: some Sequence) -> Bool { + if let other = _specialize(other, for: Self.self) { + return isEqualSet(to: other) + } + + if self.isEmpty { + return other.allSatisfy { _ in false } + } + + if other is _UniqueCollection { + // We don't need to create a temporary set. + guard other.underestimatedCount <= self.count else { return false } + var seen = 0 + for item in other { + guard self.contains(item) else { return false } + seen &+= 1 + } + precondition( + seen <= self.count, + // Otherwise other.underestimatedCount != other.count + "Invalid Collection '\(type(of: other))' (bad underestimatedCount)") + return seen == self.count + } + + return _UnsafeBitSet.withTemporaryBitSet(capacity: count) { seen in + var c = 0 + for item in other { + guard let index = _find(item).index else { return false } + if seen.insert(index) { + c &+= 1 + } + } + return c == count + } + } +} diff --git a/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra isStrictSubset.swift b/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra isStrictSubset.swift new file mode 100644 index 000000000..0efe8ce9e --- /dev/null +++ b/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra isStrictSubset.swift @@ -0,0 +1,162 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +// `OrderedSet` does not directly conform to `SetAlgebra` because its definition +// of equality conflicts with `SetAlgebra` requirements. However, it still +// implements most `SetAlgebra` requirements (except `insert`, which is replaced +// by `append`). +// +// `OrderedSet` also provides an `unordered` view that explicitly conforms to +// `SetAlgebra`. That view implements `Equatable` by ignoring element order, +// so it can satisfy `SetAlgebra` requirements. + +extension OrderedSet { + /// Returns a Boolean value that indicates whether the set is a strict subset + /// of the given set. + /// + /// Set *A* is a strict subset of another set *B* if every member of *A* is + /// also a member of *B* and *B* contains at least one element that is not a + /// member of *A*. (Ignoring the order the elements appear in the sets.) + /// + /// let a: OrderedSet = [1, 2, 3, 4] + /// let b: OrderedSet = [4, 2, 1] + /// b.isStrictSubset(of: a) // true + /// + /// - Parameter other: Another set. + /// + /// - Returns: `true` if `self` is a strict subset of `other`; otherwise, + /// `false`. + /// + /// - Complexity: Expected to be O(`self.count`) on average, if `Element` + /// implements high-quality hashing. + @inlinable + public func isStrictSubset(of other: Self) -> Bool { + self.count < other.count && self.isSubset(of: other) + } + + // Generalizations + + /// Returns a Boolean value that indicates whether the set is a strict subset + /// of the given set. + /// + /// Set *A* is a strict subset of another set *B* if every member of *A* is + /// also a member of *B* and *B* contains at least one element that is not a + /// member of *A*. (Ignoring the order the elements appear in the sets.) + /// + /// let a: OrderedSet = [1, 2, 3, 4] + /// let b: OrderedSet = [4, 2, 1] + /// b.isStrictSubset(of: a.unordered) // true + /// + /// - Parameter other: Another set. + /// + /// - Returns: `true` if `self` is a strict subset of `other`; otherwise, + /// `false`. + /// + /// - Complexity: Expected to be O(`self.count`) on average, if `Element` + /// implements high-quality hashing. + @inlinable + @inline(__always) + public func isStrictSubset(of other: UnorderedView) -> Bool { + isStrictSubset(of: other._base) + } + + /// Returns a Boolean value that indicates whether the set is a strict subset + /// of the given set. + /// + /// Set *A* is a strict subset of another set *B* if every member of *A* is + /// also a member of *B* and *B* contains at least one element that is not a + /// member of *A*. (Ignoring the order the elements appear in the sets.) + /// + /// let a: Set = [1, 2, 3, 4] + /// let b: OrderedSet = [4, 2, 1] + /// b.isStrictSubset(of: a) // true + /// + /// - Parameter other: Another set. + /// + /// - Returns: `true` if `self` is a strict subset of `other`; otherwise, + /// `false`. + /// + /// - Complexity: Expected to be O(`self.count`) on average, if `Element` + /// implements high-quality hashing. + @inlinable + public func isStrictSubset(of other: Set) -> Bool { + self.count < other.count && self.isSubset(of: other) + } + + /// Returns a Boolean value that indicates whether the set is a strict subset + /// of the given sequence. + /// + /// Set *A* is a strict subset of another set *B* if every member of *A* is + /// also a member of *B* and *B* contains at least one element that is not a + /// member of *A*. (Ignoring the order the elements appear in the sets.) + /// + /// let a: Array = [1, 2, 3, 4] + /// let b: OrderedSet = [4, 2, 1] + /// b.isStrictSubset(of: a) // true + /// + /// - Parameter other: A finite sequence of elements. + /// + /// - Returns: `true` if `self` is a strict subset of `other`; otherwise, + /// `false`. + /// + /// - Complexity: Expected to be O(`self.count` + *n*) on average, where *n* + /// is the number of elements in `other`, if `Element` implements + /// high-quality hashing. + @inlinable + public func isStrictSubset( + of other: some Sequence + ) -> Bool { + if let other = _specialize(other, for: Self.self) { + return self.isStrictSubset(of: other) + } + if let other = _specialize(other, for: Set.self) { + return self.isStrictSubset(of: other) + } + + var it = self.makeIterator() + guard let first = it.next() else { + return other.contains(where: { _ in true }) + } + if let match = other._customContainsEquatableElement(first) { + // Fast path: the sequence has fast containment checks. + guard match else { return false } + while let item = it.next() { + guard other.contains(item) else { return false } + } + return !other.allSatisfy { self.contains($0) } + } + + return _UnsafeBitSet.withTemporaryBitSet(capacity: count) { seen in + // Mark elements in `self` that we've seen in `other`. + var isKnownStrict = false + var c = 0 + for item in other { + if let index = _find(item).index { + if seen.insert(index) { + c &+= 1 + if c == self.count, isKnownStrict { + // We've seen enough. + return true + } + } + } else { + if !isKnownStrict, c == self.count { return true } + isKnownStrict = true + } + } + return false + } + } +} diff --git a/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra isStrictSuperset.swift b/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra isStrictSuperset.swift new file mode 100644 index 000000000..5e7747d2d --- /dev/null +++ b/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra isStrictSuperset.swift @@ -0,0 +1,160 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +// `OrderedSet` does not directly conform to `SetAlgebra` because its definition +// of equality conflicts with `SetAlgebra` requirements. However, it still +// implements most `SetAlgebra` requirements (except `insert`, which is replaced +// by `append`). +// +// `OrderedSet` also provides an `unordered` view that explicitly conforms to +// `SetAlgebra`. That view implements `Equatable` by ignoring element order, +// so it can satisfy `SetAlgebra` requirements. + +extension OrderedSet { + /// Returns a Boolean value that indicates whether the set is a strict + /// superset of the given set. + /// + /// Set *A* is a strict superset of another set *B* if every member of *B* is + /// also a member of *A* and *A* contains at least one element that is *not* + /// a member of *B*. (Ignoring the order the elements appear in the sets.) + /// + /// let a: OrderedSet = [1, 2, 3, 4] + /// let b: OrderedSet = [4, 2, 1] + /// a.isStrictSuperset(of: b) // true + /// + /// - Parameter other: Another set. + /// + /// - Returns: `true` if `self` is a strict superset of `other`; otherwise, + /// `false`. + /// + /// - Complexity: Expected to be O(`other.count`) on average, if `Element` + /// implements high-quality hashing. + @inlinable + public func isStrictSuperset(of other: Self) -> Bool { + self.count > other.count && other.isSubset(of: self) + } + + // Generalizations + + /// Returns a Boolean value that indicates whether the set is a strict + /// superset of the given set. + /// + /// Set *A* is a strict superset of another set *B* if every member of *B* is + /// also a member of *A* and *A* contains at least one element that is *not* + /// a member of *B*. (Ignoring the order the elements appear in the sets.) + /// + /// let a: OrderedSet = [1, 2, 3, 4] + /// let b: OrderedSet = [4, 2, 1] + /// a.isStrictSuperset(of: b.unordered) // true + /// + /// - Parameter other: Another set. + /// + /// - Returns: `true` if `self` is a strict superset of `other`; otherwise, + /// `false`. + /// + /// - Complexity: Expected to be O(`other.count`) on average, if `Element` + /// implements high-quality hashing. + @inlinable + @inline(__always) + public func isStrictSuperset(of other: UnorderedView) -> Bool { + isStrictSuperset(of: other._base) + } + + /// Returns a Boolean value that indicates whether the set is a strict + /// superset of the given set. + /// + /// Set *A* is a strict superset of another set *B* if every member of *B* is + /// also a member of *A* and *A* contains at least one element that is *not* + /// a member of *B*. (Ignoring the order the elements appear in the sets.) + /// + /// let a: OrderedSet = [1, 2, 3, 4] + /// let b: Set = [4, 2, 1] + /// a.isStrictSuperset(of: b) // true + /// + /// - Parameter other: Another set. + /// + /// - Returns: `true` if `self` is a strict superset of `other`; otherwise, + /// `false`. + /// + /// - Complexity: Expected to be O(`other.count`) on average, if `Element` + /// implements high-quality hashing. + @inlinable + public func isStrictSuperset(of other: Set) -> Bool { + self.count > other.count && other.isSubset(of: self) + } + + /// Returns a Boolean value that indicates whether the set is a strict + /// superset of the given sequence. + /// + /// Set *A* is a strict superset of another set *B* if every member of *B* is + /// also a member of *A* and *A* contains at least one element that is *not* + /// a member of *B*. (Ignoring the order the elements appear in the sets.) + /// + /// let a: OrderedSet = [1, 2, 3, 4] + /// let b: Array = [4, 2, 1] + /// a.isStrictSuperset(of: b) // true + /// + /// - Parameter other: A finite sequence of elements, some of whose members + /// may appear more than once. (Duplicate items are ignored.) + /// + /// - Returns: `true` if `self` is a strict superset of `other`; otherwise, + /// `false`. + /// + /// - Complexity: Expected to be O(`self.count` + *n*) on average, where *n* + /// is the number of elements in `other`, if `Element` implements + /// high-quality hashing. + @inlinable + public func isStrictSuperset( + of other: some Sequence + ) -> Bool { + if let other = _specialize(other, for: Self.self) { + return self.isStrictSuperset(of: other) + } + if let other = _specialize(other, for: Set.self) { + return self.isStrictSuperset(of: other) + } + + var it = self.makeIterator() + guard let first = it.next() else { return false } + if let match = other._customContainsEquatableElement(first) { + // Fast path: the sequence has fast containment checks. + guard other.allSatisfy({ self.contains($0) }) else { return false } + guard match else { return true } + while let item = it.next() { + guard other.contains(item) else { return true } + } + return false + } + + return _UnsafeBitSet.withTemporaryBitSet(capacity: count) { seen in + // Mark elements in `self` that we've seen in `other`. + var c = 0 + for item in other { + guard let index = _find(item).index else { + return false + } + if seen.insert(index) { + c &+= 1 + if c == self.count { + // We've seen enough. + return false + } + } + } + return c < self.count + } + } +} + diff --git a/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra isSubset.swift b/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra isSubset.swift new file mode 100644 index 000000000..038016cd2 --- /dev/null +++ b/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra isSubset.swift @@ -0,0 +1,155 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +// `OrderedSet` does not directly conform to `SetAlgebra` because its definition +// of equality conflicts with `SetAlgebra` requirements. However, it still +// implements most `SetAlgebra` requirements (except `insert`, which is replaced +// by `append`). +// +// `OrderedSet` also provides an `unordered` view that explicitly conforms to +// `SetAlgebra`. That view implements `Equatable` by ignoring element order, +// so it can satisfy `SetAlgebra` requirements. + +extension OrderedSet { + /// Returns a Boolean value that indicates whether this set is a subset of + /// the given set. + /// + /// Set *A* is a subset of another set *B* if every member of *A* is also a + /// member of *B*, ignoring the order they appear in the two sets. + /// + /// let a: OrderedSet = [1, 2, 3, 4] + /// let b: OrderedSet = [4, 2, 1] + /// b.isSubset(of: a) // true + /// + /// - Parameter other: Another set. + /// + /// - Returns: `true` if the set is a subset of `other`; otherwise, `false`. + /// + /// - Complexity: Expected to be O(`self.count`) on average, if `Element` + /// implements high-quality hashing. + @inlinable + public func isSubset(of other: Self) -> Bool { + guard other.count >= self.count else { return false } + for item in self { + guard other.contains(item) else { return false } + } + return true + } + + // Generalizations + + /// Returns a Boolean value that indicates whether this set is a subset of + /// the given set. + /// + /// Set *A* is a subset of another set *B* if every member of *A* is also a + /// member of *B*, ignoring the order they appear in the two sets. + /// + /// let a: OrderedSet = [1, 2, 3, 4] + /// let b: OrderedSet = [4, 2, 1] + /// b.isSubset(of: a.unordered) // true + /// + /// - Parameter other: Another set. + /// + /// - Returns: `true` if the set is a subset of `other`; otherwise, `false`. + /// + /// - Complexity: Expected to be O(`self.count`) on average, if `Element` + /// implements high-quality hashing. + @inlinable + @inline(__always) + public func isSubset(of other: UnorderedView) -> Bool { + isSubset(of: other._base) + } + + /// Returns a Boolean value that indicates whether this set is a subset of + /// the given set. + /// + /// Set *A* is a subset of another set *B* if every member of *A* is also a + /// member of *B*, ignoring the order they appear in the two sets. + /// + /// let a: OrderedSet = [1, 2, 3, 4] + /// let b: Set = [4, 2, 1] + /// b.isSubset(of: a) // true + /// + /// - Parameter other: Another set. + /// + /// - Returns: `true` if the set is a subset of `other`; otherwise, `false`. + /// + /// - Complexity: Expected to be O(`self.count`) on average, if `Element` + /// implements high-quality hashing. + @inlinable + public func isSubset(of other: Set) -> Bool { + guard other.count >= self.count else { return false } + for item in self { + guard other.contains(item) else { return false } + } + return true + } + + /// Returns a Boolean value that indicates whether this set is a subset of + /// the elements in the given sequence. + /// + /// Set *A* is a subset of another set *B* if every member of *A* is also a + /// member of *B*, ignoring the order they appear in the two sets. + /// + /// let a: Array = [1, 2, 3, 4] + /// let b: OrderedSet = [4, 2, 1] + /// b.isSubset(of: a) // true + /// + /// - Parameter other: A finite sequence. + /// + /// - Returns: `true` if the set is a subset of `other`; otherwise, `false`. + /// + /// - Complexity: Expected to be O(`self.count` + *n*) on average, where *n* + /// is the number of elements in `other`, if `Element` implements + /// high-quality hashing. + @inlinable + public func isSubset( + of other: some Sequence + ) -> Bool { + guard !isEmpty else { return true } + + if let other = _specialize(other, for: Self.self) { + return isSubset(of: other) + } + + var it = self.makeIterator() + let first = it.next()! + if let match = other._customContainsEquatableElement(first) { + // Fast path: the sequence has fast containment checks. + guard match else { return false } + while let item = it.next() { + guard other.contains(item) else { return false } + } + return true + } + + return _UnsafeBitSet.withTemporaryBitSet(capacity: count) { seen in + // Mark elements in `self` that we've seen in `other`. + var c = 0 + for item in other { + if let index = _find(item).index { + if seen.insert(index) { + c &+= 1 + if c == self.count { + // We've seen enough. + return true + } + } + } + } + return false + } + } +} diff --git a/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra isSuperset.swift b/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra isSuperset.swift new file mode 100644 index 000000000..932d2b0b7 --- /dev/null +++ b/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra isSuperset.swift @@ -0,0 +1,124 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +// `OrderedSet` does not directly conform to `SetAlgebra` because its definition +// of equality conflicts with `SetAlgebra` requirements. However, it still +// implements most `SetAlgebra` requirements (except `insert`, which is replaced +// by `append`). +// +// `OrderedSet` also provides an `unordered` view that explicitly conforms to +// `SetAlgebra`. That view implements `Equatable` by ignoring element order, +// so it can satisfy `SetAlgebra` requirements. + +extension OrderedSet { + /// Returns a Boolean value that indicates whether this set is a superset of + /// the given set. + /// + /// Set *A* is a superset of another set *B* if every member of *B* is also a + /// member of *A*, ignoring the order they appear in the two sets. + /// + /// let a: OrderedSet = [1, 2, 3, 4] + /// let b: OrderedSet = [4, 2, 1] + /// a.isSuperset(of: b) // true + /// + /// - Parameter other: Another set. + /// + /// - Returns: `true` if the set is a superset of `other`; otherwise, `false`. + /// + /// - Complexity: Expected to be O(`other.count`) on average, if `Element` + /// implements high-quality hashing. + @inlinable + public func isSuperset(of other: Self) -> Bool { + other.isSubset(of: self) + } + + // Generalizations + + /// Returns a Boolean value that indicates whether this set is a superset of + /// the given set. + /// + /// Set *A* is a superset of another set *B* if every member of *B* is also a + /// member of *A*, ignoring the order they appear in the two sets. + /// + /// let a: OrderedSet = [1, 2, 3, 4] + /// let b: Set = [4, 2, 1] + /// a.isSuperset(of: b) // true + /// + /// - Parameter other: Another set. + /// + /// - Returns: `true` if the set is a superset of `other`; otherwise, `false`. + /// + /// - Complexity: Expected to be O(`other.count`) on average, if `Element` + /// implements high-quality hashing. + @inlinable + public func isSuperset(of other: UnorderedView) -> Bool { + isSuperset(of: other._base) + } + + /// Returns a Boolean value that indicates whether this set is a superset of + /// the given set. + /// + /// Set *A* is a superset of another set *B* if every member of *B* is also a + /// member of *A*, ignoring the order they appear in the two sets. + /// + /// let a: OrderedSet = [1, 2, 3, 4] + /// let b: Set = [4, 2, 1] + /// a.isSuperset(of: b) // true + /// + /// - Parameter other: Another set. + /// + /// - Returns: `true` if the set is a superset of `other`; otherwise, `false`. + /// + /// - Complexity: Expected to be O(`other.count`) on average, if `Element` + /// implements high-quality hashing. + @inlinable + public func isSuperset(of other: Set) -> Bool { + guard self.count >= other.count else { return false } + return _isSuperset(of: other) + } + + /// Returns a Boolean value that indicates whether this set is a superset of + /// the given sequence. + /// + /// Set *A* is a superset of another set *B* if every member of *B* is also a + /// member of *A*, ignoring the order they appear in the two sets. + /// + /// let a: OrderedSet = [1, 2, 3, 4] + /// let b: Array = [4, 2, 1] + /// a.isSuperset(of: b) // true + /// + /// - Parameter other: A finite sequence of elements, some of whose members + /// may appear more than once. (Duplicate items are ignored.) + /// + /// - Returns: `true` if the set is a superset of `other`; otherwise, `false`. + /// + /// - Complexity: Expected to be O(*n*) on average, where *n* is the number of + /// elements in `other`, if `Element` implements high-quality hashing. + @inlinable + public func isSuperset(of other: some Sequence) -> Bool { + _isSuperset(of: other) + } + + @inlinable + internal func _isSuperset(of other: some Sequence) -> Bool { + if let other = _specialize(other, for: Self.self) { + return self.isSuperset(of: other) + } + for item in other { + guard self.contains(item) else { return false } + } + return true + } +} diff --git a/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra subtract.swift b/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra subtract.swift new file mode 100644 index 000000000..7248d799d --- /dev/null +++ b/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra subtract.swift @@ -0,0 +1,74 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +// `OrderedSet` does not directly conform to `SetAlgebra` because its definition +// of equality conflicts with `SetAlgebra` requirements. However, it still +// implements most `SetAlgebra` requirements (except `insert`, which is replaced +// by `append`). +// +// `OrderedSet` also provides an `unordered` view that explicitly conforms to +// `SetAlgebra`. That view implements `Equatable` by ignoring element order, +// so it can satisfy `SetAlgebra` requirements. + +extension OrderedSet { + /// Removes the elements of the given set from this set. + /// + /// var set: OrderedSet = [1, 2, 3, 4] + /// let other: OrderedSet = [6, 4, 2, 0] + /// set.subtract(other) + /// // set is now [1, 3] + /// + /// - Parameter other: Another set. + /// + /// - Complexity: Expected to be O(`self.count + other.count`) on average, if + /// `Element` implements high-quality hashing. + @inlinable + @inline(__always) + public mutating func subtract(_ other: Self) { + self = subtracting(other) + } + + // Generalizations + + /// Removes the elements of the given set from this set. + /// + /// var set: OrderedSet = [1, 2, 3, 4] + /// let other: OrderedSet = [6, 4, 2, 0] + /// set.subtract(other.unordered) + /// // set is now [1, 3] + /// + /// - Parameter other: Another set. + /// + /// - Complexity: Expected to be O(`self.count + other.count`) on average, if + /// `Element` implements high-quality hashing. + @inlinable + @inline(__always) + public mutating func subtract(_ other: UnorderedView) { + subtract(other._base) + } + + /// Removes the elements of the given sequence from this set. + /// + /// var set: OrderedSet = [1, 2, 3, 4] + /// set.subtract([6, 4, 2, 0] as Array) + /// // set is now [1, 3] + /// + /// - Parameter other: A finite sequence of elements. + /// + /// - Complexity: Expected to be O(`self.count` + *n*) on average, where *n* + /// is the number of elements in `other`, if `Element` implements + /// high-quality hashing. + @inlinable + @inline(__always) + public mutating func subtract(_ other: some Sequence) { + self = _subtracting(other) + } +} diff --git a/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra subtracting.swift b/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra subtracting.swift new file mode 100644 index 000000000..e7c91708f --- /dev/null +++ b/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra subtracting.swift @@ -0,0 +1,110 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +// `OrderedSet` does not directly conform to `SetAlgebra` because its definition +// of equality conflicts with `SetAlgebra` requirements. However, it still +// implements most `SetAlgebra` requirements (except `insert`, which is replaced +// by `append`). +// +// `OrderedSet` also provides an `unordered` view that explicitly conforms to +// `SetAlgebra`. That view implements `Equatable` by ignoring element order, +// so it can satisfy `SetAlgebra` requirements. + +extension OrderedSet { + /// Returns a new set containing the elements of this set that do not occur + /// in the given set. + /// + /// The result contains elements in the same order they appear in `self`. + /// + /// let set: OrderedSet = [1, 2, 3, 4] + /// let other: OrderedSet = [6, 4, 2, 0] + /// set.subtracting(other) // [1, 3] + /// + /// - Parameter other: Another set. + /// + /// - Returns: A new set. + /// + /// - Complexity: Expected to be O(`self.count + other.count`) on average, if + /// `Element` implements high-quality hashing. + @inlinable + @inline(__always) + public __consuming func subtracting(_ other: Self) -> Self { + _subtracting(other) + } + + // Generalizations + + /// Returns a new set containing the elements of this set that do not occur + /// in the given set. + /// + /// The result contains elements in the same order they appear in `self`. + /// + /// let set: OrderedSet = [1, 2, 3, 4] + /// let other: OrderedSet = [6, 4, 2, 0] + /// set.subtracting(other.unordered) // [1, 3] + /// + /// - Parameter other: Another set. + /// + /// - Returns: A new set. + /// + /// - Complexity: Expected to be O(`self.count + other.count`) on average, if + /// `Element` implements high-quality hashing. + @inlinable + @inline(__always) + public __consuming func subtracting(_ other: UnorderedView) -> Self { + subtracting(other._base) + } + + /// Returns a new set containing the elements of this set that do not occur + /// in the given sequence. + /// + /// The result contains elements in the same order they appear in `self`. + /// + /// let set: OrderedSet = [1, 2, 3, 4] + /// set.subtracting([6, 4, 2, 0] as Array) // [1, 3] + /// + /// - Parameter other: A finite sequence of elements. + /// + /// - Returns: A new set. + /// + /// - Complexity: Expected to be O(`self.count + other.count`) on average, if + /// `Element` implements high-quality hashing. + @inlinable + @inline(__always) + public __consuming func subtracting(_ other: some Sequence) -> Self { + _subtracting(other) + } + + @inlinable + __consuming func _subtracting(_ other: some Sequence) -> Self { + guard count > 0 else { return Self() } + return _UnsafeBitSet.withTemporaryBitSet(capacity: count) { difference in + difference.insertAll(upTo: count) + var c = count + for item in other { + if let index = self._find(item).index { + if difference.remove(index) { + c &-= 1 + if c == 0 { + return Self() + } + } + } + } + assert(c > 0) + return _extractSubset(using: difference, count: c) + } + } +} diff --git a/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra symmetricDifference.swift b/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra symmetricDifference.swift new file mode 100644 index 000000000..ad81cc640 --- /dev/null +++ b/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra symmetricDifference.swift @@ -0,0 +1,137 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +// `OrderedSet` does not directly conform to `SetAlgebra` because its definition +// of equality conflicts with `SetAlgebra` requirements. However, it still +// implements most `SetAlgebra` requirements (except `insert`, which is replaced +// by `append`). +// +// `OrderedSet` also provides an `unordered` view that explicitly conforms to +// `SetAlgebra`. That view implements `Equatable` by ignoring element order, +// so it can satisfy `SetAlgebra` requirements. + +extension OrderedSet { + /// Returns a new set with the elements that are either in this set or in + /// `other`, but not in both. + /// + /// The result contains elements from `self` followed by elements in `other`, + /// in the same order they appeared in the original sets. + /// + /// let set: OrderedSet = [1, 2, 3, 4] + /// let other: OrderedSet = [6, 4, 2, 0] + /// set.symmetricDifference(other) // [1, 3, 6, 0] + /// + /// - Parameter other: Another set. + /// + /// - Returns: A new set. + /// + /// - Complexity: Expected to be O(`self.count + other.count`) on average, if + /// `Element` implements high-quality hashing. + @inlinable + public __consuming func symmetricDifference(_ other: __owned Self) -> Self { + _UnsafeBitSet.withTemporaryBitSet(capacity: self.count) { bitset1 in + _UnsafeBitSet.withTemporaryBitSet(capacity: other.count) { bitset2 in + bitset1.insertAll(upTo: self.count) + for item in other { + if let index = self._find(item).index { + bitset1.remove(index) + } + } + bitset2.insertAll(upTo: other.count) + for item in self { + if let index = other._find(item).index { + bitset2.remove(index) + } + } + var result = self._extractSubset(using: bitset1, + extraCapacity: bitset2.count) + for offset in bitset2 { + result._appendNew(other._elements[Int(bitPattern: offset)]) + } + result._checkInvariants() + return result + } + } + } + + // Generalizations + + /// Returns a new set with the elements that are either in this set or in + /// `other`, but not in both. + /// + /// The result contains elements from `self` followed by elements in `other`, + /// in the same order they appeared in the original sets. + /// + /// let set: OrderedSet = [1, 2, 3, 4] + /// let other: OrderedSet = [6, 4, 2, 0] + /// set.symmetricDifference(other.unordered) // [1, 3, 6, 0] + /// + /// - Parameter other: Another set. + /// + /// - Returns: A new set. + /// + /// - Complexity: Expected to be O(`self.count + other.count`) on average, if + /// `Element` implements high-quality hashing. + @inlinable + @inline(__always) + public __consuming func symmetricDifference( + _ other: __owned UnorderedView + ) -> Self { + symmetricDifference(other._base) + } + + /// Returns a new set with the elements that are either in this set or in + /// `other`, but not in both. + /// + /// The result contains elements from `self` followed by elements in `other`, + /// in the same order they appeared in the original input values. + /// + /// let set: OrderedSet = [1, 2, 3, 4] + /// let other: Array = [6, 4, 2, 0] + /// set.symmetricDifference(other) // [1, 3, 6, 0] + /// + /// In case the sequence contains duplicate elements, only the first instance + /// matters -- the second and subsequent instances are ignored by this method. + /// + /// - Parameter other: A finite sequence of elements. + /// + /// - Returns: A new set. + /// + /// - Complexity: Expected to be O(`self.count` + *n*) on average where *n* is + /// the number of elements in `other`, if `Element` implements high-quality + /// hashing. + @inlinable + public __consuming func symmetricDifference( + _ other: __owned some Sequence + ) -> Self { + _UnsafeBitSet.withTemporaryBitSet(capacity: self.count) { bitset in + var new = Self() + bitset.insertAll(upTo: self.count) + for item in other { + if let index = self._find(item).index { + bitset.remove(index) + } else { + new.append(item) + } + } + var result = _extractSubset(using: bitset, extraCapacity: new.count) + for item in new._elements { + result._appendNew(item) + } + result._checkInvariants() + return result + } + } +} diff --git a/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra union.swift b/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra union.swift new file mode 100644 index 000000000..b8243a5b9 --- /dev/null +++ b/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra union.swift @@ -0,0 +1,85 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +// `OrderedSet` does not directly conform to `SetAlgebra` because its definition +// of equality conflicts with `SetAlgebra` requirements. However, it still +// implements most `SetAlgebra` requirements (except `insert`, which is replaced +// by `append`). +// +// `OrderedSet` also provides an `unordered` view that explicitly conforms to +// `SetAlgebra`. That view implements `Equatable` by ignoring element order, +// so it can satisfy `SetAlgebra` requirements. + +extension OrderedSet { + /// Returns a new set with the elements of both this and the given set. + /// + /// Members of `other` that aren't already in `self` get appended to the end + /// of the result, in the order they appear in `other`. + /// + /// let a: OrderedSet = [1, 2, 3, 4] + /// let b: OrderedSet = [0, 2, 4, 6] + /// a.union(b) // [1, 2, 3, 4, 0, 6] + /// + /// - Parameter other: The set of elements to add. + /// + /// - Complexity: Expected to be O(`self.count` + `other.count`) on average, + /// if `Element` implements high-quality hashing. + @inlinable + public __consuming func union(_ other: __owned Self) -> Self { + var result = self + result.formUnion(other) + return result + } + + // Generalizations + + /// Returns a new set with the elements of both this and the given set. + /// + /// Members of `other` that aren't already in `self` get appended to the end + /// of the result, in the order they appear in `other`. + /// + /// let a: OrderedSet = [1, 2, 3, 4] + /// let b: OrderedSet = [0, 2, 4, 6] + /// a.union(b.unordered) // [1, 2, 3, 4, 0, 6] + /// + /// - Parameter other: The set of elements to add. + /// + /// - Complexity: Expected to be O(`self.count` + `other.count`) on average, + /// if `Element` implements high-quality hashing. + @inlinable + @inline(__always) + public __consuming func union(_ other: __owned UnorderedView) -> Self { + union(other._base) + } + + /// Returns a new set with the elements of both this and the given set. + /// + /// Members of `other` that aren't already in `self` get appended to the end + /// of the result, in the order they appear in `other`. + /// + /// let a: OrderedSet = [1, 2, 3, 4] + /// let b: Array = [0, 2, 4, 6] + /// a.union(b) // [1, 2, 3, 4, 0, 6] + /// + /// - Parameter other: A finite sequence of elements. + /// + /// - Complexity: Expected to be O(`self.count` + `other.count`) on average, + /// if `Element` implements high-quality hashing. + @inlinable + public __consuming func union( + _ other: __owned some Sequence + ) -> Self { + var result = self + result.formUnion(other) + return result + } +} + diff --git a/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra+Basics.swift b/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra+Basics.swift index ecc6f7b5d..d37f05084 100644 --- a/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra+Basics.swift +++ b/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra+Basics.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 diff --git a/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra+Operations.swift b/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra+Operations.swift deleted file mode 100644 index 474d74312..000000000 --- a/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra+Operations.swift +++ /dev/null @@ -1,589 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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 -// -//===----------------------------------------------------------------------===// - -// `OrderedSet` does not directly conform to `SetAlgebra` because its definition -// of equality conflicts with `SetAlgebra` requirements. However, it still -// implements most `SetAlgebra` requirements (except `insert`, which is replaced -// by `append`). -// -// `OrderedSet` also provides an `unordered` view that explicitly conforms to -// `SetAlgebra`. That view implements `Equatable` by ignoring element order, -// so it can satisfy `SetAlgebra` requirements. - -extension OrderedSet { - /// Adds the elements of the given set to this set. - /// - /// Members of `other` that aren't already in `self` get appended to the end - /// of the set, in the order they appear in `other`. - /// - /// var a: OrderedSet = [1, 2, 3, 4] - /// let b: OrderedSet = [0, 2, 4, 6] - /// a.formUnion(b) - /// // `a` is now `[1, 2, 3, 4, 0, 6]` - /// - /// - Parameter other: The set of elements to insert. - /// - /// - Complexity: Expected to be O(`other.count`) on average, if `Element` - /// implements high-quality hashing. - @inlinable - public mutating func formUnion(_ other: __owned Self) { - append(contentsOf: other) - } - - /// Returns a new set with the elements of both this and the given set. - /// - /// Members of `other` that aren't already in `self` get appended to the end - /// of the result, in the order they appear in `other`. - /// - /// let a: OrderedSet = [1, 2, 3, 4] - /// let b: OrderedSet = [0, 2, 4, 6] - /// a.union(b) // [1, 2, 3, 4, 0, 6] - /// - /// - Parameter other: The set of elements to add. - /// - /// - Complexity: Expected to be O(`self.count` + `other.count`) on average, - /// if `Element` implements high-quality hashing. - @inlinable - public __consuming func union(_ other: __owned Self) -> Self { - var result = self - result.formUnion(other) - return result - } - - // Generalizations - - /// Adds the elements of the given set to this set. - /// - /// Members of `other` that aren't already in `self` get appended to the end - /// of the set, in the order they appear in `other`. - /// - /// let a: OrderedSet = [1, 2, 3, 4] - /// let b: OrderedSet = [0, 2, 4, 6] - /// a.formUnion(b.unordered) - /// // a is now [1, 2, 3, 4, 0, 6] - /// - /// - Parameter other: The set of elements to add. - /// - /// - Complexity: Expected to be O(`self.count` + `other.count`) on average, - /// if `Element` implements high-quality hashing. - @inlinable - @inline(__always) - public mutating func formUnion(_ other: __owned UnorderedView) { - formUnion(other._base) - } - - /// Returns a new set with the elements of both this and the given set. - /// - /// Members of `other` that aren't already in `self` get appended to the end - /// of the result, in the order they appear in `other`. - /// - /// let a: OrderedSet = [1, 2, 3, 4] - /// let b: OrderedSet = [0, 2, 4, 6] - /// a.union(b.unordered) // [1, 2, 3, 4, 0, 6] - /// - /// - Parameter other: The set of elements to add. - /// - /// - Complexity: Expected to be O(`self.count` + `other.count`) on average, - /// if `Element` implements high-quality hashing. - @inlinable - @inline(__always) - public __consuming func union(_ other: __owned UnorderedView) -> Self { - union(other._base) - } - - /// Adds the elements of the given sequence to this set. - /// - /// Members of `other` that aren't already in `self` get appended to the end - /// of the set, in the order they appear in `other`. - /// - /// let a: OrderedSet = [1, 2, 3, 4] - /// let b: Array = [0, 2, 4, 6] - /// a.formUnion(b) - /// // a is now [1, 2, 3, 4, 0, 6] - /// - /// - Parameter other: A finite sequence of elements. - /// - /// - Complexity: Expected to be O(`self.count` + `other.count`) on average, - /// if `Element` implements high-quality hashing. - @inlinable - public mutating func formUnion( - _ other: __owned S - ) where S.Element == Element { - append(contentsOf: other) - } - - /// Returns a new set with the elements of both this and the given set. - /// - /// Members of `other` that aren't already in `self` get appended to the end - /// of the result, in the order they appear in `other`. - /// - /// let a: OrderedSet = [1, 2, 3, 4] - /// let b: Array = [0, 2, 4, 6] - /// a.union(b) // [1, 2, 3, 4, 0, 6] - /// - /// - Parameter other: A finite sequence of elements. - /// - /// - Complexity: Expected to be O(`self.count` + `other.count`) on average, - /// if `Element` implements high-quality hashing. - @inlinable - public __consuming func union( - _ other: __owned S - ) -> Self where S.Element == Element { - var result = self - result.formUnion(other) - return result - } -} - -extension OrderedSet { - /// Returns a new set with the elements that are common to both this set and - /// the provided other one, in the order they appear in `self`. - /// - /// let set: OrderedSet = [1, 2, 3, 4] - /// let other: OrderedSet = [6, 4, 2, 0] - /// set.intersection(other) // [2, 4] - /// - /// - Parameter other: Another set. - /// - /// - Returns: A new set. - /// - /// - Complexity: Expected to be O(`self.count`) on average, if `Element` - /// implements high-quality hashing. - @inlinable - public __consuming func intersection(_ other: Self) -> Self { - var result = Self() - for item in self { - if other.contains(item) { - result._appendNew(item) - } - } - result._checkInvariants() - return result - } - - /// Removes the elements of this set that aren't also in the given one. - /// - /// var set: OrderedSet = [1, 2, 3, 4] - /// let other: OrderedSet = [6, 4, 2, 0] - /// set.formIntersection(other) - /// // set is now [2, 4] - /// - /// - Parameter other: A set of elements. - /// - /// - Complexity: Expected to be O(`self.count`) on average, if `Element` - /// implements high-quality hashing. - @inlinable - public mutating func formIntersection(_ other: Self) { - self = self.intersection(other) - } - - // Generalizations - - /// Returns a new set with the elements that are common to both this set and - /// the provided other one, in the order they appear in `self`. - /// - /// let set: OrderedSet = [1, 2, 3, 4] - /// let other: OrderedSet = [6, 4, 2, 0] - /// set.intersection(other) // [2, 4] - /// - /// - Parameter other: Another set. - /// - /// - Returns: A new set. - /// - /// - Complexity: Expected to be O(`self.count`) on average, if `Element` - /// implements high-quality hashing. - @inlinable - @inline(__always) - public __consuming func intersection(_ other: UnorderedView) -> Self { - intersection(other._base) - } - - /// Removes the elements of this set that aren't also in the given one. - /// - /// var set: OrderedSet = [1, 2, 3, 4] - /// let other: OrderedSet = [6, 4, 2, 0] - /// set.formIntersection(other) - /// // set is now [2, 4] - /// - /// - Parameter other: A set of elements. - /// - /// - Complexity: Expected to be O(`self.count`) on average, if `Element` - /// implements high-quality hashing. - @inlinable - @inline(__always) - public mutating func formIntersection(_ other: UnorderedView) { - formIntersection(other._base) - } - - /// Returns a new set with the elements that are common to both this set and - /// the provided sequence, in the order they appear in `self`. - /// - /// let set: OrderedSet = [1, 2, 3, 4] - /// set.intersection([6, 4, 2, 0] as Array) // [2, 4] - /// - /// - Parameter other: A finite sequence of elements. - /// - /// - Returns: A new set. - /// - /// - Complexity: Expected to be O(*n*) on average where *n* is the number of - /// elements in `other`, if `Element` implements high-quality hashing. - @inlinable - public __consuming func intersection( - _ other: S - ) -> Self where S.Element == Element { - _UnsafeBitset.withTemporaryBitset(capacity: self.count) { bitset in - for item in other { - if let index = self._find_inlined(item).index { - bitset.insert(index) - } - } - let result = self._extractSubset(using: bitset) - result._checkInvariants() - return result - } - } - - /// Removes the elements of this set that aren't also in the given sequence. - /// - /// var set: OrderedSet = [1, 2, 3, 4] - /// set.formIntersection([6, 4, 2, 0] as Array) - /// // set is now [2, 4] - /// - /// - Parameter other: A finite sequence of elements. - /// - /// - Complexity: Expected to be O(*n*) on average where *n* is the number of - /// elements in `other`, if `Element` implements high-quality hashing. - @inlinable - public mutating func formIntersection( - _ other: S - ) where S.Element == Element { - self = self.intersection(other) - } -} - -extension OrderedSet { - /// Returns a new set with the elements that are either in this set or in - /// `other`, but not in both. - /// - /// The result contains elements from `self` followed by elements in `other`, - /// in the same order they appeared in the original sets. - /// - /// let set: OrderedSet = [1, 2, 3, 4] - /// let other: OrderedSet = [6, 4, 2, 0] - /// set.symmetricDifference(other) // [1, 3, 6, 0] - /// - /// - Parameter other: Another set. - /// - /// - Returns: A new set. - /// - /// - Complexity: Expected to be O(`self.count + other.count`) on average, if - /// `Element` implements high-quality hashing. - @inlinable - public __consuming func symmetricDifference(_ other: __owned Self) -> Self { - _UnsafeBitset.withTemporaryBitset(capacity: self.count) { bitset1 in - _UnsafeBitset.withTemporaryBitset(capacity: other.count) { bitset2 in - bitset1.insertAll(upTo: self.count) - for item in other { - if let index = self._find(item).index { - bitset1.remove(index) - } - } - bitset2.insertAll(upTo: other.count) - for item in self { - if let index = other._find(item).index { - bitset2.remove(index) - } - } - var result = self._extractSubset(using: bitset1, - extraCapacity: bitset2.count) - for offset in bitset2 { - result._appendNew(other._elements[offset]) - } - result._checkInvariants() - return result - } - } - } - - /// Replace this set with the elements contained in this set or the given - /// set, but not both. - /// - /// On return, `self` contains elements originally from `self` followed by - /// elements in `other`, in the same order they appeared in the input values. - /// - /// var set: OrderedSet = [1, 2, 3, 4] - /// let other: OrderedSet = [6, 4, 2, 0] - /// set.formSymmetricDifference(other) - /// // set is now [1, 3, 6, 0] - /// - /// - Parameter other: Another set. - /// - /// - Complexity: Expected to be O(`self.count + other.count`) on average, if - /// `Element` implements high-quality hashing. - @inlinable - public mutating func formSymmetricDifference(_ other: __owned Self) { - self = self.symmetricDifference(other) - } - - // Generalizations - - /// Returns a new set with the elements that are either in this set or in - /// `other`, but not in both. - /// - /// The result contains elements from `self` followed by elements in `other`, - /// in the same order they appeared in the original sets. - /// - /// let set: OrderedSet = [1, 2, 3, 4] - /// let other: OrderedSet = [6, 4, 2, 0] - /// set.symmetricDifference(other.unordered) // [1, 3, 6, 0] - /// - /// - Parameter other: Another set. - /// - /// - Returns: A new set. - /// - /// - Complexity: Expected to be O(`self.count + other.count`) on average, if - /// `Element` implements high-quality hashing. - @inlinable - @inline(__always) - public __consuming func symmetricDifference( - _ other: __owned UnorderedView - ) -> Self { - symmetricDifference(other._base) - } - - /// Replace this set with the elements contained in this set or the given - /// set, but not both. - /// - /// On return, `self` contains elements originally from `self` followed by - /// elements in `other`, in the same order they appeared in the input values. - /// - /// var set: OrderedSet = [1, 2, 3, 4] - /// let other: OrderedSet = [6, 4, 2, 0] - /// set.formSymmetricDifference(other.unordered) - /// // set is now [1, 3, 6, 0] - /// - /// - Parameter other: Another set. - /// - /// - Complexity: Expected to be O(`self.count + other.count`) on average, if - /// `Element` implements high-quality hashing. - @inlinable - @inline(__always) - public mutating func formSymmetricDifference(_ other: __owned UnorderedView) { - formSymmetricDifference(other._base) - } - - /// Returns a new set with the elements that are either in this set or in - /// `other`, but not in both. - /// - /// The result contains elements from `self` followed by elements in `other`, - /// in the same order they appeared in the original input values. - /// - /// let set: OrderedSet = [1, 2, 3, 4] - /// let other: Array = [6, 4, 2, 0] - /// set.symmetricDifference(other) // [1, 3, 6, 0] - /// - /// - Parameter other: A finite sequence of elements. - /// - /// - Returns: A new set. - /// - /// - Complexity: Expected to be O(`self.count` + *n*) on average where *n* is - /// the number of elements in `other`, if `Element` implements high-quality - /// hashing. - @inlinable - public __consuming func symmetricDifference( - _ other: __owned S - ) -> Self where S.Element == Element { - _UnsafeBitset.withTemporaryBitset(capacity: self.count) { bitset in - var new = Self() - bitset.insertAll(upTo: self.count) - for item in other { - if let index = self._find(item).index { - bitset.remove(index) - } else { - new.append(item) - } - } - var result = _extractSubset(using: bitset, extraCapacity: new.count) - for item in new._elements { - result._appendNew(item) - } - result._checkInvariants() - return result - } - } - - /// Replace this set with the elements contained in this set or the given - /// sequence, but not both. - /// - /// On return, `self` contains elements originally from `self` followed by - /// elements in `other`, in the same order they first appeared in the input - /// values. - /// - /// var set: OrderedSet = [1, 2, 3, 4] - /// set.formSymmetricDifference([6, 4, 2, 0] as Array) - /// // set is now [1, 3, 6, 0] - /// - /// - Parameter other: A finite sequence of elements. - /// - /// - Complexity: Expected to be O(`self.count` + *n*) on average where *n* is - /// the number of elements in `other`, if `Element` implements high-quality - /// hashing. - @inlinable - public mutating func formSymmetricDifference( - _ other: __owned S - ) where S.Element == Element { - self = self.symmetricDifference(other) - } -} - -extension OrderedSet { - /// Returns a new set containing the elements of this set that do not occur - /// in the given set. - /// - /// The result contains elements in the same order they appear in `self`. - /// - /// let set: OrderedSet = [1, 2, 3, 4] - /// let other: OrderedSet = [6, 4, 2, 0] - /// set.subtracting(other) // [1, 3] - /// - /// - Parameter other: Another set. - /// - /// - Returns: A new set. - /// - /// - Complexity: Expected to be O(`self.count + other.count`) on average, if - /// `Element` implements high-quality hashing. - @inlinable - @inline(__always) - public __consuming func subtracting(_ other: Self) -> Self { - _subtracting(other) - } - - /// Removes the elements of the given set from this set. - /// - /// var set: OrderedSet = [1, 2, 3, 4] - /// let other: OrderedSet = [6, 4, 2, 0] - /// set.subtract(other) - /// // set is now [1, 3] - /// - /// - Parameter other: Another set. - /// - /// - Complexity: Expected to be O(`self.count + other.count`) on average, if - /// `Element` implements high-quality hashing. - @inlinable - @inline(__always) - public mutating func subtract(_ other: Self) { - self = subtracting(other) - } - - // Generalizations - - /// Returns a new set containing the elements of this set that do not occur - /// in the given set. - /// - /// The result contains elements in the same order they appear in `self`. - /// - /// let set: OrderedSet = [1, 2, 3, 4] - /// let other: OrderedSet = [6, 4, 2, 0] - /// set.subtracting(other.unordered) // [1, 3] - /// - /// - Parameter other: Another set. - /// - /// - Returns: A new set. - /// - /// - Complexity: Expected to be O(`self.count + other.count`) on average, if - /// `Element` implements high-quality hashing. - @inlinable - @inline(__always) - public __consuming func subtracting(_ other: UnorderedView) -> Self { - subtracting(other._base) - } - - /// Removes the elements of the given set from this set. - /// - /// var set: OrderedSet = [1, 2, 3, 4] - /// let other: OrderedSet = [6, 4, 2, 0] - /// set.subtract(other.unordered) - /// // set is now [1, 3] - /// - /// - Parameter other: Another set. - /// - /// - Complexity: Expected to be O(`self.count + other.count`) on average, if - /// `Element` implements high-quality hashing. - @inlinable - @inline(__always) - public mutating func subtract(_ other: UnorderedView) { - subtract(other._base) - } - - /// Returns a new set containing the elements of this set that do not occur - /// in the given sequence. - /// - /// The result contains elements in the same order they appear in `self`. - /// - /// let set: OrderedSet = [1, 2, 3, 4] - /// set.subtracting([6, 4, 2, 0] as Array) // [1, 3] - /// - /// - Parameter other: A finite sequence of elements. - /// - /// - Returns: A new set. - /// - /// - Complexity: Expected to be O(`self.count + other.count`) on average, if - /// `Element` implements high-quality hashing. - @inlinable - @inline(__always) - public __consuming func subtracting( - _ other: S - ) -> Self where S.Element == Element { - _subtracting(other) - } - - /// Removes the elements of the given sequence from this set. - /// - /// var set: OrderedSet = [1, 2, 3, 4] - /// set.subtract([6, 4, 2, 0] as Array) - /// // set is now [1, 3] - /// - /// - Parameter other: A finite sequence of elements. - /// - /// - Complexity: Expected to be O(`self.count` + *n*) on average, where *n* - /// is the number of elements in `other`, if `Element` implements - /// high-quality hashing. - @inlinable - @inline(__always) - public mutating func subtract( - _ other: S - ) where S.Element == Element { - self = _subtracting(other) - } - - @inlinable - __consuming func _subtracting( - _ other: S - ) -> Self where S.Element == Element { - guard count > 0 else { return Self() } - return _UnsafeBitset.withTemporaryBitset(capacity: count) { difference in - difference.insertAll(upTo: count) - for item in other { - if let index = self._find(item).index { - if difference.remove(index), difference.count == 0 { - return Self() - } - } - } - assert(difference.count > 0) - let result = _extractSubset(using: difference) - result._checkInvariants() - return result - } - } -} - - diff --git a/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra+Predicates.swift b/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra+Predicates.swift deleted file mode 100644 index e975a0b4b..000000000 --- a/Sources/OrderedCollections/OrderedSet/OrderedSet+Partial SetAlgebra+Predicates.swift +++ /dev/null @@ -1,551 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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 -// -//===----------------------------------------------------------------------===// - -// `OrderedSet` does not directly conform to `SetAlgebra` because its definition -// of equality conflicts with `SetAlgebra` requirements. However, it still -// implements most `SetAlgebra` requirements (except `insert`, which is replaced -// by `append`). -// -// `OrderedSet` also provides an `unordered` view that explicitly conforms to -// `SetAlgebra`. That view implements `Equatable` by ignoring element order, -// so it can satisfy `SetAlgebra` requirements. - -extension OrderedSet { - /// Returns a Boolean value that indicates whether this set is a subset of - /// the given set. - /// - /// Set *A* is a subset of another set *B* if every member of *A* is also a - /// member of *B*, ignoring the order they appear in the two sets. - /// - /// let a: OrderedSet = [1, 2, 3, 4] - /// let b: OrderedSet = [4, 2, 1] - /// b.isSubset(of: a) // true - /// - /// - Parameter other: Another set. - /// - /// - Returns: `true` if the set is a subset of `other`; otherwise, `false`. - /// - /// - Complexity: Expected to be O(`self.count`) on average, if `Element` - /// implements high-quality hashing. - @inlinable - public func isSubset(of other: Self) -> Bool { - guard other.count >= self.count else { return false } - for item in self { - guard other.contains(item) else { return false } - } - return true - } - - // Generalizations - - /// Returns a Boolean value that indicates whether this set is a subset of - /// the given set. - /// - /// Set *A* is a subset of another set *B* if every member of *A* is also a - /// member of *B*, ignoring the order they appear in the two sets. - /// - /// let a: OrderedSet = [1, 2, 3, 4] - /// let b: OrderedSet = [4, 2, 1] - /// b.isSubset(of: a.unordered) // true - /// - /// - Parameter other: Another set. - /// - /// - Returns: `true` if the set is a subset of `other`; otherwise, `false`. - /// - /// - Complexity: Expected to be O(`self.count`) on average, if `Element` - /// implements high-quality hashing. - @inlinable - @inline(__always) - public func isSubset(of other: UnorderedView) -> Bool { - isSubset(of: other._base) - } - - /// Returns a Boolean value that indicates whether this set is a subset of - /// the given set. - /// - /// Set *A* is a subset of another set *B* if every member of *A* is also a - /// member of *B*, ignoring the order they appear in the two sets. - /// - /// let a: OrderedSet = [1, 2, 3, 4] - /// let b: Set = [4, 2, 1] - /// b.isSubset(of: a) // true - /// - /// - Parameter other: Another set. - /// - /// - Returns: `true` if the set is a subset of `other`; otherwise, `false`. - /// - /// - Complexity: Expected to be O(`self.count`) on average, if `Element` - /// implements high-quality hashing. - @inlinable - public func isSubset(of other: Set) -> Bool { - guard other.count >= self.count else { return false } - for item in self { - guard other.contains(item) else { return false } - } - return true - } - - /// Returns a Boolean value that indicates whether this set is a subset of - /// the elements in the given sequence. - /// - /// Set *A* is a subset of another set *B* if every member of *A* is also a - /// member of *B*, ignoring the order they appear in the two sets. - /// - /// let a: Array = [1, 2, 3, 4] - /// let b: OrderedSet = [4, 2, 1] - /// b.isSubset(of: a) // true - /// - /// - Parameter other: A finite sequence. - /// - /// - Returns: `true` if the set is a subset of `other`; otherwise, `false`. - /// - /// - Complexity: Expected to be O(`self.count` + *n*) on average, where *n* - /// is the number of elements in `other`, if `Element` implements - /// high-quality hashing. - @inlinable - public func isSubset( - of other: S - ) -> Bool where S.Element == Element { - guard !isEmpty else { return true } - return _UnsafeBitset.withTemporaryBitset(capacity: count) { seen in - // Mark elements in `self` that we've seen in `other`. - for item in other { - if let index = _find(item).index { - if seen.insert(index), seen.count == self.count { - // We've seen enough. - return true - } - } - } - return false - } - } -} - -extension OrderedSet { - /// Returns a Boolean value that indicates whether this set is a superset of - /// the given set. - /// - /// Set *A* is a superset of another set *B* if every member of *B* is also a - /// member of *A*, ignoring the order they appear in the two sets. - /// - /// let a: OrderedSet = [1, 2, 3, 4] - /// let b: OrderedSet = [4, 2, 1] - /// a.isSuperset(of: b) // true - /// - /// - Parameter other: Another set. - /// - /// - Returns: `true` if the set is a superset of `other`; otherwise, `false`. - /// - /// - Complexity: Expected to be O(`other.count`) on average, if `Element` - /// implements high-quality hashing. - @inlinable - public func isSuperset(of other: Self) -> Bool { - other.isSubset(of: self) - } - - // Generalizations - - /// Returns a Boolean value that indicates whether this set is a superset of - /// the given set. - /// - /// Set *A* is a superset of another set *B* if every member of *B* is also a - /// member of *A*, ignoring the order they appear in the two sets. - /// - /// let a: OrderedSet = [1, 2, 3, 4] - /// let b: Set = [4, 2, 1] - /// a.isSuperset(of: b) // true - /// - /// - Parameter other: Another set. - /// - /// - Returns: `true` if the set is a superset of `other`; otherwise, `false`. - /// - /// - Complexity: Expected to be O(`other.count`) on average, if `Element` - /// implements high-quality hashing. - @inlinable - public func isSuperset(of other: UnorderedView) -> Bool { - isSuperset(of: other._base) - } - - @inlinable - public func isSuperset(of other: Set) -> Bool { - guard self.count >= other.count else { return false } - return _isSuperset(of: other) - } - - /// Returns a Boolean value that indicates whether this set is a superset of - /// the given sequence. - /// - /// Set *A* is a superset of another set *B* if every member of *B* is also a - /// member of *A*, ignoring the order they appear in the two sets. - /// - /// let a: OrderedSet = [1, 2, 3, 4] - /// let b: Array = [4, 2, 1] - /// a.isSuperset(of: b) // true - /// - /// - Parameter other: A finite sequence of elements. - /// - /// - Returns: `true` if the set is a superset of `other`; otherwise, `false`. - /// - /// - Complexity: Expected to be O(*n*) on average, where *n* is the number of - /// elements in `other`, if `Element` implements high-quality hashing. - @inlinable - public func isSuperset( - of other: S - ) -> Bool where S.Element == Element { - _isSuperset(of: other) - } - - @inlinable - internal func _isSuperset( - of other: S - ) -> Bool where S.Element == Element { - for item in other { - guard self.contains(item) else { return false } - } - return true - } -} - -extension OrderedSet { - /// Returns a Boolean value that indicates whether the set is a strict subset - /// of the given set. - /// - /// Set *A* is a strict subset of another set *B* if every member of *A* is - /// also a member of *B* and *B* contains at least one element that is not a - /// member of *A*. (Ignoring the order the elements appear in the sets.) - /// - /// let a: OrderedSet = [1, 2, 3, 4] - /// let b: OrderedSet = [4, 2, 1] - /// b.isStrictSubset(of: a) // true - /// - /// - Parameter other: Another set. - /// - /// - Returns: `true` if `self` is a strict subset of `other`; otherwise, - /// `false`. - /// - /// - Complexity: Expected to be O(`self.count`) on average, if `Element` - /// implements high-quality hashing. - @inlinable - public func isStrictSubset(of other: Self) -> Bool { - self.count < other.count && self.isSubset(of: other) - } - - // Generalizations - - /// Returns a Boolean value that indicates whether the set is a strict subset - /// of the given set. - /// - /// Set *A* is a strict subset of another set *B* if every member of *A* is - /// also a member of *B* and *B* contains at least one element that is not a - /// member of *A*. (Ignoring the order the elements appear in the sets.) - /// - /// let a: OrderedSet = [1, 2, 3, 4] - /// let b: OrderedSet = [4, 2, 1] - /// b.isStrictSubset(of: a.unordered) // true - /// - /// - Parameter other: Another set. - /// - /// - Returns: `true` if `self` is a strict subset of `other`; otherwise, - /// `false`. - /// - /// - Complexity: Expected to be O(`self.count`) on average, if `Element` - /// implements high-quality hashing. - @inlinable - @inline(__always) - public func isStrictSubset(of other: UnorderedView) -> Bool { - isStrictSubset(of: other._base) - } - - /// Returns a Boolean value that indicates whether the set is a strict subset - /// of the given set. - /// - /// Set *A* is a strict subset of another set *B* if every member of *A* is - /// also a member of *B* and *B* contains at least one element that is not a - /// member of *A*. (Ignoring the order the elements appear in the sets.) - /// - /// let a: Set = [1, 2, 3, 4] - /// let b: OrderedSet = [4, 2, 1] - /// b.isStrictSubset(of: a) // true - /// - /// - Parameter other: Another set. - /// - /// - Returns: `true` if `self` is a strict subset of `other`; otherwise, - /// `false`. - /// - /// - Complexity: Expected to be O(`self.count`) on average, if `Element` - /// implements high-quality hashing. - @inlinable - public func isStrictSubset(of other: Set) -> Bool { - self.count < other.count && self.isSubset(of: other) - } - - /// Returns a Boolean value that indicates whether the set is a strict subset - /// of the given sequence. - /// - /// Set *A* is a strict subset of another set *B* if every member of *A* is - /// also a member of *B* and *B* contains at least one element that is not a - /// member of *A*. (Ignoring the order the elements appear in the sets.) - /// - /// let a: Array = [1, 2, 3, 4] - /// let b: OrderedSet = [4, 2, 1] - /// b.isStrictSubset(of: a) // true - /// - /// - Parameter other: A finite sequence of elements. - /// - /// - Returns: `true` if `self` is a strict subset of `other`; otherwise, - /// `false`. - /// - /// - Complexity: Expected to be O(`self.count` + *n*) on average, where *n* - /// is the number of elements in `other`, if `Element` implements - /// high-quality hashing. - @inlinable - public func isStrictSubset( - of other: S - ) -> Bool where S.Element == Element { - _UnsafeBitset.withTemporaryBitset(capacity: count) { seen in - // Mark elements in `self` that we've seen in `other`. - var isKnownStrict = false - for item in other { - if let index = _find(item).index { - if seen.insert(index), seen.count == self.count, isKnownStrict { - // We've seen enough. - return true - } - } else { - if !isKnownStrict, seen.count == self.count { return true } - isKnownStrict = true - } - } - return false - } - } -} - -extension OrderedSet { - /// Returns a Boolean value that indicates whether the set is a strict - /// superset of the given set. - /// - /// Set *A* is a strict superset of another set *B* if every member of *B* is - /// also a member of *A* and *A* contains at least one element that is *not* - /// a member of *B*. (Ignoring the order the elements appear in the sets.) - /// - /// let a: OrderedSet = [1, 2, 3, 4] - /// let b: OrderedSet = [4, 2, 1] - /// a.isStrictSuperset(of: b) // true - /// - /// - Parameter other: Another set. - /// - /// - Returns: `true` if `self` is a strict superset of `other`; otherwise, - /// `false`. - /// - /// - Complexity: Expected to be O(`other.count`) on average, if `Element` - /// implements high-quality hashing. - @inlinable - public func isStrictSuperset(of other: Self) -> Bool { - self.count > other.count && other.isSubset(of: self) - } - - // Generalizations - - /// Returns a Boolean value that indicates whether the set is a strict - /// superset of the given set. - /// - /// Set *A* is a strict superset of another set *B* if every member of *B* is - /// also a member of *A* and *A* contains at least one element that is *not* - /// a member of *B*. (Ignoring the order the elements appear in the sets.) - /// - /// let a: OrderedSet = [1, 2, 3, 4] - /// let b: OrderedSet = [4, 2, 1] - /// a.isStrictSuperset(of: b.unordered) // true - /// - /// - Parameter other: Another set. - /// - /// - Returns: `true` if `self` is a strict superset of `other`; otherwise, - /// `false`. - /// - /// - Complexity: Expected to be O(`other.count`) on average, if `Element` - /// implements high-quality hashing. - @inlinable - @inline(__always) - public func isStrictSuperset(of other: UnorderedView) -> Bool { - isStrictSuperset(of: other._base) - } - - /// Returns a Boolean value that indicates whether the set is a strict - /// superset of the given set. - /// - /// Set *A* is a strict superset of another set *B* if every member of *B* is - /// also a member of *A* and *A* contains at least one element that is *not* - /// a member of *B*. (Ignoring the order the elements appear in the sets.) - /// - /// let a: OrderedSet = [1, 2, 3, 4] - /// let b: Set = [4, 2, 1] - /// a.isStrictSuperset(of: b) // true - /// - /// - Parameter other: Another set. - /// - /// - Returns: `true` if `self` is a strict superset of `other`; otherwise, - /// `false`. - /// - /// - Complexity: Expected to be O(`other.count`) on average, if `Element` - /// implements high-quality hashing. - @inlinable - public func isStrictSuperset(of other: Set) -> Bool { - self.count > other.count && other.isSubset(of: self) - } - - /// Returns a Boolean value that indicates whether the set is a strict - /// superset of the given sequence. - /// - /// Set *A* is a strict superset of another set *B* if every member of *B* is - /// also a member of *A* and *A* contains at least one element that is *not* - /// a member of *B*. (Ignoring the order the elements appear in the sets.) - /// - /// let a: OrderedSet = [1, 2, 3, 4] - /// let b: Array = [4, 2, 1] - /// a.isStrictSuperset(of: b) // true - /// - /// - Parameter other: A finite sequence of elements. - /// - /// - Returns: `true` if `self` is a strict superset of `other`; otherwise, - /// `false`. - /// - /// - Complexity: Expected to be O(`self.count` + *n*) on average, where *n* - /// is the number of elements in `other`, if `Element` implements - /// high-quality hashing. - @inlinable - public func isStrictSuperset( - of other: S - ) -> Bool where S.Element == Element { - _UnsafeBitset.withTemporaryBitset(capacity: count) { seen in - // Mark elements in `self` that we've seen in `other`. - for item in other { - guard let index = _find(item).index else { - return false - } - if seen.insert(index), seen.count == self.count { - // We've seen enough. - return false - } - } - return seen.count < self.count - } - } -} - -extension OrderedSet { - /// Returns a Boolean value that indicates whether the set has no members in - /// common with the given set. - /// - /// let a: OrderedSet = [1, 2, 3, 4] - /// let b: OrderedSet = [5, 6] - /// a.isDisjoint(with: b) // true - /// - /// - Parameter other: Another set. - /// - /// - Returns: `true` if `self` has no elements in common with `other`; - /// otherwise, `false`. - /// - /// - Complexity: Expected to be O(min(`self.count`, `other.count`)) on - /// average, if `Element` implements high-quality hashing. - @inlinable - public func isDisjoint(with other: Self) -> Bool { - guard !self.isEmpty && !other.isEmpty else { return true } - if self.count <= other.count { - for item in self { - if other.contains(item) { return false } - } - } else { - for item in other { - if self.contains(item) { return false } - } - } - return true - } - - // Generalizations - - /// Returns a Boolean value that indicates whether the set has no members in - /// common with the given set. - /// - /// let a: OrderedSet = [1, 2, 3, 4] - /// let b: OrderedSet = [5, 6] - /// a.isDisjoint(with: b.unordered) // true - /// - /// - Parameter other: Another set. - /// - /// - Returns: `true` if `self` has no elements in common with `other`; - /// otherwise, `false`. - /// - /// - Complexity: Expected to be O(min(`self.count`, `other.count`)) on - /// average, if `Element` implements high-quality hashing. - @inlinable - @inline(__always) - public func isDisjoint(with other: UnorderedView) -> Bool { - isDisjoint(with: other._base) - } - - /// Returns a Boolean value that indicates whether the set has no members in - /// common with the given set. - /// - /// let a: OrderedSet = [1, 2, 3, 4] - /// let b: Set = [5, 6] - /// a.isDisjoint(with: b) // true - /// - /// - Parameter other: Another set. - /// - /// - Returns: `true` if `self` has no elements in common with `other`; - /// otherwise, `false`. - /// - /// - Complexity: Expected to be O(min(`self.count`, `other.count`)) on - /// average, if `Element` implements high-quality hashing. - @inlinable - public func isDisjoint(with other: Set) -> Bool { - guard !self.isEmpty && !other.isEmpty else { return true } - if self.count <= other.count { - for item in self { - if other.contains(item) { return false } - } - } else { - for item in other { - if self.contains(item) { return false } - } - } - return true - } - - /// Returns a Boolean value that indicates whether the set has no members in - /// common with the given sequence. - /// - /// let a: OrderedSet = [1, 2, 3, 4] - /// let b: Array = [5, 6] - /// a.isDisjoint(with: b) // true - /// - /// - Parameter other: A finite sequence of elements. - /// - /// - Returns: `true` if `self` has no elements in common with `other`; - /// otherwise, `false`. - /// - /// - Complexity: Expected to be O(*n*) on average, where *n* is the number of - /// elements in `other`, if `Element` implements high-quality hashing. - @inlinable - public func isDisjoint( - with other: S - ) -> Bool where S.Element == Element { - guard !self.isEmpty else { return true } - for item in other { - if self.contains(item) { return false } - } - return true - } -} - diff --git a/Sources/OrderedCollections/OrderedSet/OrderedSet+RandomAccessCollection.swift b/Sources/OrderedCollections/OrderedSet/OrderedSet+RandomAccessCollection.swift index 7e5eed857..1a974800d 100644 --- a/Sources/OrderedCollections/OrderedSet/OrderedSet+RandomAccessCollection.swift +++ b/Sources/OrderedCollections/OrderedSet/OrderedSet+RandomAccessCollection.swift @@ -2,13 +2,17 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 // //===----------------------------------------------------------------------===// +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + extension OrderedSet: Sequence { /// The type that allows iteration over an ordered set's elements. public typealias Iterator = IndexingIterator @@ -298,3 +302,5 @@ extension OrderedSet: RandomAccessCollection { _elements._failEarlyRangeCheck(range, bounds: bounds) } } + +extension OrderedSet: _UniqueCollection {} diff --git a/Sources/OrderedCollections/OrderedSet/OrderedSet+ReserveCapacity.swift b/Sources/OrderedCollections/OrderedSet/OrderedSet+ReserveCapacity.swift index 053de8cf0..1d217483e 100644 --- a/Sources/OrderedCollections/OrderedSet/OrderedSet+ReserveCapacity.swift +++ b/Sources/OrderedCollections/OrderedSet/OrderedSet+ReserveCapacity.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 diff --git a/Sources/OrderedCollections/OrderedSet/OrderedSet+Sendable.swift b/Sources/OrderedCollections/OrderedSet/OrderedSet+Sendable.swift index 84d8e6b23..4e3c03f12 100644 --- a/Sources/OrderedCollections/OrderedSet/OrderedSet+Sendable.swift +++ b/Sources/OrderedCollections/OrderedSet/OrderedSet+Sendable.swift @@ -2,13 +2,11 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2022-2023 Apple Inc. and the Swift project authors +// Copyright (c) 2022 - 2024 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 // //===----------------------------------------------------------------------===// -#if swift(>=5.5) extension OrderedSet: @unchecked Sendable where Element: Sendable {} -#endif diff --git a/Sources/OrderedCollections/OrderedSet/OrderedSet+SubSequence.swift b/Sources/OrderedCollections/OrderedSet/OrderedSet+SubSequence.swift index 70809ad8f..11eab1a16 100644 --- a/Sources/OrderedCollections/OrderedSet/OrderedSet+SubSequence.swift +++ b/Sources/OrderedCollections/OrderedSet/OrderedSet+SubSequence.swift @@ -2,13 +2,17 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021-2023 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 // //===----------------------------------------------------------------------===// +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + extension OrderedSet { /// A collection that represents a contiguous slice of an ordered set. /// @@ -33,9 +37,7 @@ extension OrderedSet { } } -#if swift(>=5.5) extension OrderedSet.SubSequence: Sendable where Element: Sendable {} -#endif extension OrderedSet.SubSequence { @inlinable @@ -51,6 +53,20 @@ extension OrderedSet.SubSequence { } } +extension OrderedSet.SubSequence: CustomStringConvertible { + // A textual representation of this instance. + public var description: String { + _arrayDescription(for: self) + } +} + +extension OrderedSet.SubSequence: CustomDebugStringConvertible { + /// A textual representation of this instance, suitable for debugging. + public var debugDescription: String { + description + } +} + extension OrderedSet.SubSequence: Sequence { // A type representing the collection’s elements. public typealias Element = OrderedSet.Element @@ -103,6 +119,8 @@ extension OrderedSet.SubSequence: Sequence { } } +extension OrderedSet.SubSequence: _UniqueCollection {} + extension OrderedSet.SubSequence: RandomAccessCollection { /// The index type for ordered sets, `Int`. /// diff --git a/Sources/OrderedCollections/OrderedSet/OrderedSet+Testing.swift b/Sources/OrderedCollections/OrderedSet/OrderedSet+Testing.swift index edac2d8b8..40b7d3d69 100644 --- a/Sources/OrderedCollections/OrderedSet/OrderedSet+Testing.swift +++ b/Sources/OrderedCollections/OrderedSet/OrderedSet+Testing.swift @@ -2,19 +2,27 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 // //===----------------------------------------------------------------------===// +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + extension OrderedSet._UnstableInternals { @_spi(Testing) public var capacity: Int { base._capacity } @_spi(Testing) public var minimumCapacity: Int { base._minimumCapacity } @_spi(Testing) public var scale: Int { base._scale } @_spi(Testing) public var reservedScale: Int { base._reservedScale } @_spi(Testing) public var bias: Int { base._bias } + + public static var isConsistencyCheckingEnabled: Bool { + _isCollectionsInternalCheckingEnabled + } } extension OrderedSet { @@ -102,11 +110,11 @@ extension OrderedSet._UnstableInternals { extension OrderedSet { @_spi(Testing) - public init( + public init( _scale scale: Int, bias: Int, - contents: S - ) where S.Element == Element { + contents: some Sequence + ) { let contents = ContiguousArray(contents) precondition(scale >= _HashTable.scale(forCapacity: contents.count)) precondition(scale <= _HashTable.maximumScale) diff --git a/Sources/OrderedCollections/OrderedSet/OrderedSet+UnorderedView.swift b/Sources/OrderedCollections/OrderedSet/OrderedSet+UnorderedView.swift index 928b94639..9edede0bb 100644 --- a/Sources/OrderedCollections/OrderedSet/OrderedSet+UnorderedView.swift +++ b/Sources/OrderedCollections/OrderedSet/OrderedSet+UnorderedView.swift @@ -2,13 +2,17 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021-2023 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 // //===----------------------------------------------------------------------===// +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + extension OrderedSet { /// An unordered view into an ordered set, providing `SetAlgebra` /// conformance. @@ -62,21 +66,19 @@ extension OrderedSet { } } -#if swift(>=5.5) extension OrderedSet.UnorderedView: Sendable where Element: Sendable {} -#endif extension OrderedSet.UnorderedView: CustomStringConvertible { /// A textual representation of this instance. public var description: String { - _base.description + _arrayDescription(for: _base) } } extension OrderedSet.UnorderedView: CustomDebugStringConvertible { /// A textual representation of this instance, suitable for debugging. public var debugDescription: String { - _base._debugDescription(typeName: "\(_base._debugTypeName()).UnorderedView") + description } } @@ -165,7 +167,7 @@ extension OrderedSet.UnorderedView { /// high-quality hashing. @inlinable @inline(__always) - public init(_ elements: S) where S.Element == Element { + public init(_ elements: some Sequence) { _base = OrderedSet(elements) } @@ -366,9 +368,7 @@ extension OrderedSet.UnorderedView { /// - Complexity: Expected to be O(`other.count`) on average, if `Element` /// implements high-quality hashing. @inlinable - public mutating func formUnion( - _ other: __owned S - ) where S.Element == Element { + public mutating func formUnion(_ other: __owned some Sequence) { _base.formUnion(other) } @@ -385,9 +385,9 @@ extension OrderedSet.UnorderedView { /// - Complexity: Expected to be O(`self.count` + `other.count`) on average, /// if `Element` implements high-quality hashing. @inlinable - public __consuming func union( - _ other: __owned S - ) -> Self where S.Element == Element { + public __consuming func union( + _ other: __owned some Sequence + ) -> Self { _base.union(other).unordered } } @@ -442,9 +442,9 @@ extension OrderedSet.UnorderedView { /// - Complexity: Expected to be O(*n*) on average where *n* is the number of /// elements in `other`, if `Element` implements high-quality hashing. @inlinable - public __consuming func intersection( - _ other: S - ) -> Self where S.Element == Element { + public __consuming func intersection( + _ other: some Sequence + ) -> Self { _base.intersection(other).unordered } @@ -459,9 +459,9 @@ extension OrderedSet.UnorderedView { /// - Complexity: Expected to be O(*n*) on average where *n* is the number of /// elements in `other`, if `Element` implements high-quality hashing. @inlinable - public mutating func formIntersection( - _ other: S - ) where S.Element == Element { + public mutating func formIntersection( + _ other: some Sequence + ) { _base.formIntersection(other) } } @@ -527,9 +527,9 @@ extension OrderedSet.UnorderedView { /// the number of elements in `other`, if `Element` implements high-quality /// hashing. @inlinable - public __consuming func symmetricDifference( - _ other: __owned S - ) -> Self where S.Element == Element { + public __consuming func symmetricDifference( + _ other: __owned some Sequence + ) -> Self { _base.symmetricDifference(other).unordered } @@ -550,9 +550,9 @@ extension OrderedSet.UnorderedView { /// the number of elements in `other`, if `Element` implements high-quality /// hashing. @inlinable - public mutating func formSymmetricDifference( - _ other: __owned S - ) where S.Element == Element { + public mutating func formSymmetricDifference( + _ other: __owned some Sequence + ) { _base.formSymmetricDifference(other) } } @@ -611,9 +611,7 @@ extension OrderedSet.UnorderedView { /// - Complexity: Expected to be O(`self.count + other.count`) on average, if /// `Element` implements high-quality hashing. @inlinable - public __consuming func subtracting( - _ other: S - ) -> Self where S.Element == Element { + public __consuming func subtracting(_ other: some Sequence) -> Self { _base.subtracting(other).unordered } @@ -629,13 +627,40 @@ extension OrderedSet.UnorderedView { /// is the number of elements in `other`, if `Element` implements /// high-quality hashing. @inlinable - public mutating func subtract( - _ other: S - ) where S.Element == Element { + public mutating func subtract(_ other: some Sequence) { _base.subtract(other) } } +extension OrderedSet.UnorderedView { + /// Returns a Boolean value indicating whether two set values contain the + /// same elements, but not necessarily in the same order. + /// + /// - Note: This member implements different behavior than the `==(_:_:)` + /// operator -- the latter implements an ordered comparison, matching + /// the stricter concept of equality expected of an ordered collection + /// type. + /// + /// - Complexity: O(`min(left.count, right.count)`), as long as`Element` + /// properly implements hashing. + public func isEqualSet(to other: Self) -> Bool { + self == other + } + + /// Returns a Boolean value indicating whether an ordered set contains the + /// same values as a given sequence, but not necessarily in the same + /// order. + /// + /// Duplicate items in `other` do not prevent it from comparing equal to + /// `self`. + /// + /// - Complexity: O(*n*), where *n* is the number of items in + /// `other`, as long as`Element` properly implements hashing. + public func isEqualSet(to other: some Sequence) -> Bool { + self._base.isEqualSet(to: other) + } +} + extension OrderedSet.UnorderedView { /// Returns a Boolean value that indicates whether this set is a subset of /// the given set. @@ -699,9 +724,7 @@ extension OrderedSet.UnorderedView { /// is the number of elements in `other`, if `Element` implements /// high-quality hashing. @inlinable - public func isSubset( - of other: S - ) -> Bool where S.Element == Element { + public func isSubset(of other: some Sequence) -> Bool { _base.isSubset(of: other) } } @@ -768,9 +791,7 @@ extension OrderedSet.UnorderedView { /// - Complexity: Expected to be O(*n*) on average, where *n* is the number of /// elements in `other`, if `Element` implements high-quality hashing. @inlinable - public func isSuperset( - of other: S - ) -> Bool where S.Element == Element { + public func isSuperset(of other: some Sequence) -> Bool { _base.isSuperset(of: other) } } @@ -844,9 +865,7 @@ extension OrderedSet.UnorderedView { /// is the number of elements in `other`, if `Element` implements /// high-quality hashing. @inlinable - public func isStrictSubset( - of other: S - ) -> Bool where S.Element == Element { + public func isStrictSubset(of other: some Sequence) -> Bool { _base.isStrictSubset(of: other) } } @@ -920,9 +939,7 @@ extension OrderedSet.UnorderedView { /// is the number of elements in `other`, if `Element` implements /// high-quality hashing. @inlinable - public func isStrictSuperset( - of other: S - ) -> Bool where S.Element == Element { + public func isStrictSuperset(of other: some Sequence) -> Bool { _base.isStrictSuperset(of: other) } } @@ -983,9 +1000,7 @@ extension OrderedSet.UnorderedView { /// - Complexity: Expected to be O(*n*) on average, where *n* is the number of /// elements in `other`, if `Element` implements high-quality hashing. @inlinable - public func isDisjoint( - with other: S - ) -> Bool where S.Element == Element { + public func isDisjoint(with other: some Sequence) -> Bool { _base.isDisjoint(with: other) } } diff --git a/Sources/OrderedCollections/OrderedSet/OrderedSet+UnstableInternals.swift b/Sources/OrderedCollections/OrderedSet/OrderedSet+UnstableInternals.swift index 3eb8dba67..9ed755cea 100644 --- a/Sources/OrderedCollections/OrderedSet/OrderedSet+UnstableInternals.swift +++ b/Sources/OrderedCollections/OrderedSet/OrderedSet+UnstableInternals.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021-2023 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 @@ -47,7 +47,5 @@ extension OrderedSet { } } -#if swift(>=5.5) extension OrderedSet._UnstableInternals: Sendable where Element: Sendable {} -#endif diff --git a/Sources/OrderedCollections/OrderedSet/OrderedSet.swift b/Sources/OrderedCollections/OrderedSet/OrderedSet.swift index 2d8d30090..67f3958b4 100644 --- a/Sources/OrderedCollections/OrderedSet/OrderedSet.swift +++ b/Sources/OrderedCollections/OrderedSet/OrderedSet.swift @@ -2,13 +2,17 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 // //===----------------------------------------------------------------------===// +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + /// An ordered collection of unique elements. /// /// Similar to the standard `Set`, ordered sets ensure that each element appears @@ -133,11 +137,26 @@ /// could lead to duplicate values. /// /// However, `OrderedSet` is able to partially implement these two protocols; -/// namely, is supports mutation operations that merely change the +/// namely, it supports mutation operations that merely change the /// order of elements (such as ``sort()`` or ``swapAt(_:_:)``, or just remove /// some subset of existing members (such as ``remove(at:)`` or /// ``removeAll(where:)``). /// +/// Accordingly, `OrderedSet` provides permutation operations from `MutableCollection`: +/// - ``swapAt(_:_:)`` +/// - ``partition(by:)`` +/// - ``sort()``, ``sort(by:)`` +/// - ``shuffle()``, ``shuffle(using:)`` +/// - ``reverse()`` +/// +/// It also supports removal operations from `RangeReplaceableCollection`: +/// - ``removeAll(keepingCapacity:)`` +/// - ``remove(at:)`` +/// - ``removeSubrange(_:)-2fqke``, ``removeSubrange(_:)-62u6a`` +/// - ``removeLast()``, ``removeLast(_:)`` +/// - ``removeFirst()``, ``removeFirst(_:)`` +/// - ``removeAll(where:)`` +/// /// `OrderedSet` also implements ``reserveCapacity(_:)`` from /// `RangeReplaceableCollection`, to allow for efficient insertion of a known /// number of elements. (However, unlike `Array` and `Set`, `OrderedSet` does @@ -173,7 +192,64 @@ /// /// # Performance /// -/// Like the standard `Set` type, the performance of hashing operations in +/// An `OrderedSet` stores its members in a standard `Array` value (exposed by +/// the ``elements`` property). It also maintains a separate hash table +/// containing array indices into this array; this hash table is used to ensure +/// member uniqueness and to implement fast membership tests. +/// +/// ## Element Lookups +/// +/// Like the standard `Set`, looking up a member is expected to execute +/// a constant number of hashing and equality check operations. To look up +/// an element, `OrderedSet` generates a hash value from it, and then finds a +/// set of array indices within the hash table that could potentially contain +/// the element we're looking for. By looking through these indices in the +/// storage array, `OrderedSet` is able to determine if the element is a member. +/// As long as `Element` properly implements hashing, the size of this set of +/// candidate indices is expected to have a constant upper bound, so looking up +/// an item will be a constant operation. +/// +/// ## Appending New Items +/// +/// Similarly, appending a new element to the end of an `OrderedSet` is expected +/// to require amortized O(1) hashing/comparison/copy operations on the +/// element type, just like inserting an item into a standard `Set`. +/// (If the ordered set value has multiple copies, then appending an item will +/// need to copy all its items into unique storage (again just like the standard +/// `Set`) -- but once the set has been uniqued, additional appends will only +/// perform a constant number of operations, so when averaged over many appends, +/// the overall complexity comes out as O(1).) +/// +/// ## Removing Items and Inserting in Places Other Than the End +/// +/// Unfortunately, `OrderedSet` does not emulate `Set`'s performance for all +/// operations. In particular, operations that insert or remove elements at the +/// front or in the middle of an ordered set are generally expected to be +/// significantly slower than with `Set`. To perform these operations, an +/// `OrderedSet` needs to perform the corresponding operation in the storage +/// array, and then it needs to renumber all subsequent members in the hash +/// table. Both of these phases take a number of steps that grows linearly with +/// the size of the ordered set, while the standard `Set` can do the +/// corresponding operations with O(1) expected complexity. +/// +/// This generally makes `OrderedSet` a poor replacement to `Set` in use cases +/// that do not specifically require a particular element ordering. +/// +/// ## Memory Utilization +/// +/// The hash table in an ordered set never needs to store larger indices than +/// the current size of the storage array, and `OrderedSet` makes use of this +/// observation to reduce the number of bits it uses to encode these integer +/// values. Additionally, the actual hashed elements are stored in a flat array +/// value rather than the hash table itself, so they aren't subject to the hash +/// table's strict maximum load factor. These two observations combine to +/// optimize the memory utilization of `OrderedSet`, sometimes making it even +/// more efficient than the standard `Set` -- despite the additional +/// functionality of preserving element ordering. +/// +/// ## Proper Hashing is Crucial +/// +/// Similar to the standard `Set` type, the performance of hashing operations in /// `OrderedSet` is highly sensitive to the quality of hashing implemented by /// the `Element` type. Failing to correctly implement hashing can easily lead /// to unacceptable performance, with the severity of the effect increasing with @@ -186,37 +262,18 @@ /// cannot be induced merely by adding a particular list of members to the set. /// /// The easiest way to achieve this is to make sure `Element` implements hashing -/// following `Hashable`'s documented best practices. The conformance must -/// implement the `hash(into:)` requirement, and every bit of information that -/// is compared in `==` needs to be combined into the supplied `Hasher` value. -/// When used correctly, `Hasher` produces high-quality, randomly seeded hash -/// values that prevent repeatable hash collisions. -/// -/// When `Element` implements `Hashable` correctly, testing for membership in an -/// ordered set is expected to take O(1) equality checks on average. Hash -/// collisions can still occur organically, so the worst-case lookup performance -/// is technically still O(*n*) (where *n* is the size of the set); however, -/// long lookup chains are unlikely to occur in practice. -/// -/// # Implementation Details -/// -/// An `OrderedSet` stores its members in a regular `Array` value (exposed by -/// the `elements` property). It also maintains a standalone hash table -/// containing array indices alongside the array; this is used to implement fast -/// membership tests. The size of the array is limited by the capacity of the -/// corresponding hash table, so indices stored inside the hash table can be -/// encoded into fewer bits than a standard `Int` value, leading to a storage -/// representation that can often be more compact than that of `Set` itself. -/// -/// Inserting or removing a single member (or a range of members) needs to -/// perform the corresponding operation in the storage array, in addition to -/// renumbering any subsequent members in the hash table. Therefore, these -/// operations are expected to have performance characteristics similar to an -/// `Array`: inserting or removing an element to the end of an ordered set is -/// expected to execute in O(1) operations, while they are expected to take -/// linear time at the front (or in the middle) of the set. (Note that this is -/// different to the standard `Set`, where insertions and removals are expected -/// to take amortized O(1) time.) +/// following `Hashable`'s documented best practices. The `Element` type must +/// implement the `hash(into:)` requirement (not `hashValue`) in such a way that +/// every bit of information that is compared in `==` is fed into the supplied +/// `Hasher` value. When used correctly, `Hasher` produces high-quality, +/// randomly seeded hash values that prevent repeatable hash collisions and +/// therefore avoid (intentional or accidental) denial of service attacks. +/// +/// Like with all hashed collection types, all complexity guarantees are null +/// and void if `Element` implements `Hashable` incorrectly. In the worst case, +/// the hash table can regress into a particularly slow implementation of an +/// unsorted array, with even basic lookup operations taking complexity +/// proportional to the size of the set. @frozen public struct OrderedSet where Element: Hashable { @@ -428,24 +485,27 @@ extension OrderedSet { @inlinable @inline(never) internal __consuming func _extractSubset( - using bitset: _UnsafeBitset, + using bitset: _UnsafeBitSet, + count: Int? = nil, extraCapacity: Int = 0 ) -> Self { - assert(bitset.count == 0 || bitset.max()! <= count) - if bitset.count == 0 { return Self(minimumCapacity: extraCapacity) } - if bitset.count == self.count { + let c = count ?? bitset.count + assert(c == 0 || bitset.max()! <= self.count) + if c == 0 { return Self(minimumCapacity: extraCapacity) } + if c == self.count { if extraCapacity <= self._capacity - self.count { return self } var copy = self - copy.reserveCapacity(count + extraCapacity) + copy.reserveCapacity(c + extraCapacity) return copy } - var result = Self(minimumCapacity: bitset.count + extraCapacity) + var result = Self(minimumCapacity: c + extraCapacity) for offset in bitset { - result._appendNew(_elements[offset]) + result._appendNew(_elements[Int(bitPattern: offset)]) } - assert(result.count == bitset.count) + assert(result.count == c) + result._checkInvariants() return result } } @@ -480,3 +540,27 @@ extension OrderedSet { return _elements.remove(at: index) } } + +extension OrderedSet { + /// Returns a new ordered set containing all the members of this ordered set + /// that satisfy the given predicate. + /// + /// - Parameter isIncluded: A closure that takes a value as its + /// argument and returns a Boolean value indicating whether the value + /// should be included in the returned dictionary. + /// + /// - Returns: An ordered set of the values that `isIncluded` allows. + /// + /// - Complexity: O(`count`) + @inlinable + public func filter( + _ isIncluded: (Element) throws -> Bool + ) rethrows -> Self { + try _UnsafeBitSet.withTemporaryBitSet(capacity: self.count) { bitset in + for i in _elements.indices where try isIncluded(_elements[i]) { + bitset.insert(i) + } + return self._extractSubset(using: bitset) + } + } +} diff --git a/Sources/OrderedCollections/Utilities/_UnsafeBitset.swift b/Sources/OrderedCollections/Utilities/_UnsafeBitset.swift index 571436dde..e188e525c 100644 --- a/Sources/OrderedCollections/Utilities/_UnsafeBitset.swift +++ b/Sources/OrderedCollections/Utilities/_UnsafeBitset.swift @@ -2,398 +2,16 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 // //===----------------------------------------------------------------------===// -/// A simple bitmap of a fixed number of bits, implementing a sorted set of -/// small nonnegative `Int` values. -/// -/// Because `_UnsafeBitset` implements a flat bit vector, it isn't suitable for -/// holding arbitrarily large integers. The maximal element a bitset can store -/// is fixed at its initialization. -@usableFromInline -@frozen -internal struct _UnsafeBitset { - @usableFromInline - internal let _words: UnsafeMutableBufferPointer - - @usableFromInline - internal var _count: Int - - @inlinable - @inline(__always) - internal init(words: UnsafeMutableBufferPointer, count: Int) { - self._words = words - self._count = count - } - - @inlinable - @inline(__always) - internal init(words: UnsafeMutablePointer, wordCount: Int, count: Int) { - self._words = UnsafeMutableBufferPointer(start: words, count: wordCount) - self._count = count - } - - @inlinable - @inline(__always) - internal var count: Int { - _count - } -} - -extension _UnsafeBitset { - @usableFromInline - internal var _actualCount: Int { - return _words.reduce(0) { $0 + $1.count } - } -} +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities -extension _UnsafeBitset { - @inlinable - @inline(__always) - static func withTemporaryBitset( - capacity: Int, - run body: (inout _UnsafeBitset) throws -> R - ) rethrows -> R { - var result: R? - try _withTemporaryBitset(capacity: capacity) { bitset in - result = try body(&bitset) - } - return result! - } - - @usableFromInline - @inline(never) - static func _withTemporaryBitset( - capacity: Int, - run body: (inout _UnsafeBitset) throws -> Void - ) rethrows { - let wordCount = _UnsafeBitset.wordCount(forCapacity: capacity) -#if compiler(>=5.6) - return try withUnsafeTemporaryAllocation( - of: Word.self, capacity: wordCount - ) { words in - words.initialize(repeating: .empty) - var bitset = Self(words: words, count: 0) - return try body(&bitset) - } -#else - if wordCount <= 2 { - var buffer: (Word, Word) = (.empty, .empty) - return try withUnsafeMutablePointer(to: &buffer) { p in - // Homogeneous tuples are layout-compatible with their component type. - let words = UnsafeMutableRawPointer(p).assumingMemoryBound(to: Word.self) - var bitset = _UnsafeBitset(words: words, wordCount: wordCount, count: 0) - return try body(&bitset) - } - } - let words = UnsafeMutableBufferPointer.allocate(capacity: wordCount) - words.initialize(repeating: .empty) - defer { words.deallocate() } - var bitset = _UnsafeBitset(words: words, count: 0) - return try body(&bitset) +@usableFromInline +internal typealias _UnsafeBitSet = _CollectionsUtilities._UnsafeBitSet #endif - } -} - -extension _UnsafeBitset { - @inline(__always) - internal static func word(for element: Int) -> Int { - assert(element >= 0) - // Note: We perform on UInts to get faster unsigned math (shifts). - let element = UInt(bitPattern: element) - let capacity = UInt(bitPattern: Word.capacity) - return Int(bitPattern: element / capacity) - } - - @inline(__always) - internal static func bit(for element: Int) -> Int { - assert(element >= 0) - // Note: We perform on UInts to get faster unsigned math (masking). - let element = UInt(bitPattern: element) - let capacity = UInt(bitPattern: Word.capacity) - return Int(bitPattern: element % capacity) - } - - @inline(__always) - internal static func split(_ element: Int) -> (word: Int, bit: Int) { - return (word(for: element), bit(for: element)) - } - - @inline(__always) - internal static func join(word: Int, bit: Int) -> Int { - assert(bit >= 0 && bit < Word.capacity) - return word &* Word.capacity &+ bit - } -} - -extension _UnsafeBitset { - @usableFromInline - @_effects(readnone) - @inline(__always) - internal static func wordCount(forCapacity capacity: Int) -> Int { - return word(for: capacity &+ Word.capacity &- 1) - } - - internal var capacity: Int { - @inline(__always) - get { - return _words.count &* Word.capacity - } - } - - @inline(__always) - internal func isValid(_ element: Int) -> Bool { - return element >= 0 && element < capacity - } - - @inline(__always) - internal func contains(_ element: Int) -> Bool { - assert(isValid(element)) - let (word, bit) = _UnsafeBitset.split(element) - return _words[word].contains(bit) - } - - @usableFromInline - @_effects(releasenone) - @discardableResult - internal mutating func insert(_ element: Int) -> Bool { - assert(isValid(element)) - let (word, bit) = _UnsafeBitset.split(element) - let inserted = _words[word].insert(bit) - if inserted { _count += 1 } - return inserted - } - - @usableFromInline - @_effects(releasenone) - @discardableResult - internal mutating func remove(_ element: Int) -> Bool { - assert(isValid(element)) - let (word, bit) = _UnsafeBitset.split(element) - let removed = _words[word].remove(bit) - if removed { _count -= 1 } - return removed - } - - @usableFromInline - @_effects(releasenone) - internal mutating func clear() { - guard _words.count > 0 else { return } - #if swift(>=5.8) - _words.baseAddress!.update(repeating: .empty, count: _words.count) - #else - _words.baseAddress!.assign(repeating: .empty, count: _words.count) - #endif - _count = 0 - } - - @usableFromInline - @_effects(releasenone) - internal mutating func insertAll(upTo max: Int) { - assert(max <= capacity) - guard max > 0 else { return } - let (w, b) = _UnsafeBitset.split(max) - for i in 0 ..< w { - _count += Word.capacity - _words[i].count - _words[i] = .allBits - } - if b > 0 { - _count += _words[w].insert(bitsBelow: b) - } - } - - @usableFromInline - @_effects(releasenone) - internal mutating func removeAll(upTo max: Int) { - assert(max <= capacity) - guard max > 0 else { return } - let (w, b) = _UnsafeBitset.split(max) - for i in 0 ..< w { - _count -= _words[i].count - _words[i] = .empty - } - if b > 0 { - _count -= _words[w].remove(bitsBelow: b) - } - } -} - -extension _UnsafeBitset: Sequence { - @usableFromInline - internal typealias Element = Int - - @inlinable - @inline(__always) - internal var underestimatedCount: Int { - return count - } - - @inlinable - @inline(__always) - func makeIterator() -> Iterator { - return Iterator(self) - } - - @usableFromInline - @frozen - internal struct Iterator: IteratorProtocol { - @usableFromInline - internal let bitset: _UnsafeBitset - - @usableFromInline - internal var index: Int - - @usableFromInline - internal var word: Word - - @inlinable - internal init(_ bitset: _UnsafeBitset) { - self.bitset = bitset - self.index = 0 - self.word = bitset._words.count > 0 ? bitset._words[0] : .empty - } - - @usableFromInline - @_effects(releasenone) - internal mutating func next() -> Int? { - if let bit = word.next() { - return _UnsafeBitset.join(word: index, bit: bit) - } - while (index + 1) < bitset._words.count { - index += 1 - word = bitset._words[index] - if let bit = word.next() { - return _UnsafeBitset.join(word: index, bit: bit) - } - } - return nil - } - } -} - -//////////////////////////////////////////////////////////////////////////////// -extension _UnsafeBitset { - @usableFromInline - @frozen - internal struct Word { - @usableFromInline - internal var value: UInt - - @inlinable - @inline(__always) - internal init(_ value: UInt) { - self.value = value - } - } -} - -extension _UnsafeBitset.Word { - @inlinable - @inline(__always) - internal static var capacity: Int { - return UInt.bitWidth - } - - @inlinable - @inline(__always) - internal var count: Int { - value.nonzeroBitCount - } - - @inlinable - @inline(__always) - internal var isEmpty: Bool { - value == 0 - } - - @inlinable - @inline(__always) - internal func contains(_ bit: Int) -> Bool { - assert(bit >= 0 && bit < UInt.bitWidth) - return value & (1 &<< bit) != 0 - } - - @inlinable - @inline(__always) - @discardableResult - internal mutating func insert(_ bit: Int) -> Bool { - assert(bit >= 0 && bit < UInt.bitWidth) - let mask: UInt = 1 &<< bit - let inserted = value & mask == 0 - value |= mask - return inserted - } - - @inlinable - @inline(__always) - @discardableResult - internal mutating func remove(_ bit: Int) -> Bool { - assert(bit >= 0 && bit < UInt.bitWidth) - let mask: UInt = 1 &<< bit - let removed = value & mask != 0 - value &= ~mask - return removed - } -} - -extension _UnsafeBitset.Word { - @inlinable - @inline(__always) - internal mutating func insert(bitsBelow bit: Int) -> Int { - assert(bit >= 0 && bit < Self.capacity) - let mask: UInt = (1 as UInt &<< bit) &- 1 - let inserted = bit - (value & mask).nonzeroBitCount - value |= mask - return inserted - } - - @inlinable - @inline(__always) - internal mutating func remove(bitsBelow bit: Int) -> Int { - assert(bit >= 0 && bit < Self.capacity) - let mask = UInt.max &<< bit - let removed = (value & ~mask).nonzeroBitCount - value &= mask - return removed - } -} - -extension _UnsafeBitset.Word { - @inlinable - @inline(__always) - internal static var empty: Self { - Self(0) - } - - @inlinable - @inline(__always) - internal static var allBits: Self { - Self(UInt.max) - } -} - -// Word implements Sequence by using a copy of itself as its Iterator. -// Iteration with `next()` destroys the word's value; however, this won't cause -// problems in normal use, because `next()` is usually called on a separate -// iterator, not the original word. -extension _UnsafeBitset.Word: Sequence, IteratorProtocol { - @inlinable - internal var underestimatedCount: Int { - count - } - - /// Return the index of the lowest set bit in this word, - /// and also destructively clear it. - @inlinable - internal mutating func next() -> Int? { - guard value != 0 else { return nil } - let bit = value.trailingZeroBitCount - value &= value &- 1 // Clear lowest nonzero bit. - return bit - } -} diff --git a/Sources/RopeModule/BigString/Basics/BigString+Builder.swift b/Sources/RopeModule/BigString/Basics/BigString+Builder.swift new file mode 100644 index 000000000..730e39e27 --- /dev/null +++ b/Sources/RopeModule/BigString/Basics/BigString+Builder.swift @@ -0,0 +1,149 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=5.8) + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + struct Builder { + typealias _Chunk = BigString._Chunk + typealias _Ingester = BigString._Ingester + typealias _Rope = BigString._Rope + + var base: _Rope.Builder + var suffixStartState: _CharacterRecognizer + var prefixEndState: _CharacterRecognizer + + init( + base: _Rope.Builder, + prefixEndState: _CharacterRecognizer, + suffixStartState: _CharacterRecognizer + ) { + self.base = base + self.suffixStartState = suffixStartState + self.prefixEndState = prefixEndState + } + + init() { + self.base = _Rope.Builder() + self.suffixStartState = _CharacterRecognizer() + self.prefixEndState = _CharacterRecognizer() + } + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension Rope.Builder { + internal func _breakState() -> _CharacterRecognizer { + let chars = self.prefixSummary.characters + assert(self.isPrefixEmpty || chars > 0) + let metric = BigString._CharacterMetric() + var state = _CharacterRecognizer() + _ = self.forEachElementInPrefix(from: chars - 1, in: metric) { chunk, i in + if let i { + state = .init(partialCharacter: chunk.string[i...]) + } else { + state.consumePartialCharacter(chunk.string[...]) + } + return true + } + return state + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString.Builder { + mutating func append(_ str: __owned some StringProtocol) { + append(Substring(str)) + } + + mutating func append(_ str: __owned String) { + append(str[...]) + } + + mutating func append(_ str: __owned Substring) { + guard !str.isEmpty else { return } + var ingester = _Ingester(str, startState: self.prefixEndState) + if var prefix = base._prefix._take() { + if let slice = ingester.nextSlice(maxUTF8Count: prefix.value.availableSpace) { + prefix.value._append(slice) + } + self.base._prefix = prefix + } + while let next = ingester.nextChunk() { + base.insertBeforeTip(next) + } + self.prefixEndState = ingester.state + } + + mutating func append(_ newChunk: __owned _Chunk) { + var state = _CharacterRecognizer() + append(newChunk, state: &state) + } + + mutating func append(_ newChunk: __owned _Chunk, state: inout _CharacterRecognizer) { + var newChunk = newChunk + newChunk.resyncBreaksFromStartToEnd(old: &state, new: &self.prefixEndState) + self.base.insertBeforeTip(newChunk) + } + + mutating func append(_ other: __owned BigString) { + var state = _CharacterRecognizer() + append(other._rope, state: &state) + } + + mutating func append(_ other: __owned BigString, in range: Range) { + let extract = BigString(other, in: range, state: &self.prefixEndState) + self.base.insertBeforeTip(extract._rope) + } + + mutating func append(_ other: __owned _Rope, state: inout _CharacterRecognizer) { + guard !other.isEmpty else { return } + var other = BigString(_rope: other) + other._rope.resyncBreaksToEnd(old: &state, new: &self.prefixEndState) + self.base.insertBeforeTip(other._rope) + } + + mutating func append(from ingester: inout _Ingester) { + //assert(ingester.state._isKnownEqual(to: self.prefixEndState)) + if var prefix = base._prefix._take() { + if let first = ingester.nextSlice(maxUTF8Count: prefix.value.availableSpace) { + prefix.value._append(first) + } + base._prefix = prefix + } + + let suffixCount = base._suffix?.value.utf8Count ?? 0 + + while let chunk = ingester.nextWellSizedChunk(suffix: suffixCount) { + base.insertBeforeTip(chunk) + } + precondition(ingester.isAtEnd) + self.prefixEndState = ingester.state + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString.Builder { + mutating func finalize() -> BigString { + // Resync breaks in suffix. + _ = base.mutatingForEachSuffix { chunk in + chunk.resyncBreaksFromStart(old: &suffixStartState, new: &prefixEndState) + } + // Roll it all up. + let rope = self.base.finalize() + let string = BigString(_rope: rope) + string._invariantCheck() + return string + } +} + +#endif diff --git a/Sources/RopeModule/BigString/Basics/BigString+Contents.swift b/Sources/RopeModule/BigString/Basics/BigString+Contents.swift new file mode 100644 index 000000000..af1141aaf --- /dev/null +++ b/Sources/RopeModule/BigString/Basics/BigString+Contents.swift @@ -0,0 +1,534 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=5.8) + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + /// The estimated maximum number of UTF-8 code units that `BigString` is guaranteed to be able + /// to hold without encountering an overflow in its operations. This corresponds to the capacity + /// of the deepest tree where every node is the minimum possible size. + public static var _minimumCapacity: Int { + let c = _Rope._minimumCapacity + let (r, overflow) = _Chunk.minUTF8Count.multipliedReportingOverflow(by: c) + guard !overflow else { return Int.max } + return r + } + + /// The maximum number of UTF-8 code units that `BigString` may be able to store in the best + /// possible case, when every node in the underlying tree is fully filled with data. + public static var _maximumCapacity: Int { + let c = _Rope._maximumCapacity + let (r, overflow) = _Chunk.maxUTF8Count.multipliedReportingOverflow(by: c) + guard !overflow else { return Int.max } + return r + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + var _characterCount: Int { _rope.summary.characters } + var _unicodeScalarCount: Int { _rope.summary.unicodeScalars } + var _utf16Count: Int { _rope.summary.utf16 } + var _utf8Count: Int { _rope.summary.utf8 } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + func _distance( + from start: Index, + to end: Index, + in metric: some _StringMetric + ) -> Int { + precondition(start <= endIndex && end <= endIndex, "Invalid index") + guard start != end else { return 0 } + assert(!isEmpty) + let (lesser, greater) = (start <= end ? (start, end) : (end, start)) + let a = resolve(lesser, preferEnd: false) + let b = resolve(greater, preferEnd: true) + var d = 0 + + let ropeIndexA = a._rope! + let ropeIndexB = b._rope! + let chunkIndexA = a._chunkIndex + let chunkIndexB = b._chunkIndex + + if ropeIndexA == ropeIndexB { + d = metric.distance(from: chunkIndexA, to: chunkIndexB, in: _rope[ropeIndexA]) + } else { + let chunkA = _rope[ropeIndexA] + let chunkB = _rope[ropeIndexB] + d += _rope.distance(from: ropeIndexA, to: ropeIndexB, in: metric) + d -= metric.distance(from: chunkA.string.startIndex, to: chunkIndexA, in: chunkA) + d += metric.distance(from: chunkB.string.startIndex, to: chunkIndexB, in: chunkB) + } + return start <= end ? d : -d + } + + func _characterDistance(from start: Index, to end: Index) -> Int { + _distance(from: start, to: end, in: _CharacterMetric()) + } + + func _unicodeScalarDistance(from start: Index, to end: Index) -> Int { + _distance(from: start, to: end, in: _UnicodeScalarMetric()) + } + + func _utf16Distance(from start: Index, to end: Index) -> Int { + _distance(from: start, to: end, in: _UTF16Metric()) + } + + func _utf8Distance(from start: Index, to end: Index) -> Int { + end.utf8Offset - start.utf8Offset + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + // FIXME: See if we need direct implementations for these. + + func _characterOffset(of index: Index) -> Int { + _characterDistance(from: startIndex, to: index) + } + + func _unicodeScalarOffset(of index: Index) -> Int { + _unicodeScalarDistance(from: startIndex, to: index) + } + + func _utf16Offset(of index: Index) -> Int { + _utf16Distance(from: startIndex, to: index) + } + + func _utf8Offset(of index: Index) -> Int { + _utf8Distance(from: startIndex, to: index) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + // FIXME: See if we need direct implementations for these. + + func _characterIndex(at offset: Int) -> Index { + _characterIndex(startIndex, offsetBy: offset) + } + + func _unicodeScalarIndex(at offset: Int) -> Index { + _unicodeScalarIndex(startIndex, offsetBy: offset) + } + + func _utf16Index(at offset: Int) -> Index { + _utf16Index(startIndex, offsetBy: offset) + } + + func _utf8Index(at offset: Int) -> Index { + _utf8Index(startIndex, offsetBy: offset) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + func _index( + _ i: Index, + offsetBy distance: Int, + in metric: some _StringMetric + ) -> Index { + precondition(i <= endIndex, "Index out of bounds") + if isEmpty { + precondition(distance == 0, "Index out of bounds") + return startIndex + } + if i == endIndex, distance == 0 { return i } + let i = resolve(i, preferEnd: i == endIndex || distance < 0) + var ri = i._rope! + var ci = i._chunkIndex + var d = distance + var chunk = _rope[ri] + let r = metric.formIndex(&ci, offsetBy: &d, in: chunk) + if r.found { + return Index(baseUTF8Offset: i._utf8BaseOffset, _rope: ri, chunk: ci) + } + + if r.forward { + assert(distance >= 0) + assert(ci == chunk.string.endIndex) + d += metric._nonnegativeSize(of: chunk.summary) + let start = ri + _rope.formIndex(&ri, offsetBy: &d, in: metric, preferEnd: false) + if ri == _rope.endIndex { + return endIndex + } + chunk = _rope[ri] + ci = metric.index(at: d, in: chunk) + let base = i._utf8BaseOffset + _rope.distance(from: start, to: ri, in: _UTF8Metric()) + return Index(baseUTF8Offset: base, _rope: ri, chunk: ci) + } + + assert(distance <= 0) + assert(ci == chunk.string.startIndex) + let start = ri + _rope.formIndex(&ri, offsetBy: &d, in: metric, preferEnd: false) + chunk = _rope[ri] + ci = metric.index(at: d, in: chunk) + let base = i._utf8BaseOffset + _rope.distance(from: start, to: ri, in: _UTF8Metric()) + return Index(baseUTF8Offset: base, _rope: ri, chunk: ci) + } + + func _characterIndex(_ i: Index, offsetBy distance: Int) -> Index { + _index(i, offsetBy: distance, in: _CharacterMetric())._knownCharacterAligned() + } + + func _unicodeScalarIndex(_ i: Index, offsetBy distance: Int) -> Index { + _index(i, offsetBy: distance, in: _UnicodeScalarMetric())._knownScalarAligned() + } + + func _utf16Index(_ i: Index, offsetBy distance: Int) -> Index { + _index(i, offsetBy: distance, in: _UTF16Metric()) + } + + func _utf8Index(_ i: Index, offsetBy distance: Int) -> Index { + _index(i, offsetBy: distance, in: _UTF8Metric()) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + func _index( + _ i: Index, + offsetBy distance: Int, + limitedBy limit: Index, + in metric: some _StringMetric + ) -> Index? { + // FIXME: Do we need a direct implementation? + if distance >= 0 { + if limit >= i { + let d = self._distance(from: i, to: limit, in: metric) + if d < distance { return nil } + } + } else { + if limit <= i { + let d = self._distance(from: i, to: limit, in: metric) + if d > distance { return nil } + } + } + return self._index(i, offsetBy: distance, in: metric) + } + + func _characterIndex(_ i: Index, offsetBy distance: Int, limitedBy limit: Index) -> Index? { + guard let j = _index(i, offsetBy: distance, limitedBy: limit, in: _CharacterMetric()) else { + return nil + } + return j._knownCharacterAligned() + } + + func _unicodeScalarIndex(_ i: Index, offsetBy distance: Int, limitedBy limit: Index) -> Index? { + guard let j = _index(i, offsetBy: distance, limitedBy: limit, in: _UnicodeScalarMetric()) else { + return nil + } + return j._knownScalarAligned() + } + + func _utf16Index(_ i: Index, offsetBy distance: Int, limitedBy limit: Index) -> Index? { + _index(i, offsetBy: distance, limitedBy: limit, in: _UTF16Metric()) + } + + func _utf8Index(_ i: Index, offsetBy distance: Int, limitedBy limit: Index) -> Index? { + _index(i, offsetBy: distance, limitedBy: limit, in: _UTF8Metric()) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + func _characterIndex(after i: Index) -> Index { + _index(i, offsetBy: 1, in: _CharacterMetric())._knownCharacterAligned() + } + + func _unicodeScalarIndex(after i: Index) -> Index { + _index(i, offsetBy: 1, in: _UnicodeScalarMetric())._knownScalarAligned() + } + + func _utf16Index(after i: Index) -> Index { + _index(i, offsetBy: 1, in: _UTF16Metric()) + } + + func _utf8Index(after i: Index) -> Index { + precondition(i < endIndex, "Can't advance above end index") + let i = resolve(i, preferEnd: false) + let ri = i._rope! + var ci = i._chunkIndex + let chunk = _rope[ri] + chunk.string.utf8.formIndex(after: &ci) + if ci == chunk.string.endIndex { + return Index( + baseUTF8Offset: i._utf8BaseOffset + chunk.utf8Count, + _rope: _rope.index(after: ri), + chunk: String.Index(_utf8Offset: 0)) + } + return Index(_utf8Offset: i.utf8Offset + 1, _rope: ri, chunkOffset: ci._utf8Offset) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + func _characterIndex(before i: Index) -> Index { + _index(i, offsetBy: -1, in: _CharacterMetric())._knownCharacterAligned() + } + + func _unicodeScalarIndex(before i: Index) -> Index { + _index(i, offsetBy: -1, in: _UnicodeScalarMetric())._knownScalarAligned() + } + + func _utf16Index(before i: Index) -> Index { + _index(i, offsetBy: -1, in: _UTF16Metric()) + } + + func _utf8Index(before i: Index) -> Index { + precondition(i > startIndex, "Can't advance below start index") + let i = resolve(i, preferEnd: true) + var ri = i._rope! + let ci = i._chunkIndex + if ci._utf8Offset > 0 { + return Index( + _utf8Offset: i.utf8Offset &- 1, + _rope: ri, + chunkOffset: ci._utf8Offset &- 1) + } + _rope.formIndex(before: &ri) + let chunk = _rope[ri] + return Index( + baseUTF8Offset: i._utf8BaseOffset - chunk.utf8Count, + _rope: ri, + chunk: String.Index(_utf8Offset: chunk.utf8Count - 1)) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + func _characterIndex(roundingDown i: Index) -> Index { + let offset = i.utf8Offset + precondition(offset >= 0 && offset <= _utf8Count, "Index out of bounds") + guard offset > 0 else { return resolve(i, preferEnd: false)._knownCharacterAligned() } + guard offset < _utf8Count else { return resolve(i, preferEnd: true)._knownCharacterAligned() } + + let i = resolve(i, preferEnd: false) + guard !i._isKnownCharacterAligned else { return resolve(i, preferEnd: false) } + + var ri = i._rope! + let ci = i._chunkIndex + var chunk = _rope[ri] + if chunk.hasBreaks { + let first = chunk.firstBreak + let last = chunk.lastBreak + if ci == first || ci == last { return i } + if ci > last { + return Index( + baseUTF8Offset: i._utf8BaseOffset, _rope: ri, chunk: last + )._knownCharacterAligned() + } + if ci > first { + let j = chunk.wholeCharacters._index(roundingDown: ci) + return Index(baseUTF8Offset: i._utf8BaseOffset, _rope: ri, chunk: j)._knownCharacterAligned() + } + } + + var baseOffset = i._utf8BaseOffset + while ri > self._rope.startIndex { + self._rope.formIndex(before: &ri) + chunk = self._rope[ri] + baseOffset -= chunk.utf8Count + if chunk.hasBreaks { break } + } + return Index( + baseUTF8Offset: baseOffset, _rope: ri, chunk: chunk.lastBreak + )._knownCharacterAligned() + } + + func _unicodeScalarIndex(roundingDown i: Index) -> Index { + precondition(i <= endIndex, "Index out of bounds") + guard i > startIndex else { return resolve(i, preferEnd: false)._knownCharacterAligned() } + guard i < endIndex else { return resolve(i, preferEnd: true)._knownCharacterAligned() } + + let start = self.resolve(i, preferEnd: false) + guard !i._isKnownScalarAligned else { return resolve(i, preferEnd: false) } + let ri = start._rope! + let chunk = self._rope[ri] + let ci = chunk.string.unicodeScalars._index(roundingDown: start._chunkIndex) + return Index(baseUTF8Offset: start._utf8BaseOffset, _rope: ri, chunk: ci)._knownScalarAligned() + } + + func _utf8Index(roundingDown i: Index) -> Index { + precondition(i <= endIndex, "Index out of bounds") + guard i < endIndex else { return endIndex } + var r = i + if i._isUTF16TrailingSurrogate { + r._clearUTF16TrailingSurrogate() + } + return resolve(r, preferEnd: false) + } + + func _utf16Index(roundingDown i: Index) -> Index { + if i._isUTF16TrailingSurrogate { + precondition(i < endIndex, "Index out of bounds") + // (We know i can't be the endIndex -- it addresses a trailing surrogate.) + return self.resolve(i, preferEnd: false) + } + return _unicodeScalarIndex(roundingDown: i) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + func _characterIndex(roundingUp i: Index) -> Index { + let j = _characterIndex(roundingDown: i) + if i == j { return j } + return _characterIndex(after: j) + } + + func _unicodeScalarIndex(roundingUp i: Index) -> Index { + let j = _unicodeScalarIndex(roundingDown: i) + if i == j { return j } + return _unicodeScalarIndex(after: j) + } + + func _utf8Index(roundingUp i: Index) -> Index { + // Note: this orders UTF-16 trailing surrogate indices in between the first and second byte + // of the UTF-8 encoding. + let j = _utf8Index(roundingDown: i) + if i == j { return j } + return _utf8Index(after: j) + } + + func _utf16Index(roundingUp i: Index) -> Index { + // Note: if `i` addresses some byte in the middle of a non-BMP scalar then the result will + // point to the trailing surrogate. + let j = _utf16Index(roundingDown: i) + if i == j { return j } + return _utf16Index(after: j) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + func _character(at start: Index) -> (character: Character, end: Index) { + let start = _characterIndex(roundingDown: start) + precondition(start.utf8Offset < _utf8Count, "Index out of bounds") + + var ri = start._rope! + var ci = start._chunkIndex + var chunk = _rope[ri] + let char = chunk.wholeCharacters[ci] + let endOffset = start._utf8ChunkOffset + char.utf8.count + if endOffset < chunk.utf8Count { + let endStringIndex = chunk.string._utf8Index(at: endOffset) + let endIndex = Index( + baseUTF8Offset: start._utf8BaseOffset, _rope: ri, chunk: endStringIndex + )._knownCharacterAligned() + return (char, endIndex) + } + var s = String(char) + var base = start._utf8BaseOffset + chunk.utf8Count + while true { + _rope.formIndex(after: &ri) + guard ri < _rope.endIndex else { + ci = "".endIndex + break + } + chunk = _rope[ri] + s.append(contentsOf: chunk.prefix) + if chunk.hasBreaks { + ci = chunk.firstBreak + break + } + base += chunk.utf8Count + } + return (Character(s), Index(baseUTF8Offset: base, _rope: ri, chunk: ci)._knownCharacterAligned()) + } + + subscript(_utf8 index: Index) -> UInt8 { + precondition(index < endIndex, "Index out of bounds") + let index = resolve(index, preferEnd: false) + return _rope[index._rope!].string.utf8[index._chunkIndex] + } + + subscript(_utf8 offset: Int) -> UInt8 { + precondition(offset >= 0 && offset < _utf8Count, "Offset out of bounds") + let index = _utf8Index(at: offset) + return self[_utf8: index] + } + + subscript(_utf16 index: Index) -> UInt16 { + precondition(index < endIndex, "Index out of bounds") + let index = resolve(index, preferEnd: false) + return _rope[index._rope!].string.utf16[index._chunkIndex] + } + + subscript(_utf16 offset: Int) -> UInt16 { + precondition(offset >= 0 && offset < _utf16Count, "Offset out of bounds") + let index = _utf16Index(at: offset) + return self[_utf16: index] + } + + subscript(_character index: Index) -> Character { + _character(at: index).character + } + + subscript(_character offset: Int) -> Character { + precondition(offset >= 0 && offset < _utf8Count, "Offset out of bounds") + return _character(at: Index(_utf8Offset: offset)).character + } + + subscript(_unicodeScalar index: Index) -> Unicode.Scalar { + precondition(index < endIndex, "Index out of bounds") + let index = resolve(index, preferEnd: false) + return _rope[index._rope!].string.unicodeScalars[index._chunkIndex] + } + + subscript(_unicodeScalar offset: Int) -> Unicode.Scalar { + precondition(offset >= 0 && offset < _unicodeScalarCount, "Offset out of bounds") + let index = _unicodeScalarIndex(at: offset) + return self[_unicodeScalar: index] + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + func _foreachChunk( + from start: Index, + to end: Index, + _ body: (Substring) -> Void + ) { + precondition(start <= end) + guard start < end else { return } + let start = resolve(start, preferEnd: false) + let end = resolve(end, preferEnd: true) + + var ri = start._rope! + let endRopeIndex = end._rope! + + if ri == endRopeIndex { + let str = self._rope[ri].string + body(str[start._chunkIndex ..< end._chunkIndex]) + return + } + + let firstChunk = self._rope[ri].string + body(firstChunk[start._chunkIndex...]) + + _rope.formIndex(after: &ri) + while ri < endRopeIndex { + let string = _rope[ri].string + body(string[...]) + } + + let lastChunk = self._rope[ri].string + body(lastChunk[..=5.8) + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + public func _dump(heightLimit: Int = .max) { + _rope._dump(heightLimit: heightLimit) + } +} + +#endif diff --git a/Sources/RopeModule/BigString/Basics/BigString+Index.swift b/Sources/RopeModule/BigString/Basics/BigString+Index.swift new file mode 100644 index 000000000..d86081773 --- /dev/null +++ b/Sources/RopeModule/BigString/Basics/BigString+Index.swift @@ -0,0 +1,250 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=5.8) + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + public struct Index: Sendable { + typealias _Rope = BigString._Rope + + // ┌───────────────────────────────┬───┬───────────┬────────────────────┐ + // │ b63:b11 │b10│ b9:b8 │ b7:b0 │ + // ├───────────────────────────────┼───┼───────────┼────────────────────┤ + // │ UTF-8 global offset │ T │ alignment │ UTF-8 chunk offset │ + // └───────────────────────────────┴───┴───────────┴────────────────────┘ + // b10 (T): UTF-16 trailing surrogate indicator + // b9: isCharacterAligned + // b8: isScalarAligned + // + // 100: UTF-16 trailing surrogate + // 001: Index known to be scalar aligned + // 011: Index known to be Character aligned + var _rawBits: UInt64 + + /// A (possibly invalid) rope index. + var _rope: _Rope.Index? + + internal init(_raw: UInt64, _rope: _Rope.Index?) { + self._rawBits = _raw + self._rope = _rope + } + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString.Index { + @inline(__always) + internal static func _bitsForUTF8Offset(_ utf8Offset: Int) -> UInt64 { + let v = UInt64(truncatingIfNeeded: UInt(bitPattern: utf8Offset)) + assert(v &>> 53 == 0) + return v &<< 11 + } + + @inline(__always) + internal static var _flagsMask: UInt64 { 0x700 } + + @inline(__always) + internal static var _utf16TrailingSurrogateBits: UInt64 { 0x400 } + + @inline(__always) + internal static var _characterAlignmentBit: UInt64 { 0x200 } + + @inline(__always) + internal static var _scalarAlignmentBit: UInt64 { 0x100 } + + public var utf8Offset: Int { + Int(truncatingIfNeeded: _rawBits &>> 11) + } + + @inline(__always) + internal var _orderingValue: UInt64 { + _rawBits &>> 10 + } + + /// The offset within the addressed chunk. Only valid if `_rope` is not nil. + internal var _utf8ChunkOffset: Int { + assert(_rope != nil) + return Int(truncatingIfNeeded: _rawBits & 0xFF) + } + + /// The base offset of the addressed chunk. Only valid if `_rope` is not nil. + internal var _utf8BaseOffset: Int { + utf8Offset - _utf8ChunkOffset + } + + @inline(__always) + internal var _flags: UInt64 { + get { + _rawBits & Self._flagsMask + } + set { + assert(newValue & ~Self._flagsMask == 0) + _rawBits &= ~Self._flagsMask + _rawBits |= newValue + } + } +} + +extension String.Index { + @available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) + func _copyingAlignmentBits(from i: BigString.Index) -> String.Index { + var bits = _abi_rawBits & ~3 + bits |= (i._flags &>> 8) & 3 + return String.Index(_rawBits: bits) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString.Index { + internal var _chunkIndex: String.Index { + assert(_rope != nil) + return String.Index( + _utf8Offset: _utf8ChunkOffset, utf16TrailingSurrogate: _isUTF16TrailingSurrogate + )._copyingAlignmentBits(from: self) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString.Index { + internal mutating func _clearUTF16TrailingSurrogate() { + _flags = 0 + } + + public var _isUTF16TrailingSurrogate: Bool { + _orderingValue & 1 != 0 + } + + internal func _knownScalarAligned() -> Self { + var copy = self + copy._flags = Self._scalarAlignmentBit + return copy + } + + internal func _knownCharacterAligned() -> Self { + var copy = self + copy._flags = Self._characterAlignmentBit | Self._scalarAlignmentBit + return copy + } + + public var _isKnownScalarAligned: Bool { + _rawBits & Self._scalarAlignmentBit != 0 + } + + public var _isKnownCharacterAligned: Bool { + _rawBits & Self._characterAlignmentBit != 0 + } + + public init(_utf8Offset: Int) { + _rawBits = Self._bitsForUTF8Offset(_utf8Offset) + _rope = nil + } + + public init(_utf8Offset: Int, utf16TrailingSurrogate: Bool) { + _rawBits = Self._bitsForUTF8Offset(_utf8Offset) + if utf16TrailingSurrogate { + _rawBits |= Self._utf16TrailingSurrogateBits + } + _rope = nil + } + + internal init( + _utf8Offset: Int, utf16TrailingSurrogate: Bool = false, _rope: _Rope.Index, chunkOffset: Int + ) { + _rawBits = Self._bitsForUTF8Offset(_utf8Offset) + if utf16TrailingSurrogate { + _rawBits |= Self._utf16TrailingSurrogateBits + } + assert(chunkOffset >= 0 && chunkOffset <= 0xFF) + _rawBits |= UInt64(truncatingIfNeeded: chunkOffset) & 0xFF + self._rope = _rope + } + + internal init(baseUTF8Offset: Int, _rope: _Rope.Index, chunk: String.Index) { + let chunkUTF8Offset = chunk._utf8Offset + self.init( + _utf8Offset: baseUTF8Offset + chunkUTF8Offset, + utf16TrailingSurrogate: chunk._isUTF16TrailingSurrogate, + _rope: _rope, + chunkOffset: chunkUTF8Offset) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString.Index: Equatable { + public static func ==(left: Self, right: Self) -> Bool { + left._orderingValue == right._orderingValue + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString.Index: Comparable { + public static func <(left: Self, right: Self) -> Bool { + left._orderingValue < right._orderingValue + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString.Index: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(_orderingValue) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString.Index: CustomStringConvertible { + public var description: String { + let utf16Offset = _isUTF16TrailingSurrogate ? "+1" : "" + return "\(utf8Offset)[utf8]\(utf16Offset)" + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + func resolve(_ i: Index, preferEnd: Bool) -> Index { + if var ri = i._rope, _rope.isValid(ri) { + if preferEnd { + guard i.utf8Offset > 0, i._utf8ChunkOffset == 0, !i._isUTF16TrailingSurrogate else { + return i + } + _rope.formIndex(before: &ri) + let length = _rope[ri].utf8Count + let ci = String.Index(_utf8Offset: length) + var j = Index(baseUTF8Offset: i.utf8Offset - length, _rope: ri, chunk: ci) + j._flags = i._flags + return j + } + guard i.utf8Offset < _utf8Count, i._utf8ChunkOffset == _rope[ri].utf8Count else { + return i + } + _rope.formIndex(after: &ri) + let ci = String.Index(_utf8Offset: 0) + var j = Index(baseUTF8Offset: i.utf8Offset, _rope: ri, chunk: ci) + j._flags = i._flags + return j + } + + // Indices addressing trailing surrogates must never be resolved at the end of chunk, + // because the +1 doesn't make sense on any endIndex. + let trailingSurrogate = i._isUTF16TrailingSurrogate + + let (ri, chunkOffset) = _rope.find( + at: i.utf8Offset, + in: _UTF8Metric(), + preferEnd: preferEnd && !trailingSurrogate) + + let ci = String.Index( + _utf8Offset: chunkOffset, + utf16TrailingSurrogate: trailingSurrogate) + return Index(baseUTF8Offset: i.utf8Offset - ci._utf8Offset, _rope: ri, chunk: ci) + } +} +#endif diff --git a/Sources/RopeModule/BigString/Basics/BigString+Ingester.swift b/Sources/RopeModule/BigString/Basics/BigString+Ingester.swift new file mode 100644 index 000000000..6849e0a79 --- /dev/null +++ b/Sources/RopeModule/BigString/Basics/BigString+Ingester.swift @@ -0,0 +1,167 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=5.8) + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + func _ingester( + forInserting input: __owned Substring, + at index: Index, + allowForwardPeek: Bool + ) -> _Ingester { + let hint = allowForwardPeek ? input.unicodeScalars.first : nil + let state = self._breakState(upTo: index, nextScalarHint: hint) + return _Ingester(input, startState: state) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + internal struct _Ingester { + typealias _Chunk = BigString._Chunk + typealias Counts = BigString._Chunk.Counts + + var input: Substring + + /// The index of the beginning of the next chunk. + var start: String.Index + + /// Grapheme breaking state at the start of the next chunk. + var state: _CharacterRecognizer + + init(_ input: Substring) { + self.input = input + self.start = input.startIndex + self.state = _CharacterRecognizer() + } + + init(_ input: Substring, startState: __owned _CharacterRecognizer) { + self.input = input + self.start = input.startIndex + self.state = startState + } + + init(_ input: String) { + self.init(input[...]) + } + + init(_ input: S) { + self.init(Substring(input)) + } + + var isAtEnd: Bool { + start == input.endIndex + } + + var remainingUTF8: Int { + input.utf8.distance(from: start, to: input.endIndex) + } + + mutating func nextSlice( + maxUTF8Count: Int = _Chunk.maxUTF8Count + ) -> _Chunk.Slice? { + guard let range = input.base._nextSlice( + after: start, limit: input.endIndex, maxUTF8Count: maxUTF8Count) + else { + assert(start == input.endIndex) + return nil + } + if range.isEmpty { + return nil // Not enough room. + } + assert(range.lowerBound == start && range.upperBound <= input.endIndex) + start = range.upperBound + + var s = input[range] + let c8 = s.utf8.count + guard let r = state.firstBreak(in: s) else { + // Anomalous case -- chunk is entirely a continuation of a single character. + return ( + string: s, + characters: 0, + prefix: c8, + suffix: c8) + } + let first = r.lowerBound + s = s.suffix(from: r.upperBound) + + var characterCount = 1 + var last = first + while let r = state.firstBreak(in: s) { + last = r.lowerBound + s = s.suffix(from: r.upperBound) + characterCount += 1 + } + let prefixCount = input.utf8.distance(from: range.lowerBound, to: first) + let suffixCount = input.utf8.distance(from: last, to: range.upperBound) + return ( + string: input[range], + characters: characterCount, + prefix: prefixCount, + suffix: suffixCount) + } + + mutating func nextChunk(maxUTF8Count: Int = _Chunk.maxUTF8Count) -> _Chunk? { + guard let slice = nextSlice(maxUTF8Count: maxUTF8Count) else { return nil } + return _Chunk(slice) + } + + static func desiredNextChunkSize(remaining: Int) -> Int { + if remaining <= _Chunk.maxUTF8Count { + return remaining + } + if remaining >= _Chunk.maxUTF8Count + _Chunk.minUTF8Count { + return _Chunk.maxUTF8Count + } + return remaining - _Chunk.minUTF8Count + } + + mutating func nextWellSizedSlice(suffix: Int = 0) -> _Chunk.Slice? { + let desired = Self.desiredNextChunkSize(remaining: remainingUTF8 + suffix) + return nextSlice(maxUTF8Count: desired) + } + + mutating func nextWellSizedChunk(suffix: Int = 0) -> _Chunk? { + guard let slice = nextWellSizedSlice(suffix: suffix) else { return nil } + return _Chunk(slice) + } + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension String { + func _nextSlice( + after i: Index, + limit: Index, + maxUTF8Count: Int + ) -> Range? { + assert(maxUTF8Count >= 0) + assert(i._isKnownScalarAligned) + guard i < limit else { return nil } + let end = self.utf8.index(i, offsetBy: maxUTF8Count, limitedBy: limit) ?? limit + let j = self.unicodeScalars._index(roundingDown: end) + return Range(uncheckedBounds: (i, j)) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString._Chunk { + init(_ string: String) { + guard !string.isEmpty else { self.init(); return } + assert(string.utf8.count <= Self.maxUTF8Count) + var ingester = BigString._Ingester(string) + self = ingester.nextChunk()! + assert(ingester.isAtEnd) + } +} + +#endif diff --git a/Sources/RopeModule/BigString/Basics/BigString+Invariants.swift b/Sources/RopeModule/BigString/Basics/BigString+Invariants.swift new file mode 100644 index 000000000..0cb99ec88 --- /dev/null +++ b/Sources/RopeModule/BigString/Basics/BigString+Invariants.swift @@ -0,0 +1,39 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=5.8) + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + public func _invariantCheck() { +#if COLLECTIONS_INTERNAL_CHECKS + _rope._invariantCheck() + let allowUndersize = _rope.isSingleton + + var state = _CharacterRecognizer() + for chunk in _rope { + precondition(allowUndersize || !chunk.isUndersized, "Undersized chunk") + let (characters, prefix, suffix) = state.edgeCounts(consuming: chunk.string) + precondition( + chunk.prefixCount == prefix, + "Inconsistent position of first grapheme break in chunk") + precondition( + chunk.suffixCount == suffix, + "Inconsistent position of last grapheme break in chunk") + precondition( + chunk.characterCount == characters, + "Inconsistent character count in chunk") + } +#endif + } +} + +#endif diff --git a/Sources/RopeModule/BigString/Basics/BigString+Iterators.swift b/Sources/RopeModule/BigString/Basics/BigString+Iterators.swift new file mode 100644 index 000000000..0b7995585 --- /dev/null +++ b/Sources/RopeModule/BigString/Basics/BigString+Iterators.swift @@ -0,0 +1,38 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=5.8) + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + struct ChunkIterator { + var base: _Rope.Iterator + + init(base: _Rope.Iterator) { + self.base = base + } + } + + func makeChunkIterator() -> ChunkIterator { + ChunkIterator(base: _rope.makeIterator()) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString.ChunkIterator: IteratorProtocol { + typealias Element = String + + mutating func next() -> String? { + base.next()?.string + } +} + +#endif diff --git a/Sources/RopeModule/BigString/Basics/BigString+Metrics.swift b/Sources/RopeModule/BigString/Basics/BigString+Metrics.swift new file mode 100644 index 000000000..eb335b402 --- /dev/null +++ b/Sources/RopeModule/BigString/Basics/BigString+Metrics.swift @@ -0,0 +1,209 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=5.8) + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +internal protocol _StringMetric: RopeMetric where Element == BigString._Chunk { + func distance( + from start: String.Index, + to end: String.Index, + in chunk: BigString._Chunk + ) -> Int + + func formIndex( + _ i: inout String.Index, + offsetBy distance: inout Int, + in chunk: BigString._Chunk + ) -> (found: Bool, forward: Bool) +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + internal struct _CharacterMetric: _StringMetric { + typealias Element = BigString._Chunk + typealias Summary = BigString.Summary + + @inline(__always) + func size(of summary: Summary) -> Int { + summary.characters + } + + func distance( + from start: String.Index, + to end: String.Index, + in chunk: BigString._Chunk + ) -> Int { + chunk.characterDistance(from: start, to: end) + } + + func formIndex( + _ i: inout String.Index, + offsetBy distance: inout Int, + in chunk: BigString._Chunk + ) -> (found: Bool, forward: Bool) { + chunk.formCharacterIndex(&i, offsetBy: &distance) + } + + func index(at offset: Int, in chunk: BigString._Chunk) -> String.Index { + precondition(offset < chunk.characterCount) + return chunk.wholeCharacters._index(at: offset) + } + } + + internal struct _UnicodeScalarMetric: _StringMetric { + @inline(__always) + func size(of summary: Summary) -> Int { + summary.unicodeScalars + } + + func distance( + from start: String.Index, + to end: String.Index, + in chunk: BigString._Chunk + ) -> Int { + chunk.string.unicodeScalars.distance(from: start, to: end) + } + + func formIndex( + _ i: inout String.Index, + offsetBy distance: inout Int, + in chunk: BigString._Chunk + ) -> (found: Bool, forward: Bool) { + guard distance != 0 else { + i = chunk.string.unicodeScalars._index(roundingDown: i) + return (true, false) + } + if distance > 0 { + let end = chunk.string.endIndex + while distance > 0, i < end { + chunk.string.unicodeScalars.formIndex(after: &i) + distance &-= 1 + } + return (distance == 0, true) + } + let start = chunk.string.startIndex + while distance < 0, i > start { + chunk.string.unicodeScalars.formIndex(before: &i) + distance &+= 1 + } + return (distance == 0, false) + } + + func index(at offset: Int, in chunk: BigString._Chunk) -> String.Index { + chunk.string.unicodeScalars.index(chunk.string.startIndex, offsetBy: offset) + } + } + + internal struct _UTF8Metric: _StringMetric { + @inline(__always) + func size(of summary: Summary) -> Int { + summary.utf8 + } + + func distance( + from start: String.Index, + to end: String.Index, + in chunk: BigString._Chunk + ) -> Int { + chunk.string.utf8.distance(from: start, to: end) + } + + func formIndex( + _ i: inout String.Index, + offsetBy distance: inout Int, + in chunk: BigString._Chunk + ) -> (found: Bool, forward: Bool) { + // Here we make use of the fact that the UTF-8 view of native Swift strings + // have O(1) index distance & offset calculations. + let offset = chunk.string.utf8.distance(from: chunk.string.startIndex, to: i) + if distance >= 0 { + let rest = chunk.utf8Count - offset + if distance > rest { + i = chunk.string.endIndex + distance -= rest + return (false, true) + } + i = chunk.string.utf8.index(i, offsetBy: distance) + distance = 0 + return (true, true) + } + + if offset + distance < 0 { + i = chunk.string.startIndex + distance += offset + return (false, false) + } + i = chunk.string.utf8.index(i, offsetBy: distance) + distance = 0 + return (true, false) + } + + func index(at offset: Int, in chunk: BigString._Chunk) -> String.Index { + chunk.string.utf8.index(chunk.string.startIndex, offsetBy: offset) + } + } + + internal struct _UTF16Metric: _StringMetric { + @inline(__always) + func size(of summary: Summary) -> Int { + summary.utf16 + } + + func distance( + from start: String.Index, + to end: String.Index, + in chunk: BigString._Chunk + ) -> Int { + chunk.string.utf16.distance(from: start, to: end) + } + + func formIndex( + _ i: inout String.Index, + offsetBy distance: inout Int, + in chunk: BigString._Chunk + ) -> (found: Bool, forward: Bool) { + if distance >= 0 { + if + distance <= chunk.utf16Count, + let r = chunk.string.utf16.index( + i, offsetBy: distance, limitedBy: chunk.string.endIndex + ) { + i = r + distance = 0 + return (true, true) + } + distance -= chunk.string.utf16.distance(from: i, to: chunk.string.endIndex) + i = chunk.string.endIndex + return (false, true) + } + + if + distance.magnitude <= chunk.utf16Count, + let r = chunk.string.utf16.index( + i, offsetBy: distance, limitedBy: chunk.string.endIndex + ) { + i = r + distance = 0 + return (true, true) + } + distance += chunk.string.utf16.distance(from: chunk.string.startIndex, to: i) + i = chunk.string.startIndex + return (false, false) + } + + func index(at offset: Int, in chunk: BigString._Chunk) -> String.Index { + chunk.string.utf16.index(chunk.string.startIndex, offsetBy: offset) + } + } +} + +#endif diff --git a/Sources/RopeModule/BigString/Basics/BigString+Summary.swift b/Sources/RopeModule/BigString/Basics/BigString+Summary.swift new file mode 100644 index 000000000..f11bb18f8 --- /dev/null +++ b/Sources/RopeModule/BigString/Basics/BigString+Summary.swift @@ -0,0 +1,82 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=5.8) + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + struct Summary { + // FIXME: We only need 48 * 3 = 192 bits to represent a nonnegative value; pack these better + // (Unfortunately we also need to represent negative values right now.) + private(set) var characters: Int + private(set) var unicodeScalars: Int + private(set) var utf16: Int + private(set) var utf8: Int + + init() { + characters = 0 + unicodeScalars = 0 + utf16 = 0 + utf8 = 0 + } + + init(_ chunk: BigString._Chunk) { + self.utf8 = chunk.utf8Count + self.utf16 = Int(chunk.counts.utf16) + self.unicodeScalars = Int(chunk.counts.unicodeScalars) + self.characters = Int(chunk.counts.characters) + } + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString.Summary: CustomStringConvertible { + var description: String { + "❨\(utf8)⋅\(utf16)⋅\(unicodeScalars)⋅\(characters)❩" + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString.Summary: RopeSummary { + @inline(__always) + static var maxNodeSize: Int { + #if DEBUG + return 10 + #else + return 15 + #endif + } + + @inline(__always) + static var nodeSizeBitWidth: Int { 4 } + + @inline(__always) + static var zero: Self { Self() } + + @inline(__always) + var isZero: Bool { utf8 == 0 } + + mutating func add(_ other: BigString.Summary) { + characters += other.characters + unicodeScalars += other.unicodeScalars + utf16 += other.utf16 + utf8 += other.utf8 + } + + mutating func subtract(_ other: BigString.Summary) { + characters -= other.characters + unicodeScalars -= other.unicodeScalars + utf16 -= other.utf16 + utf8 -= other.utf8 + } +} + +#endif diff --git a/Sources/RopeModule/BigString/Basics/BigString.swift b/Sources/RopeModule/BigString/Basics/BigString.swift new file mode 100644 index 000000000..fe782f276 --- /dev/null +++ b/Sources/RopeModule/BigString/Basics/BigString.swift @@ -0,0 +1,33 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=5.8) + +/// The core of a B-tree based String implementation. +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +public struct BigString: Sendable { + typealias _Rope = Rope<_Chunk> + + var _rope: _Rope + + internal init(_rope: _Rope) { + self._rope = _rope + } +} + +#else + +// `BigString` depends on fixes and newly exposed functionality that landed in +// version 5.8 of the Swift Standard Library. +@available(*, unavailable, message: "BigString depends on version 5.8 of the Swift Standard Library") +public struct BigString: Sendable {} + +#endif diff --git a/Sources/RopeModule/BigString/Chunk/BigString+Chunk+Append and Insert.swift b/Sources/RopeModule/BigString/Chunk/BigString+Chunk+Append and Insert.swift new file mode 100644 index 000000000..20da42a86 --- /dev/null +++ b/Sources/RopeModule/BigString/Chunk/BigString+Chunk+Append and Insert.swift @@ -0,0 +1,174 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=5.8) + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString._Chunk { + mutating func append(_ other: __owned Self) { + self._append(other.string[...], other.counts) + } + + mutating func append(from ingester: inout BigString._Ingester) -> Self? { + let desired = BigString._Ingester.desiredNextChunkSize( + remaining: self.utf8Count + ingester.remainingUTF8) + if desired == self.utf8Count { + return nil + } + if desired > self.utf8Count { + if let slice = ingester.nextSlice(maxUTF8Count: desired - self.utf8Count) { + self._append(slice) + } + return nil + } + + // Split current chunk. + let cut = string.unicodeScalars._index(roundingDown: string._utf8Index(at: desired)) + var new = self.split(at: cut) + precondition(!self.isUndersized) + let slice = ingester.nextSlice()! + new._append(slice) + precondition(ingester.isAtEnd) + precondition(!new.isUndersized) + return new + } + + mutating func _append(_ other: Slice) { + let c = Counts(other) + _append(other.string, c) + } + + mutating func _append(_ str: __owned Substring, _ other: Counts) { + self.counts.append(other) + self.string += str + invariantCheck() + } + + mutating func _prepend(_ other: Slice) { + let c = Counts(other) + _prepend(other.string, c) + } + + mutating func _prepend(_ str: __owned Substring, _ other: Counts) { + let c = self.counts + self.counts = other + self.counts.append(c) + self.string = str + self.string + invariantCheck() + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString._Chunk { + mutating func _insert( + _ slice: Slice, + at index: String.Index, + old: inout _CharacterRecognizer, + new: inout _CharacterRecognizer + ) -> String.Index? { + let offset = string._utf8Offset(of: index) + let count = slice.string.utf8.count + precondition(utf8Count + count <= Self.maxUTF8Count) + + let parts = self.splitCounts(at: index) + self.counts = parts.left + self.counts.append(Counts(slice)) + self.counts.append(parts.right) + + string.insert(contentsOf: slice.string, at: index) + + let end = string._utf8Index(at: offset + count) + return resyncBreaks(startingAt: end, old: &old, new: &new) + } + + typealias States = (increment: Int, old: _CharacterRecognizer, new: _CharacterRecognizer) + + mutating func insertAll( + from ingester: inout BigString._Ingester, + at index: String.Index + ) -> States? { + let remaining = ingester.remainingUTF8 + precondition(self.utf8Count + remaining <= Self.maxUTF8Count) + var startState = ingester.state + guard let slice = ingester.nextSlice(maxUTF8Count: remaining) else { return nil } + var endState = ingester.state + assert(ingester.isAtEnd) + let offset = string._utf8Offset(of: index) + if let _ = self._insert(slice, at: index, old: &startState, new: &endState) { + return nil + } + return (self.utf8Count - offset, startState, endState) + } + + enum InsertResult { + case inline(States?) + case split(spawn: BigString._Chunk, endStates: States?) + case large + } + + mutating func insert( + from ingester: inout BigString._Ingester, + at index: String.Index + ) -> InsertResult { + let origCount = self.utf8Count + let rem = ingester.remainingUTF8 + guard rem > 0 else { return .inline(nil) } + let sum = origCount + rem + + let offset = string._utf8Offset(of: index) + if sum <= Self.maxUTF8Count { + let r = insertAll(from: &ingester, at: index) + return .inline(r) + } + + let desired = BigString._Ingester.desiredNextChunkSize(remaining: sum) + guard sum - desired + Self.maxSlicingError <= Self.maxUTF8Count else { return .large } + + if desired <= offset { + // Inserted text lies entirely within `spawn`. + let cut = string.unicodeScalars._index(roundingDown: string._utf8Index(at: desired)) + var spawn = split(at: cut) + let i = spawn.string._utf8Index(at: offset - self.utf8Count) + let r = spawn.insertAll(from: &ingester, at: i) + assert(r == nil || r?.increment == sum - offset) + return .split(spawn: spawn, endStates: r) + } + if desired >= offset + rem { + // Inserted text lies entirely within `self`. + let cut = string.unicodeScalars._index(roundingDown: string._utf8Index(at: desired - rem)) + assert(cut >= index) + var spawn = split(at: cut) + guard + var r = self.insertAll(from: &ingester, at: string._utf8Index(at: offset)), + nil == spawn.resyncBreaks(startingAt: spawn.string.startIndex, old: &r.old, new: &r.new) + else { + return .split(spawn: spawn, endStates: nil) + } + return .split(spawn: spawn, endStates: (sum - offset, r.old, r.new)) + } + // Inserted text is split across `self` and `spawn`. + var spawn = split(at: index) + var old = ingester.state + if let slice = ingester.nextSlice(maxUTF8Count: desired - offset) { + self._append(slice) + } + let slice = ingester.nextSlice()! + assert(ingester.isAtEnd) + var new = ingester.state + let stop = spawn._insert(slice, at: spawn.string.startIndex, old: &old, new: &new) + if stop != nil { + return .split(spawn: spawn, endStates: nil) + } + return .split(spawn: spawn, endStates: (sum - offset, old, new)) + } +} + +#endif diff --git a/Sources/RopeModule/BigString/Chunk/BigString+Chunk+Breaks.swift b/Sources/RopeModule/BigString/Chunk/BigString+Chunk+Breaks.swift new file mode 100644 index 000000000..ba4c3261a --- /dev/null +++ b/Sources/RopeModule/BigString/Chunk/BigString+Chunk+Breaks.swift @@ -0,0 +1,71 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=5.8) + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString._Chunk { + @inline(__always) + var hasBreaks: Bool { counts.hasBreaks } + + var firstBreak: String.Index { + get { + string._utf8Index(at: prefixCount) + } + set { + counts.prefix = string._utf8Offset(of: newValue) + } + } + + var lastBreak: String.Index { + get { + string._utf8Index(at: utf8Count - suffixCount) + } + set { + counts.suffix = utf8Count - string._utf8Offset(of: newValue) + } + } + + var prefix: Substring { string[.. String.Index? { + let index = string.unicodeScalars._index(roundingDown: index) + let first = firstBreak + guard index > first else { return nil } + let last = lastBreak + guard index <= last else { return last } + let w = string[first...] + let rounded = w._index(roundingDown: index) + guard rounded == index else { return rounded } + return w.index(before: rounded) + } + + func immediateBreakState( + upTo index: String.Index + ) -> (prevBreak: String.Index, state: _CharacterRecognizer)? { + guard let prev = nearestBreak(before: index) else { return nil } + let state = _CharacterRecognizer(partialCharacter: string[prev..=5.8) + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString._Chunk { + struct Counts: Equatable { + /// The number of UTF-8 code units within this chunk. + var utf8: UInt8 + /// The number of UTF-16 code units within this chunk. + var utf16: UInt8 + /// The number of Unicode scalars within this chunk. + var unicodeScalars: UInt8 + /// The number of Unicode scalars within this chunk that start a Character. + var _characters: UInt8 + /// The number of UTF-8 code units at the start of this chunk that continue a Character + /// whose start scalar is in a previous chunk. + var _prefix: UInt8 + /// The number of UTF-8 code units at the end of this chunk that form the start a Character + /// whose end scalar is in a subsequent chunk. + var _suffix: UInt8 + + init() { + self.utf8 = 0 + self.utf16 = 0 + self.unicodeScalars = 0 + self._characters = 0 + self._prefix = 0 + self._suffix = 0 + } + + init( + utf8: UInt8, + utf16: UInt8, + unicodeScalars: UInt8, + characters: UInt8, + prefix: UInt8, + suffix: UInt8 + ) { + assert(characters >= 0 && characters <= unicodeScalars && unicodeScalars <= utf16) + self.utf8 = utf8 + self.utf16 = utf16 + self.unicodeScalars = unicodeScalars + self._characters = characters + self._prefix = prefix + self._suffix = suffix + } + + init( + utf8: Int, + utf16: Int, + unicodeScalars: Int, + characters: Int, + prefix: Int, + suffix: Int + ) { + assert(characters >= 0 && characters <= unicodeScalars && unicodeScalars <= utf16) + self.utf8 = UInt8(utf8) + self.utf16 = UInt8(utf16) + self.unicodeScalars = UInt8(unicodeScalars) + self._characters = UInt8(characters) + self._prefix = UInt8(prefix) + self._suffix = UInt8(suffix) + } + + init( + anomalousUTF8 utf8: Int, + utf16: Int, + unicodeScalars: Int + ) { + self.utf8 = UInt8(utf8) + self.utf16 = UInt8(utf16) + self.unicodeScalars = UInt8(unicodeScalars) + self._characters = 0 + self._prefix = self.utf8 + self._suffix = self.utf8 + } + + init(_ slice: Slice) { + let c = slice.string.utf8.count + precondition(c <= BigString._Chunk.maxUTF8Count) + self.init( + utf8: slice.string.utf8.count, + utf16: slice.string.utf16.count, + unicodeScalars: slice.string.unicodeScalars.count, + characters: slice.characters, + prefix: slice.prefix, + suffix: slice.suffix) + } + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString._Chunk.Counts { + var characters: Int { + get { Int(_characters) } + set { _characters = UInt8(newValue) } + } + + var prefix: Int { + get { Int(_prefix) } + set { _prefix = UInt8(newValue) } + } + + var suffix: Int { + get { Int(_suffix) } + set { _suffix = UInt8(newValue) } + } + + var hasBreaks: Bool { + _prefix < utf8 + } + + func hasSpaceToMerge(_ other: Self) -> Bool { + Int(utf8) + Int(other.utf8) <= BigString._Chunk.maxUTF8Count + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString._Chunk.Counts { + mutating func append(_ other: Self) { + assert(hasSpaceToMerge(other)) + + switch (self.hasBreaks, other.hasBreaks) { + case (true, true): + self._suffix = other._suffix + case (true, false): + self._suffix += other._suffix + case (false, true): + self._prefix += other._prefix + self._suffix = other._suffix + case (false, false): + self._prefix += other._prefix + self._suffix += other._suffix + } + self.utf8 += other.utf8 + self.utf16 += other.utf16 + self.unicodeScalars += other.unicodeScalars + self._characters += other._characters + } +} + +#endif diff --git a/Sources/RopeModule/BigString/Chunk/BigString+Chunk+Description.swift b/Sources/RopeModule/BigString/Chunk/BigString+Chunk+Description.swift new file mode 100644 index 000000000..c44a37664 --- /dev/null +++ b/Sources/RopeModule/BigString/Chunk/BigString+Chunk+Description.swift @@ -0,0 +1,72 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=5.8) + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString._Chunk: CustomStringConvertible { + var description: String { + let counts = """ + ❨\(utf8Count)⋅\(utf16Count)⋅\(unicodeScalarCount)⋅\(characterCount)❩ + """._rpad(to: 17) + let d = _succinctContents(maxLength: 10) + return "Chunk(\(_identity)\(counts) \(d))" + } + + var _identity: String { +#if arch(arm64) || arch(x86_64) + // Let's use the second word of the string representation as the identity; it contains + // the String's storage reference (if any). + let b = unsafeBitCast(self.string, to: (UInt64, UInt64).self) + return "@" + String(b.1, radix: 16)._rpad(to: 17) +#else + return "" +#endif + } + + func _succinctContents(maxLength c: Int) -> String { + /// 4+"short"-1 + /// 0+"longer...string"-1 + let pc = String(prefixCount)._lpad(to: 3) + let sc = String(suffixCount) + + let s = String(wholeCharacters) + if s.isEmpty { + return "\(pc)+...-\(sc)" + } + var result = "\(pc)+\"" + var state = _CharacterRecognizer(consuming: result) + + let i = result._appendQuotedProtectingLeft(s, with: &state, maxLength: c) + let j = s.index(s.endIndex, offsetBy: -c, limitedBy: string.startIndex) ?? string.startIndex + + if i < j { + result._appendProtectingRight("...", with: &state) + result._appendQuotedProtectingLeft(String(s[j...]), with: &state) + } else if i < s.endIndex { + let k = s._index(roundingDown: i) + if i == k { + result._appendQuotedProtectingLeft(String(s[i...]), with: &state) + } else if s.index(after: k) < s.endIndex { + result._appendProtectingRight("...", with: &state) + result._appendQuotedProtectingLeft(String(s[s.index(after: k)...]), with: &state) + } else { + let suffix = String(s[i...].unicodeScalars.suffix(3)) + result._appendProtectingRight("...", with: &state) + result._appendQuotedProtectingLeft(suffix, with: &state) + } + } + result._appendProtectingRight("\"-\(sc)", with: &state) + return result + } +} + +#endif diff --git a/Sources/RopeModule/BigString/Chunk/BigString+Chunk+Indexing by Characters.swift b/Sources/RopeModule/BigString/Chunk/BigString+Chunk+Indexing by Characters.swift new file mode 100644 index 000000000..5e6f54bb3 --- /dev/null +++ b/Sources/RopeModule/BigString/Chunk/BigString+Chunk+Indexing by Characters.swift @@ -0,0 +1,112 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=5.8) + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension UInt8 { + /// Returns true if this is a leading code unit in the UTF-8 encoding of a Unicode scalar that + /// is outside the BMP. + var _isUTF8NonBMPLeadingCodeUnit: Bool { self >= 0b11110000 } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString._Chunk { + func characterDistance(from start: String.Index, to end: String.Index) -> Int { + let firstBreak = self.firstBreak + let (start, a) = start < firstBreak ? (firstBreak, 1) : (start, 0) + let (end, b) = end < firstBreak ? (firstBreak, 1) : (end, 0) + let d = wholeCharacters.distance(from: start, to: end) + return d + a - b + } + + /// If this returns false, the next position is on the first grapheme break following this + /// chunk. + func formCharacterIndex(after i: inout String.Index) -> Bool { + if i >= lastBreak { + i = string.endIndex + return false + } + let first = firstBreak + if i < first { + i = first + return true + } + wholeCharacters.formIndex(after: &i) + return true + } + + /// If this returns false, the right position is `distance` steps from the first grapheme break + /// following this chunk if `distance` was originally positive. Otherwise the right position is + /// `-distance` steps from the first grapheme break preceding this chunk. + func formCharacterIndex( + _ i: inout String.Index, offsetBy distance: inout Int + ) -> (found: Bool, forward: Bool) { + if distance == 0 { + if i < firstBreak { + i = string.startIndex + return (false, false) + } + if i >= lastBreak { + i = lastBreak + return (true, false) + } + i = wholeCharacters._index(roundingDown: i) + return (true, false) + } + if distance > 0 { + if i >= lastBreak { + i = string.endIndex + distance -= 1 + return (false, true) + } + if i < firstBreak { + i = firstBreak + distance -= 1 + if distance == 0 { return (true, true) } + } + if + distance <= characterCount, + let r = wholeCharacters.index(i, offsetBy: distance, limitedBy: string.endIndex) + { + i = r + distance = 0 + return (i < string.endIndex, true) + } + distance -= wholeCharacters.distance(from: i, to: lastBreak) + 1 + i = string.endIndex + return (false, true) + } + if i <= firstBreak { + i = string.startIndex + if i == firstBreak { distance += 1 } + return (false, false) + } + if i > lastBreak { + i = lastBreak + distance += 1 + if distance == 0 { return (true, false) } + } + if + distance.magnitude <= characterCount, + let r = self.wholeCharacters.index(i, offsetBy: distance, limitedBy: firstBreak) + { + i = r + distance = 0 + return (true, false) + } + distance += self.wholeCharacters.distance(from: firstBreak, to: i) + i = string.startIndex + return (false, false) + } +} + +#endif diff --git a/Sources/RopeModule/BigString/Chunk/BigString+Chunk+Indexing by UTF16.swift b/Sources/RopeModule/BigString/Chunk/BigString+Chunk+Indexing by UTF16.swift new file mode 100644 index 000000000..cc60346f0 --- /dev/null +++ b/Sources/RopeModule/BigString/Chunk/BigString+Chunk+Indexing by UTF16.swift @@ -0,0 +1,30 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=5.8) + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString._Chunk { + /// UTF-16 index lookup. + func index(at utf8Offset: Int, utf16TrailingSurrogate: Bool) -> String.Index { + var index = string._utf8Index(at: utf8Offset) + if + utf16TrailingSurrogate, + index < string.endIndex, + string.utf8[index]._isUTF8NonBMPLeadingCodeUnit + { + index = string.utf16.index(after: index) + } + return index + } +} + +#endif diff --git a/Sources/RopeModule/BigString/Chunk/BigString+Chunk+RopeElement.swift b/Sources/RopeModule/BigString/Chunk/BigString+Chunk+RopeElement.swift new file mode 100644 index 000000000..37bb4ad5f --- /dev/null +++ b/Sources/RopeModule/BigString/Chunk/BigString+Chunk+RopeElement.swift @@ -0,0 +1,158 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=5.8) + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString._Chunk: RopeElement { + typealias Summary = BigString.Summary + typealias Index = String.Index + + var summary: BigString.Summary { + Summary(self) + } + + var isEmpty: Bool { string.isEmpty } + + var isUndersized: Bool { utf8Count < Self.minUTF8Count } + + func invariantCheck() { +#if COLLECTIONS_INTERNAL_CHECKS + precondition(string.endIndex._canBeUTF8) + let c = utf8Count + if c == 0 { + precondition(counts == Counts(), "Non-empty counts") + return + } + precondition(c <= Self.maxUTF8Count, "Oversized chunk") + //precondition(utf8Count >= Self.minUTF8Count, "Undersized chunk") + + precondition(counts.utf8 == string.utf8.count, "UTF-8 count mismatch") + precondition(counts.utf16 == string.utf16.count, "UTF-16 count mismatch") + precondition(counts.unicodeScalars == string.unicodeScalars.count, "Scalar count mismatch") + + precondition(counts.prefix <= c, "Invalid prefix count") + precondition(counts.suffix <= c && counts.suffix > 0, "Invalid suffix count") + if Int(counts.prefix) + Int(counts.suffix) <= c { + let i = firstBreak + let j = lastBreak + precondition(i <= j, "Overlapping prefix and suffix") + let s = string[i...] + precondition(counts.characters == s.count, "Inconsistent character count") + precondition(j == s.index(before: s.endIndex), "Inconsistent suffix count") + } else { + // Anomalous case + precondition(counts.prefix == c, "Inconsistent prefix count (continuation)") + precondition(counts.suffix == c, "Inconsistent suffix count (continuation)") + precondition(counts.characters == 0, "Inconsistent character count (continuation)") + } +#endif + } + + mutating func rebalance(nextNeighbor right: inout Self) -> Bool { + if self.isEmpty { + swap(&self, &right) + return true + } + guard !right.isEmpty else { return true } + guard self.isUndersized || right.isUndersized else { return false } + let sum = self.utf8Count + right.utf8Count + let desired = BigString._Ingester.desiredNextChunkSize(remaining: sum) + + precondition(desired != self.utf8Count) + if desired < self.utf8Count { + let i = self.string._utf8Index(at: desired) + let j = self.string.unicodeScalars._index(roundingDown: i) + Self._redistributeData(&self, &right, splittingLeftAt: j) + } else { + let i = right.string._utf8Index(at: desired - self.utf8Count) + let j = right.string.unicodeScalars._index(roundingDown: i) + Self._redistributeData(&self, &right, splittingRightAt: j) + } + assert(right.isEmpty || (!self.isUndersized && !right.isUndersized)) + return right.isEmpty + } + + mutating func rebalance(prevNeighbor left: inout Self) -> Bool { + if self.isEmpty { + swap(&self, &left) + return true + } + guard !left.isEmpty else { return true } + guard left.isUndersized || self.isUndersized else { return false } + let sum = left.utf8Count + self.utf8Count + let desired = BigString._Ingester.desiredNextChunkSize(remaining: sum) + + precondition(desired != self.utf8Count) + if desired < self.utf8Count { + let i = self.string._utf8Index(at: self.utf8Count - desired) + let j = self.string.unicodeScalars._index(roundingDown: i) + let k = (i == j ? i : self.string.unicodeScalars.index(after: j)) + Self._redistributeData(&left, &self, splittingRightAt: k) + } else { + let i = left.string._utf8Index(at: left.utf8Count + self.utf8Count - desired) + let j = left.string.unicodeScalars._index(roundingDown: i) + let k = (i == j ? i : left.string.unicodeScalars.index(after: j)) + Self._redistributeData(&left, &self, splittingLeftAt: k) + } + assert(left.isEmpty || (!left.isUndersized && !self.isUndersized)) + return left.isEmpty + } + + mutating func split(at i: String.Index) -> Self { + assert(i == string.unicodeScalars._index(roundingDown: i)) + let c = splitCounts(at: i) + let new = Self(string[i...], c.right) + self = Self(string[.. right.string.startIndex) + guard i < right.string.endIndex else { + left.append(right) + right.clear() + return + } + let counts = right.splitCounts(at: i) + left._append(right.string[.. left.string.startIndex else { + left.append(right) + right.clear() + swap(&left, &right) + return + } + let counts = left.splitCounts(at: i) + right._prepend(left.string[i...], counts.right) + left = Self(left.string[..=5.8) + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString._Chunk { + func splitCounts(at i: String.Index) -> (left: Counts, right: Counts) { + precondition(i <= string.endIndex) + guard i < string.endIndex else { + return (self.counts, Counts()) + } + guard i > string.startIndex else { + return (Counts(), self.counts) + } + let i = string.unicodeScalars._index(roundingDown: i) + + let leftUTF16: Int + let leftScalars: Int + let rightUTF16: Int + let rightScalars: Int + if string._utf8Offset(of: i) <= utf8Count / 2 { + leftUTF16 = string.utf16.distance(from: string.startIndex, to: i) + rightUTF16 = self.utf16Count - leftUTF16 + leftScalars = string.unicodeScalars.distance(from: string.startIndex, to: i) + rightScalars = self.unicodeScalarCount - leftScalars + } else { + rightUTF16 = string.utf16.distance(from: i, to: string.endIndex) + leftUTF16 = self.utf16Count - rightUTF16 + rightScalars = string.unicodeScalars.distance(from: i, to: string.endIndex) + leftScalars = self.unicodeScalarCount - rightScalars + } + + let left = _counts(upTo: i, utf16: leftUTF16, scalars: leftScalars) + let right = _counts(from: i, utf16: rightUTF16, scalars: rightScalars) + + assert(left.utf8 + right.utf8 == self.counts.utf8) + assert(left.utf16 + right.utf16 == self.counts.utf16) + assert(left.unicodeScalars + right.unicodeScalars == self.counts.unicodeScalars) + assert(left.characters + right.characters == self.counts.characters) + return (left, right) + } + + func counts(upTo i: String.Index) -> Counts { + precondition(i <= string.endIndex) + let i = string.unicodeScalars._index(roundingDown: i) + guard i > string.startIndex else { return Counts() } + guard i < string.endIndex else { return self.counts } + + let utf16 = string.utf16.distance(from: string.startIndex, to: i) + let scalars = string.unicodeScalars.distance(from: string.startIndex, to: i) + return _counts(from: i, utf16: utf16, scalars: scalars) + } + + func _counts(upTo i: String.Index, utf16: Int, scalars: Int) -> Counts { + assert(i > string.startIndex && i < string.endIndex) + let s = string[.. lastBreak { + result._characters = self.counts._characters + result._prefix = self.counts._prefix + result._suffix = self.counts._suffix - (self.counts.utf8 - result.utf8) + } else { // i > firstBreak, i <= lastBreak + result._prefix = self.counts._prefix + let wholeChars = string[firstBreak ..< i] + assert(!wholeChars.isEmpty) + var state = _CharacterRecognizer() + let (characters, _, last) = state.consume(wholeChars)! + result.characters = characters + result.suffix = string.utf8.distance(from: last, to: i) + } + return result + } + + func counts(from i: String.Index) -> Counts { + precondition(i <= string.endIndex) + let i = string.unicodeScalars._index(roundingDown: i) + guard i > string.startIndex else { return self.counts } + guard i < string.endIndex else { return Counts() } + + let utf16 = string.utf16.distance(from: i, to: string.endIndex) + let scalars = string.unicodeScalars.distance(from: i, to: string.endIndex) + return _counts(from: i, utf16: utf16, scalars: scalars) + } + + func _counts(from i: String.Index, utf16: Int, scalars: Int) -> Counts { + assert(i > string.startIndex && i < string.endIndex) + let s = string[i...] + + var result = Counts( + utf8: s.utf8.count, + utf16: utf16, + unicodeScalars: scalars, + characters: 0, + prefix: 0, + suffix: 0) + + let firstBreak = self.firstBreak + let lastBreak = self.lastBreak + + if i > lastBreak { + result._characters = 0 + result._prefix = result.utf8 + result._suffix = result.utf8 + } else if i <= firstBreak { + result._characters = self.counts._characters + result.prefix = self.counts.prefix - self.string._utf8Offset(of: i) + result._suffix = self.counts._suffix + } else { // i > firstBreak, i <= lastBreak + result._suffix = self.counts._suffix + let prevBreak = string[firstBreak..= i) + result.characters = characters + 1 + result.prefix = string.utf8.distance(from: i, to: first) + } else { + result.characters = 1 + result.prefix = string.utf8.distance(from: i, to: lastBreak) + } + } + return result + } +} + +#endif diff --git a/Sources/RopeModule/BigString/Chunk/BigString+Chunk.swift b/Sources/RopeModule/BigString/Chunk/BigString+Chunk.swift new file mode 100644 index 000000000..ad240eda1 --- /dev/null +++ b/Sources/RopeModule/BigString/Chunk/BigString+Chunk.swift @@ -0,0 +1,123 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=5.8) + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + internal struct _Chunk { + typealias Slice = (string: Substring, characters: Int, prefix: Int, suffix: Int) + + var string: String + var counts: Counts + + init() { + self.string = "" + self.counts = Counts() + } + + init(_ string: String, _ counts: Counts) { + self.string = string + self.string.makeContiguousUTF8() + self.counts = counts + invariantCheck() + } + + init(_ string: Substring, _ counts: Counts) { + self.string = String(string) + self.counts = counts + invariantCheck() + } + + init(_ slice: Slice) { + self.string = String(slice.string) + self.string.makeContiguousUTF8() + self.counts = Counts((self.string[...], slice.characters, slice.prefix, slice.suffix)) + invariantCheck() + } + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString._Chunk { + @inline(__always) + static var maxUTF8Count: Int { 255 } + + @inline(__always) + static var minUTF8Count: Int { maxUTF8Count / 2 - maxSlicingError } + + @inline(__always) + static var maxSlicingError: Int { 3 } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString._Chunk { + @inline(__always) + mutating func take() -> Self { + let r = self + self = Self() + return r + } + + @inline(__always) + mutating func modify( + _ body: (inout Self) -> R + ) -> R { + body(&self) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString._Chunk { + @inline(__always) + var characterCount: Int { counts.characters } + + @inline(__always) + var unicodeScalarCount: Int { Int(counts.unicodeScalars) } + + @inline(__always) + var utf16Count: Int { Int(counts.utf16) } + + @inline(__always) + var utf8Count: Int { Int(counts.utf8) } + + @inline(__always) + var prefixCount: Int { counts.prefix } + + @inline(__always) + var suffixCount: Int { counts.suffix } + + var firstScalar: UnicodeScalar { string.unicodeScalars.first! } + var lastScalar: UnicodeScalar { string.unicodeScalars.last! } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString._Chunk { + var availableSpace: Int { Swift.max(0, Self.maxUTF8Count - utf8Count) } + + mutating func clear() { + string = "" + counts = Counts() + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString._Chunk { + func hasSpaceToMerge(_ other: some StringProtocol) -> Bool { + utf8Count + other.utf8.count <= Self.maxUTF8Count + } + + func hasSpaceToMerge(_ other: Self) -> Bool { + utf8Count + other.utf8Count <= Self.maxUTF8Count + } +} + +#endif diff --git a/Sources/RopeModule/BigString/Conformances/BigString+BidirectionalCollection.swift b/Sources/RopeModule/BigString/Conformances/BigString+BidirectionalCollection.swift new file mode 100644 index 000000000..5ae9f221c --- /dev/null +++ b/Sources/RopeModule/BigString/Conformances/BigString+BidirectionalCollection.swift @@ -0,0 +1,75 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=5.8) + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString: BidirectionalCollection { + public typealias SubSequence = BigSubstring + + public var isEmpty: Bool { + _rope.summary.isZero + } + + public var startIndex: Index { + Index(_utf8Offset: 0)._knownCharacterAligned() + } + + public var endIndex: Index { + Index(_utf8Offset: _utf8Count)._knownCharacterAligned() + } + + public var count: Int { _characterCount } + + @inline(__always) + public func index(after i: Index) -> Index { + _characterIndex(after: i) + } + + @inline(__always) + public func index(before i: Index) -> Index { + _characterIndex(before: i) + } + + @inline(__always) + public func index(_ i: Index, offsetBy distance: Int) -> Index { + _characterIndex(i, offsetBy: distance) + } + + public func index(_ i: Index, offsetBy distance: Int, limitedBy limit: Index) -> Index? { + _characterIndex(i, offsetBy: distance, limitedBy: limit) + } + + public func distance(from start: Index, to end: Index) -> Int { + _characterDistance(from: start, to: end) + } + + public subscript(position: Index) -> Character { + self[_character: position] + } + + public subscript(bounds: Range) -> BigSubstring { + BigSubstring(self, in: bounds) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + public func index(roundingDown i: Index) -> Index { + _characterIndex(roundingDown: i) + } + + public func index(roundingUp i: Index) -> Index { + _characterIndex(roundingUp: i) + } +} + +#endif diff --git a/Sources/RopeModule/BigString/Conformances/BigString+Comparable.swift b/Sources/RopeModule/BigString/Conformances/BigString+Comparable.swift new file mode 100644 index 000000000..a649cddfe --- /dev/null +++ b/Sources/RopeModule/BigString/Conformances/BigString+Comparable.swift @@ -0,0 +1,88 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=5.8) + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString: Comparable { + public static func < (left: Self, right: Self) -> Bool { + // FIXME: Implement properly normalized comparisons & hashing. + // This is somewhat tricky as we shouldn't just normalize individual pieces of the string + // split up on random Character boundaries -- Unicode does not promise that + // norm(a + c) == norm(a) + norm(b) in this case. + // To do this properly, we'll probably need to expose new stdlib entry points. :-/ + if left.isIdentical(to: right) { return false } + // FIXME: Even if we keep doing characterwise comparisons, we should skip over shared subtrees. + var it1 = left.makeIterator() + var it2 = right.makeIterator() + while true { + switch (it1.next(), it2.next()) { + case (nil, nil): return false + case (nil, .some): return true + case (.some, nil): return false + case let (a?, b?): + if a == b { continue } + return a < b + } + } + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + /// Lexicographically compare the UTF-8 representations of `left` to `right`, returning a Boolean + /// value indicating whether `left` is ordered before `right`. + internal func utf8IsLess(than other: Self) -> Bool { + if self.isIdentical(to: other) { return false } + + // FIXME: Implement a structural comparison that ignores shared subtrees when possible. + // This is somewhat tricky as this is only possible to do when the shared subtrees start + // at the same logical position in both trees. Additionally, the two input trees may not + // have the same height, even if they are equal. + + var it1 = self.makeChunkIterator() + var it2 = other.makeChunkIterator() + var s1: Substring = "" + var s2: Substring = "" + while true { + if s1.isEmpty { + s1 = it1.next()?[...] ?? "" + } + if s2.isEmpty { + s2 = it2.next()?[...] ?? "" + } + if s1.isEmpty { + return !s2.isEmpty + } + if s2.isEmpty { + return false + } + let c = Swift.min(s1.utf8.count, s2.utf8.count) + assert(c > 0) + let r: Bool? = s1.withUTF8 { b1 in + s2.withUTF8 { b2 in + for i in 0 ..< c { + let u1 = b1[i] + let u2 = b2[i] + if u1 < u2 { return true } + if u1 > u2 { return false } + } + return nil + } + } + if let r = r { return r } + s1 = s1.suffix(from: s1._utf8Index(at: c)) + s2 = s2.suffix(from: s2._utf8Index(at: c)) + } + } +} + +#endif diff --git a/Sources/RopeModule/BigString/Conformances/BigString+CustomDebugStringConvertible.swift b/Sources/RopeModule/BigString/Conformances/BigString+CustomDebugStringConvertible.swift new file mode 100644 index 000000000..4925c4a48 --- /dev/null +++ b/Sources/RopeModule/BigString/Conformances/BigString+CustomDebugStringConvertible.swift @@ -0,0 +1,21 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=5.8) + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString: CustomDebugStringConvertible { + public var debugDescription: String { + description.debugDescription + } +} + +#endif diff --git a/Sources/RopeModule/BigString/Conformances/BigString+CustomStringConvertible.swift b/Sources/RopeModule/BigString/Conformances/BigString+CustomStringConvertible.swift new file mode 100644 index 000000000..bc4ef0360 --- /dev/null +++ b/Sources/RopeModule/BigString/Conformances/BigString+CustomStringConvertible.swift @@ -0,0 +1,21 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=5.8) + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString: CustomStringConvertible { + public var description: String { + String(self) + } +} + +#endif diff --git a/Sources/RopeModule/BigString/Conformances/BigString+Equatable.swift b/Sources/RopeModule/BigString/Conformances/BigString+Equatable.swift new file mode 100644 index 000000000..49edaf578 --- /dev/null +++ b/Sources/RopeModule/BigString/Conformances/BigString+Equatable.swift @@ -0,0 +1,105 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=5.8) + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + public func isIdentical(to other: Self) -> Bool { + self._rope.isIdentical(to: other._rope) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString: Equatable { + public static func ==(left: Self, right: Self) -> Bool { + // FIXME: Implement properly normalized comparisons & hashing. + // This is somewhat tricky as we shouldn't just normalize individual pieces of the string + // split up on random Character boundaries -- Unicode does not promise that + // norm(a + c) == norm(a) + norm(b) in this case. + // To do this properly, we'll probably need to expose new stdlib entry points. :-/ + if left.isIdentical(to: right) { return true } + guard left._characterCount == right._characterCount else { return false } + // FIXME: Even if we keep doing characterwise comparisons, we should skip over shared subtrees. + var it1 = left.makeIterator() + var it2 = right.makeIterator() + var a: Character? = nil + var b: Character? = nil + repeat { + a = it1.next() + b = it2.next() + guard a == b else { return false } + } while a != nil + return true + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + /// Lexicographically compare the UTF-8 representations of `left` to `right`, returning a Boolean + /// value indicating whether `left` is equal to `right`. + internal static func utf8IsEqual(_ left: Self, to right: Self) -> Bool { + if left.isIdentical(to: right) { return true } + guard left._rope.summary == right._rope.summary else { return false } + + // FIXME: Implement a structural comparison that ignores shared subtrees when possible. + // This is somewhat tricky as this is only possible to do when the shared subtrees start + // at the same logical position in both trees. Additionally, the two input trees may not + // have the same height, even if they are equal. + + var it1 = left.utf8.makeIterator() + var it2 = right.utf8.makeIterator() + var remaining = left._utf8Count + + while remaining > 0 { + let consumed = it1.next(maximumCount: remaining) { b1 in + let consumed = it2.next(maximumCount: b1.count) { b2 in + guard b2.elementsEqual(b1.prefix(b2.count)) else { return (0, 0) } + return (b2.count, b2.count) + } + return (consumed, consumed) + } + guard consumed > 0 else { return false } + remaining -= consumed + } + return true + } + + internal static func utf8IsEqual( + _ left: Self, + in leftRange: Range, + to right: Self, + in rightRange: Range + ) -> Bool { + let leftUTF8Count = left._utf8Distance(from: leftRange.lowerBound, to: leftRange.upperBound) + let rightUTF8Count = right._utf8Distance(from: rightRange.lowerBound, to: rightRange.upperBound) + guard leftUTF8Count == rightUTF8Count else { return false } + + var remaining = leftUTF8Count + var it1 = BigString.UTF8View.Iterator(_base: left, from: leftRange.lowerBound) + var it2 = BigString.UTF8View.Iterator(_base: right, from: rightRange.lowerBound) + + while remaining > 0 { + let consumed = it1.next(maximumCount: remaining) { b1 in + let consumed = it2.next(maximumCount: b1.count) { b2 in + guard b2.elementsEqual(b1.prefix(b2.count)) else { return (0, 0) } + return (b2.count, b2.count) + } + return (consumed, consumed) + } + guard consumed > 0 else { return false } + remaining -= consumed + } + return true + } +} + +#endif diff --git a/Sources/RopeModule/BigString/Conformances/BigString+ExpressibleByStringLiteral.swift b/Sources/RopeModule/BigString/Conformances/BigString+ExpressibleByStringLiteral.swift new file mode 100644 index 000000000..0c434eaf8 --- /dev/null +++ b/Sources/RopeModule/BigString/Conformances/BigString+ExpressibleByStringLiteral.swift @@ -0,0 +1,21 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=5.8) + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString: ExpressibleByStringLiteral { + public init(stringLiteral value: String) { + self.init(value) + } +} + +#endif diff --git a/Sources/RopeModule/BigString/Conformances/BigString+Hashing.swift b/Sources/RopeModule/BigString/Conformances/BigString+Hashing.swift new file mode 100644 index 000000000..e94037170 --- /dev/null +++ b/Sources/RopeModule/BigString/Conformances/BigString+Hashing.swift @@ -0,0 +1,62 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=5.8) + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString: Hashable { + public func hash(into hasher: inout Hasher) { + hashCharacters(into: &hasher) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + internal func hashCharacters(into hasher: inout Hasher) { + // FIXME: Implement properly normalized comparisons & hashing. + // This is somewhat tricky as we shouldn't just normalize individual pieces of the string + // split up on random Character boundaries -- Unicode does not promise that + // norm(a + c) == norm(a) + norm(b) in this case. + // To do this properly, we'll probably need to expose new stdlib entry points. :-/ + var it = self.makeIterator() + while let character = it.next() { + let s = String(character) + s._withNFCCodeUnits { hasher.combine($0) } + } + hasher.combine(0xFF as UInt8) + } + + /// Feed the UTF-8 encoding of `self` into hasher, with a terminating byte. + internal func hashUTF8(into hasher: inout Hasher) { + for chunk in self._rope { + var string = chunk.string + string.withUTF8 { + hasher.combine(bytes: .init($0)) + } + } + hasher.combine(0xFF as UInt8) + } + + /// Feed the UTF-8 encoding of `self[start..=5.8) + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString: LosslessStringConvertible { + // init?(_: String) is implemented by RangeReplaceableCollection.init(_:) +} + +#endif diff --git a/Sources/RopeModule/BigString/Conformances/BigString+RangeReplaceableCollection.swift b/Sources/RopeModule/BigString/Conformances/BigString+RangeReplaceableCollection.swift new file mode 100644 index 000000000..bb5f7844e --- /dev/null +++ b/Sources/RopeModule/BigString/Conformances/BigString+RangeReplaceableCollection.swift @@ -0,0 +1,222 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=5.8) + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString: RangeReplaceableCollection { + public init() { + self.init(_rope: _Rope()) + } + + public mutating func reserveCapacity(_ n: Int) { + // Do nothing. + } + + public mutating func replaceSubrange( + _ subrange: Range, + with newElements: __owned some Sequence // Note: Sequence, not Collection + ) { + if let newElements = _specialize(newElements, for: String.self) { + self._replaceSubrange(subrange, with: newElements) + } else if let newElements = _specialize(newElements, for: Substring.self) { + self._replaceSubrange(subrange, with: newElements) + } else if let newElements = _specialize(newElements, for: BigString.self) { + self._replaceSubrange(subrange, with: newElements) + } else if let newElements = _specialize(newElements, for: BigSubstring.self) { + self._replaceSubrange(subrange, with: newElements) + } else { + self._replaceSubrange(subrange, with: BigString(newElements)) + } + } + + public mutating func replaceSubrange( + _ subrange: Range, with newElements: __owned String + ) { + _replaceSubrange(subrange, with: newElements) + } + + public mutating func replaceSubrange( + _ subrange: Range, with newElements: __owned Substring + ) { + _replaceSubrange(subrange, with: newElements) + } + + public mutating func replaceSubrange( + _ subrange: Range, with newElements: __owned BigString + ) { + _replaceSubrange(subrange, with: newElements) + } + + public mutating func replaceSubrange( + _ subrange: Range, with newElements: __owned BigSubstring + ) { + _replaceSubrange(subrange, with: newElements) + } + + public init(_ elements: some Sequence) { + if let elements = _specialize(elements, for: String.self) { + self.init(_from: elements) + } else if let elements = _specialize(elements, for: Substring.self) { + self.init(_from: elements) + } else if let elements = _specialize(elements, for: BigString.self) { + self = elements + } else if let elements = _specialize(elements, for: BigSubstring.self) { + self.init(_from: elements) + } else { + self.init(_from: elements) + } + } + + public init(_ elements: String) { + self.init(_from: elements) + } + + public init(_ elements: Substring) { + self.init(_from: elements) + } + + public init(_ elements: BigString) { + self = elements + } + + public init(_ elements: BigSubstring) { + self.init(_from: elements) + } + + public init(repeating repeatedValue: Character, count: Int) { + self.init(repeating: BigString(String(repeatedValue)), count: count) + } + + public init(repeating repeatedValue: some StringProtocol, count: Int) { + self.init(repeating: BigString(repeatedValue), count: count) + } + + public init(repeating value: Self, count: Int) { + precondition(count >= 0, "Negative count") + guard count > 0 else { + self.init() + return + } + self.init() + var c = 0 + + var piece = value + var current = 1 + + while c < count { + if count & current != 0 { + self.append(contentsOf: piece) + c |= current + } + piece.append(contentsOf: piece) + current *= 2 + } + } + + public init(repeating value: BigSubstring, count: Int) { + self.init(repeating: BigString(value), count: count) + } + + public mutating func append(_ newElement: __owned Character) { + append(contentsOf: String(newElement)) + } + + public mutating func append( + contentsOf newElements: __owned some Sequence + ) { + if let newElements = _specialize(newElements, for: String.self) { + append(contentsOf: newElements) + } else if let newElements = _specialize(newElements, for: Substring.self) { + append(contentsOf: newElements) + } else if let newElements = _specialize(newElements, for: BigString.self) { + append(contentsOf: newElements) + } else if let newElements = _specialize( + newElements, for: BigSubstring.self + ) { + append(contentsOf: newElements) + } else { + append(contentsOf: BigString(newElements)) + } + } + + public mutating func append(contentsOf newElements: __owned String) { + _append(contentsOf: newElements[...]) + } + + public mutating func append(contentsOf newElements: __owned Substring) { + _append(contentsOf: newElements) + } + + public mutating func append(contentsOf newElements: __owned BigString) { + _append(contentsOf: newElements) + } + + public mutating func append(contentsOf newElements: __owned BigSubstring) { + _append(contentsOf: newElements._base, in: newElements._bounds) + } + + public mutating func insert(_ newElement: Character, at i: Index) { + insert(contentsOf: String(newElement), at: i) + } + + public mutating func insert( + contentsOf newElements: __owned some Sequence, // Note: Sequence, not Collection + at i: Index + ) { + if let newElements = _specialize(newElements, for: String.self) { + insert(contentsOf: newElements, at: i) + } else if let newElements = _specialize(newElements, for: Substring.self) { + insert(contentsOf: newElements, at: i) + } else if let newElements = _specialize(newElements, for: BigString.self) { + insert(contentsOf: newElements, at: i) + } else if let newElements = _specialize(newElements, for: BigSubstring.self) { + insert(contentsOf: newElements, at: i) + } else { + insert(contentsOf: BigString(newElements), at: i) + } + } + + public mutating func insert(contentsOf newElements: __owned String, at i: Index) { + _insert(contentsOf: newElements[...], at: i) + } + + public mutating func insert(contentsOf newElements: __owned Substring, at i: Index) { + _insert(contentsOf: newElements, at: i) + } + + public mutating func insert(contentsOf newElements: __owned BigString, at i: Index) { + _insert(contentsOf: newElements, at: i) + } + + public mutating func insert(contentsOf newElements: __owned BigSubstring, at i: Index) { + _insert(contentsOf: newElements._base, in: newElements._bounds, at: i) + } + + @discardableResult + public mutating func remove(at i: Index) -> Character { + removeCharacter(at: i) + } + + public mutating func removeSubrange(_ bounds: Range) { + _removeSubrange(bounds) + } + + public mutating func removeAll(keepingCapacity keepCapacity: Bool = false) { + self = BigString() + } +} + +#endif diff --git a/Sources/RopeModule/BigString/Conformances/BigString+Sequence.swift b/Sources/RopeModule/BigString/Conformances/BigString+Sequence.swift new file mode 100644 index 000000000..722ceeade --- /dev/null +++ b/Sources/RopeModule/BigString/Conformances/BigString+Sequence.swift @@ -0,0 +1,229 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=5.8) + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString: Sequence { + public typealias Element = Character + + public func makeIterator() -> Iterator { + Iterator(self) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + public struct Iterator { + internal let _base: BigString + internal var _utf8BaseOffset: Int + internal var _ropeIndex: _Rope.Index + internal var _chunkIndex: String.Index + internal var _next: String.Index + + internal init(_ string: BigString) { + self._base = string + self._ropeIndex = string._rope.startIndex + string._rope.grease(&_ropeIndex) + + self._utf8BaseOffset = 0 + guard _ropeIndex < string._rope.endIndex else { + _chunkIndex = "".startIndex + _next = "".endIndex + return + } + let chunk = _base._rope[_ropeIndex] + assert(chunk.firstBreak == chunk.string.startIndex) + self._chunkIndex = chunk.firstBreak + self._next = chunk.string[_chunkIndex...].index(after: _chunkIndex) + } + + internal init( + _ string: BigString, + from start: Index + ) { + self._base = string + self._utf8BaseOffset = start.utf8Offset + + if start == string.endIndex { + self._ropeIndex = string._rope.endIndex + self._chunkIndex = "".startIndex + self._next = "".endIndex + return + } + let i = string.resolve(start, preferEnd: false) + self._ropeIndex = i._rope! + self._utf8BaseOffset = i._utf8BaseOffset + self._chunkIndex = i._chunkIndex + self._next = _base._rope[_ropeIndex].wholeCharacters.index(after: _chunkIndex) + } + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString.Iterator: IteratorProtocol { + public typealias Element = Character + + internal var isAtEnd: Bool { + _chunkIndex == _next + } + + internal var isAtStart: Bool { + _ropeIndex == _base._rope.startIndex && _chunkIndex._utf8Offset == 0 + } + + internal var current: Element { + assert(!isAtEnd) + let chunk = _base._rope[_ropeIndex] + var str = String(chunk.string[_chunkIndex ..< _next]) + if _next < chunk.string.endIndex { return Character(str) } + + var i = _base._rope.index(after: _ropeIndex) + while i < _base._rope.endIndex { + let chunk = _base._rope[i] + let b = chunk.firstBreak + str += chunk.string[.. Bool { + guard !isAtEnd else { return false } + let chunk = _base._rope[_ropeIndex] + if _next < chunk.string.endIndex { + _chunkIndex = _next + _next = chunk.wholeCharacters.index(after: _next) + return true + } + var baseOffset = _utf8BaseOffset + chunk.utf8Count + var i = _base._rope.index(after: _ropeIndex) + while i < _base._rope.endIndex { + let chunk = _base._rope[i] + let b = chunk.firstBreak + if b < chunk.string.endIndex { + _ropeIndex = i + _utf8BaseOffset = baseOffset + _chunkIndex = b + _next = chunk.string[b...].index(after: b) + return true + } + baseOffset += chunk.utf8Count + _base._rope.formIndex(after: &i) + } + return false + } + + mutating func stepBackward() -> Bool { + if !isAtEnd { + let chunk = _base._rope[_ropeIndex] + let i = chunk.firstBreak + if _chunkIndex > i { + _next = _chunkIndex + _chunkIndex = chunk.string[i...].index(before: _chunkIndex) + return true + } + } + var i = _ropeIndex + var baseOffset = _utf8BaseOffset + while i > _base._rope.startIndex { + _base._rope.formIndex(before: &i) + let chunk = _base._rope[i] + baseOffset -= chunk.utf8Count + if chunk.hasBreaks { + _ropeIndex = i + _utf8BaseOffset = baseOffset + _next = chunk.string.endIndex + _chunkIndex = chunk.lastBreak + return true + } + } + return false + } + + public mutating func next() -> Character? { + guard !isAtEnd else { return nil } + let item = self.current + if !stepForward() { + _chunkIndex = _next + } + return item + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString.Iterator { + // The UTF-8 offset of the current position, from the start of the string. + var utf8Offset: Int { + _utf8BaseOffset + _chunkIndex._utf8Offset + } + + var index: BigString.Index { + BigString.Index(baseUTF8Offset: _utf8BaseOffset, _rope: _ropeIndex, chunk: _chunkIndex) + } + + internal var nextIndex: BigString.Index { + assert(!isAtEnd) + let chunk = _base._rope[_ropeIndex] + if _next < chunk.string.endIndex { + return BigString.Index(baseUTF8Offset: _utf8BaseOffset, _rope: _ropeIndex, chunk: _next) + } + var i = _base._rope.index(after: _ropeIndex) + var base = _utf8BaseOffset + while i < _base._rope.endIndex { + let chunk = _base._rope[i] + let b = chunk.firstBreak + if b < chunk.string.endIndex { + return BigString.Index(baseUTF8Offset: base, _rope: i, chunk: b) + } + base += chunk.utf8Count + _base._rope.formIndex(after: &i) + } + return _base.endIndex + } + + /// The UTF-8 offset range of the current character, measured from the start of the string. + var utf8Range: Range { + assert(!isAtEnd) + let start = utf8Offset + var end = _utf8BaseOffset + var chunk = _base._rope[_ropeIndex] + if _next < chunk.string.endIndex { + end += _next._utf8Offset + return Range(uncheckedBounds: (start, end)) + } + end += chunk.utf8Count + var i = _base._rope.index(after: _ropeIndex) + while i < _base._rope.endIndex { + chunk = _base._rope[i] + let b = chunk.firstBreak + if b < chunk.string.endIndex { + end += b._utf8Offset + break + } + end += chunk.utf8Count + _base._rope.formIndex(after: &i) + } + return Range(uncheckedBounds: (start, end)) + } + + func isAbove(_ index: BigString.Index) -> Bool { + self.utf8Offset > index.utf8Offset + } + + func isBelow(_ index: BigString.Index) -> Bool { + self.utf8Offset < index.utf8Offset + } +} + + +#endif diff --git a/Sources/RopeModule/BigString/Conformances/BigString+TextOutputStream.swift b/Sources/RopeModule/BigString/Conformances/BigString+TextOutputStream.swift new file mode 100644 index 000000000..54bd673b2 --- /dev/null +++ b/Sources/RopeModule/BigString/Conformances/BigString+TextOutputStream.swift @@ -0,0 +1,30 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=5.8) + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString: TextOutputStream { + public mutating func write(_ string: String) { + append(contentsOf: string) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString: TextOutputStreamable { + public func write(to target: inout some TextOutputStream) { + for chunk in _rope { + chunk.string.write(to: &target) + } + } +} + +#endif diff --git a/Sources/RopeModule/BigString/Operations/BigString+Append.swift b/Sources/RopeModule/BigString/Operations/BigString+Append.swift new file mode 100644 index 000000000..6176dfc03 --- /dev/null +++ b/Sources/RopeModule/BigString/Operations/BigString+Append.swift @@ -0,0 +1,198 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=5.8) + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + mutating func _append(contentsOf other: __owned Substring) { + if other.isEmpty { return } + if isEmpty { + self = Self(other) + return + } + var ingester = _ingester(forInserting: other, at: endIndex, allowForwardPeek: true) + + let last = _rope.index(before: _rope.endIndex) + if let final = _rope[last].append(from: &ingester) { + precondition(!final.isUndersized) + _rope.append(final) + return + } + + // Make a temp rope out of the rest of the chunks and then join the two trees together. + if !ingester.isAtEnd { + var builder = _Rope.Builder() + while let chunk = ingester.nextWellSizedChunk() { + precondition(!chunk.isUndersized) + builder.insertBeforeTip(chunk) + } + precondition(ingester.isAtEnd) + _rope = _Rope.join(_rope, builder.finalize()) + } + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + var _firstUnicodeScalar: Unicode.Scalar { + assert(!isEmpty) + return _rope.root.firstItem.value.string.unicodeScalars.first! + } + + mutating func _append(contentsOf other: __owned BigString) { + guard !other.isEmpty else { return } + guard !self.isEmpty else { + self = other + return + } + + let hint = other._firstUnicodeScalar + var other = other._rope + var old = _CharacterRecognizer() + var new = self._breakState(upTo: endIndex, nextScalarHint: hint) + _ = other.resyncBreaks(old: &old, new: &new) + _append(other) + } + + mutating func _append(contentsOf other: __owned BigString, in range: Range) { + guard !range._isEmptyUTF8 else { return } + guard !self.isEmpty else { + self = Self(_from: other, in: range) + return + } + + var other = BigString(_from: other, in: range) + let hint = other._firstUnicodeScalar + var old = _CharacterRecognizer() + var new = self._breakState(upTo: endIndex, nextScalarHint: hint) + _ = other._rope.resyncBreaks(old: &old, new: &new) + _append(other._rope) + } + + mutating func prepend(contentsOf other: __owned BigString) { + guard !other.isEmpty else { return } + guard !self.isEmpty else { + self = other + return + } + let hint = self._firstUnicodeScalar + var old = _CharacterRecognizer() + var new = other._breakState(upTo: other.endIndex, nextScalarHint: hint) + _ = self._rope.resyncBreaks(old: &old, new: &new) + _prepend(other._rope) + } + + mutating func prepend(contentsOf other: __owned BigString, in range: Range) { + guard !range._isEmptyUTF8 else { return } + let extract = Self(_from: other, in: range) + guard !self.isEmpty else { + self = extract + return + } + let hint = self._firstUnicodeScalar + var old = _CharacterRecognizer() + var new = extract._breakState(upTo: extract.endIndex, nextScalarHint: hint) + _ = self._rope.resyncBreaks(old: &old, new: &new) + _prepend(extract._rope) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + var isUndersized: Bool { + _utf8Count < _Chunk.minUTF8Count + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + /// Note: This assumes `other` already has the correct break positions. + mutating func _append(_ other: __owned _Chunk) { + assert(!other.isEmpty) + guard !self.isEmpty else { + self._rope.append(other) + return + } + guard self.isUndersized || other.isUndersized else { + self._rope.append(other) + return + } + var other = other + let last = self._rope.index(before: self._rope.endIndex) + if !self._rope[last].rebalance(nextNeighbor: &other) { + assert(!other.isUndersized) + self._rope.append(other) + } + } + + /// Note: This assumes `self` and `other` already have the correct break positions. + mutating func _prepend(_ other: __owned _Chunk) { + assert(!other.isEmpty) + guard !self.isEmpty else { + self._rope.prepend(other) + return + } + guard self.isUndersized || other.isUndersized else { + self._rope.prepend(other) + return + } + var other = other + let first = self._rope.startIndex + if !self._rope[first].rebalance(prevNeighbor: &other) { + self._rope.prepend(other) + } + } + + /// Note: This assumes `other` already has the correct break positions. + mutating func _append(_ other: __owned _Rope) { + guard !other.isEmpty else { return } + guard !self._rope.isEmpty else { + self._rope = other + return + } + if other.isSingleton { + self._append(other.first!) + return + } + if self.isUndersized { + assert(self._rope.isSingleton) + let chunk = self._rope.first! + self._rope = other + self._prepend(chunk) + return + } + self._rope = _Rope.join(self._rope, other) + } + + /// Note: This assumes `self` and `other` already have the correct break positions. + mutating func _prepend(_ other: __owned _Rope) { + guard !other.isEmpty else { return } + guard !self.isEmpty else { + self._rope = other + return + } + if other.isSingleton { + self._prepend(other.first!) + return + } + if self.isUndersized { + assert(self._rope.isSingleton) + let chunk = self._rope.first! + self._rope = other + self._append(chunk) + return + } + self._rope = _Rope.join(other, self._rope) + } +} + +#endif diff --git a/Sources/RopeModule/BigString/Operations/BigString+Initializers.swift b/Sources/RopeModule/BigString/Operations/BigString+Initializers.swift new file mode 100644 index 000000000..1cb9be46a --- /dev/null +++ b/Sources/RopeModule/BigString/Operations/BigString+Initializers.swift @@ -0,0 +1,150 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=5.8) + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + internal init(_from input: some StringProtocol) { + var builder = _Rope.Builder() + var ingester = _Ingester(input) + while let chunk = ingester.nextWellSizedChunk() { + builder.insertBeforeTip(chunk) + } + self._rope = builder.finalize() + } + + internal init(_from input: some Sequence) { + var builder = Builder() + var piece = "" + for character in input { + piece.append(character) + if piece.utf8.count >= _Chunk.minUTF8Count { + builder.append(piece) + piece = "" + } + } + builder.append(piece) + self = builder.finalize() + } + + internal init(_from input: some Sequence) { + var builder = Builder() + var piece = "" + for scalar in input { + piece.unicodeScalars.append(scalar) + if piece.utf8.count >= _Chunk.minUTF8Count { + builder.append(piece) + piece = "" + } + } + builder.append(piece) + self = builder.finalize() + } + + internal init(_from other: BigSubstring) { + self.init(_from: other._base, in: other._bounds) + } + + internal init(_from other: BigSubstring.UnicodeScalarView) { + self.init(_from: other._base, in: other._bounds) + } + + internal init( + _from other: Self, + in range: Range + ) { + self._rope = other._rope.extract( + from: range.lowerBound.utf8Offset, + to: range.upperBound.utf8Offset, + in: _UTF8Metric()) + var old = other._breakState(upTo: range.lowerBound) + var new = _CharacterRecognizer() + _ = self._rope.resyncBreaks(old: &old, new: &new) + } + + internal init( + _ other: Self, + in range: Range, + state: inout _CharacterRecognizer + ) { + self._rope = other._rope.extract( + from: range.lowerBound.utf8Offset, + to: range.upperBound.utf8Offset, + in: _UTF8Metric()) + var old = other._breakState(upTo: range.lowerBound) + var new = state + self._rope.resyncBreaksToEnd(old: &old, new: &new) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension String { + public init(_ big: BigString) { + guard !big.isEmpty else { + self.init() + return + } + if big._rope.isSingleton { + self = big._rope[big._rope.startIndex].string + return + } + self.init() + self.reserveCapacity(big._utf8Count) + for chunk in big._rope { + self.append(chunk.string) + } + } + + internal init(_from big: BigString, in range: Range) { + self.init() + + var start = big._unicodeScalarIndex(roundingDown: range.lowerBound) + start = big.resolve(start, preferEnd: false) + + var end = big._unicodeScalarIndex(roundingDown: range.upperBound) + end = big.resolve(end, preferEnd: true) + + let utf8Capacity = end.utf8Offset - start.utf8Offset + guard utf8Capacity > 0 else { return } + + self.reserveCapacity(utf8Capacity) + + let startRopeIndex = start._rope! + let endRopeIndex = end._rope! + if startRopeIndex == endRopeIndex { + self += big._rope[startRopeIndex].string[start._chunkIndex ..< end._chunkIndex] + return + } + + self += big._rope[startRopeIndex].string[start._chunkIndex...] + var i = big._rope.index(after: startRopeIndex) + while i < endRopeIndex { + self += big._rope[i].string + big._rope.formIndex(after: &i) + } + self += big._rope[endRopeIndex].string[..=5.8) + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + mutating func _insert( + contentsOf other: __owned Substring, + at index: Index + ) { + precondition(index <= endIndex, "Index out of bounds") + if other.isEmpty { return } + if index == endIndex { + self.append(contentsOf: other) + return + } + + // Note: we must disable the forward peeking optimization as we'll need to know the actual + // full grapheme breaking state of the ingester at the start of the insertion. + // (We'll use it to resync grapheme breaks in the parts that follow the insertion.) + let index = self.resolve(index, preferEnd: true) + var ingester = _ingester(forInserting: other, at: index, allowForwardPeek: false) + var ri = index._rope! + var ci = index._chunkIndex + let r = _rope.update(at: &ri) { $0.insert(from: &ingester, at: ci) } + switch r { + case let .inline(r): + assert(ingester.isAtEnd) + guard var r = r else { break } + ci = String.Index(_utf8Offset: ci._utf8Offset + r.increment) + let i = Index(baseUTF8Offset: index._utf8BaseOffset, _rope: ri, chunk: ci) + resyncBreaks(startingAt: i, old: &r.old, new: &r.new) + case let .split(spawn: spawn, endStates: r): + assert(ingester.isAtEnd) + _rope.formIndex(after: &ri) + _rope.insert(spawn, at: ri) + guard var r = r else { break } + let i = Index(_utf8Offset: index.utf8Offset + r.increment, _rope: ri, chunkOffset: spawn.utf8Count) + resyncBreaks(startingAt: i, old: &r.old, new: &r.new) + case .large: + var builder = self.split(at: index, state: ingester.state) + builder.append(from: &ingester) + self = builder.finalize() + } + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + mutating func _insert(contentsOf other: __owned Self, at index: Index) { + guard index < endIndex else { + precondition(index == endIndex, "Index out of bounds") + self.append(contentsOf: other) + return + } + guard index > startIndex else { + self.prepend(contentsOf: other) + return + } + if other._rope.isSingleton { + // Fast path when `other` is tiny. + let chunk = other._rope.first! + insert(contentsOf: chunk.string, at: index) + return + } + var builder = self.split(at: index) + builder.append(other) + self = builder.finalize() + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + mutating func _insert(contentsOf other: __owned Self, in range: Range, at index: Index) { + guard index < endIndex else { + precondition(index == endIndex, "Index out of bounds") + self._append(contentsOf: other, in: range) + return + } + guard index > startIndex else { + self.prepend(contentsOf: other, in: range) + return + } + guard !range._isEmptyUTF8 else { return } + // FIXME: Fast path when `other` is tiny. + var builder = self.split(at: index) + builder.append(other, in: range) + self = builder.finalize() + } +} + +#endif diff --git a/Sources/RopeModule/BigString/Operations/BigString+Managing Breaks.swift b/Sources/RopeModule/BigString/Operations/BigString+Managing Breaks.swift new file mode 100644 index 000000000..1b2256267 --- /dev/null +++ b/Sources/RopeModule/BigString/Operations/BigString+Managing Breaks.swift @@ -0,0 +1,271 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=5.8) + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + func _breakState( + upTo index: Index, + nextScalarHint: Unicode.Scalar? = nil + ) -> _CharacterRecognizer { + assert(index == _unicodeScalarIndex(roundingDown: index)) + guard index > startIndex else { + return _CharacterRecognizer() + } + let index = resolve(index, preferEnd: true) + let ropeIndex = index._rope! + let chunkIndex = index._chunkIndex + let chunk = _rope[ropeIndex] + + guard ropeIndex > _rope.startIndex || chunkIndex > chunk.string.startIndex else { + return _CharacterRecognizer() + } + + if let next = nextScalarHint, chunkIndex > chunk.string.startIndex { + let i = chunk.string.unicodeScalars.index(before: chunkIndex) + let prev = chunk.string.unicodeScalars[i] + if _CharacterRecognizer.quickBreak(between: prev, and: next) == true { + return _CharacterRecognizer() + } + } + + if let r = chunk.immediateBreakState(upTo: chunkIndex) { + return r.state + } + // Find chunk that includes the start of the character. + var ri = ropeIndex + while ri > _rope.startIndex { + _rope.formIndex(before: &ri) + if _rope[ri].hasBreaks { break } + } + precondition(ri < ropeIndex) + + // Collect grapheme breaking state. + var state = _CharacterRecognizer(partialCharacter: _rope[ri].suffix) + _rope.formIndex(after: &ri) + while ri < ropeIndex { + state.consumePartialCharacter(_rope[ri].string[...]) + _rope.formIndex(after: &ri) + } + state.consumePartialCharacter(chunk.string[.. (ropeIndex: _Rope.Index, chunkIndex: String.Index)? { + guard index < endIndex else { return nil } + let i = resolve(index, preferEnd: false) + var ropeIndex = i._rope! + var chunkIndex = i._chunkIndex + let end = _rope.mutatingForEach(from: &ropeIndex) { chunk in + let start = chunkIndex + chunkIndex = String.Index(_utf8Offset: 0) + if let i = chunk.resyncBreaks(startingAt: start, old: &old, new: &new) { + return i + } + return nil + } + guard let end else { return nil } + return (ropeIndex, end) + } + + mutating func resyncBreaksToEnd( + startingAt index: Index, + old: inout _CharacterRecognizer, + new: inout _CharacterRecognizer + ) { + guard let _ = resyncBreaks(startingAt: index, old: &old, new: &new) else { return } + let state = _breakState(upTo: endIndex) + old = state + new = state + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString._Rope { + mutating func resyncBreaks( + old: inout _CharacterRecognizer, + new: inout _CharacterRecognizer + ) -> Bool { + _resyncBreaks(old: &old, new: &new) != nil + } + + mutating func _resyncBreaks( + old: inout _CharacterRecognizer, + new: inout _CharacterRecognizer + ) -> (ropeIndex: Index, chunkIndex: String.Index)? { + var ropeIndex = startIndex + let chunkIndex = self.mutatingForEach(from: &ropeIndex) { + $0.resyncBreaksFromStart(old: &old, new: &new) + } + guard let chunkIndex else { return nil } + return (ropeIndex, chunkIndex) + } + + mutating func resyncBreaksToEnd( + old: inout _CharacterRecognizer, + new: inout _CharacterRecognizer + ) { + guard let (ropeIndex, chunkIndex) = _resyncBreaks(old: &old, new: &new) else { return } + + let chars = self.summary.characters + if chars > 0 { + var i = endIndex + while i > ropeIndex { + formIndex(before: &i) + if self[i].hasBreaks { break } + } + if i > ropeIndex || self[i].lastBreak > chunkIndex { + new = self[i].immediateLastBreakState! + formIndex(after: &i) + while i < endIndex { + new.consumePartialCharacter(self[i].string[...]) + formIndex(after: &i) + } + old = new + return + } + } + + var ri = ropeIndex + let suffix = self[ri].string.unicodeScalars[chunkIndex...].dropFirst() + new.consumePartialCharacter(suffix) + formIndex(after: &ri) + while ri < endIndex { + new.consumePartialCharacter(self[ri].string[...]) + formIndex(after: &ri) + } + old = new + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString._Chunk { + /// Resyncronize chunk metadata with the (possibly) reshuffled grapheme + /// breaks after an insertion that ended at `index`. + /// + /// This assumes that the chunk's prefix and suffix counts have already + /// been adjusted to remain on Unicode scalar boundaries, and that they + /// are also in sync with the grapheme breaks up to `index`. If the + /// prefix ends after `index`, then this function may update it to address + /// the correct scalar. Similarly, the suffix count may be updated to + /// reflect the new position of the last grapheme break, if necessary. + mutating func resyncBreaks( + startingAt index: String.Index, + old: inout _CharacterRecognizer, + new: inout _CharacterRecognizer + ) -> String.Index? { + var i = index + let u = string.unicodeScalars + assert(u._index(roundingDown: i) == i) + + // FIXME: Rewrite in terms of `firstBreak(in:)`. + var first: String.Index? = nil + var last: String.Index? = nil + loop: + while i < u.endIndex { + let scalar = u[i] + let a = old.hasBreak(before: scalar) + let b = new.hasBreak(before: scalar) + if b { + first = first ?? i + last = i + } + switch (a, b) { + case (true, true): + break loop // Resync complete ✨ + case (false, false): + if old._isKnownEqual(to: new) { break loop } + case (false, true): + counts._characters += 1 + case (true, false): + counts._characters -= 1 + } + u.formIndex(after: &i) + } + + // Grapheme breaks in `index...i` may have changed. Update `firstBreak` and `lastBreak` + // accordingly. + + assert((first == nil) == (last == nil)) + if index <= firstBreak { + if let first { + // We have seen the new first break. + firstBreak = first + } else if i >= firstBreak { + // The old firstBreak is no longer a break. + if i < lastBreak { + // The old lastBreak is still valid. Find the first break in i+1...endIndex. + let j = u.index(after: i) + var tmp = new + firstBreak = tmp.firstBreak(in: string[j...])!.lowerBound + } else { + // No breaks anywhere in the string. + firstBreak = u.endIndex + } + } + } + + if i >= lastBreak { + if let last { + // We have seen the new last break. + lastBreak = last + } else if firstBreak == u.endIndex { + // We already know there are no breaks anywhere in the string. + lastBreak = u.startIndex + } else { + // We have a `firstBreak`, but no break in `index...i`. + // Find last break in `firstBreak... String.Index? { + resyncBreaks(startingAt: string.startIndex, old: &old, new: &new) + } + + mutating func resyncBreaksFromStartToEnd( + old: inout _CharacterRecognizer, + new: inout _CharacterRecognizer + ) { + guard let i = resyncBreaks(startingAt: string.startIndex, old: &old, new: &new) else { + return + } + if i < lastBreak { + new = _CharacterRecognizer(partialCharacter: string[lastBreak...]) + } else { + //assert(old == new) + let j = string.unicodeScalars.index(after: i) + new.consumePartialCharacter(string[j...]) + } + old = new + return + } +} + +#endif diff --git a/Sources/RopeModule/BigString/Operations/BigString+RemoveSubrange.swift b/Sources/RopeModule/BigString/Operations/BigString+RemoveSubrange.swift new file mode 100644 index 000000000..bf78f31a7 --- /dev/null +++ b/Sources/RopeModule/BigString/Operations/BigString+RemoveSubrange.swift @@ -0,0 +1,48 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=5.8) + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + mutating func _removeSubrange(_ bounds: Range) { + precondition(bounds.upperBound <= endIndex, "Index out of bounds") + if bounds.isEmpty { return } + let lower = bounds.lowerBound.utf8Offset + let upper = bounds.upperBound.utf8Offset + _rope.removeSubrange(lower ..< upper, in: _UTF8Metric()) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + mutating func removeCharacter(at i: Index) -> Character { + let start = self.resolve(i, preferEnd: false) + let (character, end) = self._character(at: start) + self.removeSubrange(start ..< end) + return character + } + + mutating func removeUnicodeScalar(at i: Index) -> Unicode.Scalar { + precondition(i < endIndex, "Index out of bounds") + let start = _unicodeScalarIndex(roundingDown: i) + let ropeIndex = start._rope! + let chunkIndex = start._chunkIndex + let chunk = _rope[ropeIndex] + let scalar = chunk.string.unicodeScalars[chunkIndex] + let next = chunk.string.unicodeScalars.index(after: chunkIndex) + let end = Index(baseUTF8Offset: start._utf8BaseOffset, _rope: ropeIndex, chunk: next) + self.removeSubrange(start ..< end) + return scalar + } +} + +#endif diff --git a/Sources/RopeModule/BigString/Operations/BigString+ReplaceSubrange.swift b/Sources/RopeModule/BigString/Operations/BigString+ReplaceSubrange.swift new file mode 100644 index 000000000..40d16ae10 --- /dev/null +++ b/Sources/RopeModule/BigString/Operations/BigString+ReplaceSubrange.swift @@ -0,0 +1,88 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=5.8) + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + mutating func _replaceSubrange( + _ range: Range, + with newElements: some StringProtocol + ) { + precondition(range.upperBound <= endIndex, "Index out of bounds") + if range.isEmpty { + insert(contentsOf: newElements, at: range.lowerBound) + return + } + var builder = _split(removing: range) + var ingester = _Ingester(Substring(newElements), startState: builder.prefixEndState) + builder.append(from: &ingester) + self = builder.finalize() + } + + mutating func _replaceSubrange( + _ range: Range, + with newElements: BigString + ) { + precondition(range.upperBound <= endIndex, "Index out of bounds") + if range.isEmpty { + insert(contentsOf: newElements, at: range.lowerBound) + return + } + var builder = _split(removing: range) + builder.append(newElements) + self = builder.finalize() + } + + mutating func _replaceSubrange( + _ targetRange: Range, + with newElements: BigSubstring + ) { + _replaceSubrange(targetRange, with: newElements._base, in: newElements._bounds) + } + + mutating func _replaceSubrange( + _ targetRange: Range, + with newElements: BigString, + in sourceRange: Range + ) { + if sourceRange._isEmptyUTF8 { + removeSubrange(targetRange) + return + } + if targetRange._isEmptyUTF8 { + _insert( + contentsOf: newElements, + in: sourceRange, + at: targetRange.lowerBound) + return + } + precondition(targetRange.upperBound <= endIndex, "Index out of bounds") + var builder = _split(removing: targetRange) + builder.append(newElements, in: sourceRange) + self = builder.finalize() + } + + mutating func _split(removing range: Range) -> Builder { + let lower = range.lowerBound.utf8Offset + let upper = range.upperBound.utf8Offset + + // FIXME: Don't split the indices twice -- they are currently split once here and once in the + // builder initializer below. + let startState = _breakState(upTo: range.lowerBound) + let endState = _breakState(upTo: range.upperBound) + + let b = _rope.builder(removing: lower ..< upper, in: _UTF8Metric()) + return Builder(base: b, prefixEndState: startState, suffixStartState: endState) + } +} + +#endif diff --git a/Sources/RopeModule/BigString/Operations/BigString+Split.swift b/Sources/RopeModule/BigString/Operations/BigString+Split.swift new file mode 100644 index 000000000..9869e6b47 --- /dev/null +++ b/Sources/RopeModule/BigString/Operations/BigString+Split.swift @@ -0,0 +1,42 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=5.8) + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + mutating func split( + at index: Index + ) -> Builder { + let b = _ropeBuilder(at: index) + let state = b._breakState() + let builder = Builder(base: b, prefixEndState: state, suffixStartState: state) + return builder + } + + mutating func split( + at index: Index, + state: _CharacterRecognizer + ) -> Builder { + let b = _ropeBuilder(at: index) + let builder = Builder(base: b, prefixEndState: state, suffixStartState: state) + return builder + } + + mutating func _ropeBuilder(at index: Index) -> _Rope.Builder { + if let ri = index._rope, _rope.isValid(ri) { + return _rope.split(at: ri, index._chunkIndex) + } + return _rope.builder(splittingAt: index.utf8Offset, in: _UTF8Metric()) + } +} + +#endif diff --git a/Sources/RopeModule/BigString/Operations/Range+BigString.swift b/Sources/RopeModule/BigString/Operations/Range+BigString.swift new file mode 100644 index 000000000..f800ff976 --- /dev/null +++ b/Sources/RopeModule/BigString/Operations/Range+BigString.swift @@ -0,0 +1,21 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=5.8) + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension Range { + internal var _isEmptyUTF8: Bool { + lowerBound.utf8Offset == upperBound.utf8Offset + } +} + +#endif diff --git a/Sources/RopeModule/BigString/Views/BigString+UTF16View.swift b/Sources/RopeModule/BigString/Views/BigString+UTF16View.swift new file mode 100644 index 000000000..db1d8742d --- /dev/null +++ b/Sources/RopeModule/BigString/Views/BigString+UTF16View.swift @@ -0,0 +1,152 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=5.8) + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + public struct UTF16View: Sendable { + var _base: BigString + + @inline(__always) + init(_base: BigString) { + self._base = _base + } + } + + @inline(__always) + public var utf16: UTF16View { + UTF16View(_base: self) + } + + public init(_ utf16: UTF16View) { + self = utf16._base + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString.UTF16View: Equatable { + public static func ==(left: Self, right: Self) -> Bool { + BigString.utf8IsEqual(left._base, to: right._base) + } + + public func isIdentical(to other: Self) -> Bool { + self._base.isIdentical(to: other._base) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString.UTF16View: Hashable { + public func hash(into hasher: inout Hasher) { + _base.hashUTF8(into: &hasher) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString.UTF16View: Sequence { + public typealias Element = UInt16 + + public struct Iterator { + internal let _base: BigString + internal var _index: BigString.Index + + internal init(_base: BigString, from start: BigString.Index) { + self._base = _base + self._index = _base._utf16Index(roundingDown: start) + } + } + + public func makeIterator() -> Iterator { + Iterator(_base: _base, from: startIndex) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString.UTF16View.Iterator: IteratorProtocol { + public typealias Element = UInt16 + + public mutating func next() -> UInt16? { + guard _index < _base.endIndex else { return nil } + let ri = _index._rope! + var ci = _index._chunkIndex + let chunk = _base._rope[ri] + let result = chunk.string.utf16[ci] + + chunk.string.utf16.formIndex(after: &ci) + if ci < chunk.string.endIndex { + _index = BigString.Index(baseUTF8Offset: _index._utf8BaseOffset, _rope: ri, chunk: ci) + } else { + _index = BigString.Index( + baseUTF8Offset: _index._utf8BaseOffset + chunk.utf8Count, + _rope: _base._rope.index(after: ri), + chunk: String.Index(_utf8Offset: 0)) + } + return result + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString.UTF16View: BidirectionalCollection { + public typealias Index = BigString.Index + public typealias SubSequence = BigSubstring.UTF16View + + @inline(__always) + public var startIndex: Index { _base.startIndex } + + @inline(__always) + public var endIndex: Index { _base.endIndex } + + public var count: Int { _base._utf16Count } + + @inline(__always) + public func index(after i: Index) -> Index { + _base._utf16Index(after: i) + } + + @inline(__always) + public func index(before i: Index) -> Index { + _base._utf16Index(before: i) + } + + @inline(__always) + public func index(_ i: Index, offsetBy distance: Int) -> Index { + _base._utf16Index(i, offsetBy: distance) + } + + public func index(_ i: Index, offsetBy distance: Int, limitedBy limit: Index) -> Index? { + _base._utf16Index(i, offsetBy: distance, limitedBy: limit) + } + + public func distance(from start: Index, to end: Index) -> Int { + _base._utf16Distance(from: start, to: end) + } + + public subscript(position: Index) -> UInt16 { + _base[_utf16: position] + } + + public subscript(bounds: Range) -> BigSubstring.UTF16View { + BigSubstring.UTF16View(_base, in: bounds) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString.UTF16View { + public func index(roundingDown i: Index) -> Index { + _base._utf16Index(roundingDown: i) + } + + public func index(roundingUp i: Index) -> Index { + _base._utf16Index(roundingUp: i) + } +} + +#endif diff --git a/Sources/RopeModule/BigString/Views/BigString+UTF8View.swift b/Sources/RopeModule/BigString/Views/BigString+UTF8View.swift new file mode 100644 index 000000000..dc122058e --- /dev/null +++ b/Sources/RopeModule/BigString/Views/BigString+UTF8View.swift @@ -0,0 +1,188 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=5.8) + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + public struct UTF8View: Sendable { + var _base: BigString + + @inline(__always) + init(_base: BigString) { + self._base = _base + } + } + + @inline(__always) + public var utf8: UTF8View { + UTF8View(_base: self) + } + + public init(_ utf8: UTF8View) { + self = utf8._base + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString.UTF8View: Equatable { + public static func ==(left: Self, right: Self) -> Bool { + BigString.utf8IsEqual(left._base, to: right._base) + } + + public func isIdentical(to other: Self) -> Bool { + self._base.isIdentical(to: other._base) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString.UTF8View: Hashable { + public func hash(into hasher: inout Hasher) { + _base.hashUTF8(into: &hasher) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString.UTF8View: Sequence { + public typealias Element = UInt8 + + public struct Iterator { + internal let _base: BigString + internal var _index: BigString.Index + + internal init(_base: BigString, from start: BigString.Index) { + self._base = _base + self._index = _base._utf8Index(roundingDown: start) + } + } + + public func makeIterator() -> Iterator { + Iterator(_base: _base, from: self.startIndex) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString.UTF8View.Iterator: IteratorProtocol { + public typealias Element = UInt8 + + public mutating func next() -> UInt8? { + guard _index < _base.endIndex else { return nil } + // Hand-optimized from `_base.subscript(utf8:)` and `_base.utf8Index(after:)`. + let ri = _index._rope! + var ci = _index._chunkIndex + let chunk = _base._rope[ri] + let result = chunk.string.utf8[ci] + + chunk.string.utf8.formIndex(after: &ci) + if ci < chunk.string.endIndex { + _index = BigString.Index(baseUTF8Offset: _index._utf8BaseOffset, _rope: ri, chunk: ci) + } else { + _index = BigString.Index( + baseUTF8Offset: _index._utf8BaseOffset + chunk.utf8Count, + _rope: _base._rope.index(after: ri), + chunk: String.Index(_utf8Offset: 0)) + } + return result + } + + public mutating func next( + maximumCount: Int, + with body: (UnsafeBufferPointer) -> (consumed: Int, result: R) + ) -> R { + guard _index < _base.endIndex else { + let r = body(UnsafeBufferPointer(start: nil, count: 0)) + precondition(r.consumed == 0) + return r.result + } + let ri = _index._rope! + var ci = _index._utf8ChunkOffset + var utf8Offset = _index.utf8Offset + var string = _base._rope[ri].string + let (haveMore, result) = string.withUTF8 { buffer in + let slice = buffer[ci...].prefix(maximumCount) + assert(!slice.isEmpty) + let (consumed, result) = body(UnsafeBufferPointer(rebasing: slice)) + precondition(consumed >= 0 && consumed <= slice.count) + utf8Offset += consumed + ci += consumed + return (ci < buffer.count, result) + } + if haveMore { + _index = BigString.Index(_utf8Offset: utf8Offset, _rope: ri, chunkOffset: ci) + } else { + _index = BigString.Index( + baseUTF8Offset: _index._utf8BaseOffset + string.utf8.count, + _rope: _base._rope.index(after: ri), + chunk: String.Index(_utf8Offset: 0)) + } + return result + } +} + + + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString.UTF8View: BidirectionalCollection { + public typealias Index = BigString.Index + public typealias SubSequence = BigSubstring.UTF8View + + @inline(__always) + public var startIndex: Index { _base.startIndex } + + @inline(__always) + public var endIndex: Index { _base.endIndex } + + public var count: Int { _base._utf8Count } + + @inline(__always) + public func index(after i: Index) -> Index { + _base._utf8Index(after: i) + } + + @inline(__always) + public func index(before i: Index) -> Index { + _base._utf8Index(before: i) + } + + @inline(__always) + public func index(_ i: Index, offsetBy distance: Int) -> Index { + _base._utf8Index(i, offsetBy: distance) + } + + public func index(_ i: Index, offsetBy distance: Int, limitedBy limit: Index) -> Index? { + _base._utf8Index(i, offsetBy: distance, limitedBy: limit) + } + + public func distance(from start: Index, to end: Index) -> Int { + _base._utf8Distance(from: start, to: end) + } + + public subscript(position: Index) -> UInt8 { + _base[_utf8: position] + } + + public subscript(bounds: Range) -> BigSubstring.UTF8View { + BigSubstring.UTF8View(_base, in: bounds) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString.UTF8View { + public func index(roundingDown i: Index) -> Index { + _base._utf8Index(roundingDown: i) + } + + public func index(roundingUp i: Index) -> Index { + _base._utf8Index(roundingUp: i) + } +} + +#endif diff --git a/Sources/RopeModule/BigString/Views/BigString+UnicodeScalarView.swift b/Sources/RopeModule/BigString/Views/BigString+UnicodeScalarView.swift new file mode 100644 index 000000000..c18e47e76 --- /dev/null +++ b/Sources/RopeModule/BigString/Views/BigString+UnicodeScalarView.swift @@ -0,0 +1,398 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=5.8) + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + public struct UnicodeScalarView: Sendable { + var _base: BigString + + @inline(__always) + init(_base: BigString) { + self._base = _base + } + } + + @inline(__always) + public var unicodeScalars: UnicodeScalarView { + get { + UnicodeScalarView(_base: self) + } + set { + self = newValue._base + } + _modify { + var view = UnicodeScalarView(_base: self) + self = .init() + defer { + self = view._base + } + yield &view + } + } + + public init(_ view: BigString.UnicodeScalarView) { + self = view._base + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString.UnicodeScalarView: ExpressibleByStringLiteral { + public init(stringLiteral value: String) { + self.init(value.unicodeScalars) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString.UnicodeScalarView: CustomStringConvertible { + public var description: String { + String(_base) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString.UnicodeScalarView: CustomDebugStringConvertible { + public var debugDescription: String { + description.debugDescription + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString.UnicodeScalarView: Equatable { + public static func ==(left: Self, right: Self) -> Bool { + BigString.utf8IsEqual(left._base, to: right._base) + } + + public func isIdentical(to other: Self) -> Bool { + self._base.isIdentical(to: other._base) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString.UnicodeScalarView: Hashable { + public func hash(into hasher: inout Hasher) { + _base.hashUTF8(into: &hasher) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString.UnicodeScalarView: Sequence { + public typealias Element = UnicodeScalar + + public struct Iterator { + internal let _base: BigString + internal var _index: BigString.Index + + internal init(_base: BigString, from start: BigString.Index) { + self._base = _base + self._index = _base._unicodeScalarIndex(roundingDown: start) + } + } + + public func makeIterator() -> Iterator { + Iterator(_base: self._base, from: self.startIndex) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString.UnicodeScalarView.Iterator: IteratorProtocol { + public typealias Element = UnicodeScalar + + public mutating func next() -> UnicodeScalar? { + guard _index < _base.endIndex else { return nil } + let ri = _index._rope! + var ci = _index._chunkIndex + let chunk = _base._rope[ri] + let result = chunk.string.unicodeScalars[ci] + + chunk.string.unicodeScalars.formIndex(after: &ci) + if ci < chunk.string.endIndex { + _index = BigString.Index(baseUTF8Offset: _index._utf8BaseOffset, _rope: ri, chunk: ci) + } else { + _index = BigString.Index( + baseUTF8Offset: _index._utf8BaseOffset + chunk.utf8Count, + _rope: _base._rope.index(after: ri), + chunk: String.Index(_utf8Offset: 0)) + } + return result + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString.UnicodeScalarView: BidirectionalCollection { + public typealias Index = BigString.Index + public typealias SubSequence = BigSubstring.UnicodeScalarView + + @inline(__always) + public var startIndex: Index { _base.startIndex } + + @inline(__always) + public var endIndex: Index { _base.endIndex } + + public var count: Int { _base._unicodeScalarCount } + + @inline(__always) + public func index(after i: Index) -> Index { + _base._unicodeScalarIndex(after: i) + } + + @inline(__always) + public func index(before i: Index) -> Index { + _base._unicodeScalarIndex(before: i) + } + + @inline(__always) + public func index(_ i: Index, offsetBy distance: Int) -> Index { + _base._unicodeScalarIndex(i, offsetBy: distance) + } + + public func index(_ i: Index, offsetBy distance: Int, limitedBy limit: Index) -> Index? { + _base._unicodeScalarIndex(i, offsetBy: distance, limitedBy: limit) + } + + public func distance(from start: Index, to end: Index) -> Int { + _base._unicodeScalarDistance(from: start, to: end) + } + + public subscript(position: Index) -> UnicodeScalar { + _base[_unicodeScalar: position] + } + + public subscript(bounds: Range) -> BigSubstring.UnicodeScalarView { + BigSubstring.UnicodeScalarView(_base, in: bounds) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString.UnicodeScalarView { + public func index(roundingDown i: Index) -> Index { + _base._unicodeScalarIndex(roundingDown: i) + } + + public func index(roundingUp i: Index) -> Index { + _base._unicodeScalarIndex(roundingUp: i) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString.UnicodeScalarView: RangeReplaceableCollection { + public init() { + self._base = BigString() + } + + public mutating func reserveCapacity(_ n: Int) { + // Do nothing. + } + + public mutating func replaceSubrange( + _ subrange: Range, + with newElements: __owned some Sequence // Note: Sequence, not Collection + ) { + if let newElements = _specialize( + newElements, for: String.UnicodeScalarView.self + ) { + _base._replaceSubrange(subrange, with: String(newElements)) + } else if let newElements = _specialize( + newElements, for: Substring.UnicodeScalarView.self + ) { + _base._replaceSubrange(subrange, with: Substring(newElements)) + } else if let newElements = _specialize( + newElements, for: BigString.UnicodeScalarView.self + ) { + _base._replaceSubrange(subrange, with: newElements._base) + } else if let newElements = _specialize( + newElements, for: BigSubstring.UnicodeScalarView.self + ) { + _base._replaceSubrange( + subrange, with: newElements._base, in: newElements._bounds) + } else { + _base._replaceSubrange(subrange, with: BigString(_from: newElements)) + } + } + + public mutating func replaceSubrange( + _ subrange: Range, + with newElements: __owned String.UnicodeScalarView + ) { + _base._replaceSubrange(subrange, with: String(newElements)) + } + + public mutating func replaceSubrange( + _ subrange: Range, + with newElements: __owned Substring.UnicodeScalarView + ) { + _base._replaceSubrange(subrange, with: Substring(newElements)) + } + + public mutating func replaceSubrange( + _ subrange: Range, + with newElements: __owned BigString.UnicodeScalarView + ) { + _base._replaceSubrange(subrange, with: newElements._base) + } + + public mutating func replaceSubrange( + _ subrange: Range, + with newElements: __owned BigSubstring.UnicodeScalarView + ) { + _base._replaceSubrange(subrange, with: newElements._base, in: newElements._bounds) + } + + public init(_ elements: some Sequence) { + if let elements = _specialize(elements, for: String.UnicodeScalarView.self) { + self.init(_base: BigString(_from: elements)) + } else if let elements = _specialize(elements, for: Substring.UnicodeScalarView.self) { + self.init(_base: BigString(_from: elements)) + } else if let elements = _specialize(elements, for: BigString.UnicodeScalarView.self) { + self.init(_base: elements._base) + } else if let elements = _specialize(elements, for: BigSubstring.UnicodeScalarView.self) { + self.init(_base: BigString(_from: elements._base, in: elements._bounds)) + } else { + self.init(_base: BigString(_from: elements)) + } + } + + public init(_ elements: String.UnicodeScalarView) { + self.init(_base: BigString(_from: elements)) + } + + public init(_ elements: Substring.UnicodeScalarView) { + self.init(_base: BigString(_from: elements)) + } + + public init(_ elements: BigString.UnicodeScalarView) { + self.init(_base: elements._base) + } + + public init(_ elements: BigSubstring.UnicodeScalarView) { + self.init(_base: BigString(_from: elements._base, in: elements._bounds)) + } + + public init(repeating repeatedValue: UnicodeScalar, count: Int) { + self.init(_base: BigString(repeating: BigString(String(repeatedValue)), count: count)) + } + + public init(repeating repeatedValue: some StringProtocol, count: Int) { + self.init(_base: BigString(repeating: BigString(_from: repeatedValue), count: count)) + } + + public init(repeating value: BigString.UnicodeScalarView, count: Int) { + self.init(_base: BigString(repeating: value._base, count: count)) + } + + public init(repeating value: BigSubstring.UnicodeScalarView, count: Int) { + let value = BigString(value) + self.init(_base: BigString(repeating: value, count: count)) + } + + public mutating func append(_ newElement: __owned UnicodeScalar) { + _base.append(contentsOf: String(newElement)) + } + + public mutating func append(contentsOf newElements: __owned some Sequence) { + if let newElements = _specialize(newElements, for: String.UnicodeScalarView.self) { + append(contentsOf: newElements) + } else if let newElements = _specialize(newElements, for: Substring.UnicodeScalarView.self) { + append(contentsOf: newElements) + } else if let newElements = _specialize(newElements, for: BigString.UnicodeScalarView.self) { + append(contentsOf: newElements) + } else if let newElements = _specialize(newElements, for: BigSubstring.UnicodeScalarView.self) { + append(contentsOf: newElements) + } else { + _base.append(contentsOf: BigString(_from: newElements)) + } + } + + public mutating func append(contentsOf newElements: __owned String.UnicodeScalarView) { + _base.append(contentsOf: String(newElements)) + } + + public mutating func append(contentsOf newElements: __owned Substring.UnicodeScalarView) { + _base.append(contentsOf: Substring(newElements)) + } + + public mutating func append(contentsOf newElements: __owned BigString.UnicodeScalarView) { + _base.append(contentsOf: newElements._base) + } + + public mutating func append(contentsOf newElements: __owned BigSubstring.UnicodeScalarView) { + _base._append(contentsOf: newElements._base, in: newElements._bounds) + } + + public mutating func insert(_ newElement: UnicodeScalar, at i: Index) { + _base.insert(contentsOf: String(newElement), at: i) + } + + public mutating func insert( + contentsOf newElements: __owned some Sequence, // Note: Sequence, not Collection + at i: Index + ) { + if let newElements = _specialize(newElements, for: String.UnicodeScalarView.self) { + insert(contentsOf: newElements, at: i) + } else if let newElements = _specialize(newElements, for: Substring.UnicodeScalarView.self) { + insert(contentsOf: newElements, at: i) + } else if let newElements = _specialize(newElements, for: BigString.UnicodeScalarView.self) { + insert(contentsOf: newElements, at: i) + } else if let newElements = _specialize(newElements, for: BigSubstring.UnicodeScalarView.self) { + insert(contentsOf: newElements, at: i) + } else { + _base.insert(contentsOf: BigString(_from: newElements), at: i) + } + } + + public mutating func insert( + contentsOf newElements: __owned String.UnicodeScalarView, + at i: Index + ) { + _base.insert(contentsOf: String(newElements), at: i) + } + + public mutating func insert( + contentsOf newElements: __owned Substring.UnicodeScalarView, + at i: Index + ) { + _base.insert(contentsOf: Substring(newElements), at: i) + } + + public mutating func insert( + contentsOf newElements: __owned BigString.UnicodeScalarView, + at i: Index + ) { + _base.insert(contentsOf: newElements._base, at: i) + } + + public mutating func insert( + contentsOf newElements: __owned BigSubstring.UnicodeScalarView, + at i: Index + ) { + _base._insert(contentsOf: newElements._base, in: newElements._bounds, at: i) + } + + @discardableResult + public mutating func remove(at i: Index) -> UnicodeScalar { + _base.removeUnicodeScalar(at: i) + } + + public mutating func removeSubrange(_ bounds: Range) { + _base._removeSubrange(bounds) + } + + public mutating func removeAll(keepingCapacity keepCapacity: Bool = false) { + self._base = BigString() + } +} + +#endif diff --git a/Sources/RopeModule/BigString/Views/BigSubstring+UTF16View.swift b/Sources/RopeModule/BigString/Views/BigSubstring+UTF16View.swift new file mode 100644 index 000000000..7e58fed12 --- /dev/null +++ b/Sources/RopeModule/BigString/Views/BigSubstring+UTF16View.swift @@ -0,0 +1,203 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=5.8) + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigSubstring { + public struct UTF16View: Sendable { + internal var _base: BigString + internal var _bounds: Range + + public init(_unchecked base: BigString, in bounds: Range) { + self._base = base + self._bounds = bounds + } + + public init(_ base: BigString, in bounds: Range) { + self._base = base + let lower = base._utf16Index(roundingDown: bounds.lowerBound) + let upper = base._utf16Index(roundingDown: bounds.upperBound) + self._bounds = Range(uncheckedBounds: (lower, upper)) + } + + internal init(_substring: BigSubstring) { + self.init(_unchecked: _substring._base, in: _substring._bounds) + } + } + + public var utf16: UTF16View { + UTF16View(_substring: self) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + public init?(_ utf16: BigSubstring.UTF16View) { + guard + !utf16.startIndex._isUTF16TrailingSurrogate, + !utf16.endIndex._isUTF16TrailingSurrogate + else { + return nil + } + self.init(_from: utf16._base, in: utf16._bounds) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigSubstring.UTF16View { + public var base: BigString.UTF16View { _base.utf16 } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigSubstring.UTF16View: Equatable { + public static func ==(left: Self, right: Self) -> Bool { + var i1 = left._bounds.lowerBound + var i2 = right._bounds.lowerBound + + var j1 = left._bounds.upperBound + var j2 = right._bounds.upperBound + + // Compare first code units, if they're trailing surrogates. + guard i1._isUTF16TrailingSurrogate == i2._isUTF16TrailingSurrogate else { return false } + if i1._isUTF16TrailingSurrogate { + guard left[i1] == right[i2] else { return false } + left.formIndex(after: &i1) + left.formIndex(after: &i2) + } + guard i1 < j1, i2 < j2 else { return i1 == j1 && i2 == j2 } + + // Compare last code units, if they're trailing surrogates. + guard j1._isUTF16TrailingSurrogate == j2._isUTF16TrailingSurrogate else { return false } + if j1._isUTF16TrailingSurrogate { + left.formIndex(before: &j1) + right.formIndex(before: &j2) + guard left[j1] == right[j2] else { return false} + } + + return BigString.utf8IsEqual(left._base, in: i1 ..< j1, to: right._base, in: i2 ..< j2) + } + + public func isIdentical(to other: Self) -> Bool { + guard self._base.isIdentical(to: other._base) else { return false } + return self._bounds == other._bounds + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigSubstring.UTF16View: Hashable { + public func hash(into hasher: inout Hasher) { + for codeUnit in self { + hasher.combine(codeUnit) + } + hasher.combine(0xFFFF as UInt16) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigSubstring.UTF16View: Sequence { + public typealias Element = UInt16 + + public struct Iterator: IteratorProtocol { + var _it: BigString.UTF16View.Iterator + var _end: BigString.Index + + init(_substring: BigSubstring.UTF16View) { + self._it = .init(_base: _substring._base, from: _substring.startIndex) + self._end = _substring.endIndex + } + + public mutating func next() -> UInt16? { + guard _it._index < _end else { return nil } + return _it.next() + } + } + + public func makeIterator() -> Iterator { + Iterator(_substring: self) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigSubstring.UTF16View: BidirectionalCollection { + public typealias Index = BigString.Index + public typealias SubSequence = Self + + @inline(__always) + public var startIndex: Index { _bounds.lowerBound } + + @inline(__always) + public var endIndex: Index { _bounds.upperBound } + + public var count: Int { + distance(from: _bounds.lowerBound, to: _bounds.upperBound) + } + + @inline(__always) + public func index(after i: Index) -> Index { + precondition(i < endIndex, "Can't advance above end index") + return _base._utf16Index(after: i) + } + + @inline(__always) + public func index(before i: Index) -> Index { + precondition(i > startIndex, "Can't advance below start index") + return _base._utf16Index(before: i) + } + + @inline(__always) + public func index(_ i: Index, offsetBy distance: Int) -> Index { + precondition(i >= startIndex && i <= endIndex, "Index out of bounds") + let j = _base._utf16Index(i, offsetBy: distance) + precondition(j >= startIndex && j <= endIndex, "Index out of bounds") + return j + } + + public func index(_ i: Index, offsetBy distance: Int, limitedBy limit: Index) -> Index? { + precondition(i >= startIndex && i <= endIndex, "Index out of bounds") + guard let j = _base._utf16Index(i, offsetBy: distance, limitedBy: limit) else { return nil } + precondition(j >= startIndex && j <= endIndex, "Index out of bounds") + return j + } + + public func distance(from start: Index, to end: Index) -> Int { + precondition(start >= startIndex && start <= endIndex, "Index out of bounds") + precondition(end >= startIndex && end <= endIndex, "Index out of bounds") + return _base._utf16Distance(from: start, to: end) + } + + public subscript(position: Index) -> UInt16 { + precondition(position >= startIndex && position < endIndex, "Index out of bounds") + return _base[_utf16: position] + } + + public subscript(bounds: Range) -> Self { + precondition( + bounds.lowerBound >= startIndex && bounds.upperBound <= endIndex, + "Range out of bounds") + return Self(_base, in: bounds) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigSubstring.UTF16View { + public func index(roundingDown i: Index) -> Index { + precondition(i >= startIndex && i <= endIndex, "Index out of bounds") + return _base._utf16Index(roundingDown: i) + } + + public func index(roundingUp i: Index) -> Index { + precondition(i >= startIndex && i <= endIndex, "Index out of bounds") + return _base._utf16Index(roundingUp: i) + } +} + +#endif diff --git a/Sources/RopeModule/BigString/Views/BigSubstring+UTF8View.swift b/Sources/RopeModule/BigString/Views/BigSubstring+UTF8View.swift new file mode 100644 index 000000000..1c6223bda --- /dev/null +++ b/Sources/RopeModule/BigString/Views/BigSubstring+UTF8View.swift @@ -0,0 +1,177 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=5.8) + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigSubstring { + public struct UTF8View: Sendable { + internal var _base: BigString + internal var _bounds: Range + + public init(_unchecked base: BigString, in bounds: Range) { + self._base = base + self._bounds = bounds + } + + public init(_ base: BigString, in bounds: Range) { + self._base = base + let lower = base._utf8Index(roundingDown: bounds.lowerBound) + let upper = base._utf8Index(roundingDown: bounds.upperBound) + self._bounds = Range(uncheckedBounds: (lower, upper)) + } + + internal init(_substring: BigSubstring) { + self.init(_unchecked: _substring._base, in: _substring._bounds) + } + } + + public var utf8: UTF8View { + UTF8View(_substring: self) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + public init?(_ utf8: BigSubstring.UTF8View) { + guard + utf8._base.unicodeScalars.index(roundingDown: utf8.startIndex) == utf8.startIndex, + utf8._base.unicodeScalars.index(roundingDown: utf8.endIndex) == utf8.endIndex + else { + return nil + } + self.init(_from: utf8._base, in: utf8._bounds) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigSubstring.UTF8View { + public var base: BigString.UTF8View { _base.utf8 } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigSubstring.UTF8View: Equatable { + public static func ==(left: Self, right: Self) -> Bool { + BigString.utf8IsEqual(left._base, in: left._bounds, to: right._base, in: right._bounds) + } + + public func isIdentical(to other: Self) -> Bool { + guard self._base.isIdentical(to: other._base) else { return false } + return self._bounds == other._bounds + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigSubstring.UTF8View: Hashable { + public func hash(into hasher: inout Hasher) { + _base.hashUTF8(into: &hasher, from: _bounds.lowerBound, to: _bounds.upperBound) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigSubstring.UTF8View: Sequence { + public typealias Element = UInt8 + + public struct Iterator: IteratorProtocol { + var _it: BigString.UTF8View.Iterator + var _end: BigString.Index + + init(_substring: BigSubstring.UTF8View) { + self._it = .init(_base: _substring._base, from: _substring.startIndex) + self._end = _substring.endIndex + } + + public mutating func next() -> UInt8? { + guard _it._index < _end else { return nil } + return _it.next() + } + } + + public func makeIterator() -> Iterator { + Iterator(_substring: self) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigSubstring.UTF8View: BidirectionalCollection { + public typealias Index = BigString.Index + public typealias SubSequence = Self + + @inline(__always) + public var startIndex: Index { _bounds.lowerBound } + + @inline(__always) + public var endIndex: Index { _bounds.upperBound } + + public var count: Int { + distance(from: _bounds.lowerBound, to: _bounds.upperBound) + } + + @inline(__always) + public func index(after i: Index) -> Index { + precondition(i < endIndex, "Can't advance above end index") + return _base._utf8Index(after: i) + } + + @inline(__always) + public func index(before i: Index) -> Index { + precondition(i > startIndex, "Can't advance below start index") + return _base._utf8Index(before: i) + } + + @inline(__always) + public func index(_ i: Index, offsetBy distance: Int) -> Index { + precondition(i >= startIndex && i <= endIndex, "Index out of bounds") + let j = _base._utf8Index(i, offsetBy: distance) + precondition(j >= startIndex && j <= endIndex, "Index out of bounds") + return j + } + + public func index(_ i: Index, offsetBy distance: Int, limitedBy limit: Index) -> Index? { + precondition(i >= startIndex && i <= endIndex, "Index out of bounds") + guard let j = _base._utf8Index(i, offsetBy: distance, limitedBy: limit) else { return nil } + precondition(j >= startIndex && j <= endIndex, "Index out of bounds") + return j + } + + public func distance(from start: Index, to end: Index) -> Int { + precondition(start >= startIndex && start <= endIndex, "Index out of bounds") + precondition(end >= startIndex && end <= endIndex, "Index out of bounds") + return _base._utf8Distance(from: start, to: end) + } + + public subscript(position: Index) -> UInt8 { + precondition(position >= startIndex && position < endIndex, "Index out of bounds") + return _base[_utf8: position] + } + + public subscript(bounds: Range) -> Self { + precondition( + bounds.lowerBound >= startIndex && bounds.upperBound <= endIndex, + "Range out of bounds") + return Self(_base, in: bounds) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigSubstring.UTF8View { + public func index(roundingDown i: Index) -> Index { + precondition(i >= startIndex && i <= endIndex, "Index out of bounds") + return _base._utf8Index(roundingDown: i) + } + + public func index(roundingUp i: Index) -> Index { + precondition(i >= startIndex && i <= endIndex, "Index out of bounds") + return _base._utf8Index(roundingUp: i) + } +} + +#endif diff --git a/Sources/RopeModule/BigString/Views/BigSubstring+UnicodeScalarView.swift b/Sources/RopeModule/BigString/Views/BigSubstring+UnicodeScalarView.swift new file mode 100644 index 000000000..e7da61caa --- /dev/null +++ b/Sources/RopeModule/BigString/Views/BigSubstring+UnicodeScalarView.swift @@ -0,0 +1,324 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=5.8) + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigSubstring { + public struct UnicodeScalarView: Sendable { + internal var _base: BigString + internal var _bounds: Range + + public init(_unchecked base: BigString, in bounds: Range) { + assert(bounds.lowerBound == base._unicodeScalarIndex(roundingDown: bounds.lowerBound)) + assert(bounds.upperBound == base._unicodeScalarIndex(roundingDown: bounds.upperBound)) + self._base = base + self._bounds = bounds + } + + public init(_ base: BigString, in bounds: Range) { + self._base = base + let lower = base._unicodeScalarIndex(roundingDown: bounds.lowerBound) + let upper = base._unicodeScalarIndex(roundingDown: bounds.upperBound) + self._bounds = Range(uncheckedBounds: (lower, upper)) + } + + internal init(_substring: BigSubstring) { + self.init(_unchecked: _substring._base, in: _substring._bounds) + } + } + + public var unicodeScalars: UnicodeScalarView { + get { + UnicodeScalarView(_substring: self) + } + set { + self = Self(newValue._base, in: newValue._bounds) + } + _modify { + var view = UnicodeScalarView(_unchecked: _base, in: _bounds) + self = Self() + defer { + self = Self(view._base, in: view._bounds) + } + yield &view + } + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigString { + public init(_ unicodeScalars: BigSubstring.UnicodeScalarView) { + self.init(_from: unicodeScalars._base, in: unicodeScalars._bounds) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigSubstring.UnicodeScalarView { + public var base: BigString.UnicodeScalarView { _base.unicodeScalars } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigSubstring.UnicodeScalarView: ExpressibleByStringLiteral { + public init(stringLiteral value: String) { + self.init(value.unicodeScalars) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigSubstring.UnicodeScalarView: CustomStringConvertible { + public var description: String { + String(_from: _base, in: _bounds) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigSubstring.UnicodeScalarView: CustomDebugStringConvertible { + public var debugDescription: String { + description.debugDescription + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigSubstring.UnicodeScalarView: Equatable { + public static func ==(left: Self, right: Self) -> Bool { + BigString.utf8IsEqual(left._base, in: left._bounds, to: right._base, in: right._bounds) + } + + public func isIdentical(to other: Self) -> Bool { + guard self._base.isIdentical(to: other._base) else { return false } + return self._bounds == other._bounds + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigSubstring.UnicodeScalarView: Hashable { + public func hash(into hasher: inout Hasher) { + _base.hashUTF8(into: &hasher, from: _bounds.lowerBound, to: _bounds.upperBound) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigSubstring.UnicodeScalarView: Sequence { + public typealias Element = UnicodeScalar + + public struct Iterator: IteratorProtocol { + var _it: BigString.UnicodeScalarView.Iterator + let _end: BigString.Index + + internal init(_substring: BigSubstring.UnicodeScalarView) { + self._it = .init(_base: _substring._base, from: _substring.startIndex) + self._end = _substring._base.resolve(_substring.endIndex, preferEnd: true) + } + + public mutating func next() -> UnicodeScalar? { + guard _it._index < _end else { return nil } + return _it.next() + } + } + + public func makeIterator() -> Iterator { + Iterator(_substring: self) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigSubstring.UnicodeScalarView: BidirectionalCollection { + public typealias Index = BigString.Index + public typealias SubSequence = Self + + @inline(__always) + public var startIndex: Index { _bounds.lowerBound } + + @inline(__always) + public var endIndex: Index { _bounds.upperBound } + + public var count: Int { + distance(from: _bounds.lowerBound, to: _bounds.upperBound) + } + + @inline(__always) + public func index(after i: Index) -> Index { + precondition(i < endIndex, "Can't advance above end index") + return _base._unicodeScalarIndex(after: i) + } + + @inline(__always) + public func index(before i: Index) -> Index { + precondition(i > startIndex, "Can't advance below start index") + return _base._unicodeScalarIndex(before: i) + } + + @inline(__always) + public func index(_ i: Index, offsetBy distance: Int) -> Index { + precondition(i >= startIndex && i <= endIndex, "Index out of bounds") + let j = _base._unicodeScalarIndex(i, offsetBy: distance) + precondition(j >= startIndex && j <= endIndex, "Index out of bounds") + return j + } + + public func index(_ i: Index, offsetBy distance: Int, limitedBy limit: Index) -> Index? { + precondition(i >= startIndex && i <= endIndex, "Index out of bounds") + guard let j = _base._unicodeScalarIndex(i, offsetBy: distance, limitedBy: limit) else { + return nil + } + precondition(j >= startIndex && j <= endIndex, "Index out of bounds") + return j + } + + public func distance(from start: Index, to end: Index) -> Int { + precondition(start >= startIndex && start <= endIndex, "Index out of bounds") + precondition(end >= startIndex && end <= endIndex, "Index out of bounds") + return _base._unicodeScalarDistance(from: start, to: end) + } + + public subscript(position: Index) -> UnicodeScalar { + precondition(position >= startIndex && position < endIndex, "Index out of bounds") + return _base[_unicodeScalar: position] + } + + public subscript(bounds: Range) -> Self { + precondition( + bounds.lowerBound >= startIndex && bounds.upperBound <= endIndex, + "Range out of bounds") + return Self(_base, in: bounds) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigSubstring.UnicodeScalarView { + public func index(roundingDown i: Index) -> Index { + precondition(i >= startIndex && i <= endIndex, "Index out of bounds") + return _base._unicodeScalarIndex(roundingDown: i) + } + + public func index(roundingUp i: Index) -> Index { + precondition(i >= startIndex && i <= endIndex, "Index out of bounds") + return _base._unicodeScalarIndex(roundingUp: i) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigSubstring.UnicodeScalarView { + /// Run the closure `body` to mutate the contents of this view within `range`, then update + /// the bounds of this view to maintain their logical position in the resulting string. + /// The `range` argument is validated to be within the original bounds of the substring. + internal mutating func _mutateBasePreservingBounds( + in range: Range, + with body: (inout BigString.UnicodeScalarView) -> R + ) -> R { + precondition( + range.lowerBound >= _bounds.lowerBound && range.upperBound <= _bounds.upperBound, + "Range out of bounds") + + let startOffset = self.startIndex.utf8Offset + let endOffset = self.endIndex.utf8Offset + let oldCount = self._base._utf8Count + + var view = BigString.UnicodeScalarView(_base: self._base) + self._base = BigString() + + defer { + // The Unicode scalar view is regular -- we just need to maintain the UTF-8 offsets of + // our bounds across the mutation. No extra adjustment/rounding is necessary. + self._base = view._base + let delta = self._base._utf8Count - oldCount + let start = _base._utf8Index(at: startOffset)._knownScalarAligned() + let end = _base._utf8Index(at: endOffset + delta)._knownScalarAligned() + self._bounds = start ..< end + } + return body(&view) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigSubstring.UnicodeScalarView: RangeReplaceableCollection { + public init() { + self.init(_substring: BigSubstring()) + } + + public mutating func reserveCapacity(_ n: Int) { + // Do nothing. + } + + public mutating func replaceSubrange( + _ subrange: Range, + with newElements: __owned some Sequence // Note: Sequence, not Collection + ) { + _mutateBasePreservingBounds(in: subrange) { $0.replaceSubrange(subrange, with: newElements) } + } + + public init(_ elements: some Sequence) { + let base = BigString.UnicodeScalarView(elements) + self.init(base._base, in: base.startIndex ..< base.endIndex) + } + + public init(repeating repeatedValue: UnicodeScalar, count: Int) { + let base = BigString.UnicodeScalarView(repeating: repeatedValue, count: count) + self.init(base._base, in: base.startIndex ..< base.endIndex) + } + + public mutating func append(_ newElement: UnicodeScalar) { + let i = endIndex + _mutateBasePreservingBounds(in: i ..< i) { + $0.insert(newElement, at: i) + } + } + + public mutating func append( + contentsOf newElements: __owned some Sequence + ) { + let i = endIndex + _mutateBasePreservingBounds(in: i ..< i) { + $0.insert(contentsOf: newElements, at: i) + } + } + + public mutating func insert(_ newElement: UnicodeScalar, at i: Index) { + _mutateBasePreservingBounds(in: i ..< i) { + $0.insert(newElement, at: i) + } + } + + + public mutating func insert( + contentsOf newElements: __owned some Sequence, // Note: Sequence, not Collection + at i: Index + ) { + _mutateBasePreservingBounds(in: i ..< i) { + $0.insert(contentsOf: newElements, at: i) + } + } + + @discardableResult + public mutating func remove(at i: Index) -> UnicodeScalar { + let j = self.index(after: i) + return _mutateBasePreservingBounds(in: i ..< j) { + $0.remove(at: i) + } + } + + public mutating func removeSubrange(_ bounds: Range) { + _mutateBasePreservingBounds(in: bounds) { + $0.removeSubrange(bounds) + } + } + + public mutating func removeAll(keepingCapacity keepCapacity: Bool = false) { + let bounds = _bounds + _mutateBasePreservingBounds(in: bounds) { + $0.removeSubrange(bounds) + } + assert(_bounds.isEmpty) + } +} + +#endif diff --git a/Sources/RopeModule/BigString/Views/BigSubstring.swift b/Sources/RopeModule/BigString/Views/BigSubstring.swift new file mode 100644 index 000000000..427b5e55f --- /dev/null +++ b/Sources/RopeModule/BigString/Views/BigSubstring.swift @@ -0,0 +1,372 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=5.8) + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +public struct BigSubstring: Sendable { + var _base: BigString + var _bounds: Range + + public init(_unchecked base: BigString, in bounds: Range) { + assert(bounds.lowerBound == base.index(roundingDown: bounds.lowerBound)) + assert(bounds.upperBound == base.index(roundingDown: bounds.upperBound)) + self._base = base + self._bounds = bounds + } + + public init(_ base: BigString, in bounds: Range) { + self._base = base + // Sub-character slicing could change character boundaries in the tree, requiring + // resyncing metadata. This would not be acceptable to do during slicing, so let's + // round substring bounds down to the nearest character. + let start = base.index(roundingDown: bounds.lowerBound) + let end = base.index(roundingDown: bounds.upperBound) + self._bounds = Range(uncheckedBounds: (start, end)) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigSubstring { + public var base: BigString { _base } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigSubstring { + func _foreachChunk( + _ body: (Substring) -> Void + ) { + self._base._foreachChunk(from: _bounds.lowerBound, to: _bounds.upperBound, body) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigSubstring: CustomStringConvertible { + public var description: String { + String(_from: _base, in: _bounds) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigSubstring: CustomDebugStringConvertible { + public var debugDescription: String { + description.debugDescription + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigSubstring: ExpressibleByStringLiteral { + public init(stringLiteral value: String) { + self.init(value) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigSubstring: LosslessStringConvertible { + // init?(_: String) is implemented by RangeReplaceableCollection.init(_:) +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigSubstring: Equatable { + public static func ==(left: Self, right: Self) -> Bool { + // FIXME: Implement properly normalized comparisons & hashing. + // This is somewhat tricky as we shouldn't just normalize individual pieces of the string + // split up on random Character boundaries -- Unicode does not promise that + // norm(a + c) == norm(a) + norm(b) in this case. + // To do this properly, we'll probably need to expose new stdlib entry points. :-/ + if left.isIdentical(to: right) { return true } + + guard left.count == right.count else { return false } + + // FIXME: Even if we keep doing characterwise comparisons, we should skip over shared subtrees. + var it1 = left.makeIterator() + var it2 = right.makeIterator() + var a: Character? = nil + var b: Character? = nil + repeat { + a = it1.next() + b = it2.next() + guard a == b else { return false } + } while a != nil + return true + } + + public func isIdentical(to other: Self) -> Bool { + guard self._base.isIdentical(to: other._base) else { return false } + return self._bounds == other._bounds + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigSubstring: Hashable { + public func hash(into hasher: inout Hasher) { + var it = self.makeIterator() + while let character = it.next() { + let s = String(character) + s._withNFCCodeUnits { hasher.combine($0) } + } + hasher.combine(0xFF as UInt8) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigSubstring: Comparable { + public static func < (left: Self, right: Self) -> Bool { + // FIXME: Implement properly normalized comparisons & hashing. + // This is somewhat tricky as we shouldn't just normalize individual pieces of the string + // split up on random Character boundaries -- Unicode does not promise that + // norm(a + c) == norm(a) + norm(b) in this case. + // To do this properly, we'll probably need to expose new stdlib entry points. :-/ + if left.isIdentical(to: right) { return false } + // FIXME: Even if we keep doing characterwise comparisons, we should skip over shared subtrees. + var it1 = left.makeIterator() + var it2 = right.makeIterator() + while true { + switch (it1.next(), it2.next()) { + case (nil, nil): return false + case (nil, .some): return true + case (.some, nil): return false + case let (a?, b?): + if a == b { continue } + return a < b + } + } + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigSubstring: Sequence { + public typealias Element = Character + + public struct Iterator: IteratorProtocol { + let _end: BigString.Index + var _it: BigString.Iterator + + init(_substring: BigSubstring) { + self._it = BigString.Iterator(_substring._base, from: _substring.startIndex) + self._end = _substring.endIndex + } + + public mutating func next() -> Character? { + guard _it.isBelow(_end) else { return nil } + return _it.next() + } + } + + public func makeIterator() -> Iterator { + Iterator(_substring: self) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigSubstring: BidirectionalCollection { + public typealias Index = BigString.Index + public typealias SubSequence = Self + + @inline(__always) + public var startIndex: Index { _bounds.lowerBound } + + @inline(__always) + public var endIndex: Index { _bounds.upperBound } + + public var count: Int { + distance(from: _bounds.lowerBound, to: _bounds.upperBound) + } + + @inline(__always) + public func index(after i: Index) -> Index { + precondition(i < endIndex, "Can't advance above end index") + return _base.index(after: i) + } + + @inline(__always) + public func index(before i: Index) -> Index { + precondition(i > startIndex, "Can't advance below start index") + return _base.index(before: i) + } + + @inline(__always) + public func index(_ i: Index, offsetBy distance: Int) -> Index { + precondition(i >= startIndex && i <= endIndex, "Index out of bounds") + let j = _base.index(i, offsetBy: distance) + precondition(j >= startIndex && j <= endIndex, "Index out of bounds") + return j + } + + public func index(_ i: Index, offsetBy distance: Int, limitedBy limit: Index) -> Index? { + precondition(i >= startIndex && i <= endIndex, "Index out of bounds") + guard let j = _base.index(i, offsetBy: distance, limitedBy: limit) else { return nil } + precondition(j >= startIndex && j <= endIndex, "Index out of bounds") + return j + } + + public func distance(from start: Index, to end: Index) -> Int { + precondition(start >= startIndex && start <= endIndex, "Index out of bounds") + precondition(end >= startIndex && end <= endIndex, "Index out of bounds") + return _base.distance(from: start, to: end) + } + + public subscript(position: Index) -> Character { + precondition(position >= startIndex && position < endIndex, "Index out of bounds") + return _base[position] + } + + public subscript(bounds: Range) -> Self { + precondition( + bounds.lowerBound >= startIndex && bounds.upperBound <= endIndex, + "Range out of bounds") + return Self(_base, in: bounds) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigSubstring { + public func index(roundingDown i: Index) -> Index { + precondition(i >= startIndex && i <= endIndex, "Index out of bounds") + return _base.index(roundingDown: i) + } + + public func index(roundingUp i: Index) -> Index { + precondition(i >= startIndex && i <= endIndex, "Index out of bounds") + return _base.index(roundingUp: i) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigSubstring { + /// Run the closure `body` to mutate the contents of this view within `range`, then update + /// the bounds of this view to maintain an approximation of their logical position in the + /// resulting string. + /// + /// The `range` argument is validated to be within the original bounds of the substring. + internal mutating func _mutateBasePreservingBounds( + in range: Range, + with body: (inout BigString) -> R + ) -> R { + precondition( + range.lowerBound >= _bounds.lowerBound && range.upperBound <= _bounds.upperBound, + "Range out of bounds") + + let startOffset = self.startIndex.utf8Offset + let endOffset = self.endIndex.utf8Offset + let oldCount = self._base._utf8Count + + defer { + // Substring mutations may change grapheme boundaries across the bounds of the original + // substring value, and we need to ensure that the substring's bounds remain well-aligned. + // Unfortunately, there are multiple ways of doing this, none of which are obviously + // superior to others. To keep the behavior easier to explan, we emulate substring + // initialization and round the start and end indices down to the nearest Character boundary + // after each mutation. + let delta = self._base._utf8Count - oldCount + let start = _base.index(roundingDown: Index(_utf8Offset: startOffset)) + let end = _base.index(roundingDown: Index(_utf8Offset: endOffset + delta)) + self._bounds = start ..< end + } + return body(&self._base) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension BigSubstring: RangeReplaceableCollection { + public init() { + let str = BigString() + let bounds = Range(uncheckedBounds: (str.startIndex, str.endIndex)) + self.init(_unchecked: str, in: bounds) + } + + public mutating func reserveCapacity(_ n: Int) { + // Do nothing. + } + + public mutating func replaceSubrange( // Note: Sequence, not Collection + _ subrange: Range, + with newElements: __owned some Sequence + ) { + _mutateBasePreservingBounds(in: subrange) { + $0.replaceSubrange(subrange, with: newElements) + } + } + + public init(_ elements: some Sequence) { + let base = BigString(elements) + self.init(base, in: base.startIndex ..< base.endIndex) + } + + public init(repeating repeatedValue: Character, count: Int) { + self.init(BigString(repeating: repeatedValue, count: count)) + } + + public init(repeating repeatedValue: some StringProtocol, count: Int) { + self.init(BigString(repeating: repeatedValue, count: count)) + } + + public init(repeating repeatedValue: BigString, count: Int) { + self.init(BigString(repeating: repeatedValue, count: count)) + } + + public init(repeating repeatedValue: BigSubstring, count: Int) { + self.init(BigString(repeating: repeatedValue, count: count)) + } + + public mutating func append(_ newElement: Character) { + let i = endIndex + _mutateBasePreservingBounds(in: i ..< i) { + $0.insert(newElement, at: i) + } + } + + public mutating func append(contentsOf newElements: __owned some Sequence) { + let i = endIndex + _mutateBasePreservingBounds(in: i ..< i) { + $0.insert(contentsOf: newElements, at: i) + } + } + + public mutating func insert(_ newElement: Character, at i: Index) { + _mutateBasePreservingBounds(in: i ..< i) { + $0.insert(newElement, at: i) + } + } + + public mutating func insert( + contentsOf newElements: __owned some Sequence, // Note: Sequence, not Collection + at i: Index + ) { + _mutateBasePreservingBounds(in: i ..< i) { + $0.insert(contentsOf: newElements, at: i) + } + } + + @discardableResult + public mutating func remove(at i: Index) -> Character { + let j = self.index(after: i) + return _mutateBasePreservingBounds(in: i ..< j) { + $0.remove(at: i) + } + } + + public mutating func removeSubrange(_ bounds: Range) { + _mutateBasePreservingBounds(in: bounds) { + $0.removeSubrange(bounds) + } + } + + public mutating func removeAll(keepingCapacity keepCapacity: Bool = false) { + let bounds = self._bounds + _mutateBasePreservingBounds(in: bounds) { + $0.removeSubrange(bounds) + } + assert(_bounds.isEmpty) + } +} + +#endif diff --git a/Sources/RopeModule/CMakeLists.txt b/Sources/RopeModule/CMakeLists.txt new file mode 100644 index 000000000..f6a6edef3 --- /dev/null +++ b/Sources/RopeModule/CMakeLists.txt @@ -0,0 +1,94 @@ +#[[ +This source file is part of the Swift Collections Open Source Project + +Copyright (c) 2023 - 2024 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 +#]] + +add_library(RopeModule + "BigString/Basics/BigString+Metrics.swift" + "BigString/Basics/BigString+Index.swift" + "BigString/Basics/BigString+Summary.swift" + "BigString/Basics/BigString.swift" + "BigString/Basics/BigString+Iterators.swift" + "BigString/Basics/BigString+Contents.swift" + "BigString/Basics/BigString+Invariants.swift" + "BigString/Basics/BigString+Ingester.swift" + "BigString/Basics/BigString+Debugging.swift" + "BigString/Basics/BigString+Builder.swift" + "BigString/Chunk/BigString+Chunk+Append and Insert.swift" + "BigString/Chunk/BigString+Chunk+Indexing by UTF16.swift" + "BigString/Chunk/BigString+Chunk+Counts.swift" + "BigString/Chunk/BigString+Chunk+Indexing by Characters.swift" + "BigString/Chunk/BigString+Chunk.swift" + "BigString/Chunk/BigString+Chunk+Description.swift" + "BigString/Chunk/BigString+Chunk+Splitting.swift" + "BigString/Chunk/BigString+Chunk+Breaks.swift" + "BigString/Chunk/BigString+Chunk+RopeElement.swift" + "BigString/Operations/BigString+Split.swift" + "BigString/Operations/BigString+Managing Breaks.swift" + "BigString/Operations/BigString+RemoveSubrange.swift" + "BigString/Operations/BigString+ReplaceSubrange.swift" + "BigString/Operations/BigString+Insert.swift" + "BigString/Operations/BigString+Initializers.swift" + "BigString/Operations/Range+BigString.swift" + "BigString/Operations/BigString+Append.swift" + "BigString/Views/BigString+UnicodeScalarView.swift" + "BigString/Views/BigString+UTF8View.swift" + "BigString/Views/BigSubstring+UnicodeScalarView.swift" + "BigString/Views/BigSubstring.swift" + "BigString/Views/BigSubstring+UTF16View.swift" + "BigString/Views/BigString+UTF16View.swift" + "BigString/Views/BigSubstring+UTF8View.swift" + "BigString/Conformances/BigString+Hashing.swift" + "BigString/Conformances/BigString+CustomStringConvertible.swift" + "BigString/Conformances/BigString+BidirectionalCollection.swift" + "BigString/Conformances/BigString+CustomDebugStringConvertible.swift" + "BigString/Conformances/BigString+Equatable.swift" + "BigString/Conformances/BigString+TextOutputStream.swift" + "BigString/Conformances/BigString+LosslessStringConvertible.swift" + "BigString/Conformances/BigString+Sequence.swift" + "BigString/Conformances/BigString+ExpressibleByStringLiteral.swift" + "BigString/Conformances/BigString+RangeReplaceableCollection.swift" + "BigString/Conformances/BigString+Comparable.swift" + "Rope/Basics/RopeMetric.swift" + "Rope/Basics/_RopeItem.swift" + "Rope/Basics/Rope+Debugging.swift" + "Rope/Basics/_RopeVersion.swift" + "Rope/Basics/RopeElement.swift" + "Rope/Basics/Rope.swift" + "Rope/Basics/Rope+_UnsafeHandle.swift" + "Rope/Basics/Rope+_Storage.swift" + "Rope/Basics/Rope+Invariants.swift" + "Rope/Basics/Rope+_UnmanagedLeaf.swift" + "Rope/Basics/RopeSummary.swift" + "Rope/Basics/Rope+Builder.swift" + "Rope/Basics/_RopePath.swift" + "Rope/Basics/Rope+_Node.swift" + "Rope/Operations/Rope+Extract.swift" + "Rope/Operations/Rope+Append.swift" + "Rope/Operations/Rope+Split.swift" + "Rope/Operations/Rope+Find.swift" + "Rope/Operations/Rope+Insert.swift" + "Rope/Operations/Rope+ForEachWhile.swift" + "Rope/Operations/Rope+MutatingForEach.swift" + "Rope/Operations/Rope+Join.swift" + "Rope/Operations/Rope+Remove.swift" + "Rope/Operations/Rope+RemoveSubrange.swift" + "Rope/Conformances/Rope+Index.swift" + "Rope/Conformances/Rope+Sequence.swift" + "Rope/Conformances/Rope+Collection.swift" + "Utilities/String Utilities.swift" + "Utilities/_CharacterRecognizer.swift" + "Utilities/String.Index+ABI.swift" + "Utilities/Optional Utilities.swift" + ) +target_link_libraries(RopeModule PRIVATE + _CollectionsUtilities) +set_target_properties(RopeModule PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) + +_install_target(RopeModule) +set_property(GLOBAL APPEND PROPERTY SWIFT_COLLECTIONS_EXPORTS RopeModule) diff --git a/Sources/RopeModule/Rope/Basics/Rope+Builder.swift b/Sources/RopeModule/Rope/Basics/Rope+Builder.swift new file mode 100644 index 000000000..5b1785b69 --- /dev/null +++ b/Sources/RopeModule/Rope/Basics/Rope+Builder.swift @@ -0,0 +1,483 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension Rope { + @inlinable + public init(_ items: some Sequence) { + if let items = items as? Self { + self = items + return + } + var builder = Builder() + for item in items { + builder.insertBeforeTip(item) + } + self = builder.finalize() + } +} + +extension Rope { + @frozen // Not really! This module isn't ABI stable. + public struct Builder { + // ║ ║ + // ║ ║ ║ ║ + // ║ ║ ║ ║ ║ ║ + // ║ ║ ║ ║ ║ ║ ║ ║ + // ──╨─╨─╨─╨──╨──╨──╨─╨─╨─╨── + // →prefixTrees→ ↑ ↑ ←suffixTrees← + // prefix suffix + + @usableFromInline internal var _prefixTrees: [Rope] = [] + @usableFromInline internal var _prefixLeaf: Rope._Node? + + @usableFromInline internal var _prefix: Rope._Item? + + @usableFromInline internal var _suffix: Rope._Item? + @usableFromInline internal var _suffixTrees: [Rope] = [] + + @inlinable + public init() {} + + @inlinable + public var isPrefixEmpty: Bool { + if _prefix != nil { return false } + if let leaf = self._prefixLeaf, !leaf.isEmpty { return false } + return _prefixTrees.isEmpty + } + + @inlinable + public var isSuffixEmpty: Bool { + if _suffix != nil { return false } + return _suffixTrees.isEmpty + } + + @inlinable + public var prefixSummary: Summary { + var sum = Summary.zero + for sapling in _prefixTrees { + sum.add(sapling.summary) + } + if let leaf = _prefixLeaf { sum.add(leaf.summary) } + if let item = _prefix { sum.add(item.summary) } + return sum + } + + @inlinable + public var suffixSummary: Summary { + var sum = Summary.zero + if let item = _suffix { sum.add(item.summary) } + for rope in _suffixTrees { + sum.add(rope.summary) + } + return sum + } + + @inlinable + var _lastPrefixItem: Rope._Item { + get { + assert(!isPrefixEmpty) + if let item = self._prefix { return item } + if let leaf = self._prefixLeaf { return leaf.lastItem } + return _prefixTrees.last!.root.lastItem + } + _modify { + assert(!isPrefixEmpty) + if _prefix != nil { + yield &_prefix! + } else if _prefixLeaf?.isEmpty == false { + yield &_prefixLeaf!.lastItem + } else { + yield &_prefixTrees[_prefixTrees.count - 1].root.lastItem + } + } + } + + @inlinable + var _firstSuffixItem: Rope._Item { + get { + assert(!isSuffixEmpty) + if let _suffix { return _suffix } + return _suffixTrees[_suffixTrees.count - 1].root.firstItem + } + _modify { + assert(!isSuffixEmpty) + if _suffix != nil { + yield &_suffix! + } else { + yield &_suffixTrees[_suffixTrees.count - 1].root.firstItem + } + } + } + + @inlinable + public func forEachElementInPrefix( + from position: Int, + in metric: some RopeMetric, + _ body: (Element, Element.Index?) -> Bool + ) -> Bool { + var position = position + var i = 0 + while i < _prefixTrees.count { + let size = metric.size(of: _prefixTrees[i].summary) + if position < size { break } + position -= size + i += 1 + } + if i < _prefixTrees.count { + guard _prefixTrees[i].forEachWhile(from: position, in: metric, body) else { return false } + i += 1 + while i < _prefixTrees.count { + guard _prefixTrees[i].forEachWhile({ body($0, nil) }) else { return false } + i += 1 + } + if let leaf = self._prefixLeaf { + guard leaf.forEachWhile({ body($0, nil) }) else { return false } + } + if let item = self._prefix { + guard body(item.value, nil) else { return false } + } + return true + } + + if let leaf = self._prefixLeaf { + let size = metric.size(of: leaf.summary) + if position < size { + guard leaf.forEachWhile(from: position, in: metric, body) else { return false } + if let item = self._prefix { + guard body(item.value, nil) else { return false } + } + return true + } + position -= size + } + if let item = self._prefix { + let i = metric.index(at: position, in: item.value) + guard body(item.value, i) else { return false} + } + return true + } + + @inlinable + public mutating func mutatingForEachSuffix( + _ body: (inout Element) -> R? + ) -> R? { + if self._suffix != nil, + let r = body(&self._suffix!.value) { + return r + } + for i in stride(from: _suffixTrees.count - 1, through: 0, by: -1) { + if let r = self._suffixTrees[i].mutatingForEach(body) { + return r + } + } + return nil + } + + @inlinable + public mutating func insertBeforeTip(_ item: __owned Element) { + _insertBeforeTip(Rope._Item(item)) + } + + @inlinable + mutating func _insertBeforeTip(_ item: __owned Rope._Item) { + guard !item.isEmpty else { return } + guard var prefix = self._prefix._take() else { + self._prefix = item + return + } + var item = item + if (prefix.isUndersized || item.isUndersized), prefix.rebalance(nextNeighbor: &item) { + self._prefix = prefix + return + } + _appendNow(prefix) + self._prefix = item + } + + @inlinable + mutating func _appendNow(_ item: __owned Rope._Item) { + assert(self._prefix == nil) + assert(!item.isUndersized) + var leaf = self._prefixLeaf._take() ?? .createLeaf() + leaf._appendItem(item) + if leaf.isFull { + self._appendNow(leaf) + } else { + self._prefixLeaf = leaf + } + _invariantCheck() + } + + @inlinable + public mutating func insertBeforeTip(_ rope: __owned Rope) { + guard rope._root != nil else { return } + _insertBeforeTip(rope.root) + } + + @inlinable + public mutating func insertBeforeTip(_ items: __owned some Sequence) { + if let items = _specialize(items, for: Rope.self) { + self.insertBeforeTip(items) + } else { + for item in items { + self.insertBeforeTip(item) + } + } + } + + @inlinable + mutating func _insertBeforeTip(_ node: __owned Rope._Node) { + defer { _invariantCheck() } + var node = node + if node.height == 0 { + if node.childCount == 1 { + _insertBeforeTip(node.firstItem) + return + } + if let item = self._prefix._take() { + if let spawn = node.prepend(item) { + _insertBeforeTip(node) + _insertBeforeTip(spawn) + return + } + } + if var leaf = self._prefixLeaf._take() { + if leaf.rebalance(nextNeighbor: &node), !leaf.isFull { + self._prefixLeaf = leaf + return + } + self._appendNow(leaf) + } + + if node.isFull { + self._appendNow(node) + } else { + self._prefixLeaf = node + } + return + } + + if var prefix = self._prefix._take() { + if !prefix.isUndersized || !node.firstItem.rebalance(prevNeighbor: &prefix) { + self._appendNow(prefix) + } + } + if let leaf = self._prefixLeaf._take() { + _appendNow(leaf) + } + _appendNow(node) + } + + @inlinable + mutating func _appendNow(_ rope: __owned Rope._Node) { + assert(self._prefix == nil && self._prefixLeaf == nil) + var new = rope + while !_prefixTrees.isEmpty { + // Join previous saplings together until they grow at least as deep as the new one. + var previous = _prefixTrees.removeLast() + while previous._height < new.height { + if _prefixTrees.isEmpty { + previous._append(new) + _prefixTrees.append(previous) + return + } + previous.prepend(_prefixTrees.removeLast()) + } + + if previous._height == new.height { + if previous.root.rebalance(nextNeighbor: &new) { + new = previous.root + } else { + new = .createInner(children: previous.root, new) + } + continue + } + + if new.isFull, !previous.root.isFull, previous._height == new.height + 1 { + // Graft node under the last sapling, as a new child branch. + previous.root._appendNode(new) + new = previous.root + continue + } + + // The new seedling can be appended to the line and we're done. + _prefixTrees.append(previous) + break + } + _prefixTrees.append(Rope(root: new)) + } + + @inlinable + public mutating func insertAfterTip(_ item: __owned Element) { + _insertAfterTip(_Item(item)) + } + + @inlinable + mutating func _insertAfterTip(_ item: __owned _Item) { + guard !item.isEmpty else { return } + if var suffixItem = self._suffix._take() { + var item = item + if !(suffixItem.isUndersized && item.rebalance(nextNeighbor: &suffixItem)) { + if _suffixTrees.isEmpty { + _suffixTrees.append(Rope(root: .createLeaf(suffixItem))) + } else { + _suffixTrees[_suffixTrees.count - 1].prepend(suffixItem.value) + } + } + } + self._suffix = item + } + + @inlinable + public mutating func insertAfterTip(_ rope: __owned Rope) { + assert(_suffix == nil) + assert(_suffixTrees.isEmpty || rope._height <= _suffixTrees.last!._height) + _suffixTrees.append(rope) + } + + @inlinable + mutating func _insertAfterTip(_ rope: __owned Rope._Node) { + insertAfterTip(Rope(root: rope)) + } + + @inlinable + mutating func _insertBeforeTip(slots: Range, in node: __owned Rope._Node) { + assert(slots.lowerBound >= 0 && slots.upperBound <= node.childCount) + let c = slots.count + guard c > 0 else { return } + if c == 1 { + if node.isLeaf { + let item = node.readLeaf { $0.children[slots.lowerBound] } + _insertBeforeTip(item) + } else { + let child = node.readInner { $0.children[slots.lowerBound] } + _insertBeforeTip(child) + } + return + } + let copy = node.copy(slots: slots) + _insertBeforeTip(copy) + } + + @inlinable + mutating func _insertAfterTip(slots: Range, in node: __owned Rope._Node) { + assert(slots.lowerBound >= 0 && slots.upperBound <= node.childCount) + let c = slots.count + guard c > 0 else { return } + if c == 1 { + if node.isLeaf { + let item = node.readLeaf { $0.children[slots.lowerBound] } + _insertAfterTip(item) + } else { + let child = node.readInner { $0.children[slots.lowerBound] } + _insertAfterTip(child) + } + return + } + let copy = node.copy(slots: slots) + _insertAfterTip(copy) + } + + @inlinable + public mutating func finalize() -> Rope { + // Integrate prefix & suffix chunks. + if let suffixItem = self._suffix._take() { + _insertBeforeTip(suffixItem) + } + if var prefix = self._prefix._take() { + if !prefix.isUndersized { + _appendNow(prefix) + } else if !self.isPrefixEmpty { + if !self._lastPrefixItem.rebalance(nextNeighbor: &prefix) { + _appendNow(prefix) + } + } else if !self.isSuffixEmpty { + if !self._firstSuffixItem.rebalance(prevNeighbor: &prefix) { + _appendNow(prefix) + } + } else { + // We only have the seed; allow undersized chunk + return Rope(root: .createLeaf(prefix)) + } + } + assert(self._prefix == nil && self._suffix == nil) + while let tree = _suffixTrees.popLast() { + insertBeforeTip(tree) + } + // Merge all saplings, the seedling and the seed into a single rope. + if let item = self._prefix._take() { + _appendNow(item) + } + var rope = Rope(root: _prefixLeaf._take()) + while let tree = _prefixTrees.popLast() { + rope.prepend(tree) + } + assert(_prefixLeaf == nil && _prefixTrees.isEmpty && _suffixTrees.isEmpty) + rope._invariantCheck() + return rope + } + + @inlinable + public func _invariantCheck() { +#if COLLECTIONS_INTERNAL_CHECKS + var h = UInt8.max + for sapling in _prefixTrees { + precondition(sapling._height <= h) + sapling._invariantCheck() + h = sapling._height + } + if let leaf = self._prefixLeaf { + precondition(leaf.height == 0) + precondition(!leaf.isFull) + leaf.invariantCheck(depth: 0, height: 0) + } + h = 0 + for tree in _suffixTrees.reversed() { + precondition(tree._height >= h) + tree._invariantCheck() + h = tree._height + } +#endif + + } + + public func _dump(heightLimit: Int = Int.max) { + for i in self._prefixTrees.indices { + print("Sapling \(i):") + self._prefixTrees[i]._dump(heightLimit: heightLimit, firstPrefix: " ", restPrefix: " ") + } + if let leaf = self._prefixLeaf { + print("Seedling:") + leaf.dump(heightLimit: heightLimit, firstPrefix: " ", restPrefix: " ") + } + if let item = self._prefix { + print("Seed:") + print(" \(item)") + } + print("---") + var i = 0 + if let item = self._suffix { + print("Suffix \(i):") + i += 1 + print(" \(item)") + } + for tree in self._suffixTrees.reversed() { + print("Suffix \(i)") + i += 1 + tree._dump(heightLimit: heightLimit, firstPrefix: " ", restPrefix: " ") + } + } + } +} diff --git a/Sources/RopeModule/Rope/Basics/Rope+Debugging.swift b/Sources/RopeModule/Rope/Basics/Rope+Debugging.swift new file mode 100644 index 000000000..dda00c994 --- /dev/null +++ b/Sources/RopeModule/Rope/Basics/Rope+Debugging.swift @@ -0,0 +1,94 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + +extension Rope._UnmanagedLeaf: CustomStringConvertible { + @usableFromInline + internal var description: String { + _addressString(for: _ref.toOpaque()) + } +} + +extension Rope { + @inlinable + public var _nodeCount: Int { + _root?.nodeCount ?? 0 + } +} + +extension Rope._Node { + @inlinable + internal var nodeCount: Int { + guard !isLeaf else { return 1 } + return readInner { $0.children.reduce(into: 1) { $0 += $1.nodeCount } } + } +} + +extension Rope { + public func _dump( + heightLimit: Int = .max, + firstPrefix: String = "", + restPrefix: String = "" + ) { + guard _root != nil else { + print("") + return + } + root.dump(heightLimit: heightLimit, firstPrefix: firstPrefix, restPrefix: restPrefix) + } +} + +extension Rope._Node: CustomStringConvertible { + @usableFromInline + internal var description: String { + """ + \(height > 0 ? "Inner@\(height)" : "Leaf")(\ + at: \(_addressString(for: object)), \ + summary: \(summary), \ + childCount: \(childCount)/\(Summary.maxNodeSize)) + """ + } +} + +extension Rope._Node { + @usableFromInline + internal func dump( + heightLimit: Int = .max, + firstPrefix: String = "", + restPrefix: String = "" + ) { + print("\(firstPrefix)\(description)") + + guard heightLimit > 0 else { return } + + if height > 0 { + readInner { + let c = $0.children + for slot in 0 ..< c.count { + c[slot].dump( + heightLimit: heightLimit - 1, + firstPrefix: "\(restPrefix)\(slot): ", + restPrefix: "\(restPrefix) ") + } + } + } else { + readLeaf { + let c = $0.children + for slot in 0 ..< c.count { + print("\(restPrefix)\(slot): \(c[slot])") + } + } + } + } +} diff --git a/Sources/RopeModule/Rope/Basics/Rope+Invariants.swift b/Sources/RopeModule/Rope/Basics/Rope+Invariants.swift new file mode 100644 index 000000000..1acb55d8e --- /dev/null +++ b/Sources/RopeModule/Rope/Basics/Rope+Invariants.swift @@ -0,0 +1,70 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 Rope { + @inlinable @inline(__always) + public func _invariantCheck() { +#if COLLECTIONS_INTERNAL_CHECKS + _root?.invariantCheck(depth: 0, height: root.height, recursive: true) +#endif + } +} + +extension Rope._Node { +#if COLLECTIONS_INTERNAL_CHECKS + @usableFromInline + internal func invariantCheck(depth: UInt8, height: UInt8, recursive: Bool = true) { + precondition(height == self.height, "Mismatching rope height") + if isLeaf { + precondition(self.childCount <= Summary.maxNodeSize, "Oversized leaf") + precondition(height == 0, "Leaf with height > 0") + precondition(depth == 0 || self.childCount >= Summary.minNodeSize, "Undersized leaf") + + let sum: Summary = readLeaf { + $0.children.reduce(into: .zero) { $0.add($1.summary) } + } + precondition(self.summary == sum, "Mismatching summary") + + guard recursive else { return } + readLeaf { leaf in + for child in leaf.children { + child.value.invariantCheck() + } + } + return + } + + precondition(self.childCount <= Summary.maxNodeSize, "Oversized node") + if depth == 0 { + precondition(self.childCount > 1, "Undersize root node") + } else { + precondition(self.childCount >= Summary.minNodeSize, "Undersized internal node") + } + + let sum: Summary = readInner { + $0.children.reduce(into: .zero) { $0.add($1.summary) } + } + precondition(self.summary == sum, "Mismatching summary") + + guard recursive else { return } + readInner { h in + for child in h.children { + child.invariantCheck(depth: depth + 1, height: height - 1, recursive: true) + } + } + } +#else + @inlinable @inline(__always) + internal func invariantCheck(depth: UInt8, height: UInt8, recursive: Bool = true) { + // Do nothing. + } +#endif +} diff --git a/Sources/RopeModule/Rope/Basics/Rope+_Node.swift b/Sources/RopeModule/Rope/Basics/Rope+_Node.swift new file mode 100644 index 000000000..fcea269fa --- /dev/null +++ b/Sources/RopeModule/Rope/Basics/Rope+_Node.swift @@ -0,0 +1,618 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if swift(<5.8) && !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities // for 5.8 polyfills +#endif + +extension Rope { + @frozen // Not really! This module isn't ABI stable. + @usableFromInline + internal struct _Node: _RopeItem { + @usableFromInline internal typealias Summary = Rope.Summary + @usableFromInline internal typealias Index = Rope.Index + @usableFromInline internal typealias _Item = Rope._Item + @usableFromInline internal typealias _Storage = Rope._Storage + @usableFromInline internal typealias _UnsafeHandle = Rope._UnsafeHandle + @usableFromInline internal typealias _Path = Rope._Path + @usableFromInline internal typealias _UnmanagedLeaf = Rope._UnmanagedLeaf + + @usableFromInline + internal var object: AnyObject + + @usableFromInline + internal var summary: Summary + + @inlinable + internal init(leaf: _Storage<_Item>, summary: Summary? = nil) { + self.object = leaf + self.summary = .zero + self.summary = readLeaf { handle in + handle.children.reduce(into: .zero) { $0.add($1.summary) } + } + } + + @inlinable + internal init(inner: _Storage<_Node>, summary: Summary? = nil) { + assert(inner.header.height > 0) + self.object = inner + self.summary = .zero + self.summary = readInner { handle in + handle.children.reduce(into: .zero) { $0.add($1.summary) } + } + } + } +} + +extension Rope._Node: @unchecked Sendable where Element: Sendable { + // @unchecked because `object` is stored as an `AnyObject` above. +} + +extension Rope._Node { + @inlinable + internal var _headerPtr: UnsafePointer<_RopeStorageHeader> { + let p = _getUnsafePointerToStoredProperties(object) + .assumingMemoryBound(to: _RopeStorageHeader.self) + return .init(p) + } + + @inlinable + internal var header: _RopeStorageHeader { + _headerPtr.pointee + } + + @inlinable @inline(__always) + internal var height: UInt8 { header.height } + + @inlinable @inline(__always) + internal var isLeaf: Bool { height == 0 } + + @inlinable @inline(__always) + internal var asLeaf: _Storage<_Item> { + assert(height == 0) + return unsafeDowncast(object, to: _Storage<_Item>.self) + } + + @inlinable @inline(__always) + internal var asInner: _Storage { + assert(height > 0) + return unsafeDowncast(object, to: _Storage.self) + } + + @inlinable @inline(__always) + internal var childCount: Int { header.childCount } + + @inlinable + internal var isEmpty: Bool { childCount == 0 } + + @inlinable + internal var isSingleton: Bool { isLeaf && childCount == 1 } + + @inlinable + internal var isUndersized: Bool { childCount < Summary.minNodeSize } + + @inlinable + internal var isFull: Bool { childCount == Summary.maxNodeSize } +} + +extension Rope._Node { + @inlinable + internal static func createLeaf() -> Self { + Self(leaf: .create(height: 0), summary: Summary.zero) + } + + @inlinable + internal static func createLeaf(_ item: __owned _Item) -> Self { + var leaf = createLeaf() + leaf._appendItem(item) + return leaf + } + + @inlinable + internal static func createInner(height: UInt8) -> Self { + Self(inner: .create(height: height), summary: .zero) + } + + @inlinable + internal static func createInner( + children left: __owned Self, _ right: __owned Self + ) -> Self { + assert(left.height == right.height) + var new = createInner(height: left.height + 1) + new.summary = left.summary + new.summary.add(right.summary) + new.updateInner { h in + h._appendChild(left) + h._appendChild(right) + } + return new + } +} + +extension Rope._Node { + @inlinable @inline(__always) + internal mutating func isUnique() -> Bool { + isKnownUniquelyReferenced(&object) + } + + @inlinable + internal mutating func ensureUnique() { + guard !isKnownUniquelyReferenced(&object) else { return } + self = copy() + } + + @inlinable @inline(never) + internal func copy() -> Self { + if isLeaf { + return Self(leaf: readLeaf { $0.copy() }, summary: self.summary) + } + return Self(inner: readInner { $0.copy() }, summary: self.summary) + } + + @inlinable @inline(never) + internal func copy(slots: Range) -> Self { + if isLeaf { + let (object, summary) = readLeaf { $0.copy(slots: slots) } + return Self(leaf: object, summary: summary) + } + let (object, summary) = readInner { $0.copy(slots: slots) } + return Self(inner: object, summary: summary) + } + + @inlinable @inline(__always) + internal func readLeaf( + _ body: (_UnsafeHandle<_Item>) -> R + ) -> R { + asLeaf.withUnsafeMutablePointers { h, p in + let handle = _UnsafeHandle(isMutable: false, header: h, start: p) + return body(handle) + } + } + + @inlinable @inline(__always) + internal mutating func updateLeaf( + _ body: (_UnsafeHandle<_Item>) -> R + ) -> R { + asLeaf.withUnsafeMutablePointers { h, p in + let handle = _UnsafeHandle(isMutable: true, header: h, start: p) + return body(handle) + } + } + + @inlinable @inline(__always) + internal func readInner( + _ body: (_UnsafeHandle) -> R + ) -> R { + asInner.withUnsafeMutablePointers { h, p in + let handle = _UnsafeHandle(isMutable: false, header: h, start: p) + return body(handle) + } + } + + @inlinable @inline(__always) + internal mutating func updateInner( + _ body: (_UnsafeHandle) -> R + ) -> R { + asInner.withUnsafeMutablePointers { h, p in + let handle = _UnsafeHandle(isMutable: true, header: h, start: p) + return body(handle) + } + } +} + +extension Rope._Node { + @inlinable + internal mutating func _insertItem(_ item: __owned _Item, at slot: Int) { + assert(isLeaf) + ensureUnique() + self.summary.add(item.summary) + updateLeaf { $0._insertChild(item, at: slot) } + } + + @inlinable + internal mutating func _insertNode(_ node: __owned Self, at slot: Int) { + assert(!isLeaf) + assert(self.height == node.height + 1) + ensureUnique() + self.summary.add(node.summary) + updateInner { $0._insertChild(node, at: slot) } + } +} + +extension Rope._Node { + @inlinable + internal mutating func _appendItem(_ item: __owned _Item) { + assert(isLeaf) + ensureUnique() + self.summary.add(item.summary) + updateLeaf { $0._appendChild(item) } + } + + @inlinable + internal mutating func _appendNode(_ node: __owned Self) { + assert(!isLeaf) + ensureUnique() + self.summary.add(node.summary) + updateInner { $0._appendChild(node) } + } +} + +extension Rope._Node { + @inlinable + internal mutating func _removeItem( + at slot: Int + ) -> (removed: _Item, delta: Summary) { + assert(isLeaf) + ensureUnique() + let item = updateLeaf { $0._removeChild(at: slot) } + let delta = item.summary + self.summary.subtract(delta) + return (item, delta) + } + + @inlinable + internal mutating func _removeNode(at slot: Int) -> Self { + assert(!isLeaf) + ensureUnique() + let result = updateInner { $0._removeChild(at: slot) } + self.summary.subtract(result.summary) + return result + } +} + +extension Rope._Node { + @inlinable + internal mutating func split(keeping desiredCount: Int) -> Self { + assert(desiredCount >= 0 && desiredCount <= childCount) + var new = isLeaf ? Self.createLeaf() : Self.createInner(height: height) + guard desiredCount < childCount else { return new } + guard desiredCount > 0 else { + swap(&self, &new) + return new + } + ensureUnique() + new.prependChildren(movingFromSuffixOf: &self, count: childCount - desiredCount) + assert(childCount == desiredCount) + return new + } +} + +extension Rope._Node { + @inlinable + internal mutating func rebalance(nextNeighbor right: inout Rope._Node) -> Bool { + assert(self.height == right.height) + if self.isEmpty { + swap(&self, &right) + return true + } + guard self.isUndersized || right.isUndersized else { return false } + let c = self.childCount + right.childCount + let desired = ( + c <= Summary.maxNodeSize ? c + : c / 2 >= Summary.minNodeSize ? c / 2 + : Summary.minNodeSize + ) + Self.redistributeChildren(&self, &right, to: desired) + return right.isEmpty + } + + @inlinable + internal mutating func rebalance(prevNeighbor left: inout Self) -> Bool { + guard left.rebalance(nextNeighbor: &self) else { return false } + swap(&self, &left) + return true + } + + /// Shift children between `left` and `right` such that the number of children in `left` + /// becomes `target`. + @inlinable + internal static func redistributeChildren( + _ left: inout Self, + _ right: inout Self, + to target: Int + ) { + assert(left.height == right.height) + assert(target >= 0 && target <= Summary.maxNodeSize) + left.ensureUnique() + right.ensureUnique() + + let lc = left.childCount + let rc = right.childCount + let target = Swift.min(target, lc + rc) + let d = target - lc + if d == 0 { return } + + if d > 0 { + left.appendChildren(movingFromPrefixOf: &right, count: d) + } else { + right.prependChildren(movingFromSuffixOf: &left, count: -d) + } + } + + @inlinable + internal mutating func appendChildren( + movingFromPrefixOf other: inout Self, count: Int + ) { + assert(self.height == other.height) + let delta: Summary + if isLeaf { + delta = self.updateLeaf { dst in + other.updateLeaf { src in + dst._appendChildren(movingFromPrefixOf: src, count: count) + } + } + } else { + delta = self.updateInner { dst in + other.updateInner { src in + dst._appendChildren(movingFromPrefixOf: src, count: count) + } + } + } + self.summary.add(delta) + other.summary.subtract(delta) + } + + @inlinable + internal mutating func prependChildren( + movingFromSuffixOf other: inout Self, count: Int + ) { + assert(self.height == other.height) + let delta: Summary + if isLeaf { + delta = self.updateLeaf { dst in + other.updateLeaf { src in + dst._prependChildren(movingFromSuffixOf: src, count: count) + } + } + } else { + delta = self.updateInner { dst in + other.updateInner { src in + dst._prependChildren(movingFromSuffixOf: src, count: count) + } + } + } + self.summary.add(delta) + other.summary.subtract(delta) + } +} + +extension Rope._Node { + @inlinable + internal var _startPath: _Path { + _Path(height: self.height) + } + + @inlinable + internal var lastPath: _Path { + var path = _Path(height: self.height) + _ = descendToLastItem(under: &path) + return path + } + + @inlinable + internal func isAtEnd(_ path: _Path) -> Bool { + path[self.height] == childCount + } + + @inlinable + internal func descendToFirstItem(under path: inout _Path) -> _UnmanagedLeaf { + path.clear(below: self.height + 1) + return unmanagedLeaf(at: path) + } + + @inlinable + internal func descendToLastItem(under path: inout _Path) -> _UnmanagedLeaf { + let h = self.height + let slot = self.childCount - 1 + path[h] = slot + if h > 0 { + return readInner { $0.children[slot].descendToLastItem(under: &path) } + } + return asUnmanagedLeaf + } +} + +extension Rope { + @inlinable + internal func _unmanagedLeaf(at path: _Path) -> _UnmanagedLeaf? { + assert(path.height == self._height) + guard path < _endPath else { return nil } + return root.unmanagedLeaf(at: path) + } +} + +extension Rope._Node { + @inlinable + internal var asUnmanagedLeaf: _UnmanagedLeaf { + assert(height == 0) + return _UnmanagedLeaf(unsafeDowncast(self.object, to: _Storage<_Item>.self)) + } + + @inlinable + internal func unmanagedLeaf(at path: _Path) -> _UnmanagedLeaf { + if height == 0 { + return asUnmanagedLeaf + } + let slot = path[height] + return readInner { $0.children[slot].unmanagedLeaf(at: path) } + } +} + +extension Rope._Node { + @inlinable + internal func formSuccessor(of i: inout Index) -> Bool { + let h = self.height + var slot = i._path[h] + if h == 0 { + slot &+= 1 + guard slot < childCount else { + return false + } + i._path[h] = slot + i._leaf = asUnmanagedLeaf + return true + } + return readInner { + let c = $0.children + if c[slot].formSuccessor(of: &i) { + return true + } + slot += 1 + guard slot < childCount else { + return false + } + i._path[h] = slot + i._leaf = c[slot].descendToFirstItem(under: &i._path) + return true + } + } + + @inlinable + internal func formPredecessor(of i: inout Index) -> Bool { + let h = self.height + var slot = i._path[h] + if h == 0 { + guard slot > 0 else { + return false + } + i._path[h] = slot &- 1 + i._leaf = asUnmanagedLeaf + return true + } + return readInner { + let c = $0.children + if slot < c.count, c[slot].formPredecessor(of: &i) { + return true + } + guard slot > 0 else { + return false + } + slot -= 1 + i._path[h] = slot + i._leaf = c[slot].descendToLastItem(under: &i._path) + return true + } + } +} + +extension Rope._Node { + @inlinable + internal var lastItem: _Item { + get { + self[lastPath] + } + _modify { + assert(childCount > 0) + var state = _prepareModifyLast() + defer { + _ = _finalizeModify(&state) + } + yield &state.item + } + } + + @inlinable + internal var firstItem: _Item { + get { + self[_startPath] + } + _modify { + yield &self[_startPath] + } + } + + @inlinable + internal subscript(path: _Path) -> _Item { + get { + let h = height + let slot = path[h] + precondition(slot < childCount, "Path out of bounds") + guard h == 0 else { + return readInner { $0.children[slot][path] } + } + return readLeaf { $0.children[slot] } + } + @inline(__always) + _modify { + var state = _prepareModify(at: path) + defer { + _ = _finalizeModify(&state) + } + yield &state.item + } + } + + @frozen // Not really! This module isn't ABI stable. + @usableFromInline + internal struct _ModifyState { + @usableFromInline internal var path: _Path + @usableFromInline internal var item: _Item + @usableFromInline internal var summary: Summary + + @inlinable + internal init(path: _Path, item: _Item, summary: Summary) { + self.path = path + self.item = item + self.summary = summary + } + } + + @inlinable + internal mutating func _prepareModify(at path: _Path) -> _ModifyState { + ensureUnique() + let h = height + let slot = path[h] + precondition(slot < childCount, "Path out of bounds") + guard h == 0 else { + return updateInner { $0.mutableChildren[slot]._prepareModify(at: path) } + } + let item = updateLeaf { $0.mutableChildren.moveElement(from: slot) } + return _ModifyState(path: path, item: item, summary: item.summary) + } + + @inlinable + internal mutating func _prepareModifyLast() -> _ModifyState { + var path = _Path(height: height) + return _prepareModifyLast(&path) + } + + @inlinable + internal mutating func _prepareModifyLast(_ path: inout _Path) -> _ModifyState { + ensureUnique() + let h = height + let slot = self.childCount - 1 + path[h] = slot + guard h == 0 else { + return updateInner { $0.mutableChildren[slot]._prepareModifyLast(&path) } + } + let item = updateLeaf { $0.mutableChildren.moveElement(from: slot) } + return _ModifyState(path: path, item: item, summary: item.summary) + } + + @inlinable + internal mutating func _finalizeModify( + _ state: inout _ModifyState + ) -> (delta: Summary, leaf: _UnmanagedLeaf) { + assert(isUnique()) + let h = height + let slot = state.path[h] + assert(slot < childCount, "Path out of bounds") + guard h == 0 else { + let r = updateInner { $0.mutableChildren[slot]._finalizeModify(&state) } + summary.add(r.delta) + return r + } + let delta = state.item.summary.subtracting(state.summary) + updateLeaf { $0.mutableChildren.initializeElement(at: slot, to: state.item) } + summary.add(delta) + return (delta, asUnmanagedLeaf) + } +} diff --git a/Sources/RopeModule/Rope/Basics/Rope+_Storage.swift b/Sources/RopeModule/Rope/Basics/Rope+_Storage.swift new file mode 100644 index 000000000..7a7773cc8 --- /dev/null +++ b/Sources/RopeModule/Rope/Basics/Rope+_Storage.swift @@ -0,0 +1,58 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +@usableFromInline +@frozen // Not really! This module isn't ABI stable. +internal struct _RopeStorageHeader { + @usableFromInline var _childCount: UInt16 + @usableFromInline let height: UInt8 + + @inlinable + internal init(height: UInt8) { + self._childCount = 0 + self.height = height + } + + @inlinable + internal var childCount: Int { + get { + numericCast(_childCount) + } + set { + _childCount = numericCast(newValue) + } + } +} + +extension Rope { + @usableFromInline + @_fixed_layout // Not really! This module isn't ABI stable. + internal final class _Storage>: + ManagedBuffer<_RopeStorageHeader, Child> + { + @usableFromInline internal typealias Summary = Element.Summary + @usableFromInline internal typealias _UnsafeHandle = Rope._UnsafeHandle + + @inlinable + internal static func create(height: UInt8) -> _Storage { + let object = create(minimumCapacity: Summary.maxNodeSize) { _ in .init(height: height) } + return unsafeDowncast(object, to: _Storage.self) + } + + @inlinable + deinit { + withUnsafeMutablePointers { h, p in + p.deinitialize(count: h.pointee.childCount) + h.pointee._childCount = .max + } + } + } +} diff --git a/Sources/RopeModule/Rope/Basics/Rope+_UnmanagedLeaf.swift b/Sources/RopeModule/Rope/Basics/Rope+_UnmanagedLeaf.swift new file mode 100644 index 000000000..9523ab090 --- /dev/null +++ b/Sources/RopeModule/Rope/Basics/Rope+_UnmanagedLeaf.swift @@ -0,0 +1,48 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 Rope { + @frozen // Not really! This module isn't ABI stable. + @usableFromInline + internal struct _UnmanagedLeaf { + @usableFromInline internal typealias _Item = Rope._Item + @usableFromInline internal typealias _Leaf = _Storage<_Item> + @usableFromInline internal typealias _UnsafeHandle = Rope._UnsafeHandle + + @usableFromInline var _ref: Unmanaged<_Leaf> + + @inlinable + internal init(_ leaf: __shared _Leaf) { + _ref = .passUnretained(leaf) + } + } +} + +extension Rope._UnmanagedLeaf: Equatable { + @inlinable + internal static func ==(left: Self, right: Self) -> Bool { + left._ref.toOpaque() == right._ref.toOpaque() + } +} + +extension Rope._UnmanagedLeaf { + @inlinable + internal func read( + body: (_UnsafeHandle<_Item>) -> R + ) -> R { + _ref._withUnsafeGuaranteedRef { leaf in + leaf.withUnsafeMutablePointers { h, p in + let handle = _UnsafeHandle(isMutable: false, header: h, start: p) + return body(handle) + } + } + } +} diff --git a/Sources/RopeModule/Rope/Basics/Rope+_UnsafeHandle.swift b/Sources/RopeModule/Rope/Basics/Rope+_UnsafeHandle.swift new file mode 100644 index 000000000..f14db0038 --- /dev/null +++ b/Sources/RopeModule/Rope/Basics/Rope+_UnsafeHandle.swift @@ -0,0 +1,229 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 Rope { + @usableFromInline + @frozen // Not really! This module isn't ABI stable. + internal struct _UnsafeHandle> { + @usableFromInline internal typealias Summary = Rope.Summary + + @usableFromInline + internal let _header: UnsafeMutablePointer<_RopeStorageHeader> + + @usableFromInline + internal let _start: UnsafeMutablePointer +#if DEBUG + @usableFromInline + internal let _isMutable: Bool +#endif + + @inlinable + internal init( + isMutable: Bool, + header: UnsafeMutablePointer<_RopeStorageHeader>, + start: UnsafeMutablePointer + ) { + self._header = header + self._start = start +#if DEBUG + self._isMutable = isMutable +#endif + } + + @inlinable @inline(__always) + internal func assertMutable() { +#if DEBUG + assert(_isMutable) +#endif + } + } +} + +extension Rope._UnsafeHandle { + @inlinable @inline(__always) + internal var capacity: Int { Summary.maxNodeSize } + + @inlinable @inline(__always) + internal var height: UInt8 { _header.pointee.height } + + @inlinable + internal var childCount: Int { + get { _header.pointee.childCount } + nonmutating set { + assertMutable() + _header.pointee.childCount = newValue + } + } + + @inlinable + internal var children: UnsafeBufferPointer { + UnsafeBufferPointer(start: _start, count: childCount) + } + + @inlinable + internal func child(at slot: Int) -> Child? { + assert(slot >= 0) + guard slot < childCount else { return nil } + return (_start + slot).pointee + } + + @inlinable + internal var mutableChildren: UnsafeMutableBufferPointer { + assertMutable() + return UnsafeMutableBufferPointer(start: _start, count: childCount) + } + + @inlinable + internal func mutableChildPtr(at slot: Int) -> UnsafeMutablePointer { + assertMutable() + assert(slot >= 0 && slot < childCount) + return _start + slot + } + + @inlinable + internal var mutableBuffer: UnsafeMutableBufferPointer { + assertMutable() + return UnsafeMutableBufferPointer(start: _start, count: capacity) + } + + @inlinable + internal func copy() -> Rope._Storage { + let new = Rope._Storage.create(height: self.height) + let c = self.childCount + new.header.childCount = c + new.withUnsafeMutablePointerToElements { target in + target.initialize(from: self._start, count: c) + } + return new + } + + @inlinable + internal func copy( + slots: Range + ) -> (object: Rope._Storage, summary: Summary) { + assert(slots.lowerBound >= 0 && slots.upperBound <= childCount) + let object = Rope._Storage.create(height: self.height) + let c = slots.count + let summary = object.withUnsafeMutablePointers { h, p in + h.pointee.childCount = c + p.initialize(from: self._start + slots.lowerBound, count: slots.count) + return UnsafeBufferPointer(start: p, count: c)._sum() + } + return (object, summary) + } + + @inlinable + internal func _insertChild(_ child: __owned Child, at slot: Int) { + assertMutable() + assert(childCount < capacity) + assert(slot >= 0 && slot <= childCount) + (_start + slot + 1).moveInitialize(from: _start + slot, count: childCount - slot) + (_start + slot).initialize(to: child) + childCount += 1 + } + + @inlinable + internal func _appendChild(_ child: __owned Child) { + assertMutable() + assert(childCount < capacity) + (_start + childCount).initialize(to: child) + childCount += 1 + } + + @inlinable + internal func _removeChild(at slot: Int) -> Child { + assertMutable() + assert(slot >= 0 && slot < childCount) + let result = (_start + slot).move() + (_start + slot).moveInitialize(from: _start + slot + 1, count: childCount - slot - 1) + childCount -= 1 + return result + } + + @inlinable + internal func _removePrefix(_ n: Int) -> Summary { + assertMutable() + assert(n <= childCount) + var delta = Summary.zero + let c = mutableChildren + for i in 0 ..< n { + let child = c.moveElement(from: i) + delta.add(child.summary) + } + childCount -= n + _start.moveInitialize(from: _start + n, count: childCount) + return delta + } + + @inlinable + internal func _removeSuffix(_ n: Int) -> Summary { + assertMutable() + assert(n <= childCount) + var delta = Summary.zero + let c = mutableChildren + for i in childCount - n ..< childCount { + let child = c.moveElement(from: i) + delta.add(child.summary) + } + childCount -= n + return delta + } + + @inlinable + internal func _appendChildren( + movingFromPrefixOf src: Self, count: Int + ) -> Summary { + assertMutable() + src.assertMutable() + assert(self.height == src.height) + guard count > 0 else { return .zero } + assert(count >= 0 && count <= src.childCount) + assert(count <= capacity - self.childCount) + + (_start + childCount).moveInitialize(from: src._start, count: count) + src._start.moveInitialize(from: src._start + count, count: src.childCount - count) + childCount += count + src.childCount -= count + return children.suffix(count)._sum() + } + + @inlinable + internal func _prependChildren( + movingFromSuffixOf src: Self, count: Int + ) -> Summary { + assertMutable() + src.assertMutable() + assert(self.height == src.height) + guard count > 0 else { return .zero } + assert(count >= 0 && count <= src.childCount) + assert(count <= capacity - childCount) + + (_start + count).moveInitialize(from: _start, count: childCount) + _start.moveInitialize(from: src._start + src.childCount - count, count: count) + childCount += count + src.childCount -= count + return children.prefix(count)._sum() + } + + @inlinable + internal func distance( + from start: Int, to end: Int, in metric: some RopeMetric + ) -> Int { + if start <= end { + return children[start ..< end].reduce(into: 0) { + $0 += metric._nonnegativeSize(of: $1.summary) + } + } + return -children[end ..< start].reduce(into: 0) { + $0 += metric._nonnegativeSize(of: $1.summary) + } + } +} diff --git a/Sources/RopeModule/Rope/Basics/Rope.swift b/Sources/RopeModule/Rope/Basics/Rope.swift new file mode 100644 index 000000000..b6039b567 --- /dev/null +++ b/Sources/RopeModule/Rope/Basics/Rope.swift @@ -0,0 +1,71 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +/// An ordered data structure of `Element` values that organizes itself into a tree. +/// The rope is augmented by the commutative group specified by `Element.Summary`, enabling +/// quick lookup operations. +@frozen // Not really! This module isn't ABI stable. +public struct Rope { + @usableFromInline + internal var _root: _Node? + + @usableFromInline + internal var _version: _RopeVersion + + @inlinable + public init() { + self._root = nil + self._version = _RopeVersion() + } + + @inlinable + internal init(root: _Node?) { + self._root = root + self._version = _RopeVersion() + } + + @inlinable + internal var root: _Node { + @inline(__always) get { _root.unsafelyUnwrapped } + @inline(__always) _modify { yield &_root! } + } + + @inlinable + public init(_ value: Element) { + self._root = .createLeaf(_Item(value)) + self._version = _RopeVersion() + } +} + +extension Rope: Sendable where Element: Sendable {} + +extension Rope { + @inlinable + internal mutating func _ensureUnique() { + guard _root != nil else { return } + root.ensureUnique() + } +} + +extension Rope { + @inlinable + public var isSingleton: Bool { + guard _root != nil else { return false } + return root.isSingleton + } +} + +extension Rope { + @inlinable + public func isIdentical(to other: Self) -> Bool { + self._root?.object === other._root?.object + } +} diff --git a/Sources/RopeModule/Rope/Basics/RopeElement.swift b/Sources/RopeModule/Rope/Basics/RopeElement.swift new file mode 100644 index 000000000..aeb9c22fb --- /dev/null +++ b/Sources/RopeModule/Rope/Basics/RopeElement.swift @@ -0,0 +1,77 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +/// The element type of a rope. Rope elements are expected to be container types +/// of their own, with logical positions within them addressed by an `Index` +/// type, similar to `Collection` indices. +/// +/// However, rope elements aren't required conform to `Collection`. In fact, +/// they often support multiple different ways to interpret/project their +/// contents, similar to `String`'s views. In some cases, they may just be +/// individual, undivisable items of varying sizes -- although it's usually +/// a better to use a simple fixed-size collection type instead. +/// +/// Each such projection may come with a different idea for how large a rope +/// element is -- this is modeled by the `RopeSummary` and `RopeMetric` +/// protocols. The `summary` property returns the size of the element as an +/// additive value, which can be projected to integer sizes using one of the +/// associated rope metrics. +public protocol RopeElement { + /// The commutative group that is used to augment the tree. + associatedtype Summary: RopeSummary + + /// A type whose values address a particular pieces of content in this rope + /// element. + associatedtype Index: Comparable + + /// Returns the summary of `self`. + var summary: Summary { get } + + var isEmpty: Bool { get } + var isUndersized: Bool { get } + + /// Check the consistency of `self`. + func invariantCheck() + + /// Rebalance contents between `self` and its next neighbor `right`, + /// eliminating an `isUndersized` condition on one of the inputs, if possible. + /// + /// On return, `self` is expected to be non-empty and well-sized. + /// + /// - Parameter right: The element immediately following `self` in some rope. + /// - Precondition: Either `self` or `right` must be undersized. + /// - Returns: A boolean value indicating whether `right` has become empty. + mutating func rebalance(nextNeighbor right: inout Self) -> Bool + + /// Rebalance contents between `self` and its previous neighbor `left`, + /// eliminating an `isUndersized` condition on one of the inputs, if possible. + /// + /// On return, `self` is expected to be non-empty and well-sized. + /// + /// - Parameter left: The element immediately preceding `self` in some rope. + /// - Precondition: Either `left` or `self` must be undersized. + /// - Returns: A boolean value indicating whether `left` has become empty. + mutating func rebalance(prevNeighbor left: inout Self) -> Bool + + /// Split `self` into two pieces at the specified index, keeping contents + /// up to `index` in `self`, and moving the rest of it into a new item. + mutating func split(at index: Index) -> Self +} + +extension RopeElement { + @inlinable + public mutating func rebalance(prevNeighbor left: inout Self) -> Bool { + guard left.rebalance(nextNeighbor: &self) else { return false } + swap(&self, &left) + return true + } +} + diff --git a/Sources/RopeModule/Rope/Basics/RopeMetric.swift b/Sources/RopeModule/Rope/Basics/RopeMetric.swift new file mode 100644 index 000000000..c39e1b923 --- /dev/null +++ b/Sources/RopeModule/Rope/Basics/RopeMetric.swift @@ -0,0 +1,35 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +public protocol RopeMetric: Sendable { + associatedtype Element: RopeElement + + /// Returns the size of a summarized rope element in this metric. + func size(of summary: Element.Summary) -> Int + + /// Returns an index addressing the content at the given offset from + /// the start of the specified rope element. + /// + /// - Parameter offset: An integer offset from the start of `element` in this + /// metric, not exceeding `size(of: element.summary)`. + /// - Parameter element: An arbitrary rope element. + /// - Returns: The index addressing the desired position in the input element. + func index(at offset: Int, in element: Element) -> Element.Index +} + +extension RopeMetric { + @inlinable @inline(__always) + internal func _nonnegativeSize(of summary: Element.Summary) -> Int { + let r = size(of: summary) + assert(r >= 0) + return r + } +} diff --git a/Sources/RopeModule/Rope/Basics/RopeSummary.swift b/Sources/RopeModule/Rope/Basics/RopeSummary.swift new file mode 100644 index 000000000..298b1a45f --- /dev/null +++ b/Sources/RopeModule/Rope/Basics/RopeSummary.swift @@ -0,0 +1,62 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +/// A commutative group that is used to augment a tree, enabling quick lookup operations. +public protocol RopeSummary: Equatable, Sendable { + static var maxNodeSize: Int { get } + static var nodeSizeBitWidth: Int { get } + + /// The identity element of the group. + static var zero: Self { get } + + /// Returns a Boolean value that indicates whether `self` is the identity element. + var isZero: Bool { get } + + /// A commutative and associative operation that combines two instances. + /// + /// (As is usually the case, this operation is not necessarily closed over `Self` in practice -- + /// e.g., some results may not be representable.) + mutating func add(_ other: Self) + + /// A (potentially partial) subtraction function that undoes a previous combination of the given + /// element to `self`. + /// + /// The inverse of any instance can be calculated by subtracting it from the `zero` instance. + /// (However, conforming types are free to require that `subtract` only be called on values + /// that "include" the given `other`.) + mutating func subtract(_ other: Self) +} + +extension RopeSummary { + @inlinable @inline(__always) + public static var nodeSizeBitWidth: Int { + Int.bitWidth - maxNodeSize.leadingZeroBitCount + } + + @inlinable @inline(__always) + public static var minNodeSize: Int { (maxNodeSize + 1) / 2 } +} + +extension RopeSummary { + @inlinable + public func adding(_ other: Self) -> Self { + var c = self + c.add(other) + return c + } + + @inlinable + public func subtracting(_ other: Self) -> Self { + var c = self + c.subtract(other) + return c + } +} diff --git a/Sources/RopeModule/Rope/Basics/_RopeItem.swift b/Sources/RopeModule/Rope/Basics/_RopeItem.swift new file mode 100644 index 000000000..53c025111 --- /dev/null +++ b/Sources/RopeModule/Rope/Basics/_RopeItem.swift @@ -0,0 +1,91 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +/// An internal protocol describing a summarizable entity that isn't a full `RopeElement`. +/// +/// Used as an implementation detail to increase code reuse across internal nodes and leaf nodes. +/// (Ideally `Rope._Node` would just conform to the full `RopeElement` protocol on its own, but +/// while that's an obvious refactoring idea, it hasn't happened yet.) +@usableFromInline +internal protocol _RopeItem { + associatedtype Summary: RopeSummary + + var summary: Summary { get } +} + +extension Sequence where Element: _RopeItem { + @inlinable + internal func _sum() -> Element.Summary { + self.reduce(into: .zero) { $0.add($1.summary) } + } +} + +extension Rope: _RopeItem { + public typealias Summary = Element.Summary + + @inlinable + public var summary: Summary { + guard _root != nil else { return .zero } + return root.summary + } +} + +extension Rope { + /// A trivial wrapper around a rope's Element type, giving it `_RopeItem` conformance without + /// having to make the protocol public. + @usableFromInline + @frozen // Not really! This module isn't ABI stable. + internal struct _Item { + @usableFromInline internal var value: Element + + @inlinable + internal init(_ value: Element) { self.value = value } + } +} + +extension Rope._Item: _RopeItem { + @usableFromInline internal typealias Summary = Rope.Summary + + @inlinable + internal var summary: Summary { value.summary } +} + +extension Rope._Item: CustomStringConvertible { + @usableFromInline + internal var description: String { + "\(value)" + } +} + +extension Rope._Item { + @inlinable + internal var isEmpty: Bool { value.isEmpty } + + @inlinable + internal var isUndersized: Bool { value.isUndersized } + + @inlinable + internal mutating func rebalance(nextNeighbor right: inout Self) -> Bool { + value.rebalance(nextNeighbor: &right.value) + } + + @inlinable + internal mutating func rebalance(prevNeighbor left: inout Self) -> Bool { + value.rebalance(prevNeighbor: &left.value) + } + + @usableFromInline internal typealias Index = Element.Index + + @inlinable + internal mutating func split(at index: Index) -> Self { + Self(self.value.split(at: index)) + } +} diff --git a/Sources/RopeModule/Rope/Basics/_RopePath.swift b/Sources/RopeModule/Rope/Basics/_RopePath.swift new file mode 100644 index 000000000..b5e4b1c4c --- /dev/null +++ b/Sources/RopeModule/Rope/Basics/_RopePath.swift @@ -0,0 +1,123 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +@usableFromInline +@frozen // Not really! This module isn't ABI stable +internal struct _RopePath { + // ┌──────────────────────────────────┬────────┐ + // │ b63:b8 │ b7:b0 │ + // ├──────────────────────────────────┼────────┤ + // │ path │ height │ + // └──────────────────────────────────┴────────┘ + @usableFromInline internal var _value: UInt64 + + @inlinable @inline(__always) + internal static var _pathBitWidth: Int { 56 } + + @inlinable + internal init(_value: UInt64) { + self._value = _value + } + + @inlinable + internal init(height: UInt8) { + self._value = UInt64(truncatingIfNeeded: height) + assert((Int(height) + 1) * Summary.nodeSizeBitWidth <= Self._pathBitWidth) + } +} + +extension Rope { + @usableFromInline internal typealias _Path = _RopePath +} + +extension _RopePath: Equatable { + @inlinable + internal static func ==(left: Self, right: Self) -> Bool { + left._value == right._value + } +} +extension _RopePath: Hashable { + @inlinable + internal func hash(into hasher: inout Hasher) { + hasher.combine(_value) + } +} + +extension _RopePath: Comparable { + @inlinable + internal static func <(left: Self, right: Self) -> Bool { + left._value < right._value + } +} + +extension _RopePath: CustomStringConvertible { + @usableFromInline + internal var description: String { + var r = "<" + for h in stride(from: height, through: 0, by: -1) { + r += "\(self[h])" + if h > 0 { r += ", " } + } + r += ">" + return r + } +} + +extension _RopePath { + @inlinable + internal var height: UInt8 { + UInt8(truncatingIfNeeded: _value) + } + + @inlinable + internal mutating func popRoot() { + let heightMask: UInt64 = 255 + let h = height + assert(h > 0 && self[h] == 0) + _value &= ~heightMask + _value |= UInt64(truncatingIfNeeded: h - 1) & heightMask + } + + @inlinable + internal subscript(height: UInt8) -> Int { + get { + assert(height <= self.height) + let shift = 8 + Int(height) * Summary.nodeSizeBitWidth + let mask: UInt64 = (1 &<< Summary.nodeSizeBitWidth) &- 1 + return numericCast((_value &>> shift) & mask) + } + set { + assert(height <= self.height) + assert(newValue >= 0 && newValue <= Summary.maxNodeSize) + let shift = 8 + Int(height) * Summary.nodeSizeBitWidth + let mask: UInt64 = (1 &<< Summary.nodeSizeBitWidth) &- 1 + _value &= ~(mask &<< shift) + _value |= numericCast(newValue) &<< shift + } + } + + @inlinable + internal func isEmpty(below height: UInt8) -> Bool { + let shift = Int(height) * Summary.nodeSizeBitWidth + assert(shift + Summary.nodeSizeBitWidth <= Self._pathBitWidth) + let mask: UInt64 = ((1 &<< shift) - 1) &<< 8 + return (_value & mask) == 0 + } + + @inlinable + internal mutating func clear(below height: UInt8) { + let shift = Int(height) * Summary.nodeSizeBitWidth + assert(shift + Summary.nodeSizeBitWidth <= Self._pathBitWidth) + let mask: UInt64 = ((1 &<< shift) - 1) &<< 8 + _value &= ~mask + } +} + diff --git a/Sources/RopeModule/Rope/Basics/_RopeVersion.swift b/Sources/RopeModule/Rope/Basics/_RopeVersion.swift new file mode 100644 index 000000000..30c4ccc3c --- /dev/null +++ b/Sources/RopeModule/Rope/Basics/_RopeVersion.swift @@ -0,0 +1,48 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +@usableFromInline +@frozen // Not really! This module isn't ABI stable. +internal struct _RopeVersion { + // FIXME: Replace this probabilistic mess with atomics when Swift gets its act together. + @usableFromInline internal var _value: UInt + + @inlinable + internal init() { + var rng = SystemRandomNumberGenerator() + _value = rng.next() + } + + @inlinable + internal init(_ value: UInt) { + self._value = value + } +} + +extension _RopeVersion: Equatable { + @inlinable + internal static func ==(left: Self, right: Self) -> Bool { + left._value == right._value + } +} + +extension _RopeVersion { + @inlinable + internal mutating func bump() { + _value &+= 1 + } + + @inlinable + internal mutating func reset() { + var rng = SystemRandomNumberGenerator() + _value = rng.next() + } +} diff --git a/Sources/RopeModule/Rope/Conformances/Rope+Collection.swift b/Sources/RopeModule/Rope/Conformances/Rope+Collection.swift new file mode 100644 index 000000000..2414baed9 --- /dev/null +++ b/Sources/RopeModule/Rope/Conformances/Rope+Collection.swift @@ -0,0 +1,493 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 Rope { + @inlinable + public func isValid(_ index: Index) -> Bool { + index._version == _version + } + + @inlinable + public func validate(_ index: Index) { + precondition(isValid(index), "Invalid index") + } + + @inlinable + internal mutating func _invalidateIndices() { + _version.bump() + } + + /// Validate `index` and fill out all cached information in it, + /// to speed up subsequent lookups. + @inlinable + public func grease(_ index: inout Index) { + validate(index) + guard index._leaf == nil else { return } + index._leaf = _unmanagedLeaf(at: index._path) + } +} + +extension Rope { + @inlinable + public var _height: UInt8 { + _root?.height ?? 0 + } + + @inlinable + internal var _startPath: _Path { + _Path(height: _height) + } + + @inlinable + internal var _endPath: _Path { + guard let root = _root else { return _startPath } + var path = _Path(height: _height) + path[_height] = root.childCount + return path + } +} + +extension Rope: BidirectionalCollection { + public typealias SubSequence = Slice + + @inlinable + public var isEmpty: Bool { + guard _root != nil else { return true } + return root.childCount == 0 + } + + @inlinable + public var startIndex: Index { + // Note: `leaf` is intentionally not set here, to speed up accessing this property. + return Index(version: _version, path: _startPath, leaf: nil) + } + + @inlinable + public var endIndex: Index { + Index(version: _version, path: _endPath, leaf: nil) + } + + @inlinable + public func index(after i: Index) -> Index { + var i = i + formIndex(after: &i) + return i + } + + @inlinable + public func index(before i: Index) -> Index { + var i = i + formIndex(before: &i) + return i + } + + @inlinable + public func formIndex(after i: inout Index) { + validate(i) + precondition(i < endIndex, "Can't move after endIndex") + if let leaf = i._leaf { + let done = leaf.read { + let slot = i._path[$0.height] &+ 1 + guard slot < $0.childCount else { return false } + i._path[$0.height] = slot + return true + } + if done { return } + } + if !root.formSuccessor(of: &i) { + i = endIndex + } + } + + @inlinable + public func formIndex(before i: inout Index) { + validate(i) + precondition(i > startIndex, "Can't move before startIndex") + if let leaf = i._leaf { + let done = leaf.read { + let slot = i._path[$0.height] + guard slot > 0 else { return false } + i._path[$0.height] = slot &- 1 + return true + } + if done { return } + } + let success = root.formPredecessor(of: &i) + precondition(success, "Invalid index") + } + + @inlinable + public subscript(i: Index) -> Element { + get { + validate(i) + if let ref = i._leaf { + return ref.read { + $0.children[i._path[$0.height]].value + } + } + return root[i._path].value + } + @inline(__always) _modify { + validate(i) + // Note: we must not use _leaf -- it may not be on a unique path. + defer { _invalidateIndices() } + yield &root[i._path].value + } + } +} + +extension Rope { + /// Update the element at the given index, while keeping the index valid. + @inlinable + public mutating func update( + at index: inout Index, + by body: (inout Element) -> R + ) -> R { + validate(index) + var state = root._prepareModify(at: index._path) + defer { + _invalidateIndices() + index._version = self._version + index._leaf = root._finalizeModify(&state).leaf + } + return body(&state.item.value) + } +} + +extension Rope { + @inlinable @inline(__always) + public static var _maxHeight: Int { + _Path._pathBitWidth / Summary.nodeSizeBitWidth + } + + /// The estimated maximum number of items that can fit in this rope in the worst possible case, + /// i.e., when the tree consists of minimum-sized nodes. (The data structure itself has no + /// inherent limit, but this implementation of it is limited by the fixed 56-bit path + /// representation used in the `Index` type.) + /// + /// This is one less than the minimum possible size for a rope whose size exceeds the maximum. + @inlinable + public static var _minimumCapacity: Int { + var c = 2 + for _ in 0 ..< _maxHeight { + let (r, overflow) = c.multipliedReportingOverflow(by: Summary.minNodeSize) + if overflow { return .max } + c = r + } + return c - 1 + } + + /// The maximum number of items that can fit in this rope in the best possible case, i.e., when + /// the tree consists of maximum-sized nodes. (The data structure itself has no inherent limit, + /// but this implementation of it is limited by the fixed 56-bit path representation used in + /// the `Index` type.) + @inlinable + public static var _maximumCapacity: Int { + var c = 1 + for _ in 0 ... _maxHeight { + let (r, overflow) = c.multipliedReportingOverflow(by: Summary.maxNodeSize) + if overflow { return .max } + c = r + } + return c + } +} + +extension Rope { + @inlinable + public func count(in metric: some RopeMetric) -> Int { + guard _root != nil else { return 0 } + return root.count(in: metric) + } +} + +extension Rope._Node { + @inlinable + public func count(in metric: some RopeMetric) -> Int { + metric._nonnegativeSize(of: self.summary) + } +} + +extension Rope { + @inlinable + public func distance(from start: Index, to end: Index, in metric: some RopeMetric) -> Int { + validate(start) + validate(end) + if start == end { return 0 } + precondition(_root != nil, "Invalid index") + if start._leaf == end._leaf, let leaf = start._leaf { + // Fast path: both indices are pointing within the same leaf. + return leaf.read { + let h = $0.height + let a = start._path[h] + let b = end._path[h] + return $0.distance(from: a, to: b, in: metric) + } + } + if start < end { + return root.distance(from: start, to: end, in: metric) + } + return -root.distance(from: end, to: start, in: metric) + } + + @inlinable + public func offset(of index: Index, in metric: some RopeMetric) -> Int { + validate(index) + if _root == nil { return 0 } + return root.distanceFromStart(to: index, in: metric) + } +} + +extension Rope._Node { + @inlinable + internal func distanceFromStart( + to index: Index, in metric: some RopeMetric + ) -> Int { + let slot = index._path[height] + precondition(slot <= childCount, "Invalid index") + if slot == childCount { + precondition(index._isEmpty(below: height), "Invalid index") + return metric._nonnegativeSize(of: self.summary) + } + if height == 0 { + return readLeaf { $0.distance(from: 0, to: slot, in: metric) } + } + return readInner { + var distance = $0.distance(from: 0, to: slot, in: metric) + distance += $0.children[slot].distanceFromStart(to: index, in: metric) + return distance + } + } + + @inlinable + internal func distanceToEnd( + from index: Index, in metric: some RopeMetric + ) -> Int { + let d = metric._nonnegativeSize(of: self.summary) - self.distanceFromStart(to: index, in: metric) + assert(d >= 0) + return d + } + + @inlinable + internal func distance( + from start: Index, to end: Index, in metric: some RopeMetric + ) -> Int { + assert(start < end) + let a = start._path[height] + let b = end._path[height] + precondition(a < childCount, "Invalid index") + precondition(b <= childCount, "Invalid index") + assert(a <= b) + if b == childCount { + precondition(end._isEmpty(below: height), "Invalid index") + return distanceToEnd(from: start, in: metric) + } + if height == 0 { + assert(a < b) + return readLeaf { $0.distance(from: a, to: b, in: metric) } + } + return readInner { + let c = $0.children + if a == b { + return c[a].distance(from: start, to: end, in: metric) + } + var d = c[a].distanceToEnd(from: start, in: metric) + d += $0.distance(from: a + 1, to: b, in: metric) + d += c[b].distanceFromStart(to: end, in: metric) + return d + } + } +} + +extension Rope { + @inlinable + public func formIndex( + _ i: inout Index, + offsetBy distance: inout Int, + in metric: some RopeMetric, + preferEnd: Bool + ) { + validate(i) + if _root == nil { + precondition(distance == 0, "Position out of bounds") + return + } + if distance <= 0 { + distance = -distance + let success = root.seekBackward( + from: &i, by: &distance, in: metric, preferEnd: preferEnd) + precondition(success, "Position out of bounds") + return + } + if let leaf = i._leaf { + // Fast path: move within a single leaf + let r = leaf.read { + $0._seekForwardInLeaf(from: &i._path, by: &distance, in: metric, preferEnd: preferEnd) + } + if r { return } + } + if root.seekForward(from: &i, by: &distance, in: metric, preferEnd: preferEnd) { + return + } + precondition(distance == 0, "Position out of bounds") + i = endIndex + } + + @inlinable + public func index( + _ i: Index, + offsetBy distance: Int, + in metric: some RopeMetric, + preferEnd: Bool + ) -> (index: Index, remaining: Int) { + var i = i + var distance = distance + formIndex(&i, offsetBy: &distance, in: metric, preferEnd: preferEnd) + return (i, distance) + } +} + +extension Rope._UnsafeHandle { + @inlinable + func _seekForwardInLeaf( + from path: inout Rope._Path, + by distance: inout Int, + in metric: some RopeMetric, + preferEnd: Bool + ) -> Bool { + assert(distance >= 0) + assert(height == 0) + let c = children + var slot = path[0] + defer { path[0] = slot } + while slot < c.count { + let d = metric._nonnegativeSize(of: c[slot].summary) + if preferEnd ? d >= distance : d > distance { + return true + } + distance &-= d + slot &+= 1 + } + return false + } + + @inlinable + func _seekBackwardInLeaf( + from path: inout Rope._Path, + by distance: inout Int, + in metric: some RopeMetric, + preferEnd: Bool + ) -> Bool { + assert(distance >= 0) + assert(height == 0) + let c = children + var slot = path[0] &- 1 + while slot >= 0 { + let d = metric._nonnegativeSize(of: c[slot].summary) + if preferEnd ? d > distance : d >= distance { + path[0] = slot + distance = d &- distance + return true + } + distance &-= d + slot &-= 1 + } + return false + } +} + +extension Rope._Node { + @inlinable + func seekForward( + from i: inout Index, + by distance: inout Int, + in metric: some RopeMetric, + preferEnd: Bool + ) -> Bool { + assert(distance >= 0) + + if height == 0 { + let r = readLeaf { + $0._seekForwardInLeaf(from: &i._path, by: &distance, in: metric, preferEnd: preferEnd) + } + if r { + i._leaf = asUnmanagedLeaf + } + return r + } + + return readInner { + var slot = i._path[height] + precondition(slot < childCount, "Invalid index") + let c = $0.children + if c[slot].seekForward(from: &i, by: &distance, in: metric, preferEnd: preferEnd) { + return true + } + slot &+= 1 + while slot < c.count { + let d = metric.size(of: c[slot].summary) + if preferEnd ? d >= distance : d > distance { + i._path[$0.height] = slot + i._clear(below: $0.height) + let success = c[slot].seekForward( + from: &i, by: &distance, in: metric, preferEnd: preferEnd) + precondition(success) + return true + } + distance &-= d + slot &+= 1 + } + return false + } + } + + @inlinable + func seekBackward( + from i: inout Index, + by distance: inout Int, + in metric: some RopeMetric, + preferEnd: Bool + ) -> Bool { + assert(distance >= 0) + guard distance > 0 || preferEnd else { return true } + if height == 0 { + return readLeaf { + $0._seekBackwardInLeaf(from: &i._path, by: &distance, in: metric, preferEnd: preferEnd) + } + } + + return readInner { + var slot = i._path[height] + precondition(slot <= childCount, "Invalid index") + let c = $0.children + if slot < childCount, + c[slot].seekBackward(from: &i, by: &distance, in: metric, preferEnd: preferEnd) { + return true + } + slot -= 1 + while slot >= 0 { + let d = metric.size(of: c[slot].summary) + if preferEnd ? d > distance : d >= distance { + i._path[$0.height] = slot + i._clear(below: $0.height) + distance = d - distance + let success = c[slot].seekForward( + from: &i, by: &distance, in: metric, preferEnd: preferEnd) + precondition(success) + return true + } + distance -= d + slot -= 1 + } + return false + } + } +} diff --git a/Sources/RopeModule/Rope/Conformances/Rope+Index.swift b/Sources/RopeModule/Rope/Conformances/Rope+Index.swift new file mode 100644 index 000000000..f951fc200 --- /dev/null +++ b/Sources/RopeModule/Rope/Conformances/Rope+Index.swift @@ -0,0 +1,94 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 Rope { + @frozen // Not really! This module isn't ABI stable. + public struct Index: @unchecked Sendable { + @usableFromInline internal typealias Summary = Rope.Summary + @usableFromInline internal typealias _Path = Rope._Path + + @usableFromInline + internal var _version: _RopeVersion + + @usableFromInline + internal var _path: _Path + + /// A direct reference to the leaf node addressed by this index. + /// This must only be dereferenced while we own a tree with a matching + /// version. + @usableFromInline + internal var _leaf: _UnmanagedLeaf? + + @inlinable + internal init( + version: _RopeVersion, path: _Path, leaf: __shared _UnmanagedLeaf? + ) { + self._version = version + self._path = path + self._leaf = leaf + } + } +} + +extension Rope.Index { + @inlinable + internal static var _invalid: Self { + Self(version: _RopeVersion(0), path: _RopePath(_value: .max), leaf: nil) + } + + @inlinable + internal var _isValid: Bool { + _path._value != .max + } +} + +extension Rope.Index: Equatable { + @inlinable + public static func ==(left: Self, right: Self) -> Bool { + left._path == right._path + } +} +extension Rope.Index: Hashable { + @inlinable + public func hash(into hasher: inout Hasher) { + hasher.combine(_path) + } +} + +extension Rope.Index: Comparable { + @inlinable + public static func <(left: Self, right: Self) -> Bool { + left._path < right._path + } +} + +extension Rope.Index: CustomStringConvertible { + public var description: String { + "\(_path)" + } +} + +extension Rope.Index { + @inlinable + internal var _height: UInt8 { + _path.height + } + + @inlinable + internal func _isEmpty(below height: UInt8) -> Bool { + _path.isEmpty(below: height) + } + + @inlinable + internal mutating func _clear(below height: UInt8) { + _path.clear(below: height) + } +} diff --git a/Sources/RopeModule/Rope/Conformances/Rope+Sequence.swift b/Sources/RopeModule/Rope/Conformances/Rope+Sequence.swift new file mode 100644 index 000000000..6aef112fe --- /dev/null +++ b/Sources/RopeModule/Rope/Conformances/Rope+Sequence.swift @@ -0,0 +1,47 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 Rope: Sequence { + @inlinable + public func makeIterator() -> Iterator { + Iterator(self, from: self.startIndex) + } + + @inlinable + public func makeIterator(from start: Index) -> Iterator { + Iterator(self, from: start) + } + + @frozen // Not really! This module isn't ABI stable. + public struct Iterator: IteratorProtocol { + @usableFromInline + internal let _rope: Rope + + @usableFromInline + internal var _index: Index + + @inlinable + internal init(_ rope: Rope, from start: Index) { + rope.validate(start) + self._rope = rope + self._index = start + self._rope.grease(&_index) + } + + @inlinable + public mutating func next() -> Element? { + guard let leaf = _index._leaf else { return nil } + let item = leaf.read { $0.children[_index._path[0]].value } + _rope.formIndex(after: &_index) + return item + } + } +} diff --git a/Sources/RopeModule/Rope/Operations/Rope+Append.swift b/Sources/RopeModule/Rope/Operations/Rope+Append.swift new file mode 100644 index 000000000..3d6ecdf0c --- /dev/null +++ b/Sources/RopeModule/Rope/Operations/Rope+Append.swift @@ -0,0 +1,74 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 Rope { + @inlinable + public mutating func append(_ item: __owned Element) { + _invalidateIndices() + if _root == nil { + _root = .createLeaf(_Item(item)) + return + } + if let spawn = root.append(_Item(item)) { + _root = .createInner(children: root, spawn) + } + } +} + +extension Rope._Node { + @inlinable + internal mutating func append(_ item: __owned _Item) -> Self? { + var item = item + if item.isUndersized, !self.isEmpty, self.lastItem.rebalance(nextNeighbor: &item) { + return nil + } + ensureUnique() + if height > 0 { + var summary = self.summary + let spawn = updateInner { + let p = $0.mutableChildPtr(at: $0.childCount - 1) + summary.subtract(p.pointee.summary) + let spawn = p.pointee.append(item) + summary.add(p.pointee.summary) + return spawn + } + self.summary = summary + guard let spawn = spawn else { return nil } + +#if true // Compress existing nodes if possible. + updateInner { + let c = $0.mutableChildren + let s = c[c.count - 2].childCount + c[c.count - 1].childCount + if s <= Summary.maxNodeSize { + Self.redistributeChildren(&c[c.count - 2], &c[c.count - 1], to: s) + let removed = $0._removeChild(at: c.count - 1) + assert(removed.childCount == 0) + } + } +#endif + guard isFull else { + _appendNode(spawn) + return nil + } + + var spawn2 = split(keeping: Summary.minNodeSize) + spawn2._appendNode(spawn) + return spawn2 + } + guard isFull else { + _appendItem(item) + return nil + } + var spawn = split(keeping: Summary.minNodeSize) + spawn._appendItem(item) + return spawn + } +} diff --git a/Sources/RopeModule/Rope/Operations/Rope+Extract.swift b/Sources/RopeModule/Rope/Operations/Rope+Extract.swift new file mode 100644 index 000000000..30eba377b --- /dev/null +++ b/Sources/RopeModule/Rope/Operations/Rope+Extract.swift @@ -0,0 +1,91 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 Rope { + @inlinable + public func extract(_ offsetRange: Range, in metric: some RopeMetric) -> Self { + extract(from: offsetRange.lowerBound, to: offsetRange.upperBound, in: metric) + } + + @inlinable + public func extract(from start: Int, to end: Int, in metric: some RopeMetric) -> Self { + if _root == nil { + precondition(start == 0 && end == 0, "Invalid range") + return Self() + } + var builder = Builder() + root.extract(from: start, to: end, in: metric, into: &builder) + return builder.finalize() + } +} + +extension Rope._Node { + @inlinable + internal func extract( + from start: Int, + to end: Int, + in metric: some RopeMetric, + into builder: inout Rope.Builder + ) { + let size = metric.size(of: summary) + precondition(start >= 0 && start <= end && end <= size, "Range out of bounds") + + guard start != end else { return } + + if self.isLeaf { + self.readLeaf { + let l = $0.findSlot(at: start, in: metric, preferEnd: false) + let u = $0.findSlot(from: l, offsetBy: end - start, in: metric, preferEnd: true) + let c = $0.children + if l.slot == u.slot { + var item = c[l.slot] + let i = metric.index(at: l.remaining, in: item.value) + var item2 = item.split(at: i) + let j = metric.index( + at: u.remaining - metric._nonnegativeSize(of: item.summary), + in: item2.value) + _ = item2.split(at: j) + builder._insertBeforeTip(item2) + return + } + assert(l.slot < u.slot) + var left = c[l.slot] + left = left.split(at: metric.index(at: l.remaining, in: left.value)) + builder._insertBeforeTip(left) + for i in l.slot + 1 ..< u.slot { + builder._insertBeforeTip(c[i]) + } + var right = c[u.slot] + _ = right.split(at: metric.index(at: u.remaining, in: right.value)) + builder._insertBeforeTip(right) + } + return + } + + self.readInner { + let l = $0.findSlot(at: start, in: metric, preferEnd: false) + let u = $0.findSlot(from: l, offsetBy: end - start, in: metric, preferEnd: true) + let c = $0.children + if l.slot == u.slot { + c[l.slot].extract( + from: l.remaining, to: u.remaining, in: metric, into: &builder) + return + } + assert(l.slot < u.slot) + let lsize = metric._nonnegativeSize(of: c[l.slot].summary) + c[l.slot].extract(from: l.remaining, to: lsize, in: metric, into: &builder) + for i in l.slot + 1 ..< u.slot { + builder._insertBeforeTip(c[i]) + } + c[u.slot].extract(from: 0, to: u.remaining, in: metric, into: &builder) + } + } +} diff --git a/Sources/RopeModule/Rope/Operations/Rope+Find.swift b/Sources/RopeModule/Rope/Operations/Rope+Find.swift new file mode 100644 index 000000000..c557922f4 --- /dev/null +++ b/Sources/RopeModule/Rope/Operations/Rope+Find.swift @@ -0,0 +1,86 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 Rope { + @inlinable + public func find( + at position: Int, + in metric: some RopeMetric, + preferEnd: Bool + ) -> (index: Index, remaining: Int) { + let wholeSize = _root == nil ? 0 : metric.size(of: root.summary) + precondition(position >= 0 && position <= wholeSize, "Position out of bounds") + guard !isEmpty, preferEnd || position < wholeSize else { + return (endIndex, 0) + } + var position = position + var node = root + var path = _Path(height: node.height) + while node.height > 0 { + node = node.readInner { + let r = $0.findSlot(at: position, in: metric, preferEnd: preferEnd) + position = r.remaining + path[$0.height] = r.slot + return $0.children[r.slot] + } + } + let r = node.readLeaf { $0.findSlot(at: position, in: metric, preferEnd: preferEnd) } + path[0] = r.slot + let index = Index(version: _version, path: path, leaf: node.asUnmanagedLeaf) + return (index, r.remaining) + } +} + +extension Rope._UnsafeHandle { + @inlinable + internal func findSlot( + at position: Int, + in metric: some RopeMetric, + preferEnd: Bool = true + ) -> (slot: Int, remaining: Int) { + var remaining = position + var size = 0 + for slot in 0 ..< childCount { + size = metric.size(of: children[slot].summary) + let next = remaining - size + let adjustment = (preferEnd ? 0 : 1) + if next + adjustment <= 0 { + return (slot, remaining) + } + remaining = next + } + precondition(remaining == 0, "Position out of bounds") + return preferEnd ? (childCount - 1, remaining + size) : (childCount, 0) + } + + @inlinable + internal func findSlot( + from p: (slot: Int, remaining: Int), + offsetBy distance: Int, + in metric: some RopeMetric, + preferEnd: Bool = true + ) -> (slot: Int, remaining: Int) { + assert(p.slot >= 0 && p.slot < childCount) + assert(p.remaining >= 0 && p.remaining <= metric.size(of: children[p.slot].summary)) + assert(distance >= 0) + let adjustment = (preferEnd ? 0 : 1) + var d = p.remaining + distance + var slot = p.slot + while slot < childCount { + let size = metric.size(of: children[slot].summary) + if d + adjustment <= size { break } + d -= size + slot += 1 + } + assert(slot < childCount || d == 0) + return (slot, d) + } +} diff --git a/Sources/RopeModule/Rope/Operations/Rope+ForEachWhile.swift b/Sources/RopeModule/Rope/Operations/Rope+ForEachWhile.swift new file mode 100644 index 000000000..ad56794e1 --- /dev/null +++ b/Sources/RopeModule/Rope/Operations/Rope+ForEachWhile.swift @@ -0,0 +1,90 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 Rope { + @inlinable + public func forEachWhile( + _ body: (Element) -> Bool + ) -> Bool { + guard _root != nil else { return true } + return root.forEachWhile(body) + } + + @inlinable + public func forEachWhile( + from position: Int, + in metric: some RopeMetric, + _ body: (Element, Element.Index?) -> Bool + ) -> Bool { + guard _root != nil else { + precondition(position == 0, "Position out of bounds") + return true + } + return root.forEachWhile(from: position, in: metric, body) + } +} + +extension Rope._Node { + @inlinable + internal func forEachWhile( + _ body: (Element) -> Bool + ) -> Bool { + if isLeaf { + return readLeaf { + let c = $0.children + for i in 0 ..< c.count { + guard body(c[i].value) else { return false } + } + return true + } + } + return readInner { + let c = $0.children + for i in 0 ..< c.count { + guard c[i].forEachWhile(body) else { return false } + } + return true + } + } + + @inlinable + internal func forEachWhile( + from position: Int, + in metric: some RopeMetric, + _ body: (Element, Element.Index?) -> Bool + ) -> Bool { + if isLeaf { + return readLeaf { + let c = $0.children + var (slot, rem) = $0.findSlot(at: position, in: metric, preferEnd: false) + let i = metric.index(at: rem, in: c[slot].value) + if !body(c[slot].value, i) { return false } + slot += 1 + while slot < c.count { + if !body(c[slot].value, nil) { return false } + slot += 1 + } + return true + } + } + return readInner { + let c = $0.children + var (slot, rem) = $0.findSlot(at: position, in: metric, preferEnd: false) + if !c[slot].forEachWhile(from: rem, in: metric, body) { return false } + slot += 1 + while slot < c.count { + if !c[slot].forEachWhile({ body($0, nil) }) { return false } + slot += 1 + } + return true + } + } +} diff --git a/Sources/RopeModule/Rope/Operations/Rope+Insert.swift b/Sources/RopeModule/Rope/Operations/Rope+Insert.swift new file mode 100644 index 000000000..cc85cde88 --- /dev/null +++ b/Sources/RopeModule/Rope/Operations/Rope+Insert.swift @@ -0,0 +1,221 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 Rope { + @inlinable + public mutating func prepend(_ item: __owned Element) { + _invalidateIndices() + insert(item, at: startIndex) + } + + @inlinable + public mutating func insert( + _ item: __owned Element, + at index: Index + ) { + validate(index) + insert(item, at: index._path) + } + + @inlinable + mutating func insert( + _ item: __owned Element, + at path: _Path + ) { + if path == _endPath { + append(item) + return + } + if let spawn = root.insert(_Item(item), at: path) { + _root = .createInner(children: root, spawn) + } + _invalidateIndices() + } + + @inlinable + public mutating func insert( + _ item: __owned Element, + at position: Int, + in metric: some RopeMetric + ) { + if position == metric.size(of: summary) { + append(item) + return + } + if let spawn = root.insert(_Item(item), at: position, in: metric) { + _root = .createInner(children: root, spawn) + } + _invalidateIndices() + } +} + +extension Rope._Node { + @inlinable + internal mutating func prepend(_ item: __owned _Item) -> Self? { + insert(item, at: _startPath) + } + + @inlinable + internal mutating func insert( + _ item: __owned _Item, + at path: _Path + ) -> Self? { + ensureUnique() + let h = height + let slot = path[h] + if h > 0 { + precondition(slot < childCount, "Index out of bounds") + return _innerInsert(at: slot) { $0.insert(item, at: path) } + } + precondition(slot <= childCount, "Index out of bounds") + return _leafInsert(item, at: slot) + } + + @inlinable + internal mutating func insert( + _ item: __owned _Item, + at position: Int, + in metric: some RopeMetric + ) -> Self? { + ensureUnique() + if height > 0 { + let (slot, remaining) = readInner { + $0.findSlot(at: position, in: metric, preferEnd: false) + } + return _innerInsert(at: slot) { $0.insert(item, at: remaining, in: metric) } + } + let (slot, remaining) = readLeaf { + $0.findSlot(at: position, in: metric, preferEnd: false) + } + precondition(remaining == 0, "Inserted element doesn't fall on an element boundary") + return _leafInsert(item, at: slot) + } +} + +extension Rope._Node { + @inlinable + internal mutating func _innerInsert( + at slot: Int, + with body: (inout Self) -> Self? + ) -> Self? { + assert(slot < childCount) + var summary = self.summary + let spawn = updateInner { + let p = $0.mutableChildPtr(at: slot) + summary.subtract(p.pointee.summary) + let spawn = body(&p.pointee) + summary.add(p.pointee.summary) + return spawn + } + self.summary = summary + guard let spawn = spawn else { return nil } + return _applySpawn(spawn, of: slot) + } + + @inlinable + internal mutating func _applySpawn( + _ spawn: __owned Self, of slot: Int + ) -> Self? { + var spawn = spawn + var nextSlot = slot + 1 +#if true // Compress existing nodes if possible. + if slot > 0 { + // Try merging remainder into previous child. + updateInner { + let c = $0.mutableChildren + let s = c[slot - 1].childCount + c[slot].childCount + guard s <= Summary.maxNodeSize else { return } + Self.redistributeChildren(&c[slot - 1], &c[slot], to: s) + let removed = $0._removeChild(at: slot) + assert(removed.childCount == 0) + nextSlot -= 1 + } + } + if nextSlot < childCount { + // Try merging new spawn into subsequent child. + let merged: Summary? = updateInner { + let c = $0.mutableChildren + let s = spawn.childCount + c[nextSlot].childCount + guard s <= Summary.maxNodeSize else { return nil } + let summary = spawn.summary + Self.redistributeChildren(&spawn, &c[nextSlot], to: 0) + assert(spawn.childCount == 0) + return summary + } + if let merged = merged { + self.summary.add(merged) + return nil + } + } +#endif + guard isFull else { + _insertNode(spawn, at: nextSlot) + return nil + } + if nextSlot < Summary.minNodeSize { + let spawn2 = split(keeping: childCount / 2) + _insertNode(spawn, at: nextSlot) + return spawn2 + } + var spawn2 = split(keeping: childCount / 2) + spawn2._insertNode(spawn, at: nextSlot - childCount) + return spawn2 + } +} + +extension Rope._Node { + @inlinable + internal mutating func _leafInsert( + _ item: __owned _Item, at slot: Int + ) -> Self? { + assert(slot <= childCount) + var item = item + if item.isUndersized, childCount > 0, _rebalanceBeforeInsert(&item, at: slot) { + return nil + } + + guard isFull else { + _insertItem(item, at: slot) + return nil + } + if slot < Summary.minNodeSize { + let spawn = split(keeping: childCount - Summary.minNodeSize) + _insertItem(item, at: slot) + return spawn + } + var spawn = split(keeping: Summary.minNodeSize) + spawn._insertItem(item, at: slot - childCount) + return spawn + } + + @inlinable + internal mutating func _rebalanceBeforeInsert( + _ item: inout _Item, at slot: Int + ) -> Bool { + assert(item.isUndersized) + let r = updateLeaf { (h) -> (merged: Bool, delta: Summary) in + if slot > 0 { + let p = h.mutableChildPtr(at: slot - 1) + let sum = p.pointee.summary + let merged = p.pointee.rebalance(nextNeighbor: &item) + let delta = p.pointee.summary.subtracting(sum) + return (merged, delta) + } + let p = h.mutableChildPtr(at: slot) + let sum = p.pointee.summary + let merged = p.pointee.rebalance(prevNeighbor: &item) + let delta = p.pointee.summary.subtracting(sum) + return (merged, delta) + } + self.summary.add(r.delta) + return r.merged + } +} diff --git a/Sources/RopeModule/Rope/Operations/Rope+Join.swift b/Sources/RopeModule/Rope/Operations/Rope+Join.swift new file mode 100644 index 000000000..92a6195e4 --- /dev/null +++ b/Sources/RopeModule/Rope/Operations/Rope+Join.swift @@ -0,0 +1,126 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 Rope { + @inlinable + public mutating func append(_ other: __owned Self) { + self = Rope.join(self, other) + } + + @inlinable + public mutating func prepend(_ other: __owned Self) { + self = Rope.join(other, self) + } + + @inlinable + internal mutating func _append(_ other: __owned _Node) { + append(Self(root: other)) + } + + @inlinable + internal mutating func _prepend(_ other: __owned _Node) { + prepend(Self(root: other)) + } + + /// Concatenate `left` and `right` by linking up the two trees. + @inlinable + public static func join(_ left: __owned Self, _ right: __owned Self) -> Self { + guard !right.isEmpty else { return left } + guard !left.isEmpty else { return right } + + var left = left.root + var right = right.root + + left.ensureUnique() + right.ensureUnique() + + if left.height >= right.height { + let r = left._graftBack(&right) + guard let remainder = r.remainder else { return Self(root: left) } + assert(left.height == remainder.height) + let root = _Node.createInner(children: left, remainder) + return Self(root: root) + + } + let r = right._graftFront(&left) + guard let remainder = r.remainder else { return Self(root: right) } + assert(right.height == remainder.height) + let root = _Node.createInner(children: remainder, right) + return Self(root: root) + } +} + +extension Rope._Node { + @inlinable + internal mutating func _graftFront( + _ scion: inout Self + ) -> (remainder: Self?, delta: Summary) { + assert(self.height >= scion.height) + guard self.height > scion.height else { + assert(self.height == scion.height) + let d = scion.summary + if self.rebalance(prevNeighbor: &scion) { + return (nil, d) + } + assert(!scion.isEmpty) + return (scion, d.subtracting(scion.summary)) + } + + var (remainder, delta) = self.updateInner { h in + h.mutableChildren[0]._graftFront(&scion) + } + self.summary.add(delta) + guard let remainder = remainder else { return (nil, delta) } + assert(self.height == remainder.height + 1) + assert(!remainder.isUndersized) + guard self.isFull else { + delta.add(remainder.summary) + self._insertNode(remainder, at: 0) + return (nil, delta) + } + var splinter = self.split(keeping: self.childCount / 2) + + swap(&self, &splinter) + delta.subtract(splinter.summary) + splinter._insertNode(remainder, at: 0) + return (splinter, delta) + } + + @inlinable + internal mutating func _graftBack( + _ scion: inout Self + ) -> (remainder: Self?, delta: Summary) { + assert(self.height >= scion.height) + guard self.height > scion.height else { + assert(self.height == scion.height) + let origSum = self.summary + let emptied = self.rebalance(nextNeighbor: &scion) + return (emptied ? nil : scion, self.summary.subtracting(origSum)) + } + + var (remainder, delta) = self.updateInner { h in + h.mutableChildren[h.childCount - 1]._graftBack(&scion) + } + self.summary.add(delta) + guard let remainder = remainder else { return (nil, delta) } + assert(self.height == remainder.height + 1) + assert(!remainder.isUndersized) + guard self.isFull else { + delta.add(remainder.summary) + self._appendNode(remainder) + return (nil, delta) + } + var splinter = self.split(keeping: self.childCount / 2) + delta.subtract(splinter.summary) + splinter._appendNode(remainder) + return (splinter, delta) + } +} diff --git a/Sources/RopeModule/Rope/Operations/Rope+MutatingForEach.swift b/Sources/RopeModule/Rope/Operations/Rope+MutatingForEach.swift new file mode 100644 index 000000000..82fdf0822 --- /dev/null +++ b/Sources/RopeModule/Rope/Operations/Rope+MutatingForEach.swift @@ -0,0 +1,97 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 Rope { + @inlinable @inline(__always) + @discardableResult + public mutating func mutatingForEach( + _ body: (inout Element) -> R? + ) -> R? { + var i = startIndex + return mutatingForEach(from: &i, body) + } + + @inlinable @inline(__always) + @discardableResult + public mutating func mutatingForEach( + from index: inout Index, + _ body: (inout Element) -> R? + ) -> R? { + var r: R? = nil + let completed = _mutatingForEach(from: &index) { + r = body(&$0) + return r == nil + } + assert(completed == (r == nil)) + return r + } + + @inlinable + internal mutating func _mutatingForEach( + from index: inout Index, + _ body: (inout Element) -> Bool + ) -> Bool { + validate(index) + guard _root != nil else { return true } + defer { + _invalidateIndices() + index._version = _version + } + let r = root.mutatingForEach(from: &index, body: body).continue + return r + } +} + +extension Rope._Node { + @inlinable + internal mutating func mutatingForEach( + from index: inout Index, + body: (inout Element) -> Bool + ) -> (continue: Bool, delta: Summary) { + ensureUnique() + let h = height + var slot = index._path[h] + precondition(slot <= childCount, "Index out of bounds") + guard slot < childCount else { return (true, .zero) } + var delta = Summary.zero + defer { self.summary.add(delta) } + if h > 0 { + let r = updateInner { + let c = $0.mutableChildren + while slot < c.count { + let (r, d) = c[slot].mutatingForEach(from: &index, body: body) + delta.add(d) + guard r else { return false } + slot += 1 + index._path.clear(below: h) + index._path[h] = slot + } + index._leaf = nil + return true + } + return (r, delta) + } + index._leaf = asUnmanagedLeaf + let r = updateLeaf { + let c = $0.mutableChildren + while slot < c.count { + let sum = c[slot].summary + let r = body(&c[slot].value) + delta.add(c[slot].summary.subtracting(sum)) + guard r else { return false } + slot += 1 + index._path[h] = slot + } + return true + } + return (r, delta) + } +} diff --git a/Sources/RopeModule/Rope/Operations/Rope+Remove.swift b/Sources/RopeModule/Rope/Operations/Rope+Remove.swift new file mode 100644 index 000000000..0ffd35bde --- /dev/null +++ b/Sources/RopeModule/Rope/Operations/Rope+Remove.swift @@ -0,0 +1,210 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 Rope { + @inlinable + @discardableResult + public mutating func remove(at index: Index) -> Element { + _remove(at: index).removed + } + + /// Remove the element at the specified index, and update `index` to address the subsequent + /// element in the new collection. (Or the `endIndex` if it originally addressed the last item.) + @inlinable + @discardableResult + public mutating func remove(at index: inout Index) -> Element { + let (old, path) = _remove(at: index) + index = Index(version: _version, path: path, leaf: _unmanagedLeaf(at: path)) + return old + } + + @inlinable + @discardableResult + internal mutating func _remove(at index: Index) -> (removed: Element, path: _Path) { + validate(index) + var path = index._path + let r = root.remove(at: &path) + if root.isEmpty { + _root = nil + assert(r.pathIsAtEnd) + } else if root.childCount == 1, root.height > 0 { + root = root.readInner { $0.children.first! } + path.popRoot() + } + _invalidateIndices() + return (r.removed.value, r.pathIsAtEnd ? _endPath : path) + } +} + +extension Rope._Node { + @inlinable + internal mutating func remove( + at path: inout _Path + ) -> (removed: _Item, delta: Summary, needsFixing: Bool, pathIsAtEnd: Bool) { + ensureUnique() + let h = height + let slot = path[h] + precondition(slot < childCount, "Invalid index") + guard h > 0 else { + let r = _removeItem(at: slot) + return (r.removed, r.delta, self.isUndersized, slot == childCount) + } + let r = updateInner { $0.mutableChildren[slot].remove(at: &path) } + self.summary.subtract(r.delta) + var isAtEnd = r.pathIsAtEnd + if r.needsFixing { + let prepended = fixDeficiency(on: &path) + isAtEnd = isAtEnd && prepended + } + if isAtEnd, path[h] < childCount - 1 { + path[h] += 1 + path.clear(below: h) + isAtEnd = false + } + return (r.removed, r.delta, self.isUndersized, isAtEnd) + } +} + +extension Rope { + @inlinable + @discardableResult + public mutating func remove( + at position: Int, + in metric: some RopeMetric + ) -> (removed: Element, next: Index) { + _invalidateIndices() + var path = _Path(height: self._height) + let r = root.remove(at: position, in: metric, initializing: &path) + if root.isEmpty { + _root = nil + } else if root.childCount == 1, root.height > 0 { + root = root.readInner { $0.children.first! } + } + if r.pathIsAtEnd { + return (r.removed.value, endIndex) + } + let i = Index(version: _version, path: path, leaf: nil) + return (r.removed.value, i) + } +} + +extension Rope._Node { + /// Note: `self` may be left undersized after calling this function, which + /// is expected to be resolved by the caller. This is indicated by the `needsFixing` component + /// in the return value. + /// + /// - Returns: A tuple `(removed, delta, needsFixing, pathIsAtEnd)`, where + /// `removed` is the element that got removed, + /// `delta` is its summary, + /// `needsFixing` indicates whether the node was left undersized, and + /// `pathIsAtEnd` indicates if `path` now addresses the end of the node's subtree. + @inlinable + internal mutating func remove( + at position: Int, + in metric: some RopeMetric, + initializing path: inout _Path + ) -> (removed: _Item, delta: Summary, needsFixing: Bool, pathIsAtEnd: Bool) { + ensureUnique() + let h = height + guard h > 0 else { + let (slot, remaining) = readLeaf { + $0.findSlot(at: position, in: metric, preferEnd: false) + } + precondition(remaining == 0, "Element to be removed doesn't fall on an element boundary") + path[h] = slot + let r = _removeItem(at: slot) + return (r.removed, r.delta, self.isUndersized, slot == childCount) + } + let r = updateInner { + let (slot, remaining) = $0.findSlot(at: position, in: metric, preferEnd: false) + path[h] = slot + return $0.mutableChildren[slot].remove(at: remaining, in: metric, initializing: &path) + } + self.summary.subtract(r.delta) + var isAtEnd = r.pathIsAtEnd + if r.needsFixing { + let prepended = fixDeficiency(on: &path) + isAtEnd = isAtEnd && prepended + } + if isAtEnd, path[h] < childCount - 1 { + path[h] += 1 + path.clear(below: h) + isAtEnd = false + } + return (r.removed, r.delta, self.isUndersized, isAtEnd) + } +} + +extension Rope._Node { + /// Returns: `true` if new items got prepended to the child addressed by `path`. + /// `false` if new items got appended. + @inlinable + @discardableResult + internal mutating func fixDeficiency(on path: inout _Path) -> Bool { + assert(isUnique()) + return updateInner { + let c = $0.mutableChildren + let h = $0.height + let slot = path[h] + assert(c[slot].isUndersized) + guard c.count > 1 else { return true } + let prev = slot - 1 + let prevSum: Int + if prev >= 0 { + let prevCount = c[prev].childCount + prevSum = prevCount + c[slot].childCount + if prevSum <= Summary.maxNodeSize { + Self.redistributeChildren(&c[prev], &c[slot], to: prevSum) + assert(c[slot].isEmpty) + _ = $0._removeChild(at: slot) + path[h] = prev + path[h - 1] += prevCount + return true + } + } else { + prevSum = 0 + } + + let next = slot + 1 + let nextSum: Int + if next < c.count { + let nextCount = c[next].childCount + nextSum = c[slot].childCount + nextCount + if nextSum <= Summary.maxNodeSize { + Self.redistributeChildren(&c[slot], &c[next], to: nextSum) + assert(c[next].isEmpty) + _ = $0._removeChild(at: next) + // `path` doesn't need updating. + return false + } + } else { + nextSum = 0 + } + + if prev >= 0 { + assert(c[prev].childCount > Summary.minNodeSize) + let origCount = c[slot].childCount + Self.redistributeChildren(&c[prev], &c[slot], to: prevSum / 2) + path[h - 1] += c[slot].childCount - origCount + assert(!c[prev].isUndersized) + assert(!c[slot].isUndersized) + return true + } + assert(next < c.count) + assert(c[next].childCount > Summary.minNodeSize) + Self.redistributeChildren(&c[slot], &c[next], to: nextSum / 2) + // `path` doesn't need updating. + assert(!c[slot].isUndersized) + assert(!c[next].isUndersized) + return false + } + } +} diff --git a/Sources/RopeModule/Rope/Operations/Rope+RemoveSubrange.swift b/Sources/RopeModule/Rope/Operations/Rope+RemoveSubrange.swift new file mode 100644 index 000000000..76e443076 --- /dev/null +++ b/Sources/RopeModule/Rope/Operations/Rope+RemoveSubrange.swift @@ -0,0 +1,321 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 Rope { + @inlinable + public mutating func removeSubrange( + _ bounds: Range, + in metric: some RopeMetric + ) { + _invalidateIndices() + precondition( + bounds.lowerBound >= 0 && bounds.upperBound <= count(in: metric), + "Position out of bounds") + guard !bounds.isEmpty else { return } + // FIXME: Add fast path for tiny removals + var builder = builder(removing: bounds, in: metric) + self = builder.finalize() + } + + @inlinable + public mutating func replaceSubrange( + _ bounds: Range, + in metric: some RopeMetric, + with newElements: __owned some Sequence + ) { + // FIXME: Implement insert(contentsOf:at:in:) and dispatch to it when bounds.isEmpty. + // FIXME: Add fast path for replacing tiny ranges with tiny data. + // FIXME: Add special cases if newElements is itself a _Rope etc. + _invalidateIndices() + var builder = builder(removing: bounds, in: metric) + builder.insertBeforeTip(newElements) + self = builder.finalize() + } + + @inlinable + public mutating func builder( + removing bounds: Range, + in metric: some RopeMetric + ) -> Builder { + _invalidateIndices() + let size = metric.size(of: summary) + precondition( + bounds.lowerBound >= 0 && bounds.upperBound <= size, + "Range out of bounds") + + guard !bounds.isEmpty else { + return builder(splittingAt: bounds.lowerBound, in: metric) + } + + var builder = Builder() + var node = root + _root = nil + var lower = bounds.lowerBound + var upper = bounds.upperBound + while !node.isLeaf { + let (l, u) = node.readInner { + let l = $0.findSlot(at: lower, in: metric, preferEnd: false) + let u = $0.findSlot(from: l, offsetBy: upper - lower, in: metric, preferEnd: true) + return (l, u) + } + if l.slot < u.slot { + node._removeSubrange(from: l, to: u, in: metric, into: &builder) + return builder + } + assert(l.slot == u.slot) + node._innerSplit(at: l.slot, into: &builder) + lower = l.remaining + upper = u.remaining + } + + let (l, u) = node.readLeaf { + let l = $0.findSlot(at: lower, in: metric, preferEnd: false) + let u = $0.findSlot(from: l, offsetBy: bounds.count, in: metric, preferEnd: true) + return (l, u) + } + if l.slot < u.slot { + node._removeSubrange(from: l, to: u, in: metric, into: &builder) + return builder + } + assert(l.slot == u.slot) + var item = node._leafSplit(at: l.slot, into: &builder) + let i2 = metric.index(at: u.remaining, in: item.value) + builder._insertAfterTip(item.split(at: i2)) + let i1 = metric.index(at: l.remaining, in: item.value) + _ = item.split(at: i1) + builder._insertBeforeTip(item) + return builder + } +} + +extension Rope._Node { + @inlinable + internal __consuming func _removeSubrange( + from start: (slot: Int, remaining: Int), + to end: (slot: Int, remaining: Int), + in metric: some RopeMetric, + into builder: inout Rope.Builder + ) { + assert(start.slot >= 0 && start.slot < end.slot && end.slot < childCount) + assert(start.remaining >= 0) + assert(end.remaining >= 0) + + builder._insertBeforeTip(slots: 0 ..< start.slot, in: self) + if end.slot < childCount { + builder._insertAfterTip(slots: end.slot + 1 ..< childCount, in: self) + } + + guard isLeaf else { + // Extract children on boundaries. + let (lower, upper) = readInner { ($0.children[start.slot], $0.children[end.slot]) } + + // Descend a lever lower. + lower.removeSuffix(from: start.remaining, in: metric, into: &builder) + upper.removePrefix(upTo: end.remaining, in: metric, into: &builder) + return + } + // Extract items on boundaries. + var (lower, upper) = readLeaf { ($0.children[start.slot], $0.children[end.slot]) } + + let i1 = metric.index(at: start.remaining, in: lower.value) + let i2 = metric.index(at: end.remaining, in: upper.value) + _ = lower.split(at: i1) + builder._insertBeforeTip(lower) + builder._insertAfterTip(upper.split(at: i2)) + } + + @inlinable + internal __consuming func removeSuffix( + from position: Int, + in metric: some RopeMetric, + into builder: inout Rope.Builder + ) { + + var node = self + var position = position + while true { + guard position > 0 else { return } + guard position < metric.size(of: node.summary) else { + builder._insertBeforeTip(node) + return + } + + guard !node.isLeaf else { break } + + let r = node.readInner { $0.findSlot(at: position, in: metric) } + position = r.remaining + node._innerRemoveSuffix(descending: r.slot, into: &builder) + } + let r = node.readLeaf { $0.findSlot(at: position, in: metric, preferEnd: false) } + var item = node._leafRemoveSuffix(returning: r.slot, into: &builder) + let i = metric.index(at: r.remaining, in: item.value) + _ = item.split(at: i) + builder._insertBeforeTip(item) + } + + @inlinable + internal __consuming func removePrefix( + upTo position: Int, + in metric: some RopeMetric, + into builder: inout Rope.Builder + ) { + var node = self + var position = position + while true { + guard position > 0 else { + builder._insertAfterTip(node) + return + } + guard position < metric.size(of: node.summary) else { return } + + guard !node.isLeaf else { break } + + let r = node.readInner { $0.findSlot(at: position, in: metric) } + position = r.remaining + node._innerRemovePrefix(descending: r.slot, into: &builder) + } + let r = node.readLeaf { $0.findSlot(at: position, in: metric) } + var item = node._leafRemovePrefix(returning: r.slot, into: &builder) + let i = metric.index(at: r.remaining, in: item.value) + builder._insertAfterTip(item.split(at: i)) + } + + @inlinable + internal mutating func _innerRemoveSuffix( + descending slot: Int, + into builder: inout Rope.Builder + ) { + assert(!self.isLeaf) + assert(slot >= 0 && slot <= childCount) + + if slot == 0 { + self = readInner { $0.children[0] } + return + } + if slot == 1 { + let (remaining, new) = readInner { + let c = $0.children + return (c[0], c[1]) + } + builder._insertBeforeTip(remaining) + self = new + return + } + + ensureUnique() + if slot < childCount - 1 { + let delta = updateInner { $0._removeSuffix($0.childCount - slot - 1) } + self.summary.subtract(delta) + } + var n = _removeNode(at: slot) + swap(&self, &n) + assert(n.childCount > 1) + builder._insertBeforeTip(n) + } + + @inlinable + internal __consuming func _leafRemoveSuffix( + returning slot: Int, + into builder: inout Rope.Builder + ) -> _Item { + assert(self.isLeaf) + assert(slot >= 0 && slot < childCount) + + if slot == 0 { + return readLeaf { $0.children[0] } + } + if slot == 1 { + let (remaining, new) = readLeaf { + let c = $0.children + return (c[0], c[1]) + } + builder._insertBeforeTip(remaining) + return new + } + + var n = self + n.ensureUnique() + if slot < n.childCount - 1 { + let delta = n.updateLeaf { $0._removeSuffix($0.childCount - slot - 1) } + n.summary.subtract(delta) + } + let item = n._removeItem(at: slot).removed + builder._insertBeforeTip(n) + return item + } + + @inlinable + internal mutating func _innerRemovePrefix( + descending slot: Int, + into builder: inout Rope.Builder + ) { + assert(!self.isLeaf) + assert(slot >= 0 && slot < childCount) + + if slot == childCount - 1 { + self = readInner { $0.children[$0.childCount - 1] } + return + } + if slot == childCount - 2 { + let (new, remaining) = readInner { + let c = $0.children + return (c[$0.childCount - 2], c[$0.childCount - 1]) + } + builder._insertAfterTip(remaining) + self = new + return + } + + ensureUnique() + var (delta, n) = updateInner { + let n = $0.children[slot] + let delta = $0._removePrefix(slot + 1) + return (delta, n) + } + self.summary.subtract(delta) + assert(self.childCount > 1) + swap(&self, &n) + builder._insertAfterTip(n) + } + + @inlinable + internal __consuming func _leafRemovePrefix( + returning slot: Int, + into builder: inout Rope.Builder + ) -> _Item { + assert(self.isLeaf) + assert(slot >= 0 && slot <= childCount) + + if slot == childCount - 1 { + return readLeaf { $0.children[$0.childCount - 1] } + } + if slot == childCount - 2 { + let (new, remaining) = readLeaf { + let c = $0.children + return (c[$0.childCount - 2], c[$0.childCount - 1]) + } + builder._insertAfterTip(remaining) + return new + } + + var n = self + n.ensureUnique() + let (delta, item) = n.updateLeaf { + let n = $0.children[slot] + let delta = $0._removePrefix(slot + 1) + return (delta, n) + } + n.summary.subtract(delta) + assert(n.childCount > 1) + builder._insertAfterTip(n) + return item + } +} diff --git a/Sources/RopeModule/Rope/Operations/Rope+Split.swift b/Sources/RopeModule/Rope/Operations/Rope+Split.swift new file mode 100644 index 000000000..38b6c1189 --- /dev/null +++ b/Sources/RopeModule/Rope/Operations/Rope+Split.swift @@ -0,0 +1,148 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 Rope { + @inlinable + public mutating func builder( + splittingAt position: Int, + in metric: some RopeMetric + ) -> Builder { + _invalidateIndices() + var builder = Builder() + + if self.isEmpty { + precondition(position == 0, "Position out of bounds") + return builder + } + var position = position + var node = root + _root = nil + while !node.isLeaf { + let r = node.readInner { $0.findSlot(at: position, in: metric) } + position = r.remaining + node._innerSplit(at: r.slot, into: &builder) + } + + let r = node.readLeaf { $0.findSlot(at: position, in: metric) } + var item = node._leafSplit(at: r.slot, into: &builder) + let index = metric.index(at: r.remaining, in: item.value) + let suffix = item.split(at: index) + builder._insertAfterTip(suffix) + builder._insertBeforeTip(item) + return builder + } + + @inlinable + public mutating func split(at index: Index) -> (builder: Builder, item: Element) { + validate(index) + precondition(index < endIndex) + var builder = Builder() + + var node = root + _root = nil + while !node.isLeaf { + let slot = index._path[node.height] + precondition(slot < node.childCount, "Index out of bounds") + node._innerSplit(at: slot, into: &builder) + } + + let slot = index._path[node.height] + precondition(slot < node.childCount, "Index out of bounds") + let item = node._leafSplit(at: slot, into: &builder) + _invalidateIndices() + return (builder, item.value) + } + + @inlinable + public mutating func split(at ropeIndex: Index, _ itemIndex: Element.Index) -> Builder { + var (builder, item) = self.split(at: ropeIndex) + let suffix = item.split(at: itemIndex) + if !suffix.isEmpty { + builder.insertAfterTip(suffix) + } + if !item.isEmpty { + builder.insertBeforeTip(item) + } + return builder + } +} + +extension Rope._Node { + @inlinable + internal mutating func _innerSplit( + at slot: Int, + into builder: inout Rope.Builder + ) { + assert(!self.isLeaf) + assert(slot >= 0 && slot < childCount) + ensureUnique() + + var slot = slot + if slot == childCount - 2 { + builder._insertAfterTip(_removeNode(at: childCount - 1)) + } + if slot == 1 { + builder._insertBeforeTip(_removeNode(at: 0)) + slot -= 1 + } + + var n = _removeNode(at: slot) + swap(&self, &n) + + guard n.childCount > 0 else { return } + if slot == 0 { + builder._insertAfterTip(n) + return + } + if slot == n.childCount { + builder._insertBeforeTip(n) + return + } + let suffix = n.split(keeping: slot) + builder._insertBeforeTip(n) + builder._insertAfterTip(suffix) + } + + @inlinable + internal __consuming func _leafSplit( + at slot: Int, + into builder: inout Rope.Builder + ) -> _Item { + var n = self + n.ensureUnique() + + assert(n.isLeaf) + assert(slot >= 0 && slot < n.childCount) + + var slot = slot + if slot == n.childCount - 2 { + builder._insertAfterTip(n._removeItem(at: childCount - 1).removed) + } + if slot == 1 { + builder.insertBeforeTip(n._removeItem(at: 0).removed.value) + slot -= 1 + } + + let item = n._removeItem(at: slot).removed + + guard n.childCount > 0 else { return item } + if slot == 0 { + builder._insertAfterTip(n) + } else if slot == n.childCount { + builder._insertBeforeTip(n) + } else { + let suffix = n.split(keeping: slot) + builder._insertBeforeTip(n) + builder._insertAfterTip(suffix) + } + return item + } +} diff --git a/Sources/RopeModule/Utilities/Optional Utilities.swift b/Sources/RopeModule/Utilities/Optional Utilities.swift new file mode 100644 index 000000000..627c429f6 --- /dev/null +++ b/Sources/RopeModule/Utilities/Optional Utilities.swift @@ -0,0 +1,19 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 Optional { + @inlinable + internal mutating func _take() -> Self { + let r = self + self = nil + return r + } +} diff --git a/Sources/RopeModule/Utilities/String Utilities.swift b/Sources/RopeModule/Utilities/String Utilities.swift new file mode 100644 index 000000000..a571016d4 --- /dev/null +++ b/Sources/RopeModule/Utilities/String Utilities.swift @@ -0,0 +1,127 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension StringProtocol { + @inline(__always) + var _indexOfLastCharacter: Index { + guard !isEmpty else { return endIndex } + return index(before: endIndex) + } + + @inline(__always) + func _index(at offset: Int) -> Index { + self.index(self.startIndex, offsetBy: offset) + } + + @inline(__always) + func _utf8Index(at offset: Int) -> Index { + self.utf8.index(startIndex, offsetBy: offset) + } + + @inline(__always) + func _utf8ClampedIndex(at offset: Int) -> Index { + self.utf8.index(startIndex, offsetBy: offset, limitedBy: endIndex) ?? endIndex + } + + @inline(__always) + func _utf8Offset(of index: Index) -> Int { + self.utf8.distance(from: startIndex, to: index) + } + + @inline(__always) + var _lastCharacter: (index: Index, utf8Length: Int) { + let i = _indexOfLastCharacter + let length = utf8.distance(from: i, to: endIndex) + return (i, length) + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension String { + internal func _lpad(to width: Int, with pad: Character = " ") -> String { + let c = self.count + if c >= width { return self } + return String(repeating: pad, count: width - c) + self + } + + internal func _rpad(to width: Int, with pad: Character = " ") -> String { + let c = self.count + if c >= width { return self } + return self + String(repeating: pad, count: width - c) + } +} + +#if swift(>=5.8) // _CharacterRecognizer +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension String { + @discardableResult + mutating func _appendQuotedProtectingLeft( + _ str: String, + with state: inout _CharacterRecognizer, + maxLength: Int = Int.max + ) -> String.Index { + guard !str.isEmpty else { return str.endIndex } + let startUTF8 = self.utf8.count + var i = str.unicodeScalars.startIndex + var needsBreak = true + while i < str.endIndex { + let us = str.unicodeScalars[i] + var scalar = us.escaped(asASCII: false) + if needsBreak { + var t = state + if let r = t.firstBreak(in: scalar[...]), r.lowerBound == scalar.startIndex { + } else { + scalar = us.escaped(asASCII: true) + } + } + needsBreak = (scalar != String(us)) + self.append(scalar) + _ = state.consume(scalar[...]) + + str.unicodeScalars.formIndex(after: &i) + + let start = self._utf8Index(at: startUTF8) + if self.distance(from: start, to: self.endIndex) >= maxLength { + break + } + } + return i + } + + mutating func _appendProtectingRight(_ str: String, with state: inout _CharacterRecognizer) { + var suffix = str + while !self.isEmpty { + guard let first = suffix.unicodeScalars.first else { return } + self.unicodeScalars.append(first) + let i = self.index(before: self.endIndex) + if self.unicodeScalars.distance(from: i, to: self.endIndex) == 1 { + self.unicodeScalars.append(contentsOf: suffix.unicodeScalars.dropFirst()) + break + } + self.unicodeScalars.removeLast() + let last = self.unicodeScalars.removeLast() + suffix.insert(contentsOf: last.escaped(asASCII: true), at: suffix.startIndex) + } + } + + /// A representation of the string that is suitable for debugging. + /// This implementation differs from `String.debugDescription` by properly quoting + /// continuation characters after the opening quotation mark and similar meta-characters. + var _properDebugDescription: String { + var result = "\"" + var state = _CharacterRecognizer(consuming: result) + result._appendQuotedProtectingLeft(self, with: &state) + result._appendProtectingRight("\"", with: &state) + return result + } +} +#endif diff --git a/Sources/RopeModule/Utilities/String.Index+ABI.swift b/Sources/RopeModule/Utilities/String.Index+ABI.swift new file mode 100644 index 000000000..f26bf9dff --- /dev/null +++ b/Sources/RopeModule/Utilities/String.Index+ABI.swift @@ -0,0 +1,113 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +// Bits of String.Index that are ABI but aren't exposed by public API. +extension String.Index { + @inline(__always) + var _abi_rawBits: UInt64 { + unsafeBitCast(self, to: UInt64.self) + } + + @inline(__always) + var _abi_encodedOffset: Int { + Int(truncatingIfNeeded: _abi_rawBits &>> 16) + } + + @inline(__always) + var _abi_transcodedOffset: Int { + Int(truncatingIfNeeded: (_abi_rawBits &>> 14) & 0x3) + } + + @inline(__always) + static var _abi_scalarAlignmentBit: UInt64 { 0x1 } + + @inline(__always) + static var _abi_characterAlignmentBit: UInt64 { 0x2 } + + @inline(__always) + static var _abi_utf8Bit: UInt64 { 0x4 } + + @inline(__always) + static var _abi_utf16Bit: UInt64 { 0x8 } + + + @inline(__always) + var _encodingBits: UInt64 { + _abi_rawBits & (Self._abi_utf8Bit | Self._abi_utf16Bit) + } + + @inline(__always) + var _canBeUTF8: Bool { + // The only way an index cannot be UTF-8 is it has only the UTF-16 flag set. + _encodingBits != Self._abi_utf16Bit + } + + var _isKnownScalarAligned: Bool { + 0 != _abi_rawBits & Self._abi_scalarAlignmentBit + } + + var _isKnownCharacterAligned: Bool { + 0 != _abi_rawBits & Self._abi_characterAlignmentBit + } + + var _knownCharacterAligned: String.Index { + let r = _abi_rawBits | Self._abi_characterAlignmentBit | Self._abi_scalarAlignmentBit + return unsafeBitCast(r, to: String.Index.self) + } + + var _knownScalarAligned: String.Index { + let r = _abi_rawBits | Self._abi_scalarAlignmentBit + return unsafeBitCast(r, to: String.Index.self) + } +} + +extension String.Index { + @inline(__always) + var _utf8Offset: Int { + assert(_canBeUTF8) + return _abi_encodedOffset + } + + @inline(__always) + var _isUTF16TrailingSurrogate: Bool { + assert(_canBeUTF8) + let r = _abi_transcodedOffset + assert(r <= 1) + return r > 0 + } + + @inline(__always) + init(_rawBits: UInt64) { + self = unsafeBitCast(_rawBits, to: String.Index.self) + } + + @inline(__always) + init(_utf8Offset: Int) { + self.init(_rawBits: (UInt64(_utf8Offset) &<< 16) | Self._abi_utf8Bit) + } + + init(_utf8Offset: Int, utf16TrailingSurrogate: Bool) { + let transcodedOffset: UInt64 = (utf16TrailingSurrogate ? 1 &<< 14 : 0) + self.init(_rawBits: (UInt64(_utf8Offset) &<< 16) | transcodedOffset | Self._abi_utf8Bit) + } +} + +extension String.Index { + @inline(__always) + var _chunkData: UInt16 { + UInt16(_abi_rawBits &>> 14) + } + + @inline(__always) + init(_chunkData: UInt16) { + self.init(_rawBits: UInt64(_chunkData) &<< 14) + } +} diff --git a/Sources/RopeModule/Utilities/_CharacterRecognizer.swift b/Sources/RopeModule/Utilities/_CharacterRecognizer.swift new file mode 100644 index 000000000..12b34d53f --- /dev/null +++ b/Sources/RopeModule/Utilities/_CharacterRecognizer.swift @@ -0,0 +1,172 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=5.8) + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +internal typealias _CharacterRecognizer = Unicode._CharacterRecognizer + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension _CharacterRecognizer { + internal func _isKnownEqual(to other: Self) -> Bool { + // FIXME: Enable when Swift 5.9 ships. +// #if swift(>=5.9) +// if #available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) { // SwiftStdlib 5.9 +// return self == other +// } +// #endif + return false + } +} + + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension _CharacterRecognizer { + mutating func firstBreak( + in str: Substring + ) -> Range? { + let r = str.utf8.withContiguousStorageIfAvailable { buffer in + self._firstBreak(inUncheckedUnsafeUTF8Buffer: buffer) + } + if let r { + guard let scalarRange = r else { return nil } + let lower = str._utf8Index(at: scalarRange.lowerBound) + let upper = str._utf8Index(at: scalarRange.upperBound) + return lower ..< upper + } + guard !str.isEmpty else { return nil } + + var i = str.startIndex + while i < str.endIndex { + let next = str.unicodeScalars.index(after: i) + let scalar = str.unicodeScalars[i] + if self.hasBreak(before: scalar) { + return i ..< next + } + i = next + } + return nil + } +} + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +extension _CharacterRecognizer { + init(partialCharacter: Substring.UnicodeScalarView) { + self.init() + var it = partialCharacter.makeIterator() + guard let first = it.next() else { return } + _ = hasBreak(before: first) + while let next = it.next() { + let b = hasBreak(before: next) + assert(!b) + } + } + + init(partialCharacter: Substring) { + self.init(partialCharacter: partialCharacter.unicodeScalars) + } + + mutating func consumePartialCharacter(_ s: String) { + for scalar in s.unicodeScalars { + let b = hasBreak(before: scalar) + assert(!b) + } + } + + mutating func consumePartialCharacter(_ s: Substring) { + for scalar in s.unicodeScalars { + let b = hasBreak(before: scalar) + assert(!b) + } + } + + mutating func consumePartialCharacter(_ s: Substring.UnicodeScalarView) { + for scalar in s { + let b = hasBreak(before: scalar) + assert(!b) + } + } + + mutating func consumeUntilFirstBreak( + in s: Substring.UnicodeScalarView, + from i: inout String.Index + ) -> String.Index? { + while i < s.endIndex { + defer { s.formIndex(after: &i) } + if hasBreak(before: s[i]) { + return i + } + } + return nil + } + + init(consuming str: some StringProtocol) { + self.init() + _ = self.consume(str) + } + + mutating func consume( + _ s: some StringProtocol + ) -> (characters: Int, firstBreak: String.Index, lastBreak: String.Index)? { + consume(Substring(s)) + } + + mutating func consume( + _ s: Substring + ) -> (characters: Int, firstBreak: String.Index, lastBreak: String.Index)? { + consume(s.unicodeScalars) + } + + mutating func consume( + _ s: Substring.UnicodeScalarView + ) -> (characters: Int, firstBreak: String.Index, lastBreak: String.Index)? { + var i = s.startIndex + guard let first = consumeUntilFirstBreak(in: s, from: &i) else { + return nil + } + var characters = 1 + var last = first + while let next = consumeUntilFirstBreak(in: s, from: &i) { + characters += 1 + last = next + } + return (characters, first, last) + } + + mutating func consume( + _ chunk: BigString._Chunk, upTo index: String.Index + ) -> (firstBreak: String.Index, prevBreak: String.Index)? { + let index = chunk.string.unicodeScalars._index(roundingDown: index) + let first = chunk.firstBreak + guard index > first else { + consumePartialCharacter(chunk.string[.. (characters: Int, prefixCount: Int, suffixCount: Int) { + let c = s.utf8.count + guard let (chars, first, last) = consume(s[...]) else { + return (0, c, c) + } + let prefix = s._utf8Offset(of: first) + let suffix = c - s._utf8Offset(of: last) + return (chars, prefix, suffix) + } +} + +#endif diff --git a/Sources/_CollectionsUtilities/CMakeLists.txt b/Sources/_CollectionsUtilities/CMakeLists.txt new file mode 100644 index 000000000..82c992068 --- /dev/null +++ b/Sources/_CollectionsUtilities/CMakeLists.txt @@ -0,0 +1,34 @@ +#[[ +This source file is part of the Swift Collections Open Source Project + +Copyright (c) 2022 - 2024 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 +#]] + +add_library(_CollectionsUtilities + "autogenerated/Debugging.swift" + "autogenerated/Descriptions.swift" + "autogenerated/RandomAccessCollection+Offsets.swift" + "autogenerated/Specialize.swift" + "autogenerated/UnsafeBufferPointer+Extras.swift" + "autogenerated/UnsafeMutableBufferPointer+Extras.swift" + "Compatibility/autogenerated/UnsafeMutableBufferPointer+SE-0370.swift" + "Compatibility/autogenerated/UnsafeMutablePointer+SE-0370.swift" + "Compatibility/autogenerated/UnsafeRawPointer extensions.swift" + "IntegerTricks/autogenerated/FixedWidthInteger+roundUpToPowerOfTwo.swift" + "IntegerTricks/autogenerated/Integer rank.swift" + "IntegerTricks/autogenerated/UInt+first and last set bit.swift" + "IntegerTricks/autogenerated/UInt+reversed.swift" + "UnsafeBitSet/autogenerated/_UnsafeBitSet+Index.swift" + "UnsafeBitSet/autogenerated/_UnsafeBitSet+_Word.swift" + "UnsafeBitSet/autogenerated/_UnsafeBitSet.swift" + "_SortedCollection.swift" + "_UniqueCollection.swift" + ) +set_target_properties(_CollectionsUtilities PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) + +_install_target(_CollectionsUtilities) +set_property(GLOBAL APPEND PROPERTY SWIFT_COLLECTIONS_EXPORTS _CollectionsUtilities) diff --git a/Sources/_CollectionsUtilities/Compatibility/UnsafeMutableBufferPointer+SE-0370.swift.gyb b/Sources/_CollectionsUtilities/Compatibility/UnsafeMutableBufferPointer+SE-0370.swift.gyb new file mode 100644 index 000000000..8e4f6debf --- /dev/null +++ b/Sources/_CollectionsUtilities/Compatibility/UnsafeMutableBufferPointer+SE-0370.swift.gyb @@ -0,0 +1,424 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +// Note: These are adapted from SE-0370 in the Swift 5.8 Standard Library. + +%{ + from gyb_utils import * +}% +${autogenerated_warning()} + +% for modifier in visibility_levels: +${visibility_boilerplate(modifier)} +#if swift(<5.8) +extension UnsafeMutableBufferPointer { + /// Deinitializes every instance in this buffer. + /// + /// The region of memory underlying this buffer must be fully initialized. + /// After calling `deinitialize(count:)`, the memory is uninitialized, + /// but still bound to the `Element` type. + /// + /// - Note: All buffer elements must already be initialized. + /// + /// - Returns: A raw buffer to the same range of memory as this buffer. + /// The range of memory is still bound to `Element`. + @discardableResult + @inlinable + ${modifier} func deinitialize() -> UnsafeMutableRawBufferPointer { + guard let start = baseAddress else { return .init(start: nil, count: 0) } + start.deinitialize(count: count) + return .init(start: UnsafeMutableRawPointer(start), + count: count * MemoryLayout.stride) + } +} +#endif + +// Note: this is left unconditionally enabled because we need the SR14663 workaround. :-( +extension UnsafeMutableBufferPointer { + /// Initializes the buffer's memory with + /// every element of the source. + /// + /// Prior to calling the `initialize(fromContentsOf:)` method on a buffer, + /// the memory referenced by the buffer must be uninitialized, + /// or the `Element` type must be a trivial type. After the call, + /// the memory referenced by the buffer up to, but not including, + /// the returned index is initialized. + /// The buffer must reference enough memory to accommodate + /// `source.count` elements. + /// + /// The returned index is the position of the next uninitialized element + /// in the buffer, one past the index of the last element written. + /// If `source` contains no elements, the returned index is equal to the + /// buffer's `startIndex`. If `source` contains as many elements as the buffer + /// can hold, the returned index is equal to the buffer's `endIndex`. + /// + /// - Precondition: `self.count` >= `source.count` + /// + /// - Note: The memory regions referenced by `source` and this buffer + /// must not overlap. + /// + /// - Parameter source: A collection of elements to be used to + /// initialize the buffer's storage. + /// - Returns: The index one past the last element of the buffer initialized + /// by this function. + @inlinable + ${modifier} func initialize( + fromContentsOf source: C + ) -> Index + where C.Element == Element { + let count: Int? = source.withContiguousStorageIfAvailable { + guard let sourceAddress = $0.baseAddress, !$0.isEmpty else { + return 0 + } + precondition( + $0.count <= self.count, + "buffer cannot contain every element from source." + ) + baseAddress?.initialize(from: sourceAddress, count: $0.count) + return $0.count + } + if let count = count { + return startIndex.advanced(by: count) + } + + var (iterator, copied) = source._copyContents(initializing: self) + precondition( + iterator.next() == nil, + "buffer cannot contain every element from source." + ) + return startIndex.advanced(by: copied) + } +} + +#if swift(<5.8) +extension UnsafeMutableBufferPointer { + /// Moves every element of an initialized source buffer into the + /// uninitialized memory referenced by this buffer, leaving the source memory + /// uninitialized and this buffer's memory initialized. + /// + /// Prior to calling the `moveInitialize(fromContentsOf:)` method on a buffer, + /// the memory it references must be uninitialized, + /// or its `Element` type must be a trivial type. After the call, + /// the memory referenced by the buffer up to, but not including, + /// the returned index is initialized. The memory referenced by + /// `source` is uninitialized after the function returns. + /// The buffer must reference enough memory to accommodate + /// `source.count` elements. + /// + /// The returned index is the position of the next uninitialized element + /// in the buffer, one past the index of the last element written. + /// If `source` contains no elements, the returned index is equal to the + /// buffer's `startIndex`. If `source` contains as many elements as the buffer + /// can hold, the returned index is equal to the buffer's `endIndex`. + /// + /// - Precondition: `self.count` >= `source.count` + /// + /// - Note: The memory regions referenced by `source` and this buffer + /// may overlap. + /// + /// - Parameter source: A buffer containing the values to copy. The memory + /// region underlying `source` must be initialized. + /// - Returns: The index one past the last element of the buffer initialized + /// by this function. + @inlinable + @_alwaysEmitIntoClient + ${modifier} func moveInitialize(fromContentsOf source: Self) -> Index { + guard let sourceAddress = source.baseAddress, !source.isEmpty else { + return startIndex + } + precondition( + source.count <= self.count, + "buffer cannot contain every element from source." + ) + baseAddress?.moveInitialize(from: sourceAddress, count: source.count) + return startIndex.advanced(by: source.count) + } + + /// Moves every element of an initialized source buffer into the + /// uninitialized memory referenced by this buffer, leaving the source memory + /// uninitialized and this buffer's memory initialized. + /// + /// Prior to calling the `moveInitialize(fromContentsOf:)` method on a buffer, + /// the memory it references must be uninitialized, + /// or its `Element` type must be a trivial type. After the call, + /// the memory referenced by the buffer up to, but not including, + /// the returned index is initialized. The memory referenced by + /// `source` is uninitialized after the function returns. + /// The buffer must reference enough memory to accommodate + /// `source.count` elements. + /// + /// The returned index is the position of the next uninitialized element + /// in the buffer, one past the index of the last element written. + /// If `source` contains no elements, the returned index is equal to the + /// buffer's `startIndex`. If `source` contains as many elements as the buffer + /// can hold, the returned index is equal to the buffer's `endIndex`. + /// + /// - Precondition: `self.count` >= `source.count` + /// + /// - Note: The memory regions referenced by `source` and this buffer + /// may overlap. + /// + /// - Parameter source: A buffer containing the values to copy. The memory + /// region underlying `source` must be initialized. + /// - Returns: The index one past the last element of the buffer initialized + /// by this function. + @inlinable + @_alwaysEmitIntoClient + ${modifier} func moveInitialize(fromContentsOf source: Slice) -> Index { + return moveInitialize(fromContentsOf: Self(rebasing: source)) + } + + /// Initializes the element at `index` to the given value. + /// + /// The memory underlying the destination element must be uninitialized, + /// or `Element` must be a trivial type. After a call to `initialize(to:)`, + /// the memory underlying this element of the buffer is initialized. + /// + /// - Parameters: + /// - value: The value used to initialize the buffer element's memory. + /// - index: The index of the element to initialize + @inlinable + @_alwaysEmitIntoClient + ${modifier} func initializeElement(at index: Index, to value: Element) { + assert(startIndex <= index && index < endIndex) + let p = baseAddress.unsafelyUnwrapped.advanced(by: index) + p.initialize(to: value) + } + + /// Retrieves and returns the element at `index`, + /// leaving that element's underlying memory uninitialized. + /// + /// The memory underlying the element at `index` must be initialized. + /// After calling `moveElement(from:)`, the memory underlying this element + /// of the buffer is uninitialized, and still bound to type `Element`. + /// + /// - Parameters: + /// - index: The index of the buffer element to retrieve and deinitialize. + /// - Returns: The instance referenced by this index in this buffer. + @inlinable + @_alwaysEmitIntoClient + ${modifier} func moveElement(from index: Index) -> Element { + assert(startIndex <= index && index < endIndex) + return baseAddress.unsafelyUnwrapped.advanced(by: index).move() + } + + /// Deinitializes the memory underlying the element at `index`. + /// + /// The memory underlying the element at `index` must be initialized. + /// After calling `deinitializeElement()`, the memory underlying this element + /// of the buffer is uninitialized, and still bound to type `Element`. + /// + /// - Parameters: + /// - index: The index of the buffer element to deinitialize. + @inlinable + @_alwaysEmitIntoClient + ${modifier} func deinitializeElement(at index: Index) { + assert(startIndex <= index && index < endIndex) + let p = baseAddress.unsafelyUnwrapped.advanced(by: index) + p.deinitialize(count: 1) + } +} +#endif + +#if swift(<5.8) +extension Slice { + /// Initializes the buffer slice's memory with with + /// every element of the source. + /// + /// Prior to calling the `initialize(fromContentsOf:)` method + /// on a buffer slice, the memory it references must be uninitialized, + /// or the `Element` type must be a trivial type. After the call, + /// the memory referenced by the buffer slice up to, but not including, + /// the returned index is initialized. + /// The buffer slice must reference enough memory to accommodate + /// `source.count` elements. + /// + /// The returned index is the index of the next uninitialized element + /// in the buffer slice, one past the index of the last element written. + /// If `source` contains no elements, the returned index is equal to + /// the buffer slice's `startIndex`. If `source` contains as many elements + /// as the buffer slice can hold, the returned index is equal to + /// to the slice's `endIndex`. + /// + /// - Precondition: `self.count` >= `source.count` + /// + /// - Note: The memory regions referenced by `source` and this buffer slice + /// must not overlap. + /// + /// - Parameter source: A collection of elements to be used to + /// initialize the buffer slice's storage. + /// - Returns: The index one past the last element of the buffer slice + /// initialized by this function. + @inlinable + @_alwaysEmitIntoClient + ${modifier} func initialize( + fromContentsOf source: C + ) -> Index where Base == UnsafeMutableBufferPointer { + let buffer = Base(rebasing: self) + let index = buffer.initialize(fromContentsOf: source) + let distance = buffer.distance(from: buffer.startIndex, to: index) + return startIndex.advanced(by: distance) + } + + /// Moves every element of an initialized source buffer into the + /// uninitialized memory referenced by this buffer slice, leaving the + /// source memory uninitialized and this buffer slice's memory initialized. + /// + /// Prior to calling the `moveInitialize(fromContentsOf:)` method on a + /// buffer slice, the memory it references must be uninitialized, + /// or its `Element` type must be a trivial type. After the call, + /// the memory referenced by the buffer slice up to, but not including, + /// the returned index is initialized. The memory referenced by + /// `source` is uninitialized after the function returns. + /// The buffer slice must reference enough memory to accommodate + /// `source.count` elements. + /// + /// The returned index is the position of the next uninitialized element + /// in the buffer slice, one past the index of the last element written. + /// If `source` contains no elements, the returned index is equal to the + /// slice's `startIndex`. If `source` contains as many elements as the slice + /// can hold, the returned index is equal to the slice's `endIndex`. + /// + /// - Note: The memory regions referenced by `source` and this buffer slice + /// may overlap. + /// + /// - Precondition: `self.count` >= `source.count` + /// + /// - Parameter source: A buffer containing the values to copy. + /// The memory region underlying `source` must be initialized. + /// - Returns: The index one past the last element of the buffer slice + /// initialized by this function. + @inlinable + @_alwaysEmitIntoClient + ${modifier} func moveInitialize( + fromContentsOf source: UnsafeMutableBufferPointer + ) -> Index where Base == UnsafeMutableBufferPointer { + let buffer = Base(rebasing: self) + let index = buffer.moveInitialize(fromContentsOf: source) + let distance = buffer.distance(from: buffer.startIndex, to: index) + return startIndex.advanced(by: distance) + } + + /// Moves every element of an initialized source buffer slice into the + /// uninitialized memory referenced by this buffer slice, leaving the + /// source memory uninitialized and this buffer slice's memory initialized. + /// + /// Prior to calling the `moveInitialize(fromContentsOf:)` method on a + /// buffer slice, the memory it references must be uninitialized, + /// or its `Element` type must be a trivial type. After the call, + /// the memory referenced by the buffer slice up to, but not including, + /// the returned index is initialized. The memory referenced by + /// `source` is uninitialized after the function returns. + /// The buffer slice must reference enough memory to accommodate + /// `source.count` elements. + /// + /// The returned index is the position of the next uninitialized element + /// in the buffer slice, one past the index of the last element written. + /// If `source` contains no elements, the returned index is equal to the + /// slice's `startIndex`. If `source` contains as many elements as the slice + /// can hold, the returned index is equal to the slice's `endIndex`. + /// + /// - Note: The memory regions referenced by `source` and this buffer slice + /// may overlap. + /// + /// - Precondition: `self.count` >= `source.count` + /// + /// - Parameter source: A buffer slice containing the values to copy. + /// The memory region underlying `source` must be initialized. + /// - Returns: The index one past the last element of the buffer slice + /// initialized by this function. + @inlinable + @_alwaysEmitIntoClient + ${modifier} func moveInitialize( + fromContentsOf source: Slice> + ) -> Index where Base == UnsafeMutableBufferPointer { + let buffer = Base(rebasing: self) + let index = buffer.moveInitialize(fromContentsOf: source) + let distance = buffer.distance(from: buffer.startIndex, to: index) + return startIndex.advanced(by: distance) + } + + /// Deinitializes every instance in this buffer slice. + /// + /// The region of memory underlying this buffer slice must be fully + /// initialized. After calling `deinitialize(count:)`, the memory + /// is uninitialized, but still bound to the `Element` type. + /// + /// - Note: All buffer elements must already be initialized. + /// + /// - Returns: A raw buffer to the same range of memory as this buffer. + /// The range of memory is still bound to `Element`. + @discardableResult + @inlinable + @_alwaysEmitIntoClient + ${modifier} func deinitialize() -> UnsafeMutableRawBufferPointer + where Base == UnsafeMutableBufferPointer { + Base(rebasing: self).deinitialize() + } + + /// Initializes the element at `index` to the given value. + /// + /// The memory underlying the destination element must be uninitialized, + /// or `Element` must be a trivial type. After a call to `initialize(to:)`, + /// the memory underlying this element of the buffer slice is initialized. + /// + /// - Parameters: + /// - value: The value used to initialize the buffer element's memory. + /// - index: The index of the element to initialize + @inlinable + @_alwaysEmitIntoClient + ${modifier} func initializeElement(at index: Int, to value: Element) + where Base == UnsafeMutableBufferPointer { + assert(startIndex <= index && index < endIndex) + base.baseAddress.unsafelyUnwrapped.advanced(by: index).initialize(to: value) + } +} +#endif + +#if swift(<5.8) +extension UnsafeMutableBufferPointer { + /// Updates every element of this buffer's initialized memory. + /// + /// The buffer’s memory must be initialized or its `Element` type + /// must be a trivial type. + /// + /// - Note: All buffer elements must already be initialized. + /// + /// - Parameters: + /// - repeatedValue: The value used when updating this pointer's memory. + @_alwaysEmitIntoClient + ${modifier} func update(repeating repeatedValue: Element) { + guard let dstBase = baseAddress else { return } + dstBase.update(repeating: repeatedValue, count: count) + } +} +#endif + +#if swift(<5.8) +extension Slice { + /// Updates every element of this buffer slice's initialized memory. + /// + /// The buffer slice’s memory must be initialized or its `Element` + /// must be a trivial type. + /// + /// - Note: All buffer elements must already be initialized. + /// + /// - Parameters: + /// - repeatedValue: The value used when updating this pointer's memory. + @_alwaysEmitIntoClient + ${modifier} func update(repeating repeatedValue: Element) + where Base == UnsafeMutableBufferPointer { + Base(rebasing: self).update(repeating: repeatedValue) + } +} +#endif +% end +${visibility_boilerplate("end")} diff --git a/Sources/_CollectionsUtilities/Compatibility/UnsafeMutablePointer+SE-0370.swift.gyb b/Sources/_CollectionsUtilities/Compatibility/UnsafeMutablePointer+SE-0370.swift.gyb new file mode 100644 index 000000000..d95199cf7 --- /dev/null +++ b/Sources/_CollectionsUtilities/Compatibility/UnsafeMutablePointer+SE-0370.swift.gyb @@ -0,0 +1,43 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +%{ + from gyb_utils import * +}% +${autogenerated_warning()} + +% for modifier in visibility_levels: +${visibility_boilerplate(modifier)} +#if swift(<5.8) +extension UnsafeMutablePointer { + /// Update this pointer's initialized memory with the specified number of + /// consecutive copies of the given value. + /// + /// The region of memory starting at this pointer and covering `count` + /// instances of the pointer's `Pointee` type must be initialized or + /// `Pointee` must be a trivial type. After calling + /// `update(repeating:count:)`, the region is initialized. + /// + /// - Parameters: + /// - repeatedValue: The value used when updating this pointer's memory. + /// - count: The number of consecutive elements to update. + /// `count` must not be negative. + @_alwaysEmitIntoClient + ${modifier} func update(repeating repeatedValue: Pointee, count: Int) { + assert(count >= 0, "UnsafeMutablePointer.update(repeating:count:) with negative count") + for i in 0 ..< count { + self[i] = repeatedValue + } + } +} +#endif +% end +${visibility_boilerplate("end")} diff --git a/Sources/_CollectionsUtilities/Compatibility/UnsafeRawPointer extensions.swift.gyb b/Sources/_CollectionsUtilities/Compatibility/UnsafeRawPointer extensions.swift.gyb new file mode 100644 index 000000000..68c2eedcd --- /dev/null +++ b/Sources/_CollectionsUtilities/Compatibility/UnsafeRawPointer extensions.swift.gyb @@ -0,0 +1,89 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +%{ + from gyb_utils import * +}% +${autogenerated_warning()} + +% for modifier in visibility_levels: +${visibility_boilerplate(modifier)} +#if compiler(<5.7) || (os(macOS) && compiler(<5.8)) // SE-0334 +extension UnsafeRawPointer { + /// Obtain the next pointer properly aligned to store a value of type `T`. + /// + /// If `self` is properly aligned for accessing `T`, + /// this function returns `self`. + /// + /// - Parameters: + /// - type: the type to be stored at the returned address. + /// - Returns: a pointer properly aligned to store a value of type `T`. + @inlinable + @_alwaysEmitIntoClient + ${modifier} func alignedUp(for type: T.Type) -> Self { + let mask = UInt(MemoryLayout.alignment) &- 1 + let bits = (UInt(bitPattern: self) &+ mask) & ~mask + return Self(bitPattern: bits)! + } + + /// Obtain the preceding pointer properly aligned to store a value of type `T`. + /// + /// If `self` is properly aligned for accessing `T`, + /// this function returns `self`. + /// + /// - Parameters: + /// - type: the type to be stored at the returned address. + /// - Returns: a pointer properly aligned to store a value of type `T`. + @inlinable + @_alwaysEmitIntoClient + ${modifier} func alignedDown(for type: T.Type) -> Self { + let mask = UInt(MemoryLayout.alignment) &- 1 + let bits = UInt(bitPattern: self) & ~mask + return Self(bitPattern: bits)! + } +} + +extension UnsafeMutableRawPointer { + /// Obtain the next pointer properly aligned to store a value of type `T`. + /// + /// If `self` is properly aligned for accessing `T`, + /// this function returns `self`. + /// + /// - Parameters: + /// - type: the type to be stored at the returned address. + /// - Returns: a pointer properly aligned to store a value of type `T`. + @inlinable + @_alwaysEmitIntoClient + ${modifier} func alignedUp(for type: T.Type) -> Self { + let mask = UInt(MemoryLayout.alignment) &- 1 + let bits = (UInt(bitPattern: self) &+ mask) & ~mask + return Self(bitPattern: bits)! + } + + /// Obtain the preceding pointer properly aligned to store a value of type `T`. + /// + /// If `self` is properly aligned for accessing `T`, + /// this function returns `self`. + /// + /// - Parameters: + /// - type: the type to be stored at the returned address. + /// - Returns: a pointer properly aligned to store a value of type `T`. + @inlinable + @_alwaysEmitIntoClient + ${modifier} func alignedDown(for type: T.Type) -> Self { + let mask = UInt(MemoryLayout.alignment) &- 1 + let bits = UInt(bitPattern: self) & ~mask + return Self(bitPattern: bits)! + } +} +#endif +% end +${visibility_boilerplate("end")} diff --git a/Sources/_CollectionsUtilities/Compatibility/autogenerated/UnsafeMutableBufferPointer+SE-0370.swift b/Sources/_CollectionsUtilities/Compatibility/autogenerated/UnsafeMutableBufferPointer+SE-0370.swift new file mode 100644 index 000000000..9068f8b91 --- /dev/null +++ b/Sources/_CollectionsUtilities/Compatibility/autogenerated/UnsafeMutableBufferPointer+SE-0370.swift @@ -0,0 +1,832 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +// Note: These are adapted from SE-0370 in the Swift 5.8 Standard Library. + + +// ############################################################################# +// # # +// # DO NOT EDIT THIS FILE; IT IS AUTOGENERATED. # +// # # +// ############################################################################# + + + +// In single module mode, we need these declarations to be internal, +// but in regular builds we want them to be public. Unfortunately +// the current best way to do this is to duplicate all definitions. +#if COLLECTIONS_SINGLE_MODULE +#if swift(<5.8) +extension UnsafeMutableBufferPointer { + /// Deinitializes every instance in this buffer. + /// + /// The region of memory underlying this buffer must be fully initialized. + /// After calling `deinitialize(count:)`, the memory is uninitialized, + /// but still bound to the `Element` type. + /// + /// - Note: All buffer elements must already be initialized. + /// + /// - Returns: A raw buffer to the same range of memory as this buffer. + /// The range of memory is still bound to `Element`. + @discardableResult + @inlinable + internal func deinitialize() -> UnsafeMutableRawBufferPointer { + guard let start = baseAddress else { return .init(start: nil, count: 0) } + start.deinitialize(count: count) + return .init(start: UnsafeMutableRawPointer(start), + count: count * MemoryLayout.stride) + } +} +#endif + +// Note: this is left unconditionally enabled because we need the SR14663 workaround. :-( +extension UnsafeMutableBufferPointer { + /// Initializes the buffer's memory with + /// every element of the source. + /// + /// Prior to calling the `initialize(fromContentsOf:)` method on a buffer, + /// the memory referenced by the buffer must be uninitialized, + /// or the `Element` type must be a trivial type. After the call, + /// the memory referenced by the buffer up to, but not including, + /// the returned index is initialized. + /// The buffer must reference enough memory to accommodate + /// `source.count` elements. + /// + /// The returned index is the position of the next uninitialized element + /// in the buffer, one past the index of the last element written. + /// If `source` contains no elements, the returned index is equal to the + /// buffer's `startIndex`. If `source` contains as many elements as the buffer + /// can hold, the returned index is equal to the buffer's `endIndex`. + /// + /// - Precondition: `self.count` >= `source.count` + /// + /// - Note: The memory regions referenced by `source` and this buffer + /// must not overlap. + /// + /// - Parameter source: A collection of elements to be used to + /// initialize the buffer's storage. + /// - Returns: The index one past the last element of the buffer initialized + /// by this function. + @inlinable + internal func initialize( + fromContentsOf source: C + ) -> Index + where C.Element == Element { + let count: Int? = source.withContiguousStorageIfAvailable { + guard let sourceAddress = $0.baseAddress, !$0.isEmpty else { + return 0 + } + precondition( + $0.count <= self.count, + "buffer cannot contain every element from source." + ) + baseAddress?.initialize(from: sourceAddress, count: $0.count) + return $0.count + } + if let count = count { + return startIndex.advanced(by: count) + } + + var (iterator, copied) = source._copyContents(initializing: self) + precondition( + iterator.next() == nil, + "buffer cannot contain every element from source." + ) + return startIndex.advanced(by: copied) + } +} + +#if swift(<5.8) +extension UnsafeMutableBufferPointer { + /// Moves every element of an initialized source buffer into the + /// uninitialized memory referenced by this buffer, leaving the source memory + /// uninitialized and this buffer's memory initialized. + /// + /// Prior to calling the `moveInitialize(fromContentsOf:)` method on a buffer, + /// the memory it references must be uninitialized, + /// or its `Element` type must be a trivial type. After the call, + /// the memory referenced by the buffer up to, but not including, + /// the returned index is initialized. The memory referenced by + /// `source` is uninitialized after the function returns. + /// The buffer must reference enough memory to accommodate + /// `source.count` elements. + /// + /// The returned index is the position of the next uninitialized element + /// in the buffer, one past the index of the last element written. + /// If `source` contains no elements, the returned index is equal to the + /// buffer's `startIndex`. If `source` contains as many elements as the buffer + /// can hold, the returned index is equal to the buffer's `endIndex`. + /// + /// - Precondition: `self.count` >= `source.count` + /// + /// - Note: The memory regions referenced by `source` and this buffer + /// may overlap. + /// + /// - Parameter source: A buffer containing the values to copy. The memory + /// region underlying `source` must be initialized. + /// - Returns: The index one past the last element of the buffer initialized + /// by this function. + @inlinable + @_alwaysEmitIntoClient + internal func moveInitialize(fromContentsOf source: Self) -> Index { + guard let sourceAddress = source.baseAddress, !source.isEmpty else { + return startIndex + } + precondition( + source.count <= self.count, + "buffer cannot contain every element from source." + ) + baseAddress?.moveInitialize(from: sourceAddress, count: source.count) + return startIndex.advanced(by: source.count) + } + + /// Moves every element of an initialized source buffer into the + /// uninitialized memory referenced by this buffer, leaving the source memory + /// uninitialized and this buffer's memory initialized. + /// + /// Prior to calling the `moveInitialize(fromContentsOf:)` method on a buffer, + /// the memory it references must be uninitialized, + /// or its `Element` type must be a trivial type. After the call, + /// the memory referenced by the buffer up to, but not including, + /// the returned index is initialized. The memory referenced by + /// `source` is uninitialized after the function returns. + /// The buffer must reference enough memory to accommodate + /// `source.count` elements. + /// + /// The returned index is the position of the next uninitialized element + /// in the buffer, one past the index of the last element written. + /// If `source` contains no elements, the returned index is equal to the + /// buffer's `startIndex`. If `source` contains as many elements as the buffer + /// can hold, the returned index is equal to the buffer's `endIndex`. + /// + /// - Precondition: `self.count` >= `source.count` + /// + /// - Note: The memory regions referenced by `source` and this buffer + /// may overlap. + /// + /// - Parameter source: A buffer containing the values to copy. The memory + /// region underlying `source` must be initialized. + /// - Returns: The index one past the last element of the buffer initialized + /// by this function. + @inlinable + @_alwaysEmitIntoClient + internal func moveInitialize(fromContentsOf source: Slice) -> Index { + return moveInitialize(fromContentsOf: Self(rebasing: source)) + } + + /// Initializes the element at `index` to the given value. + /// + /// The memory underlying the destination element must be uninitialized, + /// or `Element` must be a trivial type. After a call to `initialize(to:)`, + /// the memory underlying this element of the buffer is initialized. + /// + /// - Parameters: + /// - value: The value used to initialize the buffer element's memory. + /// - index: The index of the element to initialize + @inlinable + @_alwaysEmitIntoClient + internal func initializeElement(at index: Index, to value: Element) { + assert(startIndex <= index && index < endIndex) + let p = baseAddress.unsafelyUnwrapped.advanced(by: index) + p.initialize(to: value) + } + + /// Retrieves and returns the element at `index`, + /// leaving that element's underlying memory uninitialized. + /// + /// The memory underlying the element at `index` must be initialized. + /// After calling `moveElement(from:)`, the memory underlying this element + /// of the buffer is uninitialized, and still bound to type `Element`. + /// + /// - Parameters: + /// - index: The index of the buffer element to retrieve and deinitialize. + /// - Returns: The instance referenced by this index in this buffer. + @inlinable + @_alwaysEmitIntoClient + internal func moveElement(from index: Index) -> Element { + assert(startIndex <= index && index < endIndex) + return baseAddress.unsafelyUnwrapped.advanced(by: index).move() + } + + /// Deinitializes the memory underlying the element at `index`. + /// + /// The memory underlying the element at `index` must be initialized. + /// After calling `deinitializeElement()`, the memory underlying this element + /// of the buffer is uninitialized, and still bound to type `Element`. + /// + /// - Parameters: + /// - index: The index of the buffer element to deinitialize. + @inlinable + @_alwaysEmitIntoClient + internal func deinitializeElement(at index: Index) { + assert(startIndex <= index && index < endIndex) + let p = baseAddress.unsafelyUnwrapped.advanced(by: index) + p.deinitialize(count: 1) + } +} +#endif + +#if swift(<5.8) +extension Slice { + /// Initializes the buffer slice's memory with with + /// every element of the source. + /// + /// Prior to calling the `initialize(fromContentsOf:)` method + /// on a buffer slice, the memory it references must be uninitialized, + /// or the `Element` type must be a trivial type. After the call, + /// the memory referenced by the buffer slice up to, but not including, + /// the returned index is initialized. + /// The buffer slice must reference enough memory to accommodate + /// `source.count` elements. + /// + /// The returned index is the index of the next uninitialized element + /// in the buffer slice, one past the index of the last element written. + /// If `source` contains no elements, the returned index is equal to + /// the buffer slice's `startIndex`. If `source` contains as many elements + /// as the buffer slice can hold, the returned index is equal to + /// to the slice's `endIndex`. + /// + /// - Precondition: `self.count` >= `source.count` + /// + /// - Note: The memory regions referenced by `source` and this buffer slice + /// must not overlap. + /// + /// - Parameter source: A collection of elements to be used to + /// initialize the buffer slice's storage. + /// - Returns: The index one past the last element of the buffer slice + /// initialized by this function. + @inlinable + @_alwaysEmitIntoClient + internal func initialize( + fromContentsOf source: C + ) -> Index where Base == UnsafeMutableBufferPointer { + let buffer = Base(rebasing: self) + let index = buffer.initialize(fromContentsOf: source) + let distance = buffer.distance(from: buffer.startIndex, to: index) + return startIndex.advanced(by: distance) + } + + /// Moves every element of an initialized source buffer into the + /// uninitialized memory referenced by this buffer slice, leaving the + /// source memory uninitialized and this buffer slice's memory initialized. + /// + /// Prior to calling the `moveInitialize(fromContentsOf:)` method on a + /// buffer slice, the memory it references must be uninitialized, + /// or its `Element` type must be a trivial type. After the call, + /// the memory referenced by the buffer slice up to, but not including, + /// the returned index is initialized. The memory referenced by + /// `source` is uninitialized after the function returns. + /// The buffer slice must reference enough memory to accommodate + /// `source.count` elements. + /// + /// The returned index is the position of the next uninitialized element + /// in the buffer slice, one past the index of the last element written. + /// If `source` contains no elements, the returned index is equal to the + /// slice's `startIndex`. If `source` contains as many elements as the slice + /// can hold, the returned index is equal to the slice's `endIndex`. + /// + /// - Note: The memory regions referenced by `source` and this buffer slice + /// may overlap. + /// + /// - Precondition: `self.count` >= `source.count` + /// + /// - Parameter source: A buffer containing the values to copy. + /// The memory region underlying `source` must be initialized. + /// - Returns: The index one past the last element of the buffer slice + /// initialized by this function. + @inlinable + @_alwaysEmitIntoClient + internal func moveInitialize( + fromContentsOf source: UnsafeMutableBufferPointer + ) -> Index where Base == UnsafeMutableBufferPointer { + let buffer = Base(rebasing: self) + let index = buffer.moveInitialize(fromContentsOf: source) + let distance = buffer.distance(from: buffer.startIndex, to: index) + return startIndex.advanced(by: distance) + } + + /// Moves every element of an initialized source buffer slice into the + /// uninitialized memory referenced by this buffer slice, leaving the + /// source memory uninitialized and this buffer slice's memory initialized. + /// + /// Prior to calling the `moveInitialize(fromContentsOf:)` method on a + /// buffer slice, the memory it references must be uninitialized, + /// or its `Element` type must be a trivial type. After the call, + /// the memory referenced by the buffer slice up to, but not including, + /// the returned index is initialized. The memory referenced by + /// `source` is uninitialized after the function returns. + /// The buffer slice must reference enough memory to accommodate + /// `source.count` elements. + /// + /// The returned index is the position of the next uninitialized element + /// in the buffer slice, one past the index of the last element written. + /// If `source` contains no elements, the returned index is equal to the + /// slice's `startIndex`. If `source` contains as many elements as the slice + /// can hold, the returned index is equal to the slice's `endIndex`. + /// + /// - Note: The memory regions referenced by `source` and this buffer slice + /// may overlap. + /// + /// - Precondition: `self.count` >= `source.count` + /// + /// - Parameter source: A buffer slice containing the values to copy. + /// The memory region underlying `source` must be initialized. + /// - Returns: The index one past the last element of the buffer slice + /// initialized by this function. + @inlinable + @_alwaysEmitIntoClient + internal func moveInitialize( + fromContentsOf source: Slice> + ) -> Index where Base == UnsafeMutableBufferPointer { + let buffer = Base(rebasing: self) + let index = buffer.moveInitialize(fromContentsOf: source) + let distance = buffer.distance(from: buffer.startIndex, to: index) + return startIndex.advanced(by: distance) + } + + /// Deinitializes every instance in this buffer slice. + /// + /// The region of memory underlying this buffer slice must be fully + /// initialized. After calling `deinitialize(count:)`, the memory + /// is uninitialized, but still bound to the `Element` type. + /// + /// - Note: All buffer elements must already be initialized. + /// + /// - Returns: A raw buffer to the same range of memory as this buffer. + /// The range of memory is still bound to `Element`. + @discardableResult + @inlinable + @_alwaysEmitIntoClient + internal func deinitialize() -> UnsafeMutableRawBufferPointer + where Base == UnsafeMutableBufferPointer { + Base(rebasing: self).deinitialize() + } + + /// Initializes the element at `index` to the given value. + /// + /// The memory underlying the destination element must be uninitialized, + /// or `Element` must be a trivial type. After a call to `initialize(to:)`, + /// the memory underlying this element of the buffer slice is initialized. + /// + /// - Parameters: + /// - value: The value used to initialize the buffer element's memory. + /// - index: The index of the element to initialize + @inlinable + @_alwaysEmitIntoClient + internal func initializeElement(at index: Int, to value: Element) + where Base == UnsafeMutableBufferPointer { + assert(startIndex <= index && index < endIndex) + base.baseAddress.unsafelyUnwrapped.advanced(by: index).initialize(to: value) + } +} +#endif + +#if swift(<5.8) +extension UnsafeMutableBufferPointer { + /// Updates every element of this buffer's initialized memory. + /// + /// The buffer’s memory must be initialized or its `Element` type + /// must be a trivial type. + /// + /// - Note: All buffer elements must already be initialized. + /// + /// - Parameters: + /// - repeatedValue: The value used when updating this pointer's memory. + @_alwaysEmitIntoClient + internal func update(repeating repeatedValue: Element) { + guard let dstBase = baseAddress else { return } + dstBase.update(repeating: repeatedValue, count: count) + } +} +#endif + +#if swift(<5.8) +extension Slice { + /// Updates every element of this buffer slice's initialized memory. + /// + /// The buffer slice’s memory must be initialized or its `Element` + /// must be a trivial type. + /// + /// - Note: All buffer elements must already be initialized. + /// + /// - Parameters: + /// - repeatedValue: The value used when updating this pointer's memory. + @_alwaysEmitIntoClient + internal func update(repeating repeatedValue: Element) + where Base == UnsafeMutableBufferPointer { + Base(rebasing: self).update(repeating: repeatedValue) + } +} +#endif +#else // !COLLECTIONS_SINGLE_MODULE +#if swift(<5.8) +extension UnsafeMutableBufferPointer { + /// Deinitializes every instance in this buffer. + /// + /// The region of memory underlying this buffer must be fully initialized. + /// After calling `deinitialize(count:)`, the memory is uninitialized, + /// but still bound to the `Element` type. + /// + /// - Note: All buffer elements must already be initialized. + /// + /// - Returns: A raw buffer to the same range of memory as this buffer. + /// The range of memory is still bound to `Element`. + @discardableResult + @inlinable + public func deinitialize() -> UnsafeMutableRawBufferPointer { + guard let start = baseAddress else { return .init(start: nil, count: 0) } + start.deinitialize(count: count) + return .init(start: UnsafeMutableRawPointer(start), + count: count * MemoryLayout.stride) + } +} +#endif + +// Note: this is left unconditionally enabled because we need the SR14663 workaround. :-( +extension UnsafeMutableBufferPointer { + /// Initializes the buffer's memory with + /// every element of the source. + /// + /// Prior to calling the `initialize(fromContentsOf:)` method on a buffer, + /// the memory referenced by the buffer must be uninitialized, + /// or the `Element` type must be a trivial type. After the call, + /// the memory referenced by the buffer up to, but not including, + /// the returned index is initialized. + /// The buffer must reference enough memory to accommodate + /// `source.count` elements. + /// + /// The returned index is the position of the next uninitialized element + /// in the buffer, one past the index of the last element written. + /// If `source` contains no elements, the returned index is equal to the + /// buffer's `startIndex`. If `source` contains as many elements as the buffer + /// can hold, the returned index is equal to the buffer's `endIndex`. + /// + /// - Precondition: `self.count` >= `source.count` + /// + /// - Note: The memory regions referenced by `source` and this buffer + /// must not overlap. + /// + /// - Parameter source: A collection of elements to be used to + /// initialize the buffer's storage. + /// - Returns: The index one past the last element of the buffer initialized + /// by this function. + @inlinable + public func initialize( + fromContentsOf source: C + ) -> Index + where C.Element == Element { + let count: Int? = source.withContiguousStorageIfAvailable { + guard let sourceAddress = $0.baseAddress, !$0.isEmpty else { + return 0 + } + precondition( + $0.count <= self.count, + "buffer cannot contain every element from source." + ) + baseAddress?.initialize(from: sourceAddress, count: $0.count) + return $0.count + } + if let count = count { + return startIndex.advanced(by: count) + } + + var (iterator, copied) = source._copyContents(initializing: self) + precondition( + iterator.next() == nil, + "buffer cannot contain every element from source." + ) + return startIndex.advanced(by: copied) + } +} + +#if swift(<5.8) +extension UnsafeMutableBufferPointer { + /// Moves every element of an initialized source buffer into the + /// uninitialized memory referenced by this buffer, leaving the source memory + /// uninitialized and this buffer's memory initialized. + /// + /// Prior to calling the `moveInitialize(fromContentsOf:)` method on a buffer, + /// the memory it references must be uninitialized, + /// or its `Element` type must be a trivial type. After the call, + /// the memory referenced by the buffer up to, but not including, + /// the returned index is initialized. The memory referenced by + /// `source` is uninitialized after the function returns. + /// The buffer must reference enough memory to accommodate + /// `source.count` elements. + /// + /// The returned index is the position of the next uninitialized element + /// in the buffer, one past the index of the last element written. + /// If `source` contains no elements, the returned index is equal to the + /// buffer's `startIndex`. If `source` contains as many elements as the buffer + /// can hold, the returned index is equal to the buffer's `endIndex`. + /// + /// - Precondition: `self.count` >= `source.count` + /// + /// - Note: The memory regions referenced by `source` and this buffer + /// may overlap. + /// + /// - Parameter source: A buffer containing the values to copy. The memory + /// region underlying `source` must be initialized. + /// - Returns: The index one past the last element of the buffer initialized + /// by this function. + @inlinable + @_alwaysEmitIntoClient + public func moveInitialize(fromContentsOf source: Self) -> Index { + guard let sourceAddress = source.baseAddress, !source.isEmpty else { + return startIndex + } + precondition( + source.count <= self.count, + "buffer cannot contain every element from source." + ) + baseAddress?.moveInitialize(from: sourceAddress, count: source.count) + return startIndex.advanced(by: source.count) + } + + /// Moves every element of an initialized source buffer into the + /// uninitialized memory referenced by this buffer, leaving the source memory + /// uninitialized and this buffer's memory initialized. + /// + /// Prior to calling the `moveInitialize(fromContentsOf:)` method on a buffer, + /// the memory it references must be uninitialized, + /// or its `Element` type must be a trivial type. After the call, + /// the memory referenced by the buffer up to, but not including, + /// the returned index is initialized. The memory referenced by + /// `source` is uninitialized after the function returns. + /// The buffer must reference enough memory to accommodate + /// `source.count` elements. + /// + /// The returned index is the position of the next uninitialized element + /// in the buffer, one past the index of the last element written. + /// If `source` contains no elements, the returned index is equal to the + /// buffer's `startIndex`. If `source` contains as many elements as the buffer + /// can hold, the returned index is equal to the buffer's `endIndex`. + /// + /// - Precondition: `self.count` >= `source.count` + /// + /// - Note: The memory regions referenced by `source` and this buffer + /// may overlap. + /// + /// - Parameter source: A buffer containing the values to copy. The memory + /// region underlying `source` must be initialized. + /// - Returns: The index one past the last element of the buffer initialized + /// by this function. + @inlinable + @_alwaysEmitIntoClient + public func moveInitialize(fromContentsOf source: Slice) -> Index { + return moveInitialize(fromContentsOf: Self(rebasing: source)) + } + + /// Initializes the element at `index` to the given value. + /// + /// The memory underlying the destination element must be uninitialized, + /// or `Element` must be a trivial type. After a call to `initialize(to:)`, + /// the memory underlying this element of the buffer is initialized. + /// + /// - Parameters: + /// - value: The value used to initialize the buffer element's memory. + /// - index: The index of the element to initialize + @inlinable + @_alwaysEmitIntoClient + public func initializeElement(at index: Index, to value: Element) { + assert(startIndex <= index && index < endIndex) + let p = baseAddress.unsafelyUnwrapped.advanced(by: index) + p.initialize(to: value) + } + + /// Retrieves and returns the element at `index`, + /// leaving that element's underlying memory uninitialized. + /// + /// The memory underlying the element at `index` must be initialized. + /// After calling `moveElement(from:)`, the memory underlying this element + /// of the buffer is uninitialized, and still bound to type `Element`. + /// + /// - Parameters: + /// - index: The index of the buffer element to retrieve and deinitialize. + /// - Returns: The instance referenced by this index in this buffer. + @inlinable + @_alwaysEmitIntoClient + public func moveElement(from index: Index) -> Element { + assert(startIndex <= index && index < endIndex) + return baseAddress.unsafelyUnwrapped.advanced(by: index).move() + } + + /// Deinitializes the memory underlying the element at `index`. + /// + /// The memory underlying the element at `index` must be initialized. + /// After calling `deinitializeElement()`, the memory underlying this element + /// of the buffer is uninitialized, and still bound to type `Element`. + /// + /// - Parameters: + /// - index: The index of the buffer element to deinitialize. + @inlinable + @_alwaysEmitIntoClient + public func deinitializeElement(at index: Index) { + assert(startIndex <= index && index < endIndex) + let p = baseAddress.unsafelyUnwrapped.advanced(by: index) + p.deinitialize(count: 1) + } +} +#endif + +#if swift(<5.8) +extension Slice { + /// Initializes the buffer slice's memory with with + /// every element of the source. + /// + /// Prior to calling the `initialize(fromContentsOf:)` method + /// on a buffer slice, the memory it references must be uninitialized, + /// or the `Element` type must be a trivial type. After the call, + /// the memory referenced by the buffer slice up to, but not including, + /// the returned index is initialized. + /// The buffer slice must reference enough memory to accommodate + /// `source.count` elements. + /// + /// The returned index is the index of the next uninitialized element + /// in the buffer slice, one past the index of the last element written. + /// If `source` contains no elements, the returned index is equal to + /// the buffer slice's `startIndex`. If `source` contains as many elements + /// as the buffer slice can hold, the returned index is equal to + /// to the slice's `endIndex`. + /// + /// - Precondition: `self.count` >= `source.count` + /// + /// - Note: The memory regions referenced by `source` and this buffer slice + /// must not overlap. + /// + /// - Parameter source: A collection of elements to be used to + /// initialize the buffer slice's storage. + /// - Returns: The index one past the last element of the buffer slice + /// initialized by this function. + @inlinable + @_alwaysEmitIntoClient + public func initialize( + fromContentsOf source: C + ) -> Index where Base == UnsafeMutableBufferPointer { + let buffer = Base(rebasing: self) + let index = buffer.initialize(fromContentsOf: source) + let distance = buffer.distance(from: buffer.startIndex, to: index) + return startIndex.advanced(by: distance) + } + + /// Moves every element of an initialized source buffer into the + /// uninitialized memory referenced by this buffer slice, leaving the + /// source memory uninitialized and this buffer slice's memory initialized. + /// + /// Prior to calling the `moveInitialize(fromContentsOf:)` method on a + /// buffer slice, the memory it references must be uninitialized, + /// or its `Element` type must be a trivial type. After the call, + /// the memory referenced by the buffer slice up to, but not including, + /// the returned index is initialized. The memory referenced by + /// `source` is uninitialized after the function returns. + /// The buffer slice must reference enough memory to accommodate + /// `source.count` elements. + /// + /// The returned index is the position of the next uninitialized element + /// in the buffer slice, one past the index of the last element written. + /// If `source` contains no elements, the returned index is equal to the + /// slice's `startIndex`. If `source` contains as many elements as the slice + /// can hold, the returned index is equal to the slice's `endIndex`. + /// + /// - Note: The memory regions referenced by `source` and this buffer slice + /// may overlap. + /// + /// - Precondition: `self.count` >= `source.count` + /// + /// - Parameter source: A buffer containing the values to copy. + /// The memory region underlying `source` must be initialized. + /// - Returns: The index one past the last element of the buffer slice + /// initialized by this function. + @inlinable + @_alwaysEmitIntoClient + public func moveInitialize( + fromContentsOf source: UnsafeMutableBufferPointer + ) -> Index where Base == UnsafeMutableBufferPointer { + let buffer = Base(rebasing: self) + let index = buffer.moveInitialize(fromContentsOf: source) + let distance = buffer.distance(from: buffer.startIndex, to: index) + return startIndex.advanced(by: distance) + } + + /// Moves every element of an initialized source buffer slice into the + /// uninitialized memory referenced by this buffer slice, leaving the + /// source memory uninitialized and this buffer slice's memory initialized. + /// + /// Prior to calling the `moveInitialize(fromContentsOf:)` method on a + /// buffer slice, the memory it references must be uninitialized, + /// or its `Element` type must be a trivial type. After the call, + /// the memory referenced by the buffer slice up to, but not including, + /// the returned index is initialized. The memory referenced by + /// `source` is uninitialized after the function returns. + /// The buffer slice must reference enough memory to accommodate + /// `source.count` elements. + /// + /// The returned index is the position of the next uninitialized element + /// in the buffer slice, one past the index of the last element written. + /// If `source` contains no elements, the returned index is equal to the + /// slice's `startIndex`. If `source` contains as many elements as the slice + /// can hold, the returned index is equal to the slice's `endIndex`. + /// + /// - Note: The memory regions referenced by `source` and this buffer slice + /// may overlap. + /// + /// - Precondition: `self.count` >= `source.count` + /// + /// - Parameter source: A buffer slice containing the values to copy. + /// The memory region underlying `source` must be initialized. + /// - Returns: The index one past the last element of the buffer slice + /// initialized by this function. + @inlinable + @_alwaysEmitIntoClient + public func moveInitialize( + fromContentsOf source: Slice> + ) -> Index where Base == UnsafeMutableBufferPointer { + let buffer = Base(rebasing: self) + let index = buffer.moveInitialize(fromContentsOf: source) + let distance = buffer.distance(from: buffer.startIndex, to: index) + return startIndex.advanced(by: distance) + } + + /// Deinitializes every instance in this buffer slice. + /// + /// The region of memory underlying this buffer slice must be fully + /// initialized. After calling `deinitialize(count:)`, the memory + /// is uninitialized, but still bound to the `Element` type. + /// + /// - Note: All buffer elements must already be initialized. + /// + /// - Returns: A raw buffer to the same range of memory as this buffer. + /// The range of memory is still bound to `Element`. + @discardableResult + @inlinable + @_alwaysEmitIntoClient + public func deinitialize() -> UnsafeMutableRawBufferPointer + where Base == UnsafeMutableBufferPointer { + Base(rebasing: self).deinitialize() + } + + /// Initializes the element at `index` to the given value. + /// + /// The memory underlying the destination element must be uninitialized, + /// or `Element` must be a trivial type. After a call to `initialize(to:)`, + /// the memory underlying this element of the buffer slice is initialized. + /// + /// - Parameters: + /// - value: The value used to initialize the buffer element's memory. + /// - index: The index of the element to initialize + @inlinable + @_alwaysEmitIntoClient + public func initializeElement(at index: Int, to value: Element) + where Base == UnsafeMutableBufferPointer { + assert(startIndex <= index && index < endIndex) + base.baseAddress.unsafelyUnwrapped.advanced(by: index).initialize(to: value) + } +} +#endif + +#if swift(<5.8) +extension UnsafeMutableBufferPointer { + /// Updates every element of this buffer's initialized memory. + /// + /// The buffer’s memory must be initialized or its `Element` type + /// must be a trivial type. + /// + /// - Note: All buffer elements must already be initialized. + /// + /// - Parameters: + /// - repeatedValue: The value used when updating this pointer's memory. + @_alwaysEmitIntoClient + public func update(repeating repeatedValue: Element) { + guard let dstBase = baseAddress else { return } + dstBase.update(repeating: repeatedValue, count: count) + } +} +#endif + +#if swift(<5.8) +extension Slice { + /// Updates every element of this buffer slice's initialized memory. + /// + /// The buffer slice’s memory must be initialized or its `Element` + /// must be a trivial type. + /// + /// - Note: All buffer elements must already be initialized. + /// + /// - Parameters: + /// - repeatedValue: The value used when updating this pointer's memory. + @_alwaysEmitIntoClient + public func update(repeating repeatedValue: Element) + where Base == UnsafeMutableBufferPointer { + Base(rebasing: self).update(repeating: repeatedValue) + } +} +#endif +#endif // COLLECTIONS_SINGLE_MODULE diff --git a/Sources/_CollectionsUtilities/Compatibility/autogenerated/UnsafeMutablePointer+SE-0370.swift b/Sources/_CollectionsUtilities/Compatibility/autogenerated/UnsafeMutablePointer+SE-0370.swift new file mode 100644 index 000000000..2e89d5aa0 --- /dev/null +++ b/Sources/_CollectionsUtilities/Compatibility/autogenerated/UnsafeMutablePointer+SE-0370.swift @@ -0,0 +1,72 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + + +// ############################################################################# +// # # +// # DO NOT EDIT THIS FILE; IT IS AUTOGENERATED. # +// # # +// ############################################################################# + + + +// In single module mode, we need these declarations to be internal, +// but in regular builds we want them to be public. Unfortunately +// the current best way to do this is to duplicate all definitions. +#if COLLECTIONS_SINGLE_MODULE +#if swift(<5.8) +extension UnsafeMutablePointer { + /// Update this pointer's initialized memory with the specified number of + /// consecutive copies of the given value. + /// + /// The region of memory starting at this pointer and covering `count` + /// instances of the pointer's `Pointee` type must be initialized or + /// `Pointee` must be a trivial type. After calling + /// `update(repeating:count:)`, the region is initialized. + /// + /// - Parameters: + /// - repeatedValue: The value used when updating this pointer's memory. + /// - count: The number of consecutive elements to update. + /// `count` must not be negative. + @_alwaysEmitIntoClient + internal func update(repeating repeatedValue: Pointee, count: Int) { + assert(count >= 0, "UnsafeMutablePointer.update(repeating:count:) with negative count") + for i in 0 ..< count { + self[i] = repeatedValue + } + } +} +#endif +#else // !COLLECTIONS_SINGLE_MODULE +#if swift(<5.8) +extension UnsafeMutablePointer { + /// Update this pointer's initialized memory with the specified number of + /// consecutive copies of the given value. + /// + /// The region of memory starting at this pointer and covering `count` + /// instances of the pointer's `Pointee` type must be initialized or + /// `Pointee` must be a trivial type. After calling + /// `update(repeating:count:)`, the region is initialized. + /// + /// - Parameters: + /// - repeatedValue: The value used when updating this pointer's memory. + /// - count: The number of consecutive elements to update. + /// `count` must not be negative. + @_alwaysEmitIntoClient + public func update(repeating repeatedValue: Pointee, count: Int) { + assert(count >= 0, "UnsafeMutablePointer.update(repeating:count:) with negative count") + for i in 0 ..< count { + self[i] = repeatedValue + } + } +} +#endif +#endif // COLLECTIONS_SINGLE_MODULE diff --git a/Sources/_CollectionsUtilities/Compatibility/autogenerated/UnsafeRawPointer extensions.swift b/Sources/_CollectionsUtilities/Compatibility/autogenerated/UnsafeRawPointer extensions.swift new file mode 100644 index 000000000..c389967eb --- /dev/null +++ b/Sources/_CollectionsUtilities/Compatibility/autogenerated/UnsafeRawPointer extensions.swift @@ -0,0 +1,164 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + + +// ############################################################################# +// # # +// # DO NOT EDIT THIS FILE; IT IS AUTOGENERATED. # +// # # +// ############################################################################# + + + +// In single module mode, we need these declarations to be internal, +// but in regular builds we want them to be public. Unfortunately +// the current best way to do this is to duplicate all definitions. +#if COLLECTIONS_SINGLE_MODULE +#if compiler(<5.7) || (os(macOS) && compiler(<5.8)) // SE-0334 +extension UnsafeRawPointer { + /// Obtain the next pointer properly aligned to store a value of type `T`. + /// + /// If `self` is properly aligned for accessing `T`, + /// this function returns `self`. + /// + /// - Parameters: + /// - type: the type to be stored at the returned address. + /// - Returns: a pointer properly aligned to store a value of type `T`. + @inlinable + @_alwaysEmitIntoClient + internal func alignedUp(for type: T.Type) -> Self { + let mask = UInt(MemoryLayout.alignment) &- 1 + let bits = (UInt(bitPattern: self) &+ mask) & ~mask + return Self(bitPattern: bits)! + } + + /// Obtain the preceding pointer properly aligned to store a value of type `T`. + /// + /// If `self` is properly aligned for accessing `T`, + /// this function returns `self`. + /// + /// - Parameters: + /// - type: the type to be stored at the returned address. + /// - Returns: a pointer properly aligned to store a value of type `T`. + @inlinable + @_alwaysEmitIntoClient + internal func alignedDown(for type: T.Type) -> Self { + let mask = UInt(MemoryLayout.alignment) &- 1 + let bits = UInt(bitPattern: self) & ~mask + return Self(bitPattern: bits)! + } +} + +extension UnsafeMutableRawPointer { + /// Obtain the next pointer properly aligned to store a value of type `T`. + /// + /// If `self` is properly aligned for accessing `T`, + /// this function returns `self`. + /// + /// - Parameters: + /// - type: the type to be stored at the returned address. + /// - Returns: a pointer properly aligned to store a value of type `T`. + @inlinable + @_alwaysEmitIntoClient + internal func alignedUp(for type: T.Type) -> Self { + let mask = UInt(MemoryLayout.alignment) &- 1 + let bits = (UInt(bitPattern: self) &+ mask) & ~mask + return Self(bitPattern: bits)! + } + + /// Obtain the preceding pointer properly aligned to store a value of type `T`. + /// + /// If `self` is properly aligned for accessing `T`, + /// this function returns `self`. + /// + /// - Parameters: + /// - type: the type to be stored at the returned address. + /// - Returns: a pointer properly aligned to store a value of type `T`. + @inlinable + @_alwaysEmitIntoClient + internal func alignedDown(for type: T.Type) -> Self { + let mask = UInt(MemoryLayout.alignment) &- 1 + let bits = UInt(bitPattern: self) & ~mask + return Self(bitPattern: bits)! + } +} +#endif +#else // !COLLECTIONS_SINGLE_MODULE +#if compiler(<5.7) || (os(macOS) && compiler(<5.8)) // SE-0334 +extension UnsafeRawPointer { + /// Obtain the next pointer properly aligned to store a value of type `T`. + /// + /// If `self` is properly aligned for accessing `T`, + /// this function returns `self`. + /// + /// - Parameters: + /// - type: the type to be stored at the returned address. + /// - Returns: a pointer properly aligned to store a value of type `T`. + @inlinable + @_alwaysEmitIntoClient + public func alignedUp(for type: T.Type) -> Self { + let mask = UInt(MemoryLayout.alignment) &- 1 + let bits = (UInt(bitPattern: self) &+ mask) & ~mask + return Self(bitPattern: bits)! + } + + /// Obtain the preceding pointer properly aligned to store a value of type `T`. + /// + /// If `self` is properly aligned for accessing `T`, + /// this function returns `self`. + /// + /// - Parameters: + /// - type: the type to be stored at the returned address. + /// - Returns: a pointer properly aligned to store a value of type `T`. + @inlinable + @_alwaysEmitIntoClient + public func alignedDown(for type: T.Type) -> Self { + let mask = UInt(MemoryLayout.alignment) &- 1 + let bits = UInt(bitPattern: self) & ~mask + return Self(bitPattern: bits)! + } +} + +extension UnsafeMutableRawPointer { + /// Obtain the next pointer properly aligned to store a value of type `T`. + /// + /// If `self` is properly aligned for accessing `T`, + /// this function returns `self`. + /// + /// - Parameters: + /// - type: the type to be stored at the returned address. + /// - Returns: a pointer properly aligned to store a value of type `T`. + @inlinable + @_alwaysEmitIntoClient + public func alignedUp(for type: T.Type) -> Self { + let mask = UInt(MemoryLayout.alignment) &- 1 + let bits = (UInt(bitPattern: self) &+ mask) & ~mask + return Self(bitPattern: bits)! + } + + /// Obtain the preceding pointer properly aligned to store a value of type `T`. + /// + /// If `self` is properly aligned for accessing `T`, + /// this function returns `self`. + /// + /// - Parameters: + /// - type: the type to be stored at the returned address. + /// - Returns: a pointer properly aligned to store a value of type `T`. + @inlinable + @_alwaysEmitIntoClient + public func alignedDown(for type: T.Type) -> Self { + let mask = UInt(MemoryLayout.alignment) &- 1 + let bits = UInt(bitPattern: self) & ~mask + return Self(bitPattern: bits)! + } +} +#endif +#endif // COLLECTIONS_SINGLE_MODULE diff --git a/Sources/_CollectionsUtilities/Debugging.swift.gyb b/Sources/_CollectionsUtilities/Debugging.swift.gyb new file mode 100644 index 000000000..0771c41bf --- /dev/null +++ b/Sources/_CollectionsUtilities/Debugging.swift.gyb @@ -0,0 +1,34 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +%{ + from gyb_utils import * +}% +${autogenerated_warning()} + +% for modifier in visibility_levels: +${visibility_boilerplate(modifier)} +/// True if consistency checking is enabled in the implementation of the +/// Swift Collections package, false otherwise. +/// +/// Documented performance promises are null and void when this property +/// returns true -- for example, operations that are documented to take +/// O(1) time might take O(*n*) time, or worse. +@inlinable @inline(__always) +${modifier} var _isCollectionsInternalCheckingEnabled: Bool { +#if COLLECTIONS_INTERNAL_CHECKS + return true +#else + return false +#endif +} +% end +${visibility_boilerplate("end")} diff --git a/Sources/_CollectionsUtilities/Descriptions.swift.gyb b/Sources/_CollectionsUtilities/Descriptions.swift.gyb new file mode 100644 index 000000000..fa44ef68b --- /dev/null +++ b/Sources/_CollectionsUtilities/Descriptions.swift.gyb @@ -0,0 +1,75 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +%{ + from gyb_utils import * +}% +${autogenerated_warning()} + +% for modifier in visibility_levels: +${visibility_boilerplate(modifier)} + +${"@usableFromInline" if modifier != "public" else "//@usableFromInline" } +${modifier} func _addressString(for pointer: UnsafeRawPointer) -> String { + let address = UInt(bitPattern: pointer) + return "0x\(String(address, radix: 16))" +} + +${"@usableFromInline" if modifier != "public" else "//@usableFromInline" } +${modifier} func _addressString(for object: AnyObject) -> String { + _addressString(for: Unmanaged.passUnretained(object).toOpaque()) +} + +${"@usableFromInline" if modifier != "public" else "//@usableFromInline" } +${modifier} func _addressString(for object: Unmanaged) -> String { + _addressString(for: object.toOpaque()) +} + +@inlinable +${modifier} func _arrayDescription( + for elements: C +) -> String { + var result = "[" + var first = true + for item in elements { + if first { + first = false + } else { + result += ", " + } + debugPrint(item, terminator: "", to: &result) + } + result += "]" + return result +} + +@inlinable +${modifier} func _dictionaryDescription( + for elements: C +) -> String where C.Element == (key: Key, value: Value) { + guard !elements.isEmpty else { return "[:]" } + var result = "[" + var first = true + for (key, value) in elements { + if first { + first = false + } else { + result += ", " + } + debugPrint(key, terminator: "", to: &result) + result += ": " + debugPrint(value, terminator: "", to: &result) + } + result += "]" + return result +} +% end +${visibility_boilerplate("end")} diff --git a/Sources/_CollectionsUtilities/IntegerTricks/FixedWidthInteger+roundUpToPowerOfTwo.swift.gyb b/Sources/_CollectionsUtilities/IntegerTricks/FixedWidthInteger+roundUpToPowerOfTwo.swift.gyb new file mode 100644 index 000000000..8c0e5e43d --- /dev/null +++ b/Sources/_CollectionsUtilities/IntegerTricks/FixedWidthInteger+roundUpToPowerOfTwo.swift.gyb @@ -0,0 +1,30 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +%{ + from gyb_utils import * +}% +${autogenerated_warning()} + +% for modifier in visibility_levels: +${visibility_boilerplate(modifier)} +extension FixedWidthInteger { + /// Round up `self` to the nearest power of two, assuming it's representable. + /// Returns 0 if `self` isn't positive. + @inlinable + ${modifier} func _roundUpToPowerOfTwo() -> Self { + guard self > 0 else { return 0 } + let l = Self.bitWidth - (self &- 1).leadingZeroBitCount + return 1 << l + } +} +% end +${visibility_boilerplate("end")} diff --git a/Sources/_CollectionsUtilities/IntegerTricks/Integer rank.swift.gyb b/Sources/_CollectionsUtilities/IntegerTricks/Integer rank.swift.gyb new file mode 100644 index 000000000..f0f77aafe --- /dev/null +++ b/Sources/_CollectionsUtilities/IntegerTricks/Integer rank.swift.gyb @@ -0,0 +1,159 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +%{ + from gyb_utils import * +}% +${autogenerated_warning()} + +% for modifier in visibility_levels: +${visibility_boilerplate(modifier)} +extension FixedWidthInteger { + @inlinable @inline(__always) + internal var _nonzeroBitCount: Self { + Self(truncatingIfNeeded: nonzeroBitCount) + } + + @inlinable @inline(__always) + ${modifier} func _rank(ofBit bit: UInt) -> Int { + assert(bit < Self.bitWidth) + let mask: Self = (1 &<< bit) &- 1 + return (self & mask).nonzeroBitCount + } +} + +extension UInt { + @_effects(releasenone) + ${"@usableFromInline" if modifier != "public" else "//@usableFromInline" } + ${modifier} func _bit(ranked n: Int) -> UInt? { + // FIXME: Use bit deposit instruction when available (PDEP on Intel). + assert(UInt.bitWidth == 64 || UInt.bitWidth == 32, + "Unsupported UInt bitWidth") + + var shift: Self = 0 + var n = Self(truncatingIfNeeded: n) + + if MemoryLayout.size == 8 { + let c32 = (self & 0xFFFFFFFF)._nonzeroBitCount + if n >= c32 { + shift &+= 32 + n &-= c32 + } + } else { + assert(MemoryLayout.size == 4, "Unknown platform") + } + + let c16 = ((self &>> shift) & 0xFFFF)._nonzeroBitCount + if n >= c16 { + shift &+= 16 + n &-= c16 + } + let c8 = ((self &>> shift) & 0xFF)._nonzeroBitCount + if n >= c8 { + shift &+= 8 + n &-= c8 + } + let c4 = ((self &>> shift) & 0xF)._nonzeroBitCount + if n >= c4 { + shift &+= 4 + n &-= c4 + } + let c2 = ((self &>> shift) & 0x3)._nonzeroBitCount + if n >= c2 { + shift &+= 2 + n &-= c2 + } + let c1 = (self &>> shift) & 0x1 + if n >= c1 { + shift &+= 1 + n &-= c1 + } + guard n == 0 && (self &>> shift) & 0x1 == 1 else { return nil } + return shift + } +} + +extension UInt32 { + // Returns the position of the `n`th set bit in `self`, i.e., the bit with + // rank `n`. + @_effects(releasenone) + ${"@usableFromInline" if modifier != "public" else "//@usableFromInline" } + ${modifier} func _bit(ranked n: Int) -> UInt? { + // FIXME: Use bit deposit instruction when available (PDEP on Intel). + assert(n >= 0 && n < Self.bitWidth) + var shift: Self = 0 + var n = Self(truncatingIfNeeded: n) + let c16 = (self & 0xFFFF)._nonzeroBitCount + if n >= c16 { + shift = 16 + n -= c16 + } + let c8 = ((self &>> shift) & 0xFF)._nonzeroBitCount + if n >= c8 { + shift &+= 8 + n -= c8 + } + let c4 = ((self &>> shift) & 0xF)._nonzeroBitCount + if n >= c4 { + shift &+= 4 + n -= c4 + } + let c2 = ((self &>> shift) & 0x3)._nonzeroBitCount + if n >= c2 { + shift &+= 2 + n -= c2 + } + let c1 = (self &>> shift) & 0x1 + if n >= c1 { + shift &+= 1 + n -= c1 + } + guard n == 0, (self &>> shift) & 0x1 == 1 else { return nil } + return UInt(truncatingIfNeeded: shift) + } +} + +extension UInt16 { + // Returns the position of the `n`th set bit in `self`, i.e., the bit with + // rank `n`. + @_effects(releasenone) + ${"@usableFromInline" if modifier != "public" else "//@usableFromInline" } + ${modifier} func _bit(ranked n: Int) -> UInt? { + // FIXME: Use bit deposit instruction when available (PDEP on Intel). + assert(n >= 0 && n < Self.bitWidth) + var shift: Self = 0 + var n = Self(truncatingIfNeeded: n) + let c8 = ((self &>> shift) & 0xFF)._nonzeroBitCount + if n >= c8 { + shift &+= 8 + n -= c8 + } + let c4 = ((self &>> shift) & 0xF)._nonzeroBitCount + if n >= c4 { + shift &+= 4 + n -= c4 + } + let c2 = ((self &>> shift) & 0x3)._nonzeroBitCount + if n >= c2 { + shift &+= 2 + n -= c2 + } + let c1 = (self &>> shift) & 0x1 + if n >= c1 { + shift &+= 1 + n -= c1 + } + guard n == 0, (self &>> shift) & 0x1 == 1 else { return nil } + return UInt(truncatingIfNeeded: shift) + } +} +% end +${visibility_boilerplate("end")} diff --git a/Sources/_CollectionsUtilities/IntegerTricks/UInt+first and last set bit.swift.gyb b/Sources/_CollectionsUtilities/IntegerTricks/UInt+first and last set bit.swift.gyb new file mode 100644 index 000000000..2f12f5991 --- /dev/null +++ b/Sources/_CollectionsUtilities/IntegerTricks/UInt+first and last set bit.swift.gyb @@ -0,0 +1,34 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +%{ + from gyb_utils import * +}% +${autogenerated_warning()} + +% for modifier in visibility_levels: +${visibility_boilerplate(modifier)} +extension UInt { + @inlinable @inline(__always) + ${modifier} var _firstSetBit: UInt? { + guard self != 0 else { return nil } + let v = UInt.bitWidth &- 1 &- self.leadingZeroBitCount + return UInt(truncatingIfNeeded: v) + } + + @inlinable @inline(__always) + ${modifier} var _lastSetBit: UInt? { + guard self != 0 else { return nil } + return UInt(truncatingIfNeeded: self.trailingZeroBitCount) + } +} +% end +${visibility_boilerplate("end")} diff --git a/Sources/_CollectionsUtilities/IntegerTricks/UInt+reversed.swift.gyb b/Sources/_CollectionsUtilities/IntegerTricks/UInt+reversed.swift.gyb new file mode 100644 index 000000000..e4f18702e --- /dev/null +++ b/Sources/_CollectionsUtilities/IntegerTricks/UInt+reversed.swift.gyb @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +%{ + from gyb_utils import * +}% +${autogenerated_warning()} + +% for modifier in visibility_levels: +${visibility_boilerplate(modifier)} +extension UInt { + @inlinable + ${modifier} var _reversed: UInt { + // https://graphics.stanford.edu/~seander/bithacks.html#ReverseParallel + var shift: UInt = UInt(UInt.bitWidth) + var mask: UInt = ~0; + var result = self + while true { + shift &>>= 1 + guard shift > 0 else { break } + mask ^= mask &<< shift + result = ((result &>> shift) & mask) | ((result &<< shift) & ~mask) + } + return result + } +} +% end +${visibility_boilerplate("end")} diff --git a/Sources/_CollectionsUtilities/IntegerTricks/autogenerated/FixedWidthInteger+roundUpToPowerOfTwo.swift b/Sources/_CollectionsUtilities/IntegerTricks/autogenerated/FixedWidthInteger+roundUpToPowerOfTwo.swift new file mode 100644 index 000000000..4f6fdb1e5 --- /dev/null +++ b/Sources/_CollectionsUtilities/IntegerTricks/autogenerated/FixedWidthInteger+roundUpToPowerOfTwo.swift @@ -0,0 +1,46 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + + +// ############################################################################# +// # # +// # DO NOT EDIT THIS FILE; IT IS AUTOGENERATED. # +// # # +// ############################################################################# + + + +// In single module mode, we need these declarations to be internal, +// but in regular builds we want them to be public. Unfortunately +// the current best way to do this is to duplicate all definitions. +#if COLLECTIONS_SINGLE_MODULE +extension FixedWidthInteger { + /// Round up `self` to the nearest power of two, assuming it's representable. + /// Returns 0 if `self` isn't positive. + @inlinable + internal func _roundUpToPowerOfTwo() -> Self { + guard self > 0 else { return 0 } + let l = Self.bitWidth - (self &- 1).leadingZeroBitCount + return 1 << l + } +} +#else // !COLLECTIONS_SINGLE_MODULE +extension FixedWidthInteger { + /// Round up `self` to the nearest power of two, assuming it's representable. + /// Returns 0 if `self` isn't positive. + @inlinable + public func _roundUpToPowerOfTwo() -> Self { + guard self > 0 else { return 0 } + let l = Self.bitWidth - (self &- 1).leadingZeroBitCount + return 1 << l + } +} +#endif // COLLECTIONS_SINGLE_MODULE diff --git a/Sources/_CollectionsUtilities/IntegerTricks/autogenerated/Integer rank.swift b/Sources/_CollectionsUtilities/IntegerTricks/autogenerated/Integer rank.swift new file mode 100644 index 000000000..1ef0f2996 --- /dev/null +++ b/Sources/_CollectionsUtilities/IntegerTricks/autogenerated/Integer rank.swift @@ -0,0 +1,304 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + + +// ############################################################################# +// # # +// # DO NOT EDIT THIS FILE; IT IS AUTOGENERATED. # +// # # +// ############################################################################# + + + +// In single module mode, we need these declarations to be internal, +// but in regular builds we want them to be public. Unfortunately +// the current best way to do this is to duplicate all definitions. +#if COLLECTIONS_SINGLE_MODULE +extension FixedWidthInteger { + @inlinable @inline(__always) + internal var _nonzeroBitCount: Self { + Self(truncatingIfNeeded: nonzeroBitCount) + } + + @inlinable @inline(__always) + internal func _rank(ofBit bit: UInt) -> Int { + assert(bit < Self.bitWidth) + let mask: Self = (1 &<< bit) &- 1 + return (self & mask).nonzeroBitCount + } +} + +extension UInt { + @_effects(releasenone) + @usableFromInline + internal func _bit(ranked n: Int) -> UInt? { + // FIXME: Use bit deposit instruction when available (PDEP on Intel). + assert(UInt.bitWidth == 64 || UInt.bitWidth == 32, + "Unsupported UInt bitWidth") + + var shift: Self = 0 + var n = Self(truncatingIfNeeded: n) + + if MemoryLayout.size == 8 { + let c32 = (self & 0xFFFFFFFF)._nonzeroBitCount + if n >= c32 { + shift &+= 32 + n &-= c32 + } + } else { + assert(MemoryLayout.size == 4, "Unknown platform") + } + + let c16 = ((self &>> shift) & 0xFFFF)._nonzeroBitCount + if n >= c16 { + shift &+= 16 + n &-= c16 + } + let c8 = ((self &>> shift) & 0xFF)._nonzeroBitCount + if n >= c8 { + shift &+= 8 + n &-= c8 + } + let c4 = ((self &>> shift) & 0xF)._nonzeroBitCount + if n >= c4 { + shift &+= 4 + n &-= c4 + } + let c2 = ((self &>> shift) & 0x3)._nonzeroBitCount + if n >= c2 { + shift &+= 2 + n &-= c2 + } + let c1 = (self &>> shift) & 0x1 + if n >= c1 { + shift &+= 1 + n &-= c1 + } + guard n == 0 && (self &>> shift) & 0x1 == 1 else { return nil } + return shift + } +} + +extension UInt32 { + // Returns the position of the `n`th set bit in `self`, i.e., the bit with + // rank `n`. + @_effects(releasenone) + @usableFromInline + internal func _bit(ranked n: Int) -> UInt? { + // FIXME: Use bit deposit instruction when available (PDEP on Intel). + assert(n >= 0 && n < Self.bitWidth) + var shift: Self = 0 + var n = Self(truncatingIfNeeded: n) + let c16 = (self & 0xFFFF)._nonzeroBitCount + if n >= c16 { + shift = 16 + n -= c16 + } + let c8 = ((self &>> shift) & 0xFF)._nonzeroBitCount + if n >= c8 { + shift &+= 8 + n -= c8 + } + let c4 = ((self &>> shift) & 0xF)._nonzeroBitCount + if n >= c4 { + shift &+= 4 + n -= c4 + } + let c2 = ((self &>> shift) & 0x3)._nonzeroBitCount + if n >= c2 { + shift &+= 2 + n -= c2 + } + let c1 = (self &>> shift) & 0x1 + if n >= c1 { + shift &+= 1 + n -= c1 + } + guard n == 0, (self &>> shift) & 0x1 == 1 else { return nil } + return UInt(truncatingIfNeeded: shift) + } +} + +extension UInt16 { + // Returns the position of the `n`th set bit in `self`, i.e., the bit with + // rank `n`. + @_effects(releasenone) + @usableFromInline + internal func _bit(ranked n: Int) -> UInt? { + // FIXME: Use bit deposit instruction when available (PDEP on Intel). + assert(n >= 0 && n < Self.bitWidth) + var shift: Self = 0 + var n = Self(truncatingIfNeeded: n) + let c8 = ((self &>> shift) & 0xFF)._nonzeroBitCount + if n >= c8 { + shift &+= 8 + n -= c8 + } + let c4 = ((self &>> shift) & 0xF)._nonzeroBitCount + if n >= c4 { + shift &+= 4 + n -= c4 + } + let c2 = ((self &>> shift) & 0x3)._nonzeroBitCount + if n >= c2 { + shift &+= 2 + n -= c2 + } + let c1 = (self &>> shift) & 0x1 + if n >= c1 { + shift &+= 1 + n -= c1 + } + guard n == 0, (self &>> shift) & 0x1 == 1 else { return nil } + return UInt(truncatingIfNeeded: shift) + } +} +#else // !COLLECTIONS_SINGLE_MODULE +extension FixedWidthInteger { + @inlinable @inline(__always) + internal var _nonzeroBitCount: Self { + Self(truncatingIfNeeded: nonzeroBitCount) + } + + @inlinable @inline(__always) + public func _rank(ofBit bit: UInt) -> Int { + assert(bit < Self.bitWidth) + let mask: Self = (1 &<< bit) &- 1 + return (self & mask).nonzeroBitCount + } +} + +extension UInt { + @_effects(releasenone) + //@usableFromInline + public func _bit(ranked n: Int) -> UInt? { + // FIXME: Use bit deposit instruction when available (PDEP on Intel). + assert(UInt.bitWidth == 64 || UInt.bitWidth == 32, + "Unsupported UInt bitWidth") + + var shift: Self = 0 + var n = Self(truncatingIfNeeded: n) + + if MemoryLayout.size == 8 { + let c32 = (self & 0xFFFFFFFF)._nonzeroBitCount + if n >= c32 { + shift &+= 32 + n &-= c32 + } + } else { + assert(MemoryLayout.size == 4, "Unknown platform") + } + + let c16 = ((self &>> shift) & 0xFFFF)._nonzeroBitCount + if n >= c16 { + shift &+= 16 + n &-= c16 + } + let c8 = ((self &>> shift) & 0xFF)._nonzeroBitCount + if n >= c8 { + shift &+= 8 + n &-= c8 + } + let c4 = ((self &>> shift) & 0xF)._nonzeroBitCount + if n >= c4 { + shift &+= 4 + n &-= c4 + } + let c2 = ((self &>> shift) & 0x3)._nonzeroBitCount + if n >= c2 { + shift &+= 2 + n &-= c2 + } + let c1 = (self &>> shift) & 0x1 + if n >= c1 { + shift &+= 1 + n &-= c1 + } + guard n == 0 && (self &>> shift) & 0x1 == 1 else { return nil } + return shift + } +} + +extension UInt32 { + // Returns the position of the `n`th set bit in `self`, i.e., the bit with + // rank `n`. + @_effects(releasenone) + //@usableFromInline + public func _bit(ranked n: Int) -> UInt? { + // FIXME: Use bit deposit instruction when available (PDEP on Intel). + assert(n >= 0 && n < Self.bitWidth) + var shift: Self = 0 + var n = Self(truncatingIfNeeded: n) + let c16 = (self & 0xFFFF)._nonzeroBitCount + if n >= c16 { + shift = 16 + n -= c16 + } + let c8 = ((self &>> shift) & 0xFF)._nonzeroBitCount + if n >= c8 { + shift &+= 8 + n -= c8 + } + let c4 = ((self &>> shift) & 0xF)._nonzeroBitCount + if n >= c4 { + shift &+= 4 + n -= c4 + } + let c2 = ((self &>> shift) & 0x3)._nonzeroBitCount + if n >= c2 { + shift &+= 2 + n -= c2 + } + let c1 = (self &>> shift) & 0x1 + if n >= c1 { + shift &+= 1 + n -= c1 + } + guard n == 0, (self &>> shift) & 0x1 == 1 else { return nil } + return UInt(truncatingIfNeeded: shift) + } +} + +extension UInt16 { + // Returns the position of the `n`th set bit in `self`, i.e., the bit with + // rank `n`. + @_effects(releasenone) + //@usableFromInline + public func _bit(ranked n: Int) -> UInt? { + // FIXME: Use bit deposit instruction when available (PDEP on Intel). + assert(n >= 0 && n < Self.bitWidth) + var shift: Self = 0 + var n = Self(truncatingIfNeeded: n) + let c8 = ((self &>> shift) & 0xFF)._nonzeroBitCount + if n >= c8 { + shift &+= 8 + n -= c8 + } + let c4 = ((self &>> shift) & 0xF)._nonzeroBitCount + if n >= c4 { + shift &+= 4 + n -= c4 + } + let c2 = ((self &>> shift) & 0x3)._nonzeroBitCount + if n >= c2 { + shift &+= 2 + n -= c2 + } + let c1 = (self &>> shift) & 0x1 + if n >= c1 { + shift &+= 1 + n -= c1 + } + guard n == 0, (self &>> shift) & 0x1 == 1 else { return nil } + return UInt(truncatingIfNeeded: shift) + } +} +#endif // COLLECTIONS_SINGLE_MODULE diff --git a/Sources/_CollectionsUtilities/IntegerTricks/autogenerated/UInt+first and last set bit.swift b/Sources/_CollectionsUtilities/IntegerTricks/autogenerated/UInt+first and last set bit.swift new file mode 100644 index 000000000..ae5ccb01d --- /dev/null +++ b/Sources/_CollectionsUtilities/IntegerTricks/autogenerated/UInt+first and last set bit.swift @@ -0,0 +1,54 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + + +// ############################################################################# +// # # +// # DO NOT EDIT THIS FILE; IT IS AUTOGENERATED. # +// # # +// ############################################################################# + + + +// In single module mode, we need these declarations to be internal, +// but in regular builds we want them to be public. Unfortunately +// the current best way to do this is to duplicate all definitions. +#if COLLECTIONS_SINGLE_MODULE +extension UInt { + @inlinable @inline(__always) + internal var _firstSetBit: UInt? { + guard self != 0 else { return nil } + let v = UInt.bitWidth &- 1 &- self.leadingZeroBitCount + return UInt(truncatingIfNeeded: v) + } + + @inlinable @inline(__always) + internal var _lastSetBit: UInt? { + guard self != 0 else { return nil } + return UInt(truncatingIfNeeded: self.trailingZeroBitCount) + } +} +#else // !COLLECTIONS_SINGLE_MODULE +extension UInt { + @inlinable @inline(__always) + public var _firstSetBit: UInt? { + guard self != 0 else { return nil } + let v = UInt.bitWidth &- 1 &- self.leadingZeroBitCount + return UInt(truncatingIfNeeded: v) + } + + @inlinable @inline(__always) + public var _lastSetBit: UInt? { + guard self != 0 else { return nil } + return UInt(truncatingIfNeeded: self.trailingZeroBitCount) + } +} +#endif // COLLECTIONS_SINGLE_MODULE diff --git a/Sources/_CollectionsUtilities/IntegerTricks/autogenerated/UInt+reversed.swift b/Sources/_CollectionsUtilities/IntegerTricks/autogenerated/UInt+reversed.swift new file mode 100644 index 000000000..ac02ad29f --- /dev/null +++ b/Sources/_CollectionsUtilities/IntegerTricks/autogenerated/UInt+reversed.swift @@ -0,0 +1,58 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + + +// ############################################################################# +// # # +// # DO NOT EDIT THIS FILE; IT IS AUTOGENERATED. # +// # # +// ############################################################################# + + + +// In single module mode, we need these declarations to be internal, +// but in regular builds we want them to be public. Unfortunately +// the current best way to do this is to duplicate all definitions. +#if COLLECTIONS_SINGLE_MODULE +extension UInt { + @inlinable + internal var _reversed: UInt { + // https://graphics.stanford.edu/~seander/bithacks.html#ReverseParallel + var shift: UInt = UInt(UInt.bitWidth) + var mask: UInt = ~0; + var result = self + while true { + shift &>>= 1 + guard shift > 0 else { break } + mask ^= mask &<< shift + result = ((result &>> shift) & mask) | ((result &<< shift) & ~mask) + } + return result + } +} +#else // !COLLECTIONS_SINGLE_MODULE +extension UInt { + @inlinable + public var _reversed: UInt { + // https://graphics.stanford.edu/~seander/bithacks.html#ReverseParallel + var shift: UInt = UInt(UInt.bitWidth) + var mask: UInt = ~0; + var result = self + while true { + shift &>>= 1 + guard shift > 0 else { break } + mask ^= mask &<< shift + result = ((result &>> shift) & mask) | ((result &<< shift) & ~mask) + } + return result + } +} +#endif // COLLECTIONS_SINGLE_MODULE diff --git a/Sources/OrderedCollections/Utilities/RandomAccessCollection+Offsets.swift b/Sources/_CollectionsUtilities/RandomAccessCollection+Offsets.swift.gyb similarity index 50% rename from Sources/OrderedCollections/Utilities/RandomAccessCollection+Offsets.swift rename to Sources/_CollectionsUtilities/RandomAccessCollection+Offsets.swift.gyb index 54a56fa86..b9fef5be9 100644 --- a/Sources/OrderedCollections/Utilities/RandomAccessCollection+Offsets.swift +++ b/Sources/_CollectionsUtilities/RandomAccessCollection+Offsets.swift.gyb @@ -2,29 +2,35 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 // //===----------------------------------------------------------------------===// +%{ + from gyb_utils import * +}% +${autogenerated_warning()} + +% for modifier in visibility_levels: +${visibility_boilerplate(modifier)} extension RandomAccessCollection { - @inlinable - @inline(__always) - internal func _index(at offset: Int) -> Index { + @_alwaysEmitIntoClient @inline(__always) + ${modifier} func _index(at offset: Int) -> Index { index(startIndex, offsetBy: offset) } - @inlinable - @inline(__always) - internal func _offset(of index: Index) -> Int { + @_alwaysEmitIntoClient @inline(__always) + ${modifier} func _offset(of index: Index) -> Int { distance(from: startIndex, to: index) } - @inlinable - @inline(__always) - internal subscript(_offset offset: Int) -> Element { + @_alwaysEmitIntoClient @inline(__always) + ${modifier} subscript(_offset offset: Int) -> Element { self[_index(at: offset)] } } +% end +${visibility_boilerplate("end")} diff --git a/Sources/_CollectionsUtilities/Specialize.swift.gyb b/Sources/_CollectionsUtilities/Specialize.swift.gyb new file mode 100644 index 000000000..c654d78cc --- /dev/null +++ b/Sources/_CollectionsUtilities/Specialize.swift.gyb @@ -0,0 +1,34 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +%{ + from gyb_utils import * +}% +${autogenerated_warning()} + +% for modifier in visibility_levels: +${visibility_boilerplate(modifier)} +/// Returns `x` as its concrete type `U`, or `nil` if `x` has a different +/// concrete type. +/// +/// This cast can be useful for dispatching to specializations of generic +/// functions. +@_transparent +@inlinable +${modifier} func _specialize(_ x: T, for: U.Type) -> U? { + // Note: this was ported from recent versions of the Swift stdlib. + guard T.self == U.self else { + return nil + } + return _identityCast(x, to: U.self) +} +% end +${visibility_boilerplate("end")} diff --git a/Sources/_CollectionsUtilities/UnsafeBitSet/_UnsafeBitSet+Index.swift.gyb b/Sources/_CollectionsUtilities/UnsafeBitSet/_UnsafeBitSet+Index.swift.gyb new file mode 100644 index 000000000..f5770d6b3 --- /dev/null +++ b/Sources/_CollectionsUtilities/UnsafeBitSet/_UnsafeBitSet+Index.swift.gyb @@ -0,0 +1,99 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +%{ + from gyb_utils import * +}% +${autogenerated_warning()} + +% for modifier in visibility_levels: +${visibility_boilerplate(modifier)} +extension _UnsafeBitSet { + ${"@frozen" if modifier == "public" else "@frozen @usableFromInline"} + ${modifier} struct Index: Comparable, Hashable { + @usableFromInline + internal typealias _Word = _UnsafeBitSet._Word + + ${"@usableFromInline" if modifier != "public" else ""} + ${modifier} var value: UInt + + @inlinable + ${modifier} init(_ value: UInt) { + self.value = value + } + + @inlinable + ${modifier} init(_ value: Int) { + self.value = UInt(value) + } + + @inlinable + ${modifier} init(word: Int, bit: UInt) { + assert(word >= 0 && word <= Int.max / _Word.capacity) + assert(bit < _Word.capacity) + self.value = UInt(word &* _Word.capacity) &+ bit + } + } +} + +extension _UnsafeBitSet.Index { + @inlinable + ${modifier} var word: Int { + // Note: We perform on UInts to get faster unsigned math (shifts). + Int(truncatingIfNeeded: value / UInt(bitPattern: _Word.capacity)) + } + + @inlinable + ${modifier} var bit: UInt { + // Note: We perform on UInts to get faster unsigned math (masking). + value % UInt(bitPattern: _Word.capacity) + } + + @inlinable + ${modifier} var split: (word: Int, bit: UInt) { + (word, bit) + } + + @inlinable + ${modifier} var endSplit: (word: Int, bit: UInt) { + let w = word + let b = bit + if w > 0, b == 0 { return (w &- 1, UInt(_Word.capacity)) } + return (w, b) + } + + @inlinable + ${modifier} static func ==(left: Self, right: Self) -> Bool { + left.value == right.value + } + + @inlinable + ${modifier} static func <(left: Self, right: Self) -> Bool { + left.value < right.value + } + + @inlinable + ${modifier} func hash(into hasher: inout Hasher) { + hasher.combine(value) + } + + @inlinable + internal func _successor() -> Self { + Self(value + 1) + } + + @inlinable + internal func _predecessor() -> Self { + Self(value - 1) + } +} +% end +${visibility_boilerplate("end")} diff --git a/Sources/_CollectionsUtilities/UnsafeBitSet/_UnsafeBitSet+_Word.swift.gyb b/Sources/_CollectionsUtilities/UnsafeBitSet/_UnsafeBitSet+_Word.swift.gyb new file mode 100644 index 000000000..bacd11990 --- /dev/null +++ b/Sources/_CollectionsUtilities/UnsafeBitSet/_UnsafeBitSet+_Word.swift.gyb @@ -0,0 +1,329 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +%{ + from gyb_utils import * +}% +${autogenerated_warning()} + + +% for modifier in visibility_levels: +${visibility_boilerplate(modifier)} +extension _UnsafeBitSet { + ${"@frozen" if modifier == "public" else "@frozen @usableFromInline"} + ${modifier} struct _Word { + ${"@usableFromInline" if modifier != "public" else ""} + ${modifier} var value: UInt + + @inlinable + @inline(__always) + ${modifier} init(_ value: UInt) { + self.value = value + } + + @inlinable + @inline(__always) + ${modifier} init(upTo bit: UInt) { + assert(bit <= _Word.capacity) + self.init((1 << bit) &- 1) + } + + @inlinable + @inline(__always) + ${modifier} init(from start: UInt, to end: UInt) { + assert(start <= end && end <= _Word.capacity) + self = Self(upTo: end).symmetricDifference(Self(upTo: start)) + } + } +} + +extension _UnsafeBitSet._Word: CustomStringConvertible { + ${"@usableFromInline" if modifier != "public" else "//@usableFromInline" } + ${modifier} var description: String { + String(value, radix: 16) + } +} + +extension _UnsafeBitSet._Word { + /// Returns the `n`th member in `self`. + /// + /// - Parameter n: The offset of the element to retrieve. This value is + /// decremented by the number of items found in this `self` towards the + /// value we're looking for. (If the function returns non-nil, then `n` + /// is set to `0` on return.) + /// - Returns: If this word contains enough members to satisfy the request, + /// then this function returns the member found. Otherwise it returns nil. + @inline(never) + internal func nthElement(_ n: inout UInt) -> UInt? { + let c = UInt(bitPattern: count) + guard n < c else { + n &-= c + return nil + } + let m = Int(bitPattern: n) + n = 0 + return value._bit(ranked: m)! + } + + @inline(never) + internal func nthElementFromEnd(_ n: inout UInt) -> UInt? { + let c = UInt(bitPattern: count) + guard n < c else { + n &-= c + return nil + } + let m = Int(bitPattern: c &- 1 &- n) + n = 0 + return value._bit(ranked: m)! + } +} + +extension _UnsafeBitSet._Word { + @inlinable + @inline(__always) + ${modifier} static func wordCount(forBitCount count: UInt) -> Int { + // Note: We perform on UInts to get faster unsigned math (shifts). + let width = UInt(bitPattern: Self.capacity) + return Int(bitPattern: (count + width - 1) / width) + } +} + +extension _UnsafeBitSet._Word { + @inlinable + @inline(__always) + ${modifier} static var capacity: Int { + return UInt.bitWidth + } + + @inlinable + @inline(__always) + ${modifier} var count: Int { + value.nonzeroBitCount + } + + @inlinable + @inline(__always) + ${modifier} var isEmpty: Bool { + value == 0 + } + + @inlinable + @inline(__always) + ${modifier} var isFull: Bool { + value == UInt.max + } + + @inlinable + @inline(__always) + ${modifier} func contains(_ bit: UInt) -> Bool { + assert(bit >= 0 && bit < UInt.bitWidth) + return value & (1 &<< bit) != 0 + } + + @inlinable + @inline(__always) + ${modifier} var firstMember: UInt? { + value._lastSetBit + } + + @inlinable + @inline(__always) + ${modifier} var lastMember: UInt? { + value._firstSetBit + } + + @inlinable + @inline(__always) + @discardableResult + ${modifier} mutating func insert(_ bit: UInt) -> Bool { + assert(bit < UInt.bitWidth) + let mask: UInt = 1 &<< bit + let inserted = value & mask == 0 + value |= mask + return inserted + } + + @inlinable + @inline(__always) + @discardableResult + ${modifier} mutating func remove(_ bit: UInt) -> Bool { + assert(bit < UInt.bitWidth) + let mask: UInt = 1 &<< bit + let removed = (value & mask) != 0 + value &= ~mask + return removed + } + + @inlinable + @inline(__always) + ${modifier} mutating func update(_ bit: UInt, to value: Bool) { + assert(bit < UInt.bitWidth) + let mask: UInt = 1 &<< bit + if value { + self.value |= mask + } else { + self.value &= ~mask + } + } +} + +extension _UnsafeBitSet._Word { + @inlinable + @inline(__always) + internal mutating func insertAll(upTo bit: UInt) { + assert(bit >= 0 && bit < Self.capacity) + let mask: UInt = (1 as UInt &<< bit) &- 1 + value |= mask + } + + @inlinable + @inline(__always) + internal mutating func removeAll(upTo bit: UInt) { + assert(bit >= 0 && bit < Self.capacity) + let mask = UInt.max &<< bit + value &= mask + } + + @inlinable + @inline(__always) + internal mutating func removeAll(through bit: UInt) { + assert(bit >= 0 && bit < Self.capacity) + var mask = UInt.max &<< bit + mask &= mask &- 1 // Clear lowest nonzero bit. + value &= mask + } + + @inlinable + @inline(__always) + internal mutating func removeAll(from bit: UInt) { + assert(bit >= 0 && bit < Self.capacity) + let mask: UInt = (1 as UInt &<< bit) &- 1 + value &= mask + } +} + +extension _UnsafeBitSet._Word { + @inlinable + @inline(__always) + ${modifier} static var empty: Self { + Self(0) + } + + @inline(__always) + ${modifier} static var allBits: Self { + Self(UInt.max) + } +} + +// _Word implements Sequence by using a copy of itself as its Iterator. +// Iteration with `next()` destroys the word's value; however, this won't cause +// problems in normal use, because `next()` is usually called on a separate +// iterator, not the original word. +extension _UnsafeBitSet._Word: Sequence, IteratorProtocol { + @inlinable @inline(__always) + ${modifier} var underestimatedCount: Int { + count + } + + /// Return the index of the lowest set bit in this word, + /// and also destructively clear it. + @inlinable + ${modifier} mutating func next() -> UInt? { + guard value != 0 else { return nil } + let bit = UInt(truncatingIfNeeded: value.trailingZeroBitCount) + value &= value &- 1 // Clear lowest nonzero bit. + return bit + } +} + +extension _UnsafeBitSet._Word: Equatable { + @inlinable + ${modifier} static func ==(left: Self, right: Self) -> Bool { + left.value == right.value + } +} + +extension _UnsafeBitSet._Word: Hashable { + @inlinable + ${modifier} func hash(into hasher: inout Hasher) { + hasher.combine(value) + } +} + +extension _UnsafeBitSet._Word { + @inlinable @inline(__always) + ${modifier} func complement() -> Self { + Self(~self.value) + } + + @inlinable @inline(__always) + ${modifier} mutating func formComplement() { + self.value = ~self.value + } + + @inlinable @inline(__always) + ${modifier} func union(_ other: Self) -> Self { + Self(self.value | other.value) + } + + @inlinable @inline(__always) + ${modifier} mutating func formUnion(_ other: Self) { + self.value |= other.value + } + + @inlinable @inline(__always) + ${modifier} func intersection(_ other: Self) -> Self { + Self(self.value & other.value) + } + + @inlinable @inline(__always) + ${modifier} mutating func formIntersection(_ other: Self) { + self.value &= other.value + } + + @inlinable @inline(__always) + ${modifier} func symmetricDifference(_ other: Self) -> Self { + Self(self.value ^ other.value) + } + + @inlinable @inline(__always) + ${modifier} mutating func formSymmetricDifference(_ other: Self) { + self.value ^= other.value + } + + @inlinable @inline(__always) + ${modifier} func subtracting(_ other: Self) -> Self { + Self(self.value & ~other.value) + } + + @inlinable @inline(__always) + ${modifier} mutating func subtract(_ other: Self) { + self.value &= ~other.value + } +} + +extension _UnsafeBitSet._Word { + @inlinable + @inline(__always) + ${modifier} func shiftedDown(by shift: UInt) -> Self { + assert(shift >= 0 && shift < Self.capacity) + return Self(self.value &>> shift) + } + + @inlinable + @inline(__always) + ${modifier} func shiftedUp(by shift: UInt) -> Self { + assert(shift >= 0 && shift < Self.capacity) + return Self(self.value &<< shift) + } +} +% end +${visibility_boilerplate("end")} diff --git a/Sources/_CollectionsUtilities/UnsafeBitSet/_UnsafeBitSet.swift.gyb b/Sources/_CollectionsUtilities/UnsafeBitSet/_UnsafeBitSet.swift.gyb new file mode 100644 index 000000000..a88c8457f --- /dev/null +++ b/Sources/_CollectionsUtilities/UnsafeBitSet/_UnsafeBitSet.swift.gyb @@ -0,0 +1,491 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +%{ + from gyb_utils import * +}% +${autogenerated_warning()} + +% for modifier in visibility_levels: +${visibility_boilerplate(modifier)} +/// An unsafe-unowned bitset view over `UInt` storage, providing bit set +/// primitives. +${"@frozen" if modifier == "public" else "@frozen @usableFromInline"} +${modifier} struct _UnsafeBitSet { + /// An unsafe-unowned storage view. + ${"@usableFromInline" if modifier != "public" else ""} + ${modifier} let _words: UnsafeBufferPointer<_Word> + +#if DEBUG + /// True when this handle does not support table mutations. + /// (This is only checked in debug builds.) + @usableFromInline + internal let _mutable: Bool +#endif + + @inlinable + @inline(__always) + ${modifier} func ensureMutable() { +#if DEBUG + assert(_mutable) +#endif + } + + @inlinable + @inline(__always) + ${modifier} var _mutableWords: UnsafeMutableBufferPointer<_Word> { + ensureMutable() + return UnsafeMutableBufferPointer(mutating: _words) + } + + @inlinable + @inline(__always) + ${modifier} init( + words: UnsafeBufferPointer<_Word>, + mutable: Bool + ) { + assert(words.baseAddress != nil) + self._words = words +#if DEBUG + self._mutable = mutable +#endif + } + + @inlinable + @inline(__always) + ${modifier} init( + words: UnsafeMutableBufferPointer<_Word>, + mutable: Bool + ) { + self.init(words: UnsafeBufferPointer(words), mutable: mutable) + } +} + +extension _UnsafeBitSet { + @inlinable + @inline(__always) + ${modifier} var wordCount: Int { + _words.count + } +} + +extension _UnsafeBitSet { + @inlinable + @inline(__always) + ${modifier} static func withTemporaryBitSet( + capacity: Int, + run body: (inout _UnsafeBitSet) throws -> R + ) rethrows -> R { + let wordCount = _UnsafeBitSet.wordCount(forCapacity: UInt(capacity)) + return try withTemporaryBitSet(wordCount: wordCount, run: body) + } + + @inlinable + @inline(__always) + ${modifier} static func withTemporaryBitSet( + wordCount: Int, + run body: (inout Self) throws -> R + ) rethrows -> R { + var result: R? + try _withTemporaryBitSet(wordCount: wordCount) { bitset in + result = try body(&bitset) + } + return result! + } + + @inline(never) + @usableFromInline + internal static func _withTemporaryBitSet( + wordCount: Int, + run body: (inout Self) throws -> Void + ) rethrows { + try _withTemporaryUninitializedBitSet(wordCount: wordCount) { handle in + handle._mutableWords.initialize(repeating: .empty) + try body(&handle) + } + } + + internal static func _withTemporaryUninitializedBitSet( + wordCount: Int, + run body: (inout Self) throws -> Void + ) rethrows { + assert(wordCount >= 0) +#if compiler(>=5.6) + return try withUnsafeTemporaryAllocation( + of: _Word.self, capacity: wordCount + ) { words in + var bitset = Self(words: words, mutable: true) + return try body(&bitset) + } +#else + if wordCount <= 2 { + var buffer: (_Word, _Word) = (.empty, .empty) + return try withUnsafeMutablePointer(to: &buffer) { p in + // Homogeneous tuples are layout-compatible with their component type. + let start = UnsafeMutableRawPointer(p) + .assumingMemoryBound(to: _Word.self) + let words = UnsafeMutableBufferPointer(start: start, count: wordCount) + var bitset = Self(words: words, mutable: true) + return try body(&bitset) + } + } + let words = UnsafeMutableBufferPointer<_Word>.allocate(capacity: wordCount) + defer { words.deallocate() } + var bitset = Self(words: words, mutable: true) + return try body(&bitset) +#endif + } +} + +extension _UnsafeBitSet { + @_effects(readnone) + @inlinable @inline(__always) + ${modifier} static func wordCount(forCapacity capacity: UInt) -> Int { + _Word.wordCount(forBitCount: capacity) + } + + @inlinable @inline(__always) + ${modifier} var capacity: UInt { + UInt(wordCount &* _Word.capacity) + } + + @inlinable @inline(__always) + internal func isWithinBounds(_ element: UInt) -> Bool { + element < capacity + } + + @_effects(releasenone) + @inline(__always) + ${"@usableFromInline" if modifier != "public" else "//@usableFromInline" } + ${modifier} func contains(_ element: UInt) -> Bool { + let (word, bit) = Index(element).split + guard word < wordCount else { return false } + return _words[word].contains(bit) + } + + @_effects(releasenone) + ${"@usableFromInline" if modifier != "public" else "//@usableFromInline" } + @discardableResult + ${modifier} mutating func insert(_ element: UInt) -> Bool { + ensureMutable() + assert(isWithinBounds(element)) + let index = Index(element) + return _mutableWords[index.word].insert(index.bit) + } + + @_effects(releasenone) + ${"@usableFromInline" if modifier != "public" else "//@usableFromInline" } + @discardableResult + ${modifier} mutating func remove(_ element: UInt) -> Bool { + ensureMutable() + let index = Index(element) + if index.word >= _words.count { return false } + return _mutableWords[index.word].remove(index.bit) + } + + @_effects(releasenone) + ${"@usableFromInline" if modifier != "public" else "//@usableFromInline" } + ${modifier} mutating func update(_ member: UInt, to newValue: Bool) -> Bool { + ensureMutable() + let (w, b) = Index(member).split + _mutableWords[w].update(b, to: newValue) + return w == _words.count &- 1 + } + + @_effects(releasenone) + ${"@usableFromInline" if modifier != "public" else "//@usableFromInline" } + ${modifier} mutating func insertAll(upTo max: UInt) { + assert(max <= capacity) + guard max > 0 else { return } + let (w, b) = Index(max).split + for i in 0 ..< w { + _mutableWords[i] = .allBits + } + if b > 0 { + _mutableWords[w].insertAll(upTo: b) + } + } + + @_alwaysEmitIntoClient + ${"@usableFromInline" if modifier != "public" else "//@usableFromInline" } + @inline(__always) + @discardableResult + ${modifier} mutating func insert(_ element: Int) -> Bool { + precondition(element >= 0) + return insert(UInt(bitPattern: element)) + } + + @_alwaysEmitIntoClient + ${"@usableFromInline" if modifier != "public" else "//@usableFromInline" } + @inline(__always) + @discardableResult + ${modifier} mutating func remove(_ element: Int) -> Bool { + guard element >= 0 else { return false } + return remove(UInt(bitPattern: element)) + } + + @_alwaysEmitIntoClient + ${"@usableFromInline" if modifier != "public" else "//@usableFromInline" } + @inline(__always) + ${modifier} mutating func insertAll(upTo max: Int) { + precondition(max >= 0) + return insertAll(upTo: UInt(bitPattern: max)) + } +} + +extension _UnsafeBitSet: Sequence { + ${"@usableFromInline" if modifier != "public" else "//@usableFromInline" } + ${modifier} typealias Element = UInt + + @inlinable + @inline(__always) + ${modifier} var underestimatedCount: Int { + count // FIXME: really? + } + + @inlinable + @inline(__always) + ${modifier} func makeIterator() -> Iterator { + return Iterator(self) + } + + ${"@frozen" if modifier == "public" else "@usableFromInline @frozen"} + ${modifier} struct Iterator: IteratorProtocol { + @usableFromInline + internal let _bitset: _UnsafeBitSet + + @usableFromInline + internal var _index: Int + + @usableFromInline + internal var _word: _Word + + @inlinable + internal init(_ bitset: _UnsafeBitSet) { + self._bitset = bitset + self._index = 0 + self._word = bitset.wordCount > 0 ? bitset._words[0] : .empty + } + + @_effects(releasenone) + ${"@usableFromInline" if modifier != "public" else "//@usableFromInline" } + ${modifier} mutating func next() -> UInt? { + if let bit = _word.next() { + return Index(word: _index, bit: bit).value + } + while (_index + 1) < _bitset.wordCount { + _index += 1 + _word = _bitset._words[_index] + if let bit = _word.next() { + return Index(word: _index, bit: bit).value + } + } + return nil + } + } +} + +extension _UnsafeBitSet: BidirectionalCollection { + @inlinable + @inline(__always) + ${modifier} var count: Int { + _words.reduce(0) { $0 + $1.count } + } + + @inlinable + @inline(__always) + ${modifier} var isEmpty: Bool { + _words.firstIndex(where: { !$0.isEmpty }) == nil + } + + @inlinable + ${modifier} var startIndex: Index { + let word = _words.firstIndex { !$0.isEmpty } + guard let word = word else { return endIndex } + return Index(word: word, bit: _words[word].firstMember!) + } + + @inlinable + ${modifier} var endIndex: Index { + Index(word: wordCount, bit: 0) + } + + @inlinable + ${modifier} subscript(position: Index) -> UInt { + position.value + } + + @_effects(releasenone) + ${"@usableFromInline" if modifier != "public" else "//@usableFromInline" } + ${modifier} func index(after index: Index) -> Index { + precondition(index < endIndex, "Index out of bounds") + var word = index.word + var w = _words[word] + w.removeAll(through: index.bit) + while w.isEmpty { + word += 1 + guard word < wordCount else { + return Index(word: wordCount, bit: 0) + } + w = _words[word] + } + return Index(word: word, bit: w.firstMember!) + } + + @_effects(releasenone) + ${"@usableFromInline" if modifier != "public" else "//@usableFromInline" } + ${modifier} func index(before index: Index) -> Index { + precondition(index <= endIndex, "Index out of bounds") + var word = index.word + var w: _Word + if index.bit > 0 { + w = _words[word] + w.removeAll(from: index.bit) + } else { + w = .empty + } + while w.isEmpty { + word -= 1 + precondition(word >= 0, "Can't advance below startIndex") + w = _words[word] + } + return Index(word: word, bit: w.lastMember!) + } + + @_effects(releasenone) + ${"@usableFromInline" if modifier != "public" else "//@usableFromInline" } + ${modifier} func distance(from start: Index, to end: Index) -> Int { + precondition(start <= endIndex && end <= endIndex, "Index out of bounds") + let isNegative = end < start + let (start, end) = (Swift.min(start, end), Swift.max(start, end)) + + let (w1, b1) = start.split + let (w2, b2) = end.split + + if w1 == w2 { + guard w1 < wordCount else { return 0 } + let mask = _Word(from: b1, to: b2) + let c = _words[w1].intersection(mask).count + return isNegative ? -c : c + } + + var c = 0 + var w = w1 + guard w < wordCount else { return 0 } + + c &+= _words[w].subtracting(_Word(upTo: b1)).count + w &+= 1 + while w < w2 { + c &+= _words[w].count + w &+= 1 + } + guard w < wordCount else { return isNegative ? -c : c } + c &+= _words[w].intersection(_Word(upTo: b2)).count + return isNegative ? -c : c + } + + @_effects(releasenone) + ${"@usableFromInline" if modifier != "public" else "//@usableFromInline" } + ${modifier} func index(_ i: Index, offsetBy distance: Int) -> Index { + precondition(i <= endIndex, "Index out of bounds") + precondition(i == endIndex || contains(i.value), "Invalid index") + guard distance != 0 else { return i } + var remaining = distance.magnitude + if distance > 0 { + var (w, b) = i.split + precondition(w < wordCount, "Index out of bounds") + if let v = _words[w].subtracting(_Word(upTo: b)).nthElement(&remaining) { + return Index(word: w, bit: v) + } + while true { + w &+= 1 + guard w < wordCount else { break } + if let v = _words[w].nthElement(&remaining) { + return Index(word: w, bit: v) + } + } + precondition(remaining == 0, "Index out of bounds") + return endIndex + } + + // distance < 0 + remaining -= 1 + var (w, b) = i.endSplit + if w < wordCount { + if let v = _words[w].intersection(_Word(upTo: b)).nthElementFromEnd(&remaining) { + return Index(word: w, bit: v) + } + } + while true { + precondition(w > 0, "Index out of bounds") + w &-= 1 + if let v = _words[w].nthElementFromEnd(&remaining) { + return Index(word: w, bit: v) + } + } + } + + @_effects(releasenone) + ${"@usableFromInline" if modifier != "public" else "//@usableFromInline" } + ${modifier} func index( + _ i: Index, offsetBy distance: Int, limitedBy limit: Index + ) -> Index? { + precondition(i <= endIndex && limit <= endIndex, "Index out of bounds") + precondition(i == endIndex || contains(i.value), "Invalid index") + guard distance != 0 else { return i } + var remaining = distance.magnitude + if distance > 0 { + guard i <= limit else { + return self.index(i, offsetBy: distance) + } + var (w, b) = i.split + if w < wordCount, + let v = _words[w].subtracting(_Word(upTo: b)).nthElement(&remaining) + { + let r = Index(word: w, bit: v) + return r <= limit ? r : nil + } + let maxWord = Swift.min(wordCount - 1, limit.word) + while w < maxWord { + w &+= 1 + if let v = _words[w].nthElement(&remaining) { + let r = Index(word: w, bit: v) + return r <= limit ? r : nil + } + } + return remaining == 0 && limit == endIndex ? endIndex : nil + } + + // distance < 0 + guard i >= limit else { + return self.index(i, offsetBy: distance) + } + remaining &-= 1 + var (w, b) = i.endSplit + if w < wordCount { + if let v = _words[w].intersection(_Word(upTo: b)).nthElementFromEnd(&remaining) { + let r = Index(word: w, bit: v) + return r >= limit ? r : nil + } + } + let minWord = limit.word + while w > minWord { + w &-= 1 + if let v = _words[w].nthElementFromEnd(&remaining) { + let r = Index(word: w, bit: v) + return r >= limit ? r : nil + } + } + return nil + } +} +% end +${visibility_boilerplate("end")} diff --git a/Sources/_CollectionsUtilities/UnsafeBitSet/autogenerated/_UnsafeBitSet+Index.swift b/Sources/_CollectionsUtilities/UnsafeBitSet/autogenerated/_UnsafeBitSet+Index.swift new file mode 100644 index 000000000..f2d2b9d5d --- /dev/null +++ b/Sources/_CollectionsUtilities/UnsafeBitSet/autogenerated/_UnsafeBitSet+Index.swift @@ -0,0 +1,184 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + + +// ############################################################################# +// # # +// # DO NOT EDIT THIS FILE; IT IS AUTOGENERATED. # +// # # +// ############################################################################# + + + +// In single module mode, we need these declarations to be internal, +// but in regular builds we want them to be public. Unfortunately +// the current best way to do this is to duplicate all definitions. +#if COLLECTIONS_SINGLE_MODULE +extension _UnsafeBitSet { + @frozen @usableFromInline + internal struct Index: Comparable, Hashable { + @usableFromInline + internal typealias _Word = _UnsafeBitSet._Word + + @usableFromInline + internal var value: UInt + + @inlinable + internal init(_ value: UInt) { + self.value = value + } + + @inlinable + internal init(_ value: Int) { + self.value = UInt(value) + } + + @inlinable + internal init(word: Int, bit: UInt) { + assert(word >= 0 && word <= Int.max / _Word.capacity) + assert(bit < _Word.capacity) + self.value = UInt(word &* _Word.capacity) &+ bit + } + } +} + +extension _UnsafeBitSet.Index { + @inlinable + internal var word: Int { + // Note: We perform on UInts to get faster unsigned math (shifts). + Int(truncatingIfNeeded: value / UInt(bitPattern: _Word.capacity)) + } + + @inlinable + internal var bit: UInt { + // Note: We perform on UInts to get faster unsigned math (masking). + value % UInt(bitPattern: _Word.capacity) + } + + @inlinable + internal var split: (word: Int, bit: UInt) { + (word, bit) + } + + @inlinable + internal var endSplit: (word: Int, bit: UInt) { + let w = word + let b = bit + if w > 0, b == 0 { return (w &- 1, UInt(_Word.capacity)) } + return (w, b) + } + + @inlinable + internal static func ==(left: Self, right: Self) -> Bool { + left.value == right.value + } + + @inlinable + internal static func <(left: Self, right: Self) -> Bool { + left.value < right.value + } + + @inlinable + internal func hash(into hasher: inout Hasher) { + hasher.combine(value) + } + + @inlinable + internal func _successor() -> Self { + Self(value + 1) + } + + @inlinable + internal func _predecessor() -> Self { + Self(value - 1) + } +} +#else // !COLLECTIONS_SINGLE_MODULE +extension _UnsafeBitSet { + @frozen + public struct Index: Comparable, Hashable { + @usableFromInline + internal typealias _Word = _UnsafeBitSet._Word + + + public var value: UInt + + @inlinable + public init(_ value: UInt) { + self.value = value + } + + @inlinable + public init(_ value: Int) { + self.value = UInt(value) + } + + @inlinable + public init(word: Int, bit: UInt) { + assert(word >= 0 && word <= Int.max / _Word.capacity) + assert(bit < _Word.capacity) + self.value = UInt(word &* _Word.capacity) &+ bit + } + } +} + +extension _UnsafeBitSet.Index { + @inlinable + public var word: Int { + // Note: We perform on UInts to get faster unsigned math (shifts). + Int(truncatingIfNeeded: value / UInt(bitPattern: _Word.capacity)) + } + + @inlinable + public var bit: UInt { + // Note: We perform on UInts to get faster unsigned math (masking). + value % UInt(bitPattern: _Word.capacity) + } + + @inlinable + public var split: (word: Int, bit: UInt) { + (word, bit) + } + + @inlinable + public var endSplit: (word: Int, bit: UInt) { + let w = word + let b = bit + if w > 0, b == 0 { return (w &- 1, UInt(_Word.capacity)) } + return (w, b) + } + + @inlinable + public static func ==(left: Self, right: Self) -> Bool { + left.value == right.value + } + + @inlinable + public static func <(left: Self, right: Self) -> Bool { + left.value < right.value + } + + @inlinable + public func hash(into hasher: inout Hasher) { + hasher.combine(value) + } + + @inlinable + internal func _successor() -> Self { + Self(value + 1) + } + + @inlinable + internal func _predecessor() -> Self { + Self(value - 1) + } +} +#endif // COLLECTIONS_SINGLE_MODULE diff --git a/Sources/_CollectionsUtilities/UnsafeBitSet/autogenerated/_UnsafeBitSet+_Word.swift b/Sources/_CollectionsUtilities/UnsafeBitSet/autogenerated/_UnsafeBitSet+_Word.swift new file mode 100644 index 000000000..586e872e4 --- /dev/null +++ b/Sources/_CollectionsUtilities/UnsafeBitSet/autogenerated/_UnsafeBitSet+_Word.swift @@ -0,0 +1,643 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + + +// ############################################################################# +// # # +// # DO NOT EDIT THIS FILE; IT IS AUTOGENERATED. # +// # # +// ############################################################################# + + + + +// In single module mode, we need these declarations to be internal, +// but in regular builds we want them to be public. Unfortunately +// the current best way to do this is to duplicate all definitions. +#if COLLECTIONS_SINGLE_MODULE +extension _UnsafeBitSet { + @frozen @usableFromInline + internal struct _Word { + @usableFromInline + internal var value: UInt + + @inlinable + @inline(__always) + internal init(_ value: UInt) { + self.value = value + } + + @inlinable + @inline(__always) + internal init(upTo bit: UInt) { + assert(bit <= _Word.capacity) + self.init((1 << bit) &- 1) + } + + @inlinable + @inline(__always) + internal init(from start: UInt, to end: UInt) { + assert(start <= end && end <= _Word.capacity) + self = Self(upTo: end).symmetricDifference(Self(upTo: start)) + } + } +} + +extension _UnsafeBitSet._Word: CustomStringConvertible { + @usableFromInline + internal var description: String { + String(value, radix: 16) + } +} + +extension _UnsafeBitSet._Word { + /// Returns the `n`th member in `self`. + /// + /// - Parameter n: The offset of the element to retrieve. This value is + /// decremented by the number of items found in this `self` towards the + /// value we're looking for. (If the function returns non-nil, then `n` + /// is set to `0` on return.) + /// - Returns: If this word contains enough members to satisfy the request, + /// then this function returns the member found. Otherwise it returns nil. + @inline(never) + internal func nthElement(_ n: inout UInt) -> UInt? { + let c = UInt(bitPattern: count) + guard n < c else { + n &-= c + return nil + } + let m = Int(bitPattern: n) + n = 0 + return value._bit(ranked: m)! + } + + @inline(never) + internal func nthElementFromEnd(_ n: inout UInt) -> UInt? { + let c = UInt(bitPattern: count) + guard n < c else { + n &-= c + return nil + } + let m = Int(bitPattern: c &- 1 &- n) + n = 0 + return value._bit(ranked: m)! + } +} + +extension _UnsafeBitSet._Word { + @inlinable + @inline(__always) + internal static func wordCount(forBitCount count: UInt) -> Int { + // Note: We perform on UInts to get faster unsigned math (shifts). + let width = UInt(bitPattern: Self.capacity) + return Int(bitPattern: (count + width - 1) / width) + } +} + +extension _UnsafeBitSet._Word { + @inlinable + @inline(__always) + internal static var capacity: Int { + return UInt.bitWidth + } + + @inlinable + @inline(__always) + internal var count: Int { + value.nonzeroBitCount + } + + @inlinable + @inline(__always) + internal var isEmpty: Bool { + value == 0 + } + + @inlinable + @inline(__always) + internal var isFull: Bool { + value == UInt.max + } + + @inlinable + @inline(__always) + internal func contains(_ bit: UInt) -> Bool { + assert(bit >= 0 && bit < UInt.bitWidth) + return value & (1 &<< bit) != 0 + } + + @inlinable + @inline(__always) + internal var firstMember: UInt? { + value._lastSetBit + } + + @inlinable + @inline(__always) + internal var lastMember: UInt? { + value._firstSetBit + } + + @inlinable + @inline(__always) + @discardableResult + internal mutating func insert(_ bit: UInt) -> Bool { + assert(bit < UInt.bitWidth) + let mask: UInt = 1 &<< bit + let inserted = value & mask == 0 + value |= mask + return inserted + } + + @inlinable + @inline(__always) + @discardableResult + internal mutating func remove(_ bit: UInt) -> Bool { + assert(bit < UInt.bitWidth) + let mask: UInt = 1 &<< bit + let removed = (value & mask) != 0 + value &= ~mask + return removed + } + + @inlinable + @inline(__always) + internal mutating func update(_ bit: UInt, to value: Bool) { + assert(bit < UInt.bitWidth) + let mask: UInt = 1 &<< bit + if value { + self.value |= mask + } else { + self.value &= ~mask + } + } +} + +extension _UnsafeBitSet._Word { + @inlinable + @inline(__always) + internal mutating func insertAll(upTo bit: UInt) { + assert(bit >= 0 && bit < Self.capacity) + let mask: UInt = (1 as UInt &<< bit) &- 1 + value |= mask + } + + @inlinable + @inline(__always) + internal mutating func removeAll(upTo bit: UInt) { + assert(bit >= 0 && bit < Self.capacity) + let mask = UInt.max &<< bit + value &= mask + } + + @inlinable + @inline(__always) + internal mutating func removeAll(through bit: UInt) { + assert(bit >= 0 && bit < Self.capacity) + var mask = UInt.max &<< bit + mask &= mask &- 1 // Clear lowest nonzero bit. + value &= mask + } + + @inlinable + @inline(__always) + internal mutating func removeAll(from bit: UInt) { + assert(bit >= 0 && bit < Self.capacity) + let mask: UInt = (1 as UInt &<< bit) &- 1 + value &= mask + } +} + +extension _UnsafeBitSet._Word { + @inlinable + @inline(__always) + internal static var empty: Self { + Self(0) + } + + @inline(__always) + internal static var allBits: Self { + Self(UInt.max) + } +} + +// _Word implements Sequence by using a copy of itself as its Iterator. +// Iteration with `next()` destroys the word's value; however, this won't cause +// problems in normal use, because `next()` is usually called on a separate +// iterator, not the original word. +extension _UnsafeBitSet._Word: Sequence, IteratorProtocol { + @inlinable @inline(__always) + internal var underestimatedCount: Int { + count + } + + /// Return the index of the lowest set bit in this word, + /// and also destructively clear it. + @inlinable + internal mutating func next() -> UInt? { + guard value != 0 else { return nil } + let bit = UInt(truncatingIfNeeded: value.trailingZeroBitCount) + value &= value &- 1 // Clear lowest nonzero bit. + return bit + } +} + +extension _UnsafeBitSet._Word: Equatable { + @inlinable + internal static func ==(left: Self, right: Self) -> Bool { + left.value == right.value + } +} + +extension _UnsafeBitSet._Word: Hashable { + @inlinable + internal func hash(into hasher: inout Hasher) { + hasher.combine(value) + } +} + +extension _UnsafeBitSet._Word { + @inlinable @inline(__always) + internal func complement() -> Self { + Self(~self.value) + } + + @inlinable @inline(__always) + internal mutating func formComplement() { + self.value = ~self.value + } + + @inlinable @inline(__always) + internal func union(_ other: Self) -> Self { + Self(self.value | other.value) + } + + @inlinable @inline(__always) + internal mutating func formUnion(_ other: Self) { + self.value |= other.value + } + + @inlinable @inline(__always) + internal func intersection(_ other: Self) -> Self { + Self(self.value & other.value) + } + + @inlinable @inline(__always) + internal mutating func formIntersection(_ other: Self) { + self.value &= other.value + } + + @inlinable @inline(__always) + internal func symmetricDifference(_ other: Self) -> Self { + Self(self.value ^ other.value) + } + + @inlinable @inline(__always) + internal mutating func formSymmetricDifference(_ other: Self) { + self.value ^= other.value + } + + @inlinable @inline(__always) + internal func subtracting(_ other: Self) -> Self { + Self(self.value & ~other.value) + } + + @inlinable @inline(__always) + internal mutating func subtract(_ other: Self) { + self.value &= ~other.value + } +} + +extension _UnsafeBitSet._Word { + @inlinable + @inline(__always) + internal func shiftedDown(by shift: UInt) -> Self { + assert(shift >= 0 && shift < Self.capacity) + return Self(self.value &>> shift) + } + + @inlinable + @inline(__always) + internal func shiftedUp(by shift: UInt) -> Self { + assert(shift >= 0 && shift < Self.capacity) + return Self(self.value &<< shift) + } +} +#else // !COLLECTIONS_SINGLE_MODULE +extension _UnsafeBitSet { + @frozen + public struct _Word { + + public var value: UInt + + @inlinable + @inline(__always) + public init(_ value: UInt) { + self.value = value + } + + @inlinable + @inline(__always) + public init(upTo bit: UInt) { + assert(bit <= _Word.capacity) + self.init((1 << bit) &- 1) + } + + @inlinable + @inline(__always) + public init(from start: UInt, to end: UInt) { + assert(start <= end && end <= _Word.capacity) + self = Self(upTo: end).symmetricDifference(Self(upTo: start)) + } + } +} + +extension _UnsafeBitSet._Word: CustomStringConvertible { + //@usableFromInline + public var description: String { + String(value, radix: 16) + } +} + +extension _UnsafeBitSet._Word { + /// Returns the `n`th member in `self`. + /// + /// - Parameter n: The offset of the element to retrieve. This value is + /// decremented by the number of items found in this `self` towards the + /// value we're looking for. (If the function returns non-nil, then `n` + /// is set to `0` on return.) + /// - Returns: If this word contains enough members to satisfy the request, + /// then this function returns the member found. Otherwise it returns nil. + @inline(never) + internal func nthElement(_ n: inout UInt) -> UInt? { + let c = UInt(bitPattern: count) + guard n < c else { + n &-= c + return nil + } + let m = Int(bitPattern: n) + n = 0 + return value._bit(ranked: m)! + } + + @inline(never) + internal func nthElementFromEnd(_ n: inout UInt) -> UInt? { + let c = UInt(bitPattern: count) + guard n < c else { + n &-= c + return nil + } + let m = Int(bitPattern: c &- 1 &- n) + n = 0 + return value._bit(ranked: m)! + } +} + +extension _UnsafeBitSet._Word { + @inlinable + @inline(__always) + public static func wordCount(forBitCount count: UInt) -> Int { + // Note: We perform on UInts to get faster unsigned math (shifts). + let width = UInt(bitPattern: Self.capacity) + return Int(bitPattern: (count + width - 1) / width) + } +} + +extension _UnsafeBitSet._Word { + @inlinable + @inline(__always) + public static var capacity: Int { + return UInt.bitWidth + } + + @inlinable + @inline(__always) + public var count: Int { + value.nonzeroBitCount + } + + @inlinable + @inline(__always) + public var isEmpty: Bool { + value == 0 + } + + @inlinable + @inline(__always) + public var isFull: Bool { + value == UInt.max + } + + @inlinable + @inline(__always) + public func contains(_ bit: UInt) -> Bool { + assert(bit >= 0 && bit < UInt.bitWidth) + return value & (1 &<< bit) != 0 + } + + @inlinable + @inline(__always) + public var firstMember: UInt? { + value._lastSetBit + } + + @inlinable + @inline(__always) + public var lastMember: UInt? { + value._firstSetBit + } + + @inlinable + @inline(__always) + @discardableResult + public mutating func insert(_ bit: UInt) -> Bool { + assert(bit < UInt.bitWidth) + let mask: UInt = 1 &<< bit + let inserted = value & mask == 0 + value |= mask + return inserted + } + + @inlinable + @inline(__always) + @discardableResult + public mutating func remove(_ bit: UInt) -> Bool { + assert(bit < UInt.bitWidth) + let mask: UInt = 1 &<< bit + let removed = (value & mask) != 0 + value &= ~mask + return removed + } + + @inlinable + @inline(__always) + public mutating func update(_ bit: UInt, to value: Bool) { + assert(bit < UInt.bitWidth) + let mask: UInt = 1 &<< bit + if value { + self.value |= mask + } else { + self.value &= ~mask + } + } +} + +extension _UnsafeBitSet._Word { + @inlinable + @inline(__always) + internal mutating func insertAll(upTo bit: UInt) { + assert(bit >= 0 && bit < Self.capacity) + let mask: UInt = (1 as UInt &<< bit) &- 1 + value |= mask + } + + @inlinable + @inline(__always) + internal mutating func removeAll(upTo bit: UInt) { + assert(bit >= 0 && bit < Self.capacity) + let mask = UInt.max &<< bit + value &= mask + } + + @inlinable + @inline(__always) + internal mutating func removeAll(through bit: UInt) { + assert(bit >= 0 && bit < Self.capacity) + var mask = UInt.max &<< bit + mask &= mask &- 1 // Clear lowest nonzero bit. + value &= mask + } + + @inlinable + @inline(__always) + internal mutating func removeAll(from bit: UInt) { + assert(bit >= 0 && bit < Self.capacity) + let mask: UInt = (1 as UInt &<< bit) &- 1 + value &= mask + } +} + +extension _UnsafeBitSet._Word { + @inlinable + @inline(__always) + public static var empty: Self { + Self(0) + } + + @inline(__always) + public static var allBits: Self { + Self(UInt.max) + } +} + +// _Word implements Sequence by using a copy of itself as its Iterator. +// Iteration with `next()` destroys the word's value; however, this won't cause +// problems in normal use, because `next()` is usually called on a separate +// iterator, not the original word. +extension _UnsafeBitSet._Word: Sequence, IteratorProtocol { + @inlinable @inline(__always) + public var underestimatedCount: Int { + count + } + + /// Return the index of the lowest set bit in this word, + /// and also destructively clear it. + @inlinable + public mutating func next() -> UInt? { + guard value != 0 else { return nil } + let bit = UInt(truncatingIfNeeded: value.trailingZeroBitCount) + value &= value &- 1 // Clear lowest nonzero bit. + return bit + } +} + +extension _UnsafeBitSet._Word: Equatable { + @inlinable + public static func ==(left: Self, right: Self) -> Bool { + left.value == right.value + } +} + +extension _UnsafeBitSet._Word: Hashable { + @inlinable + public func hash(into hasher: inout Hasher) { + hasher.combine(value) + } +} + +extension _UnsafeBitSet._Word { + @inlinable @inline(__always) + public func complement() -> Self { + Self(~self.value) + } + + @inlinable @inline(__always) + public mutating func formComplement() { + self.value = ~self.value + } + + @inlinable @inline(__always) + public func union(_ other: Self) -> Self { + Self(self.value | other.value) + } + + @inlinable @inline(__always) + public mutating func formUnion(_ other: Self) { + self.value |= other.value + } + + @inlinable @inline(__always) + public func intersection(_ other: Self) -> Self { + Self(self.value & other.value) + } + + @inlinable @inline(__always) + public mutating func formIntersection(_ other: Self) { + self.value &= other.value + } + + @inlinable @inline(__always) + public func symmetricDifference(_ other: Self) -> Self { + Self(self.value ^ other.value) + } + + @inlinable @inline(__always) + public mutating func formSymmetricDifference(_ other: Self) { + self.value ^= other.value + } + + @inlinable @inline(__always) + public func subtracting(_ other: Self) -> Self { + Self(self.value & ~other.value) + } + + @inlinable @inline(__always) + public mutating func subtract(_ other: Self) { + self.value &= ~other.value + } +} + +extension _UnsafeBitSet._Word { + @inlinable + @inline(__always) + public func shiftedDown(by shift: UInt) -> Self { + assert(shift >= 0 && shift < Self.capacity) + return Self(self.value &>> shift) + } + + @inlinable + @inline(__always) + public func shiftedUp(by shift: UInt) -> Self { + assert(shift >= 0 && shift < Self.capacity) + return Self(self.value &<< shift) + } +} +#endif // COLLECTIONS_SINGLE_MODULE diff --git a/Sources/_CollectionsUtilities/UnsafeBitSet/autogenerated/_UnsafeBitSet.swift b/Sources/_CollectionsUtilities/UnsafeBitSet/autogenerated/_UnsafeBitSet.swift new file mode 100644 index 000000000..a18b3a543 --- /dev/null +++ b/Sources/_CollectionsUtilities/UnsafeBitSet/autogenerated/_UnsafeBitSet.swift @@ -0,0 +1,968 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + + +// ############################################################################# +// # # +// # DO NOT EDIT THIS FILE; IT IS AUTOGENERATED. # +// # # +// ############################################################################# + + + +// In single module mode, we need these declarations to be internal, +// but in regular builds we want them to be public. Unfortunately +// the current best way to do this is to duplicate all definitions. +#if COLLECTIONS_SINGLE_MODULE +/// An unsafe-unowned bitset view over `UInt` storage, providing bit set +/// primitives. +@frozen @usableFromInline +internal struct _UnsafeBitSet { + /// An unsafe-unowned storage view. + @usableFromInline + internal let _words: UnsafeBufferPointer<_Word> + +#if DEBUG + /// True when this handle does not support table mutations. + /// (This is only checked in debug builds.) + @usableFromInline + internal let _mutable: Bool +#endif + + @inlinable + @inline(__always) + internal func ensureMutable() { +#if DEBUG + assert(_mutable) +#endif + } + + @inlinable + @inline(__always) + internal var _mutableWords: UnsafeMutableBufferPointer<_Word> { + ensureMutable() + return UnsafeMutableBufferPointer(mutating: _words) + } + + @inlinable + @inline(__always) + internal init( + words: UnsafeBufferPointer<_Word>, + mutable: Bool + ) { + assert(words.baseAddress != nil) + self._words = words +#if DEBUG + self._mutable = mutable +#endif + } + + @inlinable + @inline(__always) + internal init( + words: UnsafeMutableBufferPointer<_Word>, + mutable: Bool + ) { + self.init(words: UnsafeBufferPointer(words), mutable: mutable) + } +} + +extension _UnsafeBitSet { + @inlinable + @inline(__always) + internal var wordCount: Int { + _words.count + } +} + +extension _UnsafeBitSet { + @inlinable + @inline(__always) + internal static func withTemporaryBitSet( + capacity: Int, + run body: (inout _UnsafeBitSet) throws -> R + ) rethrows -> R { + let wordCount = _UnsafeBitSet.wordCount(forCapacity: UInt(capacity)) + return try withTemporaryBitSet(wordCount: wordCount, run: body) + } + + @inlinable + @inline(__always) + internal static func withTemporaryBitSet( + wordCount: Int, + run body: (inout Self) throws -> R + ) rethrows -> R { + var result: R? + try _withTemporaryBitSet(wordCount: wordCount) { bitset in + result = try body(&bitset) + } + return result! + } + + @inline(never) + @usableFromInline + internal static func _withTemporaryBitSet( + wordCount: Int, + run body: (inout Self) throws -> Void + ) rethrows { + try _withTemporaryUninitializedBitSet(wordCount: wordCount) { handle in + handle._mutableWords.initialize(repeating: .empty) + try body(&handle) + } + } + + internal static func _withTemporaryUninitializedBitSet( + wordCount: Int, + run body: (inout Self) throws -> Void + ) rethrows { + assert(wordCount >= 0) +#if compiler(>=5.6) + return try withUnsafeTemporaryAllocation( + of: _Word.self, capacity: wordCount + ) { words in + var bitset = Self(words: words, mutable: true) + return try body(&bitset) + } +#else + if wordCount <= 2 { + var buffer: (_Word, _Word) = (.empty, .empty) + return try withUnsafeMutablePointer(to: &buffer) { p in + // Homogeneous tuples are layout-compatible with their component type. + let start = UnsafeMutableRawPointer(p) + .assumingMemoryBound(to: _Word.self) + let words = UnsafeMutableBufferPointer(start: start, count: wordCount) + var bitset = Self(words: words, mutable: true) + return try body(&bitset) + } + } + let words = UnsafeMutableBufferPointer<_Word>.allocate(capacity: wordCount) + defer { words.deallocate() } + var bitset = Self(words: words, mutable: true) + return try body(&bitset) +#endif + } +} + +extension _UnsafeBitSet { + @_effects(readnone) + @inlinable @inline(__always) + internal static func wordCount(forCapacity capacity: UInt) -> Int { + _Word.wordCount(forBitCount: capacity) + } + + @inlinable @inline(__always) + internal var capacity: UInt { + UInt(wordCount &* _Word.capacity) + } + + @inlinable @inline(__always) + internal func isWithinBounds(_ element: UInt) -> Bool { + element < capacity + } + + @_effects(releasenone) + @inline(__always) + @usableFromInline + internal func contains(_ element: UInt) -> Bool { + let (word, bit) = Index(element).split + guard word < wordCount else { return false } + return _words[word].contains(bit) + } + + @_effects(releasenone) + @usableFromInline + @discardableResult + internal mutating func insert(_ element: UInt) -> Bool { + ensureMutable() + assert(isWithinBounds(element)) + let index = Index(element) + return _mutableWords[index.word].insert(index.bit) + } + + @_effects(releasenone) + @usableFromInline + @discardableResult + internal mutating func remove(_ element: UInt) -> Bool { + ensureMutable() + let index = Index(element) + if index.word >= _words.count { return false } + return _mutableWords[index.word].remove(index.bit) + } + + @_effects(releasenone) + @usableFromInline + internal mutating func update(_ member: UInt, to newValue: Bool) -> Bool { + ensureMutable() + let (w, b) = Index(member).split + _mutableWords[w].update(b, to: newValue) + return w == _words.count &- 1 + } + + @_effects(releasenone) + @usableFromInline + internal mutating func insertAll(upTo max: UInt) { + assert(max <= capacity) + guard max > 0 else { return } + let (w, b) = Index(max).split + for i in 0 ..< w { + _mutableWords[i] = .allBits + } + if b > 0 { + _mutableWords[w].insertAll(upTo: b) + } + } + + @_alwaysEmitIntoClient + @usableFromInline + @inline(__always) + @discardableResult + internal mutating func insert(_ element: Int) -> Bool { + precondition(element >= 0) + return insert(UInt(bitPattern: element)) + } + + @_alwaysEmitIntoClient + @usableFromInline + @inline(__always) + @discardableResult + internal mutating func remove(_ element: Int) -> Bool { + guard element >= 0 else { return false } + return remove(UInt(bitPattern: element)) + } + + @_alwaysEmitIntoClient + @usableFromInline + @inline(__always) + internal mutating func insertAll(upTo max: Int) { + precondition(max >= 0) + return insertAll(upTo: UInt(bitPattern: max)) + } +} + +extension _UnsafeBitSet: Sequence { + @usableFromInline + internal typealias Element = UInt + + @inlinable + @inline(__always) + internal var underestimatedCount: Int { + count // FIXME: really? + } + + @inlinable + @inline(__always) + internal func makeIterator() -> Iterator { + return Iterator(self) + } + + @usableFromInline @frozen + internal struct Iterator: IteratorProtocol { + @usableFromInline + internal let _bitset: _UnsafeBitSet + + @usableFromInline + internal var _index: Int + + @usableFromInline + internal var _word: _Word + + @inlinable + internal init(_ bitset: _UnsafeBitSet) { + self._bitset = bitset + self._index = 0 + self._word = bitset.wordCount > 0 ? bitset._words[0] : .empty + } + + @_effects(releasenone) + @usableFromInline + internal mutating func next() -> UInt? { + if let bit = _word.next() { + return Index(word: _index, bit: bit).value + } + while (_index + 1) < _bitset.wordCount { + _index += 1 + _word = _bitset._words[_index] + if let bit = _word.next() { + return Index(word: _index, bit: bit).value + } + } + return nil + } + } +} + +extension _UnsafeBitSet: BidirectionalCollection { + @inlinable + @inline(__always) + internal var count: Int { + _words.reduce(0) { $0 + $1.count } + } + + @inlinable + @inline(__always) + internal var isEmpty: Bool { + _words.firstIndex(where: { !$0.isEmpty }) == nil + } + + @inlinable + internal var startIndex: Index { + let word = _words.firstIndex { !$0.isEmpty } + guard let word = word else { return endIndex } + return Index(word: word, bit: _words[word].firstMember!) + } + + @inlinable + internal var endIndex: Index { + Index(word: wordCount, bit: 0) + } + + @inlinable + internal subscript(position: Index) -> UInt { + position.value + } + + @_effects(releasenone) + @usableFromInline + internal func index(after index: Index) -> Index { + precondition(index < endIndex, "Index out of bounds") + var word = index.word + var w = _words[word] + w.removeAll(through: index.bit) + while w.isEmpty { + word += 1 + guard word < wordCount else { + return Index(word: wordCount, bit: 0) + } + w = _words[word] + } + return Index(word: word, bit: w.firstMember!) + } + + @_effects(releasenone) + @usableFromInline + internal func index(before index: Index) -> Index { + precondition(index <= endIndex, "Index out of bounds") + var word = index.word + var w: _Word + if index.bit > 0 { + w = _words[word] + w.removeAll(from: index.bit) + } else { + w = .empty + } + while w.isEmpty { + word -= 1 + precondition(word >= 0, "Can't advance below startIndex") + w = _words[word] + } + return Index(word: word, bit: w.lastMember!) + } + + @_effects(releasenone) + @usableFromInline + internal func distance(from start: Index, to end: Index) -> Int { + precondition(start <= endIndex && end <= endIndex, "Index out of bounds") + let isNegative = end < start + let (start, end) = (Swift.min(start, end), Swift.max(start, end)) + + let (w1, b1) = start.split + let (w2, b2) = end.split + + if w1 == w2 { + guard w1 < wordCount else { return 0 } + let mask = _Word(from: b1, to: b2) + let c = _words[w1].intersection(mask).count + return isNegative ? -c : c + } + + var c = 0 + var w = w1 + guard w < wordCount else { return 0 } + + c &+= _words[w].subtracting(_Word(upTo: b1)).count + w &+= 1 + while w < w2 { + c &+= _words[w].count + w &+= 1 + } + guard w < wordCount else { return isNegative ? -c : c } + c &+= _words[w].intersection(_Word(upTo: b2)).count + return isNegative ? -c : c + } + + @_effects(releasenone) + @usableFromInline + internal func index(_ i: Index, offsetBy distance: Int) -> Index { + precondition(i <= endIndex, "Index out of bounds") + precondition(i == endIndex || contains(i.value), "Invalid index") + guard distance != 0 else { return i } + var remaining = distance.magnitude + if distance > 0 { + var (w, b) = i.split + precondition(w < wordCount, "Index out of bounds") + if let v = _words[w].subtracting(_Word(upTo: b)).nthElement(&remaining) { + return Index(word: w, bit: v) + } + while true { + w &+= 1 + guard w < wordCount else { break } + if let v = _words[w].nthElement(&remaining) { + return Index(word: w, bit: v) + } + } + precondition(remaining == 0, "Index out of bounds") + return endIndex + } + + // distance < 0 + remaining -= 1 + var (w, b) = i.endSplit + if w < wordCount { + if let v = _words[w].intersection(_Word(upTo: b)).nthElementFromEnd(&remaining) { + return Index(word: w, bit: v) + } + } + while true { + precondition(w > 0, "Index out of bounds") + w &-= 1 + if let v = _words[w].nthElementFromEnd(&remaining) { + return Index(word: w, bit: v) + } + } + } + + @_effects(releasenone) + @usableFromInline + internal func index( + _ i: Index, offsetBy distance: Int, limitedBy limit: Index + ) -> Index? { + precondition(i <= endIndex && limit <= endIndex, "Index out of bounds") + precondition(i == endIndex || contains(i.value), "Invalid index") + guard distance != 0 else { return i } + var remaining = distance.magnitude + if distance > 0 { + guard i <= limit else { + return self.index(i, offsetBy: distance) + } + var (w, b) = i.split + if w < wordCount, + let v = _words[w].subtracting(_Word(upTo: b)).nthElement(&remaining) + { + let r = Index(word: w, bit: v) + return r <= limit ? r : nil + } + let maxWord = Swift.min(wordCount - 1, limit.word) + while w < maxWord { + w &+= 1 + if let v = _words[w].nthElement(&remaining) { + let r = Index(word: w, bit: v) + return r <= limit ? r : nil + } + } + return remaining == 0 && limit == endIndex ? endIndex : nil + } + + // distance < 0 + guard i >= limit else { + return self.index(i, offsetBy: distance) + } + remaining &-= 1 + var (w, b) = i.endSplit + if w < wordCount { + if let v = _words[w].intersection(_Word(upTo: b)).nthElementFromEnd(&remaining) { + let r = Index(word: w, bit: v) + return r >= limit ? r : nil + } + } + let minWord = limit.word + while w > minWord { + w &-= 1 + if let v = _words[w].nthElementFromEnd(&remaining) { + let r = Index(word: w, bit: v) + return r >= limit ? r : nil + } + } + return nil + } +} +#else // !COLLECTIONS_SINGLE_MODULE +/// An unsafe-unowned bitset view over `UInt` storage, providing bit set +/// primitives. +@frozen +public struct _UnsafeBitSet { + /// An unsafe-unowned storage view. + + public let _words: UnsafeBufferPointer<_Word> + +#if DEBUG + /// True when this handle does not support table mutations. + /// (This is only checked in debug builds.) + @usableFromInline + internal let _mutable: Bool +#endif + + @inlinable + @inline(__always) + public func ensureMutable() { +#if DEBUG + assert(_mutable) +#endif + } + + @inlinable + @inline(__always) + public var _mutableWords: UnsafeMutableBufferPointer<_Word> { + ensureMutable() + return UnsafeMutableBufferPointer(mutating: _words) + } + + @inlinable + @inline(__always) + public init( + words: UnsafeBufferPointer<_Word>, + mutable: Bool + ) { + assert(words.baseAddress != nil) + self._words = words +#if DEBUG + self._mutable = mutable +#endif + } + + @inlinable + @inline(__always) + public init( + words: UnsafeMutableBufferPointer<_Word>, + mutable: Bool + ) { + self.init(words: UnsafeBufferPointer(words), mutable: mutable) + } +} + +extension _UnsafeBitSet { + @inlinable + @inline(__always) + public var wordCount: Int { + _words.count + } +} + +extension _UnsafeBitSet { + @inlinable + @inline(__always) + public static func withTemporaryBitSet( + capacity: Int, + run body: (inout _UnsafeBitSet) throws -> R + ) rethrows -> R { + let wordCount = _UnsafeBitSet.wordCount(forCapacity: UInt(capacity)) + return try withTemporaryBitSet(wordCount: wordCount, run: body) + } + + @inlinable + @inline(__always) + public static func withTemporaryBitSet( + wordCount: Int, + run body: (inout Self) throws -> R + ) rethrows -> R { + var result: R? + try _withTemporaryBitSet(wordCount: wordCount) { bitset in + result = try body(&bitset) + } + return result! + } + + @inline(never) + @usableFromInline + internal static func _withTemporaryBitSet( + wordCount: Int, + run body: (inout Self) throws -> Void + ) rethrows { + try _withTemporaryUninitializedBitSet(wordCount: wordCount) { handle in + handle._mutableWords.initialize(repeating: .empty) + try body(&handle) + } + } + + internal static func _withTemporaryUninitializedBitSet( + wordCount: Int, + run body: (inout Self) throws -> Void + ) rethrows { + assert(wordCount >= 0) +#if compiler(>=5.6) + return try withUnsafeTemporaryAllocation( + of: _Word.self, capacity: wordCount + ) { words in + var bitset = Self(words: words, mutable: true) + return try body(&bitset) + } +#else + if wordCount <= 2 { + var buffer: (_Word, _Word) = (.empty, .empty) + return try withUnsafeMutablePointer(to: &buffer) { p in + // Homogeneous tuples are layout-compatible with their component type. + let start = UnsafeMutableRawPointer(p) + .assumingMemoryBound(to: _Word.self) + let words = UnsafeMutableBufferPointer(start: start, count: wordCount) + var bitset = Self(words: words, mutable: true) + return try body(&bitset) + } + } + let words = UnsafeMutableBufferPointer<_Word>.allocate(capacity: wordCount) + defer { words.deallocate() } + var bitset = Self(words: words, mutable: true) + return try body(&bitset) +#endif + } +} + +extension _UnsafeBitSet { + @_effects(readnone) + @inlinable @inline(__always) + public static func wordCount(forCapacity capacity: UInt) -> Int { + _Word.wordCount(forBitCount: capacity) + } + + @inlinable @inline(__always) + public var capacity: UInt { + UInt(wordCount &* _Word.capacity) + } + + @inlinable @inline(__always) + internal func isWithinBounds(_ element: UInt) -> Bool { + element < capacity + } + + @_effects(releasenone) + @inline(__always) + //@usableFromInline + public func contains(_ element: UInt) -> Bool { + let (word, bit) = Index(element).split + guard word < wordCount else { return false } + return _words[word].contains(bit) + } + + @_effects(releasenone) + //@usableFromInline + @discardableResult + public mutating func insert(_ element: UInt) -> Bool { + ensureMutable() + assert(isWithinBounds(element)) + let index = Index(element) + return _mutableWords[index.word].insert(index.bit) + } + + @_effects(releasenone) + //@usableFromInline + @discardableResult + public mutating func remove(_ element: UInt) -> Bool { + ensureMutable() + let index = Index(element) + if index.word >= _words.count { return false } + return _mutableWords[index.word].remove(index.bit) + } + + @_effects(releasenone) + //@usableFromInline + public mutating func update(_ member: UInt, to newValue: Bool) -> Bool { + ensureMutable() + let (w, b) = Index(member).split + _mutableWords[w].update(b, to: newValue) + return w == _words.count &- 1 + } + + @_effects(releasenone) + //@usableFromInline + public mutating func insertAll(upTo max: UInt) { + assert(max <= capacity) + guard max > 0 else { return } + let (w, b) = Index(max).split + for i in 0 ..< w { + _mutableWords[i] = .allBits + } + if b > 0 { + _mutableWords[w].insertAll(upTo: b) + } + } + + @_alwaysEmitIntoClient + //@usableFromInline + @inline(__always) + @discardableResult + public mutating func insert(_ element: Int) -> Bool { + precondition(element >= 0) + return insert(UInt(bitPattern: element)) + } + + @_alwaysEmitIntoClient + //@usableFromInline + @inline(__always) + @discardableResult + public mutating func remove(_ element: Int) -> Bool { + guard element >= 0 else { return false } + return remove(UInt(bitPattern: element)) + } + + @_alwaysEmitIntoClient + //@usableFromInline + @inline(__always) + public mutating func insertAll(upTo max: Int) { + precondition(max >= 0) + return insertAll(upTo: UInt(bitPattern: max)) + } +} + +extension _UnsafeBitSet: Sequence { + //@usableFromInline + public typealias Element = UInt + + @inlinable + @inline(__always) + public var underestimatedCount: Int { + count // FIXME: really? + } + + @inlinable + @inline(__always) + public func makeIterator() -> Iterator { + return Iterator(self) + } + + @frozen + public struct Iterator: IteratorProtocol { + @usableFromInline + internal let _bitset: _UnsafeBitSet + + @usableFromInline + internal var _index: Int + + @usableFromInline + internal var _word: _Word + + @inlinable + internal init(_ bitset: _UnsafeBitSet) { + self._bitset = bitset + self._index = 0 + self._word = bitset.wordCount > 0 ? bitset._words[0] : .empty + } + + @_effects(releasenone) + //@usableFromInline + public mutating func next() -> UInt? { + if let bit = _word.next() { + return Index(word: _index, bit: bit).value + } + while (_index + 1) < _bitset.wordCount { + _index += 1 + _word = _bitset._words[_index] + if let bit = _word.next() { + return Index(word: _index, bit: bit).value + } + } + return nil + } + } +} + +extension _UnsafeBitSet: BidirectionalCollection { + @inlinable + @inline(__always) + public var count: Int { + _words.reduce(0) { $0 + $1.count } + } + + @inlinable + @inline(__always) + public var isEmpty: Bool { + _words.firstIndex(where: { !$0.isEmpty }) == nil + } + + @inlinable + public var startIndex: Index { + let word = _words.firstIndex { !$0.isEmpty } + guard let word = word else { return endIndex } + return Index(word: word, bit: _words[word].firstMember!) + } + + @inlinable + public var endIndex: Index { + Index(word: wordCount, bit: 0) + } + + @inlinable + public subscript(position: Index) -> UInt { + position.value + } + + @_effects(releasenone) + //@usableFromInline + public func index(after index: Index) -> Index { + precondition(index < endIndex, "Index out of bounds") + var word = index.word + var w = _words[word] + w.removeAll(through: index.bit) + while w.isEmpty { + word += 1 + guard word < wordCount else { + return Index(word: wordCount, bit: 0) + } + w = _words[word] + } + return Index(word: word, bit: w.firstMember!) + } + + @_effects(releasenone) + //@usableFromInline + public func index(before index: Index) -> Index { + precondition(index <= endIndex, "Index out of bounds") + var word = index.word + var w: _Word + if index.bit > 0 { + w = _words[word] + w.removeAll(from: index.bit) + } else { + w = .empty + } + while w.isEmpty { + word -= 1 + precondition(word >= 0, "Can't advance below startIndex") + w = _words[word] + } + return Index(word: word, bit: w.lastMember!) + } + + @_effects(releasenone) + //@usableFromInline + public func distance(from start: Index, to end: Index) -> Int { + precondition(start <= endIndex && end <= endIndex, "Index out of bounds") + let isNegative = end < start + let (start, end) = (Swift.min(start, end), Swift.max(start, end)) + + let (w1, b1) = start.split + let (w2, b2) = end.split + + if w1 == w2 { + guard w1 < wordCount else { return 0 } + let mask = _Word(from: b1, to: b2) + let c = _words[w1].intersection(mask).count + return isNegative ? -c : c + } + + var c = 0 + var w = w1 + guard w < wordCount else { return 0 } + + c &+= _words[w].subtracting(_Word(upTo: b1)).count + w &+= 1 + while w < w2 { + c &+= _words[w].count + w &+= 1 + } + guard w < wordCount else { return isNegative ? -c : c } + c &+= _words[w].intersection(_Word(upTo: b2)).count + return isNegative ? -c : c + } + + @_effects(releasenone) + //@usableFromInline + public func index(_ i: Index, offsetBy distance: Int) -> Index { + precondition(i <= endIndex, "Index out of bounds") + precondition(i == endIndex || contains(i.value), "Invalid index") + guard distance != 0 else { return i } + var remaining = distance.magnitude + if distance > 0 { + var (w, b) = i.split + precondition(w < wordCount, "Index out of bounds") + if let v = _words[w].subtracting(_Word(upTo: b)).nthElement(&remaining) { + return Index(word: w, bit: v) + } + while true { + w &+= 1 + guard w < wordCount else { break } + if let v = _words[w].nthElement(&remaining) { + return Index(word: w, bit: v) + } + } + precondition(remaining == 0, "Index out of bounds") + return endIndex + } + + // distance < 0 + remaining -= 1 + var (w, b) = i.endSplit + if w < wordCount { + if let v = _words[w].intersection(_Word(upTo: b)).nthElementFromEnd(&remaining) { + return Index(word: w, bit: v) + } + } + while true { + precondition(w > 0, "Index out of bounds") + w &-= 1 + if let v = _words[w].nthElementFromEnd(&remaining) { + return Index(word: w, bit: v) + } + } + } + + @_effects(releasenone) + //@usableFromInline + public func index( + _ i: Index, offsetBy distance: Int, limitedBy limit: Index + ) -> Index? { + precondition(i <= endIndex && limit <= endIndex, "Index out of bounds") + precondition(i == endIndex || contains(i.value), "Invalid index") + guard distance != 0 else { return i } + var remaining = distance.magnitude + if distance > 0 { + guard i <= limit else { + return self.index(i, offsetBy: distance) + } + var (w, b) = i.split + if w < wordCount, + let v = _words[w].subtracting(_Word(upTo: b)).nthElement(&remaining) + { + let r = Index(word: w, bit: v) + return r <= limit ? r : nil + } + let maxWord = Swift.min(wordCount - 1, limit.word) + while w < maxWord { + w &+= 1 + if let v = _words[w].nthElement(&remaining) { + let r = Index(word: w, bit: v) + return r <= limit ? r : nil + } + } + return remaining == 0 && limit == endIndex ? endIndex : nil + } + + // distance < 0 + guard i >= limit else { + return self.index(i, offsetBy: distance) + } + remaining &-= 1 + var (w, b) = i.endSplit + if w < wordCount { + if let v = _words[w].intersection(_Word(upTo: b)).nthElementFromEnd(&remaining) { + let r = Index(word: w, bit: v) + return r >= limit ? r : nil + } + } + let minWord = limit.word + while w > minWord { + w &-= 1 + if let v = _words[w].nthElementFromEnd(&remaining) { + let r = Index(word: w, bit: v) + return r >= limit ? r : nil + } + } + return nil + } +} +#endif // COLLECTIONS_SINGLE_MODULE diff --git a/Sources/_CollectionsUtilities/UnsafeBufferPointer+Extras.swift.gyb b/Sources/_CollectionsUtilities/UnsafeBufferPointer+Extras.swift.gyb new file mode 100644 index 000000000..603e3047e --- /dev/null +++ b/Sources/_CollectionsUtilities/UnsafeBufferPointer+Extras.swift.gyb @@ -0,0 +1,28 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +%{ + from gyb_utils import * +}% +${autogenerated_warning()} + +% for modifier in visibility_levels: +${visibility_boilerplate(modifier)} +extension UnsafeBufferPointer { + @inlinable + @inline(__always) + ${modifier} func _ptr(at index: Int) -> UnsafePointer { + assert(index >= 0 && index < count) + return baseAddress.unsafelyUnwrapped + index + } +} +% end +${visibility_boilerplate("end")} diff --git a/Sources/_CollectionsUtilities/UnsafeMutableBufferPointer+Extras.swift.gyb b/Sources/_CollectionsUtilities/UnsafeMutableBufferPointer+Extras.swift.gyb new file mode 100644 index 000000000..c7c6ee0fb --- /dev/null +++ b/Sources/_CollectionsUtilities/UnsafeMutableBufferPointer+Extras.swift.gyb @@ -0,0 +1,146 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +%{ + from gyb_utils import * +}% +${autogenerated_warning()} + +% for modifier in visibility_levels: +${visibility_boilerplate(modifier)} +extension UnsafeMutableBufferPointer { + @inlinable + public func initialize(fromContentsOf source: Self) -> Index { + guard source.count > 0 else { return 0 } + precondition( + source.count <= self.count, + "buffer cannot contain every element from source.") + baseAddress.unsafelyUnwrapped.initialize( + from: source.baseAddress.unsafelyUnwrapped, + count: source.count) + return source.count + } + + @inlinable + public func initialize(fromContentsOf source: Slice) -> Index { + let sourceCount = source.count + guard sourceCount > 0 else { return 0 } + precondition( + sourceCount <= self.count, + "buffer cannot contain every element from source.") + baseAddress.unsafelyUnwrapped.initialize( + from: source.base.baseAddress.unsafelyUnwrapped + source.startIndex, + count: sourceCount) + return sourceCount + } +} + +extension Slice { + @inlinable @inline(__always) + public func initialize( + fromContentsOf source: UnsafeMutableBufferPointer + ) -> Index + where Base == UnsafeMutableBufferPointer + { + let target = UnsafeMutableBufferPointer(rebasing: self) + let i = target.initialize(fromContentsOf: source) + return self.startIndex + i + } + + @inlinable @inline(__always) + public func initialize( + fromContentsOf source: Slice> + ) -> Index + where Base == UnsafeMutableBufferPointer + { + let target = UnsafeMutableBufferPointer(rebasing: self) + let i = target.initialize(fromContentsOf: source) + return self.startIndex + i + } +} + +extension UnsafeMutableBufferPointer { + @inlinable @inline(__always) + public func initializeAll( + fromContentsOf source: C + ) where C.Element == Element { + let i = self.initialize(fromContentsOf: source) + assert(i == self.endIndex) + } + + @inlinable @inline(__always) + public func initializeAll(fromContentsOf source: Self) { + let i = self.initialize(fromContentsOf: source) + assert(i == self.endIndex) + } + + @inlinable @inline(__always) + public func initializeAll(fromContentsOf source: Slice) { + let i = self.initialize(fromContentsOf: source) + assert(i == self.endIndex) + } + + @inlinable @inline(__always) + public func moveInitializeAll(fromContentsOf source: Self) { + let i = self.moveInitialize(fromContentsOf: source) + assert(i == self.endIndex) + } + + @inlinable @inline(__always) + public func moveInitializeAll(fromContentsOf source: Slice) { + let i = self.moveInitialize(fromContentsOf: source) + assert(i == self.endIndex) + } +} + +extension Slice { + @inlinable @inline(__always) + public func initializeAll( + fromContentsOf source: C + ) where Base == UnsafeMutableBufferPointer { + let i = self.initialize(fromContentsOf: source) + assert(i == self.endIndex) + } + + @inlinable @inline(__always) + public func initializeAll( + fromContentsOf source: UnsafeMutableBufferPointer + ) where Base == UnsafeMutableBufferPointer { + let target = UnsafeMutableBufferPointer(rebasing: self) + target.initializeAll(fromContentsOf: source) + } + + @inlinable @inline(__always) + public func initializeAll( + fromContentsOf source: Slice> + ) where Base == UnsafeMutableBufferPointer { + let target = UnsafeMutableBufferPointer(rebasing: self) + target.initializeAll(fromContentsOf: source) + } + + @inlinable @inline(__always) + public func moveInitializeAll( + fromContentsOf source: UnsafeMutableBufferPointer + ) where Base == UnsafeMutableBufferPointer { + let target = UnsafeMutableBufferPointer(rebasing: self) + target.moveInitializeAll(fromContentsOf: source) + } + + @inlinable @inline(__always) + public func moveInitializeAll( + fromContentsOf source: Slice> + ) where Base == UnsafeMutableBufferPointer { + let target = UnsafeMutableBufferPointer(rebasing: self) + target.moveInitializeAll(fromContentsOf: source) + } +} +% end +${visibility_boilerplate("end")} diff --git a/Sources/_CollectionsUtilities/_SortedCollection.swift b/Sources/_CollectionsUtilities/_SortedCollection.swift new file mode 100644 index 000000000..0aded257f --- /dev/null +++ b/Sources/_CollectionsUtilities/_SortedCollection.swift @@ -0,0 +1,27 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +/// A Collection type that is guaranteed to contain elements in monotonically +/// increasing order. (Duplicates are still allowed unless the collection +/// also conforms to `_UniqueCollection`.) +/// +/// Types conforming to this protocol must also conform to `Collection`, +/// with an `Element` type that conforms to `Comparable`. +/// (However, this protocol does not specify these as explicit requirements, +/// to allow simple conformance tests such as `someValue is _SortedCollection` +/// to be possible.) +/// +/// For any two valid indices `i` and `j` for a conforming collection `c` +/// (both below the end index), it must hold true that if `i < j`, then +/// `c[i] <= c[j]`. +public protocol _SortedCollection {} + +extension Slice: _SortedCollection where Base: _SortedCollection {} diff --git a/Sources/_CollectionsUtilities/_UniqueCollection.swift b/Sources/_CollectionsUtilities/_UniqueCollection.swift new file mode 100644 index 000000000..237638c41 --- /dev/null +++ b/Sources/_CollectionsUtilities/_UniqueCollection.swift @@ -0,0 +1,37 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +/// A Collection type that is guaranteed not to contain any duplicate elements. +/// +/// Types conforming to this protocol must also conform to `Collection`, +/// with an `Element` type that conforms to `Equatable`. +/// (However, this protocol does not specify these as explicit requirements, +/// to allow simple conformance tests such as `someValue is _SortedCollection` +/// to be possible.) +/// +/// For any two valid indices `i` and `j` in a conforming collection `c` +/// (both below the end index), it must hold true that if `i != j` then +/// `c[i] != c[j]`. +/// +/// Types that conform to this protocol should also implement the following +/// underscored requirements in a way that they never return nil values: +/// +/// - `Sequence._customContainsEquatableElement` +/// - `Collection._customIndexOfEquatableElement` +/// - `Collection._customLastIndexOfEquatableElement` +/// +/// The idea with these is that presumably a collection that can guarantee +/// element uniqueness has a way to quickly find existing elements. +public protocol _UniqueCollection {} + +extension Set: _UniqueCollection {} +extension Dictionary.Keys: _UniqueCollection {} +extension Slice: _UniqueCollection where Base: _UniqueCollection {} diff --git a/Sources/_CollectionsUtilities/autogenerated/Debugging.swift b/Sources/_CollectionsUtilities/autogenerated/Debugging.swift new file mode 100644 index 000000000..84e495300 --- /dev/null +++ b/Sources/_CollectionsUtilities/autogenerated/Debugging.swift @@ -0,0 +1,54 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + + +// ############################################################################# +// # # +// # DO NOT EDIT THIS FILE; IT IS AUTOGENERATED. # +// # # +// ############################################################################# + + + +// In single module mode, we need these declarations to be internal, +// but in regular builds we want them to be public. Unfortunately +// the current best way to do this is to duplicate all definitions. +#if COLLECTIONS_SINGLE_MODULE +/// True if consistency checking is enabled in the implementation of the +/// Swift Collections package, false otherwise. +/// +/// Documented performance promises are null and void when this property +/// returns true -- for example, operations that are documented to take +/// O(1) time might take O(*n*) time, or worse. +@inlinable @inline(__always) +internal var _isCollectionsInternalCheckingEnabled: Bool { +#if COLLECTIONS_INTERNAL_CHECKS + return true +#else + return false +#endif +} +#else // !COLLECTIONS_SINGLE_MODULE +/// True if consistency checking is enabled in the implementation of the +/// Swift Collections package, false otherwise. +/// +/// Documented performance promises are null and void when this property +/// returns true -- for example, operations that are documented to take +/// O(1) time might take O(*n*) time, or worse. +@inlinable @inline(__always) +public var _isCollectionsInternalCheckingEnabled: Bool { +#if COLLECTIONS_INTERNAL_CHECKS + return true +#else + return false +#endif +} +#endif // COLLECTIONS_SINGLE_MODULE diff --git a/Sources/_CollectionsUtilities/autogenerated/Descriptions.swift b/Sources/_CollectionsUtilities/autogenerated/Descriptions.swift new file mode 100644 index 000000000..2c84f8eae --- /dev/null +++ b/Sources/_CollectionsUtilities/autogenerated/Descriptions.swift @@ -0,0 +1,136 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + + +// ############################################################################# +// # # +// # DO NOT EDIT THIS FILE; IT IS AUTOGENERATED. # +// # # +// ############################################################################# + + + +// In single module mode, we need these declarations to be internal, +// but in regular builds we want them to be public. Unfortunately +// the current best way to do this is to duplicate all definitions. +#if COLLECTIONS_SINGLE_MODULE + +@usableFromInline +internal func _addressString(for pointer: UnsafeRawPointer) -> String { + let address = UInt(bitPattern: pointer) + return "0x\(String(address, radix: 16))" +} + +@usableFromInline +internal func _addressString(for object: AnyObject) -> String { + _addressString(for: Unmanaged.passUnretained(object).toOpaque()) +} + +@usableFromInline +internal func _addressString(for object: Unmanaged) -> String { + _addressString(for: object.toOpaque()) +} + +@inlinable +internal func _arrayDescription( + for elements: C +) -> String { + var result = "[" + var first = true + for item in elements { + if first { + first = false + } else { + result += ", " + } + debugPrint(item, terminator: "", to: &result) + } + result += "]" + return result +} + +@inlinable +internal func _dictionaryDescription( + for elements: C +) -> String where C.Element == (key: Key, value: Value) { + guard !elements.isEmpty else { return "[:]" } + var result = "[" + var first = true + for (key, value) in elements { + if first { + first = false + } else { + result += ", " + } + debugPrint(key, terminator: "", to: &result) + result += ": " + debugPrint(value, terminator: "", to: &result) + } + result += "]" + return result +} +#else // !COLLECTIONS_SINGLE_MODULE + +//@usableFromInline +public func _addressString(for pointer: UnsafeRawPointer) -> String { + let address = UInt(bitPattern: pointer) + return "0x\(String(address, radix: 16))" +} + +//@usableFromInline +public func _addressString(for object: AnyObject) -> String { + _addressString(for: Unmanaged.passUnretained(object).toOpaque()) +} + +//@usableFromInline +public func _addressString(for object: Unmanaged) -> String { + _addressString(for: object.toOpaque()) +} + +@inlinable +public func _arrayDescription( + for elements: C +) -> String { + var result = "[" + var first = true + for item in elements { + if first { + first = false + } else { + result += ", " + } + debugPrint(item, terminator: "", to: &result) + } + result += "]" + return result +} + +@inlinable +public func _dictionaryDescription( + for elements: C +) -> String where C.Element == (key: Key, value: Value) { + guard !elements.isEmpty else { return "[:]" } + var result = "[" + var first = true + for (key, value) in elements { + if first { + first = false + } else { + result += ", " + } + debugPrint(key, terminator: "", to: &result) + result += ": " + debugPrint(value, terminator: "", to: &result) + } + result += "]" + return result +} +#endif // COLLECTIONS_SINGLE_MODULE diff --git a/Sources/_CollectionsUtilities/autogenerated/RandomAccessCollection+Offsets.swift b/Sources/_CollectionsUtilities/autogenerated/RandomAccessCollection+Offsets.swift new file mode 100644 index 000000000..ebcc44ca2 --- /dev/null +++ b/Sources/_CollectionsUtilities/autogenerated/RandomAccessCollection+Offsets.swift @@ -0,0 +1,58 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + + +// ############################################################################# +// # # +// # DO NOT EDIT THIS FILE; IT IS AUTOGENERATED. # +// # # +// ############################################################################# + + + +// In single module mode, we need these declarations to be internal, +// but in regular builds we want them to be public. Unfortunately +// the current best way to do this is to duplicate all definitions. +#if COLLECTIONS_SINGLE_MODULE +extension RandomAccessCollection { + @_alwaysEmitIntoClient @inline(__always) + internal func _index(at offset: Int) -> Index { + index(startIndex, offsetBy: offset) + } + + @_alwaysEmitIntoClient @inline(__always) + internal func _offset(of index: Index) -> Int { + distance(from: startIndex, to: index) + } + + @_alwaysEmitIntoClient @inline(__always) + internal subscript(_offset offset: Int) -> Element { + self[_index(at: offset)] + } +} +#else // !COLLECTIONS_SINGLE_MODULE +extension RandomAccessCollection { + @_alwaysEmitIntoClient @inline(__always) + public func _index(at offset: Int) -> Index { + index(startIndex, offsetBy: offset) + } + + @_alwaysEmitIntoClient @inline(__always) + public func _offset(of index: Index) -> Int { + distance(from: startIndex, to: index) + } + + @_alwaysEmitIntoClient @inline(__always) + public subscript(_offset offset: Int) -> Element { + self[_index(at: offset)] + } +} +#endif // COLLECTIONS_SINGLE_MODULE diff --git a/Sources/_CollectionsUtilities/autogenerated/Specialize.swift b/Sources/_CollectionsUtilities/autogenerated/Specialize.swift new file mode 100644 index 000000000..07087af24 --- /dev/null +++ b/Sources/_CollectionsUtilities/autogenerated/Specialize.swift @@ -0,0 +1,54 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + + +// ############################################################################# +// # # +// # DO NOT EDIT THIS FILE; IT IS AUTOGENERATED. # +// # # +// ############################################################################# + + + +// In single module mode, we need these declarations to be internal, +// but in regular builds we want them to be public. Unfortunately +// the current best way to do this is to duplicate all definitions. +#if COLLECTIONS_SINGLE_MODULE +/// Returns `x` as its concrete type `U`, or `nil` if `x` has a different +/// concrete type. +/// +/// This cast can be useful for dispatching to specializations of generic +/// functions. +@_transparent +@inlinable +internal func _specialize(_ x: T, for: U.Type) -> U? { + // Note: this was ported from recent versions of the Swift stdlib. + guard T.self == U.self else { + return nil + } + return _identityCast(x, to: U.self) +} +#else // !COLLECTIONS_SINGLE_MODULE +/// Returns `x` as its concrete type `U`, or `nil` if `x` has a different +/// concrete type. +/// +/// This cast can be useful for dispatching to specializations of generic +/// functions. +@_transparent +@inlinable +public func _specialize(_ x: T, for: U.Type) -> U? { + // Note: this was ported from recent versions of the Swift stdlib. + guard T.self == U.self else { + return nil + } + return _identityCast(x, to: U.self) +} +#endif // COLLECTIONS_SINGLE_MODULE diff --git a/Sources/_CollectionsUtilities/autogenerated/UnsafeBufferPointer+Extras.swift b/Sources/_CollectionsUtilities/autogenerated/UnsafeBufferPointer+Extras.swift new file mode 100644 index 000000000..749f17bc8 --- /dev/null +++ b/Sources/_CollectionsUtilities/autogenerated/UnsafeBufferPointer+Extras.swift @@ -0,0 +1,42 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + + +// ############################################################################# +// # # +// # DO NOT EDIT THIS FILE; IT IS AUTOGENERATED. # +// # # +// ############################################################################# + + + +// In single module mode, we need these declarations to be internal, +// but in regular builds we want them to be public. Unfortunately +// the current best way to do this is to duplicate all definitions. +#if COLLECTIONS_SINGLE_MODULE +extension UnsafeBufferPointer { + @inlinable + @inline(__always) + internal func _ptr(at index: Int) -> UnsafePointer { + assert(index >= 0 && index < count) + return baseAddress.unsafelyUnwrapped + index + } +} +#else // !COLLECTIONS_SINGLE_MODULE +extension UnsafeBufferPointer { + @inlinable + @inline(__always) + public func _ptr(at index: Int) -> UnsafePointer { + assert(index >= 0 && index < count) + return baseAddress.unsafelyUnwrapped + index + } +} +#endif // COLLECTIONS_SINGLE_MODULE diff --git a/Sources/_CollectionsUtilities/autogenerated/UnsafeMutableBufferPointer+Extras.swift b/Sources/_CollectionsUtilities/autogenerated/UnsafeMutableBufferPointer+Extras.swift new file mode 100644 index 000000000..2661d8ca1 --- /dev/null +++ b/Sources/_CollectionsUtilities/autogenerated/UnsafeMutableBufferPointer+Extras.swift @@ -0,0 +1,278 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + + +// ############################################################################# +// # # +// # DO NOT EDIT THIS FILE; IT IS AUTOGENERATED. # +// # # +// ############################################################################# + + + +// In single module mode, we need these declarations to be internal, +// but in regular builds we want them to be public. Unfortunately +// the current best way to do this is to duplicate all definitions. +#if COLLECTIONS_SINGLE_MODULE +extension UnsafeMutableBufferPointer { + @inlinable + public func initialize(fromContentsOf source: Self) -> Index { + guard source.count > 0 else { return 0 } + precondition( + source.count <= self.count, + "buffer cannot contain every element from source.") + baseAddress.unsafelyUnwrapped.initialize( + from: source.baseAddress.unsafelyUnwrapped, + count: source.count) + return source.count + } + + @inlinable + public func initialize(fromContentsOf source: Slice) -> Index { + let sourceCount = source.count + guard sourceCount > 0 else { return 0 } + precondition( + sourceCount <= self.count, + "buffer cannot contain every element from source.") + baseAddress.unsafelyUnwrapped.initialize( + from: source.base.baseAddress.unsafelyUnwrapped + source.startIndex, + count: sourceCount) + return sourceCount + } +} + +extension Slice { + @inlinable @inline(__always) + public func initialize( + fromContentsOf source: UnsafeMutableBufferPointer + ) -> Index + where Base == UnsafeMutableBufferPointer + { + let target = UnsafeMutableBufferPointer(rebasing: self) + let i = target.initialize(fromContentsOf: source) + return self.startIndex + i + } + + @inlinable @inline(__always) + public func initialize( + fromContentsOf source: Slice> + ) -> Index + where Base == UnsafeMutableBufferPointer + { + let target = UnsafeMutableBufferPointer(rebasing: self) + let i = target.initialize(fromContentsOf: source) + return self.startIndex + i + } +} + +extension UnsafeMutableBufferPointer { + @inlinable @inline(__always) + public func initializeAll( + fromContentsOf source: C + ) where C.Element == Element { + let i = self.initialize(fromContentsOf: source) + assert(i == self.endIndex) + } + + @inlinable @inline(__always) + public func initializeAll(fromContentsOf source: Self) { + let i = self.initialize(fromContentsOf: source) + assert(i == self.endIndex) + } + + @inlinable @inline(__always) + public func initializeAll(fromContentsOf source: Slice) { + let i = self.initialize(fromContentsOf: source) + assert(i == self.endIndex) + } + + @inlinable @inline(__always) + public func moveInitializeAll(fromContentsOf source: Self) { + let i = self.moveInitialize(fromContentsOf: source) + assert(i == self.endIndex) + } + + @inlinable @inline(__always) + public func moveInitializeAll(fromContentsOf source: Slice) { + let i = self.moveInitialize(fromContentsOf: source) + assert(i == self.endIndex) + } +} + +extension Slice { + @inlinable @inline(__always) + public func initializeAll( + fromContentsOf source: C + ) where Base == UnsafeMutableBufferPointer { + let i = self.initialize(fromContentsOf: source) + assert(i == self.endIndex) + } + + @inlinable @inline(__always) + public func initializeAll( + fromContentsOf source: UnsafeMutableBufferPointer + ) where Base == UnsafeMutableBufferPointer { + let target = UnsafeMutableBufferPointer(rebasing: self) + target.initializeAll(fromContentsOf: source) + } + + @inlinable @inline(__always) + public func initializeAll( + fromContentsOf source: Slice> + ) where Base == UnsafeMutableBufferPointer { + let target = UnsafeMutableBufferPointer(rebasing: self) + target.initializeAll(fromContentsOf: source) + } + + @inlinable @inline(__always) + public func moveInitializeAll( + fromContentsOf source: UnsafeMutableBufferPointer + ) where Base == UnsafeMutableBufferPointer { + let target = UnsafeMutableBufferPointer(rebasing: self) + target.moveInitializeAll(fromContentsOf: source) + } + + @inlinable @inline(__always) + public func moveInitializeAll( + fromContentsOf source: Slice> + ) where Base == UnsafeMutableBufferPointer { + let target = UnsafeMutableBufferPointer(rebasing: self) + target.moveInitializeAll(fromContentsOf: source) + } +} +#else // !COLLECTIONS_SINGLE_MODULE +extension UnsafeMutableBufferPointer { + @inlinable + public func initialize(fromContentsOf source: Self) -> Index { + guard source.count > 0 else { return 0 } + precondition( + source.count <= self.count, + "buffer cannot contain every element from source.") + baseAddress.unsafelyUnwrapped.initialize( + from: source.baseAddress.unsafelyUnwrapped, + count: source.count) + return source.count + } + + @inlinable + public func initialize(fromContentsOf source: Slice) -> Index { + let sourceCount = source.count + guard sourceCount > 0 else { return 0 } + precondition( + sourceCount <= self.count, + "buffer cannot contain every element from source.") + baseAddress.unsafelyUnwrapped.initialize( + from: source.base.baseAddress.unsafelyUnwrapped + source.startIndex, + count: sourceCount) + return sourceCount + } +} + +extension Slice { + @inlinable @inline(__always) + public func initialize( + fromContentsOf source: UnsafeMutableBufferPointer + ) -> Index + where Base == UnsafeMutableBufferPointer + { + let target = UnsafeMutableBufferPointer(rebasing: self) + let i = target.initialize(fromContentsOf: source) + return self.startIndex + i + } + + @inlinable @inline(__always) + public func initialize( + fromContentsOf source: Slice> + ) -> Index + where Base == UnsafeMutableBufferPointer + { + let target = UnsafeMutableBufferPointer(rebasing: self) + let i = target.initialize(fromContentsOf: source) + return self.startIndex + i + } +} + +extension UnsafeMutableBufferPointer { + @inlinable @inline(__always) + public func initializeAll( + fromContentsOf source: C + ) where C.Element == Element { + let i = self.initialize(fromContentsOf: source) + assert(i == self.endIndex) + } + + @inlinable @inline(__always) + public func initializeAll(fromContentsOf source: Self) { + let i = self.initialize(fromContentsOf: source) + assert(i == self.endIndex) + } + + @inlinable @inline(__always) + public func initializeAll(fromContentsOf source: Slice) { + let i = self.initialize(fromContentsOf: source) + assert(i == self.endIndex) + } + + @inlinable @inline(__always) + public func moveInitializeAll(fromContentsOf source: Self) { + let i = self.moveInitialize(fromContentsOf: source) + assert(i == self.endIndex) + } + + @inlinable @inline(__always) + public func moveInitializeAll(fromContentsOf source: Slice) { + let i = self.moveInitialize(fromContentsOf: source) + assert(i == self.endIndex) + } +} + +extension Slice { + @inlinable @inline(__always) + public func initializeAll( + fromContentsOf source: C + ) where Base == UnsafeMutableBufferPointer { + let i = self.initialize(fromContentsOf: source) + assert(i == self.endIndex) + } + + @inlinable @inline(__always) + public func initializeAll( + fromContentsOf source: UnsafeMutableBufferPointer + ) where Base == UnsafeMutableBufferPointer { + let target = UnsafeMutableBufferPointer(rebasing: self) + target.initializeAll(fromContentsOf: source) + } + + @inlinable @inline(__always) + public func initializeAll( + fromContentsOf source: Slice> + ) where Base == UnsafeMutableBufferPointer { + let target = UnsafeMutableBufferPointer(rebasing: self) + target.initializeAll(fromContentsOf: source) + } + + @inlinable @inline(__always) + public func moveInitializeAll( + fromContentsOf source: UnsafeMutableBufferPointer + ) where Base == UnsafeMutableBufferPointer { + let target = UnsafeMutableBufferPointer(rebasing: self) + target.moveInitializeAll(fromContentsOf: source) + } + + @inlinable @inline(__always) + public func moveInitializeAll( + fromContentsOf source: Slice> + ) where Base == UnsafeMutableBufferPointer { + let target = UnsafeMutableBufferPointer(rebasing: self) + target.moveInitializeAll(fromContentsOf: source) + } +} +#endif // COLLECTIONS_SINGLE_MODULE diff --git a/Tests/BitCollectionsTests/BitArrayTests.swift b/Tests/BitCollectionsTests/BitArrayTests.swift new file mode 100644 index 000000000..a1dd95468 --- /dev/null +++ b/Tests/BitCollectionsTests/BitArrayTests.swift @@ -0,0 +1,1021 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +import XCTest +#if COLLECTIONS_SINGLE_MODULE +@_spi(Testing) import Collections +#else +import _CollectionsTestSupport +@_spi(Testing) import BitCollections +#endif + +extension BitArray { + static func _fromSequence( + _ value: S + ) -> BitArray where S.Element == Bool { + BitArray(value) + } +} + +func randomBoolArray(count: Int) -> [Bool] { + var rng = SystemRandomNumberGenerator() + return randomBoolArray(count: count, using: &rng) +} + +func randomBoolArray( + count: Int, using rng: inout R +) -> [Bool] { + let wordCount = (count + UInt.bitWidth - 1) / UInt.bitWidth + var array: [Bool] = [] + array.reserveCapacity(wordCount * UInt.bitWidth) + for _ in 0 ..< wordCount { + var word: UInt = rng.next() + for _ in 0 ..< UInt.bitWidth { + array.append(word & 1 == 1) + word &>>= 1 + } + } + array.removeLast(array.count - count) + return array +} + +final class BitArrayTests: CollectionTestCase { + func test_empty_initializer() { + let array = BitArray() + expectEqual(array.count, 0) + expectEqual(array.isEmpty, true) + expectEqual(array.startIndex, 0) + expectEqual(array.endIndex, 0) + expectEqualElements(array, []) + } + + func test_RandomAccessCollection() { + var rng = RepeatableRandomNumberGenerator(seed: 0) + withEvery("count", in: [0, 1, 2, 13, 64, 65, 127, 128, 129]) { count in + let reference = randomBoolArray(count: count, using: &rng) + let value = BitArray(reference) + print(count) + checkBidirectionalCollection( + value, expectedContents: reference, maxSamples: 100) + } + } + + func test_init_Sequence_BitArray() { + var rng = RepeatableRandomNumberGenerator(seed: 0) + withEvery("count", in: [0, 1, 2, 13, 64, 65, 127, 128, 129]) { count in + let reference = BitArray(randomBoolArray(count: count, using: &rng)) + let value = BitArray._fromSequence(reference) + expectEqualElements(value, reference) + + let value2 = BitArray(reference) + expectEqualElements(value2, reference) + } + } + + func test_init_Sequence_BitArray_SubSequence() { + var rng = RepeatableRandomNumberGenerator(seed: 0) + withEvery("count", in: [0, 1, 2, 13, 64, 65, 127, 128, 129]) { count in + let array = randomBoolArray(count: count, using: &rng) + let ref = BitArray(array) + withSomeRanges("range", in: 0 ..< count, maxSamples: 100) { range in + let value = BitArray._fromSequence(ref[range]) + expectEqualElements(value, array[range]) + + let value2 = BitArray(ref[range]) + expectEqualElements(value2, array[range]) + } + } + } + + func test_MutableCollection() { + var rng = RepeatableRandomNumberGenerator(seed: 0) + withEvery("count", in: [0, 1, 2, 13, 64, 65, 127, 128, 129]) { count in + var ref = randomBoolArray(count: count, using: &rng) + let replacements = randomBoolArray(count: count, using: &rng) + var value = BitArray(ref) + withSome("i", in: 0 ..< count, maxSamples: 100) { i in + ref[i] = replacements[i] + value[i] = replacements[i] + expectEqualElements(value, ref) + } + } + } + + func test_fill() { + var rng = RepeatableRandomNumberGenerator(seed: 0) + withEvery("count", in: [0, 1, 2, 13, 64, 65, 128, 129]) { count in + withSomeRanges("range", in: 0 ..< count, maxSamples: 200) { range in + withEvery("v", in: [false, true]) { v in + var ref = randomBoolArray(count: count, using: &rng) + var value = BitArray(ref) + ref.replaceSubrange(range, with: repeatElement(v, count: range.count)) + value.fill(in: range, with: v) + expectEqualElements(value, ref) + } + } + + var value = BitArray(randomBoolArray(count: count, using: &rng)) + value.fill(with: false) + expectEqualElements(value, repeatElement(false, count: count)) + value.fill(with: true) + expectEqualElements(value, repeatElement(true, count: count)) + } + } + + func test_init_bitPattern() { + expectEqualElements( + BitArray(bitPattern: 42 as UInt8), + [false, true, false, true, false, true, false, false]) + + withSome( + "value", in: -1_000_000 ..< 1_000_000, maxSamples: 1000 + ) { value in + let actual = BitArray(bitPattern: value) + var expected: [Bool] = [] + var v = value + for _ in 0 ..< Int.bitWidth { + expected.append(v & 1 == 1) + v &>>= 1 + } + expectEqualElements(actual, expected) + } + } + + func test_conversion_to_BinaryInteger_truncating() { + let cases: [(bits: BitArray, signed: Int8, unsigned: UInt8)] = [ + ("", 0, 0), + ("0", 0, 0), + ("1", -1, 1), + ("00", 0, 0), + ("01", 1, 1), + ("10", -2, 2), + ("11", -1, 3), + ("001", 1, 1), + ("010", 2, 2), + ("011", 3, 3), + ("100", -4, 4), + ("101", -3, 5), + ("110", -2, 6), + ("111", -1, 7), + // 8 bits + ("00000000", 0, 0), + ("10000000", -128, 128), + ("10000001", -127, 129), + ("11111111", -1, 255), + // 9 bits + ("000000000", 0, 0), + ("000000100", 4, 4), + ("010000000", -128, 128), + ("010000001", -127, 129), + ("011111111", -1, 255), + ("100000000", 0, 0), + ("100000001", 1, 1), + ("101111111", 127, 127), + ("110000000", -128, 128), + ("110000001", -127, 129), + ("111111110", -2, 254), + ("111111111", -1, 255), + // 32 bits + ("00000000000000000000000000000000", 0, 0), + ("00000000000000000000000001111111", 127, 127), + ("00000000000000000000000010000000", -128, 128), + ("00000000000000000000000011111111", -1, 255), + ("00000000000000000000000100000000", 0, 0), + ("11111111111111111111111110000000", -128, 128), + ("11111111111111111111111111111110", -2, 254), + ("11111111111111111111111111111111", -1, 255), + ] + + withEvery("pair", in: cases) { (bits, signed, unsigned) in + let actual1 = Int8(truncatingIfNeeded: bits) + expectEqual(actual1, signed) + + let actual2 = UInt8(truncatingIfNeeded: bits) + expectEqual(actual2, unsigned) + } + } + + func test_conversion_to_BinaryInteger_exact() { + let cases: [(bits: BitArray, signed: Int8?, unsigned: UInt8?)] = [ + ("", 0, 0), + ("0", 0, 0), + ("1", -1, 1), + ("00", 0, 0), + ("01", 1, 1), + ("10", -2, 2), + ("11", -1, 3), + ("001", 1, 1), + ("010", 2, 2), + ("011", 3, 3), + ("100", -4, 4), + ("101", -3, 5), + ("110", -2, 6), + ("111", -1, 7), + // 8 bits + ("00000000", 0, 0), + ("10000000", -128, 128), + ("10000001", -127, 129), + ("11111111", -1, 255), + // 9 bits + ("000000000", 0, 0), + ("000000100", 4, 4), + ("010000000", nil, 128), + ("010000001", nil, 129), + ("011111111", nil, 255), + ("100000000", nil, nil), + ("100000001", nil, nil), + ("101111111", nil, nil), + ("110000000", -128, nil), + ("110000001", -127, nil), + ("111111110", -2, nil), + ("111111111", -1, nil), + // 32 bits + ("00000000000000000000000000000000", 0, 0), + ("00000000000000000000000001111111", 127, 127), + ("00000000000000000000000010000000", nil, 128), + ("00000000000000000000000011111111", nil, 255), + ("00000000000000000000000100000000", nil, nil), + ("11111111111111111111111110000000", -128, nil), + ("11111111111111111111111111111110", -2, nil), + ("11111111111111111111111111111111", -1, nil), + ] + + withEvery("pair", in: cases) { (bits, signed, unsigned) in + let actual1 = Int8(exactly: bits) + expectEqual(actual1, signed) + + let actual2 = UInt8(exactly: bits) + expectEqual(actual2, unsigned) + } + } + + func test_init_BitSet() { + expectEqualElements(BitArray(BitSet([])), []) + expectEqualElements(BitArray(BitSet([0])), [true]) + expectEqualElements(BitArray(BitSet([1])), [false, true]) + expectEqualElements(BitArray(BitSet([0, 1])), [true, true]) + expectEqualElements(BitArray(BitSet([1, 2])), [false, true, true]) + expectEqualElements(BitArray(BitSet([0, 2])), [true, false, true]) + + expectEqualElements( + BitArray(BitSet([1, 3, 6, 7])), + [false, true, false, true, false, false, true, true]) + + withEvery("count", in: [5, 63, 64, 65, 100, 1000]) { count in + var reference = randomBoolArray(count: count) + reference[reference.count - 1] = true // Fix the size of the bitset + let bitset = BitSet(reference.indices.filter { reference[$0] }) + let actual = BitArray(bitset) + expectEqualElements(actual, reference) + } + } + + func test_init_repeating() { + withEvery("count", in: [0, 1, 2, 13, 63, 64, 65, 127, 128, 129, 1000]) { count in + withEvery("v", in: [false, true]) { v in + let value = BitArray(repeating: v, count: count) + let reference = repeatElement(v, count: count) + expectEqualElements(value, reference) + } + } + } + + func test_ExpressibleByArrayLiteral() { + let a: BitArray = [] + expectEqualElements(a, []) + + let b: BitArray = [true] + expectEqualElements(b, [true]) + + let c: BitArray = [true, false, false] + expectEqualElements(c, [true, false, false]) + + let d: BitArray = [true, false, false, false, true] + expectEqualElements(d, [true, false, false, false, true]) + + let e: BitArray = [ + true, false, false, false, true, false, false, + true, true, false, false, true, false, false, + false, false, false, false, true, false, false, + true, false, true, false, true, false, false, + true, false, false, true, true, false, false, + true, false, false, false, false, false, false, + false, false, true, false, true, true, false, + true, false, false, false, true, false, true, + true, true, false, false, true, true, true, + ] + expectEqualElements(e, [ + true, false, false, false, true, false, false, + true, true, false, false, true, false, false, + false, false, false, false, true, false, false, + true, false, true, false, true, false, false, + true, false, false, true, true, false, false, + true, false, false, false, false, false, false, + false, false, true, false, true, true, false, + true, false, false, false, true, false, true, + true, true, false, false, true, true, true, + ]) + } + + func test_ExpressibleByStringLiteral() { + let a: BitArray = "" + expectEqualElements(a, []) + + let b: BitArray = "1" + expectEqualElements(b, [true]) + + let c: BitArray = "001" + expectEqualElements(c, [true, false, false]) + + let d: BitArray = "10001" + expectEqualElements(d, [true, false, false, false, true]) + + let e: BitArray = """ + 111001110100010110100000000100110010010101001000000100110010001 + """ + expectEqualElements(e, [ + true, false, false, false, true, false, false, + true, true, false, false, true, false, false, + false, false, false, false, true, false, false, + true, false, true, false, true, false, false, + true, false, false, true, true, false, false, + true, false, false, false, false, false, false, + false, false, true, false, true, true, false, + true, false, false, false, true, false, true, + true, true, false, false, true, true, true, + ]) + } + + func test_literals() { + let cases: [(a: BitArray, b: BitArray)] = [ + ("", []), + ("0", [false]), + ("1", [true]), + ("1010", [false, true, false, true]), + ("0101", [true, false, true, false]), + ("111000", [false, false, false, true, true, true]), + ("000111", [true, true, true, false, false, false]), + ] + withEvery("i", in: cases.indices) { i in + let (a, b) = cases[i] + expectEqual(a, b) + } + } + + func test_LosslessStringConvertible() { + let cases: [(a: String, b: BitArray?)] = [ + ("", []), + ("<>", []), + ("0", [false]), + ("<0>", [false]), + ("1", [true]), + ("<1>", [true]), + ("1010", [false, true, false, true]), + ("<1010>", [false, true, false, true]), + ("0101", [true, false, true, false]), + ("<0101>", [true, false, true, false]), + ("111000", [false, false, false, true, true, true]), + ("<111000>", [false, false, false, true, true, true]), + ("000111", [true, true, true, false, false, false]), + ("<000111>", [true, true, true, false, false, false]), + ("_", nil), + ("<", nil), + ("<<", nil), + (">", nil), + (">>", nil), + ("<01", nil), + ("101>", nil), + ("<<100>>", nil), + ("01<10", nil), + ("10>10", nil), + ("1<010>", nil), + ("<010>1", nil), + ("00010101X", nil), + ("①⓪⓪①", nil), + ("2341", nil), + ("00 10 01", nil), + (" 01", nil), + ("01 ", nil), + ] + withEvery("i", in: cases.indices) { i in + let (a, b) = cases[i] + let bits = BitArray(a) + expectEqual(bits, b) + } + } + + func test_Hashable() { + // This is a silly test, but it does exercise hashing a bit. + let classes: [[BitArray]] = [ + [[]], + [[false], [false]], + [[false, false], [false, false]], + [[false, false, true], [false, false, true]], + [[false, false, true, false, false], + [false, false, true, false, false]], + ] + checkHashable(equivalenceClasses: classes) + } + + func test_Encodable() throws { + let b1: BitArray = [] + let v1: MinimalEncoder.Value = .array([.uint64(0)]) + expectEqual(try MinimalEncoder.encode(b1), v1) + + let b2: BitArray = [true, true, false, true] + let v2: MinimalEncoder.Value = .array([.uint64(4), .uint64(11)]) + expectEqual(try MinimalEncoder.encode(b2), v2) + + let b3 = BitArray(repeating: true, count: 145) + let v3: MinimalEncoder.Value = .array([ + .uint64(145), + .uint64(UInt64.max), + .uint64(UInt64.max), + .uint64((1 << 17) - 1)]) + expectEqual(try MinimalEncoder.encode(b3), v3) + + let b4 = BitArray(Array(repeating: false, count: 343) + [true]) + let v4: MinimalEncoder.Value = .array([ + .uint64(344), + .uint64(0), + .uint64(0), + .uint64(0), + .uint64(0), + .uint64(0), + .uint64(1 << 23), + ]) + expectEqual(try MinimalEncoder.encode(b4), v4) + } + + func test_Decodable() throws { + let b1: BitArray = [] + let v1: MinimalEncoder.Value = .array([.uint64(0)]) + expectEqual(try MinimalDecoder.decode(v1, as: BitArray.self), b1) + + let b2: BitArray = [true, true, false, true] + let v2: MinimalEncoder.Value = .array([.uint64(4), .uint64(11)]) + expectEqual(try MinimalDecoder.decode(v2, as: BitArray.self), b2) + + let b3 = BitArray(repeating: true, count: 145) + let v3: MinimalEncoder.Value = .array([ + .uint64(145), + .uint64(UInt64.max), + .uint64(UInt64.max), + .uint64((1 << 17) - 1)]) + expectEqual(try MinimalDecoder.decode(v3, as: BitArray.self), b3) + + let b4 = BitArray(Array(repeating: false, count: 343) + [true]) + let v4: MinimalEncoder.Value = .array([ + .uint64(344), + .uint64(0), + .uint64(0), + .uint64(0), + .uint64(0), + .uint64(0), + .uint64(1 << 23), + ]) + expectEqual(try MinimalDecoder.decode(v4, as: BitArray.self), b4) + + let v5: MinimalEncoder.Value = .uint64(42) + expectThrows(try MinimalDecoder.decode(v5, as: BitArray.self)) + + let v6: MinimalEncoder.Value = .array([]) + expectThrows(try MinimalDecoder.decode(v6, as: BitArray.self)) + + let v7: MinimalEncoder.Value = .array([.uint64(1)]) + expectThrows(try MinimalDecoder.decode(v7, as: BitArray.self)) + + let v8: MinimalEncoder.Value = .array([ + .uint64(1), + .uint64(0), + .uint64(0), + .uint64(0) + ]) + expectThrows(try MinimalDecoder.decode(v8, as: BitArray.self)) + + let v9: MinimalEncoder.Value = .array([.uint64(100), .uint64(0)]) + expectThrows(try MinimalDecoder.decode(v9, as: BitArray.self)) + + let v10: MinimalEncoder.Value = .array([.uint64(16), .uint64(UInt64.max)]) + expectThrows(try MinimalDecoder.decode(v10, as: BitArray.self)) + } + + func test_replaceSubrange_Array() { + withSome("count", in: 0 ..< 512, maxSamples: 10) { count in + print(count) + withSomeRanges("range", in: 0 ..< count, maxSamples: 50) { range in + withEvery( + "length", in: [0, 1, range.count, 2 * range.count] + ) { length in + var reference = randomBoolArray(count: count) + let replacement = randomBoolArray(count: length) + var actual = BitArray(reference) + + reference.replaceSubrange(range, with: replacement) + actual.replaceSubrange(range, with: replacement) + expectEqualElements(actual, reference) + } + } + } + } + + func test_replaceSubrange_BitArray() { + withSome("count", in: 0 ..< 512, maxSamples: 10) { count in + print(count) + withSomeRanges("range", in: 0 ..< count, maxSamples: 50) { range in + withEvery( + "length", in: [0, 1, range.count, 2 * range.count] + ) { length in + var reference = randomBoolArray(count: count) + let value = BitArray(reference) + + let refReplacement = randomBoolArray(count: length) + let replacement = BitArray(refReplacement) + + reference.replaceSubrange(range, with: refReplacement) + + var actual = value + actual.replaceSubrange(range, with: replacement) + expectEqualElements(actual, reference) + + // Also check through the generic API + func forceCollection(_ v: C) where C.Element == Bool { + var actual = value + actual.replaceSubrange(range, with: v) + expectEqualElements(actual, reference) + } + forceCollection(replacement) + } + } + } + } + + func test_replaceSubrange_BitArray_SubSequence() { + withSome("count", in: 0 ..< 512, maxSamples: 10) { count in + print(count) + withSomeRanges("range", in: 0 ..< count, maxSamples: 25) { range in + withSomeRanges( + "replacementRange", + in: 0 ..< count, + maxSamples: 10 + ) { replacementRange in + var reference = randomBoolArray(count: count) + let value = BitArray(reference) + + let refReplacement = randomBoolArray(count: count) + let replacement = BitArray(refReplacement) + + reference.replaceSubrange( + range, with: refReplacement[replacementRange]) + + var actual = value + actual.replaceSubrange(range, with: replacement[replacementRange]) + expectEqualElements(actual, reference) + + // Also check through the generic API + func forceCollection(_ v: C) where C.Element == Bool { + var actual = value + actual.replaceSubrange(range, with: v) + expectEqualElements(actual, reference) + } + forceCollection(replacement[replacementRange]) + } + } + } + } + + func test_append() { + withEvery("count", in: 0 ..< 129) { count in + withEvery("v", in: [false, true]) { v in + var reference = randomBoolArray(count: count) + var actual = BitArray(reference) + reference.append(v) + actual.append(v) + expectEqualElements(actual, reference) + } + } + } + + func test_append_Sequence() { + withSome("count", in: 0 ..< 512, maxSamples: 10) { count in + print(count) + withSome("length", in: 0 ..< 256, maxSamples: 50) { length in + let reference = randomBoolArray(count: count) + let addition = randomBoolArray(count: length) + + let value = BitArray(reference) + + func check(_ addition: S) where S.Element == Bool { + var ref = reference + var actual = value + + ref.append(contentsOf: addition) + actual.append(contentsOf: addition) + expectEqualElements(actual, ref) + } + check(addition) + check(BitArray(addition)) + check(BitArray(addition)[...]) + } + } + } + + func test_append_BitArray() { + withSome("count", in: 0 ..< 512, maxSamples: 10) { count in + print(count) + withSome("length", in: 0 ..< 256, maxSamples: 50) { length in + var reference = randomBoolArray(count: count) + let addition = randomBoolArray(count: length) + + var actual = BitArray(reference) + + reference.append(contentsOf: addition) + actual.append(contentsOf: BitArray(addition)) + expectEqualElements(actual, reference) + } + } + } + + func test_append_BitArray_SubSequence() { + withSome("count", in: 0 ..< 512, maxSamples: 10) { count in + print(count) + withSomeRanges("range", in: 0 ..< 512, maxSamples: 50) { range in + var reference = randomBoolArray(count: count) + let addition = randomBoolArray(count: 512) + + var actual = BitArray(reference) + + reference.append(contentsOf: addition[range]) + actual.append(contentsOf: BitArray(addition)[range]) + expectEqualElements(actual, reference) + } + } + } + + func test_insert() { + withEvery("count", in: 0 ..< 129) { count in + withSome("i", in: 0 ..< count + 1, maxSamples: 20) { i in + withEvery("v", in: [false, true]) { v in + var reference = randomBoolArray(count: count) + var actual = BitArray(reference) + reference.insert(v, at: i) + actual.insert(v, at: i) + expectEqualElements(actual, reference) + } + } + } + } + + func test_insert_contentsOf_Sequence() { + withSome("count", in: 0 ..< 512, maxSamples: 10) { count in + print(count) + withSome("length", in: 0 ..< 256, maxSamples: 5) { length in + withSome("i", in: 0 ..< count + 1, maxSamples: 10) { i in + let reference = randomBoolArray(count: count) + let addition = randomBoolArray(count: length) + + let value = BitArray(reference) + + func check(_ addition: C) where C.Element == Bool { + var ref = reference + var actual = value + + ref.insert(contentsOf: addition, at: i) + actual.insert(contentsOf: addition, at: i) + expectEqualElements(actual, ref) + } + check(addition) + check(BitArray(addition)) + check(BitArray(addition)[...]) + } + } + } + } + + func test_insert_contentsOf_BitArray() { + withSome("count", in: 0 ..< 512, maxSamples: 10) { count in + print(count) + withSome("length", in: 0 ..< 256, maxSamples: 5) { length in + withSome("i", in: 0 ..< count + 1, maxSamples: 10) { i in + let reference = randomBoolArray(count: count) + let addition = randomBoolArray(count: length) + + let value = BitArray(reference) + + var ref = reference + var actual = value + + ref.insert(contentsOf: addition, at: i) + actual.insert(contentsOf: BitArray(addition), at: i) + expectEqualElements(actual, ref) + } + } + } + } + + func test_remove() { + withSome("count", in: 0 ..< 512, maxSamples: 50) { count in + withSome("i", in: 0 ..< count, maxSamples: 30) { i in + var reference = randomBoolArray(count: count) + var actual = BitArray(reference) + + let v1 = reference.remove(at: i) + let v2 = actual.remove(at: i) + expectEqual(v2, v1) + expectEqualElements(actual, reference) + } + } + } + + func test_removeSubrange() { + withSome("count", in: 0 ..< 512, maxSamples: 50) { count in + withSomeRanges("range", in: 0 ..< count, maxSamples: 50) { range in + var reference = randomBoolArray(count: count) + var actual = BitArray(reference) + reference.removeSubrange(range) + actual.removeSubrange(range) + expectEqualElements(actual, reference) + } + } + } + + func test_removeLast() { + withEvery("count", in: 1 ..< 512) { count in + var reference = randomBoolArray(count: count) + var actual = BitArray(reference) + let v1 = reference.removeLast() + let v2 = actual.removeLast() + expectEqual(v1, v2) + expectEqualElements(actual, reference) + } + } + + func test_removeFirst() { + withEvery("count", in: 1 ..< 512) { count in + var reference = randomBoolArray(count: count) + var actual = BitArray(reference) + let v1 = reference.removeFirst() + let v2 = actual.removeFirst() + expectEqual(v1, v2) + expectEqualElements(actual, reference) + } + } + + func test_removeFirst_n() { + withSome("count", in: 0 ..< 512, maxSamples: 50) { count in + withSome("n", in: 0 ... count, maxSamples: 30) { n in + var reference = randomBoolArray(count: count) + var actual = BitArray(reference) + + reference.removeFirst(n) + actual.removeFirst(n) + expectEqualElements(actual, reference) + } + } + } + + func test_removeLast_n() { + withSome("count", in: 0 ..< 512, maxSamples: 50) { count in + withSome("n", in: 0 ... count, maxSamples: 30) { n in + var reference = randomBoolArray(count: count) + var actual = BitArray(reference) + + reference.removeLast(n) + actual.removeLast(n) + expectEqualElements(actual, reference) + } + } + } + + func test_removeAll() { + withSome("count", in: 0 ..< 512, maxSamples: 50) { count in + withEvery("keep", in: [false, true]) { keep in + let reference = randomBoolArray(count: count) + var actual = BitArray(reference) + actual.removeAll(keepingCapacity: keep) + expectEqualElements(actual, []) + if keep { + expectGreaterThanOrEqual(actual._capacity, count) + } else { + expectEqual(actual._capacity, 0) + } + } + } + } + + func test_reserveCapacity() { + var bits = BitArray() + expectEqual(bits._capacity, 0) + bits.reserveCapacity(1) + expectGreaterThanOrEqual(bits._capacity, 1) + bits.reserveCapacity(0) + expectGreaterThanOrEqual(bits._capacity, 1) + bits.reserveCapacity(100) + expectGreaterThanOrEqual(bits._capacity, 100) + bits.reserveCapacity(0) + expectGreaterThanOrEqual(bits._capacity, 100) + bits.append(contentsOf: repeatElement(true, count: 1000)) + expectGreaterThanOrEqual(bits._capacity, 1000) + bits.reserveCapacity(2000) + expectGreaterThanOrEqual(bits._capacity, 2000) + } + + func test_init_minimumCapacity() { + let b1 = BitArray(minimumCapacity: 0) + expectEqual(b1._capacity, 0) + + let cases = [0, 1, 100, 1000, 2000] + withEvery("capacity", in: cases) { capacity in + let bits = BitArray(minimumCapacity: capacity) + expectTrue(bits.isEmpty) + expectGreaterThanOrEqual(bits._capacity, capacity) + } + } + + #if false // FIXME: Bitwise operations disabled for now + func test_bitwiseOr() { + withSome("count", in: 0 ..< 512, maxSamples: 100) { count in + withEvery("i", in: 0 ..< 10) { i in + let a = randomBoolArray(count: count) + let b = randomBoolArray(count: count) + + let c = BitArray(a) + let d = BitArray(b) + + let expected = zip(a, b).map { $0 || $1 } + let actual = c | d + + expectEqualElements(actual, expected) + } + } + } + + func test_bitwiseAnd() { + withSome("count", in: 0 ..< 512, maxSamples: 100) { count in + withEvery("i", in: 0 ..< 10) { i in + let a = randomBoolArray(count: count) + let b = randomBoolArray(count: count) + + let c = BitArray(a) + let d = BitArray(b) + + let expected = zip(a, b).map { $0 && $1 } + let actual = c & d + + expectEqualElements(actual, expected) + } + } + } + + func test_bitwiseXor() { + withSome("count", in: 0 ..< 512, maxSamples: 100) { count in + withEvery("i", in: 0 ..< 10) { i in + let a = randomBoolArray(count: count) + let b = randomBoolArray(count: count) + + let c = BitArray(a) + let d = BitArray(b) + + let expected = zip(a, b).map { $0 != $1 } + let actual = c ^ d + + expectEqualElements(actual, expected) + } + } + } + + func test_bitwiseComplement() { + withSome("count", in: 0 ..< 512, maxSamples: 100) { count in + withEvery("i", in: 0 ..< 10) { i in + let a = randomBoolArray(count: count) + + let b = BitArray(a) + + let expected = a.map { !$0 } + let actual = ~b + + expectEqualElements(actual, expected) + } + } + } + #endif + + func test_toggleAll() { + withSome("count", in: 0 ..< 512, maxSamples: 100) { count in + withEvery("i", in: 0 ..< 10) { i in + let a = randomBoolArray(count: count) + + var b = BitArray(a) + b.toggleAll() + + let expected = a.map { !$0 } + + expectEqualElements(b, expected) + } + } + } + + func test_toggleAll_range() { + withEvery("count", in: [0, 10, 64, 90, 127, 128, 129]) { count in + let a = randomBoolArray(count: count) + + withSomeRanges("range", in: 0 ..< count, maxSamples: 100) { range in + withEvery("shared", in: [false, true]) { shared in + var expected = a + for i in range { expected[i].toggle() } + + var b = BitArray(a) + withHiddenCopies(if: shared, of: &b) { b in + b.toggleAll(in: range) + expectEqualElements(b, expected) + } + } + } + } + } + + func test_truncateOrExtend() { + withSome("oldCount", in: 0 ..< 512, maxSamples: 50) { oldCount in + withSome("newCount", in: 0 ... 1024, maxSamples: 30) { newCount in + withEvery("padding", in: [false, true]) { padding in + let array = randomBoolArray(count: oldCount) + + var bits = BitArray(array) + bits.truncateOrExtend(toCount: newCount, with: padding) + + let delta = newCount - oldCount + if delta >= 0 { + let expected = array + repeatElement(padding, count: delta) + expectEqualElements(bits, expected) + } else { + expectEqualElements(bits, array[0 ..< newCount]) + } + } + } + } + } + + func test_random() { + var rng = AllOnesRandomNumberGenerator() + for c in [0, 10, 64, 65, 77, 1200] { + let array = BitArray.randomBits(count: c, using: &rng) + expectEqual(array.count, c) + expectEqualElements(array, repeatElement(true, count: c)) + } + + let a = Set((0..<10).map { _ in BitArray.randomBits(count: 1000) }) + expectEqual(a.count, 10) + } + + func test_description() { + let a: BitArray = [] + expectEqual("\(a)", "<>") + + let b: BitArray = [true, false, true, true, true] + expectEqual("\(b)", "<11101>") + + let c: BitArray = [false, false, false, false, true, true, true, false] + expectEqual("\(c)", "<01110000>") + } + + func test_debugDescription() { + let a: BitArray = [] + expectEqual("\(String(reflecting: a))", "<>") + + let b: BitArray = [true, false, true, true, true] + expectEqual("\(String(reflecting: b))", "<11101>") + + let c: BitArray = [false, false, false, false, true, true, true, false] + expectEqual("\(String(reflecting: c))", "<01110000>") + } + + func test_mirror() { + func check(_ v: T) -> String { + var str = "" + dump(v, to: &str) + return str + } + + expectEqual(check(BitArray()), """ + - 0 elements + + """) + + expectEqual(check([true, false, false] as BitArray), """ + ▿ 3 elements + - true + - false + - false + + """) + } + +} diff --git a/Tests/BitCollectionsTests/BitSet.Counted Tests.swift b/Tests/BitCollectionsTests/BitSet.Counted Tests.swift new file mode 100644 index 000000000..e28676816 --- /dev/null +++ b/Tests/BitCollectionsTests/BitSet.Counted Tests.swift @@ -0,0 +1,253 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +import XCTest +#if COLLECTIONS_SINGLE_MODULE +import Collections +#else +import _CollectionsTestSupport +import BitCollections +import OrderedCollections +#endif + +extension BitSet.Counted: SetAPIExtras { + public mutating func update(_ member: Int, at index: Index) -> Int { + fatalError("Not this one though") + } +} + +extension BitSet.Counted: SortedCollectionAPIChecker {} + +final class BitSetCountedTests: CollectionTestCase { + func test_union() { + let a: BitSet.Counted = [1, 2, 3, 4] + + let b = 3 ..< 7 + let exp = 1 ..< 7 + expectEqualElements(a.union(BitSet.Counted(b)), exp) + expectEqualElements(a.union(BitSet(b)), exp) + expectEqualElements(a.union(b), exp) + expectEqualElements(a.union(Set(b)), exp) + } + + func test_intersection() { + let a: BitSet.Counted = [1, 2, 3, 4] + + let b = 3 ..< 7 + let exp = 3 ..< 5 + expectEqualElements(a.intersection(BitSet.Counted(b)), exp) + expectEqualElements(a.intersection(BitSet(b)), exp) + expectEqualElements(a.intersection(b), exp) + expectEqualElements(a.intersection(Set(b)), exp) + } + + func test_symmetricDifference() { + let a: BitSet.Counted = [1, 2, 3, 4] + + let b = 3 ..< 7 + let exp = [1, 2, 5, 6] + expectEqualElements(a.symmetricDifference(BitSet.Counted(b)), exp) + expectEqualElements(a.symmetricDifference(BitSet(b)), exp) + expectEqualElements(a.symmetricDifference(b), exp) + expectEqualElements(a.symmetricDifference(Set(b)), exp) + } + + func test_subtracting() { + let a: BitSet.Counted = [1, 2, 3, 4] + + let b = 3 ..< 7 + let exp = [1, 2] + expectEqualElements(a.subtracting(BitSet.Counted(b)), exp) + expectEqualElements(a.subtracting(BitSet(b)), exp) + expectEqualElements(a.subtracting(b), exp) + expectEqualElements(a.subtracting(Set(b)), exp) + } + + func test_formUnion() { + func check( + _ expected: S, + _ body: (inout BitSet.Counted) -> Void, + file: StaticString = #file, + line: UInt = #line + ) where S.Element == Int { + var a: BitSet.Counted = [1, 2, 3, 4] + + body(&a) + expectEqualElements(a, expected, file: file, line: line) + } + + let b = 3 ..< 7 + let exp = 1 ..< 7 + check(exp) { $0.formUnion(BitSet.Counted(b)) } + check(exp) { $0.formUnion(BitSet(b)) } + check(exp) { $0.formUnion(b) } + check(exp) { $0.formUnion(Set(b)) } + } + + func test_formIntersection() { + func check( + _ expected: S, + _ body: (inout BitSet.Counted) -> Void, + file: StaticString = #file, + line: UInt = #line + ) where S.Element == Int { + var a: BitSet.Counted = [1, 2, 3, 4] + + body(&a) + expectEqualElements(a, expected, file: file, line: line) + } + + let b = 3 ..< 7 + let exp = 3 ..< 5 + check(exp) { $0.formIntersection(BitSet.Counted(b)) } + check(exp) { $0.formIntersection(BitSet(b)) } + check(exp) { $0.formIntersection(b) } + check(exp) { $0.formIntersection(Set(b)) } + } + + func test_formSymmetricDifference() { + func check( + _ expected: S, + _ body: (inout BitSet.Counted) -> Void, + file: StaticString = #file, + line: UInt = #line + ) where S.Element == Int { + var a: BitSet.Counted = [1, 2, 3, 4] + + body(&a) + expectEqualElements(a, expected, file: file, line: line) + } + + let b = 3 ..< 7 + let exp = [1, 2, 5, 6] + check(exp) { $0.formSymmetricDifference(BitSet.Counted(b)) } + check(exp) { $0.formSymmetricDifference(BitSet(b)) } + check(exp) { $0.formSymmetricDifference(b) } + check(exp) { $0.formSymmetricDifference(Set(b)) } + } + + func test_subtract() { + func check( + _ expected: S, + _ body: (inout BitSet.Counted) -> Void, + file: StaticString = #file, + line: UInt = #line + ) where S.Element == Int { + var a: BitSet.Counted = [1, 2, 3, 4] + + body(&a) + expectEqualElements(a, expected, file: file, line: line) + } + + let b = 3 ..< 7 + let exp = [1, 2] + check(exp) { $0.subtract(BitSet.Counted(b)) } + check(exp) { $0.subtract(BitSet(b)) } + check(exp) { $0.subtract(b) } + check(exp) { $0.subtract(Set(b)) } + } + + func test_isEqual() { + let a: BitSet.Counted = [1, 2, 3, 4] + + let b = 1 ..< 5 + expectTrue(a.isEqualSet(to: BitSet.Counted(b))) + expectTrue(a.isEqualSet(to: BitSet(b))) + expectTrue(a.isEqualSet(to: b)) + expectTrue(a.isEqualSet(to: Set(b))) + + let c = 2 ..< 7 + expectFalse(a.isEqualSet(to: BitSet.Counted(c))) + expectFalse(a.isEqualSet(to: BitSet(c))) + expectFalse(a.isEqualSet(to: c)) + expectFalse(a.isEqualSet(to: Set(c))) + } + + func test_isSubset() { + let a: BitSet.Counted = [1, 2, 3, 4] + + let b = 1 ..< 6 + expectTrue(a.isSubset(of: BitSet.Counted(b))) + expectTrue(a.isSubset(of: BitSet(b))) + expectTrue(a.isSubset(of: b)) + expectTrue(a.isSubset(of: Set(b))) + + let c = 2 ..< 4 + expectFalse(a.isSubset(of: BitSet.Counted(c))) + expectFalse(a.isSubset(of: BitSet(c))) + expectFalse(a.isSubset(of: c)) + expectFalse(a.isSubset(of: Set(c))) + } + + func test_isSuperset() { + let a: BitSet.Counted = [1, 2, 3, 4] + + let b = 2 ..< 5 + expectTrue(a.isSuperset(of: BitSet.Counted(b))) + expectTrue(a.isSuperset(of: BitSet(b))) + expectTrue(a.isSuperset(of: b)) + expectTrue(a.isSuperset(of: Set(b))) + + let c = 2 ..< 8 + expectFalse(a.isSuperset(of: BitSet.Counted(c))) + expectFalse(a.isSuperset(of: BitSet(c))) + expectFalse(a.isSuperset(of: c)) + expectFalse(a.isSuperset(of: Set(c))) + } + + func test_isStrictSubset() { + let a: BitSet.Counted = [1, 2, 3, 4] + + let b = 1 ..< 6 + expectTrue(a.isStrictSubset(of: BitSet.Counted(b))) + expectTrue(a.isStrictSubset(of: BitSet(b))) + expectTrue(a.isStrictSubset(of: b)) + expectTrue(a.isStrictSubset(of: Set(b))) + + let c = 1 ..< 5 + expectFalse(a.isStrictSubset(of: BitSet.Counted(c))) + expectFalse(a.isStrictSubset(of: BitSet(c))) + expectFalse(a.isStrictSubset(of: c)) + expectFalse(a.isStrictSubset(of: Set(c))) + } + + func test_isStrictSuperset() { + let a: BitSet.Counted = [1, 2, 3, 4] + + let b = 2 ..< 5 + expectTrue(a.isStrictSuperset(of: BitSet.Counted(b))) + expectTrue(a.isStrictSuperset(of: BitSet(b))) + expectTrue(a.isStrictSuperset(of: b)) + expectTrue(a.isStrictSuperset(of: Set(b))) + + let c = 1 ..< 5 + expectFalse(a.isStrictSuperset(of: BitSet.Counted(c))) + expectFalse(a.isStrictSuperset(of: BitSet(c))) + expectFalse(a.isStrictSuperset(of: c)) + expectFalse(a.isStrictSuperset(of: Set(c))) + } + + func test_isDisjoint() { + let a: BitSet.Counted = [1, 2, 3, 4] + + let b = 5 ..< 10 + expectTrue(a.isDisjoint(with: BitSet.Counted(b))) + expectTrue(a.isDisjoint(with: BitSet(b))) + expectTrue(a.isDisjoint(with: b)) + expectTrue(a.isDisjoint(with: Set(b))) + + let c = 4 ..< 10 + expectFalse(a.isDisjoint(with: BitSet.Counted(c))) + expectFalse(a.isDisjoint(with: BitSet(c))) + expectFalse(a.isDisjoint(with: c)) + expectFalse(a.isDisjoint(with: Set(c))) + } +} diff --git a/Tests/BitCollectionsTests/BitSetTests.swift b/Tests/BitCollectionsTests/BitSetTests.swift new file mode 100644 index 000000000..ace4d570c --- /dev/null +++ b/Tests/BitCollectionsTests/BitSetTests.swift @@ -0,0 +1,1569 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +import XCTest +#if COLLECTIONS_SINGLE_MODULE +import Collections +#else +import _CollectionsTestSupport +import BitCollections +import OrderedCollections +#endif + +extension BitSet: SetAPIExtras { + public mutating func update(_ member: Int, at index: Index) -> Int { + fatalError("Not this one though") + } +} + +extension BitSet: SortedCollectionAPIChecker {} + +final class BitSetTest: CollectionTestCase { + func test_empty_initializer() { + let set = BitSet() + expectEqual(set.count, 0) + expectTrue(set.isEmpty) + expectEqualElements(set, []) + } + + func test_array_literal_initializer() { + let set0: BitSet = [] + expectEqual(set0.count, 0) + expectTrue(set0.isEmpty) + expectEqualElements(set0, []) + + let set1: BitSet = [0, 3, 50, 10, 21, 11, 3, 100, 300, 20] + expectEqual(set1.count, 9) + expectEqualElements(set1, [0, 3, 10, 11, 20, 21, 50, 100, 300]) + } + + func test_sequence_initializer() { + withEvery("i", in: 0 ..< 100) { i in + var rng = RepeatableRandomNumberGenerator(seed: i) + let input = (0 ..< 1000).shuffled(using: &rng).prefix(100) + let expected = input.sorted() + let actual = BitSet(input) + expectEqual(actual.count, expected.count) + expectEqualElements(actual, expected) + + let actual2 = BitSet(actual) + expectEqualElements(actual2, actual) + } + } + + func test_range_initializer() { + withEveryRange("range", in: 0 ..< 200) { range in + let set = BitSet(range) + expectEqualElements(set, range) + } + } + + func test_words_initializer() { + let s0 = BitSet(words: []) + expectEqualElements(s0, []) + + let s1 = BitSet(words: [23]) + expectEqualElements(s1, [0, 1, 2, 4]) + + let s2 = BitSet(words: [1, 1]) + expectEqualElements(s2, [0, UInt.bitWidth]) + + let s3 = BitSet(words: [1, 2, 4]) + expectEqualElements(s3, [0, UInt.bitWidth + 1, 2 * UInt.bitWidth + 2]) + + let s4 = BitSet(words: [UInt.max, UInt.max, UInt.max]) + expectEqualElements(s4, 0 ..< 3 * UInt.bitWidth) + } + + func test_bitPattern_initializer() { + let s1 = BitSet(bitPattern: 0 as UInt) + expectEqualElements(s1, []) + + let s2 = BitSet(bitPattern: 1 as UInt) + expectEqualElements(s2, [0]) + + let s3 = BitSet(bitPattern: 2 as UInt) + expectEqualElements(s3, [1]) + + let s4 = BitSet(bitPattern: 23) + expectEqualElements(s4, [0, 1, 2, 4]) + + let s5 = BitSet(bitPattern: -1) + expectEqualElements(s5, 0 ..< UInt.bitWidth) + } + + func test_BitArray_initializer() { + let a0: BitArray = [] + let s0 = BitSet(a0) + expectEqualElements(s0, []) + + let a1: BitArray = [true, false, true] + let s1 = BitSet(a1) + expectEqualElements(s1, [0, 2]) + + var a2 = BitArray(repeatElement(false, count: 145)) + a2.append(contentsOf: repeatElement(true, count: 277)) + let s2 = BitSet(a2) + expectEqualElements(s2, 145 ..< 145 + 277) + } + + func test_collection_strides() { + withEvery("stride", in: [1, 2, 3, 5, 7, 8, 11, 13, 63, 79, 300]) { stride in + withEvery("count", in: [0, 1, 2, 5, 10, 20, 25]) { count in + let input = Swift.stride(from: 0, to: stride * count, by: stride) + let expected = Array(input) + let actual = BitSet(input) + checkBidirectionalCollection(actual, expectedContents: expected) + } + } + } + + func withInterestingSets( + _ label: String, + maximum: Int, + file: StaticString = #file, + line: UInt = #line, + run body: (Set) -> Void + ) { + let context = TestContext.current + func yield(_ desc: String, _ set: Set) { + let entry = context.push( + "\(label): \(desc)", file: file, line: line) + defer { context.pop(entry) } + body(set) + } + + yield("empty", []) + yield("full", Set(0 ..< maximum)) + var rng = RepeatableRandomNumberGenerator(seed: 0) + let c = 10 + + func randomSelection(_ desc: String, count: Int) { + // 1% filled + for i in 0 ..< c { + let set = Set((0 ..< maximum) + .shuffled(using: &rng) + .prefix(count)) + yield("\(desc)/\(i)", set) + } + } + if maximum > 100 { + randomSelection("1%", count: maximum / 100) + randomSelection("9%", count: 9 * maximum / 100) + } + randomSelection("50%", count: maximum / 2) + + let a = maximum / 3 + let b = 2 * maximum / 3 + yield("0..(_ seq: S) + where S.Element == Int + { + withEvery("value", in: 0 ..< 1000) { value in + expectEqual(seq.contains(value), input.contains(value)) + } + expectFalse(seq.contains(-1)) + expectFalse(seq.contains(5000)) + } + let bitset = BitSet(input) + check(bitset) + } + } + + func test_insert() { + let count = 100 + withEvery("seed", in: 0 ..< 10) { seed in + var rng = RepeatableRandomNumberGenerator(seed: seed) + var actual: BitSet = [] + var expected: Set = [] + let input = (0 ..< count).shuffled(using: &rng) + withEvery("i", in: input.indices) { i in + let (i1, m1) = actual.insert(input[i]) + expected.insert(input[i]) + expectTrue(i1) + expectEqual(m1, input[i]) + if i % 25 == 0 { + expectEqual(Array(actual), expected.sorted()) + } + let (i2, m2) = actual.insert(input[i]) + expectFalse(i2) + expectEqual(m2, m1) + } + expectEqual(Array(actual), expected.sorted()) + } + } + + func test_update() { + let count = 100 + withEvery("seed", in: 0 ..< 10) { seed in + var rng = RepeatableRandomNumberGenerator(seed: seed) + var actual: BitSet = [] + var expected: Set = [] + let input = (0 ..< count).shuffled(using: &rng) + withEvery("i", in: input.indices) { i in + let old = actual.update(with: input[i]) + expected.update(with: input[i]) + expectEqual(old, input[i]) + if i % 25 == 0 { + expectEqual(Array(actual), expected.sorted()) + } + expectNil(actual.update(with: input[i])) + } + expectEqual(Array(actual), expected.sorted()) + } + } + + func test_remove() { + let count = 100 + withEvery("seed", in: 0 ..< 10) { seed in + var rng = RepeatableRandomNumberGenerator(seed: seed) + var actual = BitSet(0 ..< count) + var expected = Set(0 ..< count) + let input = (0 ..< count).shuffled(using: &rng) + + expectNil(actual.remove(-1)) + + withEvery("i", in: input.indices) { i in + let v = input[i] + let old = actual.remove(v) + expected.remove(v) + expectEqual(old, v) + if i % 25 == 0 { + expectEqual(Array(actual), expected.sorted()) + } + expectNil(actual.remove(v)) + } + expectEqual(Array(actual), expected.sorted()) + } + } + + func test_remove_at() { + let count = 100 + withEvery("seed", in: 0 ..< 10) { seed in + var rng = RepeatableRandomNumberGenerator(seed: seed) + var actual = BitSet(0 ..< count) + var expected = Set(0 ..< count) + var c = count + + func nextOffset() -> Int? { + guard let next = (0 ..< c).randomElement(using: &rng) + else { return nil } + c -= 1 + return next + } + + withEvery("offset", by: nextOffset) { offset in + let i = actual.index(actual.startIndex, offsetBy: offset) + let old = actual.remove(at: i) + + let old2 = expected.remove(old) + expectEqual(old, old2) + } + expectEqual(Array(actual), expected.sorted()) + } + } + + func test_member_subscript_getter() { + withInterestingSets("input", maximum: 1000) { input in + let bitset = BitSet(input) + withEvery("value", in: 0 ..< 1000) { value in + expectEqual(bitset[member: value], input.contains(value)) + } + expectFalse(bitset[member: -1]) + expectFalse(bitset[member: Int.min]) + expectFalse(bitset[member: 5000]) + expectFalse(bitset[member: Int.max]) + } + } + + func test_member_subscript_setter_insert() { + let count = 100 + withEvery("seed", in: 0 ..< 10) { seed in + var rng = RepeatableRandomNumberGenerator(seed: seed) + var actual: BitSet = [] + var expected: Set = [] + let input = (0 ..< count).shuffled(using: &rng) + withEvery("i", in: input.indices) { i in + actual[member: input[i]] = true + expected.insert(input[i]) + expectEqual(actual.count, expected.count) + if i % 25 == 0 { + expectEqual(Array(actual), expected.sorted()) + } + actual[member: input[i]] = true + expectEqual(actual.count, expected.count) + } + expectEqual(Array(actual), expected.sorted()) + } + } + + func test_member_subscript_setter_remove() { + let count = 100 + withEvery("seed", in: 0 ..< 10) { seed in + var rng = RepeatableRandomNumberGenerator(seed: seed) + var actual = BitSet(0 ..< count) + var expected = Set(0 ..< count) + let input = (0 ..< count).shuffled(using: &rng) + + actual[member: -1] = false + + withEvery("i", in: input.indices) { i in + let v = input[i] + actual[member: v] = false + expected.remove(v) + expectEqual(actual.count, expected.count) + if i % 25 == 0 { + expectEqual(Array(actual), expected.sorted()) + } + actual[member: v] = false + expectEqual(actual.count, expected.count) + } + expectEqual(Array(actual), expected.sorted()) + } + } + + func test_member_range_subscript() { + let bits: BitSet = [2, 5, 6, 8, 9] + + let a = bits[members: 3..<7] + expectEqualElements(a, [5, 6]) + expectEqual(a[a.startIndex], 5) + expectEqual(bits[a.endIndex], 8) + + let b = bits[members: 4...] + expectEqualElements(b, [5, 6, 8, 9]) + expectEqual(b[b.startIndex], 5) + expectEqual(b.endIndex, bits.endIndex) + + let c = bits[members: ..<8] + expectEqualElements(c, [2, 5, 6]) + expectEqual(c[c.startIndex], 2) + expectEqual(bits[c.endIndex], 8) + + let d = bits[members: -10 ..< 100] + expectEqualElements(d, [2, 5, 6, 8, 9]) + expectEqual(d.startIndex, bits.startIndex) + expectEqual(d.endIndex, bits.endIndex) + + let e = bits[members: Int.min ..< Int.max] + expectEqualElements(e, [2, 5, 6, 8, 9]) + expectEqual(e.startIndex, bits.startIndex) + expectEqual(e.endIndex, bits.endIndex) + + let f = bits[members: -100 ..< -10] + expectEqualElements(f, []) + expectEqual(f.startIndex, bits.startIndex) + expectEqual(f.endIndex, bits.startIndex) + + let g = bits[members: 10 ..< 100] + expectEqualElements(g, []) + expectEqual(g.startIndex, bits.endIndex) + expectEqual(g.endIndex, bits.endIndex) + + let h = bits[members: 100 ..< 1000] + expectEqualElements(h, []) + expectEqual(h.startIndex, bits.endIndex) + expectEqual(h.endIndex, bits.endIndex) + } + + func test_member_range_subscript_exhaustive() { + withInterestingSets("bits", maximum: 200) { reference in + let bits = BitSet(reference) + withEveryRange("range", in: 0 ..< reference.count) { range in + let actual = bits[members: range] + let expected = reference.filter { range.contains($0) }.sorted() + expectEqualElements(actual, expected) + } + } + } + + func test_Encodable() throws { + let b1: BitSet = [] + let v1: MinimalEncoder.Value = .array([]) + expectEqual(try MinimalEncoder.encode(b1), v1) + + let b2: BitSet = [0, 1, 2, 3] + let v2: MinimalEncoder.Value = .array([.uint64(15)]) + expectEqual(try MinimalEncoder.encode(b2), v2) + + let b3 = BitSet(0 ..< 145) + let v3: MinimalEncoder.Value = .array([ + .uint64(UInt64.max), + .uint64(UInt64.max), + .uint64((1 << 17) - 1) + ]) + expectEqual(try MinimalEncoder.encode(b3), v3) + + let b4: BitSet = [343] + let v4: MinimalEncoder.Value = .array([ + .uint64(0), + .uint64(0), + .uint64(0), + .uint64(0), + .uint64(0), + .uint64(1 << 23), + ]) + expectEqual(try MinimalEncoder.encode(b4), v4) + } + + func test_Decodable() throws { + let b1: BitSet = [] + let v1: MinimalEncoder.Value = .array([]) + expectEqual(try MinimalDecoder.decode(v1, as: BitSet.self), b1) + + let b2: BitSet = [0, 1, 2, 3] + let v2: MinimalEncoder.Value = .array([.uint64(15)]) + expectEqual(try MinimalDecoder.decode(v2, as: BitSet.self), b2) + + let b3 = BitSet(0 ..< 145) + let v3: MinimalEncoder.Value = .array([ + .uint64(UInt64.max), + .uint64(UInt64.max), + .uint64((1 << 17) - 1) + ]) + expectEqual(try MinimalDecoder.decode(v3, as: BitSet.self), b3) + + let b4: BitSet = [343] + let v4: MinimalEncoder.Value = .array([ + .uint64(0), + .uint64(0), + .uint64(0), + .uint64(0), + .uint64(0), + .uint64(1 << 23), + ]) + expectEqual(try MinimalDecoder.decode(v4, as: BitSet.self), b4) + } + + func test_union() { + withInterestingSets("a", maximum: 200) { a in + withInterestingSets("b", maximum: 200) { b in + let expected = a.union(b).sorted() + let x = BitSet(a) + let y = BitSet(b) + let z = Array(b) + + expectEqualElements(x.union(b), expected) + expectEqualElements(x.union(y), expected) + expectEqualElements(x.union(y.counted), expected) + + func union(_ first: BitSet,_ second: S) -> BitSet + where S.Element == Int { + first.union(second) + } + + expectEqualElements(union(x, y), expected) + expectEqualElements(union(x, y.counted), expected) + expectEqualElements(x.union(z), expected) + expectEqualElements(x.union(z + z), expected) + } + } + } + + func test_union_Range() { + withEvery("step", in: [1, 5, 16, 23, 24, UInt.bitWidth]) { step in + let a = BitSet() + + let b = a.union(0 ..< 5*step) + expectEqualElements(b, 0 ..< 5*step) + + let c = b.union(0 ..< 10*step) + expectEqualElements(c, 0 ..< 10*step) + + let d = c.union(50*step ..< 50*step) + expectEqualElements(d, 0 ..< 10*step) + + let e = d.union(20*step ..< 30*step) + expectEqualElements(e, Array(0 ..< 10*step) + Array(20*step ..< 30*step)) + + let f = e.union(5*step ..< 25*step) + expectEqualElements(f, 0 ..< 30*step) + + let g = f.union(30*step ..< 30*step) + expectEqualElements(g, 0 ..< 30*step) + + func union(_ first: BitSet,_ second: S) -> BitSet + where S.Element == Int { + first.union(second) + } + + let h = union(f, 20*step ..< 40*step) + expectEqualElements(h, 0 ..< 40*step) + } + } + + func test_formUnion() { + withInterestingSets("a", maximum: 200) { a in + withInterestingSets("b", maximum: 200) { b in + let expected = a.union(b).sorted() + let c = BitSet(b) + let d = Array(b) + withEvery("shared", in: [false, true]) { shared in + do { + var x = BitSet(a) + withHiddenCopies(if: shared, of: &x) { x in + x.formUnion(c) + expectEqualElements(x, expected) + } + } + do { + var x = BitSet(a) + withHiddenCopies(if: shared, of: &x) { x in + x.formUnion(c.counted) + expectEqualElements(x, expected) + } + } + func formUnion(_ first: inout BitSet, _ second: S) + where S.Element == Int { + first.formUnion(second) + } + do { + var x = BitSet(a) + withHiddenCopies(if: shared, of: &x) { x in + formUnion(&x, c) + expectEqualElements(x, expected) + } + } + do { + var x = BitSet(a) + withHiddenCopies(if: shared, of: &x) { x in + formUnion(&x, c.counted) + expectEqualElements(x, expected) + } + } + + do { + var x = BitSet(a) + withHiddenCopies(if: shared, of: &x) { x in + x.formUnion(d) + expectEqualElements(x, expected) + } + } + do { + var x = BitSet(a) + withHiddenCopies(if: shared, of: &x) { x in + x.formUnion(d + d) + expectEqualElements(x, expected) + } + } + } + } + } + } + + func test_formUnion_Range() { + withEvery("step", in: [1, 5, 16, 23, 24, UInt.bitWidth]) { step in + var a = BitSet() + + a.formUnion(0 ..< 5*step) + expectEqualElements(a, 0 ..< 5*step) + + a.formUnion(0 ..< 10*step) + expectEqualElements(a, 0 ..< 10*step) + + a.formUnion(50*step ..< 50*step) + expectEqualElements(a, 0 ..< 10*step) + + a.formUnion(20*step ..< 30*step) + expectEqualElements(a, Array(0 ..< 10*step) + Array(20*step ..< 30*step)) + + a.formUnion(5*step ..< 25*step) + expectEqualElements(a, 0 ..< 30*step) + + a.formUnion(30*step ..< 30*step) + expectEqualElements(a, 0 ..< 30*step) + + func formUnion(_ first: inout BitSet,_ second: S) + where S.Element == Int { + first.formUnion(second) + } + + formUnion(&a, 20*step ..< 40*step) + expectEqualElements(a, 0 ..< 40*step) + } + } + + func test_intersection() { + withInterestingSets("a", maximum: 200) { a in + withInterestingSets("b", maximum: 200) { b in + let expected = a.intersection(b).sorted() + let x = BitSet(a) + let y = BitSet(b) + let z = Array(b) + + expectEqualElements(x.intersection(b), expected) + expectEqualElements(x.intersection(y), expected) + expectEqualElements(x.intersection(y.counted), expected) + + func intersection(_ first: BitSet,_ second: S) -> BitSet + where S.Element == Int { + first.intersection(second) + } + + expectEqualElements(intersection(x, y), expected) + expectEqualElements(intersection(x, y.counted), expected) + expectEqualElements(x.intersection(z), expected) + expectEqualElements(x.intersection(z + z), expected) + } + } + } + + func test_intersection_Range() { + withEvery("step", in: [1, 5, 16, 23, 24, UInt.bitWidth]) { step in + let a = BitSet(0 ..< 10*step) + let b = a.intersection(10*step ..< 20*step) + expectEqualElements(b, []) + + let c = a.intersection(0 ..< 10*step) + expectEqualElements(c, 0 ..< 10*step) + + let d = a.intersection(0 ..< 5*step) + expectEqualElements(d, 0 ..< 5*step) + + let e = d.intersection(20*step ..< 20*step) + expectEqualElements(e, []) + + let f = a.intersection(-100*step ..< -10*step) + expectEqualElements(f, []) + + let g = a.intersection(-100*step ..< 10*step) + expectEqualElements(g, 0 ..< 10*step) + + func intersection(_ first: BitSet,_ second: S) -> BitSet + where S.Element == Int { + first.intersection(second) + } + + let h = intersection(g, 5*step ..< 15*step) + expectEqualElements(h, 5*step ..< 10*step) + } + } + + func test_formIntersection() { + withInterestingSets("a", maximum: 200) { a in + withInterestingSets("b", maximum: 200) { b in + let expected = a.intersection(b).sorted() + let c = BitSet(b) + let d = Array(b) + withEvery("shared", in: [false, true]) { shared in + do { + var x = BitSet(a) + withHiddenCopies(if: shared, of: &x) { x in + x.formIntersection(c) + expectEqualElements(x, expected) + } + } + do { + var x = BitSet(a) + withHiddenCopies(if: shared, of: &x) { x in + x.formIntersection(c.counted) + expectEqualElements(x, expected) + } + } + func formIntersection(_ first: inout BitSet, _ second: S) + where S.Element == Int { + first.formIntersection(second) + } + do { + var x = BitSet(a) + withHiddenCopies(if: shared, of: &x) { x in + formIntersection(&x, c) + expectEqualElements(x, expected) + } + } + do { + var x = BitSet(a) + withHiddenCopies(if: shared, of: &x) { x in + formIntersection(&x, c.counted) + expectEqualElements(x, expected) + } + } + + do { + var x = BitSet(a) + withHiddenCopies(if: shared, of: &x) { x in + x.formIntersection(d) + expectEqualElements(x, expected) + } + } + do { + var x = BitSet(a) + withHiddenCopies(if: shared, of: &x) { x in + x.formIntersection(d + d) + expectEqualElements(x, expected) + } + } + } + } + } + } + + func test_formIntersection_Range() { + withEvery("step", in: [1, 5, 16, 23, 24, UInt.bitWidth]) { step in + var a = BitSet(0 ..< 10*step) + a.formIntersection(10*step ..< 20*step) + expectEqualElements(a, []) + + var b = BitSet(0 ..< 10*step) + b.formIntersection(0 ..< 10*step) + expectEqualElements(b, 0 ..< 10*step) + + var c = BitSet(0 ..< 10*step) + c.formIntersection(0 ..< 5*step) + expectEqualElements(c, 0 ..< 5*step) + + var d = BitSet(0 ..< 10*step) + d.formIntersection(20*step ..< 20*step) + expectEqualElements(d, []) + + var e = BitSet(0 ..< 100*step) + e.formIntersection(50*step ..< 100*step) + expectEqualElements(e, 50*step ..< 100*step) + + var f = BitSet(0 ..< 100*step) + f.formIntersection(-100*step ..< -10*step) + expectEqualElements(f, []) + + var g = BitSet(0 ..< 100*step) + g.formIntersection(-100*step ..< 10*step) + expectEqualElements(g, 0 ..< 10 * step) + + func formIntersection(_ first: inout BitSet,_ second: S) + where S.Element == Int { + first.formIntersection(second) + } + + var h = BitSet(0 ..< 100*step) + formIntersection(&h, 20*step ..< 120*step) + expectEqualElements(h, 20*step ..< 100*step) + } + } + + func test_symmetricDifference() { + withInterestingSets("a", maximum: 200) { a in + withInterestingSets("b", maximum: 200) { b in + let expected = a.symmetricDifference(b).sorted() + let x = BitSet(a) + let y = BitSet(b) + let z = Array(b) + + expectEqualElements(x.symmetricDifference(b), expected) + expectEqualElements(x.symmetricDifference(y), expected) + expectEqualElements(x.symmetricDifference(y.counted), expected) + + func symmetricDifference( + _ first: BitSet,_ second: S + ) -> BitSet + where S.Element == Int { + first.symmetricDifference(second) + } + + expectEqualElements(symmetricDifference(x, y), expected) + expectEqualElements(symmetricDifference(x, y.counted), expected) + expectEqualElements(x.symmetricDifference(z), expected) + expectEqualElements(x.symmetricDifference(z + z), expected) + } + } + } + + func test_symmetricDifference_Range() { + withEvery("step", in: [1, 5, 16, 23, 24, UInt.bitWidth]) { step in + let a = BitSet() + + let b = a.symmetricDifference(0 ..< 10*step) + expectEqualElements(b, 0 ..< 10*step) + + let c = b.symmetricDifference(5*step ..< 10*step) + expectEqualElements(c, 0 ..< 5*step) + + let d = b.symmetricDifference(0 ..< 5*step) + expectEqualElements(d, 5*step ..< 10*step) + + let e = b.symmetricDifference(3*step ..< 7*step) + expectEqualElements(e, Array(0 ..< 3*step) + Array(7*step ..< 10*step)) + + let f = e.symmetricDifference(20*step ..< 20*step) + expectEqualElements(f, e) + + func symmetricDifference( + _ first: BitSet,_ second: S + ) -> BitSet + where S.Element == Int { + first.symmetricDifference(second) + } + let g = symmetricDifference(e, 3*step ..< 7*step) + expectEqualElements(g, 0 ..< 10*step) + } + } + + func test_formSymmetricDifference() { + withInterestingSets("a", maximum: 200) { a in + withInterestingSets("b", maximum: 200) { b in + let expected = a.symmetricDifference(b).sorted() + let c = BitSet(b) + let d = Array(b) + + withEvery("shared", in: [false, true]) { shared in + do { + var x = BitSet(a) + withHiddenCopies(if: shared, of: &x) { x in + x.formSymmetricDifference(c) + expectEqualElements(x, expected) + } + } + do { + var x = BitSet(a) + withHiddenCopies(if: shared, of: &x) { x in + x.formSymmetricDifference(c.counted) + expectEqualElements(x, expected) + } + } + func formSymmetricDifference(_ first: inout BitSet, _ second: S) + where S.Element == Int { + first.formSymmetricDifference(second) + } + do { + var x = BitSet(a) + withHiddenCopies(if: shared, of: &x) { x in + formSymmetricDifference(&x, c) + expectEqualElements(x, expected) + } + } + do { + var x = BitSet(a) + withHiddenCopies(if: shared, of: &x) { x in + formSymmetricDifference(&x, c.counted) + expectEqualElements(x, expected) + } + } + + do { + var x = BitSet(a) + withHiddenCopies(if: shared, of: &x) { x in + x.formSymmetricDifference(d) + expectEqualElements(x, expected) + } + } + do { + var x = BitSet(a) + withHiddenCopies(if: shared, of: &x) { x in + x.formSymmetricDifference(d + d) + expectEqualElements(x, expected) + } + } + } + } + } + } + + func test_formSymmetricDifference_Range() { + withEvery("step", in: [1, 5, 16, 23, 24, UInt.bitWidth]) { step in + var a = BitSet() + + a.formSymmetricDifference(0 ..< 10*step) + expectEqualElements(a, 0 ..< 10*step) + + a.formSymmetricDifference(5*step ..< 10*step) + expectEqualElements(a, 0 ..< 5*step) + + a.formSymmetricDifference(0 ..< 5*step) + expectEqualElements(a, []) + + a = BitSet(0 ..< 10*step) + a.formSymmetricDifference(3*step ..< 7*step) + expectEqualElements(a, Array(0 ..< 3*step) + Array(7*step ..< 10*step)) + + a.formSymmetricDifference(20*step ..< 20*step) + expectEqualElements(a, Array(0 ..< 3*step) + Array(7*step ..< 10*step)) + + func formSymmetricDifference(_ first: inout BitSet, _ second: S) + where S.Element == Int { + first.formSymmetricDifference(second) + } + + formSymmetricDifference(&a, 3*step ..< 7*step) + expectEqualElements(a, 0 ..< 10*step) + } + } + + func test_subtracting() { + withInterestingSets("a", maximum: 200) { a in + withInterestingSets("b", maximum: 200) { b in + let expected = a.subtracting(b).sorted() + let x = BitSet(a) + let y = BitSet(b) + let z = Array(b) + + expectEqualElements(x.subtracting(b), expected) + expectEqualElements(x.subtracting(y), expected) + expectEqualElements(x.subtracting(y.counted), expected) + + func subtracting(_ first: BitSet,_ second: S) -> BitSet + where S.Element == Int { + first.subtracting(second) + } + + expectEqualElements(subtracting(x, y), expected) + expectEqualElements(subtracting(x, y.counted), expected) + expectEqualElements(x.subtracting(z), expected) + expectEqualElements(x.subtracting(z + z), expected) + } + } + } + + func test_subtracting_Range() { + withEvery("step", in: [1, 5, 16, 23, 24, UInt.bitWidth]) { step in + let a = BitSet(0 ..< 10*step) + + let b = a.subtracting(9*step ..< 11*step) + expectEqualElements(b, 0 ..< 9*step) + + let c = b.subtracting(-1*step ..< 1*step) + expectEqualElements(c, 1*step ..< 9*step) + + let expected = Array(1*step ..< 4*step) + Array(6*step ..< 9*step) + let d = c.subtracting(4*step ..< 6*step) + expectEqualElements(d, expected) + + let e = d.subtracting(10*step ..< 100*step) + expectEqualElements(e, expected) + + let f = e.subtracting(-10*step ..< -1*step) + expectEqualElements(f, expected) + + func subtracting(_ first: BitSet,_ second: S) -> BitSet + where S.Element == Int { + first.subtracting(second) + } + + let g = subtracting(e, 4*step ..< 10*step) + expectEqualElements(g, 1*step ..< 4*step) + } + } + + func test_subtract() { + withInterestingSets("a", maximum: 200) { a in + withInterestingSets("b", maximum: 200) { b in + let expected = a.subtracting(b).sorted() + let c = BitSet(b) + let d = Array(b) + withEvery("shared", in: [false, true]) { shared in + do { + var x = BitSet(a) + withHiddenCopies(if: shared, of: &x) { x in + x.subtract(c) + expectEqualElements(x, expected) + } + } + do { + var x = BitSet(a) + withHiddenCopies(if: shared, of: &x) { x in + x.subtract(c.counted) + expectEqualElements(x, expected) + } + } + func subtract(_ first: inout BitSet, _ second: S) + where S.Element == Int { + first.subtract(second) + } + do { + var x = BitSet(a) + withHiddenCopies(if: shared, of: &x) { x in + subtract(&x, c) + expectEqualElements(x, expected) + } + } + do { + var x = BitSet(a) + withHiddenCopies(if: shared, of: &x) { x in + subtract(&x, c.counted) + expectEqualElements(x, expected) + } + } + + do { + var x = BitSet(a) + withHiddenCopies(if: shared, of: &x) { x in + x.subtract(d) + expectEqualElements(x, expected) + } + } + do { + var x = BitSet(a) + withHiddenCopies(if: shared, of: &x) { x in + x.subtract(d + d) + expectEqualElements(x, expected) + } + } + } + } + } + } + + func test_subtract_Range() { + withEvery("step", in: [1, 5, 16, 23, 24, UInt.bitWidth]) { step in + var a = BitSet(0 ..< 10*step) + + a.subtract(9*step ..< 11*step) + expectEqualElements(a, 0 ..< 9*step) + + a.subtract(-1*step ..< 1*step) + expectEqualElements(a, 1*step ..< 9*step) + + let expected = Array(1*step ..< 4*step) + Array(6*step ..< 9*step) + a.subtract(4*step ..< 6*step) + expectEqualElements(a, expected) + + a.subtract(10*step ..< 100*step) + expectEqualElements(a, expected) + + a.subtract(-10*step ..< -1*step) + expectEqualElements(a, expected) + + func subtract(_ first: inout BitSet, _ second: S) + where S.Element == Int { + first.subtract(second) + } + subtract(&a, 3*step ..< 10*step) + expectEqualElements(a, 1*step ..< 3*step) + } + } + + func test_isEqual_to_integer_range() { + let a = BitSet(200 ..< 400) + expectTrue(a.isEqualSet(to: 200 ..< 400)) + expectFalse(a.isEqualSet(to: 201 ..< 401)) + expectFalse(a.isEqualSet(to: -1 ..< 200)) + expectFalse(a.isEqualSet(to: 0 ..< 0)) + expectFalse(a.isEqualSet(to: 0 ..< 1000)) + expectFalse(a.isEqualSet(to: 0 ..< 250)) + expectFalse(a.isEqualSet(to: 270 ..< 400)) + + var b = a + b.remove(300) + expectFalse(b.isEqualSet(to: 200 ..< 400)) + + let c = BitSet(130 ..< 160) + expectTrue(c.isEqualSet(to: 130 ..< 160)) + + withEvery("i", in: stride(from: 0, to: 200, by: 4)) { i in + withEvery("j", in: stride(from: i, to: 200, by: 4)) { j in + let c = BitSet(i ..< j) + expectTrue(c.isEqualSet(to: i ..< j)) + expectFalse(c.isEqualSet(to: i ..< (j + 1))) + } + } + } + + func test_isEqual_to_counted_BitSet() { + let a = BitSet(200 ..< 400) + expectTrue(a.isEqualSet(to: a.counted)) + } + + func test_isEqual_to_Sequence() { + func check( + _ bits: BitSet, + _ items: S + ) -> Bool where S.Element == Int { + bits.isEqualSet(to: items) + } + + let bits: BitSet = [3, 6, 8, 11] + expectTrue(check(bits, bits)) + expectTrue(check(bits, bits.counted)) + expectFalse(check(bits, 0 ..< 10)) + + expectFalse(check(bits, [3, 6, 8] as OrderedSet)) + expectTrue(check(bits, [3, 6, 8, 11] as OrderedSet)) + expectFalse(check(bits, [3, 6, 8, 11, 2] as OrderedSet)) + expectFalse(check(bits, [3, 6, 8, 20] as OrderedSet)) + expectFalse(check(bits, [3, 6, 8, -1] as OrderedSet)) + + expectTrue(check([], [] as BitSet)) + expectTrue(check([], [] as BitSet.Counted)) + expectTrue(check([], [] as Set)) + expectTrue(check([], MinimalSequence(elements: []))) + + expectFalse(check([], [1] as BitSet)) + expectFalse(check([], [1] as BitSet.Counted)) + expectFalse(check([], [1] as Set)) + expectFalse( + check([], + MinimalSequence(elements: [1], underestimatedCount: .value(0)))) + + let x1 = MinimalSequence( + elements: [3, 6, 8, 11], + underestimatedCount: .value(0)) + expectTrue(check(bits, x1)) + + let x2 = MinimalSequence( + elements: [3, 6, -1], + underestimatedCount: .value(0)) + expectFalse(check(bits, x2)) + + let x3 = MinimalSequence( + elements: [3, 6, 0], + underestimatedCount: .value(0)) + expectFalse(check(bits, x3)) + + let x4 = MinimalSequence( + elements: [3, 6, 8], + underestimatedCount: .value(0)) + expectFalse(check(bits, x4)) + + let x5 = MinimalSequence( + elements: [3, 6, 8, 11, 3, 6, 8, 11], + underestimatedCount: .value(0)) + expectTrue(check(bits, x5)) // Dupes are okay! + + let x6 = MinimalSequence( + elements: [3, 6, 8, 11, -1], + underestimatedCount: .value(0)) + expectFalse(check(bits, x6)) + + let x7 = MinimalSequence( + elements: [3, 6, 8, 11, 100], + underestimatedCount: .value(0)) + expectFalse(check(bits, x7)) + } + + + func test_isSubset() { + withEvery("step", in: [1, 5, 16, 23, 24, UInt.bitWidth]) { step in + + let inputs: [String: BitSet] = [ + "empty": BitSet(), + "a": BitSet(10*step ..< 20*step), + "b": BitSet(10*step ..< 20*step).subtracting(13*step ..< 14*step), + "c": BitSet(10*step ..< 20*step - 1), + ] + + let tests: [(range: Range, expected: Set)] = [ + ( 10*step ..< 12*step, ["empty"]), + ( 12*step ..< 18*step, ["empty"]), + ( 18*step ..< 20*step, ["empty"]), + ( 11*step ..< 21*step, ["empty"]), + ( 9*step ..< 19*step, step > 1 ? ["empty"] : ["empty", "c"]), + ( 10*step ..< 20*step, ["empty", "a", "b", "c"]), + ( 10*step ..< 20*step - 1, ["empty", "c"]), + (-10*step ..< 20*step, ["empty", "a", "b", "c"]), + ( 10*step ..< 21*step, ["empty", "a", "b", "c"]), + ( 15*step ..< 15*step, ["empty"]), + ] + + withEvery("input", in: inputs.keys) { input in + let set = inputs[input]! + expectTrue(set.isSubset(of: set)) + + withEvery("test", in: tests) { test in + let expected = test.expected.contains(input) + + func forceSequence(_ other: S) -> Bool + where S.Element == Int { + set.isSubset(of: other) + } + + if test.range.lowerBound >= 0 { + expectEqual(set.isSubset(of: BitSet(test.range)), expected) + expectEqual(forceSequence(BitSet(test.range)), expected) + + expectEqual( + set.isSubset(of: BitSet.Counted(test.range)), expected) + expectEqual( + forceSequence(BitSet.Counted(test.range)), expected) + } + + let a = Array(test.range) + + expectEqual(set.isSubset(of: a), expected) + expectEqual(set.isSubset(of: a + a), expected) + expectEqual(set.isSubset(of: Set(test.range)), expected) + + expectEqual(set.isSubset(of: test.range), expected) + expectEqual(forceSequence(test.range), expected) + } + } + } + } + + func test_isStrictSubset() { + withEvery("step", in: [1, 5, 16, 23, 24, UInt.bitWidth]) { step in + + let inputs: [String: BitSet] = [ + "empty": BitSet(), + "a": BitSet(10*step ..< 20*step), + "b": BitSet(10*step ..< 20*step).subtracting(13*step ..< 14*step), + "c": BitSet(10*step ..< 20*step - 1), + ] + + let tests: [(range: Range, expected: Set)] = [ + ( 10*step ..< 12*step, ["empty"]), + ( 12*step ..< 18*step, ["empty"]), + ( 18*step ..< 20*step, ["empty"]), + ( 11*step ..< 21*step, ["empty"]), + ( 9*step ..< 19*step, step > 1 ? ["empty"] : ["empty", "c"]), + ( 10*step ..< 20*step, ["empty", "b", "c"]), + ( 10*step ..< 20*step - 1, ["empty"]), + (-10*step ..< 20*step, ["empty", "a", "b", "c"]), + ( 10*step ..< 21*step, ["empty", "a", "b", "c"]), + ( 15*step ..< 15*step, []), + ] + + withEvery("input", in: inputs.keys) { input in + let set = inputs[input]! + expectFalse(set.isStrictSubset(of: set)) + + withEvery("test", in: tests) { test in + let expected = test.expected.contains(input) + + func forceSequence(_ other: S) -> Bool + where S.Element == Int { + set.isStrictSubset(of: other) + } + + if test.range.lowerBound >= 0 { + expectEqual(set.isStrictSubset(of: BitSet(test.range)), expected) + expectEqual(forceSequence(BitSet(test.range)), expected) + + expectEqual( + set.isStrictSubset(of: BitSet.Counted(test.range)), expected) + expectEqual( + forceSequence(BitSet.Counted(test.range)), expected) + } + + let a = Array(test.range) + + expectEqual(set.isStrictSubset(of: a), expected) + expectEqual(set.isStrictSubset(of: a + a), expected) + + expectEqual(set.isStrictSubset(of: test.range), expected) + expectEqual(forceSequence(test.range), expected) + } + } + } + } + + func test_isSuperset() { + withEvery("step", in: [1, 5, 16, 23, 24, UInt.bitWidth]) { step in + + let inputs: [String: BitSet] = [ + "empty": BitSet(), + "a": BitSet(10*step ..< 20*step), + "b": BitSet(10*step ..< 20*step).subtracting(13*step ..< 14*step), + "c": BitSet(10*step ..< 20*step - 1), + ] + + let tests: [(range: Range, expected: Set)] = [ + ( 10*step ..< 12*step, ["a", "b", "c"]), + ( 12*step ..< 18*step, ["a", "c"]), + ( 18*step ..< 20*step, ["a", "b"]), + ( 11*step ..< 21*step, []), + ( 9*step ..< 19*step, []), + ( 10*step ..< 20*step, ["a"]), + ( 10*step ..< 20*step - 1, ["a", "c"]), + (-10*step ..< 20*step, []), + ( 10*step ..< 21*step, []), + ( 15*step ..< 15*step, ["empty", "a", "b", "c"]), + ] + + withEvery("input", in: inputs.keys) { input in + let set = inputs[input]! + expectTrue(set.isSuperset(of: set)) + + withEvery("test", in: tests) { test in + let expected = test.expected.contains(input) + + func forceSequence(_ other: S) -> Bool + where S.Element == Int { + set.isSuperset(of: other) + } + + if test.range.lowerBound >= 0 { + expectEqual(set.isSuperset(of: BitSet(test.range)), expected) + expectEqual(forceSequence(BitSet(test.range)), expected) + + expectEqual( + set.isSuperset(of: BitSet.Counted(test.range)), expected) + expectEqual( + forceSequence(BitSet.Counted(test.range)), expected) + } + + let a = Array(test.range) + + expectEqual(set.isSuperset(of: a), expected) + expectEqual(set.isSuperset(of: a + a), expected) + + expectEqual(set.isSuperset(of: test.range), expected) + expectEqual(forceSequence(test.range), expected) + } + } + } + } + + func test_isStrictSuperset() { + withEvery("step", in: [1, 5, 16, 23, 24, UInt.bitWidth]) { step in + + let inputs: [String: BitSet] = [ + "empty": BitSet(), + "a": BitSet(10*step ..< 20*step), + "b": BitSet(10*step ..< 20*step).subtracting(13*step ..< 14*step), + "c": BitSet(10*step ..< 20*step - 1), + ] + + let tests: [(range: Range, expected: Set)] = [ + ( 10*step ..< 12*step, ["a", "b", "c"]), + ( 12*step ..< 18*step, ["a", "c"]), + ( 18*step ..< 20*step, ["a", "b"]), + ( 11*step ..< 21*step, []), + ( 9*step ..< 19*step, []), + ( 10*step ..< 20*step, []), + ( 10*step ..< 20*step - 1, ["a"]), + (-10*step ..< 20*step, []), + ( 10*step ..< 21*step, []), + ( 15*step ..< 15*step, ["a", "b", "c"]), + ] + + withEvery("input", in: inputs.keys) { input in + let set = inputs[input]! + expectFalse(set.isStrictSuperset(of: set)) + + withEvery("test", in: tests) { test in + let expected = test.expected.contains(input) + + func forceSequence(_ other: S) -> Bool + where S.Element == Int { + set.isStrictSuperset(of: other) + } + + if test.range.lowerBound >= 0 { + expectEqual(set.isStrictSuperset(of: BitSet(test.range)), expected) + expectEqual(forceSequence(BitSet(test.range)), expected) + + expectEqual( + set.isStrictSuperset(of: BitSet.Counted(test.range)), expected) + expectEqual( + forceSequence(BitSet.Counted(test.range)), expected) + } + + let a = Array(test.range) + + expectEqual(set.isStrictSuperset(of: a), expected) + expectEqual(set.isStrictSuperset(of: a + a), expected) + + expectEqual(set.isStrictSuperset(of: test.range), expected) + expectEqual(forceSequence(test.range), expected) + } + } + } + } + + func test_isDisjoint() { + withEvery("step", in: [1, 5, 16, 23, 24, UInt.bitWidth]) { step in + + let inputs: [String: BitSet] = [ + "empty": BitSet(), + "a": BitSet(10*step ..< 20*step), + "b": BitSet(10*step ..< 20*step).subtracting(13*step ..< 14*step), + "c": BitSet(10*step ..< 20*step - 1), + ] + + let tests: [(range: Range, expected: Set)] = [ + ( 10*step ..< 12*step, ["empty"]), + ( 12*step ..< 18*step, ["empty"]), + ( 18*step ..< 20*step, ["empty"]), + ( 11*step ..< 21*step, ["empty"]), + ( 9*step ..< 19*step, ["empty"]), + ( 10*step ..< 20*step, ["empty"]), + ( 10*step ..< 20*step - 1, ["empty"]), + (-10*step ..< 20*step, ["empty"]), + ( 10*step ..< 21*step, ["empty"]), + ( 15*step ..< 15*step, ["empty", "a", "b", "c"]), + ( 13*step ..< 14*step, ["empty", "b"]), + ( 20*step - 1 ..< 22*step, ["empty", "c"]), + ] + + withEvery("input", in: inputs.keys) { input in + let set = inputs[input]! + expectEqual(set.isDisjoint(with: set), set.isEmpty) + + withEvery("test", in: tests) { test in + let expected = test.expected.contains(input) + + func forceSequence(_ other: S) -> Bool + where S.Element == Int { + set.isDisjoint(with: other) + } + + if test.range.lowerBound >= 0 { + expectEqual(set.isDisjoint(with: BitSet(test.range)), expected) + expectEqual(forceSequence(BitSet(test.range)), expected) + + expectEqual( + set.isDisjoint(with: BitSet.Counted(test.range)), expected) + expectEqual( + forceSequence(BitSet.Counted(test.range)), expected) + } + + let a = Array(test.range) + expectEqual(set.isDisjoint(with: a), expected) + expectEqual(set.isDisjoint(with: a + a), expected) + + expectEqual(set.isDisjoint(with: test.range), expected) + expectEqual(forceSequence(test.range), expected) + } + } + } + } + + func test_sorted() { + let s1: BitSet = [283, 3, 5, 6362, 0, 23] + let s2 = s1.sorted() + + expectTrue(type(of: s2) == BitSet.self) + expectEqualElements(s1, [0, 3, 5, 23, 283, 6362]) + expectEqualElements(s2, [0, 3, 5, 23, 283, 6362]) + } + + func test_random() { + var rng = AllOnesRandomNumberGenerator() + for c in [0, 10, 64, 65, 77, 1200] { + let set = BitSet.random(upTo: c, using: &rng) + expectEqual(set.count, c) + expectEqualElements(set, 0 ..< c) + } + + let a = Set((0..<10).map { _ in BitSet.random(upTo: 1000) }) + expectEqual(a.count, 10) + } + + func test_description() { + let a: BitSet = [] + expectEqual("\(a)", "[]") + + let b: BitSet = [1, 2, 3] + expectEqual("\(b)", "[1, 2, 3]") + + let c: BitSet = [23, 652, 892, 19230] + expectEqual("\(c)", "[23, 652, 892, 19230]") + } + + func test_debugDescription() { + let a: BitSet = [] + expectEqual("\(String(reflecting: a))", "[]") + + let b: BitSet = [1, 2, 3] + expectEqual("\(String(reflecting: b))", "[1, 2, 3]") + + let c: BitSet = [23, 652, 892, 19230] + expectEqual("\(String(reflecting: c))", "[23, 652, 892, 19230]") + } + + func test_index_descriptions() { + let a: BitSet = [3, 6, 8] + let i = a.startIndex + + expectEqual(i.description, "3") + expectEqual(i.debugDescription, "3") + } + + func test_mirror() { + func check(_ v: T) -> String { + var str = "" + dump(v, to: &str) + return str + } + + expectEqual(check(BitSet()), """ + - 0 members + + """) + + expectEqual(check([1, 2, 3] as BitSet), """ + ▿ 3 members + - 1 + - 2 + - 3 + + """) + } + + func test_filter() { + let a: BitSet = [] + expectEqual(a.filter { $0.isMultiple(of: 4) }, []) + + let b = BitSet(0 ..< 1000) + expectEqualElements( + b.filter { $0.isMultiple(of: 4) }, + stride(from: 0, to: 1000, by: 4)) + + let c = BitSet(0 ..< 1000) + expectEqualElements(c.filter { _ in false }, []) + + let d = BitSet(0 ..< 1000) + expectEqualElements(d.filter { _ in true }, 0 ..< 1000) + } +} diff --git a/Tests/CollectionsTestSupportTests/CombinatoricsChecks.swift b/Tests/CollectionsTestSupportTests/CombinatoricsChecks.swift new file mode 100644 index 000000000..607773716 --- /dev/null +++ b/Tests/CollectionsTestSupportTests/CombinatoricsChecks.swift @@ -0,0 +1,53 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +import XCTest +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsTestSupport +#endif + +class CombinatoricsTests: CollectionTestCase { + func testEverySubset_smoke() { + func collectSubsets(of set: [Int]) -> Set<[Int]> { + var result: Set<[Int]> = [] + withEverySubset("subset", of: set) { subset in + let r = result.insert(subset) + expectTrue(r.inserted) + } + return result + } + + expectEqual(collectSubsets(of: []), [[]]) + expectEqual(collectSubsets(of: [0]), [[], [0]]) + expectEqual(collectSubsets(of: [0, 1]), [[], [0], [1], [0, 1]]) + expectEqual( + collectSubsets(of: [0, 1, 2]), + [[], [0], [1], [2], [0, 1], [0, 2], [1, 2], [0, 1, 2]]) + } + + func testEveryPermutation_smoke() { + func collectPermutations(of items: [Int]) -> Set<[Int]> { + var result: Set<[Int]> = [] + withEveryPermutation("permutation", of: items) { permutation in + let r = result.insert(permutation) + expectTrue(r.inserted) + } + return result + } + + expectEqual(collectPermutations(of: []), [[]]) + expectEqual(collectPermutations(of: [0]), [[0]]) + expectEqual(collectPermutations(of: [0, 1]), [[0, 1], [1, 0]]) + expectEqual( + collectPermutations(of: [0, 1, 2]), + [[0, 1, 2], [0, 2, 1], [1, 0, 2], [1, 2, 0], [2, 0, 1], [2, 1, 0]]) + } +} diff --git a/Tests/CollectionsTestSupportTests/IndexRangeCollectionTests.swift b/Tests/CollectionsTestSupportTests/IndexRangeCollectionTests.swift new file mode 100644 index 000000000..a4ae0bc5e --- /dev/null +++ b/Tests/CollectionsTestSupportTests/IndexRangeCollectionTests.swift @@ -0,0 +1,29 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +import XCTest +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsTestSupport +#endif + +final class IndexRangeCollectionTests: CollectionTestCase { + func testCollection() { + withEvery("b", in: [0, 1]) { b in + withEvery("c", in: 0 ..< 3) { c in + let expected = (b ... b + c).flatMap { end in + (b ... end).lazy.map { start in start ..< end } + } + let actual = IndexRangeCollection(bounds: b ..< b + c) + checkBidirectionalCollection(actual, expectedContents: expected) + } + } + } +} diff --git a/Tests/CollectionsTestSupportTests/MinimalTypeConformances.swift b/Tests/CollectionsTestSupportTests/MinimalTypeConformances.swift index 21e8534cb..786e69f30 100644 --- a/Tests/CollectionsTestSupportTests/MinimalTypeConformances.swift +++ b/Tests/CollectionsTestSupportTests/MinimalTypeConformances.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 @@ -10,9 +10,11 @@ //===----------------------------------------------------------------------===// import XCTest +#if !COLLECTIONS_SINGLE_MODULE import _CollectionsTestSupport +#endif -final class DequeTests: CollectionTestCase { +final class MinimalTypeTests: CollectionTestCase { func testMinimalSequence() { withEvery( "behavior", diff --git a/Tests/CollectionsTestSupportTests/UtilitiesTests.swift b/Tests/CollectionsTestSupportTests/UtilitiesTests.swift new file mode 100644 index 000000000..17c7bab3b --- /dev/null +++ b/Tests/CollectionsTestSupportTests/UtilitiesTests.swift @@ -0,0 +1,31 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +import XCTest + +#if !COLLECTIONS_SINGLE_MODULE && DEBUG +@testable import _CollectionsTestSupport +#endif + +#if COLLECTIONS_SINGLE_MODULE || DEBUG +final class UtilitiesTests: CollectionTestCase { + func testIntegerSquareRoot() { + withSome("i", in: 0 ..< Int.max, maxSamples: 100_000) { i in + let s = i._squareRoot() + expectLessThanOrEqual(s * s, i) + let next = (s + 1).multipliedReportingOverflow(by: s + 1) + if !next.overflow { + expectGreaterThan(next.partialValue, i) + } + } + } +} +#endif diff --git a/Tests/DequeTests/DequeInternals.swift b/Tests/DequeTests/DequeInternals.swift index 1f4e165e8..c34a3f18f 100644 --- a/Tests/DequeTests/DequeInternals.swift +++ b/Tests/DequeTests/DequeInternals.swift @@ -2,15 +2,19 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 // //===----------------------------------------------------------------------===// +#if COLLECTIONS_SINGLE_MODULE +@_spi(Testing) import Collections +#else import _CollectionsTestSupport @_spi(Testing) import DequeModule +#endif internal struct DequeLayout: CustomStringConvertible { let capacity: Int diff --git a/Tests/DequeTests/DequeTests.swift b/Tests/DequeTests/DequeTests.swift index 22227a6f3..34c947fe2 100644 --- a/Tests/DequeTests/DequeTests.swift +++ b/Tests/DequeTests/DequeTests.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 @@ -10,8 +10,12 @@ //===----------------------------------------------------------------------===// import XCTest +#if COLLECTIONS_SINGLE_MODULE +@_spi(Testing) import Collections +#else import _CollectionsTestSupport @_spi(Testing) import DequeModule +#endif final class DequeTests: CollectionTestCase { func test_testingSPIs() { @@ -48,19 +52,18 @@ final class DequeTests: CollectionTestCase { expectEqual("\([1, 2, nil, 3] as Deque)", "[Optional(1), Optional(2), nil, Optional(3)]") let deque: Deque = [1, 2, 3] - expectEqual("\(deque)", "[description(1), description(2), description(3)]") + expectEqual("\(deque)", "[debugDescription(1), debugDescription(2), debugDescription(3)]") } func test_debugDescription() { - expectEqual(String(reflecting: [] as Deque), - "Deque([])") - expectEqual(String(reflecting: [1, 2, 3] as Deque), - "Deque([1, 2, 3])") - expectEqual(String(reflecting: [1, 2, nil, 3] as Deque), - "Deque>([Optional(1), Optional(2), nil, Optional(3)])") + expectEqual(String(reflecting: [] as Deque), "[]") + expectEqual(String(reflecting: [1, 2, 3] as Deque), "[1, 2, 3]") + expectEqual( + String(reflecting: [1, 2, nil, 3] as Deque), + "[Optional(1), Optional(2), nil, Optional(3)]") let deque: Deque = [1, 2, 3] - expectEqual(String(reflecting: deque), "Deque([debugDescription(1), debugDescription(2), debugDescription(3)])") + expectEqual(String(reflecting: deque), "[debugDescription(1), debugDescription(2), debugDescription(3)]") } func test_customMirror() { diff --git a/Tests/DequeTests/MutableCollectionTests.swift b/Tests/DequeTests/MutableCollectionTests.swift index 1d5ea85dc..364a1b8dd 100644 --- a/Tests/DequeTests/MutableCollectionTests.swift +++ b/Tests/DequeTests/MutableCollectionTests.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 @@ -10,8 +10,12 @@ //===----------------------------------------------------------------------===// import XCTest +#if COLLECTIONS_SINGLE_MODULE +import Collections +#else import _CollectionsTestSupport @_spi(Testing) import DequeModule +#endif final class MutableCollectiontests: CollectionTestCase { // Note: Most of the test below are exhaustively testing the behavior diff --git a/Tests/DequeTests/RangeReplaceableCollectionTests.swift b/Tests/DequeTests/RangeReplaceableCollectionTests.swift index e62710194..c6305dd3d 100644 --- a/Tests/DequeTests/RangeReplaceableCollectionTests.swift +++ b/Tests/DequeTests/RangeReplaceableCollectionTests.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 @@ -10,8 +10,12 @@ //===----------------------------------------------------------------------===// import XCTest +#if COLLECTIONS_SINGLE_MODULE +@_spi(Testing) import Collections +#else import _CollectionsTestSupport @_spi(Testing) import DequeModule +#endif /// Exhaustive tests for `Deque`'s implementations for `RangeReplaceableCollection` /// requirements. diff --git a/Tests/HashTreeCollectionsTests/Colliders.swift b/Tests/HashTreeCollectionsTests/Colliders.swift new file mode 100644 index 000000000..e118f6568 --- /dev/null +++ b/Tests/HashTreeCollectionsTests/Colliders.swift @@ -0,0 +1,98 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +/// A type with manually controlled hash values, for easy testing of collision +/// scenarios. +struct Collider: + Hashable, CustomStringConvertible, CustomDebugStringConvertible +{ + var identity: Int + var hash: Hash + + init(_ identity: Int, _ hash: Hash) { + self.identity = identity + self.hash = hash + } + + init(_ identity: Int) { + self.identity = identity + self.hash = Hash(identity) + } + + init(_ identity: String) { + self.hash = Hash(identity)! + self.identity = hash.value + } + + static func ==(left: Self, right: Self) -> Bool { + guard left.identity == right.identity else { return false } + precondition(left.hash == right.hash) + return true + } + + func hash(into hasher: inout Hasher) { + hasher.combine(hash.value) + } + + var description: String { + "\(identity)(#\(hash))" + } + + var debugDescription: String { + description + } +} + +/// A type with precisely controlled hash values. This can be used to set up +/// hash tables with fully deterministic contents. +struct RawCollider: + Hashable, CustomStringConvertible +{ + var identity: Int + var hash: Hash + + init(_ identity: Int, _ hash: Hash) { + self.identity = identity + self.hash = hash + } + + init(_ identity: Int) { + self.identity = identity + self.hash = Hash(identity) + } + + init(_ identity: String) { + self.hash = Hash(identity)! + self.identity = hash.value + } + + static func ==(left: Self, right: Self) -> Bool { + guard left.identity == right.identity else { return false } + precondition(left.hash == right.hash) + return true + } + + var hashValue: Int { + fatalError("Don't") + } + + func hash(into hasher: inout Hasher) { + fatalError("Don't") + } + + func _rawHashValue(seed: Int) -> Int { + hash.value + } + + var description: String { + "\(identity)#\(hash)" + } +} diff --git a/Tests/HashTreeCollectionsTests/Hash.swift b/Tests/HashTreeCollectionsTests/Hash.swift new file mode 100644 index 000000000..95f62be28 --- /dev/null +++ b/Tests/HashTreeCollectionsTests/Hash.swift @@ -0,0 +1,80 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +/// An abstract representation of a hash value. +struct Hash { + var value: Int + + init(_ value: Int) { + self.value = value + } + + init(_ value: UInt) { + self.value = Int(bitPattern: value) + } +} + + +extension Hash { + static var bucketBitWidth: Int { 5 } + static var bucketCount: Int { 1 << bucketBitWidth } + static var bitWidth: Int { UInt.bitWidth } +} + +extension Hash: Equatable { + static func ==(left: Self, right: Self) -> Bool { + left.value == right.value + } +} + +extension Hash: Hashable { + func hash(into hasher: inout Hasher) { + hasher.combine(value) + } +} + +extension Hash: CustomStringConvertible { + var description: String { + // Print hash values in radix 32 & reversed, so that the path in the hash + // tree is readily visible. + let p = String( + UInt(bitPattern: value), + radix: Self.bucketCount, + uppercase: true) + #if false // The zeroes look overwhelmingly long in this context + let c = (Self.bitWidth + Self.bucketBitWidth - 1) / Self.bucketBitWidth + let path = String(repeating: "0", count: Swift.max(0, c - p.count)) + p + return String(path.reversed()) + #else + return String(p.reversed()) + #endif + } +} + +extension Hash: LosslessStringConvertible { + init?(_ description: String) { + let s = String(description.reversed()) + guard let hash = UInt(s, radix: 32) else { return nil } + self.init(Int(bitPattern: hash)) + } +} + +extension Hash: ExpressibleByIntegerLiteral { + init(integerLiteral value: UInt) { + self.init(value) + } +} + +extension Hash: ExpressibleByStringLiteral { + init(stringLiteral value: String) { + self.init(value)! + } +} diff --git a/Tests/HashTreeCollectionsTests/TreeDictionary Smoke Tests.swift b/Tests/HashTreeCollectionsTests/TreeDictionary Smoke Tests.swift new file mode 100644 index 000000000..4d826dbb7 --- /dev/null +++ b/Tests/HashTreeCollectionsTests/TreeDictionary Smoke Tests.swift @@ -0,0 +1,628 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2019 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if COLLECTIONS_SINGLE_MODULE +import Collections +#else +import _CollectionsTestSupport +import HashTreeCollections +#endif + +extension TreeDictionary { + fileprivate func contains(_ key: Key) -> Bool { + self[key] != nil + } +} + +final class TreeDictionarySmokeTests: CollectionTestCase { + func testDummy() { + let map = TreeDictionary( + uniqueKeysWithValues: (0 ..< 100).map { ($0, 2 * $0) }) + var it = map.makeIterator() + var seen: Set = [] + while let item = it.next() { + if !seen.insert(item.key).inserted { + print(item) + } + } + expectEqual(seen.count, 100) + print("---") + for item in map { + if seen.remove(item.key) == nil { + print(item) + } + } + expectEqual(seen.count, 0) + } + + func testSubscriptAdd() { + var map: TreeDictionary = [1: "a", 2: "b"] + + map[3] = "x" + map[4] = "y" + + expectEqual(map.count, 4) + expectEqual(map[1], "a") + expectEqual(map[2], "b") + expectEqual(map[3], "x") + expectEqual(map[4], "y") + } + + func testSubscriptOverwrite() { + var map: TreeDictionary = [1: "a", 2: "b"] + + map[1] = "x" + map[2] = "y" + + expectEqual(map.count, 2) + expectEqual(map[1], "x") + expectEqual(map[2], "y") + } + + func testSubscriptRemove() { + var map: TreeDictionary = [1: "a", 2: "b"] + + map[1] = nil + map[2] = nil + + expectEqual(map.count, 0) + expectEqual(map[1], nil) + expectEqual(map[2], nil) + } + + func testTriggerOverwrite1() { + var map: TreeDictionary = [1: "a", 2: "b"] + + map.updateValue("x", forKey: 1) // triggers COW + map.updateValue("y", forKey: 2) // triggers COW + + var res1: TreeDictionary = [:] + res1.updateValue("a", forKey: 1) // in-place + res1.updateValue("b", forKey: 2) // in-place + + var res2: TreeDictionary = [:] + res2[1] = "a" // in-place + res2[2] = "b" // in-place + + var res3: TreeDictionary = res2 + res3[1] = "x" // triggers COW + res3[2] = "y" // in-place + + expectEqual(res2.count, 2) + expectEqual(res2[1], "a") + expectEqual(res2[2], "b") + + expectEqual(res3.count, 2) + expectEqual(res3[1], "x") + expectEqual(res3[2], "y") + } + + func testTriggerOverwrite2() { + var res1: TreeDictionary = [:] + res1.updateValue("a", forKey: Collider(10, 01)) // in-place + res1.updateValue("a", forKey: Collider(11, 33)) // in-place + res1.updateValue("b", forKey: Collider(20, 02)) // in-place + + res1.updateValue("x", forKey: Collider(10, 01)) // in-place + res1.updateValue("x", forKey: Collider(11, 33)) // in-place + res1.updateValue("y", forKey: Collider(20, 02)) // in-place + + var res2: TreeDictionary = res1 + res2.updateValue("a", forKey: Collider(10, 01)) // triggers COW + res2.updateValue("a", forKey: Collider(11, 33)) // in-place + res2.updateValue("b", forKey: Collider(20, 02)) // in-place + + expectEqual(res1[Collider(10, 01)], "x") + expectEqual(res1[Collider(11, 33)], "x") + expectEqual(res1[Collider(20, 02)], "y") + + expectEqual(res2[Collider(10, 01)], "a") + expectEqual(res2[Collider(11, 33)], "a") + expectEqual(res2[Collider(20, 02)], "b") + + } + + func testTriggerOverwrite3() { + let upperBound = 1_000 + + // Populating `map1` + var map1: TreeDictionary = [:] + for index in 0.. = map1 + for index in 0.. = map2 + for index in 0..( + _ key: Key, + _ value: Value + ) -> Int { + var hasher = Hasher() + hasher.combine(key) + hasher.combine(value) + return hasher.finalize() + } + + func testHashable() { + let map: TreeDictionary = [1: "a", 2: "b"] + + let hashPair1 = hashPair(1, "a") + let hashPair2 = hashPair(2, "b") + + var commutativeHasher = Hasher() + commutativeHasher.combine(hashPair1 ^ hashPair2) + + let expectedHashValue = commutativeHasher.finalize() + + expectEqual(map.hashValue, expectedHashValue) + + var inoutHasher = Hasher() + map.hash(into: &inoutHasher) + + expectEqual(inoutHasher.finalize(), expectedHashValue) + } + + func testCollisionNodeNotEqual() { + let map: TreeDictionary = [:] + + var res12 = map + res12[Collider(1, 1)] = Collider(1, 1) + res12[Collider(2, 1)] = Collider(2, 1) + + var res13 = map + res13[Collider(1, 1)] = Collider(1, 1) + res13[Collider(3, 1)] = Collider(3, 1) + + var res31 = map + res31[Collider(3, 1)] = Collider(3, 1) + res31[Collider(1, 1)] = Collider(1, 1) + + expectEqual(res13, res31) + expectNotEqual(res13, res12) + expectNotEqual(res31, res12) + } + + func testCompactionWhenDeletingFromHashCollisionNode1() { + let map: TreeDictionary = [:] + + + var res1 = map + res1[Collider(11, 1)] = Collider(11, 1) + res1[Collider(12, 1)] = Collider(12, 1) + + expectTrue(res1.contains(Collider(11, 1))) + expectTrue(res1.contains(Collider(12, 1))) + + expectEqual(res1.count, 2) + expectEqual(TreeDictionary([ + Collider(11, 1): Collider(11, 1), + Collider(12, 1): Collider(12, 1) + ]), res1) + + + var res2 = res1 + res2[Collider(12, 1)] = nil + + expectTrue(res2.contains(Collider(11, 1))) + expectFalse(res2.contains(Collider(12, 1))) + + expectEqual(res2.count, 1) + expectEqual( + TreeDictionary([ + Collider(11, 1): Collider(11, 1) + ]), + res2) + + + var res3 = res1 + res3[Collider(11, 1)] = nil + + expectFalse(res3.contains(Collider(11, 1))) + expectTrue(res3.contains(Collider(12, 1))) + + expectEqual(res3.count, 1) + expectEqual( + TreeDictionary([Collider(12, 1): Collider(12, 1)]), + res3) + + + var resX = res1 + resX[Collider(32769)] = Collider(32769) + resX[Collider(12, 1)] = nil + + expectTrue(resX.contains(Collider(11, 1))) + expectFalse(resX.contains(Collider(12, 1))) + expectTrue(resX.contains(Collider(32769))) + + expectEqual(resX.count, 2) + expectEqual( + TreeDictionary([ + Collider(11, 1): Collider(11, 1), + Collider(32769): Collider(32769)]), + resX) + + + var resY = res1 + resY[Collider(32769)] = Collider(32769) + resY[Collider(32769)] = nil + + expectTrue(resY.contains(Collider(11, 1))) + expectTrue(resY.contains(Collider(12, 1))) + expectFalse(resY.contains(Collider(32769))) + + expectEqual(resY.count, 2) + expectEqual( + TreeDictionary([ + Collider(11, 1): Collider(11, 1), + Collider(12, 1): Collider(12, 1)]), + resY) + } + + func testCompactionWhenDeletingFromHashCollisionNode2() { + let map: TreeDictionary = [:] + + + var res1 = map + res1[Collider(32769_1, 32769)] = Collider(32769_1, 32769) + res1[Collider(32769_2, 32769)] = Collider(32769_2, 32769) + + expectTrue(res1.contains(Collider(32769_1, 32769))) + expectTrue(res1.contains(Collider(32769_2, 32769))) + + expectEqual(res1.count, 2) + expectEqual( + TreeDictionary([ + Collider(32769_1, 32769): Collider(32769_1, 32769), + Collider(32769_2, 32769): Collider(32769_2, 32769)]), + res1) + + + var res2 = res1 + res2[Collider(1)] = Collider(1) + + expectTrue(res2.contains(Collider(1))) + expectTrue(res2.contains(Collider(32769_1, 32769))) + expectTrue(res2.contains(Collider(32769_2, 32769))) + + expectEqual(res2.count, 3) + expectEqual( + TreeDictionary([ + Collider(1): Collider(1), + Collider(32769_1, 32769): Collider(32769_1, 32769), + Collider(32769_2, 32769): Collider(32769_2, 32769)]), + res2) + + + var res3 = res2 + res3[Collider(32769_2, 32769)] = nil + + expectTrue(res3.contains(Collider(1))) + expectTrue(res3.contains(Collider(32769_1, 32769))) + + expectEqual(res3.count, 2) + expectEqual( + TreeDictionary([ + Collider(1): Collider(1), + Collider(32769_1, 32769): Collider(32769_1, 32769)]), + res3) + } + + func testCompactionWhenDeletingFromHashCollisionNode3() { + let map: TreeDictionary = [:] + + + var res1 = map + res1[Collider(32769_1, 32769)] = Collider(32769_1, 32769) + res1[Collider(32769_2, 32769)] = Collider(32769_2, 32769) + + expectTrue(res1.contains(Collider(32769_1, 32769))) + expectTrue(res1.contains(Collider(32769_2, 32769))) + + expectEqual(res1.count, 2) + expectEqual( + TreeDictionary([ + Collider(32769_1, 32769): Collider(32769_1, 32769), + Collider(32769_2, 32769): Collider(32769_2, 32769)]), + res1) + + + var res2 = res1 + res2[Collider(1)] = Collider(1) + + expectTrue(res2.contains(Collider(1))) + expectTrue(res2.contains(Collider(32769_1, 32769))) + expectTrue(res2.contains(Collider(32769_2, 32769))) + + expectEqual(res2.count, 3) + expectEqual( + TreeDictionary([ + Collider(1): Collider(1), + Collider(32769_1, 32769): Collider(32769_1, 32769), + Collider(32769_2, 32769): Collider(32769_2, 32769)]), + res2) + + + var res3 = res2 + res3[Collider(1)] = nil + + expectTrue(res3.contains(Collider(32769_1, 32769))) + expectTrue(res3.contains(Collider(32769_2, 32769))) + + expectEqual(res3.count, 2) + expectEqual( + TreeDictionary([ + Collider(32769_1, 32769): Collider(32769_1, 32769), + Collider(32769_2, 32769): Collider(32769_2, 32769)]), + res3) + + + expectEqual(res1, res3) + } + + func testCompactionWhenDeletingFromHashCollisionNode4() { + let map: TreeDictionary = [:] + + + var res1 = map + res1[Collider(32769_1, 32769)] = Collider(32769_1, 32769) + res1[Collider(32769_2, 32769)] = Collider(32769_2, 32769) + + expectTrue(res1.contains(Collider(32769_1, 32769))) + expectTrue(res1.contains(Collider(32769_2, 32769))) + + expectEqual(res1.count, 2) + expectEqual( + TreeDictionary([ + Collider(32769_1, 32769): Collider(32769_1, 32769), + Collider(32769_2, 32769): Collider(32769_2, 32769)]), + res1) + + + var res2 = res1 + res2[Collider(5)] = Collider(5) + + expectTrue(res2.contains(Collider(5))) + expectTrue(res2.contains(Collider(32769_1, 32769))) + expectTrue(res2.contains(Collider(32769_2, 32769))) + + expectEqual(res2.count, 3) + expectEqual( + TreeDictionary([ + Collider(5): Collider(5), + Collider(32769_1, 32769): Collider(32769_1, 32769), + Collider(32769_2, 32769): Collider(32769_2, 32769)]), + res2) + + + var res3 = res2 + res3[Collider(5)] = nil + + expectTrue(res3.contains(Collider(32769_1, 32769))) + expectTrue(res3.contains(Collider(32769_2, 32769))) + + expectEqual(res3.count, 2) + expectEqual( + TreeDictionary([ + Collider(32769_1, 32769): Collider(32769_1, 32769), + Collider(32769_2, 32769): Collider(32769_2, 32769)]), + res3) + + + expectEqual(res1, res3) + } + + func testCompactionWhenDeletingFromHashCollisionNode5() { + let map: TreeDictionary = [:] + + + var res1 = map + res1[Collider(1)] = Collider(1) + res1[Collider(1026)] = Collider(1026) + res1[Collider(32770_1, 32770)] = Collider(32770_1, 32770) + res1[Collider(32770_2, 32770)] = Collider(32770_2, 32770) + + expectTrue(res1.contains(Collider(1))) + expectTrue(res1.contains(Collider(1026))) + expectTrue(res1.contains(Collider(32770_1, 32770))) + expectTrue(res1.contains(Collider(32770_2, 32770))) + + expectEqual(res1.count, 4) + expectEqual( + TreeDictionary([ + Collider(1): Collider(1), + Collider(1026): Collider(1026), + Collider(32770_1, 32770): Collider(32770_1, 32770), + Collider(32770_2, 32770): Collider(32770_2, 32770)]), + res1) + + + var res2 = res1 + res2[Collider(1026)] = nil + + expectTrue(res2.contains(Collider(1))) + expectFalse(res2.contains(Collider(1026))) + expectTrue(res2.contains(Collider(32770_1, 32770))) + expectTrue(res2.contains(Collider(32770_2, 32770))) + + expectEqual(res2.count, 3) + expectEqual( + TreeDictionary([ + Collider(1): Collider(1), + Collider(32770_1, 32770): Collider(32770_1, 32770), + Collider(32770_2, 32770): Collider(32770_2, 32770)]), + res2) + } + + func inferSize(_ map: TreeDictionary) -> Int { + var size = 0 + + for _ in map { + size += 1 + } + + return size + } + + func testIteratorEnumeratesAllIfCollision() { + let upperBound = 1_000 + + // '+' prefixed values + var map1: TreeDictionary = [:] + for index in 0.. = map1 + for index in 0.. = map1 + for index in 0.. = [:] + for index in 0..( + _ map1: TreeDictionary + ) { + var count = 0 + for _ in map1 { + count = count + 1 + } + expectEqual(map1.count, count) + } + + func testIteratorEnumeratesAll() { + let map1: TreeDictionary = [ + Collider(11, 1): "a", + Collider(12, 1): "a", + Collider(32769): "b" + ] + + var map2: TreeDictionary = [:] + for (key, value) in map1 { + map2[key] = value + } + + expectEqual(map1, map2) + } + + func test_indexForKey_hashCollision() { + let a = Collider(1, "1000") + let b = Collider(2, "1000") + let c = Collider(3, "1001") + let map: TreeDictionary = [ + a: "a", + b: "b", + c: "c", + ] + + let indices = Array(map.indices) + + typealias Index = TreeDictionary.Index + expectEqual(map.index(forKey: a), indices[1]) + expectEqual(map.index(forKey: b), indices[2]) + expectEqual(map.index(forKey: c), indices[0]) + expectNil(map.index(forKey: Collider(4, "1000"))) + } + + func test_indexForKey() { + let input = 0 ..< 10_000 + let d = TreeDictionary( + uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + for key in input { + expectNotNil(d.index(forKey: key)) { index in + expectNotNil(d[index]) { item in + expectEqual(item.key, key) + expectEqual(item.value, 2 * key) + } + } + } + } + + func test_indexForKey_exhaustIndices() { + var map: TreeDictionary = [:] + + let range = 0 ..< 10_000 + + for value in range { + map[Collider(value)] = value + } + + var expectedPositions = Set(map.indices) + + for expectedValue in range { + expectNotNil(map.index(forKey: Collider(expectedValue))) { index in + let actualValue = map[index].value + expectEqual(expectedValue, actualValue) + expectedPositions.remove(index) + } + } + + expectTrue(expectedPositions.isEmpty) + } +} diff --git a/Tests/HashTreeCollectionsTests/TreeDictionary Tests.swift b/Tests/HashTreeCollectionsTests/TreeDictionary Tests.swift new file mode 100644 index 000000000..247deb70a --- /dev/null +++ b/Tests/HashTreeCollectionsTests/TreeDictionary Tests.swift @@ -0,0 +1,2621 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if COLLECTIONS_SINGLE_MODULE +import Collections +#else +import _CollectionsTestSupport +import HashTreeCollections +#endif + +extension TreeDictionary: DictionaryAPIExtras {} + +class TreeDictionaryTests: CollectionTestCase { + func test_empty() { + let d = TreeDictionary() + expectEqualElements(d, []) + expectEqual(d.count, 0) + + var it = d.makeIterator() + expectNil(it.next()) + expectNil(it.next()) + expectNil(it.next()) + + expectEqual(d.startIndex, d.endIndex) + + expectEqual(d.distance(from: d.startIndex, to: d.endIndex), 0) + } + + func test_remove_update_basics() throws { + var d = TreeDictionary() + + d.updateValue(1, forKey: "One") + d.updateValue(2, forKey: "Two") + d.updateValue(3, forKey: "Three") + + expectEqual(d.count, 3) + expectEqual(d["One"], 1) + expectEqual(d["Two"], 2) + expectEqual(d["Three"], 3) + expectEqual(d["Four"], nil) + + expectEqual(d.removeValue(forKey: "Two"), 2) + + expectEqual(d.count, 2) + expectEqual(d["One"], 1) + expectEqual(d["Two"], nil) + expectEqual(d["Three"], 3) + expectEqual(d["Four"], nil) + + expectEqual(d.removeValue(forKey: "Two"), nil) + expectEqual(d.removeValue(forKey: "One"), 1) + + expectEqual(d.count, 1) + expectEqual(d["One"], nil) + expectEqual(d["Two"], nil) + expectEqual(d["Three"], 3) + expectEqual(d["Four"], nil) + + expectEqual(d.removeValue(forKey: "One"), nil) + expectEqual(d.removeValue(forKey: "Two"), nil) + expectEqual(d.removeValue(forKey: "Three"), 3) + + expectEqual(d.count, 0) + expectEqual(d["One"], nil) + expectEqual(d["Two"], nil) + expectEqual(d["Three"], nil) + expectEqual(d["Four"], nil) + } + + func test_subscript_setter_basics() throws { + var d = TreeDictionary() + + d["One"] = 1 + d["Two"] = 2 + d["Three"] = 3 + + expectEqual(d.count, 3) + expectEqual(d["One"], 1) + expectEqual(d["Two"], 2) + expectEqual(d["Three"], 3) + expectEqual(d["Four"], nil) + + d["Two"] = nil + + expectEqual(d.count, 2) + expectEqual(d["One"], 1) + expectEqual(d["Two"], nil) + expectEqual(d["Three"], 3) + expectEqual(d["Four"], nil) + + d["Two"] = nil + d["One"] = nil + + expectEqual(d.count, 1) + expectEqual(d["One"], nil) + expectEqual(d["Two"], nil) + expectEqual(d["Three"], 3) + expectEqual(d["Four"], nil) + + d["One"] = nil + d["Two"] = nil + d["Three"] = nil + + expectEqual(d.count, 0) + expectEqual(d["One"], nil) + expectEqual(d["Two"], nil) + expectEqual(d["Three"], nil) + expectEqual(d["Four"], nil) + } + + func test_add_remove() throws { + var d = TreeDictionary() + + let c = 400 + for i in 0 ..< c { + expectNil(d.updateValue(i, forKey: "\(i)")) + expectEqual(d.count, i + 1) + } + + for i in 0 ..< c { + expectEqual(d["\(i)"], i) + } + + for i in 0 ..< c { + expectEqual(d.updateValue(2 * i, forKey: "\(i)"), i) + expectEqual(d.count, c) + } + + for i in 0 ..< c { + expectEqual(d["\(i)"], 2 * i) + } + + var remaining = c + for i in 0 ..< c { + expectEqual(d.removeValue(forKey: "\(i)"), 2 * i) + remaining -= 1 + expectEqual(d.count, remaining) + } + } + + func test_collisions() throws { + var d = TreeDictionary() + + let count = 100 + let groups = 20 + + for i in 0 ..< count { + let h = i % groups + let key = Collider(i, Hash(h)) + expectEqual(d[key], nil) + expectNil(d.updateValue(i, forKey: key)) + expectEqual(d[key], i) + } + + for i in 0 ..< count { + let h = i % groups + let key = Collider(i, Hash(h)) + expectEqual(d[key], i) + expectEqual(d.updateValue(2 * i, forKey: key), i) + expectEqual(d[key], 2 * i) + } + + for i in 0 ..< count { + let h = i % groups + let key = Collider(i, Hash(h)) + expectEqual(d[key], 2 * i) + expectEqual(d.removeValue(forKey: key), 2 * i) + expectEqual(d[key], nil) + } + } + + func test_shared_copies() throws { + var d = TreeDictionary() + + let c = 200 + for i in 0 ..< c { + expectNil(d.updateValue(i, forKey: i)) + } + + let copy = d + for i in 0 ..< c { + expectEqual(d.updateValue(2 * i, forKey: i), i) + } + + for i in 0 ..< c { + expectEqual(copy[i], i) + } + + let copy2 = d + for i in 0 ..< c { + expectEqual(d.removeValue(forKey: i), 2 * i) + } + + for i in 0 ..< c { + expectEqual(copy2[i], 2 * i) + } + } + + func test_Sequence_basic() { + var d: TreeDictionary = [1: 2] + var it = d.makeIterator() + expectEquivalent(it.next(), (1, 2), by: { $0 == $1 }) + expectNil(it.next()) + expectNil(it.next()) + + d[1] = nil + it = d.makeIterator() + expectNil(it.next()) + expectNil(it.next()) + } + + func test_Sequence_400() { + var d = TreeDictionary() + let c = 400 + for i in 0 ..< c { + expectNil(d.updateValue(i, forKey: i)) + } + + var seen: Set = [] + for (key, value) in d { + expectEqual(key, value) + expectTrue(seen.insert(key).inserted, "Duplicate key seen: \(key)") + } + expectEqual(seen.count, c) + expectTrue(seen.isSuperset(of: 0 ..< c)) + } + + func test_Sequence_collisions() { + var d = TreeDictionary() + + let count = 100 + let groups = 20 + + for i in 0 ..< count { + let h = i % groups + let key = Collider(i, Hash(h)) + expectNil(d.updateValue(i, forKey: key)) + } + + var seen: Set = [] + for (key, value) in d { + expectEqual(key.identity, value) + expectTrue(seen.insert(key.identity).inserted, "Duplicate key: \(key)") + } + expectEqual(seen.count, count) + expectTrue(seen.isSuperset(of: 0 ..< count)) + } + + func test_BidirectionalCollection_fixtures() { + withEachFixture { fixture in + withLifetimeTracking { tracker in + let (d, ref) = tracker.shareableDictionary(for: fixture) + checkCollection(d, expectedContents: ref, by: ==) + _checkBidirectionalCollection_indexOffsetBy( + d, expectedContents: ref, by: ==) + } + } + } + + func test_BidirectionalCollection_random100() { + let d = TreeDictionary( + uniqueKeysWithValues: (0 ..< 100).map { ($0, $0) }) + let ref = Array(d) + checkCollection(d, expectedContents: ref, by: ==) + _checkBidirectionalCollection_indexOffsetBy( + d, expectedContents: ref, by: ==) + } + + @available(macOS 12.3, iOS 15.4, watchOS 8.5, tvOS 15.4, *) + struct FancyDictionaryKey: CodingKeyRepresentable, Hashable, Codable { + var value: Int + + var codingKey: CodingKey { value.codingKey } + + init(_ value: Int) { + self.value = value + } + + init?(codingKey: T) { + guard let value = codingKey.intValue else { return nil} + self.init(value) + } + } + + struct BoringDictionaryKey: Hashable, Codable { + var value: Int + + init(_ value: Int) { + self.value = value + } + } + + func test_Encodable() throws { + let d1: TreeDictionary = [ + "one": 1, "two": 2, "three": 3] + let v1: MinimalEncoder.Value = .dictionary([ + "one": .int(1), "two": .int(2), "three": .int(3)]) + expectEqual(try MinimalEncoder.encode(d1), v1) + + let d2: TreeDictionary = [ + 1: "one", 2: "two", 3: "three"] + let v2: MinimalEncoder.Value = .dictionary([ + "1": .string("one"), + "2": .string("two"), + "3": .string("three"), + ]) + expectEqual(try MinimalEncoder.encode(d2), v2) + + if #available(macOS 12.3, iOS 15.4, watchOS 8.5, tvOS 15.4, *) { + let d3: TreeDictionary = [ + FancyDictionaryKey(1): 10, FancyDictionaryKey(2): 20 + ] + let v3: MinimalEncoder.Value = .dictionary([ + "1": .int16(10), "2": .int16(20) + ]) + expectEqual(try MinimalEncoder.encode(d3), v3) + } + + let d4: TreeDictionary = [ + // Note: we only have a single element to prevent ordering + // problems. + BoringDictionaryKey(42): 23 + ] + let v4: MinimalEncoder.Value = .array([ + .dictionary(["value": .int(42)]), + .uint8(23), + ]) + expectEqual(try MinimalEncoder.encode(d4), v4) + } + + func test_Decodable() throws { + typealias PD = TreeDictionary + + let d1: TreeDictionary = [ + "one": 1, "two": 2, "three": 3] + let v1: MinimalEncoder.Value = .dictionary([ + "one": .int(1), "two": .int(2), "three": .int(3)]) + expectEqual( + try MinimalDecoder.decode(v1, as: PD.self), + d1) + + let d2: TreeDictionary = [ + 1: "one", 2: "two", 3: "three"] + let v2: MinimalEncoder.Value = .dictionary([ + "1": .string("one"), + "2": .string("two"), + "3": .string("three"), + ]) + expectEqual( + try MinimalDecoder.decode(v2, as: PD.self), + d2) + + if #available(macOS 12.3, iOS 15.4, watchOS 8.5, tvOS 15.4, *) { + let d3: TreeDictionary = [ + FancyDictionaryKey(1): 10, FancyDictionaryKey(2): 20 + ] + let v3: MinimalEncoder.Value = .dictionary([ + "1": .int16(10), "2": .int16(20) + ]) + expectEqual( + try MinimalDecoder.decode(v3, as: PD.self), + d3) + } + + let d4: TreeDictionary = [ + // Note: we only have a single element to prevent ordering + // problems. + BoringDictionaryKey(42): 23 + ] + let v4: MinimalEncoder.Value = .array([ + .dictionary(["value": .int(42)]), + .uint8(23), + ]) + expectEqual( + try MinimalDecoder.decode(v4, as: PD.self), + d4) + + let v5: MinimalEncoder.Value = .dictionary([ + "This is not a number": .string("bus"), + "42": .string("train")]) + expectThrows(try MinimalDecoder.decode(v5, as: PD.self)) { + expectTrue($0 is DecodingError) + } + + if #available(macOS 12.3, iOS 15.4, watchOS 8.5, tvOS 15.4, *) { + let v6: MinimalEncoder.Value = .dictionary([ + "This is not a number": .string("bus"), + "42": .string("train")]) + expectThrows( + try MinimalDecoder.decode( + v6, as: PD.self) + ) { + expectTrue($0 is DecodingError) + } + + let v7: MinimalEncoder.Value = .dictionary([ + "23": .string("bus"), + "42": .string("train")]) + let d7: PD = [ + FancyDictionaryKey(23): "bus", + FancyDictionaryKey(42): "train", + ] + expectEqual( + try MinimalDecoder.decode(v7, as: PD.self), + d7) + } + + let v8: MinimalEncoder.Value = .array([ + .int32(1), .string("bike"), .int32(2), + ]) + expectThrows(try MinimalDecoder.decode(v8, as: PD.self)) + } + + func test_CustomReflectable() { + do { + let d: TreeDictionary = [1: 2, 3: 4, 5: 6] + let mirror = Mirror(reflecting: d) + expectEqual(mirror.displayStyle, .dictionary) + expectNil(mirror.superclassMirror) + expectTrue(mirror.children.compactMap { $0.label }.isEmpty) // No label + expectEqualElements( + mirror.children.compactMap { $0.value as? (key: Int, value: Int) }, + d.map { $0 }) + } + } + + func test_Equatable_Hashable() { + let samples: [[TreeDictionary]] = [ + [[:], [:]], + [[1: 100], [1: 100]], + [[2: 200], [2: 200]], + [[3: 300], [3: 300]], + [[100: 1], [100: 1]], + [[1: 1], [1: 1]], + [[100: 100], [100: 100]], + [[1: 100, 2: 200], [2: 200, 1: 100]], + [[1: 100, 2: 200, 3: 300], + [1: 100, 3: 300, 2: 200], + [2: 200, 1: 100, 3: 300], + [2: 200, 3: 300, 1: 100], + [3: 300, 1: 100, 2: 200], + [3: 300, 2: 200, 1: 100]] + ] + checkHashable(equivalenceClasses: samples) + } + + func test_CustomStringConvertible() { + let a: TreeDictionary = [:] + expectEqual(a.description, "[:]") + + let b: TreeDictionary = [ + RawCollider(0): 1 + ] + expectEqual(b.description, "[0#0: 1]") + + let c: TreeDictionary = [ + RawCollider(0): 1, + RawCollider(2): 3, + RawCollider(4): 5, + ] + expectEqual(c.description, "[0#0: 1, 2#2: 3, 4#4: 5]") + } + + func test_CustomDebugStringConvertible() { + let a: TreeDictionary = [:] + expectEqual(a.debugDescription, "[:]") + + let b: TreeDictionary = [0: 1] + expectEqual(b.debugDescription, "[0: 1]") + + let c: TreeDictionary = [0: 1, 2: 3, 4: 5] + let cd = c.map { "\($0.key): \($0.value)"}.joined(separator: ", ") + expectEqual(c.debugDescription, "[\(cd)]") + } + + + func test_index_descriptions() { + let a: TreeDictionary = [ + RawCollider(1, "1"): 1, + RawCollider(2, "21"): 2, + RawCollider(3, "22"): 3, + ] + + let i = a.startIndex + expectEqual(i.description, "@[0]") + expectEqual(i.debugDescription, "@[0]") + + let j = a.index(i, offsetBy: 1) + expectEqual(j.description, "@.0[0]") + expectEqual(j.debugDescription, "@.0[0]") + + let k = a.index(j, offsetBy: 1) + expectEqual(k.description, "@.0[1]") + expectEqual(k.debugDescription, "@.0[1]") + + let end = a.endIndex + expectEqual(end.description, "@.end(1)") + expectEqual(end.debugDescription, "@.end(1)") + } + + + func test_updateValueForKey_fixtures() { + withEachFixture { fixture in + withEvery("isShared", in: [false, true]) { isShared in + withLifetimeTracking { tracker in + var d: TreeDictionary, LifetimeTracked> = [:] + var ref: Dictionary, LifetimeTracked> = [:] + withEvery("i", in: 0 ..< fixture.count) { i in + withHiddenCopies(if: isShared, of: &d) { d in + let item = fixture.itemsInInsertionOrder[i] + let key1 = tracker.instance(for: item) + let key2 = tracker.instance(for: item) + let value = tracker.instance(for: 1000 + item.identity) + d[key1] = value + ref[key2] = value + expectEqualDictionaries(d, ref) + } + } + } + } + } + } + + func test_updateValueForKey_fixtures_tiny() { + struct Empty: Hashable {} + + withEachFixture { fixture in + withEvery("isShared", in: [false, true]) { isShared in + withLifetimeTracking { tracker in + var d: TreeDictionary, Empty> = [:] + var ref: Dictionary, Empty> = [:] + withEvery("i", in: 0 ..< fixture.count) { i in + withHiddenCopies(if: isShared, of: &d) { d in + let item = fixture.itemsInInsertionOrder[i] + let key = tracker.instance(for: item) + d[key] = Empty() + ref[key] = Empty() + expectEqualDictionaries(d, ref) + } + } + } + } + } + } + + func test_removeValueForKey_fixtures_justOne() { + withEachFixture { fixture in + withEvery("offset", in: 0 ..< fixture.count) { offset in + withEvery("isShared", in: [false, true]) { isShared in + withLifetimeTracking { tracker in + var (d, ref) = tracker.shareableDictionary(for: fixture) + withHiddenCopies(if: isShared, of: &d) { d in + let old = d.removeValue(forKey: ref[offset].key) + d._invariantCheck() + expectEqual(old, ref[offset].value) + ref.remove(at: offset) + expectEqualDictionaries(d, ref) + } + } + } + } + } + } + + func test_removeValueForKey_fixtures_all() { + withEachFixture { fixture in + withEvery("isShared", in: [false, true]) { isShared in + withLifetimeTracking { tracker in + var (d, ref) = tracker.shareableDictionary(for: fixture) + var rng = RepeatableRandomNumberGenerator(seed: 0) + withEvery("i", in: 0 ..< fixture.count) { _ in + withHiddenCopies(if: isShared, of: &d) { d in + let offset = ref.indices.randomElement(using: &rng)! + let old = d.removeValue(forKey: ref[offset].key) + d._invariantCheck() + expectEqual(old, ref[offset].value) + ref.remove(at: offset) + expectEqualDictionaries(d, ref) + } + } + } + } + } + } + + func test_subscript_getter_data() { + func check(count: Int, generator: G) { + context.withTrace("count: \(count), generator: \(generator)") { + withLifetimeTracking { tracker in + let (d, ref) = tracker.shareableDictionary(0 ..< count, with: generator) + withEvery("key", in: 0 ..< count) { key in + let key = tracker.instance(for: generator.key(for: key)) + expectEqual(d[key], ref[key]) + } + expectNil(d[tracker.instance(for: generator.key(for: -1))]) + expectNil(d[tracker.instance(for: generator.key(for: count))]) + } + } + } + + let c = 100 + check(count: c, generator: IntDataGenerator(valueOffset: c)) + check(count: c, generator: ColliderDataGenerator(groups: 5, valueOffset: c)) + } + + func test_subscript_getter_fixtures() { + withEachFixture { fixture in + withLifetimeTracking { tracker in + let (d, ref) = tracker.shareableDictionary(for: fixture) + for (k, v) in ref { + expectEqual(d[k], v, "\(k)") + } + } + } + } + + func test_subscript_setter_update_data() { + func check(count: Int, generator: G) { + context.withTrace("count: \(count), generator: \(generator)") { + withEvery("key", in: 0 ..< count) { key in + withEvery("isShared", in: [false, true]) { isShared in + withLifetimeTracking { tracker in + var (d, ref) = tracker.shareableDictionary( + 0 ..< count, with: generator) + let key = tracker.instance(for: generator.key(for: key)) + let value = tracker.instance(for: generator.value(for: -1)) + withHiddenCopies(if: isShared, of: &d) { d in + d[key] = value + ref[key] = value + expectEqualDictionaries(d, ref) + } + } + } + } + } + } + let c = 40 + check(count: c, generator: IntDataGenerator(valueOffset: c)) + check(count: c, generator: ColliderDataGenerator(groups: 5, valueOffset: c)) + } + + func test_subscript_setter_update_fixtures() { + withEachFixture { fixture in + withEvery("offset", in: 0 ..< fixture.count) { offset in + withEvery("isShared", in: [false, true]) { isShared in + withLifetimeTracking { tracker in + var (d, ref) = tracker.shareableDictionary(for: fixture) + let replacement = tracker.instance(for: -1000) + withHiddenCopies(if: isShared, of: &d) { d in + let key = ref[offset].key + d[key] = replacement + ref[offset].value = replacement + expectEqualDictionaries(d, ref) + } + } + } + } + } + } + + func test_subscript_setter_remove_data() { + func check(count: Int, generator: G) { + context.withTrace("count: \(count), generator: \(generator)") { + withEvery("key", in: 0 ..< count) { key in + withEvery("isShared", in: [false, true]) { isShared in + withLifetimeTracking { tracker in + var (d, reference) = tracker.shareableDictionary(keys: 0 ..< count) + let key = tracker.instance(for: key) + withHiddenCopies(if: isShared, of: &d) { d in + d[key] = nil + reference.removeValue(forKey: key) + expectEqualDictionaries(d, reference) + } + } + } + } + } + } + let c = 40 + check(count: c, generator: IntDataGenerator(valueOffset: c)) + check(count: c, generator: ColliderDataGenerator(groups: 5, valueOffset: c)) + } + + func test_subscript_setter_remove_fixtures() { + withEachFixture { fixture in + withEvery("offset", in: 0 ..< fixture.count) { offset in + withEvery("isShared", in: [false, true]) { isShared in + withLifetimeTracking { tracker in + var (d, ref) = tracker.shareableDictionary(for: fixture) + withHiddenCopies(if: isShared, of: &d) { d in + d[ref[offset].key] = nil + ref.remove(at: offset) + expectEqualDictionaries(d, ref) + } + } + } + } + } + } + + func test_subscript_setter_remove_fixtures_removeAll() { + withEachFixture { fixture in + withEvery("isShared", in: [false, true]) { isShared in + withLifetimeTracking { tracker in + var (d, ref) = tracker.shareableDictionary(for: fixture) + withEvery("i", in: 0 ..< ref.count) { _ in + withHiddenCopies(if: isShared, of: &d) { d in + d[ref[0].key] = nil + ref.remove(at: 0) + expectEqualDictionaries(d, ref) + } + } + } + } + } + } + + func test_subscript_setter_insert_data() { + func check(count: Int, generator: G) { + context.withTrace("count: \(count), generator: \(generator)") { + withEvery("isShared", in: [false, true]) { isShared in + withLifetimeTracking { tracker in + let keys = tracker.instances( + for: (0 ..< count).map { generator.key(for: $0) }) + let values = tracker.instances( + for: (0 ..< count).map { generator.value(for: $0) }) + var d: TreeDictionary, LifetimeTracked> = [:] + var ref: Dictionary, LifetimeTracked> = [:] + withEvery("offset", in: 0 ..< count) { offset in + withHiddenCopies(if: isShared, of: &d) { d in + d[keys[offset]] = values[offset] + ref[keys[offset]] = values[offset] + expectEqualDictionaries(d, ref) + } + } + } + } + } + } + let c = 100 + check(count: c, generator: IntDataGenerator(valueOffset: c)) + check(count: c, generator: ColliderDataGenerator(groups: 5, valueOffset: c)) + } + + func test_subscript_setter_insert_fixtures() { + withEachFixture { fixture in + withEvery("isShared", in: [false, true]) { isShared in + withLifetimeTracking { tracker in + var d: TreeDictionary, LifetimeTracked> = [:] + var ref: Dictionary, LifetimeTracked> = [:] + withEvery("i", in: 0 ..< fixture.count) { i in + withHiddenCopies(if: isShared, of: &d) { d in + let item = fixture.itemsInInsertionOrder[i] + let key = tracker.instance(for: item) + let value = tracker.instance(for: 1000 + item.identity) + d[key] = value + ref[key] = value + expectEqualDictionaries(d, ref) + } + } + } + } + } + } + + func test_subscript_setter_noop() { + func check(count: Int, generator: G) { + context.withTrace("count: \(count), generator: \(generator)") { + withEvery("isShared", in: [false, true]) { isShared in + withLifetimeTracking { tracker in + var (d, ref) = tracker.shareableDictionary(0 ..< count, with: generator) + let key = tracker.instance(for: generator.key(for: -1)) + withHiddenCopies(if: isShared, of: &d) { d in + d[key] = nil + } + expectEqualDictionaries(d, ref) + } + } + } + } + let c = 100 + check(count: c, generator: IntDataGenerator(valueOffset: c)) + check(count: c, generator: ColliderDataGenerator(groups: 5, valueOffset: c)) + } + + func test_subscript_modify_basics() { + func check(count: Int, generator: G) where G.Value == Int { + context.withTrace("count: \(count), generator: \(generator)") { + var d: TreeDictionary = [:] + var ref: Dictionary = [:] + + // Insertions + withEvery("i", in: 0 ..< count) { i in + let key = generator.key(for: i) + let value = generator.value(for: i) + mutate(&d[key]) { v in + expectNil(v) + v = value + } + ref[key] = value + expectEqualDictionaries(d, ref) + } + + // Updates + withEvery("i", in: 0 ..< count) { i in + let key = generator.key(for: i) + let value = generator.value(for: i) + + mutate(&d[key]) { v in + expectEqual(v, value) + v! *= 2 + } + ref[key]! *= 2 + expectEqualDictionaries(d, ref) + } + + // Removals + withEvery("i", in: 0 ..< count) { i in + let key = generator.key(for: i) + let value = generator.value(for: i) + + mutate(&d[key]) { v in + expectEqual(v, 2 * value) + v = nil + } + ref[key] = nil + expectEqualDictionaries(d, ref) + } + } + } + + let c = 100 + check(count: c, generator: IntDataGenerator(valueOffset: c)) + check(count: c, generator: ColliderDataGenerator(groups: 3, valueOffset: c)) + } + + func test_subscript_modify_update_data() { + func check(count: Int, generator: G) { + context.withTrace("count: \(count), generator: \(generator)") { + withEvery("key", in: 0 ..< count) { key in + withEvery("isShared", in: [false, true]) { isShared in + withLifetimeTracking { tracker in + var (d, ref) = tracker.shareableDictionary( + 0 ..< count, with: generator) + let key = tracker.instance(for: generator.key(for: key)) + let replacement = tracker.instance(for: generator.value(for: -1)) + withHiddenCopies(if: isShared, of: &d) { d in + mutate(&d[key]) { value in + expectNotNil(value) + value = replacement + } + ref[key] = replacement + expectEqualDictionaries(d, ref) + } + } + } + } + } + } + let c = 50 + check(count: c, generator: IntDataGenerator(valueOffset: c)) + check(count: c, generator: ColliderDataGenerator(groups: 5, valueOffset: c)) + } + + func test_subscript_modify_update_fixtures() { + withEachFixture { fixture in + withEvery("offset", in: 0 ..< fixture.count) { offset in + withEvery("isShared", in: [false, true]) { isShared in + withLifetimeTracking { tracker in + var (d, ref) = tracker.shareableDictionary(for: fixture) + let replacement = tracker.instance(for: -1000) + withHiddenCopies(if: isShared, of: &d) { d in + let key = ref[offset].key + mutate(&d[key]) { value in + expectNotNil(value) + value = replacement + } + ref[offset].value = replacement + expectEqualDictionaries(d, ref) + } + } + } + } + } + } + + func test_subscript_modify_in_place() { + withEachFixture { fixture in + withEvery("offset", in: 0 ..< fixture.count) { offset in + withLifetimeTracking { tracker in + var (d, ref) = tracker.shareableDictionary(for: fixture) + let key = ref[offset].key + mutate(&d[key]) { value in + expectNotNil(value) + expectTrue(isKnownUniquelyReferenced(&value)) + } + } + } + } + } + + + + func test_subscript_modify_remove_fixtures() { + withEachFixture { fixture in + withEvery("offset", in: 0 ..< fixture.count) { offset in + withEvery("isShared", in: [false, true]) { isShared in + withLifetimeTracking { tracker in + var (d, ref) = tracker.shareableDictionary(for: fixture) + withHiddenCopies(if: isShared, of: &d) { d in + mutate(&d[ref[offset].key]) { value in + expectNotNil(value) + value = nil + } + ref.remove(at: offset) + expectEqualDictionaries(d, ref) + } + } + } + } + } + } + + func test_subscript_modify_remove_fixtures_removeAll() { + withEachFixture { fixture in + withEvery("isShared", in: [false, true]) { isShared in + withLifetimeTracking { tracker in + var (d, ref) = tracker.shareableDictionary(for: fixture) + withEvery("i", in: 0 ..< ref.count) { i in + withHiddenCopies(if: isShared, of: &d) { d in + mutate(&d[ref[0].key]) { value in + expectNotNil(value) + value = nil + } + ref.remove(at: 0) + expectEqualDictionaries(d, ref) + } + } + } + } + } + } + + func test_subscript_modify_insert_data() { + func check(count: Int, generator: G) { + context.withTrace("count: \(count), generator: \(generator)") { + withEvery("isShared", in: [false, true]) { isShared in + withLifetimeTracking { tracker in + let keys = tracker.instances( + for: (0 ..< count).map { generator.key(for: $0) }) + let values = tracker.instances( + for: (0 ..< count).map { generator.value(for: $0) }) + var d: TreeDictionary, LifetimeTracked> = [:] + var ref: Dictionary, LifetimeTracked> = [:] + withEvery("offset", in: 0 ..< count) { offset in + withHiddenCopies(if: isShared, of: &d) { d in + mutate(&d[keys[offset]]) { value in + expectNil(value) + value = values[offset] + } + ref[keys[offset]] = values[offset] + expectEqualDictionaries(d, ref) + } + } + } + } + } + } + let c = 100 + check(count: c, generator: IntDataGenerator(valueOffset: c)) + check(count: c, generator: ColliderDataGenerator(groups: 5, valueOffset: c)) + } + + func test_subscript_modify_insert_fixtures() { + withEachFixture { fixture in + withEvery("seed", in: 0..<3) { seed in + withEvery("isShared", in: [false, true]) { isShared in + withLifetimeTracking { tracker in + var d: TreeDictionary, LifetimeTracked> = [:] + var ref: Dictionary, LifetimeTracked> = [:] + withEvery("i", in: 0 ..< fixture.count) { i in + withHiddenCopies(if: isShared, of: &d) { d in + let item = fixture.itemsInInsertionOrder[i] + let key = tracker.instance(for: item) + let value = tracker.instance(for: 1000 + item.identity) + mutate(&d[key]) { v in + expectNil(v) + v = value + } + ref[key] = value + expectEqualDictionaries(d, ref) + } + } + } + } + } + } + } + + func test_subscript_modify_noop_data() { + func check(count: Int, generator: G) { + context.withTrace("count: \(count), generator: \(generator)") { + withEvery("isShared", in: [false, true]) { isShared in + withLifetimeTracking { tracker in + var (d, ref) = tracker.shareableDictionary(0 ..< count, with: generator) + let key = tracker.instance(for: generator.key(for: -1)) + withHiddenCopies(if: isShared, of: &d) { d in + mutate(&d[key]) { value in + expectNil(value) + value = nil + } + expectEqualDictionaries(d, ref) + } + } + } + } + } + let c = 50 + check(count: c, generator: IntDataGenerator(valueOffset: c)) + check(count: c, generator: ColliderDataGenerator(groups: 5, valueOffset: c)) + } + + func test_defaulted_subscript_basics() { + var d: TreeDictionary = [:] + + expectEqual(d[1, default: 0], 0) + + d[1, default: 0] = 2 + expectEqual(d[1, default: 0], 2) + expectEqual(d[2, default: 0], 0) + + mutate(&d[2, default: 0]) { value in + expectEqual(value, 0) + value = 4 + } + expectEqual(d[2, default: 0], 4) + + mutate(&d[2, default: 0]) { value in + expectEqual(value, 4) + value = 6 + } + expectEqual(d[2, default: 0], 6) + } + + func test_defaulted_subscript_getter_fixtures() { + withEachFixture { fixture in + withLifetimeTracking { tracker in + let (d, ref) = tracker.shareableDictionary(for: fixture) + let def = tracker.instance(for: -1) + for (k, v) in ref { + expectEqual(d[k, default: def], v, "\(k)") + } + } + } + } + + func test_defaulted_subscript_setter_update_data() { + func check(count: Int, generator: G) { + context.withTrace("count: \(count), generator: \(generator)") { + withEvery("key", in: 0 ..< count) { key in + withEvery("isShared", in: [false, true]) { isShared in + withLifetimeTracking { tracker in + var (d, ref) = tracker.shareableDictionary( + 0 ..< count, with: generator) + let key = tracker.instance(for: generator.key(for: key)) + let value = tracker.instance(for: generator.value(for: -1)) + let def = tracker.instance(for: generator.value(for: -2)) + withHiddenCopies(if: isShared, of: &d) { d in + d[key, default: def] = value + ref[key, default: def] = value + expectEqualDictionaries(d, ref) + } + } + } + } + } + } + let c = 40 + check(count: c, generator: IntDataGenerator(valueOffset: c)) + check(count: c, generator: ColliderDataGenerator(groups: 5, valueOffset: c)) + } + + func test_defaulted_subscript_setter_update_fixtures() { + withEachFixture { fixture in + withEvery("offset", in: 0 ..< fixture.count) { offset in + withEvery("isShared", in: [false, true]) { isShared in + withLifetimeTracking { tracker in + var (d, ref) = tracker.shareableDictionary(for: fixture) + let replacement = tracker.instance(for: -1000) + let def = tracker.instance(for: -1) + withHiddenCopies(if: isShared, of: &d) { d in + let key = ref[offset].key + d[key, default: def] = replacement + ref[offset].value = replacement + expectEqualDictionaries(d, ref) + } + } + } + } + } + } + + func test_defaulted_subscript_setter_insert_data() { + func check(count: Int, generator: G) { + context.withTrace("count: \(count), generator: \(generator)") { + withEvery("isShared", in: [false, true]) { isShared in + withLifetimeTracking { tracker in + let keys = tracker.instances( + for: (0 ..< count).map { generator.key(for: $0) }) + let values = tracker.instances( + for: (0 ..< count).map { generator.value(for: $0) }) + var d: TreeDictionary, LifetimeTracked> = [:] + var ref: Dictionary, LifetimeTracked> = [:] + let def = tracker.instance(for: generator.value(for: -1000)) + withEvery("offset", in: 0 ..< count) { offset in + withHiddenCopies(if: isShared, of: &d) { d in + d[keys[offset], default: def] = values[offset] + ref[keys[offset]] = values[offset] + expectEqualDictionaries(d, ref) + } + } + } + } + } + } + let c = 100 + check(count: c, generator: IntDataGenerator(valueOffset: c)) + check(count: c, generator: ColliderDataGenerator(groups: 5, valueOffset: c)) + } + + func test_defaulted_subscript_setter_insert_fixtures() { + withEachFixture { fixture in + withEvery("seed", in: 0..<3) { seed in + withEvery("isShared", in: [false, true]) { isShared in + withLifetimeTracking { tracker in + var d: TreeDictionary, LifetimeTracked> = [:] + var ref: Dictionary, LifetimeTracked> = [:] + let def = tracker.instance(for: -1000) + withEvery("i", in: 0 ..< fixture.count) { i in + withHiddenCopies(if: isShared, of: &d) { d in + let item = fixture.itemsInInsertionOrder[i] + let key = tracker.instance(for: item) + let value = tracker.instance(for: 1000 + item.identity) + d[key, default: def] = value + ref[key] = value + expectEqualDictionaries(d, ref) + } + } + } + } + } + } + } + + func test_defaulted_subscript_modify_update_data() { + func check(count: Int, generator: G) { + context.withTrace("count: \(count), generator: \(generator)") { + withEvery("key", in: 0 ..< count) { key in + withEvery("isShared", in: [false, true]) { isShared in + withLifetimeTracking { tracker in + var (d, ref) = tracker.shareableDictionary( + 0 ..< count, with: generator) + let key = tracker.instance(for: generator.key(for: key)) + let replacement = tracker.instance(for: generator.value(for: -1)) + let def = tracker.instance(for: generator.value(for: -2)) + withHiddenCopies(if: isShared, of: &d) { d in + mutate(&d[key, default: def]) { value in + expectNotEqual(value, def) + value = replacement + } + ref[key] = replacement + expectEqualDictionaries(d, ref) + } + } + } + } + } + } + let c = 50 + check(count: c, generator: IntDataGenerator(valueOffset: c)) + check(count: c, generator: ColliderDataGenerator(groups: 5, valueOffset: c)) + } + + func test_defaulted_subscript_modify_update_fixtures() { + withEachFixture { fixture in + withEvery("offset", in: 0 ..< fixture.count) { offset in + withEvery("isShared", in: [false, true]) { isShared in + withLifetimeTracking { tracker in + var (d, ref) = tracker.shareableDictionary(for: fixture) + let replacement = tracker.instance(for: -1000) + let def = tracker.instance(for: -1) + withHiddenCopies(if: isShared, of: &d) { d in + let key = ref[offset].key + mutate(&d[key, default: def]) { value in + expectNotNil(value) + value = replacement + } + ref[offset].value = replacement + expectEqualDictionaries(d, ref) + } + } + } + } + } + } + + func test_defaulted_subscript_modify_in_place() { + withEachFixture { fixture in + withEvery("offset", in: 0 ..< fixture.count) { offset in + withLifetimeTracking { tracker in + var (d, ref) = tracker.shareableDictionary(for: fixture) + let key = ref[offset].key + mutate(&d[key, default: tracker.instance(for: -1)]) { value in + expectNotNil(value) + expectTrue(isKnownUniquelyReferenced(&value)) + } + } + } + } + } + + func test_defaulted_subscript_modify_insert_data() { + func check(count: Int, generator: G) { + context.withTrace("count: \(count), generator: \(generator)") { + withEvery("isShared", in: [false, true]) { isShared in + withLifetimeTracking { tracker in + let keys = tracker.instances( + for: (0 ..< count).map { generator.key(for: $0) }) + let values = tracker.instances( + for: (0 ..< count).map { generator.value(for: $0) }) + let def = tracker.instance(for: generator.value(for: -1000)) + var d: TreeDictionary, LifetimeTracked> = [:] + var ref: Dictionary, LifetimeTracked> = [:] + withEvery("offset", in: 0 ..< count) { offset in + withHiddenCopies(if: isShared, of: &d) { d in + mutate(&d[keys[offset], default: def]) { value in + expectEqual(value, def) + value = values[offset] + } + d._invariantCheck() + ref[keys[offset]] = values[offset] + expectEqualDictionaries(d, ref) + } + } + } + } + } + } + let c = 100 + check(count: c, generator: IntDataGenerator(valueOffset: c)) + check(count: c, generator: ColliderDataGenerator(groups: 5, valueOffset: c)) + } + + func test_defaulted_subscript_modify_insert_fixtures() { + withEachFixture { fixture in + withEvery("seed", in: 0..<3) { seed in + withEvery("isShared", in: [false, true]) { isShared in + withLifetimeTracking { tracker in + var d: TreeDictionary, LifetimeTracked> = [:] + var ref: Dictionary, LifetimeTracked> = [:] + let def = tracker.instance(for: -1) + withEvery("i", in: 0 ..< fixture.count) { i in + withHiddenCopies(if: isShared, of: &d) { d in + let item = fixture.itemsInInsertionOrder[i] + let key = tracker.instance(for: item) + let value = tracker.instance(for: 1000 + item.identity) + mutate(&d[key, default: def]) { v in + expectEqual(v, def) + v = value + } + ref[key] = value + expectEqualDictionaries(d, ref) + } + } + } + } + } + } + } + + + func test_indexForKey_data() { + func check(count: Int, generator: G) { + context.withTrace("count: \(count), generator: \(generator)") { + withLifetimeTracking { tracker in + let (d, ref) = tracker.shareableDictionary(0 ..< count, with: generator) + withEvery("key", in: ref.keys) { key in + let index = d.index(forKey: key) + expectNotNil(index) { index in + expectEqual(d[index].key, key) + expectEqual(d[index].value, ref[key]) + } + } + } + } + } + let c = 50 + check(count: c, generator: IntDataGenerator(valueOffset: c)) + check(count: c, generator: ColliderDataGenerator(groups: 5, valueOffset: c)) + } + + func test_indexForKey_fixtures() { + withEachFixture { fixture in + withLifetimeTracking { tracker in + let (d, ref) = tracker.shareableDictionary(for: fixture) + withEvery("offset", in: ref.indices) { offset in + let key = ref[offset].key + let value = ref[offset].value + let index = d.index(forKey: key) + expectNotNil(index) { index in + expectEqual(d[index].key, key) + expectEqual(d[index].value, value) + } + } + } + } + } + + func test_removeAt_fixtures() { + withEachFixture { fixture in + withLifetimeTracking { tracker in + withEvery("isShared", in: [false, true]) { isShared in + var (d, ref) = tracker.shareableDictionary(for: fixture) + withEvery("i", in: ref.indices) { _ in + let (key, value) = ref.removeFirst() + let index = d.index(forKey: key) + expectNotNil(index) { index in + withHiddenCopies(if: isShared, of: &d) { d in + let (k, v) = d.remove(at: index) + expectEqual(k, key) + expectEqual(v, value) + expectEqualDictionaries(d, ref) + } + } + } + } + } + } + } + + func test_mapValues_basics() { + let items = (0 ..< 100).map { ($0, 100 * $0) } + let d = TreeDictionary(uniqueKeysWithValues: items) + + var c = 0 + let d2 = d.mapValues { value -> String in + c += 1 + expectTrue(value.isMultiple(of: 100)) + return "\(value)" + } + expectEqual(c, 100) + expectEqualDictionaries(d, items) + + expectEqualDictionaries(d2, (0 ..< 100).compactMap { key in + (key: key, value: "\(100 * key)") + }) + } + + func test_mapValues_fixtures() { + withEachFixture { fixture in + withLifetimeTracking { tracker in + withEvery("isShared", in: [false, true]) { isShared in + var (d, ref) = tracker.shareableDictionary(for: fixture) + withHiddenCopies(if: isShared, of: &d) { d in + let d2 = d.mapValues { tracker.instance(for: "\($0.payload)") } + let ref2 = Dictionary(uniqueKeysWithValues: ref.lazy.map { + ($0.key, tracker.instance(for: "\($0.value.payload)")) + }) + expectEqualDictionaries(d2, ref2) + } + } + } + } + } + + func test_compactMapValues_basics() { + let items = (0 ..< 100).map { ($0, 100 * $0) } + let d = TreeDictionary(uniqueKeysWithValues: items) + + var c = 0 + let d2 = d.compactMapValues { value -> String? in + c += 1 + guard value.isMultiple(of: 200) else { return nil } + expectTrue(value.isMultiple(of: 100)) + return "\(value)" + } + expectEqual(c, 100) + expectEqualDictionaries(d, items) + + expectEqualDictionaries(d2, (0 ..< 50).map { key in + (key: 2 * key, value: "\(200 * key)") + }) + } + + func test_compactMapValues_fixtures() { + typealias Key = LifetimeTracked + typealias Value = LifetimeTracked + typealias Value2 = LifetimeTracked + + withEachFixture { fixture in + withLifetimeTracking { tracker in + func transform(_ value: Value) -> Value2? { + guard value.payload.isMultiple(of: 2) else { return nil } + return tracker.instance(for: "\(value.payload)") + } + + withEvery("isShared", in: [false, true]) { isShared in + var (d, ref) = tracker.shareableDictionary(for: fixture) + withHiddenCopies(if: isShared, of: &d) { d in + let d2 = d.compactMapValues(transform) + let r: [(Key, Value2)] = ref.compactMap { + guard let v = transform($0.value) else { return nil } + return ($0.key, v) + } + let ref2 = Dictionary(uniqueKeysWithValues: r) + expectEqualDictionaries(d2, ref2) + } + } + } + } + } + + func test_filter_basics() { + let items = (0 ..< 100).map { ($0, 100 * $0) } + let d = TreeDictionary(uniqueKeysWithValues: items) + + var c = 0 + let d2 = d.filter { item in + c += 1 + expectEqual(item.value, 100 * item.key) + return item.key.isMultiple(of: 2) + } + expectEqual(c, 100) + expectEqualDictionaries(d, items) + + expectEqualDictionaries(d2, (0 ..< 50).compactMap { key in + return (key: 2 * key, value: 200 * key) + }) + } + + func test_filter_fixtures_evens() { + typealias Key = LifetimeTracked + typealias Value = LifetimeTracked + + withEachFixture { fixture in + withLifetimeTracking { tracker in + withEvery("isShared", in: [false, true]) { isShared in + var (d, ref) = tracker.shareableDictionary(for: fixture) + withHiddenCopies(if: isShared, of: &d) { d in + func predicate(_ item: (key: Key, value: Value)) -> Bool { + expectEqual(item.value.payload, 1000 + item.key.payload.identity) + return item.value.payload.isMultiple(of: 2) + } + let d2 = d.filter(predicate) + let ref2 = Dictionary( + uniqueKeysWithValues: ref.filter(predicate)) + expectEqualDictionaries(d2, ref2) + } + } + } + } + } + + func test_filter_fixtures_filter_one() { + typealias Key = LifetimeTracked + typealias Value = LifetimeTracked + + withEachFixture { fixture in + withLifetimeTracking { tracker in + withEvery("isShared", in: [false, true]) { isShared in + withEvery("i", in: 0 ..< fixture.count) { i in + var (d, ref) = tracker.shareableDictionary(for: fixture) + withHiddenCopies(if: isShared, of: &d) { d in + func predicate(_ item: (key: Key, value: Value)) -> Bool { + expectEqual(item.value.payload, 1000 + item.key.payload.identity) + return item.key.payload.identity == i + } + let d2 = d.filter(predicate) + let ref2 = Dictionary( + uniqueKeysWithValues: ref.filter(predicate)) + expectEqualDictionaries(d2, ref2) + } + } + } + } + } + } + + func test_filter_exhaustive() { + withEverySubset("a", of: testItems) { a in + let x = TreeDictionary( + uniqueKeysWithValues: a.lazy.map { + ($0, $0.identity + 100) + }) + withEverySubset("b", of: a) { b in + let y = Dictionary( + uniqueKeysWithValues: b.lazy.map { + ($0, $0.identity + 100) + }) + expectEqualDictionaries(x.filter { y[$0.key] == $0.value }, y) + } + } + } + + func test_removeAll_where_exhaustive() { + withEvery("isShared", in: [false, true]) { isShared in + withEverySubset("a", of: testItems) { a in + withEverySubset("b", of: a) { b in + var x = TreeDictionary( + uniqueKeysWithValues: a.lazy.map { + ($0, $0.identity + 100) + }) + let y = Dictionary( + uniqueKeysWithValues: b.lazy.map { + ($0, $0.identity + 100) + }) + withHiddenCopies(if: isShared, of: &x) { x in + x.removeAll { y[$0.key] == nil } + expectEqualDictionaries(x, y) + } + } + } + } + } + + func test_merge_exhaustive() { + withEverySubset("a", of: testItems) { a in + withEverySubset("b", of: testItems) { b in + withEvery("isShared", in: [false, true]) { isShared in + withLifetimeTracking { tracker in + func combine( + _ a: LifetimeTracked, _ b: LifetimeTracked + ) -> LifetimeTracked { + expectEqual(b.payload, a.payload + 100) + return tracker.instance(for: 100 - a.payload) + } + var expected1 = tracker.dictionary( + for: a, by: { $0.identity + 100 }) + let expected2 = tracker.dictionary( + for: b, by: { $0.identity + 200 }) + + var actual1 = tracker.shareableDictionary( + for: a, by: { $0.identity + 100 }) + let actual2 = tracker.shareableDictionary( + for: b, by: { $0.identity + 200 }) + + expected1.merge(expected2, uniquingKeysWith: combine) + + withHiddenCopies(if: isShared, of: &expected1) { expected1 in + actual1.merge(actual2, uniquingKeysWith: combine) + expectEqualDictionaries(actual1, expected1) + } + } + } + } + } + } + + + // MARK: - + + // func test_uniqueKeysWithValues_Dictionary() { + // let items: Dictionary = [ + // "zero": 0, + // "one": 1, + // "two": 2, + // "three": 3, + // ] + // let d = TreeDictionary(uniqueKeysWithValues: items) + // expectEqualElements(d.sorted(by: <), items.sorted(by: <)) + // } + + // func test_uniqueKeysWithValues_labeled_tuples() { + // let items: KeyValuePairs = [ + // "zero": 0, + // "one": 1, + // "two": 2, + // "three": 3, + // ] + // let d = TreeDictionary(uniqueKeysWithValues: items) + // expectEqualElements(d.sorted(by: <), items.sorted(by: <)) + // } + + func test_uniqueKeysWithValues_unlabeled_tuples() { + let items: [(String, Int)] = [ + ("zero", 0), + ("one", 1), + ("two", 2), + ("three", 3), + ] + let d = TreeDictionary(uniqueKeysWithValues: items) + expectEqualElements(d.sorted(by: <), items.sorted(by: <)) + } + + // func test_uniquing_initializer_labeled_tuples() { + // let items: KeyValuePairs = [ + // "a": 1, + // "b": 1, + // "c": 1, + // "a": 2, + // "a": 2, + // "b": 1, + // "d": 3, + // ] + // let d = TreeDictionary(items, uniquingKeysWith: +) + // expectEqualElements(d, [ + // (key: "a", value: 5), + // (key: "b", value: 2), + // (key: "c", value: 1), + // (key: "d", value: 3) + // ]) + // } + + // func test_uniquing_initializer_unlabeled_tuples() { + // let items: [(String, Int)] = [ + // ("a", 1), + // ("b", 1), + // ("c", 1), + // ("a", 2), + // ("a", 2), + // ("b", 1), + // ("d", 3), + // ] + // let d = TreeDictionary(items, uniquingKeysWith: +) + // expectEqualElements(d, [ + // (key: "a", value: 5), + // (key: "b", value: 2), + // (key: "c", value: 1), + // (key: "d", value: 3) + // ]) + // } + + // func test_grouping_initializer() { + // let items: [String] = [ + // "one", "two", "three", "four", "five", + // "six", "seven", "eight", "nine", "ten" + // ] + // let d = TreeDictionary( + // grouping: items, + // by: { $0.count }) + // expectEqualElements(d, [ + // (key: 3, value: ["one", "two", "six", "ten"]), + // (key: 5, value: ["three", "seven", "eight"]), + // (key: 4, value: ["four", "five", "nine"]), + // ]) + // } + + // func test_uniqueKeysWithValues_labeled_tuples() { + // let items: KeyValuePairs = [ + // "zero": 0, + // "one": 1, + // "two": 2, + // "three": 3, + // ] + // let d = TreeDictionary(uniqueKeysWithValues: items) + // expectEqualElements(d, items) + // } + + // func test_uniqueKeysWithValues_unlabeled_tuples() { + // let items: [(String, Int)] = [ + // ("zero", 0), + // ("one", 1), + // ("two", 2), + // ("three", 3), + // ] + // let d = TreeDictionary(uniqueKeysWithValues: items) + // expectEqualElements(d, items) + // } + + func test_ExpressibleByDictionaryLiteral() { + let d0: TreeDictionary = [:] + expectTrue(d0.isEmpty) + + let d1: TreeDictionary = [ + "1~one": 1, + "2~two": 2, + "3~three": 3, + "4~four": 4, + ] + expectEqualElements( + d1.map { $0.key }.sorted(), + ["1~one", "2~two", "3~three", "4~four"]) + expectEqualElements( + d1.map { $0.value }.sorted(), + [1, 2, 3, 4]) + } + + // func test_keys() { + // let d: TreeDictionary = [ + // "one": 1, + // "two": 2, + // "three": 3, + // "four": 4, + // ] + // expectEqual(d.keys, ["one", "two", "three", "four"] as OrderedSet) + // } + + func test_counts() { + withEvery("count", in: 0 ..< 30) { count in + withLifetimeTracking { tracker in + let (d, _, _) = tracker.shareableDictionary(keys: 0 ..< count) + expectEqual(d.isEmpty, count == 0) + expectEqual(d.count, count) + expectEqual(d.underestimatedCount, count) + } + } + } + + #if false + // TODO: determine how to best calculate the expected order of the hash tree + // for testing purposes, without relying on the actual implementation + func test_index_forKey() { + withEvery("count", in: 0 ..< 30) { count in + withLifetimeTracking { tracker in + let (d, _, _) = tracker.shareableDictionary(keys: 0 ..< count) + withEvery("offset", in: 0 ..< count) { offset in + expectEqual( + // NOTE: uses the actual order `d.keys` + d.index(forKey: d.keys[offset])?._value, + offset) + } + expectNil(d.index(forKey: tracker.instance(for: -1))) + expectNil(d.index(forKey: tracker.instance(for: count))) + } + } + } + #endif + + #if false + // TODO: determine how to best calculate the expected order of the hash tree + // for testing purposes, without relying on the actual implementation + func test_subscript_offset() { + withEvery("count", in: 0 ..< 30) { count in + withLifetimeTracking { tracker in + let (d, _, _) = tracker.shareableDictionary(keys: 0 ..< count) + withEvery("offset", in: 0 ..< count) { offset in + let item = d[TreeDictionaryIndex(value: offset)] + // NOTE: uses the actual order `d.keys` + expectEqual(item.key, d.keys[offset]) + // NOTE: uses the actual order `d.values` + expectEqual(item.value, d.values[offset]) + } + } + } + } + #endif + + func test_subscript_getter() { + withEvery("count", in: 0 ..< 30) { count in + withLifetimeTracking { tracker in + let (d, keys, values) = tracker.shareableDictionary(keys: 0 ..< count) + withEvery("offset", in: 0 ..< count) { offset in + expectEqual(d[keys[offset]], values[offset]) + } + expectNil(d[tracker.instance(for: -1)]) + expectNil(d[tracker.instance(for: count)]) + } + } + } + +// func test_subscript_setter_update() { +// withEvery("count", in: 0 ..< 30) { count in +// withEvery("offset", in: 0 ..< count) { offset in +// withEvery("isShared", in: [false, true]) { isShared in +// withLifetimeTracking { tracker in +// var (d, keys, values) = tracker.shareableDictionary( +// keys: 0 ..< count) +// let replacement = tracker.instance(for: -1) +// withHiddenCopies(if: isShared, of: &d) { d in +// d[keys[offset]] = replacement +// values[offset] = replacement +// withEvery("i", in: 0 ..< count) { i in +// let (k, v) = d[offset: i] +// expectEqual(k, keys[i]) +// expectEqual(v, values[i]) +// } +// } +// } +// } +// } +// } +// } + +// func test_subscript_setter_remove() { +// withEvery("count", in: 0 ..< 30) { count in +// withEvery("offset", in: 0 ..< count) { offset in +// withEvery("isShared", in: [false, true]) { isShared in +// withLifetimeTracking { tracker in +// var (d, keys, values) = tracker.shareableDictionary( +// keys: 0 ..< count) +// withHiddenCopies(if: isShared, of: &d) { d in +// d[keys[offset]] = nil +// keys.remove(at: offset) +// values.remove(at: offset) +// withEvery("i", in: 0 ..< count - 1) { i in +// let (k, v) = d[offset: i] +// expectEqual(k, keys[i]) +// expectEqual(v, values[i]) +// } +// } +// } +// } +// } +// } +// } + +// func test_subscript_setter_insert() { +// withEvery("count", in: 0 ..< 30) { count in +// withEvery("isShared", in: [false, true]) { isShared in +// withLifetimeTracking { tracker in +// let keys = tracker.instances(for: 0 ..< count) +// let values = tracker.instances(for: (0 ..< count).map { 100 + $0 }) +// var d: TreeDictionary, LifetimeTracked> = [:] +// withEvery("offset", in: 0 ..< count) { offset in +// withHiddenCopies(if: isShared, of: &d) { d in +// d[keys[offset]] = values[offset] +// withEvery("i", in: 0 ... offset) { i in +// let (k, v) = d[offset: i] +// expectEqual(k, keys[i]) +// expectEqual(v, values[i]) +// } +// } +// } +// } +// } +// } +// } + +// func test_subscript_setter_noop() { +// withEvery("count", in: 0 ..< 30) { count in +// withEvery("isShared", in: [false, true]) { isShared in +// withLifetimeTracking { tracker in +// var (d, keys, values) = tracker.shareableDictionary(keys: 0 ..< count) +// let key = tracker.instance(for: -1) +// withHiddenCopies(if: isShared, of: &d) { d in +// d[key] = nil +// } +// withEvery("i", in: 0 ..< count) { i in +// let (k, v) = d[offset: i] +// expectEqual(k, keys[i]) +// expectEqual(v, values[i]) +// } +// } +// } +// } +// } + + // func test_subscript_modify_update() { + // withEvery("count", in: 0 ..< 30) { count in + // withEvery("offset", in: 0 ..< count) { offset in + // withEvery("isShared", in: [false, true]) { isShared in + // withLifetimeTracking { tracker in + // var (d, keys, values) = tracker.shareableDictionary(keys: 0 ..< count) + // let replacement = tracker.instance(for: -1) + // withHiddenCopies(if: isShared, of: &d) { d in + // mutate(&d[keys[offset]]) { $0 = replacement } + // values[offset] = replacement + // withEvery("i", in: 0 ..< count) { i in + // let (k, v) = d[offset: i] + // expectEqual(k, keys[i]) + // expectEqual(v, values[i]) + // } + // } + // } + // } + // } + // } + // } + + // func test_subscript_modify_remove() { + // withEvery("count", in: 0 ..< 30) { count in + // withEvery("offset", in: 0 ..< count) { offset in + // withEvery("isShared", in: [false, true]) { isShared in + // withLifetimeTracking { tracker in + // var (d, keys, values) = tracker.shareableDictionary(keys: 0 ..< count) + // withHiddenCopies(if: isShared, of: &d) { d in + // let key = keys[offset] + // mutate(&d[key]) { v in + // expectEqual(v, values[offset]) + // v = nil + // } + // keys.remove(at: offset) + // values.remove(at: offset) + // withEvery("i", in: 0 ..< count - 1) { i in + // let (k, v) = d[offset: i] + // expectEqual(k, keys[i]) + // expectEqual(v, values[i]) + // } + // } + // } + // } + // } + // } + // } + + // func test_subscript_modify_insert() { + // withEvery("count", in: 0 ..< 30) { count in + // withEvery("isShared", in: [false, true]) { isShared in + // withLifetimeTracking { tracker in + // let keys = tracker.instances(for: 0 ..< count) + // let values = tracker.instances(for: (0 ..< count).map { 100 + $0 }) + // var d: TreeDictionary, LifetimeTracked> = [:] + // withEvery("offset", in: 0 ..< count) { offset in + // withHiddenCopies(if: isShared, of: &d) { d in + // mutate(&d[keys[offset]]) { v in + // expectNil(v) + // v = values[offset] + // } + // expectEqual(d.count, offset + 1) + // withEvery("i", in: 0 ... offset) { i in + // let (k, v) = d[offset: i] + // expectEqual(k, keys[i]) + // expectEqual(v, values[i]) + // } + // } + // } + // } + // } + // } + // } + + // func test_subscript_modify_noop() { + // withEvery("count", in: 0 ..< 30) { count in + // withEvery("isShared", in: [false, true]) { isShared in + // withLifetimeTracking { tracker in + // var (d, keys, values) = tracker.shareableDictionary(keys: 0 ..< count) + // let key = tracker.instance(for: -1) + // withHiddenCopies(if: isShared, of: &d) { d in + // mutate(&d[key]) { v in + // expectNil(v) + // v = nil + // } + // } + // withEvery("i", in: 0 ..< count) { i in + // let (k, v) = d[offset: i] + // expectEqual(k, keys[i]) + // expectEqual(v, values[i]) + // } + // } + // } + // } + // } + + // func test_defaulted_subscript_getter() { + // withEvery("count", in: 0 ..< 30) { count in + // withEvery("isShared", in: [false, true]) { isShared in + // withLifetimeTracking { tracker in + // let (d, keys, values) = tracker.shareableDictionary(keys: 0 ..< count) + // let fallback = tracker.instance(for: -1) + // withEvery("offset", in: 0 ..< count) { offset in + // let key = keys[offset] + // expectEqual(d[key, default: fallback], values[offset]) + // } + // expectEqual( + // d[tracker.instance(for: -1), default: fallback], + // fallback) + // expectEqual( + // d[tracker.instance(for: count), default: fallback], + // fallback) + // } + // } + // } + // } + + // func test_defaulted_subscript_modify_update() { + // withEvery("count", in: 0 ..< 30) { count in + // withEvery("offset", in: 0 ..< count) { offset in + // withEvery("isShared", in: [false, true]) { isShared in + // withLifetimeTracking { tracker in + // var (d, keys, values) = tracker.shareableDictionary(keys: 0 ..< count) + // let replacement = tracker.instance(for: -1) + // let fallback = tracker.instance(for: -1) + // withHiddenCopies(if: isShared, of: &d) { d in + // let key = keys[offset] + // mutate(&d[key, default: fallback]) { v in + // expectEqual(v, values[offset]) + // v = replacement + // } + // values[offset] = replacement + // withEvery("i", in: 0 ..< count) { i in + // let (k, v) = d[offset: i] + // expectEqual(k, keys[i]) + // expectEqual(v, values[i]) + // } + // } + // } + // } + // } + // } + // } + + // func test_defaulted_subscript_modify_insert() { + // withEvery("count", in: 0 ..< 30) { count in + // withEvery("isShared", in: [false, true]) { isShared in + // withLifetimeTracking { tracker in + // let keys = tracker.instances(for: 0 ..< count) + // let values = tracker.instances(for: (0 ..< count).map { 100 + $0 }) + // var d: TreeDictionary, LifetimeTracked> = [:] + // let fallback = tracker.instance(for: -1) + // withEvery("offset", in: 0 ..< count) { offset in + // withHiddenCopies(if: isShared, of: &d) { d in + // let key = keys[offset] + // mutate(&d[key, default: fallback]) { v in + // expectEqual(v, fallback) + // v = values[offset] + // } + // expectEqual(d.count, offset + 1) + // withEvery("i", in: 0 ... offset) { i in + // let (k, v) = d[offset: i] + // expectEqual(k, keys[i]) + // expectEqual(v, values[i]) + // } + // } + // } + // } + // } + // } + // } + + // func test_updateValue_forKey_update() { + // withEvery("count", in: 0 ..< 30) { count in + // withEvery("offset", in: 0 ..< count) { offset in + // withEvery("isShared", in: [false, true]) { isShared in + // withLifetimeTracking { tracker in + // var (d, keys, values) = tracker.shareableDictionary(keys: 0 ..< count) + // let replacement = tracker.instance(for: -1) + // withHiddenCopies(if: isShared, of: &d) { d in + // let key = keys[offset] + // let old = d.updateValue(replacement, forKey: key) + // expectEqual(old, values[offset]) + // values[offset] = replacement + // withEvery("i", in: 0 ..< count) { i in + // let (k, v) = d[offset: i] + // expectEqual(k, keys[i]) + // expectEqual(v, values[i]) + // } + // } + // } + // } + // } + // } + // } + + // func test_updateValue_forKey_insert() { + // withEvery("count", in: 0 ..< 30) { count in + // withEvery("isShared", in: [false, true]) { isShared in + // withLifetimeTracking { tracker in + // let keys = tracker.instances(for: 0 ..< count) + // let values = tracker.instances(for: (0 ..< count).map { 100 + $0 }) + // var d: TreeDictionary, LifetimeTracked> = [:] + // withEvery("offset", in: 0 ..< count) { offset in + // withHiddenCopies(if: isShared, of: &d) { d in + // let key = keys[offset] + // let old = d.updateValue(values[offset], forKey: key) + // expectNil(old) + // expectEqual(d.count, offset + 1) + // withEvery("i", in: 0 ... offset) { i in + // let (k, v) = d[offset: i] + // expectEqual(k, keys[i]) + // expectEqual(v, values[i]) + // } + // } + // } + // } + // } + // } + // } + + // func test_updateValue_forKey_insertingAt_update() { + // withEvery("count", in: 0 ..< 30) { count in + // withEvery("offset", in: 0 ..< count) { offset in + // withEvery("isShared", in: [false, true]) { isShared in + // withLifetimeTracking { tracker in + // var (d, keys, values) = tracker.shareableDictionary(keys: 0 ..< count) + // let replacement = tracker.instance(for: -1) + // withHiddenCopies(if: isShared, of: &d) { d in + // let key = keys[offset] + // let (old, index) = + // d.updateValue(replacement, forKey: key, insertingAt: 0) + // expectEqual(old, values[offset]) + // expectEqual(index, offset) + // values[offset] = replacement + // withEvery("i", in: 0 ..< count) { i in + // let (k, v) = d[offset: i] + // expectEqual(k, keys[i]) + // expectEqual(v, values[i]) + // } + // } + // } + // } + // } + // } + // } + + // func test_updateValue_forKey_insertingAt_insert() { + // withEvery("count", in: 0 ..< 30) { count in + // withEvery("isShared", in: [false, true]) { isShared in + // withLifetimeTracking { tracker in + // let keys = tracker.instances(for: 0 ..< count) + // let values = tracker.instances(for: (0 ..< count).map { 100 + $0 }) + // var d: TreeDictionary, LifetimeTracked> = [:] + // withEvery("offset", in: 0 ..< count) { offset in + // withHiddenCopies(if: isShared, of: &d) { d in + // let key = keys[count - 1 - offset] + // let value = values[count - 1 - offset] + // let (old, index) = + // d.updateValue(value, forKey: key, insertingAt: 0) + // expectNil(old) + // expectEqual(index, 0) + // expectEqual(d.count, offset + 1) + // withEvery("i", in: 0 ... offset) { i in + // let (k, v) = d[offset: i] + // expectEqual(k, keys[count - 1 - offset + i]) + // expectEqual(v, values[count - 1 - offset + i]) + // } + // } + // } + // } + // } + // } + // } + + // func test_modifyValue_forKey_default_closure_update() { + // withEvery("count", in: 0 ..< 30) { count in + // withEvery("offset", in: 0 ..< count) { offset in + // withEvery("isShared", in: [false, true]) { isShared in + // withLifetimeTracking { tracker in + // var (d, keys, values) = tracker.shareableDictionary(keys: 0 ..< count) + // let replacement = tracker.instance(for: -1) + // let fallback = tracker.instance(for: -2) + // withHiddenCopies(if: isShared, of: &d) { d in + // let key = keys[offset] + // d.modifyValue(forKey: key, default: fallback) { value in + // expectEqual(value, values[offset]) + // value = replacement + // } + // values[offset] = replacement + // withEvery("i", in: 0 ..< count) { i in + // let (k, v) = d[offset: i] + // expectEqual(k, keys[i]) + // expectEqual(v, values[i]) + // } + // } + // } + // } + // } + // } + // } + + // func test_modifyValue_forKey_default_closure_insert() { + // withEvery("count", in: 0 ..< 30) { count in + // withEvery("isShared", in: [false, true]) { isShared in + // withLifetimeTracking { tracker in + // let keys = tracker.instances(for: 0 ..< count) + // let values = tracker.instances(for: (0 ..< count).map { 100 + $0 }) + // var d: TreeDictionary, LifetimeTracked> = [:] + // let fallback = tracker.instance(for: -2) + // withEvery("offset", in: 0 ..< count) { offset in + // withHiddenCopies(if: isShared, of: &d) { d in + // let key = keys[offset] + // d.modifyValue(forKey: key, default: fallback) { value in + // expectEqual(value, fallback) + // value = values[offset] + // } + // expectEqual(d.count, offset + 1) + // withEvery("i", in: 0 ... offset) { i in + // let (k, v) = d[offset: i] + // expectEqual(k, keys[i]) + // expectEqual(v, values[i]) + // } + // } + // } + // } + // } + // } + // } + + // func test_modifyValue_forKey_insertingDefault_at_closure_update() { + // withEvery("count", in: 0 ..< 30) { count in + // withEvery("offset", in: 0 ..< count) { offset in + // withEvery("isShared", in: [false, true]) { isShared in + // withLifetimeTracking { tracker in + // var (d, keys, values) = tracker.shareableDictionary(keys: 0 ..< count) + // let replacement = tracker.instance(for: -1) + // let fallback = tracker.instance(for: -2) + // withHiddenCopies(if: isShared, of: &d) { d in + // let key = keys[offset] + // let value = values[offset] + // d.modifyValue(forKey: key, insertingDefault: fallback, at: 0) { v in + // expectEqual(v, value) + // v = replacement + // } + // values[offset] = replacement + // withEvery("i", in: 0 ..< count) { i in + // let (k, v) = d[offset: i] + // expectEqual(k, keys[i]) + // expectEqual(v, values[i]) + // } + // } + // } + // } + // } + // } + // } + + // func test_modifyValue_forKey_insertingDefault_at_closure_insert() { + // withEvery("count", in: 0 ..< 30) { count in + // withEvery("isShared", in: [false, true]) { isShared in + // withLifetimeTracking { tracker in + // let keys = tracker.instances(for: 0 ..< count) + // let values = tracker.instances(for: (0 ..< count).map { 100 + $0 }) + // var d: TreeDictionary, LifetimeTracked> = [:] + // let fallback = tracker.instance(for: -2) + // withEvery("offset", in: 0 ..< count) { offset in + // withHiddenCopies(if: isShared, of: &d) { d in + // let key = keys[count - 1 - offset] + // let value = values[count - 1 - offset] + // d.modifyValue(forKey: key, insertingDefault: fallback, at: 0) { v in + // expectEqual(v, fallback) + // v = value + // } + // expectEqual(d.count, offset + 1) + // withEvery("i", in: 0 ... offset) { i in + // let (k, v) = d[offset: i] + // expectEqual(k, keys[count - 1 - offset + i]) + // expectEqual(v, values[count - 1 - offset + i]) + // } + // } + // } + // } + // } + // } + // } + + // func test_removeValue_forKey() { + // withEvery("count", in: 0 ..< 30) { count in + // withEvery("offset", in: 0 ..< count) { offset in + // withEvery("isShared", in: [false, true]) { isShared in + // withLifetimeTracking { tracker in + // var (d, keys, values) = tracker.shareableDictionary(keys: 0 ..< count) + // withHiddenCopies(if: isShared, of: &d) { d in + // let key = keys.remove(at: offset) + // let expected = values.remove(at: offset) + // let actual = d.removeValue(forKey: key) + // expectEqual(actual, expected) + // + // expectEqual(d.count, values.count) + // withEvery("i", in: 0 ..< values.count) { i in + // let (k, v) = d[offset: i] + // expectEqual(k, keys[i]) + // expectEqual(v, values[i]) + // } + // expectNil(d.removeValue(forKey: key)) + // } + // } + // } + // } + // } + // } + + // func test_merge_labeled_tuple() { + // var d: TreeDictionary = [ + // "one": 1, + // "two": 1, + // "three": 1, + // ] + // + // let items: KeyValuePairs = [ + // "one": 1, + // "one": 1, + // "three": 1, + // "four": 1, + // "one": 1, + // ] + // + // d.merge(items, uniquingKeysWith: +) + // + // expectEqualElements(d, [ + // "one": 4, + // "two": 1, + // "three": 2, + // "four": 1, + // ] as KeyValuePairs) + // } + + // func test_merge_unlabeled_tuple() { + // var d: TreeDictionary = [ + // "one": 1, + // "two": 1, + // "three": 1, + // ] + // + // let items: [(String, Int)] = [ + // ("one", 1), + // ("one", 1), + // ("three", 1), + // ("four", 1), + // ("one", 1), + // ] + // + // d.merge(items, uniquingKeysWith: +) + // + // expectEqualElements(d, [ + // "one": 4, + // "two": 1, + // "three": 2, + // "four": 1, + // ] as KeyValuePairs) + // } + + // func test_merging_labeled_tuple() { + // let d: TreeDictionary = [ + // "one": 1, + // "two": 1, + // "three": 1, + // ] + // + // let items: KeyValuePairs = [ + // "one": 1, + // "one": 1, + // "three": 1, + // "four": 1, + // "one": 1, + // ] + // + // let d2 = d.merging(items, uniquingKeysWith: +) + // + // expectEqualElements(d, [ + // "one": 1, + // "two": 1, + // "three": 1, + // ] as KeyValuePairs) + // + // expectEqualElements(d2, [ + // "one": 4, + // "two": 1, + // "three": 2, + // "four": 1, + // ] as KeyValuePairs) + // } + + // func test_merging_unlabeled_tuple() { + // let d: TreeDictionary = [ + // "one": 1, + // "two": 1, + // "three": 1, + // ] + // + // let items: [(String, Int)] = [ + // ("one", 1), + // ("one", 1), + // ("three", 1), + // ("four", 1), + // ("one", 1), + // ] + // + // let d2 = d.merging(items, uniquingKeysWith: +) + // + // expectEqualElements(d, [ + // "one": 1, + // "two": 1, + // "three": 1, + // ] as KeyValuePairs) + // + // expectEqualElements(d2, [ + // "one": 4, + // "two": 1, + // "three": 2, + // "four": 1, + // ] as KeyValuePairs) + // } + + // func test_filter() { + // let items = (0 ..< 100).map { ($0, 100 * $0) } + // let d = TreeDictionary(uniqueKeysWithValues: items) + // + // var c = 0 + // let d2 = d.filter { item in + // c += 1 + // expectEqual(item.value, 100 * item.key) + // return item.key.isMultiple(of: 2) + // } + // expectEqual(c, 100) + // expectEqualElements(d, items) + // + // expectEqualElements(d2, (0 ..< 50).compactMap { key in + // return (key: 2 * key, value: 200 * key) + // }) + // } + + // func test_mapValues() { + // let items = (0 ..< 100).map { ($0, 100 * $0) } + // let d = TreeDictionary(uniqueKeysWithValues: items) + // + // var c = 0 + // let d2 = d.mapValues { value -> String in + // c += 1 + // expectTrue(value.isMultiple(of: 100)) + // return "\(value)" + // } + // expectEqual(c, 100) + // expectEqualElements(d, items) + // + // expectEqualElements(d2, (0 ..< 100).compactMap { key in + // (key: key, value: "\(100 * key)") + // }) + // } + + // func test_compactMapValue() { + // let items = (0 ..< 100).map { ($0, 100 * $0) } + // let d = TreeDictionary(uniqueKeysWithValues: items) + // + // var c = 0 + // let d2 = d.compactMapValues { value -> String? in + // c += 1 + // guard value.isMultiple(of: 200) else { return nil } + // expectTrue(value.isMultiple(of: 100)) + // return "\(value)" + // } + // expectEqual(c, 100) + // expectEqualElements(d, items) + // + // expectEqualElements(d2, (0 ..< 50).map { key in + // (key: 2 * key, value: "\(200 * key)") + // }) + // } + + // func test_removeAll() { + // withEvery("count", in: 0 ..< 30) { count in + // withEvery("isShared", in: [false, true]) { isShared in + // withLifetimeTracking { tracker in + // var (d, _, _) = tracker.shareableDictionary(keys: 0 ..< count) + // withHiddenCopies(if: isShared, of: &d) { d in + // d.removeAll() + // expectEqual(d.keys.__unstable.scale, 0) + // expectEqualElements(d, []) + // } + // } + // } + // } + // } + + // func test_remove_at() { + // withEvery("count", in: 0 ..< 30) { count in + // withEvery("offset", in: 0 ..< count) { offset in + // withEvery("isShared", in: [false, true]) { isShared in + // withLifetimeTracking { tracker in + // var (d, keys, values) = tracker.shareableDictionary(keys: 0 ..< count) + // withHiddenCopies(if: isShared, of: &d) { d in + // let actual = d.remove(at: offset) + // let expectedKey = keys.remove(at: offset) + // let expectedValue = values.remove(at: offset) + // expectEqual(actual.key, expectedKey) + // expectEqual(actual.value, expectedValue) + // expectEqualElements( + // d, + // zip(keys, values).map { (key: $0.0, value: $0.1) }) + // } + // } + // } + // } + // } + // } + + // func test_removeSubrange() { + // withEvery("count", in: 0 ..< 30) { count in + // withEveryRange("range", in: 0 ..< count) { range in + // withEvery("isShared", in: [false, true]) { isShared in + // withLifetimeTracking { tracker in + // var (d, keys, values) = tracker.shareableDictionary(keys: 0 ..< count) + // withHiddenCopies(if: isShared, of: &d) { d in + // d.removeSubrange(range) + // keys.removeSubrange(range) + // values.removeSubrange(range) + // expectEqualElements( + // d, + // zip(keys, values).map { (key: $0.0, value: $0.1) }) + // } + // } + // } + // } + // } + // } + + // func test_removeSubrange_rangeExpression() { + // let d = TreeDictionary(uniqueKeys: 0 ..< 30, values: 100 ..< 130) + // let item = (0 ..< 30).map { (key: $0, value: 100 + $0) } + // + // var d1 = d + // d1.removeSubrange(...10) + // expectEqualElements(d1, item[11...]) + // + // var d2 = d + // d2.removeSubrange(..<10) + // expectEqualElements(d2, item[10...]) + // + // var d3 = d + // d3.removeSubrange(10...) + // expectEqualElements(d3, item[0 ..< 10]) + // } + // + // func test_removeLast() { + // withEvery("isShared", in: [false, true]) { isShared in + // withLifetimeTracking { tracker in + // var (d, keys, values) = tracker.shareableDictionary(keys: 0 ..< 30) + // withEvery("i", in: 0 ..< d.count) { i in + // withHiddenCopies(if: isShared, of: &d) { d in + // let actual = d.removeLast() + // let expectedKey = keys.removeLast() + // let expectedValue = values.removeLast() + // expectEqual(actual.key, expectedKey) + // expectEqual(actual.value, expectedValue) + // expectEqualElements( + // d, + // zip(keys, values).map { (key: $0.0, value: $0.1) }) + // } + // } + // } + // } + // } + + // func test_removeFirst() { + // withEvery("isShared", in: [false, true]) { isShared in + // withLifetimeTracking { tracker in + // var (d, keys, values) = tracker.shareableDictionary(keys: 0 ..< 30) + // withEvery("i", in: 0 ..< d.count) { i in + // withHiddenCopies(if: isShared, of: &d) { d in + // let actual = d.removeFirst() + // let expectedKey = keys.removeFirst() + // let expectedValue = values.removeFirst() + // expectEqual(actual.key, expectedKey) + // expectEqual(actual.value, expectedValue) + // expectEqualElements( + // d, + // zip(keys, values).map { (key: $0.0, value: $0.1) }) + // } + // } + // } + // } + // } + + // func test_removeLast_n() { + // withEvery("count", in: 0 ..< 30) { count in + // withEvery("suffix", in: 0 ..< count) { suffix in + // withEvery("isShared", in: [false, true]) { isShared in + // withLifetimeTracking { tracker in + // var (d, keys, values) = tracker.shareableDictionary(keys: 0 ..< count) + // withHiddenCopies(if: isShared, of: &d) { d in + // d.removeLast(suffix) + // keys.removeLast(suffix) + // values.removeLast(suffix) + // expectEqualElements( + // d, + // zip(keys, values).map { (key: $0.0, value: $0.1) }) + // } + // } + // } + // } + // } + // } + + // func test_removeFirst_n() { + // withEvery("count", in: 0 ..< 30) { count in + // withEvery("prefix", in: 0 ..< count) { prefix in + // withEvery("isShared", in: [false, true]) { isShared in + // withLifetimeTracking { tracker in + // var (d, keys, values) = tracker.shareableDictionary(keys: 0 ..< count) + // withHiddenCopies(if: isShared, of: &d) { d in + // d.removeFirst(prefix) + // keys.removeFirst(prefix) + // values.removeFirst(prefix) + // expectEqualElements( + // d, + // zip(keys, values).map { (key: $0.0, value: $0.1) }) + // } + // } + // } + // } + // } + // } + + // func test_removeAll_where() { + // withEvery("count", in: 0 ..< 30) { count in + // withEvery("n", in: [2, 3, 4]) { n in + // withEvery("isShared", in: [false, true]) { isShared in + // withLifetimeTracking { tracker in + // var (d, keys, values) = tracker.shareableDictionary(keys: 0 ..< count) + // var items = zip(keys, values).map { (key: $0.0, value: $0.1) } + // withHiddenCopies(if: isShared, of: &d) { d in + // d.removeAll(where: { !$0.key.payload.isMultiple(of: n) }) + // items.removeAll(where: { !$0.key.payload.isMultiple(of: n) }) + // expectEqualElements(d, items) + // } + // } + // } + // } + // } + // } + + func test_Sequence() { + withEvery("count", in: 0 ..< 30) { count in + withLifetimeTracking { tracker in + let (d, keys, values) = tracker.shareableDictionary(keys: 0 ..< count) + let items = zip(keys, values).map { (key: $0.0, value: $0.1) } + checkSequence( + { d.sorted(by: <) }, + expectedContents: items, + by: { $0.key == $1.0 && $0.value == $1.1 }) + } + } + } + +} diff --git a/Tests/HashTreeCollectionsTests/TreeDictionary.Keys Tests.swift b/Tests/HashTreeCollectionsTests/TreeDictionary.Keys Tests.swift new file mode 100644 index 000000000..71903642f --- /dev/null +++ b/Tests/HashTreeCollectionsTests/TreeDictionary.Keys Tests.swift @@ -0,0 +1,185 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +import XCTest +#if COLLECTIONS_SINGLE_MODULE +import Collections +#else +import _CollectionsTestSupport +import HashTreeCollections +#endif + +class TreeDictionaryKeysTests: CollectionTestCase { + func test_BidirectionalCollection_fixtures() { + withEachFixture { fixture in + withLifetimeTracking { tracker in + let (d, ref) = tracker.shareableDictionary(for: fixture) + let k = ref.map { $0.key } + checkCollection(d.keys, expectedContents: k, by: ==) + _checkBidirectionalCollection_indexOffsetBy( + d.keys, expectedContents: k, by: ==) + } + } + } + + func test_descriptions() { + let d: TreeDictionary = [ + "a": 1, + "b": 2 + ] + + if d.first!.key == "a" { + expectEqual(d.keys.description, #"["a", "b"]"#) + expectEqual(d.keys.debugDescription, #"["a", "b"]"#) + } else { + expectEqual(d.keys.description, #"["b", "a"]"#) + expectEqual(d.keys.debugDescription, #"["b", "a"]"#) + } + } + + func test_contains() { + withEverySubset("a", of: testItems) { a in + let x = TreeDictionary( + uniqueKeysWithValues: a.lazy.map { ($0, 2 * $0.identity) }) + let u = Set(a) + + func checkSequence( + _ items: S, + _ value: S.Element + ) -> Bool + where S.Element: Equatable { + items.contains(value) + } + + withEvery("key", in: testItems) { key in + expectEqual(x.keys.contains(key), u.contains(key)) + expectEqual(checkSequence(x.keys, key), u.contains(key)) + } + } + } + + func test_intersection_exhaustive() { + withEverySubset("a", of: testItems) { a in + let x = TreeDictionary( + uniqueKeysWithValues: a.lazy.map { ($0, 2 * $0.identity) }) + let u = Set(a) + expectEqualSets(x.keys.intersection(x.keys), u) + withEverySubset("b", of: testItems) { b in + let y = TreeDictionary( + uniqueKeysWithValues: b.lazy.map { ($0, -$0.identity - 1) }) + let v = Set(b) + let z = TreeSet(b) + + let reference = u.intersection(v) + + expectEqualSets(x.keys.intersection(y.keys), reference) + expectEqualSets(x.keys.intersection(z), reference) + } + } + } + + func test_subtracting_exhaustive() { + withEverySubset("a", of: testItems) { a in + let x = TreeDictionary( + uniqueKeysWithValues: a.lazy.map { ($0, 2 * $0.identity) }) + let u = Set(a) + expectEqualSets(x.keys.subtracting(x.keys), []) + withEverySubset("b", of: testItems) { b in + let y = TreeDictionary( + uniqueKeysWithValues: b.lazy.map { ($0, -$0.identity - 1) }) + let v = Set(b) + let z = TreeSet(b) + + let reference = u.subtracting(v) + + expectEqualSets(x.keys.subtracting(y.keys), reference) + expectEqualSets(x.keys.subtracting(z), reference) + } + } + } + + func test_isEqual_exhaustive() { + withEverySubset("a", of: testItems) { a in + let x = TreeDictionary( + uniqueKeysWithValues: a.lazy.map { ($0, 2 * $0.identity) }) + let u = Set(a) + expectEqualSets(x.keys, u) + withEverySubset("b", of: testItems) { b in + let y = TreeDictionary( + uniqueKeysWithValues: b.lazy.map { ($0, -$0.identity - 1) }) + let v = Set(b) + expectEqualSets(y.keys, v) + + let reference = u == v + print(reference) + + expectEqual(x.keys == y.keys, reference) + } + } + } + + func test_Hashable() { + let strings: [[[String]]] = [ + [ + [] + ], + [ + ["a"] + ], + [ + ["b"] + ], + [ + ["c"] + ], + [ + ["d"] + ], + [ + ["e"] + ], + [ + ["f"], ["f"], + ], + [ + ["g"], ["g"], + ], + [ + ["h"], ["h"], + ], + [ + ["i"], ["i"], + ], + [ + ["j"], ["j"], + ], + [ + ["a", "b"], ["b", "a"], + ], + [ + ["a", "d"], ["d", "a"], + ], + [ + ["a", "b", "c"], ["a", "c", "b"], + ["b", "a", "c"], ["b", "c", "a"], + ["c", "a", "b"], ["c", "b", "a"], + ], + [ + ["a", "d", "e"], ["a", "e", "d"], + ["d", "a", "e"], ["d", "e", "a"], + ["e", "a", "d"], ["e", "d", "a"], + ], + ] + let keys = strings.map { $0.map { TreeDictionary(uniqueKeysWithValues: $0.map { ($0, Int.random(in: 1...100)) }).keys }} + checkHashable(equivalenceClasses: keys) + } + +} diff --git a/Tests/HashTreeCollectionsTests/TreeDictionary.Values Tests.swift b/Tests/HashTreeCollectionsTests/TreeDictionary.Values Tests.swift new file mode 100644 index 000000000..70cc4f52f --- /dev/null +++ b/Tests/HashTreeCollectionsTests/TreeDictionary.Values Tests.swift @@ -0,0 +1,45 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +import XCTest +#if COLLECTIONS_SINGLE_MODULE +import Collections +#else +import _CollectionsTestSupport +import HashTreeCollections +#endif + +class TreeDictionaryValuesTests: CollectionTestCase { + func test_BidirectionalCollection_fixtures() { + withEachFixture { fixture in + withLifetimeTracking { tracker in + let (d, ref) = tracker.shareableDictionary(for: fixture) + let v = ref.map { $0.value } + checkCollection(d.values, expectedContents: v, by: ==) + _checkBidirectionalCollection_indexOffsetBy( + d.values, expectedContents: v, by: ==) + } + } + } + + func test_descriptions() { + let d: TreeDictionary = [ + "a": 1, + "b": 2 + ] + + if d.first!.key == "a" { + expectEqual(d.values.description, "[1, 2]") + } else { + expectEqual(d.values.description, "[2, 1]") + } + } +} diff --git a/Tests/HashTreeCollectionsTests/TreeHashedCollections Fixtures.swift b/Tests/HashTreeCollectionsTests/TreeHashedCollections Fixtures.swift new file mode 100644 index 000000000..f2ee7b2b7 --- /dev/null +++ b/Tests/HashTreeCollectionsTests/TreeHashedCollections Fixtures.swift @@ -0,0 +1,471 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if COLLECTIONS_SINGLE_MODULE +import Collections +#else +import _CollectionsTestSupport +import HashTreeCollections +#endif + +/// A set of items whose subsets will produce a bunch of interesting test +/// cases. +/// +/// Note: Try to keep this short. Every new item added here will quadruple +/// testing costs. +let testItems: [RawCollider] = { + var testItems = [ + RawCollider(1, "A"), + RawCollider(2, "B"), + RawCollider(3, "ACA"), + RawCollider(4, "ACB"), + RawCollider(5, "ACAD"), + RawCollider(6, "ACAD"), + ] + if MemoryLayout.size == 8 { + // Cut testing workload down a bit on 32-bit systems. In practice a 32-bit Int + // usually means we're running on a watchOS device (arm64_32), and those are relatively slow + // to run these. + testItems += [ + RawCollider(7, "ACAEB"), + RawCollider(8, "ACAEB"), + RawCollider(9, "ACAEB"), + ] + } + #if false // Enable for even deeper testing + testItems += [ + RawCollider(10, "ACAEB"), + RawCollider(11, "ADAD"), + RawCollider(12, "ACC"), + ] + #endif + return testItems +}() + +extension LifetimeTracker { + func shareableDictionary( + for items: C, + keyTransform: (C.Element) -> Key, + valueTransform: (C.Element) -> Value + ) -> TreeDictionary, LifetimeTracked> { + let keys = instances(for: items, by: keyTransform) + let values = instances(for: items, by: valueTransform) + return TreeDictionary(uniqueKeysWithValues: zip(keys, values)) + } + + func shareableDictionary( + for keys: C, + by valueTransform: (C.Element) -> Value + ) -> TreeDictionary, LifetimeTracked> + where C.Element: Hashable { + let k = instances(for: keys) + let v = instances(for: keys, by: valueTransform) + return TreeDictionary(uniqueKeysWithValues: zip(k, v)) + } + + func dictionary( + for items: C, + keyTransform: (C.Element) -> Key, + valueTransform: (C.Element) -> Value + ) -> Dictionary, LifetimeTracked> { + Dictionary( + uniqueKeysWithValues: zip( + instances(for: items, by: keyTransform), + instances(for: items, by: valueTransform))) + } + + func dictionary( + for keys: C, + by valueTransform: (C.Element) -> Value + ) -> Dictionary, LifetimeTracked> + where C.Element: Hashable { + Dictionary( + uniqueKeysWithValues: zip( + instances(for: keys), + instances(for: keys, by: valueTransform))) + } + +} + +/// A list of example trees to use while testing persistent hash maps. +/// +/// Each example has a name and a list of path specifications or collisions. +/// +/// A path spec is an ASCII `String` representing the hash of a key/value pair, +/// a.k.a a path in the prefix tree. Each character in the string identifies +/// a bucket index of a tree node, starting from the root. +/// (Encoded in radix 32, with digits 0-9 followed by letters. In order to +/// prepare for a potential reduction in the maximum node size, it is best to +/// keep the digits in the range 0-F.) The prefix tree's depth is limited by the +/// size of hash values. +/// +/// For example, the string "5A" corresponds to a key/value pair +/// that is in bucket 10 of a second-level node that is found at bucket 5 +/// of the root node. +/// +/// Hash collisions are modeled by strings of the form `*` where +/// `` is a path specification, and `` is the number of times that +/// path needs to be repeated. (To implement the collisions, the path is +/// extended with an infinite number of zeroes.) +/// +/// To generate input data from these fixtures, the items are sorted into +/// the same order as we expect a preorder walk would visit them in the +/// resulting tree. The resulting ordering is then used to insert key/value +/// pairs into the map, with sequentially increasing keys. +let fixtures: [Fixture] = { + var fixtures: Array = [] + + enum FixtureFlavor { + case any + case small // 32-bit platforms + case large // 64-bit platforms + + func isAllowed() -> Bool { + let reject: FixtureFlavor +#if arch(i386) || arch(arm64_32) + reject = .large +#else + precondition(MemoryLayout.size == 8, "Unknown platform") + reject = .small +#endif + return self != reject + } + } + + func add(_ title: String, flavor: FixtureFlavor = .any, _ contents: [String]) { + // Ignore unsupported flavors + guard flavor.isAllowed() else { return } + fixtures.append(Fixture(title: title, contents: contents)) + } + + add("empty", []) + add("single-item", ["A"]) + add("single-node", [ + "0", + "1", + "2", + "3", + "4", + "A", + "B", + "C", + "D", + ]) + add("few-collisions", [ + "42*5" + ]) + add("many-collisions", [ + "42*40" + ]) + + add("few-different-collisions", [ + "1*3", + "21*3", + "22*3", + "3*3", + ]) + + add("everything-on-the-2nd-level", [ + "00", "01", "02", "03", "04", + "10", "11", "12", "13", "14", + "20", "21", "22", "23", "24", + "30", "31", "32", "33", "34", + ]) + add("two-levels-mixed", [ + "00", "01", + "2", + "30", "33", + "4", + "5", + "60", "61", "66", + "71", "75", "77", + "8", + "94", "98", "9A", + "A3", "A4", + ]) + add("vee", [ + "11110", + "11115", + "11119", + "1111B", + "66664", + "66667", + ]) + + add("fork", [ + "31110", + "31115", + "31119", + "3111B", + "36664", + "36667", + ]) + add("chain-left", [ + "0", + "10", + "110", + "1110", + "11110", + "11111", + ]) + add("chain-right", [ + "1", + "01", + "001", + "0001", + "00001", + "000001", + ]) + add("expansion0", [ + "000001*3", + "0001", + ]) + add("expansion1", [ + "000001*3", + "01", + "0001", + ]) + add("expansion2", [ + "111111*3", + "10", + "1110", + ]) + add("expansion3", [ + "01", + "0001", + "000001*3", + ]) + add("expansion4", [ + "10", + "1110", + "111111*3", + ]) + add("nested", flavor: .large, [ + "50", + "51", + "520", + "521", + "5220", + "5221", + "52220", + "52221", + "522220", + "522221", + "5222220", + "5222221", + "52222220", + "52222221", + "522222220", + "522222221", + "5222222220", + "5222222221", + "5222222222", + "5222222223", + "522222223", + "522222224", + "52222223", + "52222224", + "5222223", + "5222224", + "522223", + "522224", + "52223", + "52224", + "5223", + "5224", + "53", + "54", + ]) + add("deep", [ + "0", + + // Deeply nested children with only the leaf containing items + "123450", + "123451", + "123452", + "123453", + + "22", + "25", + ]) + return fixtures +}() + +struct Fixture { + let title: String + let itemsInIterationOrder: [RawCollider] + let itemsInInsertionOrder: [RawCollider] + + init(title: String, contents: [String]) { + self.title = title + + let maxDepth = TreeDictionary._maxDepth + + func normalized(_ path: String) -> String { + precondition(path.unicodeScalars.count < maxDepth) + let c = Swift.max(0, maxDepth - path.unicodeScalars.count) + return path.uppercased() + String(repeating: "0", count: c) + } + + var items: [(path: String, item: RawCollider)] = [] + var seen: Set = [] + for path in contents { + if let i = path.unicodeScalars.firstIndex(of: "*") { + // We need to extend the path of collisions with zeroes to + // make sure they sort correctly. + let p = String(path.unicodeScalars.prefix(upTo: i)) + guard let count = Int(path.suffix(from: i).dropFirst(), radix: 10) + else { fatalError("Invalid item: '\(path)'") } + let path = normalized(p) + let hash = Hash(path)! + for _ in 0 ..< count { + items.append((path, RawCollider(items.count, hash))) + } + } else { + let path = normalized(path) + let hash = Hash(path)! + items.append((path, RawCollider(items.count, hash))) + } + + if !seen.insert(path).inserted { + fatalError("Unexpected duplicate path: '\(path)'") + } + } + + var seenPrefixes: Set = [] + var collidingPrefixes: Set = [] + for p in items { + assert(p.path.count == maxDepth) + for i in p.path.indices { + let prefix = p.path[.. b.path[j] { return false } + a.path.formIndex(after: &i) + b.path.formIndex(after: &j) + } + precondition(i == a.path.endIndex && j == b.path.endIndex) + return false + } + + self.itemsInIterationOrder = items.map { $0.item } + } + + var count: Int { itemsInInsertionOrder.count } +} + +func withEachFixture( + _ label: String = "fixture", + body: (Fixture) -> Void +) { + for fixture in fixtures { + let entry = TestContext.current.push("\(label): \(fixture.title)") + defer { TestContext.current.pop(entry) } + + body(fixture) + } +} + +extension LifetimeTracker { + func shareableSet( + for fixture: Fixture, + with transform: (RawCollider) -> Element + ) -> ( + map: TreeSet>, + ref: [LifetimeTracked] + ) { + let ref = fixture.itemsInIterationOrder.map { key in + self.instance(for: transform(key)) + } + let ref2 = fixture.itemsInInsertionOrder.map { key in + self.instance(for: transform(key)) + } + return (TreeSet(ref2), ref) + } + + func shareableSet( + for fixture: Fixture + ) -> ( + map: TreeSet>, + ref: [LifetimeTracked] + ) { + shareableSet(for: fixture) { key in key } + } + + func shareableDictionary( + for fixture: Fixture, + keyTransform: (RawCollider) -> Key, + valueTransform: (RawCollider) -> Value + ) -> ( + map: TreeDictionary, LifetimeTracked>, + ref: [(key: LifetimeTracked, value: LifetimeTracked)] + ) { + typealias K = LifetimeTracked + typealias V = LifetimeTracked + + let ref: [(key: K, value: V)] = fixture.itemsInIterationOrder.map { item in + let key = keyTransform(item) + let value = valueTransform(item) + return (key: self.instance(for: key), value: self.instance(for: value)) + } + let ref2: [(key: K, value: V)] = fixture.itemsInInsertionOrder.map { item in + let key = keyTransform(item) + let value = valueTransform(item) + return (key: self.instance(for: key), value: self.instance(for: value)) + } + return (TreeDictionary(uniqueKeysWithValues: ref2), ref) + } + + func shareableDictionary( + for fixture: Fixture, + valueTransform: (Int) -> Value + ) -> ( + map: TreeDictionary, LifetimeTracked>, + ref: [(key: LifetimeTracked, value: LifetimeTracked)] + ) { + shareableDictionary( + for: fixture, + keyTransform: { $0 }, + valueTransform: { valueTransform($0.identity) }) + } + + func shareableDictionary( + for fixture: Fixture + ) -> ( + map: TreeDictionary, LifetimeTracked>, + ref: [(key: LifetimeTracked, value: LifetimeTracked)] + ) { + shareableDictionary(for: fixture) { $0 + 1000 } + } +} diff --git a/Tests/HashTreeCollectionsTests/TreeSet Tests.swift b/Tests/HashTreeCollectionsTests/TreeSet Tests.swift new file mode 100644 index 000000000..ce018b6e1 --- /dev/null +++ b/Tests/HashTreeCollectionsTests/TreeSet Tests.swift @@ -0,0 +1,789 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if COLLECTIONS_SINGLE_MODULE +import Collections +#else +import _CollectionsTestSupport +import HashTreeCollections +#endif + +extension TreeSet: SetAPIExtras {} + +class TreeSetTests: CollectionTestCase { + func test_init_empty() { + let set = TreeSet() + expectEqual(set.count, 0) + expectTrue(set.isEmpty) + expectEqualElements(set, []) + } + + func test_BidirectionalCollection_fixtures() { + withEachFixture { fixture in + withLifetimeTracking { tracker in + let (set, ref) = tracker.shareableSet(for: fixture) + checkCollection(set, expectedContents: ref, by: ==) + _checkBidirectionalCollection_indexOffsetBy( + set, expectedContents: ref, by: ==) + } + } + } + + func test_BidirectionalCollection_random100() { + let s = TreeSet(0 ..< 100) + let ref = Array(s) + checkCollection(s, expectedContents: ref) + _checkBidirectionalCollection_indexOffsetBy( + s, expectedContents: ref, by: ==) + } + + func test_basics() { + var set: TreeSet> = [] + + let a1 = HashableBox(1) + let a2 = HashableBox(1) + + var r = set.insert(a1) + expectTrue(r.inserted) + expectIdentical(r.memberAfterInsert, a1) + expectIdentical(set.first, a1) + expectTrue(set.contains(a1)) + expectTrue(set.contains(a2)) + + r = set.insert(a2) + expectFalse(r.inserted) + expectIdentical(r.memberAfterInsert, a1) + expectIdentical(set.first, a1) + expectTrue(set.contains(a1)) + expectTrue(set.contains(a2)) + + var old = set.update(with: a2) + expectIdentical(old, a1) + expectIdentical(set.first, a2) + expectTrue(set.contains(a1)) + expectTrue(set.contains(a2)) + + old = set.remove(a1) + expectIdentical(old, a2) + expectNil(set.first) + expectFalse(set.contains(a1)) + expectFalse(set.contains(a2)) + + old = set.update(with: a1) + expectNil(old) + expectIdentical(set.first, a1) + expectTrue(set.contains(a1)) + expectTrue(set.contains(a2)) + } + + func test_descriptions() { + let empty: TreeSet = [] + expectEqual(empty.description, "[]") + expectEqual(empty.debugDescription, "[]") + + let a: TreeSet = ["a"] + expectEqual(a.description, #"["a"]"#) + expectEqual(a.debugDescription, #"["a"]"#) + } + + func test_index_descriptions() { + let a: TreeSet = [ + RawCollider(1, "1"), + RawCollider(2, "21"), + RawCollider(3, "22"), + ] + let i = a.startIndex + expectEqual(i.description, "@[0]") + expectEqual(i.debugDescription, "@[0]") + + let j = a.index(i, offsetBy: 1) + expectEqual(j.description, "@.0[0]") + expectEqual(j.debugDescription, "@.0[0]") + + let k = a.index(j, offsetBy: 1) + expectEqual(k.description, "@.0[1]") + expectEqual(k.debugDescription, "@.0[1]") + + let end = a.endIndex + expectEqual(end.description, "@.end(1)") + expectEqual(end.debugDescription, "@.end(1)") + } + + func test_index_hashing() { + let s = TreeSet(0 ..< 100) + checkHashable(s.indices, equalityOracle: ==) + } + + func test_insert_fixtures() { + withEachFixture { fixture in + withEvery("isShared", in: [false, true]) { isShared in + withLifetimeTracking { tracker in + var s: TreeSet> = [] + var ref: Set> = [] + withEvery("i", in: 0 ..< fixture.count) { i in + withHiddenCopies(if: isShared, of: &s) { s in + let item = fixture.itemsInInsertionOrder[i] + let key1 = tracker.instance(for: item) + let r = s.insert(key1) + expectTrue(r.inserted) + expectEqual(r.memberAfterInsert, key1) + + let key2 = tracker.instance(for: item) + ref.insert(key2) + expectEqualSets(s, ref) + } + } + } + } + } + } + + func test_remove_at() { + withEachFixture { fixture in + withEvery("isShared", in: [false, true]) { isShared in + withLifetimeTracking { tracker in + withEvery("offset", in: 0 ..< fixture.count) { offset in + let f = tracker.shareableSet(for: fixture) + var s = f.map + var ref = Set(f.ref) + withHiddenCopies(if: isShared, of: &s) { s in + let i = s.index(s.startIndex, offsetBy: offset) + let old = s.remove(at: i) + expectNotNil(ref.remove(old)) + expectEqualSets(s, ref) + } + } + } + } + } + } + + func test_intersection_Self_basics() { + let a = RawCollider(1, "A") + let b = RawCollider(2, "B") + let c = RawCollider(3, "C") + + let s0: TreeSet = [] + let s1: TreeSet = [a, b] + let s2: TreeSet = [a, c] + + expectEqualSets(s0.intersection(s0), []) + expectEqualSets(s1.intersection(s0), []) + expectEqualSets(s0.intersection(s1), []) + + expectEqualSets(s1.intersection(s1), [a, b]) + expectEqualSets(s1.intersection(s2), [a]) + expectEqualSets(s2.intersection(s1), [a]) + + let ab = RawCollider(4, "AB") + let ac = RawCollider(5, "AC") + let s3: TreeSet = [ab, ac] + let s4: TreeSet = [a, ab] + expectEqualSets(s1.intersection(s3), []) + expectEqualSets(s2.intersection(s3), []) + expectEqualSets(s1.intersection(s4), [a]) + expectEqualSets(s2.intersection(s4), [a]) + expectEqualSets(s3.intersection(s1), []) + expectEqualSets(s3.intersection(s2), []) + expectEqualSets(s4.intersection(s1), [a]) + expectEqualSets(s4.intersection(s2), [a]) + + let ad = RawCollider(6, "AD") + let ae = RawCollider(7, "AE") + let s5: TreeSet = [a, ab, ad, ae] + expectEqualSets(s1.intersection(s5), [a]) + expectEqualSets(s2.intersection(s5), [a]) + expectEqualSets(s3.intersection(s5), [ab]) + expectEqualSets(s4.intersection(s5), [a, ab]) + + expectEqualSets(s5.intersection(s1), [a]) + expectEqualSets(s5.intersection(s2), [a]) + expectEqualSets(s5.intersection(s3), [ab]) + expectEqualSets(s5.intersection(s4), [a, ab]) + + let af1 = RawCollider(8, "AF") + let af2 = RawCollider(9, "AF") + let s6: TreeSet = [af1, af2] + expectEqualSets(s1.intersection(s6), []) + expectEqualSets(s2.intersection(s6), []) + expectEqualSets(s3.intersection(s6), []) + expectEqualSets(s4.intersection(s6), []) + expectEqualSets(s5.intersection(s6), []) + + expectEqualSets(s6.intersection(s1), []) + expectEqualSets(s6.intersection(s2), []) + expectEqualSets(s6.intersection(s3), []) + expectEqualSets(s6.intersection(s4), []) + expectEqualSets(s6.intersection(s5), []) + + let af3 = RawCollider(10, "AF") + let s7: TreeSet = [af1, af3] + expectEqualSets(s1.intersection(s7), []) + expectEqualSets(s2.intersection(s7), []) + expectEqualSets(s3.intersection(s7), []) + expectEqualSets(s4.intersection(s7), []) + expectEqualSets(s5.intersection(s7), []) + expectEqualSets(s6.intersection(s7), [af1]) + + expectEqualSets(s7.intersection(s1), []) + expectEqualSets(s7.intersection(s2), []) + expectEqualSets(s7.intersection(s3), []) + expectEqualSets(s7.intersection(s4), []) + expectEqualSets(s7.intersection(s5), []) + expectEqualSets(s7.intersection(s6), [af1]) + + let s8: TreeSet = [a, af1] + expectEqualSets(s1.intersection(s8), [a]) + expectEqualSets(s2.intersection(s8), [a]) + expectEqualSets(s3.intersection(s8), []) + expectEqualSets(s4.intersection(s8), [a]) + expectEqualSets(s5.intersection(s8), [a]) + expectEqualSets(s6.intersection(s8), [af1]) + expectEqualSets(s7.intersection(s8), [af1]) + + expectEqualSets(s8.intersection(s1), [a]) + expectEqualSets(s8.intersection(s2), [a]) + expectEqualSets(s8.intersection(s3), []) + expectEqualSets(s8.intersection(s4), [a]) + expectEqualSets(s8.intersection(s5), [a]) + expectEqualSets(s8.intersection(s6), [af1]) + expectEqualSets(s8.intersection(s7), [af1]) + + let afa1 = RawCollider(11, "AFA") + let afa2 = RawCollider(12, "AFA") + let s9: TreeSet = [afa1, afa2] + expectEqualSets(s9.intersection(s6), []) + expectEqualSets(s6.intersection(s9), []) + } + + func test_isEqual_exhaustive() { + withEverySubset("a", of: testItems) { a in + let x = TreeSet(a) + let u = Set(a) + expectTrue(x.isEqualSet(to: x)) + withEverySubset("b", of: testItems) { b in + let y = TreeSet(b) + let v = Set(b) + let z = TreeDictionary(uniqueKeysWithValues: b.map { ($0, $0) }) + + let reference = (u == v) + + func checkSequence( + _ a: TreeSet, + _ b: S + ) -> Bool + where S.Element == RawCollider { + a.isEqualSet(to: b) + } + + expectEqual(x.isEqualSet(to: y), reference) + expectEqual(x.isEqualSet(to: z.keys), reference) + expectEqual(checkSequence(x, y), reference) + expectEqual(x.isEqualSet(to: v), reference) + expectEqual(x.isEqualSet(to: b), reference) + expectEqual(x.isEqualSet(to: b + b), reference) + } + } + } + + func test_isSubset_exhaustive() { + withEverySubset("a", of: testItems) { a in + let x = TreeSet(a) + let u = Set(a) + expectTrue(x.isSubset(of: x)) + withEverySubset("b", of: testItems) { b in + let y = TreeSet(b) + let v = Set(b) + let z = TreeDictionary(uniqueKeysWithValues: b.map { ($0, $0) }) + + let reference = u.isSubset(of: v) + + func checkSequence( + _ a: TreeSet, + _ b: S + ) -> Bool + where S.Element == RawCollider { + a.isSubset(of: b) + } + + expectEqual(x.isSubset(of: y), reference) + expectEqual(x.isSubset(of: z.keys), reference) + expectEqual(checkSequence(x, y), reference) + expectEqual(x.isSubset(of: v), reference) + expectEqual(x.isSubset(of: b), reference) + expectEqual(x.isSubset(of: b + b), reference) + } + } + } + + func test_isSuperset_exhaustive() { + withEverySubset("a", of: testItems) { a in + let x = TreeSet(a) + let u = Set(a) + expectTrue(x.isSuperset(of: x)) + withEverySubset("b", of: testItems) { b in + let y = TreeSet(b) + let v = Set(b) + let z = TreeDictionary(uniqueKeysWithValues: b.map { ($0, $0) }) + + let reference = u.isSuperset(of: v) + + func checkSequence( + _ a: TreeSet, + _ b: S + ) -> Bool + where S.Element == RawCollider { + a.isSuperset(of: b) + } + + expectEqual(x.isSuperset(of: y), reference) + expectEqual(x.isSuperset(of: z.keys), reference) + expectEqual(checkSequence(x, y), reference) + expectEqual(x.isSuperset(of: v), reference) + expectEqual(x.isSuperset(of: b), reference) + expectEqual(x.isSuperset(of: b + b), reference) + } + } + } + + func test_isStrictSubset_exhaustive() { + withEverySubset("a", of: testItems) { a in + let x = TreeSet(a) + let u = Set(a) + expectFalse(x.isStrictSubset(of: x)) + withEverySubset("b", of: testItems) { b in + let y = TreeSet(b) + let v = Set(b) + let z = TreeDictionary(uniqueKeysWithValues: b.map { ($0, $0) }) + + let reference = u.isStrictSubset(of: v) + + func checkSequence( + _ a: TreeSet, + _ b: S + ) -> Bool + where S.Element == RawCollider { + a.isStrictSubset(of: b) + } + + expectEqual(x.isStrictSubset(of: y), reference) + expectEqual(x.isStrictSubset(of: z.keys), reference) + expectEqual(checkSequence(x, y), reference) + expectEqual(x.isStrictSubset(of: v), reference) + expectEqual(x.isStrictSubset(of: b), reference) + expectEqual(x.isStrictSubset(of: b + b), reference) + } + } + } + + func test_isStrictSuperset_exhaustive() { + withEverySubset("a", of: testItems) { a in + let x = TreeSet(a) + let u = Set(a) + expectFalse(x.isStrictSuperset(of: x)) + withEverySubset("b", of: testItems) { b in + let y = TreeSet(b) + let v = Set(b) + let z = TreeDictionary(uniqueKeysWithValues: b.map { ($0, $0) }) + + let reference = u.isStrictSuperset(of: v) + + func checkSequence( + _ a: TreeSet, + _ b: S + ) -> Bool + where S.Element == RawCollider { + a.isStrictSuperset(of: b) + } + + expectEqual(x.isStrictSuperset(of: y), reference) + expectEqual(x.isStrictSuperset(of: z.keys), reference) + expectEqual(checkSequence(x, y), reference) + expectEqual(x.isStrictSuperset(of: v), reference) + expectEqual(x.isStrictSuperset(of: b), reference) + expectEqual(x.isStrictSuperset(of: b + b), reference) + } + } + } + + func test_isDisjoint_exhaustive() { + withEverySubset("a", of: testItems) { a in + let x = TreeSet(a) + let u = Set(a) + expectEqual(x.isDisjoint(with: x), x.isEmpty) + withEverySubset("b", of: testItems) { b in + let y = TreeSet(b) + let v = Set(b) + let z = TreeDictionary(uniqueKeysWithValues: b.map { ($0, $0) }) + let reference = u.isDisjoint(with: v) + + func checkSequence( + _ a: TreeSet, + _ b: S + ) -> Bool + where S.Element == RawCollider { + a.isDisjoint(with: b) + } + + expectEqual(x.isDisjoint(with: y), reference) + expectEqual(x.isDisjoint(with: z.keys), reference) + expectEqual(checkSequence(x, y), reference) + expectEqual(x.isDisjoint(with: v), reference) + expectEqual(x.isDisjoint(with: b), reference) + expectEqual(x.isDisjoint(with: b + b), reference) + } + } + } + + func test_intersection_exhaustive() { + withEverySubset("a", of: testItems) { a in + let x = TreeSet(a) + let u = Set(a) + expectEqualSets(x.intersection(x), u) + withEverySubset("b", of: testItems) { b in + let y = TreeSet(b) + let v = Set(b) + let z = TreeDictionary(uniqueKeysWithValues: b.map { ($0, $0) }) + + let reference = u.intersection(v) + + func checkSequence( + _ a: TreeSet, + _ b: S + ) -> TreeSet + where S.Element == RawCollider { + a.intersection(b) + } + + expectEqualSets(x.intersection(y), reference) + expectEqualSets(x.intersection(z.keys), reference) + expectEqualSets(checkSequence(x, y), reference) + expectEqualSets(x.intersection(v), reference) + expectEqualSets(x.intersection(b), reference) + expectEqualSets(x.intersection(b + b), reference) + } + } + } + + func test_subtracting_exhaustive() { + withEverySubset("a", of: testItems) { a in + let x = TreeSet(a) + let u = Set(a) + expectEqualSets(x.subtracting(x), []) + withEverySubset("b", of: testItems) { b in + let y = TreeSet(b) + let v = Set(b) + let z = TreeDictionary(uniqueKeysWithValues: b.map { ($0, $0) }) + + let reference = u.subtracting(v) + + func checkSequence( + _ a: TreeSet, + _ b: S + ) -> TreeSet + where S.Element == RawCollider { + a.subtracting(b) + } + + expectEqualSets(x.subtracting(y), reference) + expectEqualSets(x.subtracting(z.keys), reference) + expectEqualSets(checkSequence(x, y), reference) + expectEqualSets(x.subtracting(v), reference) + expectEqualSets(x.subtracting(b), reference) + expectEqualSets(x.subtracting(b + b), reference) + } + } + } + + func test_filter_exhaustive() { + withEverySubset("a", of: testItems) { a in + let x = TreeSet(a) + withEverySubset("b", of: a) { b in + let v = Set(b) + expectEqualSets(x.filter { v.contains($0) }, v) + } + } + } + + func test_removeAll_where_exhaustive() { + withEvery("isShared", in: [false, true]) { isShared in + withEverySubset("a", of: testItems) { a in + withEverySubset("b", of: a) { b in + var x = TreeSet(a) + let v = Set(b) + withHiddenCopies(if: isShared, of: &x) { x in + x.removeAll { !v.contains($0) } + expectEqualSets(x, v) + } + } + } + } + } + + func test_union_exhaustive() { + withEverySubset("a", of: testItems) { a in + let x = TreeSet(a) + let u = Set(a) + expectEqualSets(x.union(x), u) + withEverySubset("b", of: testItems) { b in + let y = TreeSet(b) + let v = Set(b) + let z = TreeDictionary(uniqueKeysWithValues: b.map { ($0, $0) }) + + let reference = u.union(v) + + func checkSequence( + _ a: TreeSet, + _ b: S + ) -> TreeSet + where S.Element == RawCollider { + a.union(b) + } + + expectEqualSets(x.union(y), reference) + expectEqualSets(x.union(z.keys), reference) + expectEqualSets(checkSequence(x, y), reference) + expectEqualSets(x.union(v), reference) + expectEqualSets(x.union(b), reference) + expectEqualSets(x.union(b + b), reference) + } + } + } + + func test_symmetricDifference_exhaustive() { + withEverySubset("a", of: testItems) { a in + let x = TreeSet(a) + let u = Set(a) + expectEqualSets(x.symmetricDifference(x), []) + withEverySubset("b", of: testItems) { b in + let y = TreeSet(b) + let v = Set(b) + let z = TreeDictionary(uniqueKeysWithValues: b.map { ($0, $0) }) + + let reference = u.symmetricDifference(v) + + func checkSequence( + _ a: TreeSet, + _ b: S + ) -> TreeSet + where S.Element == RawCollider { + a.symmetricDifference(b) + } + + expectEqualSets(x.symmetricDifference(y), reference) + expectEqualSets(x.symmetricDifference(z.keys), reference) + expectEqualSets(checkSequence(x, y), reference) + expectEqualSets(x.symmetricDifference(v), reference) + expectEqualSets(x.symmetricDifference(b), reference) + expectEqualSets(x.symmetricDifference(b + b), reference) + } + } + } + + func test_mutating_binary_set_operations() { + let a = [1, 2, 3, 4] + let b = [0, 2, 4, 6] + + let x = TreeSet(a) + let y = TreeSet(b) + let u = Set(a) + let v = Set(b) + let z = TreeDictionary(uniqueKeysWithValues: b.map { ($0, $0) }) + + func check( + _ reference: Set, + _ body: (inout TreeSet) -> Void, + file: StaticString = #file, + line: UInt = #line + ) { + var set = x + body(&set) + expectEqualSets(set, reference, file: file, line: line) + } + + do { + let reference = u.intersection(v) + check(reference) { $0.formIntersection(y) } + check(reference) { $0.formIntersection(z.keys) } + check(reference) { $0.formIntersection(b) } + check(reference) { $0.formIntersection(b + b) } + } + + do { + let reference = u.union(v) + check(reference) { $0.formUnion(y) } + check(reference) { $0.formUnion(z.keys) } + check(reference) { $0.formUnion(b) } + check(reference) { $0.formUnion(b + b) } + } + + do { + let reference = u.symmetricDifference(v) + check(reference) { $0.formSymmetricDifference(y) } + check(reference) { $0.formSymmetricDifference(z.keys) } + check(reference) { $0.formSymmetricDifference(b) } + check(reference) { $0.formSymmetricDifference(b + b) } + } + + do { + let reference = u.subtracting(v) + check(reference) { $0.subtract(y) } + check(reference) { $0.subtract(z.keys) } + check(reference) { $0.subtract(b) } + check(reference) { $0.subtract(b + b) } + } + } + + func test_update_at() { + withEverySubset("a", of: testItems) { a in + withEvery("offset", in: 0 ..< a.count) { offset in + withEvery("isShared", in: [false, true]) { isShared in + withLifetimeTracking { tracker in + var x = TreeSet(tracker.instances(for: a)) + let i = x.firstIndex { $0.payload == a[offset] }! + let replacement = tracker.instance(for: a[offset]) + withHiddenCopies(if: isShared, of: &x) { x in + let old = x.update(replacement, at: i) + expectEqual(old, replacement) + expectNotIdentical(old, replacement) + } + } + } + } + } + } + + func test_Hashable() { + let classes: [[TreeSet]] = [ + [ + [] + ], + [ + ["a"] + ], + [ + ["b"] + ], + [ + ["c"] + ], + [ + ["d"] + ], + [ + ["e"] + ], + [ + ["f"], ["f"], + ], + [ + ["g"], ["g"], + ], + [ + ["h"], ["h"], + ], + [ + ["i"], ["i"], + ], + [ + ["j"], ["j"], + ], + [ + ["a", "b"], ["b", "a"], + ], + [ + ["a", "d"], ["d", "a"], + ], + [ + ["a", "b", "c"], ["a", "c", "b"], + ["b", "a", "c"], ["b", "c", "a"], + ["c", "a", "b"], ["c", "b", "a"], + ], + [ + ["a", "d", "e"], ["a", "e", "d"], + ["d", "a", "e"], ["d", "e", "a"], + ["e", "a", "d"], ["e", "d", "a"], + ], + ] + checkHashable(equivalenceClasses: classes) + } + + func test_Codable() throws { + let s1: TreeSet = [] + let v1: MinimalEncoder.Value = .array([]) + expectEqual(try MinimalEncoder.encode(s1), v1) + + let s2: TreeSet = [3] + let v2: MinimalEncoder.Value = .array([.int(3)]) + expectEqual(try MinimalEncoder.encode(s2), v2) + + let s3: TreeSet = [0, 1, 2, 3] + let v3: MinimalEncoder.Value = .array(s3.map { .int($0) }) + expectEqual(try MinimalEncoder.encode(s3), v3) + + let s4 = TreeSet(0 ..< 100) + let v4: MinimalEncoder.Value = .array(s4.map { .int($0) }) + expectEqual(try MinimalEncoder.encode(s4), v4) + } + + func test_Decodable() throws { + let s1: TreeSet = [] + let v1: MinimalEncoder.Value = .array([]) + expectEqual(try MinimalDecoder.decode(v1, as: TreeSet.self), s1) + + let s2: TreeSet = [3] + let v2: MinimalEncoder.Value = .array([.int(3)]) + expectEqual(try MinimalDecoder.decode(v2, as: TreeSet.self), s2) + + let s3: TreeSet = [0, 1, 2, 3] + let v3: MinimalEncoder.Value = .array([.int(0), .int(1), .int(2), .int(3)]) + expectEqual(try MinimalDecoder.decode(v3, as: TreeSet.self), s3) + + let s4 = TreeSet(0 ..< 100) + let v4: MinimalEncoder.Value = .array((0 ..< 100).map { .int($0) }) + expectEqual(try MinimalDecoder.decode(v4, as: TreeSet.self), s4) + + let v5: MinimalEncoder.Value = .array([.int(0), .int(1), .int(0)]) + expectThrows( + try MinimalDecoder.decode(v5, as: TreeSet.self) + ) { error in + expectNotNil(error as? DecodingError) { error in + guard case .dataCorrupted(let context) = error else { + expectFailure("Unexpected error \(error)") + return + } + expectEqual(context.debugDescription, + "Decoded elements aren't unique (first duplicate at offset 2)") + } + } + + let v6: MinimalEncoder.Value = .array([.int16(42)]) + expectThrows( + try MinimalDecoder.decode(v6, as: TreeSet.self) + ) { error in + expectNotNil(error as? DecodingError) { error in + guard case .typeMismatch(_, _) = error else { + expectFailure("Unexpected error \(error)") + return + } + } + } + } + + func test_CustomReflectable() { + let s: TreeSet = [0, 1, 2, 3] + let mirror = Mirror(reflecting: s) + expectEqual(mirror.displayStyle, .set) + expectNil(mirror.superclassMirror) + expectTrue(mirror.children.compactMap { $0.label }.isEmpty) + expectEqualElements(mirror.children.map { $0.value as? Int }, s.map { $0 }) + } +} diff --git a/Tests/HashTreeCollectionsTests/Utilities.swift b/Tests/HashTreeCollectionsTests/Utilities.swift new file mode 100644 index 000000000..6e4569179 --- /dev/null +++ b/Tests/HashTreeCollectionsTests/Utilities.swift @@ -0,0 +1,233 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if COLLECTIONS_SINGLE_MODULE +import Collections +#else +import _CollectionsTestSupport +import HashTreeCollections +#endif + +extension LifetimeTracker { + func shareableDictionary( + keys: Keys + ) -> ( + dictionary: TreeDictionary, LifetimeTracked>, + keys: [LifetimeTracked], + values: [LifetimeTracked] + ) + where Keys.Element == Int + { + let k = Array(keys) + let keys = self.instances(for: k) + let values = self.instances(for: k.map { $0 + 100 }) + let dictionary = TreeDictionary( + uniqueKeysWithValues: zip(keys, values)) + return (dictionary, keys, values) + } +} + +protocol DataGenerator { + associatedtype Key: Hashable + associatedtype Value: Equatable + + func key(for i: Int) -> Key + func value(for i: Int) -> Value +} + +struct IntDataGenerator: DataGenerator { + typealias Key = Int + typealias Value = Int + + let valueOffset: Int + + init(valueOffset: Int) { + self.valueOffset = valueOffset + } + + func key(for i: Int) -> Key { + i + } + + func value(for i: Int) -> Value { + i + valueOffset + } +} + +struct ColliderDataGenerator: DataGenerator { + typealias Key = Collider + typealias Value = Int + + let groups: Int + let valueOffset: Int + + init(groups: Int, valueOffset: Int) { + self.groups = groups + self.valueOffset = valueOffset + } + + func key(for i: Int) -> Key { + Collider(i, Hash(i % groups)) + } + + func value(for i: Int) -> Value { + i + valueOffset + } +} + +extension LifetimeTracker { + func shareableDictionary( + _ payloads: Payloads, + with generator: G + ) -> ( + map: TreeDictionary, LifetimeTracked>, + expected: [LifetimeTracked: LifetimeTracked] + ) + where Payloads.Element == Int + { + typealias Key = LifetimeTracked + typealias Value = LifetimeTracked + func gen() -> [(Key, Value)] { + payloads.map { + (instance(for: generator.key(for: $0)), + instance(for: generator.value(for: $0))) + } + } + let map = TreeDictionary(uniqueKeysWithValues: gen()) + let expected = Dictionary(uniqueKeysWithValues: gen()) + return (map, expected) + } + + func shareableDictionary( + keys: Keys + ) -> ( + map: TreeDictionary, LifetimeTracked>, + expected: [LifetimeTracked: LifetimeTracked] + ) + where Keys.Element == Int + { + shareableDictionary(keys, with: IntDataGenerator(valueOffset: 100)) + } +} + +func expectEqualSets( + _ set: TreeSet, + _ ref: [Element], + _ message: @autoclosure () -> String = "", + trapping: Bool = false, + file: StaticString = #file, + line: UInt = #line +) { + expectEqualSets( + set, Set(ref), + message(), + trapping: trapping, + file: file, line: line) +} + +func expectEqualSets( + _ set: C, + _ ref: Set, + _ message: @autoclosure () -> String = "", + trapping: Bool = false, + file: StaticString = #file, + line: UInt = #line +) { + var ref = ref + var seen: Set = [] + var extras: [C.Element] = [] + var dupes: [C.Element: Int] = [:] + for item in set { + if !seen.insert(item).inserted { + dupes[item, default: 1] += 1 + } else if ref.remove(item) == nil { + extras.append(item) + } + } + let missing = Array(ref) + var msg = "" + if !extras.isEmpty { + msg += "\nUnexpected items: \(extras)" + } + if !missing.isEmpty { + msg += "\nMissing items: \(missing)" + } + if !dupes.isEmpty { + msg += "\nDuplicate items: \(dupes)" + } + if !msg.isEmpty { + _expectFailure( + "\n\(msg)", + message, trapping: trapping, file: file, line: line) + } +} + + +func expectEqualDictionaries( + _ map: TreeDictionary, + _ ref: [(key: Key, value: Value)], + _ message: @autoclosure () -> String = "", + trapping: Bool = false, + file: StaticString = #file, + line: UInt = #line +) { + expectEqualDictionaries( + map, Dictionary(uniqueKeysWithValues: ref), + message(), + trapping: trapping, + file: file, line: line) +} + +func expectEqualDictionaries( + _ map: TreeDictionary, + _ dict: Dictionary, + _ message: @autoclosure () -> String = "", + trapping: Bool = false, + file: StaticString = #file, + line: UInt = #line +) { + expectEqual(map.count, dict.count, "Mismatching count", file: file, line: line) + var dict = dict + var seen: Set = [] + var mismatches: [(key: Key, map: Value?, dict: Value?)] = [] + var dupes: [(key: Key, map: Value)] = [] + for (key, value) in map { + if !seen.insert(key).inserted { + dupes.append((key, value)) + } else { + let expected = dict.removeValue(forKey: key) + if value != expected { + mismatches.append((key, value, expected)) + } + } + } + for (key, value) in dict { + mismatches.append((key, nil, value)) + } + if !mismatches.isEmpty || !dupes.isEmpty { + let msg1 = mismatches.lazy.map { k, m, d in + "\n \(k): \(m == nil ? "nil" : "\(m!)") vs \(d == nil ? "nil" : "\(d!)")" + }.joined(separator: "") + let msg2 = dupes.lazy.map { k, v in + "\n \(k): \(v) (duped)" + }.joined(separator: "") + _expectFailure( + "\n\(mismatches.count) mismatches (actual vs expected):\(msg1)\(msg2)", + message, trapping: trapping, file: file, line: line) + } +} + +func mutate( + _ value: inout T, + _ body: (inout T) throws -> R +) rethrows -> R { + try body(&value) +} diff --git a/Tests/HeapTests/HeapNodeTests.swift b/Tests/HeapTests/HeapNodeTests.swift new file mode 100644 index 000000000..9f2783824 --- /dev/null +++ b/Tests/HeapTests/HeapNodeTests.swift @@ -0,0 +1,34 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if DEBUG // These unit tests need access to HeapModule internals +import XCTest +#if COLLECTIONS_SINGLE_MODULE +@testable import Collections +#else +@testable import HeapModule +#endif + +class HeapNodeTests: XCTestCase { + func test_levelCalculation() { + // Check alternating min and max levels in the heap + var isMin = true + for exp in 0...12 { + // Check [2^exp, 2^(exp + 1)) + for offset in Int(pow(2, Double(exp)) - 1).. [Element] { + Array(sequence(state: self) { $0.popMin() }) + } +} + +extension Heap { + /// Creates a heap from the given array of elements, which must have already + /// been heapified to form a binary min-max heap. + /// + /// - Precondition: `storage` has already been heapified. + /// + /// - Parameter storage: The elements of the heap. + /// - Postcondition: `unordered.elementsEqual(s)`, where *s* is a sequence + /// with the same elements as pre-call `storage`. + /// + /// - Complexity: O(1) + public init(raw storage: [Element]) { + self.init(storage) + precondition(self.unordered == storage) + } +} + +final class HeapTests: CollectionTestCase { + func test_isEmpty() { + var heap = Heap() + expectTrue(heap.isEmpty) + + heap.insert(42) + expectFalse(heap.isEmpty) + + let _ = heap.popMin() + expectTrue(heap.isEmpty) + } + + func test_count() { + var heap = Heap() + expectEqual(heap.count, 0) + + heap.insert(20) + expectEqual(heap.count, 1) + + heap.insert(40) + expectEqual(heap.count, 2) + + _ = heap.popMin() + expectEqual(heap.count, 1) + } + + func test_descriptions() { + let a: Heap = [] + expectTrue( + a.description.starts(with: "<0 items @"), + "\(a.description)") + expectTrue( + a.debugDescription.starts(with: "<0 items @"), + "\(a.debugDescription)") + + let b: Heap = [1] + expectTrue( + b.description.starts(with: "<1 item @"), + "\(b.description)") + expectTrue( + b.debugDescription.starts(with: "<1 item @")) + + let c: Heap = [1, 2] + expectTrue(c.description.starts(with: "<2 items @")) + expectTrue(c.debugDescription.starts(with: "<2 items @")) + } + + func test_unordered() { + let heap = Heap((1...10)) + expectEqual(Set(heap.unordered), Set(1...10)) + } + + struct Task: Comparable { + let name: String + let priority: Int + + static func < (lhs: Task, rhs: Task) -> Bool { + lhs.priority < rhs.priority + } + } + + func test_insert() { + var heap = Heap() + + expectEqual(heap.count, 0) + heap.insert(Task(name: "Hello, world", priority: 50)) + expectEqual(heap.count, 1) + } + + func test_insert_random() { + let c = 128 + withEvery("seed", in: 0 ..< 5_000) { seed in + var rng = RepeatableRandomNumberGenerator(seed: seed) + let input = (0 ..< c).shuffled(using: &rng) + var heap: Heap = [] + var i = 0 + withEvery("value", in: input) { value in + expectEqual(heap.count, i) + heap.insert(value) + i += 1 + expectEqual(heap.count, i) + } + expectEqualElements(heap.itemsInAscendingOrder(), 0 ..< c) + } + } + + func test_insert_contentsOf() { + var heap = Heap() + heap.insert(contentsOf: (1...10).shuffled()) + expectEqual(heap.count, 10) + expectEqual(heap.popMax(), 10) + expectEqual(heap.popMin(), 1) + + heap.insert(contentsOf: (21...50).shuffled()) + expectEqual(heap.count, 38) + expectEqual(heap.max, 50) + expectEqual(heap.min, 2) + + heap.insert(contentsOf: [-10, -9, -8, -7, -6, -5].shuffled()) + expectEqual(heap.count, 44) + expectEqual(heap.min, -10) + } + + func test_insert_contentsOf_withSequenceFunction() { + func addTwo(_ i: Int) -> Int { + i + 2 + } + + let evens = sequence(first: 0, next: addTwo(_:)).prefix(20) + var heap = Heap(evens) + expectEqual(heap.count, 20) + + heap.insert(contentsOf: sequence(first: 1, next: addTwo(_:)).prefix(20)) + expectEqual(heap.count, 40) + + withEvery("i", in: 0 ..< 40) { i in + expectNotNil(heap.popMin()) { min in + expectEqual(min, i) + } + } + expectNil(heap.popMin()) + } + + func test_insert_contentsOf_exhaustive() { + withEvery("c", in: 0 ..< 15) { c in + withEverySubset("a", of: 0 ..< c) { a in + let startInput = (0 ..< c).filter { !a.contains($0) } + var heap = Heap(startInput) + heap.insert(contentsOf: a.shuffled()) + expectEqualElements(heap.itemsInAscendingOrder(), 0 ..< c) + } + } + } + + func test_min() { + var heap = Heap() + expectNil(heap.min) + + heap.insert(5) + expectEqual(5, heap.min) + + heap.insert(12) + expectEqual(5, heap.min) + + heap.insert(2) + expectEqual(2, heap.min) + + heap.insert(1) + expectEqual(1, heap.min) + } + + func test_max() { + var heap = Heap() + expectNil(heap.max) + + heap.insert(42) + expectEqual(42, heap.max) + + heap.insert(20) + expectEqual(42, heap.max) + + heap.insert(63) + expectEqual(63, heap.max) + + heap.insert(90) + expectEqual(90, heap.max) + } + + func test_popMin() { + var heap = Heap() + expectNil(heap.popMin()) + + heap.insert(7) + expectEqual(heap.popMin(), 7) + + heap.insert(12) + heap.insert(9) + expectEqual(heap.popMin(), 9) + + heap.insert(13) + heap.insert(1) + heap.insert(4) + expectEqual(heap.popMin(), 1) + + for i in (1...20).shuffled() { + heap.insert(i) + } + + expectEqual(heap.popMin(), 1) + expectEqual(heap.popMin(), 2) + expectEqual(heap.popMin(), 3) + expectEqual(heap.popMin(), 4) + expectEqual(heap.popMin(), 4) // One 4 was still in the heap from before + expectEqual(heap.popMin(), 5) + expectEqual(heap.popMin(), 6) + expectEqual(heap.popMin(), 7) + expectEqual(heap.popMin(), 8) + expectEqual(heap.popMin(), 9) + expectEqual(heap.popMin(), 10) + expectEqual(heap.popMin(), 11) + expectEqual(heap.popMin(), 12) + expectEqual(heap.popMin(), 12) // One 12 was still in the heap from before + expectEqual(heap.popMin(), 13) + expectEqual(heap.popMin(), 13) // One 13 was still in the heap from before + expectEqual(heap.popMin(), 14) + expectEqual(heap.popMin(), 15) + expectEqual(heap.popMin(), 16) + expectEqual(heap.popMin(), 17) + expectEqual(heap.popMin(), 18) + expectEqual(heap.popMin(), 19) + expectEqual(heap.popMin(), 20) + + expectNil(heap.popMin()) + } + + func test_popMax() { + var heap = Heap() + expectNil(heap.popMax()) + + heap.insert(7) + expectEqual(heap.popMax(), 7) + + heap.insert(12) + heap.insert(9) + expectEqual(heap.popMax(), 12) + + heap.insert(13) + heap.insert(1) + heap.insert(4) + expectEqual(heap.popMax(), 13) + + for i in (1...20).shuffled() { + heap.insert(i) + } + + expectEqual(heap.popMax(), 20) + expectEqual(heap.popMax(), 19) + expectEqual(heap.popMax(), 18) + expectEqual(heap.popMax(), 17) + expectEqual(heap.popMax(), 16) + expectEqual(heap.popMax(), 15) + expectEqual(heap.popMax(), 14) + expectEqual(heap.popMax(), 13) + expectEqual(heap.popMax(), 12) + expectEqual(heap.popMax(), 11) + expectEqual(heap.popMax(), 10) + expectEqual(heap.popMax(), 9) + expectEqual(heap.popMax(), 9) // One 9 was still in the heap from before + expectEqual(heap.popMax(), 8) + expectEqual(heap.popMax(), 7) + expectEqual(heap.popMax(), 6) + expectEqual(heap.popMax(), 5) + expectEqual(heap.popMax(), 4) + expectEqual(heap.popMax(), 4) // One 4 was still in the heap from before + expectEqual(heap.popMax(), 3) + expectEqual(heap.popMax(), 2) + expectEqual(heap.popMax(), 1) + expectEqual(heap.popMax(), 1) // One 1 was still in the heap from before + + expectNil(heap.popMax()) + } + + func test_removeMin() { + var heap = Heap((1...20).shuffled()) + + expectEqual(heap.removeMin(), 1) + expectEqual(heap.removeMin(), 2) + expectEqual(heap.removeMin(), 3) + expectEqual(heap.removeMin(), 4) + expectEqual(heap.removeMin(), 5) + expectEqual(heap.removeMin(), 6) + expectEqual(heap.removeMin(), 7) + expectEqual(heap.removeMin(), 8) + expectEqual(heap.removeMin(), 9) + expectEqual(heap.removeMin(), 10) + expectEqual(heap.removeMin(), 11) + expectEqual(heap.removeMin(), 12) + expectEqual(heap.removeMin(), 13) + expectEqual(heap.removeMin(), 14) + expectEqual(heap.removeMin(), 15) + expectEqual(heap.removeMin(), 16) + expectEqual(heap.removeMin(), 17) + expectEqual(heap.removeMin(), 18) + expectEqual(heap.removeMin(), 19) + expectEqual(heap.removeMin(), 20) + } + + func test_removeMax() { + var heap = Heap((1...20).shuffled()) + + expectEqual(heap.removeMax(), 20) + expectEqual(heap.removeMax(), 19) + expectEqual(heap.removeMax(), 18) + expectEqual(heap.removeMax(), 17) + expectEqual(heap.removeMax(), 16) + expectEqual(heap.removeMax(), 15) + expectEqual(heap.removeMax(), 14) + expectEqual(heap.removeMax(), 13) + expectEqual(heap.removeMax(), 12) + expectEqual(heap.removeMax(), 11) + expectEqual(heap.removeMax(), 10) + expectEqual(heap.removeMax(), 9) + expectEqual(heap.removeMax(), 8) + expectEqual(heap.removeMax(), 7) + expectEqual(heap.removeMax(), 6) + expectEqual(heap.removeMax(), 5) + expectEqual(heap.removeMax(), 4) + expectEqual(heap.removeMax(), 3) + expectEqual(heap.removeMax(), 2) + expectEqual(heap.removeMax(), 1) + } + + func test_minimumReplacement() { + var heap = Heap(stride(from: 0, through: 27, by: 3).shuffled()) + expectEqual( + heap.itemsInAscendingOrder(), [0, 3, 6, 9, 12, 15, 18, 21, 24, 27]) + expectEqual(heap.min, 0) + + // No change + heap.replaceMin(with: 0) + expectEqual( + heap.itemsInAscendingOrder(), [0, 3, 6, 9, 12, 15, 18, 21, 24, 27]) + expectEqual(heap.min, 0) + + // Even smaller + heap.replaceMin(with: -1) + expectEqual( + heap.itemsInAscendingOrder(), [-1, 3, 6, 9, 12, 15, 18, 21, 24, 27]) + expectEqual(heap.min, -1) + + // Larger, but not enough to usurp + heap.replaceMin(with: 2) + expectEqual( + heap.itemsInAscendingOrder(), [2, 3, 6, 9, 12, 15, 18, 21, 24, 27]) + expectEqual(heap.min, 2) + + // Larger, moving another element to be the smallest + heap.replaceMin(with: 5) + expectEqual( + heap.itemsInAscendingOrder(), [3, 5, 6, 9, 12, 15, 18, 21, 24, 27]) + expectEqual(heap.min, 3) + } + + func test_maximumReplacement() { + var heap = Heap(stride(from: 0, through: 27, by: 3).shuffled()) + expectEqual( + heap.itemsInAscendingOrder(), [0, 3, 6, 9, 12, 15, 18, 21, 24, 27]) + expectEqual(heap.max, 27) + + // No change + heap.replaceMax(with: 27) + expectEqual( + heap.itemsInAscendingOrder(), [0, 3, 6, 9, 12, 15, 18, 21, 24, 27]) + expectEqual(heap.max, 27) + + // Even larger + heap.replaceMax(with: 28) + expectEqual( + heap.itemsInAscendingOrder(), [0, 3, 6, 9, 12, 15, 18, 21, 24, 28]) + expectEqual(heap.max, 28) + + // Smaller, but not enough to usurp + heap.replaceMax(with: 26) + expectEqual( + heap.itemsInAscendingOrder(), [0, 3, 6, 9, 12, 15, 18, 21, 24, 26]) + expectEqual(heap.max, 26) + + // Smaller, moving another element to be the largest + heap.replaceMax(with: 23) + expectEqual( + heap.itemsInAscendingOrder(), [0, 3, 6, 9, 12, 15, 18, 21, 23, 24]) + expectEqual(heap.max, 24) + + // Check the finer details. As these peek into the stored structure, they + // may need to be updated whenever the internal format changes. + var heap2 = Heap(raw: [1]) + expectEqual(heap2.max, 1) + expectEqual(Array(heap2.unordered), [1]) + expectEqual(heap2.replaceMax(with: 2), 1) + expectEqual(heap2.max, 2) + expectEqual(Array(heap2.unordered), [2]) + + heap2 = Heap(raw: [1, 2]) + expectEqual(heap2.max, 2) + expectEqual(Array(heap2.unordered), [1, 2]) + expectEqual(heap2.replaceMax(with: 3), 2) + expectEqual(heap2.max, 3) + expectEqual(Array(heap2.unordered), [1, 3]) + expectEqual(heap2.replaceMax(with: 0), 3) + expectEqual(heap2.max, 1) + expectEqual(Array(heap2.unordered), [0, 1]) + + heap2 = Heap(raw: [5, 20, 31, 16, 8, 7, 18]) + expectEqual(heap2.max, 31) + expectEqual(Array(heap2.unordered), [5, 20, 31, 16, 8, 7, 18]) + expectEqual(heap2.replaceMax(with: 29), 31) + expectEqual(Array(heap2.unordered), [5, 20, 29, 16, 8, 7, 18]) + expectEqual(heap2.max, 29) + expectEqual(heap2.replaceMax(with: 19), 29) + expectEqual(Array(heap2.unordered), [5, 20, 19, 16, 8, 7, 18]) + expectEqual(heap2.max, 20) + expectEqual(heap2.replaceMax(with: 15), 20) + expectEqual(Array(heap2.unordered), [5, 16, 19, 15, 8, 7, 18]) + expectEqual(heap2.max, 19) + expectEqual(heap2.replaceMax(with: 4), 19) + expectEqual(Array(heap2.unordered), [4, 16, 18, 15, 8, 7, 5]) + expectEqual(heap2.max, 18) + } + + // MARK: - + + func test_min_struct() { + var heap = Heap() + expectNil(heap.min) + + let firstTask = Task(name: "Do something", priority: 10) + heap.insert(firstTask) + expectEqual(heap.min, firstTask) + + let higherPriorityTask = Task(name: "Urgent", priority: 100) + heap.insert(higherPriorityTask) + expectEqual(heap.min, firstTask) + + let lowerPriorityTask = Task(name: "Get this done today", priority: 1) + heap.insert(lowerPriorityTask) + expectEqual(heap.min, lowerPriorityTask) + } + + func test_max_struct() { + var heap = Heap() + expectNil(heap.max) + + let firstTask = Task(name: "Do something", priority: 10) + heap.insert(firstTask) + expectEqual(heap.max, firstTask) + + let lowerPriorityTask = Task(name: "Get this done today", priority: 1) + heap.insert(lowerPriorityTask) + expectEqual(heap.max, firstTask) + + let higherPriorityTask = Task(name: "Urgent", priority: 100) + heap.insert(higherPriorityTask) + expectEqual(heap.max, higherPriorityTask) + } + + func test_popMin_struct() { + var heap = Heap() + expectNil(heap.popMin()) + + let lowPriorityTask = Task(name: "Do something when you have time", priority: 1) + heap.insert(lowPriorityTask) + + let highPriorityTask = Task(name: "Get this done today", priority: 50) + heap.insert(highPriorityTask) + + let urgentTask = Task(name: "Urgent", priority: 100) + heap.insert(urgentTask) + + expectEqual(heap.popMin(), lowPriorityTask) + expectEqual(heap.popMin(), highPriorityTask) + expectEqual(heap.popMin(), urgentTask) + expectNil(heap.popMin()) + } + + func test_popMax_struct() { + var heap = Heap() + expectNil(heap.popMax()) + + let lowPriorityTask = Task(name: "Do something when you have time", priority: 1) + heap.insert(lowPriorityTask) + + let highPriorityTask = Task(name: "Get this done today", priority: 50) + heap.insert(highPriorityTask) + + let urgentTask = Task(name: "Urgent", priority: 100) + heap.insert(urgentTask) + + expectEqual(heap.popMax(), urgentTask) + expectEqual(heap.popMax(), highPriorityTask) + expectEqual(heap.popMax(), lowPriorityTask) + expectNil(heap.popMax()) + } + + // MARK: - + + func test_initializer_fromCollection() { + var heap = Heap((1...20).shuffled()) + expectEqual(heap.max, 20) + + expectEqual(heap.popMin(), 1) + expectEqual(heap.popMax(), 20) + expectEqual(heap.popMin(), 2) + expectEqual(heap.popMax(), 19) + expectEqual(heap.popMin(), 3) + expectEqual(heap.popMax(), 18) + expectEqual(heap.popMin(), 4) + expectEqual(heap.popMax(), 17) + expectEqual(heap.popMin(), 5) + expectEqual(heap.popMax(), 16) + expectEqual(heap.popMin(), 6) + expectEqual(heap.popMax(), 15) + expectEqual(heap.popMin(), 7) + expectEqual(heap.popMax(), 14) + expectEqual(heap.popMin(), 8) + expectEqual(heap.popMax(), 13) + expectEqual(heap.popMin(), 9) + expectEqual(heap.popMax(), 12) + expectEqual(heap.popMin(), 10) + expectEqual(heap.popMax(), 11) + } + + func test_initializer_fromSequence() { + let heap = Heap((1...).prefix(20)) + expectEqual(heap.count, 20) + } + + func test_initializer_fromArrayLiteral() { + var heap: Heap = [1, 3, 5, 7, 9] + expectEqual(heap.count, 5) + + expectEqual(heap.popMax(), 9) + expectEqual(heap.popMax(), 7) + expectEqual(heap.popMax(), 5) + expectEqual(heap.popMax(), 3) + expectEqual(heap.popMax(), 1) + } + + func test_initializer_fromSequence_random() { + withEvery("c", in: 0 ... 128) { c in + withEvery( + "seed", in: 0 ..< Swift.min((c + 2) * (c + 1), 100) + ) { seed in + var rng = RepeatableRandomNumberGenerator(seed: seed) + let input = (0 ..< c).shuffled(using: &rng) + let heap = Heap(input) + if c > 0 { + expectEqual(heap.min, 0) + expectEqual(heap.max, c - 1) + expectEqualElements(heap.itemsInAscendingOrder(), 0 ..< c) + } else { + expectNil(heap.min) + expectNil(heap.max) + expectEqualElements(heap.itemsInAscendingOrder(), []) + } + } + } + } +} diff --git a/Tests/OrderedCollectionsTests/HashTable/BitsetTests.swift b/Tests/OrderedCollectionsTests/HashTable/BitsetTests.swift deleted file mode 100644 index 9c0da2639..000000000 --- a/Tests/OrderedCollectionsTests/HashTable/BitsetTests.swift +++ /dev/null @@ -1,187 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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 -// -//===----------------------------------------------------------------------===// - -#if DEBUG // These unit tests need access to OrderedSet internals -import XCTest -import _CollectionsTestSupport -@_spi(Testing) @testable import OrderedCollections - -class BitsetTests: CollectionTestCase { - typealias Word = _UnsafeBitset.Word - - func test_empty() { - withEvery("capacity", in: 0 ..< 500) { capacity in - _UnsafeBitset.withTemporaryBitset(capacity: capacity) { bitset in - expectGreaterThanOrEqual(bitset.capacity, capacity) - expectEqual(bitset.count, 0) - expectEqual(bitset._actualCount, 0) - withEvery("i", in: 0 ..< capacity) { i in - expectFalse(bitset.contains(i)) - } - } - } - } - - func test_insert() { - withEvery("capacity", in: [16, 64, 100, 128, 1000, 1024]) { capacity in - withEvery("seed", in: 0 ..< 3) { seed in - var rng = RepeatableRandomNumberGenerator(seed: seed) - let items = (0 ..< capacity).shuffled(using: &rng) - _UnsafeBitset.withTemporaryBitset(capacity: capacity) { bitset in - var c = 0 - withEvery("item", in: items) { item in - expectFalse(bitset.contains(item)) - expectTrue(bitset.insert(item)) - - expectTrue(bitset.contains(item)) - expectFalse(bitset.insert(item)) - - c += 1 - expectEqual(bitset.count, c) - } - } - } - } - } - - func withRandomBitsets( - capacity: Int, - loadFactor: Double, - body: (inout _UnsafeBitset, inout Set) throws -> Void - ) rethrows { - precondition(loadFactor >= 0 && loadFactor <= 1) - try withEvery("seed", in: 0 ..< 10) { seed in - var rng = RepeatableRandomNumberGenerator(seed: seed) - var items = (0 ..< capacity).shuffled(using: &rng) - items.removeLast(Int((1 - loadFactor) * Double(capacity))) - try _UnsafeBitset.withTemporaryBitset(capacity: capacity) { bitset in - for item in items { - bitset.insert(item) - } - var set = Set(items) - try body(&bitset, &set) - } - } - } - - func test_remove() { - withEvery("capacity", in: [16, 64, 100, 128, 1000, 1024]) { capacity in - withRandomBitsets(capacity: capacity, loadFactor: 0.5) { bitset, contents in - var c = contents.count - withEvery("item", in: 0 ..< capacity) { item in - if contents.contains(item) { - expectTrue(bitset.remove(item)) - expectFalse(bitset.contains(item)) - c -= 1 - expectEqual(bitset.count, c) - } else { - expectFalse(bitset.remove(item)) - expectEqual(bitset.count, c) - } - } - expectEqual(bitset.count, 0) - withEvery("item", in: 0 ..< capacity) { item in - expectFalse(bitset.contains(item)) - } - } - } - } - - func test_clear() { - withEvery("capacity", in: [16, 64, 100, 128, 1000, 1024]) { capacity in - withRandomBitsets(capacity: capacity, loadFactor: 0.5) { bitset, contents in - bitset.clear() - expectEqual(bitset.count, 0) - withEvery("item", in: 0 ..< capacity) { item in - expectFalse(bitset.contains(item)) - } - } - } - } - - func test_insertAll_upTo() { - withEvery("capacity", in: [16, 64, 100, 128, 1000, 1024]) { capacity in - withRandomBitsets(capacity: capacity, loadFactor: 0.5) { bitset, contents in - - let cutoff = capacity / 2 - bitset.insertAll(upTo: cutoff) - expectEqual( - bitset.count, - capacity / 2 + contents.lazy.filter { $0 >= cutoff }.count) - withEvery("item", in: 0 ..< capacity) { item in - if item < cutoff { - expectTrue(bitset.contains(item)) - } else { - expectEqual(bitset.contains(item), contents.contains(item)) - } - } - - bitset.insertAll(upTo: capacity) - expectEqual(bitset.count, capacity) - withEvery("item", in: 0 ..< capacity) { item in - expectTrue(bitset.contains(item)) - } - } - } - } - - func test_removeAll_upTo() { - withEvery("capacity", in: [16, 64, 100, 128, 1000, 1024]) { capacity in - withRandomBitsets(capacity: capacity, loadFactor: 0.5) { bitset, contents in - - let cutoff = capacity / 2 - bitset.removeAll(upTo: cutoff) - expectEqual( - bitset.count, - contents.lazy.filter { $0 >= cutoff }.count) - withEvery("item", in: 0 ..< capacity) { item in - if item < cutoff { - expectFalse(bitset.contains(item)) - } else { - expectEqual(bitset.contains(item), contents.contains(item)) - } - } - - bitset.removeAll(upTo: capacity) - expectEqual(bitset.count, 0) - withEvery("item", in: 0 ..< capacity) { item in - expectFalse(bitset.contains(item)) - } - } - } - } - - func test_Sequence() { - withEvery("capacity", in: [16, 64, 100, 128, 1000, 1024]) { capacity in - withRandomBitsets(capacity: capacity, loadFactor: 0.5) { bitset, contents in - expectEqual(bitset.underestimatedCount, contents.count) - let expected = contents.sorted() - var actual: [Int] = [] - actual.reserveCapacity(bitset.count) - for item in bitset { - actual.append(item) - } - expectEqual(actual, expected) - } - } - } - - func test_max() { - withEvery("capacity", in: [16, 64, 100, 128, 1000, 1024]) { capacity in - withRandomBitsets(capacity: capacity, loadFactor: 0.5) { bitset, contents in - let max = bitset.max() - expectEqual(max, contents.max()) - } - } - } - -} -#endif // DEBUG diff --git a/Tests/OrderedCollectionsTests/HashTable/HashTableTests.swift b/Tests/OrderedCollectionsTests/HashTable/HashTableTests.swift index e3289a5bc..3f334f0af 100644 --- a/Tests/OrderedCollectionsTests/HashTable/HashTableTests.swift +++ b/Tests/OrderedCollectionsTests/HashTable/HashTableTests.swift @@ -2,17 +2,23 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 // //===----------------------------------------------------------------------===// -#if DEBUG // These unit tests need access to OrderedSet internals import XCTest + +#if DEBUG // These unit tests use internal decls + +#if COLLECTIONS_SINGLE_MODULE +@_spi(Testing) @testable import Collections +#else import _CollectionsTestSupport @_spi(Testing) @testable import OrderedCollections +#endif class HashTableTests: CollectionTestCase { typealias Bucket = _HashTable.Bucket diff --git a/Tests/OrderedCollectionsTests/OrderedDictionary/OrderedDictionary Tests.swift b/Tests/OrderedCollectionsTests/OrderedDictionary/OrderedDictionary Tests.swift index 2ede2d57b..eacd60980 100644 --- a/Tests/OrderedCollectionsTests/OrderedDictionary/OrderedDictionary Tests.swift +++ b/Tests/OrderedCollectionsTests/OrderedDictionary/OrderedDictionary Tests.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 @@ -10,9 +10,14 @@ //===----------------------------------------------------------------------===// import XCTest +#if COLLECTIONS_SINGLE_MODULE +@_spi(Testing) import Collections +#else @_spi(Testing) import OrderedCollections - import _CollectionsTestSupport +#endif + +extension OrderedDictionary: DictionaryAPIExtras {} class OrderedDictionaryTests: CollectionTestCase { func test_empty() { @@ -861,19 +866,16 @@ class OrderedDictionaryTests: CollectionTestCase { func test_CustomDebugStringConvertible() { let a: OrderedDictionary = [:] - expectEqual(a.debugDescription, - "OrderedDictionary([:])") + expectEqual(a.debugDescription, "[:]") let b: OrderedDictionary = [0: 1] - expectEqual(b.debugDescription, - "OrderedDictionary([0: 1])") + expectEqual(b.debugDescription, "[0: 1]") let c: OrderedDictionary = [0: 1, 2: 3, 4: 5] - expectEqual(c.debugDescription, - "OrderedDictionary([0: 1, 2: 3, 4: 5])") + expectEqual(c.debugDescription, "[0: 1, 2: 3, 4: 5]") } - func test_customReflectable() { + func test_CustomReflectable() { do { let d: OrderedDictionary = [1: 2, 3: 4, 5: 6] let mirror = Mirror(reflecting: d) diff --git a/Tests/OrderedCollectionsTests/OrderedDictionary/OrderedDictionary Utils.swift b/Tests/OrderedCollectionsTests/OrderedDictionary/OrderedDictionary Utils.swift index a79c993bf..0a29fed55 100644 --- a/Tests/OrderedCollectionsTests/OrderedDictionary/OrderedDictionary Utils.swift +++ b/Tests/OrderedCollectionsTests/OrderedDictionary/OrderedDictionary Utils.swift @@ -2,15 +2,19 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 // //===----------------------------------------------------------------------===// +#if COLLECTIONS_SINGLE_MODULE +import Collections +#else import _CollectionsTestSupport import OrderedCollections +#endif extension LifetimeTracker { func orderedDictionary( diff --git a/Tests/OrderedCollectionsTests/OrderedDictionary/OrderedDictionary+Elements Tests.swift b/Tests/OrderedCollectionsTests/OrderedDictionary/OrderedDictionary+Elements Tests.swift index ae07e1e69..9b22d1ba3 100644 --- a/Tests/OrderedCollectionsTests/OrderedDictionary/OrderedDictionary+Elements Tests.swift +++ b/Tests/OrderedCollectionsTests/OrderedDictionary/OrderedDictionary+Elements Tests.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 @@ -10,9 +10,12 @@ //===----------------------------------------------------------------------===// import XCTest +#if COLLECTIONS_SINGLE_MODULE +@_spi(Testing) import Collections +#else @_spi(Testing) import OrderedCollections - import _CollectionsTestSupport +#endif class OrderedDictionaryElementsTests: CollectionTestCase { func test_elements_getter() { @@ -96,16 +99,25 @@ class OrderedDictionaryElementsTests: CollectionTestCase { func test_CustomDebugStringConvertible() { let a: OrderedDictionary = [:] - expectEqual(a.elements.debugDescription, - "OrderedDictionary.Elements([:])") + expectEqual(a.elements.debugDescription, "[:]") let b: OrderedDictionary = [0: 1] - expectEqual(b.elements.debugDescription, - "OrderedDictionary.Elements([0: 1])") + expectEqual(b.elements.debugDescription, "[0: 1]") let c: OrderedDictionary = [0: 1, 2: 3, 4: 5] - expectEqual(c.elements.debugDescription, - "OrderedDictionary.Elements([0: 1, 2: 3, 4: 5])") + expectEqual(c.elements.debugDescription, "[0: 1, 2: 3, 4: 5]") + } + + func test_SubSequence_descriptions() { + let d: OrderedDictionary = [ + "a": 1, + "b": 2 + ] + + let s = d.elements[0 ..< 1] + + expectEqual(s.description, #"["a": 1]"#) + expectEqual(s.debugDescription, #"["a": 1]"#) } func test_customReflectable() { diff --git a/Tests/OrderedCollectionsTests/OrderedDictionary/OrderedDictionary+Values Tests.swift b/Tests/OrderedCollectionsTests/OrderedDictionary/OrderedDictionary+Values Tests.swift index 6fc45ac51..e6c04040d 100644 --- a/Tests/OrderedCollectionsTests/OrderedDictionary/OrderedDictionary+Values Tests.swift +++ b/Tests/OrderedCollectionsTests/OrderedDictionary/OrderedDictionary+Values Tests.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 @@ -10,9 +10,12 @@ //===----------------------------------------------------------------------===// import XCTest +#if COLLECTIONS_SINGLE_MODULE +import Collections +#else @_spi(Testing) import OrderedCollections - import _CollectionsTestSupport +#endif class OrderedDictionaryValueTests: CollectionTestCase { func test_values_getter_equal() { @@ -59,6 +62,16 @@ class OrderedDictionaryValueTests: CollectionTestCase { expectEqualElements(d.values, [1, 2, 3, 4]) } + func test_descriptions() { + let d: OrderedDictionary = [ + "a": 1, + "b": 2 + ] + + expectEqual(d.values.description, "[1, 2]") + expectEqual(d.values.debugDescription, "[1, 2]") + } + func test_values_RandomAccessCollection() { withEvery("count", in: 0 ..< 30) { count in let keys = 0 ..< count diff --git a/Tests/OrderedCollectionsTests/OrderedSet/OrderedSet Diffing Tests.swift b/Tests/OrderedCollectionsTests/OrderedSet/OrderedSet Diffing Tests.swift index 1f06a148a..b5ea03f20 100644 --- a/Tests/OrderedCollectionsTests/OrderedSet/OrderedSet Diffing Tests.swift +++ b/Tests/OrderedCollectionsTests/OrderedSet/OrderedSet Diffing Tests.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 @@ -10,8 +10,12 @@ //===----------------------------------------------------------------------===// import XCTest +#if COLLECTIONS_SINGLE_MODULE +import Collections +#else import OrderedCollections import _CollectionsTestSupport +#endif class MeasuringHashable: Hashable { static var equalityChecks = 0 diff --git a/Tests/OrderedCollectionsTests/OrderedSet/OrderedSet.UnorderedView Tests.swift b/Tests/OrderedCollectionsTests/OrderedSet/OrderedSet.UnorderedView Tests.swift index b9021ca60..84b2ea99d 100644 --- a/Tests/OrderedCollectionsTests/OrderedSet/OrderedSet.UnorderedView Tests.swift +++ b/Tests/OrderedCollectionsTests/OrderedSet/OrderedSet.UnorderedView Tests.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 @@ -10,8 +10,15 @@ //===----------------------------------------------------------------------===// import XCTest +#if COLLECTIONS_SINGLE_MODULE +import Collections +#else @_spi(Testing) import OrderedCollections import _CollectionsTestSupport +#endif + +// Note: This cannot really work unless `UnorderedView` becomes a Collection. +// extension OrderedSet.UnorderedView: SetAPIChecker {} class OrderedSetUnorderedViewTests: CollectionTestCase { func test_unordered_insert() { diff --git a/Tests/OrderedCollectionsTests/OrderedSet/OrderedSetInternals.swift b/Tests/OrderedCollectionsTests/OrderedSet/OrderedSetInternals.swift index 397db56d3..5eb353409 100644 --- a/Tests/OrderedCollectionsTests/OrderedSet/OrderedSetInternals.swift +++ b/Tests/OrderedCollectionsTests/OrderedSet/OrderedSetInternals.swift @@ -2,15 +2,19 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 // //===----------------------------------------------------------------------===// +#if COLLECTIONS_SINGLE_MODULE +@_spi(Testing) import Collections +#else import _CollectionsTestSupport @_spi(Testing) import OrderedCollections +#endif struct OrderedSetLayout: Hashable, CustomStringConvertible { let scale: Int diff --git a/Tests/OrderedCollectionsTests/OrderedSet/OrderedSetTests.swift b/Tests/OrderedCollectionsTests/OrderedSet/OrderedSetTests.swift index f1e47ab6b..53e5cd77d 100644 --- a/Tests/OrderedCollectionsTests/OrderedSet/OrderedSetTests.swift +++ b/Tests/OrderedCollectionsTests/OrderedSet/OrderedSetTests.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 @@ -10,8 +10,14 @@ //===----------------------------------------------------------------------===// import XCTest +#if COLLECTIONS_SINGLE_MODULE +@_spi(Testing) import Collections +#else @_spi(Testing) import OrderedCollections import _CollectionsTestSupport +#endif + +extension OrderedSet: SetAPIExtras {} class OrderedSetTests: CollectionTestCase { func test_init_uncheckedUniqueElements_concrete() { @@ -141,20 +147,29 @@ class OrderedSetTests: CollectionTestCase { func test_CustomDebugStringConvertible() { let a: OrderedSet = [] - expectEqual(a.debugDescription, "OrderedSet([])") + expectEqual(a.debugDescription, "[]") let b: OrderedSet = [0] - expectEqual(b.debugDescription, "OrderedSet([0])") + expectEqual(b.debugDescription, "[0]") let c: OrderedSet = [0, 1, 2, 3, 4] - expectEqual(c.debugDescription, "OrderedSet([0, 1, 2, 3, 4])") + expectEqual(c.debugDescription, "[0, 1, 2, 3, 4]") + } + + func test_SubSequence_descriptions() { + let s: OrderedSet = [0, 1, 2, 3] + + let slice = s[1 ..< 3] + + expectEqual(slice.description, "[1, 2]") + expectEqual(slice.debugDescription, "[1, 2]") } func test_customReflectable() { do { let set: OrderedSet = [1, 2, 3] let mirror = Mirror(reflecting: set) - expectEqual(mirror.displayStyle, .collection) + expectEqual(mirror.displayStyle, .set) expectNil(mirror.superclassMirror) expectTrue(mirror.children.compactMap { $0.label }.isEmpty) // No label expectEqualElements(mirror.children.map { $0.value as? Int }, set.map { $0 }) @@ -938,6 +953,9 @@ class OrderedSetTests: CollectionTestCase { let actual1 = u1.union(u2) expectEqualElements(actual1, expected) + let actual1u = u1.union(u2.unordered) + expectEqualElements(actual1u, expected) + let actual2 = actual1.union(u2).union(u1) expectEqualElements(actual2, expected) } @@ -963,6 +981,26 @@ class OrderedSetTests: CollectionTestCase { } } + func test_formUnion_UnorderedView() { + withSampleRanges { r1, r2 in + let expected = Set(r1).union(r2).sorted() + + var res: OrderedSet = [] + + let u1 = OrderedSet(r1) + res.formUnion(u1.unordered) + expectEqualElements(res, r1) + + let u2 = OrderedSet(r2) + res.formUnion(u2.unordered) + expectEqualElements(res, expected) + + res.formUnion(u1.unordered) + res.formUnion(u2.unordered) + expectEqualElements(res, expected) + } + } + func test_union_generic() { withSampleRanges { r1, r2 in let expected = Set(r1).union(r2).sorted() @@ -972,6 +1010,13 @@ class OrderedSetTests: CollectionTestCase { let u3 = u2.union(r1) expectEqualElements(u3, expected) + + let a = Array(r2) + let actual3 = u1.union(a) + expectEqualElements(actual3, expected) + + let actual4 = u1.union(a + a) + expectEqualElements(actual4, expected) } } @@ -1002,6 +1047,9 @@ class OrderedSetTests: CollectionTestCase { let actual1 = u1.intersection(u2) expectEqualElements(actual1, expected) + let actual1u = u1.intersection(u2.unordered) + expectEqualElements(actual1u, expected) + let actual2 = actual1.intersection(r1) expectEqualElements(actual2, expected) } @@ -1024,6 +1072,23 @@ class OrderedSetTests: CollectionTestCase { } } + func test_formIntersection_UnorderedView() { + withSampleRanges { r1, r2 in + let expected = Set(r1).intersection(r2).sorted() + + let u1 = OrderedSet(r1) + let u2 = OrderedSet(r2) + var res = u1 + res.formIntersection(u2.unordered) + expectEqualElements(res, expected) + expectEqualElements(u1, r1) + + res.formIntersection(u1.unordered) + res.formIntersection(u2.unordered) + expectEqualElements(res, expected) + } + } + func test_intersection_generic() { withSampleRanges { r1, r2 in let expected = Set(r1).intersection(r2).sorted() @@ -1034,6 +1099,13 @@ class OrderedSetTests: CollectionTestCase { let actual2 = actual1.intersection(r1).intersection(r2) expectEqualElements(actual2, expected) + + let a = Array(r2) + let actual3 = u1.intersection(a) + expectEqualElements(actual3, expected) + + let actual4 = u1.intersection(a + a) + expectEqualElements(actual4, expected) } } @@ -1060,6 +1132,9 @@ class OrderedSetTests: CollectionTestCase { let actual1 = u1.symmetricDifference(u2) expectEqualElements(actual1, expected) + let actual1u = u1.symmetricDifference(u2.unordered) + expectEqualElements(actual1u, expected) + let actual2 = actual1.symmetricDifference(u1).symmetricDifference(u2) expectEqual(actual2.count, 0) } @@ -1082,6 +1157,23 @@ class OrderedSetTests: CollectionTestCase { } } + func test_formSymmetricDifference_UnorderedView() { + withSampleRanges { r1, r2 in + let expected = Set(r1).symmetricDifference(r2).sorted() + + let u1 = OrderedSet(r1) + let u2 = OrderedSet(r2) + var res = u1 + res.formSymmetricDifference(u2.unordered) + expectEqualElements(res, expected) + expectEqualElements(u1, r1) + + res.formSymmetricDifference(u1.unordered) + res.formSymmetricDifference(u2.unordered) + expectEqual(res.count, 0) + } + } + func test_symmetricDifference_generic() { withSampleRanges { r1, r2 in let expected = Set(r1).symmetricDifference(r2).sorted() @@ -1092,6 +1184,13 @@ class OrderedSetTests: CollectionTestCase { let actual2 = actual1.symmetricDifference(r1).symmetricDifference(r2) expectEqual(actual2.count, 0) + + let a = Array(r2) + let actual3 = u1.symmetricDifference(a) + expectEqualElements(actual3, expected) + + let actual4 = u1.symmetricDifference(a + a) + expectEqualElements(actual4, expected) } } @@ -1118,6 +1217,9 @@ class OrderedSetTests: CollectionTestCase { let actual1 = u1.subtracting(u2) expectEqualElements(actual1, expected) + let actual1u = u1.subtracting(u2.unordered) + expectEqualElements(actual1u, expected) + let actual2 = actual1.subtracting(u2) expectEqualElements(actual2, expected) } @@ -1139,6 +1241,22 @@ class OrderedSetTests: CollectionTestCase { } } + func test_subtract_UnorderedView() { + withSampleRanges { r1, r2 in + let expected = Set(r1).subtracting(r2).sorted() + + let u1 = OrderedSet(r1) + let u2 = OrderedSet(r2) + var res = u1 + res.subtract(u2.unordered) + expectEqualElements(res, expected) + expectEqualElements(u1, r1) + + res.subtract(u2.unordered) + expectEqualElements(res, expected) + } + } + func test_subtracting_generic() { withSampleRanges { r1, r2 in let expected = Set(r1).subtracting(r2).sorted() @@ -1149,6 +1267,13 @@ class OrderedSetTests: CollectionTestCase { let actual2 = actual1.subtracting(r2) expectEqualElements(actual2, expected) + + let a = Array(r2) + let actual3 = u1.subtracting(a) + expectEqualElements(actual3, expected) + + let actual4 = u1.subtracting(a + a) + expectEqualElements(actual4, expected) } } @@ -1196,169 +1321,198 @@ class OrderedSetTests: CollectionTestCase { } } - func test_isSubset_Self() { + func test_isEqual() { withEvery("unit", in: [1, 3, 7, 10, 20, 50]) { unit in SampleRanges(unit: unit).withEveryPair { r1, r2 in - let expected = Set(r1).isSubset(of: r2) + let expected = Set(r1) == Set(r2) let a = OrderedSet(r1) let b = OrderedSet(r2) - expectEqual(a.isSubset(of: b), expected) - } - } - } + let c = Set(r2) + let d = Array(r2) + + func checkSequence( + _ set: OrderedSet, + _ other: S + ) -> Bool where S.Element == Int { + set.isEqualSet(to: other) + } - func test_isSubset_Set() { - withEvery("unit", in: [1, 3, 7, 10, 20, 50]) { unit in - SampleRanges(unit: unit).withEveryPair { r1, r2 in - let expected = Set(r1).isSubset(of: r2) - let a = OrderedSet(r1) - let b = Set(r2) - expectEqual(a.isSubset(of: b), expected) + expectEqual(a.isEqualSet(to: b), expected) + expectEqual(a.isEqualSet(to: b.unordered), expected) + expectEqual(a.isEqualSet(to: c), expected) + + expectEqual(checkSequence(a, b), expected) + expectEqual(checkSequence(a, c), expected) + expectEqual(a.isEqualSet(to: d), expected) + expectEqual(a.isEqualSet(to: d + d), expected) + expectEqual(a.isEqualSet(to: r2), expected) } } } - func test_isSubset_generic() { + func test_isSubset() { withEvery("unit", in: [1, 3, 7, 10, 20, 50]) { unit in SampleRanges(unit: unit).withEveryPair { r1, r2 in let expected = Set(r1).isSubset(of: r2) let a = OrderedSet(r1) - let b = r2 + let b = OrderedSet(r2) + let c = Set(r2) + let d = Array(r2) + + func checkSequence( + _ set: OrderedSet, + _ other: S + ) -> Bool where S.Element == Int { + set.isSubset(of: other) + } + expectEqual(a.isSubset(of: b), expected) + expectEqual(a.isSubset(of: b.unordered), expected) + expectEqual(a.isSubset(of: c), expected) + + expectEqual(checkSequence(a, b), expected) + expectEqual(checkSequence(a, c), expected) + expectEqual(a.isSubset(of: d), expected) + expectEqual(a.isSubset(of: d + d), expected) + expectEqual(a.isSubset(of: r2), expected) } } } - func test_isSuperset_Self() { + func test_isSuperset() { withEvery("unit", in: [1, 3, 7, 10, 20, 50]) { unit in SampleRanges(unit: unit).withEveryPair { r1, r2 in let expected = Set(r1).isSuperset(of: r2) let a = OrderedSet(r1) let b = OrderedSet(r2) - expectEqual(a.isSuperset(of: b), expected) - } - } - } - - func test_isSuperset_Set() { - withEvery("unit", in: [1, 3, 7, 10, 20, 50]) { unit in - SampleRanges(unit: unit).withEveryPair { r1, r2 in - let expected = Set(r1).isSuperset(of: r2) - let a = OrderedSet(r1) - let b = Set(r2) - expectEqual(a.isSuperset(of: b), expected) - } - } - } + let c = Set(r2) + let d = Array(r2) + + func checkSequence( + _ set: OrderedSet, + _ other: S + ) -> Bool where S.Element == Int { + set.isSuperset(of: other) + } - func test_isSuperset_generic() { - withEvery("unit", in: [1, 3, 7, 10, 20, 50]) { unit in - SampleRanges(unit: unit).withEveryPair { r1, r2 in - let expected = Set(r1).isSuperset(of: r2) - let a = OrderedSet(r1) - let b = r2 expectEqual(a.isSuperset(of: b), expected) + expectEqual(a.isSuperset(of: b.unordered), expected) + expectEqual(a.isSuperset(of: c), expected) + + expectEqual(checkSequence(a, b), expected) + expectEqual(checkSequence(a, c), expected) + expectEqual(a.isSuperset(of: d), expected) + expectEqual(a.isSuperset(of: d + d), expected) + expectEqual(a.isSuperset(of: r2), expected) } } } - func test_isStrictSubset_Self() { + func test_isStrictSubset() { withEvery("unit", in: [1, 3, 7, 10, 20, 50]) { unit in SampleRanges(unit: unit).withEveryPair { r1, r2 in let expected = Set(r1).isStrictSubset(of: r2) let a = OrderedSet(r1) let b = OrderedSet(r2) - expectEqual(a.isStrictSubset(of: b), expected) - } - } - } + let c = Set(r2) + let d = Array(r2) + + func checkSequence( + _ set: OrderedSet, + _ other: S + ) -> Bool where S.Element == Int { + set.isStrictSubset(of: other) + } - func test_isStrictSubset_Set() { - withEvery("unit", in: [1, 3, 7, 10, 20, 50]) { unit in - SampleRanges(unit: unit).withEveryPair { r1, r2 in - let expected = Set(r1).isStrictSubset(of: r2) - let a = OrderedSet(r1) - let b = Set(r2) expectEqual(a.isStrictSubset(of: b), expected) + expectEqual(a.isStrictSubset(of: b.unordered), expected) + expectEqual(a.isStrictSubset(of: c), expected) + + expectEqual(checkSequence(a, b), expected) + expectEqual(checkSequence(a, c), expected) + expectEqual(a.isStrictSubset(of: d), expected) + expectEqual(a.isStrictSubset(of: d + d), expected) + expectEqual(a.isStrictSubset(of: r2), expected) } } } - func test_isStrictSubset_generic() { - withEvery("unit", in: [1, 3, 7, 10, 20, 50]) { unit in - SampleRanges(unit: unit).withEveryPair { r1, r2 in - let expected = Set(r1).isStrictSubset(of: r2) - let a = OrderedSet(r1) - let b = r2 - expectEqual(a.isStrictSubset(of: b), expected) - } - } - } - - func test_isStrictSuperset_Self() { + func test_isStrictSuperset() { withEvery("unit", in: [1, 3, 7, 10, 20, 50]) { unit in SampleRanges(unit: unit).withEveryPair { r1, r2 in let expected = Set(r1).isStrictSuperset(of: r2) let a = OrderedSet(r1) let b = OrderedSet(r2) - expectEqual(a.isStrictSuperset(of: b), expected) - } - } - } + let c = Set(r2) + let d = Array(r2) + + func checkSequence( + _ set: OrderedSet, + _ other: S + ) -> Bool where S.Element == Int { + set.isStrictSuperset(of: other) + } - func test_isStrictSuperset_Set() { - withEvery("unit", in: [1, 3, 7, 10, 20, 50]) { unit in - SampleRanges(unit: unit).withEveryPair { r1, r2 in - let expected = Set(r1).isStrictSuperset(of: r2) - let a = OrderedSet(r1) - let b = Set(r2) expectEqual(a.isStrictSuperset(of: b), expected) + expectEqual(a.isStrictSuperset(of: b.unordered), expected) + expectEqual(a.isStrictSuperset(of: c), expected) + + expectEqual(checkSequence(a, b), expected) + expectEqual(checkSequence(a, c), expected) + expectEqual(a.isStrictSuperset(of: d), expected) + expectEqual(a.isStrictSuperset(of: d + d), expected) + expectEqual(a.isStrictSuperset(of: r2), expected) } } } - func test_isStrictSuperset_generic() { - withEvery("unit", in: [1, 3, 7, 10, 20, 50]) { unit in - SampleRanges(unit: unit).withEveryPair { r1, r2 in - let expected = Set(r1).isStrictSuperset(of: r2) - let a = OrderedSet(r1) - let b = r2 - expectEqual(a.isStrictSuperset(of: b), expected) - } - } - } - - func test_isDisjoint_Self() { + func test_isDisjoint() { withEvery("unit", in: [1, 3, 7, 10, 20, 50]) { unit in SampleRanges(unit: unit).withEveryPair { r1, r2 in let expected = Set(r1).isDisjoint(with: r2) let a = OrderedSet(r1) let b = OrderedSet(r2) + let c = Set(r2) + let d = Array(r2) + + func checkSequence( + _ set: OrderedSet, + _ other: S + ) -> Bool where S.Element == Int { + set.isDisjoint(with: other) + } + expectEqual(a.isDisjoint(with: b), expected) + expectEqual(a.isDisjoint(with: b.unordered), expected) + expectEqual(a.isDisjoint(with: c), expected) + + expectEqual(checkSequence(a, b), expected) + expectEqual(checkSequence(a, c), expected) + expectEqual(a.isDisjoint(with: d), expected) + expectEqual(a.isDisjoint(with: d + d), expected) + expectEqual(a.isDisjoint(with: r2), expected) } } } - func test_isDisjoint_Set() { - withEvery("unit", in: [1, 3, 7, 10, 20, 50]) { unit in - SampleRanges(unit: unit).withEveryPair { r1, r2 in - let expected = Set(r1).isDisjoint(with: r2) - let a = OrderedSet(r1) - let b = Set(r2) - expectEqual(a.isDisjoint(with: b), expected) + func test_filter() { + withOrderedSetLayouts(scales: [0, 5, 6]) { layout in + withEvery("factor", in: [1, 2, 3, 5, 10]) { factor in + let count = layout.count + let input = OrderedSet(layout: layout, contents: 0 ..< count) + let expected = (0 ..< count).filter { $0 % factor == 0 } + let actual = input.filter { $0 % factor == 0 } + + expectEqualElements(actual, expected) } } } + func test_filter_type() { + let s = Set([1, 2, 3, 4]).filter { $0.isMultiple(of: 2) } + expectType(s, Set.self) - func test_isDisjoint_generic() { - withEvery("unit", in: [1, 3, 7, 10, 20, 50]) { unit in - SampleRanges(unit: unit).withEveryPair { r1, r2 in - let expected = Set(r1).isDisjoint(with: r2) - let a = OrderedSet(r1) - let b = r2 - expectEqual(a.isDisjoint(with: b), expected) - } - } + let os = OrderedSet([1, 2, 3, 4]).filter { $0.isMultiple(of: 2) } + expectType(os, OrderedSet.self) } func test_equal() { diff --git a/Tests/OrderedCollectionsTests/OrderedSet/RandomAccessCollection+Offsets.swift b/Tests/OrderedCollectionsTests/OrderedSet/RandomAccessCollection+Extras.swift similarity index 91% rename from Tests/OrderedCollectionsTests/OrderedSet/RandomAccessCollection+Offsets.swift rename to Tests/OrderedCollectionsTests/OrderedSet/RandomAccessCollection+Extras.swift index 79db7eded..d4e327c2b 100644 --- a/Tests/OrderedCollectionsTests/OrderedSet/RandomAccessCollection+Offsets.swift +++ b/Tests/OrderedCollectionsTests/OrderedSet/RandomAccessCollection+Extras.swift @@ -2,14 +2,19 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 // //===----------------------------------------------------------------------===// +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsUtilities +#endif + extension RandomAccessCollection { + #if SWIFT_PACKAGE @inline(__always) internal func _index(at offset: Int) -> Index { index(startIndex, offsetBy: offset) @@ -20,6 +25,12 @@ extension RandomAccessCollection { distance(from: startIndex, to: index) } + @inline(__always) + internal subscript(_offset offset: Int) -> Element { + self[_index(at: offset)] + } + #endif + @inline(__always) internal func _indexRange(at offsets: Range) -> Range { _index(at: offsets.lowerBound) ..< _index(at: offsets.upperBound) @@ -42,11 +53,6 @@ extension RandomAccessCollection { return _offsetRange(of: range.relative(to: self)) } - @inline(__always) - internal subscript(_offset offset: Int) -> Element { - self[_index(at: offset)] - } - @inline(__always) internal subscript(_offsets range: Range) -> SubSequence { self[_indexRange(at: range)] diff --git a/Tests/README.md b/Tests/README.md new file mode 100644 index 000000000..701eda8a5 --- /dev/null +++ b/Tests/README.md @@ -0,0 +1,3 @@ +# Unit tests + +Beware! The contents of this directory are not source stable. They are provided as is, with no compatibility promises across package releases. Future versions of this package can arbitrarily change these files or remove them, without any advance notice. (This can include patch releases.) diff --git a/Tests/RopeModuleTests/Availability.swift b/Tests/RopeModuleTests/Availability.swift new file mode 100644 index 000000000..66234e4a4 --- /dev/null +++ b/Tests/RopeModuleTests/Availability.swift @@ -0,0 +1,33 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +var isRunningOnSwiftStdlib5_8: Bool { + if #available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) { + return true + } + return false +} + + +#if swift(<5.8) +extension String.Index { + var _description: String { + let raw = unsafeBitCast(self, to: UInt64.self) + let encodedOffset = Int(truncatingIfNeeded: raw &>> 16) + let transcodedOffset = Int(truncatingIfNeeded: (raw &>> 14) & 0x3) + var d = "\(encodedOffset)[unknown]" + if transcodedOffset > 0 { + d += "+\(transcodedOffset)" + } + return d + } +} +#endif diff --git a/Tests/RopeModuleTests/SampleStrings.swift b/Tests/RopeModuleTests/SampleStrings.swift new file mode 100644 index 000000000..f0da14543 --- /dev/null +++ b/Tests/RopeModuleTests/SampleStrings.swift @@ -0,0 +1,168 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import _CollectionsTestSupport +#endif + +func randomStride( + from start: Int, + to end: Int, + by maxStep: Int, + seed: Int +) -> UnfoldSequence { + var rng = RepeatableRandomNumberGenerator(seed: seed) + return sequence(state: start, next: { + $0 += Int.random(in: 1 ... maxStep, using: &rng) + guard $0 < end else { return nil } + return $0 + }) +} + +let sampleString: String = { + var str = #""" + The powerful programming language that is also easy to learn. + Swift is a powerful and intuitive programming language for iOS, iPadOS, macOS, \# + tvOS, and watchOS. Writing Swift code is interactive and fun, the syntax is \# + concise yet expressive, and Swift includes modern features developers love. \# + Swift code is safe by design and produces software that runs lightning-fast. + + 一种强大但极易学习的编程语言。 + Swift 是一种强大直观的编程语言,适用于 iOS、iPadOS、macOS、Apple tvOS 和 watchOS。\# + 编写 Swift 代码的过程充满了乐趣和互动。Swift 语法简洁,但表现力强,更包含了开发者喜爱的现代功能。\# + Swift 代码从设计上保证安全,并能开发出运行快如闪电的软件。 + + パワフルなプログラミング言語でありながら、簡単に習得することができます。 + Swiftは、iOS、iPadOS、macOS、tvOS、watchOS向けのパワフルで直感的なプログラミング言語です。\# + Swiftのコーディングはインタラクティブで楽しく、構文はシンプルでいて表現力に富んでいます。\# + さらに、デベロッパが求める最新の機能も備えています。安全性を重視しつつ非常に軽快に動作す\# + るソフトウェアを作り出すことができます。それがSwiftです。 + + 손쉽게 학습할 수 있는 강력한 프로그래밍 언어. + Swift는 iOS, iPadOS, macOS, tvOS 및 watchOS를 위한 강력하고 직관적인 프로그래밍 언어입니다. \# + Swift 코드 작성은 대화식으로 재미있고, 구문은 간결하면서도 표현력이 풍부하며, Swift에는 개발자들이 \# + 좋아하는 첨단 기능이 포함되어 있습니다. Swift 코드는 안전하게 설계되었으며 빛의 속도로 빠르게 실행되는 \# + 소프트웨어를 제작할 수 있습니다. + + 🪙 A 🥞 short 🍰 piece 🫘 of 🌰 text 👨‍👨‍👧‍👧 with 👨‍👩‍👦 some 🚶🏽 emoji 🇺🇸🇨🇦 characters 🧈 + some🔩times 🛺 placed 🎣 in 🥌 the 🆘 mid🔀dle 🇦🇶or🏁 around 🏳️‍🌈 a 🍇 w🍑o🥒r🥨d + + ⌘⏎ ⌃⇧⌥⌘W + ¯\_(ツ)_/¯ + ಠ_ಠ + + 🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦 + """# + if MemoryLayout.size == 8 { + /// Add even more flags and extra long combining sequences. This considerably increases test + /// workload. (Not necessarily due to grapheme cluster length, but because test performance is + /// quadratic or worse in the size of the overall text.) + str += #""" + 🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦\# + 🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦🇺🇸🇨🇦 + + Unicode is such fun! + U̷n̷i̷c̷o̴d̴e̷ ̶i̸s̷ ̸s̵u̵c̸h̷ ̸f̵u̷n̴!̵ + U̴̡̲͋̾n̵̻̳͌ì̶̠̕c̴̭̈͘ǫ̷̯͋̊d̸͖̩̈̈́ḛ̴́ ̴̟͎͐̈i̴̦̓s̴̜̱͘ ̶̲̮̚s̶̙̞͘u̵͕̯̎̽c̵̛͕̜̓h̶̘̍̽ ̸̜̞̿f̵̤̽ṷ̴͇̎͘ń̷͓̒!̷͍̾̚ + U̷̢̢̧̨̼̬̰̪͓̞̠͔̗̼̙͕͕̭̻̗̮̮̥̣͉̫͉̬̲̺͍̺͊̂ͅn̶̨̢̨̯͓̹̝̲̣̖̞̼̺̬̤̝̊̌́̑̋̋͜͝ͅḭ̸̦̺̺͉̳͎́͑c̵̛̘̥̮̙̥̟̘̝͙̤̮͉͔̭̺̺̅̀̽̒̽̏̊̆͒͌̂͌̌̓̈́̐̔̿̂͑͠͝͝ͅö̶̱̠̱̤̙͚͖̳̜̰̹̖̣̻͎͉̞̫̬̯͕̝͔̝̟̘͔̙̪̭̲́̆̂͑̌͂̉̀̓́̏̎̋͗͛͆̌̽͌̄̎̚͝͝͝͝ͅd̶̨̨̡̡͙̟͉̱̗̝͙͍̮͍̘̮͔͑e̶̢͕̦̜͔̘̘̝͈̪̖̺̥̺̹͉͎͈̫̯̯̻͑͑̿̽͂̀̽͋́̎̈́̈̿͆̿̒̈́̽̔̇͐͛̀̓͆̏̾̀̌̈́̆̽̕ͅ ̷̢̳̫̣̼̙̯̤̬̥̱͓̹͇̽̄̄̋̿̐̇̌̒̾̑̆̈́̏͐̒̈̋̎͐̿̽̆̉͋͊̀̍͘̕̕̕͝͠͠͝ͅͅì̸̢̧̨̨̮͇̤͍̭̜̗̪̪͖̭͇͔̜̗͈̫̩͔̗͔̜̖̲̱͍̗̱̩͍̘̜̙̩͔̏̋̓̊́́̋̐̌͊͘̕͠s̶̨̢̧̥̲̖̝̩͖̱͋́͑͐̇̐̔̀̉͒͒́̐̉̔͘͠͠ ̵̧̛͕̦̭̣̝̩͕̠͎̮͓͉̟̠̘͎͋͗͆̋̌̓̃̏̊̔̾̒̿s̸̟͚̪̘̰̮͉̖̝̅̓͛̏̆ư̵͍̙̠͍̜͖͔̮̠̦̤̣̯̘̲͍͂͌̌̅̍͌̈́̆̋̎͋̓̍͆̃̑͌͘̕͜ͅç̸̟̗͉̟̤̙̹͓̖͇̳̈́̍̏͐̓̓̈̆̉̈͆̍ͅh̵̛̛̹̪͇͓̤̺̟͙̣̰͓̺̩̤̘̫͔̺͙͌́̑̓͗̏͆́͊̈́̋̿͒̐̀́̌͜͜͝ ̴̗͓͚͖̣̥͛́̓͐͂͛̐͑̈́͗̂̈͠f̶̡̩̟̤̭̩̱̥͈̼̥̳͕̣͓̱̰͎̖̦͎̦̻̫͉̝̗̝͚̎͌͑̾̿̊̉͆̉̏̅̔̓̈́̀͐̚͘ͅư̷̦̮͖͙̺̱̼̜̺̤͎̜͐͐̊̊̈͋̔̓̍͊̇̊̈́̈͑̐̎̿̑̋͋̀̅̓͛̚͜n̷̡̨͉̠̖̙͎̳̠̦̼̻̲̳̿̀̓̍͋̎͆̓̇̾̅͊̐͘͘̕!̷̡̨̧̢̡̡̼̹̭̝̝̭̫̫̥̰̤̪̦̤̼̖̖̳̰̲͙͕̖̬̳̪͖̹̮͐͐͊̈́̐͑͛̾̈͊̊͋͑̉͒̈̿̈̃̑͋͐́͊̀͝͠͝͠ + Ų̷̧̧̧̧̡̢̨̨̢̢̢̧̡̨̨̧̢̡̡̢̨̨̮̜͈̳̮͔̺͚̹͉͍̫̪͖͙͙̳͖͖̦̮̜̫̗̣̙̪͇̩̻̬̖̝̻̰͙̖̙̭̤͎̠͇̹̦̤̟̦͎̹̝̗̫͔̳̣̦͍̹͈̺̮͈͈̬̭̘͕̟͉̮̟͖̦̥͇̠̙̳̲̝̦͖̻̪̺̬̫͈͈̘͍͚͖̝̥̙͖̪͔̫̣̘͙͓̱̠̲̯̦͓͖͚͎͉͖̘̺͕͇̱̺̗̙̮̮̹̯̤̮̺͓̘̫͕̞̮͕̠̗͍̦̣̮͙͉͈̭̜̭̘̼̼̖̮̘̝͈̌̑͋͂̈́̐̄̂̆̊̈̅͆͋̔͗̍͒̆̐̒̽͑̋́͛͗̓̃̽͋̒͑̈́̕̕̚̕̕͜͜͝͠͝͠͝ͅͅņ̴̡̢̡̢̢̨̛̛̛̛̛̛̛̻̬̲̰̗̭͕̯͇̩̦̮̫̭̰̪͉̹̭͇̣̦͕̹̗̭̬͓͕͍̯͇͕̩̱̲͍̟̙͓̣̖̱͍̟͚͔̞̪͕̣̺̻̭̖̤̜͈̰̻̘̹̝̝̮̗͔̯̻̻͍͕̬͇͓̲̗̟̭̰̬̳͈̼̤̙̱̻̜͍̪̣̈̉̉͑̇̇͐̆̆̀̋̄̂̿͒͐͒͛̒̍͆̿͐͊̂̿̐̇̋̄̈̂̓̅̇̈́̾̒̔̍̈́͐̋͊̑͐͆̈́̿́̽͛́̊̏̓̾̇̈́̀̃͐̃̈́́͒̏̀͑̑̅̈́̇͂̓̐̒́̾̈́͗̋̅̀́͋̍͑͒̌̔̈́̆̂̉͌̈́̑̾̑̽̓̓̏̄͋̽̒̓͌̊̂̀͑̂͑̌͋̓͐̈̃̾̏̃̏͑̋̒̈̀̔͐̓̋̉̐̐͋͆̂̈́̒͊̏̓̔͌̈̾͗͑͂̾͆͑̂̍̂͗͆̄̊̐̏̈́͂̌͐̃̐͌̊̀͗̐͑̔͋͒̊̋̒̐̄͐̏̓͌̃̌͋̔̎̈̆̍̃̿́̇̉̀́̊̅̌̏̆̆͛̔͋̈̽̂͂̇̓́͛͗̔̍̃̈̽͑͐̿̽̉̓̎̔͗̊͂̽͘̕̚̕͘̕̕͘̚͘̚̕̚̚̚͘̚͜͜͝͝͠͝͝͠͝͝͝͝͝͝͝͝͝͝͠͝ͅͅͅi̴̧̡̢̡̧̢̢̢̧̡̧̢̡̹̤̗̭̭̭̪̳̮͉̦̪͉͈̦̗̣̼̻̜̰̳̯͕̩̘̙̯̼͉̖͕̰͓͚͙̠̫̞̰̰̪͖̹̬̥̣̞̯̳̘̙͖̪̗̼̹̝̣͕̯̺̱͉̻̖͓͙̘̗̖̠̫̥̻̱̖͇̬͇̹͕͍̗̻̝̻͕̫̱̻͈̫͕̜̼͎̮̘̫̮̭͉̜͔͈̻͕͍̠̞͔̪̳͕̰͈͖͇͍͇͎͕̙̟͔͔͇̭̥̠͇̖͎͙̖̫͚͇͕̮̳̯̟̺̺̪̪̙̣̥̰̜̺̥̭̦̤̲͓̮̦̹͙̼͓̼̮̙̮̠͎͍͖̇͐͐͐̿́͊̃̀͋̔͑̓͂͌͗̋̇̎͛͊̋̔̓̇̚͜ͅč̷̨̡̡̨̫̗̠̥̫̩͕͉͉͇̮͉̲͎̭̝̬͚̜̮̼̰̭̞̘̠̘̰̹͈̯̫̟͓͙̻̤̰͈̌͂̓͐̑̌̏̊͂͂̉͌̐̇̎͋̍̉̑̃̇̃̎̓͋͛̑͊̾͊̔̍̄͂͘͘͝͝͠ǫ̴̡̧̨̧̧̢̢̧̧̢̨̢̨̡̧̨̡̡̨̨̨̡̡̨̡̨̨̨̧̧̡̛̛̤͚̖̫̰̣̣͍̱̜͈̻̙̲̙͚͚̖͕̠̼̲͚̯͚̳̻͇̘̲̦͕̦̜̣̙̣̣̜̰̝͕̤̝̫̺̳͙̮̬̪̹̲͍̣̹̙̠̫̘̥̦̘̰̞̙̟̟̤͓̙͖̣͓͔͓̩̩͈̗̤͇̠̞̩̮̪̥̪̱͚͍̝͕͎̞͔̩̖̲͔͈̩̻̩͕̫͓̳̙͓̞̟͙͉͉̬̮̗͓̱̲̮̯͇͕̰̖͚̦͈̞̺̠̳͕̭̭̳͓̻̯̞̳͔͍̟̬̳̩͚͎̲̹͍͇͈̙̳̞̖̗̯͖̱͖̯̤̠̲͍̩͈̭̙͈̲̱̲̼̩̘̘̜͔̲̱̯͔̈́̈́̋̿̋̿͋͋͌̏̽̇͗͊͑̔͆͌̿̋͌̋͗̉̊̿̐̄̈́̈́̈̄̆̅̃́̄̅̍́̽͒̈́̽̄̌̇̓̈͂͆̊͆̿́͗͛̅̋́͆͑͛̆̔̍̀̍̃̎͒̀͋͛̽́̏̄̓̌͘͘͘̚̕̕͘͜͜͜͜͜͜͠͠͠͝͠͝͝ͅͅͅd̷̨̧̧̢̢̧̛̛̛̛̛̛̛͖̭̘̺͕̜̬̤̭̬̠̫̤͍͚̪̬̣͉̳̮̱͖̻̟͓̫̹͚̝̗̳̰̺͍͎͉̟̱̜̫͇̫̯̼̠̞̝̤͖̖̻͍͖̰̻͕̙͙͚͈̱̝͉͙̘̰͚̩̗̟͕̞̞̼̣͖̜̳̥̼͉̘͈̘̩͕̦̺̝̟̼͍̥̲̤̪̗̀͌̊̃̆̑̓̓̌͌̏̎͋̀͌͐̈́̀̂̍̒̅̓̏̓̈́̀̀͊̒̎̓́̒̔̉̀̍̿̒͛̍̍̅̇͑̆͒̓̌̑̏̏̈͊̈́́͌̀̃̆́̔̃̀͛̾̅́̿̀́̿̒͆́̍̂̀̿̆͑̊̉͆́̒̑̅̽̂̄͂̏̿̍̽̃͂̈́̀͌̒͗̅̉̎̓̐̀̌̿̓̈̓͛̽̄̉̑̄̊͂̀̽̔̇̍̀͂̇̈͊͐͗̽͐͊͐̑͘͘͘͘̕̚̕̕̚̚̕͘͘͜͜͜͜͜͝͝͝͝͠͠͝͠͝͠͠͝͝͠ͅͅͅę̴̨̡̨̡̢̛͈̳̞͈͇͕͙̪̩̼̩̗̲̳̹̯̖̙̱͔̺̪͇̜̼̍͌̆̅̽͛̏̑͊͒͊͌́̇̄͊́̏́̄̈͆̿͌̌̀̈̃͊̈̀̽͒͑̊́̍̑̃̒̐́̓̈̃̀͛̽̔̎̀̄̑͌̾͒̊̀̓̆̀̕̕͘͜͜͠͝͠͝ͅͅ ̴̨̡̢̢̢̢̛̛̛̛̛̛̛̛̼̻̬̪͎̖̭̯̤̥̭͚̖̖͚̳͍͎̻̰̯̗̭͔͎͇̖̮̻̯̰̯̦̗͔̺͔̩͈̫̣̪͕̜͇͓͓̅͐́̃̿̀̍͂̽̂͒̇̉̿̑̀̈̒̇͋͗̓͑̒̿͒̃̏̏̔͐̓͐̽͛̆̈͗̿͆́͌́̀̇̈́̓̄̾̇̈́̀͑̽̔͒̌͑̓́̀̈́̀͊̓̏̾̿̓͒̅͋̓̂͆͊̎̾̆̀̾̏̆̈̿̆͗̀̿͊̒̌̓́͆̈́͂̍͆͗͌̇̇͋̅̍̈́̊̽̈́̑́̅͐́̌̉͆͊͑̓̿͆̊͒̑̑̉̑̔̀̀́̐̓̽͂͌̾͑̌͑̓͒̀͗̈́̑̀̋͂͆̓̍̆́͛̈́̈́̀́̀͋͂͛̎̑̌͊̅͑̔̓͛͂̓̇̾́͌̓́̆͋̓̓͑̔̈́̎̍͊̃̋̃͌͛̓́̔͆̈͐͒̂̅̂̓͂̋̅̽̏̉̎̊̈̿̾̊̃͆̆͊͂̎͋̌͊͌̍̄̔͒̄͗̈́͒̇̕̕̕̕̕̚͘̚̕͘̕̚͘͘͘̕͘̚͘̕̚͝͝͝͠͝͠͝͝͝͝͝͠͠͝͝͝͠͠͝͝ͅͅį̴̛͕͍̠̩͎͇̳̪̱̖̝͙͉̩̩̯̜͕͓͕̀͂͛̃͑̉͐̏͑́̒̃̑̐̾̈͐͝ͅs̶̨̢̨̡̧̡̡̧̨̪̱̪̙̤̥̺̰͚̦̞̫̟̭̟̠̗̺̲̺̹͙͇͈̭̱̪͔̦̦̻̭͙̱̱̬̙̺̤̤̙͈̭͖̯͇̞͙͈̟͓̖̠͚̳̤̺̙̤͔̯͍͔͖̱͈͍̞͚̗̭̮̣͕̻̝̮̯͐̑̃̌̐̈́̌̈̔́͋̂͊̿̈́̉̄̆̎̃̏͑̈̑̔̋͐̂̽̈́̔͗̒̌́̓̉̕̕͜͜͠ͅͅ ̴̢̡̧̧̡̢̡̨̢̢̧̡̧̨̡̡̨̧̧̡̨̛̛̛̱͚̭̯̯̘͍͕͓̱̯̩̪̠͓̫͕̖̠̤̱͕̬̞̘̭̗͍͙͚͎̗̫̘̹̫͔̹̱̟̻̬̞͙͇͉͔͙͍̟͙͈̪̞̤̪͉̫̠̤̫̭̦͍̰̪͎̠̲̣̰̠͍̪̦̞̬̘̟̳̣̼̜̻̬̗͎͓͓̳̙̳̩͙̼̬͍̝͓̲̰̤͇͚͖̠̹͖͓̜̳̳̼͈͈̝̘̹̪̱̳̱͎̙̳̩͕̞̻͍͓̗̪̖̣͚̤͇͈̳͓̝̗͔͇̖̲͙̤͉̺̮͔̞̫̱̮̻͇̼̯̹͓̥̪̩̹̳̰͍͓̖̟̮͉͔̰͙̲͓͇͉̞͓̥̖̗̘̜͖̱̯͎̺͓̬͎͕̘̻̻̥̲̖̬̯̰̞̜̫̬̪̲͎̠̳̥̫̜̠͍̼̟͓͈̻͍͈̙̮̠̱̻̫̼̯̜̯͓́͂͊̽͑̾̃̽̈́̒̓̒́̑̽͗̃̏̏̿̅̃͑̒́͌̈́́̒͊͊̆́͒̒̓͌̊͆̿̉̈́̇̑̃̇̋̾̒̽̎̍́̕̚̕̕̕̕͘̚̚͜͜͝͠͝͠͠͝ͅͅͅͅͅͅs̸̨̨̨̢̡̢̨̡̢̢̨̧̡̧̡̧̡̧̧̛̯͕̦̪̹̦͓͓̮̹͈̩͎̗̻͍̪̩̮͖̺͕͉̲̖̹̹̻͈̗͎̮̬͔̦̹͔̞̳̙̤͙͈̗̪̥̦͉̯̮͓̰̙̝͇̦̤̳̣̦͎̬̬͈͖̙̺͉̥̮̖͕̗̗͓̥͔̥̬̘͉̠̝͕̥̦̙͉͎͚͔̖͍͓̖̩̳͚͔̟̰̝̳͖̲̬̗̹͈͙̳̘̠̱͇͎̗̞̳̯̣͖͎͇̮̞̗̻̞̱̪̳͓̣̱͙̩̼͍͖̭͓͇̗̫͔̗̘̤͖͈̦̭̻͓̤͚͍̜̝̯͍͓͖̥̳̮͓̦͕̦͕̱͉̗͙̫̞͔̪͍̭͕̄̊͒͌͛̅̑̅̂͒̂̈́̃̂̀́̈͋̑̋̃̊̇̈̄̽̃̆̑̔͆̂̍͑͛͊̇̒̍̂̏̋̓̂́͐͆̎̿́̑̚̕͘̕͘͘͜͜͜͜͜͜͜͝͝͝͝͝͝ͅͅu̴̢̧̨̡̨̡̨̢̢̧̨̧̡̧̨̢̢̡̢̡̨̨̢̡̡̧̢̨͈͔̟̯͇̻͙̬̟͖̘͎͈̘͙̬̰͉̟̠͉̻̯͖̼̪͎͓͚̟̞̺͖̞͕͚̗͇͔̩̼̪͔̺̯̹͍̮̗͍͚̻̙̹͙͉͈̙͔̜̬̙̺̥̬̜̩̜̟̘̪͍̤̤̪͈͈͖̲̥͇̣͈̥͖̩̞̬̟̺̻̩̝͉̮̜̖͖̺͉̺̱̖̗̰͕͓̼̱̥̠̖̫̱̖̝̤̭̲̭̖̙͎̫̰͈̲͈̣̪̣̳͓̝͚̘̪̞͖̩̮̗̱͈͉̰̻̻̠̞͙̭̰̪͙̝̰̞͖̩͖͇̩̺̗̬̦͙͉̬̜̱̰̱͓̪͙̮̝̼̙̻͔͎̱͖͙͓̣̼̩̰̗͖̱̞̼͇̙̦̹̯̖͇̫͕͍̒̀̂̿͆̊̔̐̿̔̀͆͂̅̽̽̋̊̈́̈́̌͂̿̀̌́̔̉̑́̓̒̃̿͛̓̋̆͆́̈́̆̍̔͊͗̏̆̈́̑͑̓̀̆͘͘̕͘͘̚͜͜͜͜͝͝͠͝ͅͅͅͅͅc̶̡̢̢̢̢̨̡̨̢̧̢̨̢̨̧̢̢̡͍̖͎̪͉̼̮̲̣̪̘̮̯͖͖̼̯͙̻̮͍̲̖̙͕͖̯̠̪̯̲̞̞̠̳͈͚̜̟̙̫͎̫̱̩͈͚͎̮̱̝̼͚̺͚͇̪̱̫͇̱͈̟̲͇͔̝̯͎̗̣̘̘̺͈̼̦̖̺̖͉̬̫̥̲̣̞͔̣̣͚̤͇̻̫͉̥̖̦̫̪̠͈̙̰͈̤̤͎͕͎̙͔̪̭̼̞̙͇͕͎͔̼̘̖̦͚͔͉̫͕͔̜̮̱͉̠͓̪͕̼̳̖͙͍̭̬̞̻̬͔͕̑̐̄̆́͊͂͗̒̐̅̾͗̉̕͜͜͜͝ͅḫ̸̢̧̧̧̧̡̢̢̢̫̝̬̣̺̠̯̮͚̦̩͍̻̯̪̪̝̩̹̠̘̤͓͇̪͍̲̠͍̝͉̭̲̘̼͙͍̜̙̣̫̪̬͓̻̤͚͖͛̈̀̌͜͜͝ͅ ̴̨̢̡̡̨̨̡̢̧̡̨̡̡̛̛̛̛̹̭̗̖̹̰̼̗̳̹̯͔͚̻͚̙̹̰̪̺̩͈̳͉̼̗̝̳͖̞̯̠̭̯͎͓͎̘͉̺͇̬͇̯̜̯͓̳̞͚͍̭̯̦̺̳̘̰̲̲̜͓͔̼̺͍̟̠̙̱̞̲͉̣̮̭̗͈͕͚͚̣͓̻͍̩̣̻̲̳̹̲̫̮͚̲͍͎̰̮̮̯͖̰̥̝̮̞͍͇̹̹̫̞͔̫̭̥͉͉̱̯̻̥͈̑̔͊̆̌̇̇̌͆̍̓͛̅̂̊̂̃̌̋́͑̐̄̃̾̔͗̿͛̊̀̉͐̋͑̇̿̃̏̈́̏̔́̌̿̈́̃̈́́̈́̄̃͌̆̅̓̎̽́̑͊̑̈͛̌́̈́̿̿̐͐͗̾͛̉̐͊̏̉̏̉̏̌͋͊̍͒͐̑́̽̈́͊́̃̂͂̓̂͌̓̉̏͛̍̍̄̐̃͐̐͒̉̏̈̐̒̔̈́͒͆̈́̾̄͛̋̔̿̅́̃͌̎͐̀̿̊̍̿̊͌̚̚̚͘͘͘̚̕̕͜͜͜͜͠͠͠͠͝͝͝͝͝ͅͅf̶̨̢̡̢̢̢̢̡̨̡̢̧̢̨̨̧̢̨̨̨̨̢̧̢̢̦͇̼̹̫͔̬͔͎͇̱͉̜̤̟̩̖̯̞̱̗̺͎͖̙͖̱̻̩̮̯̲̝̥̟̠̰̘̮̖̹̲͖̖̪͖̲̖̫͈̞̫̣̗̖̝̹̟̙͓͙͖̺͉͚̪̣͓̮͉̦̪͕̗͈̩̗̤͈̮̱͈̙͉͙̭̼̺̻͙̟̥̬̤̤̺̼͚̣̗͙͇̬̭͇̖͇̖̣͎̜̭͙̠̤̫̫͙͕̺̱͙̟̤̦͓̦͍̜̖͉̥̤͓̹͉̮͇̙͍̙̠̳̲͉̦̮̣͇̠͕͙̫̗̲̘̞͎͕̗̹̞̟̺̥̤̥̪̝̹͎͚̦͍͓͓̘̟̼͙̯̠̮̖̲̲̪̜̖̝̙͉͚̼̳̘̘̻͕̘͓̜̯͙͕͉̬̲̲̼̥̭̰͕̣̻̬̫̬̼̖͈̺̞̹̘̭͈̼̣̮̘̜̗̦̬͓͇͉͍̮͕̲͙̗͍̝̝͉͔̻͔̭͇̟̞̜̘̗̥̲̉̎̀͛̈͊̋̾͋͑̾̃̽̽̓͆̉̃́̈́́͐͂͜͜͜͜͜͝͝͝ͅͅͅͅͅͅͅͅư̸̡̧̢̨̨̢̨̡̧̛̛̛͕̺̘̗͕̪̟̞̳͖̲̻̻̦̤̤͔̬͍̦͈̬͇̯̭͉̬͚̙͙̼͎̩̠̺͙̫̜͙̜̮̬͎̩̼̣͔̦̘̫̞͖͕̻̩̻̱̫̻͉̬͚̘̫̟̣̱̮̞͍̖̰̫͙̭̳͓̦̯͙̣̫̼͓̱͚̩̺͉̭̮̠̮̲̭̙̳͇͙͉̺̖͙̼͔͇̤͙͈̭̟̹̰͎̝͓̗̘̼̤̞̪̱̜̭̖̣͔̹̹̠̝̖̪͉͕̌̍̋́̈̾̓͆͗̑͆̄̓̏̃͒̄̎̐͌̍̐̓̎͊͂̉̍͆́͆̋̒̆̌͗̓͗͋̃̈́̈́̌̏͒̌̏͗̓̿͌̄̃̎̿͋͌̒̏̀͐͆̋̿̕̕̕͜͝͝͠͝͠͝͠͠ͅͅͅǹ̵̨̨̧̨̧̢̡̧̨̡̨̨̨̢̢̨̡̧̨̡̢̡̢̳̙̻̘͔͈̹͓̺̩̦̦̻̥͓͚̟͚͕͔͉̺͈̝͓̯͔͕͍͕̖̬̟̝̰̩̩̰͍̮̥̦̝̲̜̬̝̩͔̺͍͍̻̺̱͎̫͍̥͉̯̳͉͓̟͈̳̤͚̘̮̱̭̞̻̦̗̹̠͖̫̫̤̹̗̺͔̖̦̰̮̬̱̖̹͉̩̬̭̖̦͚̻̹̖̮͙͎̣͓̳̪̳͍͍̗̺̯͈̙͓̣̗̦͙̺͚̰̪͍̤̻̥̣̬̻̜̘̘͇̟̜̝̼̩͇̣̤̳̹̩̜͖̜̤̩̺̼̻̩̟̝̩̼̩̲͖̟̣̝͇̜͙̗̞̦̘͙̳͚̺̫̰̜̖͉̟̗͖͕̬̦̥̮̜̙̬̺͓̠̯̤̮̼̜͙͉̰̙̗͕͍͚̦͕̮͉͈̙̪͓̳̯̟̱̦̹̭̺̼͉̯̯͖̖̘̮̞̼͎̪͖͙̭͍̣̯͚̾̽͒͒̊̈̃̐͐͒̉̋̎͑͆̈̎͌̃̒͊̔͑̄̿͑̃̓͐͆̿̿̾̃̏̀̚̕͜͜͜͜͝͝ͅͅͅͅͅͅ!̶̡̢̨̨̨̡̡̨̡̨̨̨̢̢̧̢̢̢̢̡̨̛̛̛̛̭̲̺͔̰͓͓͈͍̖̮̭̤̩͚̭̩̼̫͈̹̙̭͚͇̗͖̙̙̼̰͔̭͓͎̯͓̯̜͚̗̝͔͉̼̠̹͚͇̩̬̬͓͚̭͎̠̖͖̬̞͉͎͎͉̘̩̬̺̳̖̻̘͎̹̹͕̟̗͉̰͖̮͔͇̘̞̭̮͈̲̪͉͈̻̹̻̣͖̠̙͎͍͉̤̭̳̞̳̝̠̳̺͈̺͍̮̭̺͚̹̫͓͎̱̹̰͓̺̘͓͇̙͙̱͉̟̙͖̭̙̳̰̮̻̬̖̲̹͖̝̬̳͉̗̫̮̮̹͚̹͕̤͓̺̮̜̜̻̩̦̪͓͎̬͍̗̺̝̗̣̘̖͇̬̻͍̮̜͚̱͍̗͔͎͙̗̳̞̩̩͇͕͈͙̬̻̮͕̤̲͚̘̥̙̻̲̠̦͍̩̺̩̭͓̘͙͎̳̝̞͚̄̍̇̎̊̉̃̄͐͆̒͗̈́͛̊͗̉̑̅̒̈́̀̈̈́̌̊̂̃̍̈́̊͒̂̆̎̈́͌́̆͑͆̈́̐͗͋̾̇̂̍̃͑̏͐̍̐̌̑̃̄̓̓̃̽̑̂̂͌͊͆͋̉̇̓̽͛̂̅̀̀̌̎̌̈́̐̽͊͒̀͛̏̌͐̈́̽͒̉̇͒͑͋͛́̽͋̋̊̋̒̈́͛̉̒͊́̓̋͂̋̓̅̃͗́́̾͋͛̃́́̋̎̌̓̄̽̔̌́̅̎̓̎͆́̋͊͆͒͒̈́̂̆͐͋̐̿͆̿̾̿̅͑́̿̏̌̅̌͊͐̔͌̽͆͒͋̈̇̀̈́́͑͐̍̏̀̓̀̓̐͑̓͒̉̂̇͐͐͌̀̈́̅̓͊̓͌̔͂̀͗̓͑́̈́̀̉̀̓̇͐̒͛̈́̀̉̏̐̃̈͂̋̋̀͛̈̌̎̏̐͛̑͊͗̐͘̚͘͘̚͘̚̕͘͘͘͘͘̚̕͘̕̕͘̚͜͜͜͜͜͜͜͜͜͝͠͠͠͝͠͝͝͠͝͝͝͠͝͝͠͝͠͝͝͝͠͝ͅͅͅͅͅͅ + + T̸h̴e̶ ̵p̷o̷w̶e̵r̷f̸u̷l̷ ̵p̴r̷o̷g̶r̷a̸m̸m̶i̸n̴g̴ ̷l̶a̴n̸g̵u̵a̶g̸e̶ ̸t̶h̴a̵t̵ ̶i̷s̶ ̵a̷l̴s̸o̷ ̵e̵a̷s̷y̴ ̵t̵o̷ ̷l̷e̶a̵r̴n̸.̵ + """# + } + return str +}() + +let shortSample = #""" + Swift 👨‍👨‍👧‍👧简c̴̭̈͘ǫ̷̯͋̊d̸͖̩̈̈́ḛ̴́🇺🇸🇨🇦🇺🇸코 + """# + +let sampleString2 = + #""" + The powerful programming language that is also easy to learn. + Swift is a powerful and intuitive programming language for iOS, iPadOS, macOS, \# + tvOS, and watchOS. Writing Swift code is interactive and fun, the syntax is \# + concise yet expressive, and Swift includes modern features developers love. \# + Swift code is safe by design and produces software that runs lightning-fast. + + The powerful programming language that is also easy to learn. + Swift is a powerful and intuitive programming language for iOS, iPadOS, macOS, \# + tvOS, and watchOS. Writing Swift code is interactive and fun, the syntax is \# + concise yet expressive, and Swift includes modern features developers love. \# + Swift code is safe by design and produces software that runs lightning-fast. + + The powerful programming language that is also easy to learn. + Swift is a powerful and intuitive programming language for iOS, iPadOS, macOS, \# + tvOS, and watchOS. Writing Swift code is interactive and fun, the syntax is \# + concise yet expressive, and Swift includes modern features developers love. \# + Swift code is safe by design and produces software that runs lightning-fast. + + The powerful programming language that is also easy to learn. + Swift is a powerful and intuitive programming language for iOS, iPadOS, macOS, \# + tvOS, and watchOS. Writing Swift code is interactive and fun, the syntax is \# + concise yet expressive, and Swift includes modern features developers love. \# + Swift code is safe by design and produces software that runs lightning-fast. + + The powerful programming language that is also easy to learn. + Swift is a powerful and intuitive programming language for iOS, iPadOS, macOS, \# + tvOS, and watchOS. Writing Swift code is interactive and fun, the syntax is \# + concise yet expressive, and Swift includes modern features developers love. \# + Swift code is safe by design and produces software that runs lightning-fast. + + The powerful programming language that is also easy to learn. + Swift is a powerful and intuitive programming language for iOS, iPadOS, macOS, \# + tvOS, and watchOS. Writing Swift code is interactive and fun, the syntax is \# + concise yet expressive, and Swift includes modern features developers love. \# + Swift code is safe by design and produces software that runs lightning-fast. + + The powerful programming language that is also easy to learn. + Swift is a powerful and intuitive programming language for iOS, iPadOS, macOS, \# + tvOS, and watchOS. Writing Swift code is interactive and fun, the syntax is \# + concise yet expressive, and Swift includes modern features developers love. \# + Swift code is safe by design and produces software that runs lightning-fast. + + The powerful programming language that is also easy to learn. + Swift is a powerful and intuitive programming language for iOS, iPadOS, macOS, \# + tvOS, and watchOS. Writing Swift code is interactive and fun, the syntax is \# + concise yet expressive, and Swift includes modern features developers love. \# + Swift code is safe by design and produces software that runs lightning-fast. + + The powerful programming language that is also easy to learn. + Swift is a powerful and intuitive programming language for iOS, iPadOS, macOS, \# + tvOS, and watchOS. Writing Swift code is interactive and fun, the syntax is \# + concise yet expressive, and Swift includes modern features developers love. \# + Swift code is safe by design and produces software that runs lightning-fast. + + The powerful programming language that is also easy to learn. + Swift is a powerful and intuitive programming language for iOS, iPadOS, macOS, \# + tvOS, and watchOS. Writing Swift code is interactive and fun, the syntax is \# + concise yet expressive, and Swift includes modern features developers love. \# + Swift code is safe by design and produces software that runs lightning-fast. + + The powerful programming language that is also easy to learn. + Swift is a powerful and intuitive programming language for iOS, iPadOS, macOS, \# + tvOS, and watchOS. Writing Swift code is interactive and fun, the syntax is \# + concise yet expressive, and Swift includes modern features developers love. \# + Swift code is safe by design and produces software that runs lightning-fast. + + The powerful programming language that is also easy to learn. + Swift is a powerful and intuitive programming language for iOS, iPadOS, macOS, \# + tvOS, and watchOS. Writing Swift code is interactive and fun, the syntax is \# + concise yet expressive, and Swift includes modern features developers love. \# + Swift code is safe by design and produces software that runs lightning-fast. + + The powerful programming language that is also easy to learn. + Swift is a powerful and intuitive programming language for iOS, iPadOS, macOS, \# + tvOS, and watchOS. Writing Swift code is interactive and fun, the syntax is \# + concise yet expressive, and Swift includes modern features developers love. \# + Swift code is safe by design and produces software that runs lightning-fast. + + """# diff --git a/Tests/RopeModuleTests/TestBigString.swift b/Tests/RopeModuleTests/TestBigString.swift new file mode 100644 index 000000000..84067d443 --- /dev/null +++ b/Tests/RopeModuleTests/TestBigString.swift @@ -0,0 +1,1035 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=5.8) +import XCTest +#if COLLECTIONS_SINGLE_MODULE +import Collections +#else +import _CollectionsTestSupport +import _RopeModule +#endif + +@available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) +class TestBigString: CollectionTestCase { + override var isAvailable: Bool { isRunningOnSwiftStdlib5_8 } + + override class func setUp() { + // Turn off output buffering. + setbuf(stdout, nil) + setbuf(stderr, nil) + super.setUp() + } + + override func setUp() { + print("Global seed: \(RepeatableRandomNumberGenerator.globalSeed)") + super.setUp() + } + + func test_capacity() { + let min = BigString._minimumCapacity + let max = BigString._maximumCapacity + + expectLessThanOrEqual(min, max) +#if !DEBUG // Debug builds have smaller nodes + // We want big strings to hold at least as many UTF-8 code units as a regular String. + expectGreaterThanOrEqual(min, 1 << 48) +#endif + } + + func test_empty() { + let s = BigString() + s._invariantCheck() + expectEqual(s.count, 0) + expectEqual(s.unicodeScalars.count, 0) + expectEqual(s.utf16.count, 0) + expectEqual(s.utf8.count, 0) + expectTrue(s.isEmpty) + expectEqual(s.startIndex, s.endIndex) + expectEqual(s.startIndex.utf8Offset, 0) + + expectEqual(String(s), "") + expectEqual(String(s[...]), "") + } + + func test_Equatable() { + let a: BigString = "Cafe\u{301}" + let b: BigString = "Café" + expectEqual(a, b) + expectNotEqual(a.unicodeScalars, b.unicodeScalars) + expectNotEqual(a.utf8, b.utf8) + expectNotEqual(a.utf16, b.utf16) + } + + func test_descriptions() { + let s: BigString = "Café" + expectEqual(s.description, "Café") + expectEqual(s.unicodeScalars.description, "Café") + #if false // Should we? + expectEqual(s.utf8.description, "<43 61 66 C3 A9>") + expectEqual(s.utf16.description, "<0043 0061 0066 00E9>") + #endif + } + + func test_string_conversion() { + let big = BigString(sampleString) + + big._invariantCheck() + expectEqual(big.count, sampleString.count) + expectEqual(big.unicodeScalars.count, sampleString.unicodeScalars.count) + expectEqual(big.utf16.count, sampleString.utf16.count) + expectEqual(big.utf8.count, sampleString.utf8.count) + + let flat = String(big) + expectEqual(flat, sampleString) + } + + func testUTF8View() { + let str = BigString(shortSample) + checkBidirectionalCollection(str.utf8, expectedContents: shortSample.utf8) + } + + func testUTF16View() { + let str = BigString(shortSample) + checkBidirectionalCollection(str.utf16, expectedContents: shortSample.utf16) + } + + func testUnicodeScalarView() { + let str = BigString(shortSample) + checkBidirectionalCollection(str.unicodeScalars, expectedContents: shortSample.unicodeScalars) + } + + func testCharacterView() { + let str = BigString(shortSample) + checkBidirectionalCollection(str, expectedContents: shortSample) + } + + func testHashable_Characters() { + let classes: [[BigString]] = [ + ["Cafe\u{301}", "Café"], + ["Foo\u{301}\u{327}", "Foo\u{327}\u{301}"], + ["Foo;bar", "Foo\u{37e}bar"], + ] + checkHashable(equivalenceClasses: classes) + } + + func testHashable_Scalars() { + let classes: [BigString] = [ + "Cafe\u{301}", + "Café", + "Foo\u{301}\u{327}", + "Foo\u{327}\u{301}", + "Foo;bar", + "Foo\u{37e}bar", + ] + checkHashable(equivalenceClasses: classes.map { [$0.unicodeScalars] }) + checkHashable(equivalenceClasses: classes.map { [$0.utf8] }) + checkHashable(equivalenceClasses: classes.map { [$0.utf16] }) + } + + @discardableResult + func checkCharacterIndices( + _ flat: String, + _ big: BigString, + file: StaticString = #file, line: UInt = #line + ) -> (flat: [String.Index], big: [BigString.Index]) { + // Check iterators + var it1 = flat.makeIterator() + var it2 = big.makeIterator() + while true { + let a = it1.next() + let b = it2.next() + guard a == b else { + expectEqual(a, b) + break + } + if a == nil { break } + } + + // Check indices + let indices1 = Array(flat.indices) + [flat.endIndex] + let indices2 = Array( + sequence(first: big.startIndex) { + $0 == big.endIndex ? nil : big.index(after: $0) + } + ) + + expectEqual(indices2.count, indices1.count, file: file, line: line) + + let c = min(indices1.count, indices2.count) + + for i in 0 ..< c { + let i1 = indices1[i] + let i2 = indices2[i] + guard i1 < flat.endIndex, i2 < big.endIndex else { continue } + let c1 = flat[i1] + let c2 = big[i2] + expectEqual(c1, c2, "i: \(i), i1: \(i1._description), i2: \(i2)", file: file, line: line) + } + + for i in 0 ..< c { + let i1 = indices1[i] + let i2 = indices2[i] + let d1 = flat.utf8.distance(from: flat.startIndex, to: i1) + let d2 = big.utf8.distance(from: big.startIndex, to: i2) + expectEqual(d2, d1, "i: \(i), i1: \(i1._description), i2: \(i2)", file: file, line: line) + } + return (indices1, indices2) + } + + @discardableResult + func checkScalarIndices( + _ flat: String, + _ big: BigString, + file: StaticString = #file, line: UInt = #line + ) -> (flat: [String.Index], big: [BigString.Index]) { + // Check iterators + var it1 = flat.unicodeScalars.makeIterator() + var it2 = big.unicodeScalars.makeIterator() + while true { + let a = it1.next() + let b = it2.next() + guard a == b else { + expectEqual(a, b) + break + } + if a == nil { break } + } + + // Check indices + let indices1 = Array(flat.unicodeScalars.indices) + [flat.unicodeScalars.endIndex] + let indices2 = Array( + sequence(first: big.startIndex) { + $0 == big.endIndex ? nil : big.unicodeScalars.index(after: $0) + } + ) + + expectEqual(indices2.count, indices1.count, file: file, line: line) + + let c = min(indices1.count, indices2.count) + + for i in 0 ..< c { + let i1 = indices1[i] + let i2 = indices2[i] + guard i1 < flat.endIndex, i2 < big.endIndex else { continue } + let c1 = flat.unicodeScalars[i1] + let c2 = big.unicodeScalars[i2] + expectEqual(c1, c2, "i: \(i), i1: \(i1._description), i2: \(i2)", file: file, line: line) + } + + for i in 0 ..< c { + let i1 = indices1[i] + let i2 = indices2[i] + let d1 = flat.utf8.distance(from: flat.startIndex, to: i1) + let d2 = big.utf8.distance(from: big.startIndex, to: i2) + expectEqual(d2, d1, "i: \(i), i1: \(i1._description), i2: \(i2)", file: file, line: line) + } + return (indices1, indices2) + } + + @discardableResult + func checkUTF8Indices( + _ flat: String, + _ big: BigString, + file: StaticString = #file, line: UInt = #line + ) -> (flat: [String.Index], big: [BigString.Index]) { + // Check iterators + var it1 = flat.utf8.makeIterator() + var it2 = big.utf8.makeIterator() + while true { + let a = it1.next() + let b = it2.next() + guard a == b else { + expectEqual(a, b) + break + } + if a == nil { break } + } + + // Check indices + let indices1 = Array(flat.utf8.indices) + [flat.utf8.endIndex] + let indices2 = Array( + sequence(first: big.startIndex) { + $0 == big.endIndex ? nil : big.utf8.index(after: $0) + } + ) + + expectEqual(indices2.count, indices1.count, file: file, line: line) + + let c = min(indices1.count, indices2.count) + + for i in 0 ..< c { + let i1 = indices1[i] + let i2 = indices2[i] + guard i1 < flat.endIndex, i2 < big.endIndex else { continue } + let c1 = flat.utf8[i1] + let c2 = big.utf8[i2] + expectEqual(c1, c2, "i: \(i), i1: \(i1._description), i2: \(i2)", file: file, line: line) + } + + for i in 0 ..< c { + let i1 = indices1[i] + let i2 = indices2[i] + let d1 = flat.utf8.distance(from: flat.startIndex, to: i1) + let d2 = big.utf8.distance(from: big.startIndex, to: i2) + expectEqual(d2, d1, "i: \(i), i1: \(i1._description), i2: \(i2)", file: file, line: line) + } + return (indices1, indices2) + } + + @discardableResult + func checkUTF16Indices( + _ flat: String, + _ big: BigString, + file: StaticString = #file, line: UInt = #line + ) -> (flat: [String.Index], big: [BigString.Index]) { + // Check iterators + var it1 = flat.utf16.makeIterator() + var it2 = big.utf16.makeIterator() + while true { + let a = it1.next() + let b = it2.next() + guard a == b else { + expectEqual(a, b) + break + } + if a == nil { break } + } + + // Check indices + let indices1 = Array(flat.utf16.indices) + [flat.utf16.endIndex] + let indices2 = Array( + sequence(first: big.startIndex) { + $0 == big.endIndex ? nil : big.utf16.index(after: $0) + } + ) + + expectEqual(indices2.count, indices1.count, file: file, line: line) + + let c = min(indices1.count, indices2.count) + + for i in 0 ..< c { + let i1 = indices1[i] + let i2 = indices2[i] + guard i1 < flat.endIndex, i2 < big.endIndex else { continue } + let c1 = flat.utf16[i1] + let c2 = big.utf16[i2] + expectEqual(c1, c2, "i: \(i), i1: \(i1._description), i2: \(i2)", file: file, line: line) + } + + for i in 0 ..< c { + let i1 = indices1[i] + let i2 = indices2[i] + let d1 = flat.utf16.distance(from: flat.startIndex, to: i1) + let d2 = big.utf16.distance(from: big.startIndex, to: i2) + expectEqual(d2, d1, "i: \(i), i1: \(i1._description), i2: \(i2)", file: file, line: line) + } + return (indices1, indices2) + } + + func test_iterator_character() { + let small: BigString = "Cafe\u{301} 👩‍👩‍👧" + var it = small.makeIterator() + expectEqual(it.next(), "C") + expectEqual(it.next(), "a") + expectEqual(it.next(), "f") + expectEqual(it.next(), "e\u{301}") + expectEqual(it.next(), " ") + expectEqual(it.next(), "👩‍👩‍👧") + expectNil(it.next()) + + let flat = sampleString + let big = BigString(flat) + + let c1 = Array(flat) + let c2 = Array(big) + expectEqual(c1, c2) + } + + func test_iterator_scalar() { + let small: BigString = "Cafe\u{301} 👩‍👩‍👧" + var it = small.unicodeScalars.makeIterator() + expectEqual(it.next(), "C") + expectEqual(it.next(), "a") + expectEqual(it.next(), "f") + expectEqual(it.next(), "e") + expectEqual(it.next(), "\u{301}") + expectEqual(it.next(), " ") + expectEqual(it.next(), "👩") + expectEqual(it.next(), "\u{200D}") + expectEqual(it.next(), "👩") + expectEqual(it.next(), "\u{200D}") + expectEqual(it.next(), "👧") + expectNil(it.next()) + + let flat = sampleString + let big = BigString(flat) + + let c1 = Array(flat.unicodeScalars) + let c2 = Array(big.unicodeScalars) + expectEqual(c1, c2) + } + + func test_iterator_utf8() { + let flat = sampleString + let big = BigString(flat) + + let c1 = Array(flat.utf8) + let c2 = Array(big.utf8) + expectEqual(c1, c2) + } + + func test_iterator_utf16() { + let flat = sampleString + let big = BigString(flat) + + let c1 = Array(flat.utf16) + let c2 = Array(big.utf16) + expectEqual(c1, c2) + } + + func test_indices_character() { + let flat = sampleString + let big = BigString(flat) + + let (indices1, indices2) = checkCharacterIndices(flat, big) + + let c = min(indices1.count, indices2.count) + for i in randomStride(from: 0, to: c, by: 5, seed: 0) { + for j in randomStride(from: i, to: c, by: 5, seed: i) { + let i1 = indices1[i] + let j1 = indices1[j] + let a = String(sampleString[i1 ..< j1]) + + let i2 = indices2[i] + let j2 = big.index(i2, offsetBy: j - i) + expectEqual(big.index(roundingDown: i2), i2) + expectEqual(big.index(roundingDown: j2), j2) + let b = String(big[i2 ..< j2]) + expectEqual(b, a) + } + } + } + + func test_indices_scalar() { + let flat = sampleString + let big = BigString(flat) + + let (indices1, indices2) = checkScalarIndices(flat, big) + + let c = min(indices1.count, indices2.count) + for i in randomStride(from: 0, to: c, by: 20, seed: 0) { + for j in randomStride(from: i, to: c, by: 20, seed: i) { + let a = String(sampleString.unicodeScalars[indices1[i] ..< indices1[j]]) + + let i2 = indices2[i] + let j2 = big.unicodeScalars.index(i2, offsetBy: j - i) + expectEqual(big.unicodeScalars.index(roundingDown: i2), i2) + expectEqual(big.unicodeScalars.index(roundingDown: j2), j2) + let slice = big.unicodeScalars[i2 ..< j2] + let b = String(slice) + expectEqual(b, a) + } + } + } + + func test_indices_utf16() { + let flat = sampleString + let big = BigString(flat) + + let (indices1, indices2) = checkUTF16Indices(flat, big) + + let c = min(indices1.count, indices2.count) + for i in randomStride(from: 0, to: c, by: 20, seed: 0) { + for j in randomStride(from: i, to: c, by: 20, seed: i) { + let a = sampleString.utf16[indices1[i] ..< indices1[j]] + + let i2 = indices2[i] + let j2 = big.utf16.index(i2, offsetBy: j - i) + expectEqual(big.utf16.index(roundingDown: i2), i2) + expectEqual(big.utf16.index(roundingDown: j2), j2) + let b = big.utf16[i2 ..< j2] + expectEqual(b.first, a.first) + expectEqual(b.last, a.last) + } + } + } + + func test_indices_utf8() { + let flat = sampleString + let big = BigString(flat) + + let (indices1, indices2) = checkUTF8Indices(flat, big) + + let c = min(indices1.count, indices2.count) + for i in randomStride(from: 0, to: c, by: 40, seed: 0) { + for j in randomStride(from: i, to: c, by: 40, seed: i) { + let a = sampleString.utf8[indices1[i] ..< indices1[j]] + + let i2 = indices2[i] + let j2 = big.utf8.index(i2, offsetBy: j - i) + expectEqual(big.utf8.index(roundingDown: i2), i2) + expectEqual(big.utf8.index(roundingDown: j2), j2) + let b = big.utf8[i2 ..< j2] + expectEqual(b.first, a.first) + expectEqual(b.last, a.last) + } + } + } + + func test_append_string() { + let flat = sampleString + let ref = BigString(flat) + for stride in [1, 2, 4, 8, 16, 32, 64, 128, 250, 1000, 10000, 20000] { + var big: BigString = "" + var i = flat.startIndex + while i < flat.endIndex { + let j = flat.unicodeScalars.index(i, offsetBy: stride, limitedBy: flat.endIndex) ?? flat.endIndex + let next = String(flat[i ..< j]) + big.append(contentsOf: next) + //big.invariantCheck() + //expectEqual(String(big)[...], s[.. [(i: Int, str: String)] { + var pieces: [(i: Int, str: String)] = [] + var c = 0 + var i = str.startIndex + while i < str.endIndex { + let j = str.unicodeScalars.index(i, offsetBy: stride, limitedBy: str.endIndex) ?? str.endIndex + let next = String(str[i ..< j]) + pieces.append((c, next)) + c += 1 + i = j + } + return pieces + } + + func test_insert_string() { + let flat = sampleString + let ref = BigString(flat) + for stride in [1, 2, 4, 8, 16, 32, 64, 128, 250, 1000, 10000, 20000] { + print("Stride: \(stride)") + var pieces = pieces(of: flat, by: stride) + var rng = RepeatableRandomNumberGenerator(seed: 0) + pieces.shuffle(using: &rng) + + var big: BigString = "" + var smol = "" + for i in pieces.indices { + let piece = pieces[i] + let utf8Offset = pieces[.., + file: StaticString = #file, + line: UInt = #line + ) { + var current = str.startIndex + var next = str.index(after: current) + for i in indices { + if i >= next { + current = next + next = str.index(after: current) + } + let j = str.index(roundingDown: i) + expectEqual(j, current, "i: \(i)", file: file, line: line) + expectEqual(str[i], str[j], "i: \(i)", file: file, line: line) + } + expectEqual(next, str.endIndex, "end", file: file, line: line) + } + + check(str.utf8.indices) + check(str.utf16.indices) + check(str.unicodeScalars.indices) + check(str.indices) + expectEqual(str.index(roundingDown: str.endIndex), str.endIndex) + + } + + func testCharacterIndexRoundingUp() { + let ref = sampleString + let str = BigString(ref) + + func check( + _ indices: some Sequence, + file: StaticString = #file, + line: UInt = #line + ) { + var current = str.startIndex + for i in indices { + let j = str.index(roundingUp: i) + expectEqual(j, current, "i: \(i)", file: file, line: line) + while i >= current { + current = str.index(after: current) + } + } + expectEqual(current, str.endIndex, "end", file: file, line: line) + } + + check(str.utf8.indices) + check(str.utf16.indices) + check(str.unicodeScalars.indices) + check(str.indices) + expectEqual(str.index(roundingDown: str.endIndex), str.endIndex) + + } + + func testUnicodeScalarIndexRoundingDown() { + let ref = sampleString + let str = BigString(ref) + + func check( + _ indices: some Sequence, + file: StaticString = #file, + line: UInt = #line + ) { + var current = str.startIndex + var next = str.unicodeScalars.index(after: current) + for i in indices { + while i >= next { + current = next + next = str.unicodeScalars.index(after: current) + } + let j = str.unicodeScalars.index(roundingDown: i) + expectEqual(j, current, "\(i)", file: file, line: line) + expectEqual(str.unicodeScalars[i], str.unicodeScalars[j], "i: \(i)", file: file, line: line) + } + } + + check(str.utf8.indices) + check(str.utf16.indices) + check(str.unicodeScalars.indices) + check(str.indices) + expectEqual(str.unicodeScalars.index(roundingDown: str.endIndex), str.endIndex) + } + + func testUnicodeScalarIndexRoundingUp() { + let ref = sampleString + let str = BigString(ref) + + func check( + _ indices: some Sequence, + file: StaticString = #file, + line: UInt = #line + ) { + var current = str.startIndex + for i in indices { + while i > current { + current = str.unicodeScalars.index(after: current) + } + let j = str.unicodeScalars.index(roundingUp: i) + expectEqual(j, current, "i: \(i)", file: file, line: line) + } + } + + check(str.utf8.indices) + check(str.utf16.indices) + check(str.unicodeScalars.indices) + check(str.indices) + expectEqual(str.unicodeScalars.index(roundingDown: str.endIndex), str.endIndex) + } + + func testUTF8IndexRoundingDown() { + let ref = sampleString + let str = BigString(ref) + + func check( + _ indices: some Sequence, + file: StaticString = #file, + line: UInt = #line + ) { + var current = str.startIndex + var next = str.utf8.index(after: current) + for i in indices { + while i >= next { + current = next + next = str.utf8.index(after: current) + } + let j = str.utf8.index(roundingDown: i) + expectEqual(j, current, "i: \(i)", file: file, line: line) + expectEqual(str.utf8[i], str.utf8[j], "i: \(i)", file: file, line: line) + } + } + + check(str.utf8.indices) + check(str.utf16.indices) + check(str.unicodeScalars.indices) + check(str.indices) + expectEqual(str.utf8.index(roundingDown: str.endIndex), str.endIndex) + } + + func testUTF8IndexRoundingUp() { + let ref = sampleString + let str = BigString(ref) + + func check( + _ indices: some Sequence, + file: StaticString = #file, + line: UInt = #line + ) { + var current = str.startIndex + for i in indices { + while i > current { + current = str.utf8.index(after: current) + } + let j = str.utf8.index(roundingUp: i) + expectEqual(j, current, "i: \(i)", file: file, line: line) + } + } + + check(str.utf8.indices) + check(str.utf16.indices) + check(str.unicodeScalars.indices) + check(str.indices) + expectEqual(str.utf8.index(roundingDown: str.endIndex), str.endIndex) + } + + func testUTF16IndexRoundingDown() { + let ref = sampleString + let str = BigString(ref) + + func check( + _ indices: some Sequence, + file: StaticString = #file, + line: UInt = #line + ) { + var current = str.startIndex + // Note: UTF-16 index rounding is not rounding in the usual sense -- it rounds UTF-8 indices + // down to the nearest scalar boundary, not the nearest UTF-16 index. This is because + // UTF-16 indices addressing trailing surrogates are ordered below UTF-8 continuation bytes, + // but this rounds those down to the scalar. + var next = str.unicodeScalars.index(after: current) // Note: intentionally not utf16 + for i in indices { + while i >= next { + current = next + next = str.unicodeScalars.index(after: current) // Note: intentionally not utf16 + } + let j = str.utf16.index(roundingDown: i) + if UTF16.isTrailSurrogate(str.utf16[i]) { + expectEqual(j, i, "i: \(i)", file: file, line: line) + } else { + expectEqual(j, current, "i: \(i)", file: file, line: line) + } + expectEqual(str.utf16[i], str.utf16[j], "i: \(i)", file: file, line: line) + } + } + + check(str.utf8.indices) + check(str.utf16.indices) + check(str.unicodeScalars.indices) + check(str.indices) + expectEqual(str.utf16.index(roundingDown: str.endIndex), str.endIndex) + } + + func testUTF1t6IndexRoundingUp() { + let ref = sampleString + let str = BigString(ref) + + func check( + _ indices: some Sequence, + file: StaticString = #file, + line: UInt = #line + ) { + var current = str.startIndex + for i in indices { + while i > current { + current = str.utf8.index(after: current) + } + let j = str.utf8.index(roundingUp: i) + expectEqual(j, current, "i: \(i)", file: file, line: line) + } + } + + check(str.utf8.indices) + check(str.utf16.indices) + check(str.unicodeScalars.indices) + check(str.indices) + expectEqual(str.utf8.index(roundingDown: str.endIndex), str.endIndex) + } + + func testSubstringMutationIndexRounding() { + let b1: BigString = "Foobar" + var s1 = b1.suffix(3) + expectEqual(s1, "bar") + s1.insert("\u{308}", at: s1.startIndex) // Combining diaeresis + expectEqual(s1.base, "Foöbar") + expectEqual(s1, "öbar") + + let b2: BigString = "Foo👩👧bar" // WOMAN, GIRL + var s2: BigSubstring = b2.prefix(4) + expectEqual(s2, "Foo👩") + s2.append("\u{200d}") // ZWJ + expectEqual(s2, "Foo") + expectEqual(s2.base, "Foo👩‍👧bar") // family with mother and daughter + + let b3: BigString = "Foo🇺🇸🇨🇦🇺🇸🇨🇦bar" // Regional indicators "USCAUSCA" + var s3: BigSubstring = b3.prefix(6) + expectEqual(s3, "Foo🇺🇸🇨🇦🇺🇸") + s3.insert("\u{1f1ed}", at: s3.index(s3.startIndex, offsetBy: 3)) // Regional indicator "H" + expectEqual(s3, "Foo🇭🇺🇸🇨🇦🇺") // Regional indicators "HUSCAUSCA" + expectEqual(s3.base, "Foo🇭🇺🇸🇨🇦🇺🇸🇨\u{1f1e6}bar") + } + + func test_unicodeScalars_mutations() { + let b1: BigString = "Foo👩👧bar" // WOMAN, GIRL + var s1 = b1.prefix(4) + expectEqual(s1, "Foo👩") + + // Append a ZWJ at the end of the substring via its Unicode scalars view. + func mutate(_ value: inout T, by body: (inout T) -> Void) { + body(&value) + } + mutate(&s1.unicodeScalars) { view in + view.append("\u{200d}") // ZWJ + expectEqual(view, "Foo👩\u{200d}") + } + + expectEqual(s1, "Foo") + expectEqual(s1.base, "Foo👩‍👧bar") + } + + func test_ExpressibleByStringLiteral_and_CustomStringConvertible() { + let a: BigString = "foobar" + expectEqual(a.description, "foobar") + expectEqual(a.debugDescription, "\"foobar\"") + + let b: BigSubstring = "foobar" + expectEqual(b.description, "foobar") + expectEqual(b.debugDescription, "\"foobar\"") + + let c: BigString.UnicodeScalarView = "foobar" + expectEqual(c.description, "foobar") + expectEqual(c.debugDescription, "\"foobar\"") + + let d: BigSubstring.UnicodeScalarView = "foobar" + expectEqual(d.description, "foobar") + expectEqual(d.debugDescription, "\"foobar\"") + } + + func testCharacterwiseEquality() { + let cafe1: BigString = "Cafe\u{301}" + let cafe2: BigString = "Café" + expectEqual(cafe1, cafe2) + expectNotEqual(cafe1.unicodeScalars, cafe2.unicodeScalars) + expectNotEqual(cafe1.utf8, cafe2.utf8) + expectNotEqual(cafe1.utf16, cafe2.utf16) + } +} +#endif diff --git a/Tests/RopeModuleTests/TestRope.swift b/Tests/RopeModuleTests/TestRope.swift new file mode 100644 index 000000000..0d0fa2eab --- /dev/null +++ b/Tests/RopeModuleTests/TestRope.swift @@ -0,0 +1,670 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +import XCTest +#if COLLECTIONS_SINGLE_MODULE +import Collections +#else +import _RopeModule +import _CollectionsTestSupport +#endif + +struct Chunk: RopeElement, Equatable, CustomStringConvertible { + var length: Int + var value: Int + + struct Summary: RopeSummary, Comparable { + var length: Int + + init(_ length: Int) { + self.length = length + } + + static var zero: Self { Self(0) } + + var isZero: Bool { length == 0 } + + mutating func add(_ other: Self) { + length += other.length + } + + mutating func subtract(_ other: Self) { + length -= other.length + } + + static func ==(left: Self, right: Self) -> Bool { left.length == right.length } + static func <(left: Self, right: Self) -> Bool { left.length < right.length } + + static var maxNodeSize: Int { 6 } + static var nodeSizeBitWidth: Int { 3 } + } + + struct Metric: RopeMetric { + typealias Element = Chunk + + func size(of summary: Chunk.Summary) -> Int { + summary.length + } + + func index(at offset: Int, in element: Chunk) -> Int { + precondition(offset >= 0 && offset <= element.length) + return offset + } + } + + init(length: Int, value: Int) { + self.length = length + self.value = value + } + + var description: String { + "\(value)*\(length)" + } + + var summary: Summary { + Summary(length) + } + + var isEmpty: Bool { length == 0 } + var isUndersized: Bool { isEmpty } + + func invariantCheck() {} + + mutating func rebalance(prevNeighbor left: inout Chunk) -> Bool { + // Fully merge neighbors that have the same value + if left.value == self.value { + self.length += left.length + left.length = 0 + return true + } + if left.isEmpty { return true } + guard self.isEmpty else { return false } + swap(&self, &left) + return true + } + + mutating func rebalance(nextNeighbor right: inout Chunk) -> Bool { + // Fully merge neighbors that have the same value + if self.value == right.value { + self.length += right.length + right.length = 0 + return true + } + if right.isEmpty { return true } + guard self.isEmpty else { return false } + swap(&self, &right) + return true + } + + typealias Index = Int + + mutating func split(at index: Int) -> Chunk { + precondition(index >= 0 && index <= length) + let tail = Chunk(length: length - index, value: value) + self.length = index + return tail + } +} + +class TestRope: CollectionTestCase { + override func setUp() { + super.setUp() + print("Global seed: \(RepeatableRandomNumberGenerator.globalSeed)") + } + + func test_empty() { + let empty = Rope() + empty._invariantCheck() + expectTrue(empty.isEmpty) + expectEqual(empty.count, 0) + expectTrue(empty.summary.isZero) + expectEqual(empty.startIndex, empty.endIndex) + } + + func test_build() { + let c = 1000 + + let ref = (0 ..< c).map { + Chunk(length: ($0 % 4) + 1, value: $0) + } + var builder = Rope.Builder() + for chunk in ref { + builder.insertBeforeTip(chunk) + builder._invariantCheck() + } + let rope = builder.finalize() + + let actualSum = rope.summary + let expectedSum: Chunk.Summary = ref.reduce(into: .zero) { $0.add($1.summary) } + expectEqual(actualSum, expectedSum) + + expectTrue(rope.elementsEqual(ref)) + } + + func test_iteration() { + for c in [0, 1, 2, 10, 100, 500, 1000, 10000] { + let ref = (0 ..< c).map { + Chunk(length: ($0 % 4) + 1, value: $0) + } + let rope = Rope(ref) + + var it = rope.makeIterator() + var i = 0 + while let next = it.next() { + let expected = ref[i] + expectEqual(next, expected) + guard next == expected else { break } + i += 1 + } + expectEqual(i, ref.count) + + let expectedLength = ref.reduce(into: 0) { $0 += $1.length } + let actualLength = rope.reduce(into: 0) { $0 += $1.length } + expectEqual(actualLength, expectedLength) + } + } + + func test_subscript() { + for c in [0, 1, 2, 10, 100, 500, 1000, 10000] { + let ref = (0 ..< c).map { + Chunk(length: ($0 % 4) + 1, value: $0) + } + let rope = Rope(ref) + expectTrue(rope.elementsEqual(ref)) + + var i = rope.startIndex + var j = 0 + while i != rope.endIndex, j != ref.count { + expectEqual(rope[i], ref[j]) + i = rope.index(after: i) + j += 1 + } + expectEqual(i, rope.endIndex) + expectEqual(j, ref.count) + } + } + + func test_index_before() { + for c in [0, 1, 2, 10, 100, 500, 1000, 10000] { + let ref = (0 ..< c).map { + Chunk(length: ($0 % 4) + 1, value: $0) + } + let rope = Rope(ref) + expectTrue(rope.elementsEqual(ref)) + + var indices: [Rope.Index] = [] + var i = rope.startIndex + while i != rope.endIndex { + indices.append(i) + i = rope.index(after: i) + } + + while let j = indices.popLast() { + i = rope.index(before: i) + expectEqual(i, j) + } + } + } + + func test_distance() { + let c = 500 + + let ref = (0 ..< c).map { + Chunk(length: ($0 % 4) + 1, value: $0) + } + let rope = Rope(ref) + + let indices = Array(rope.indices) + [rope.endIndex] + expectEqual(indices.count, c + 1) + for i in indices.indices { + for j in indices.indices { + let d = rope.distance(from: indices[i], to: indices[j], in: Chunk.Metric()) + let r = ( + i <= j + ? ref[i..(chunks) + let ref = chunks.flatMap { Array(repeating: $0.value, count: $0.length) } + + for i in 0 ..< ref.count { + let (index, remaining) = rope.find(at: i, in: Chunk.Metric(), preferEnd: false) + let chunk = rope[index] + expectEqual(chunk.value, ref[i]) + let pos = ref[..(ref) + + let indices = Array(rope.indices) + [rope.endIndex] + expectEqual(indices.count, c + 1) + for i in indices.indices { + for j in indices.indices { + let d = ( + i <= j + ? ref[i..() + var ref: [Chunk] = [] + + for i in 0 ..< c { + let chunk = Chunk(length: (i % 4) + 1, value: i) + ref.append(chunk) + rope.append(chunk) + rope._invariantCheck() + } + + let actualSum = rope.summary + let expectedSum: Chunk.Summary = ref.reduce(into: .zero) { $0.add($1.summary) } + expectEqual(actualSum, expectedSum) + + expectTrue(rope.elementsEqual(ref)) + } + + func test_prepend_item() { + let c = 1000 + + let ref = (0 ..< c).map { + Chunk(length: ($0 % 4) + 1, value: $0) + } + + var rope = Rope() + for chunk in ref.reversed() { + rope.insert(chunk, at: 0, in: Chunk.Metric()) + rope._invariantCheck() + } + + let actualSum = rope.summary + let expectedSum: Chunk.Summary = ref.reduce(into: .zero) { $0.add($1.summary) } + expectEqual(actualSum, expectedSum) + + expectTrue(rope.elementsEqual(ref)) + } + + func test_insert_item() { + let c = 1000 + let ref = (0 ..< c).map { + Chunk(length: ($0 % 4) + 1, value: $0) + } + + var rng = RepeatableRandomNumberGenerator(seed: 0) + let input = ref.shuffled(using: &rng) + + var rope = Rope() + for i in input.indices { + let chunk = input[i] + let position = input[..(ref) + + var rng = RepeatableRandomNumberGenerator(seed: 0) + let input = ref.shuffled(using: &rng) + + for i in input.indices { + let chunk = input[i] + let offset = input[i...].reduce(into: 0) { + $0 += $1.value < chunk.value ? 1 : 0 + } + let index = rope.index(rope.startIndex, offsetBy: offset) + let removed = rope.remove(at: index) + expectEqual(removed, chunk) + rope._invariantCheck() + } + expectTrue(rope.isEmpty) + expectEqual(rope.summary, .zero) + } + + func test_remove_at_inout_index() { + let c = 1000 + let ref = (0 ..< c).map { + Chunk(length: ($0 % 4) + 1, value: $0) + } + + var rope = Rope(ref) + + var rng = RepeatableRandomNumberGenerator(seed: 0) + let input = ref.shuffled(using: &rng) + + for i in input.indices { + let chunk = input[i] + let (offset, position) = input[i...].reduce(into: (0, 0)) { + guard $1.value < chunk.value else { return } + $0.0 += 1 + $0.1 += $1.length + } + var index = rope.index(rope.startIndex, offsetBy: offset) + let removed = rope.remove(at: &index) + expectEqual(removed, chunk) + expectEqual(rope.offset(of: index, in: Chunk.Metric()), position, "\(i)") + rope._invariantCheck() + } + expectTrue(rope.isEmpty) + expectEqual(rope.summary, .zero) + } + + func test_remove_at_position() { + let c = 1000 + let ref = (0 ..< c).map { + Chunk(length: ($0 % 4) + 1, value: $0) + } + + var rope = Rope(ref) + + var rng = RepeatableRandomNumberGenerator(seed: 0) + let input = ref.shuffled(using: &rng) + + for i in input.indices { + let chunk = input[i] + let position = input[i...].reduce(into: 0) { + $0 += $1.value < chunk.value ? $1.length : 0 + } + let r = rope.remove(at: position, in: Chunk.Metric()) + expectEqual(r.removed, chunk) + expectEqual(rope.offset(of: r.next, in: Chunk.Metric()), position, "\(i)") + rope._invariantCheck() + } + expectTrue(rope.isEmpty) + expectEqual(rope.summary, .zero) + } + + func test_join() { + let c = 100_000 + var trees = (0 ..< c).map { + let chunk = Chunk(length: ($0 % 4) + 1, value: $0) + return Rope(CollectionOfOne(chunk)) + } + var ranges = (0 ..< c).map { $0 ..< $0 + 1 } + + var rng = RepeatableRandomNumberGenerator(seed: 0) + while trees.count >= 2 { + let i = (0 ..< trees.count - 1).randomElement(using: &rng)! + let expectedRange = ranges[i].lowerBound ..< ranges[i + 1].upperBound + + let a = trees[i] + let b = trees.remove(at: i + 1) + trees[i] = Rope() + + let joined = Rope.join(a, b) + joined._invariantCheck() + let actualValues = joined.map { $0.value } + expectEqual(actualValues, Array(expectedRange)) + trees[i] = joined + ranges.replaceSubrange(i ... i + 1, with: CollectionOfOne(expectedRange)) + } + expectEqual(ranges, [0 ..< c]) + } + + func chunkify(_ values: [Int]) -> [Chunk] { + var result: [Chunk] = [] + var last = Int.min + var length = 0 + for i in values { + if length == 0 || i == last { + length += 1 + } else { + result.append(Chunk(length: length, value: last)) + length = 1 + } + last = i + } + if length > 0 { + result.append(Chunk(length: length, value: last)) + } + return result + } + + func checkEqual( + _ x: Rope, + _ y: [Int], + file: StaticString = #file, + line: UInt = #line + ) { + checkEqual(x, chunkify(y), file: file, line: line) + } + + func checkEqual( + _ x: Rope, + _ y: [Chunk], + file: StaticString = #file, + line: UInt = #line + ) { + let u = Array(x) + expectEqual(u, y, file: file, line: line) + } + + func checkRemoveSubrange( + _ a: Rope, + _ b: [Int], + range: Range, + file: StaticString = #file, + line: UInt = #line + ) { + var x = a + x.removeSubrange(range, in: Chunk.Metric()) + var y = b + y.removeSubrange(range) + + checkEqual(x, y, file: file, line: line) + } + + func test_removeSubrange_simple() { + var rope = Rope() + + checkRemoveSubrange(rope, [], range: 0 ..< 0) + + for i in 0 ..< 10 { + rope.append(Chunk(length: 10, value: i)) + } + let ref = (0 ..< 10).flatMap { Array(repeating: $0, count: 10) } + + // Basics + checkRemoveSubrange(rope, ref, range: 0 ..< 0) + checkRemoveSubrange(rope, ref, range: 30 ..< 30) + checkRemoveSubrange(rope, ref, range: 0 ..< 100) + + // Whole individual chunks + checkRemoveSubrange(rope, ref, range: 90 ..< 100) + checkRemoveSubrange(rope, ref, range: 0 ..< 10) + checkRemoveSubrange(rope, ref, range: 30 ..< 40) + checkRemoveSubrange(rope, ref, range: 70 ..< 80) + + // Prefixes of single chunks + checkRemoveSubrange(rope, ref, range: 0 ..< 1) + checkRemoveSubrange(rope, ref, range: 30 ..< 35) + checkRemoveSubrange(rope, ref, range: 60 ..< 66) + checkRemoveSubrange(rope, ref, range: 90 ..< 98) + + // Suffixes of single chunks + checkRemoveSubrange(rope, ref, range: 9 ..< 10) + checkRemoveSubrange(rope, ref, range: 35 ..< 40) + checkRemoveSubrange(rope, ref, range: 64 ..< 70) + checkRemoveSubrange(rope, ref, range: 98 ..< 100) + + // Neighboring couple of whole chunks + checkRemoveSubrange(rope, ref, range: 0 ..< 20) + checkRemoveSubrange(rope, ref, range: 80 ..< 100) + checkRemoveSubrange(rope, ref, range: 10 ..< 30) + checkRemoveSubrange(rope, ref, range: 50 ..< 70) // Crosses nodes! + + // Longer whole chunk sequences + checkRemoveSubrange(rope, ref, range: 0 ..< 30) + checkRemoveSubrange(rope, ref, range: 70 ..< 90) + checkRemoveSubrange(rope, ref, range: 0 ..< 60) // entire first node + checkRemoveSubrange(rope, ref, range: 60 ..< 100) // entire second node + checkRemoveSubrange(rope, ref, range: 40 ..< 70) // crosses into second node + checkRemoveSubrange(rope, ref, range: 10 ..< 90) // crosses into second node + + // Arbitrary cuts + checkRemoveSubrange(rope, ref, range: 0 ..< 69) + checkRemoveSubrange(rope, ref, range: 42 ..< 73) + checkRemoveSubrange(rope, ref, range: 21 ..< 89) + checkRemoveSubrange(rope, ref, range: 1 ..< 99) + checkRemoveSubrange(rope, ref, range: 1 ..< 59) + checkRemoveSubrange(rope, ref, range: 61 ..< 99) + } + + func test_removeSubrange_larger() { + var rope = Rope() + for i in 0 ..< 100 { + rope.append(Chunk(length: 10, value: i)) + } + let ref = (0 ..< 100).flatMap { Array(repeating: $0, count: 10) } + + checkRemoveSubrange(rope, ref, range: 0 ..< 0) + checkRemoveSubrange(rope, ref, range: 0 ..< 1000) + checkRemoveSubrange(rope, ref, range: 0 ..< 100) + checkRemoveSubrange(rope, ref, range: 900 ..< 1000) + checkRemoveSubrange(rope, ref, range: 120 ..< 330) + checkRemoveSubrange(rope, ref, range: 734 ..< 894) + checkRemoveSubrange(rope, ref, range: 183 ..< 892) + + checkRemoveSubrange(rope, ref, range: 181 ..< 479) + checkRemoveSubrange(rope, ref, range: 191 ..< 469) + checkRemoveSubrange(rope, ref, range: 2 ..< 722) + checkRemoveSubrange(rope, ref, range: 358 ..< 718) + checkRemoveSubrange(rope, ref, range: 12 ..< 732) + checkRemoveSubrange(rope, ref, range: 348 ..< 728) + checkRemoveSubrange(rope, ref, range: 63 ..< 783) + checkRemoveSubrange(rope, ref, range: 297 ..< 655) + } + + func test_removeSubrange_random() { + for iteration in 0 ..< 20 { + var rng = RepeatableRandomNumberGenerator(seed: iteration) + let c = 1000 + var rope = Rope() + for i in 0 ..< c { + rope.append(Chunk(length: 2, value: i)) + } + var ref = (0 ..< c).flatMap { Array(repeating: $0, count: 2) } + + while !ref.isEmpty { + print(ref.count) + let i = (0 ..< ref.count).randomElement(using: &rng)! + let j = (i + 1 ... ref.count).randomElement(using: &rng)! + rope.removeSubrange(i ..< j, in: Chunk.Metric()) + ref.removeSubrange(i ..< j) + checkEqual(rope, ref) + } + } + } + + func test_replaceSubrange_simple() { + let ranges: [Range] = [ + // Basics + 0 ..< 0, 30 ..< 30, 0 ..< 100, + // Whole individual chunks + 90 ..< 100, 0 ..< 10, 30 ..< 40, 70 ..< 80, + // Prefixes of single chunks + 0 ..< 1, 30 ..< 35, 60 ..< 66, 90 ..< 98, + + // Suffixes of single chunks + 9 ..< 10, 35 ..< 40, 64 ..< 70, 98 ..< 100, + + // Neighboring couple of whole chunks + 0 ..< 20, 80 ..< 100, 10 ..< 30, + 50 ..< 70, // Crosses nodes! + + // Longer whole chunk sequences + 0 ..< 30, 70 ..< 90, + 0 ..< 60, // entire first node + 60 ..< 100, // entire second node + 40 ..< 70, // crosses into second node + 10 ..< 90, // crosses into second node + + // Arbitrary cuts + 0 ..< 69, 42 ..< 73, 21 ..< 89, 1 ..< 99, 1 ..< 59, 61 ..< 99, + ] + + let replacements: [[Chunk]] = [ + [], + [-1], + Array(repeating: -1, count: 10), + Array(repeating: -1, count: 100), + ].map { chunkify($0) } + + let rope: Rope = { + var rope = Rope() + for i in 0 ..< 10 { + rope.append(Chunk(length: 10, value: i)) + } + return rope + }() + let ref = (0 ..< 10).flatMap { Array(repeating: $0, count: 10) } + + withEvery("replacement", in: replacements) { replacement in + var rope = Rope() + rope.replaceSubrange(0 ..< 0, in: Chunk.Metric(), with: replacement) + checkEqual(rope, replacement) + } + + withEvery("replacement", in: replacements) { replacement in + withEvery("range", in: ranges) { range in + var expected = ref + expected.removeSubrange(range) + + var actual = rope + actual.removeSubrange(range, in: Chunk.Metric()) + + checkEqual(actual, expected) + } + } + } +} diff --git a/Sources/_CollectionsTestSupport/AssertionContexts/Assertions.swift b/Tests/_CollectionsTestSupport/AssertionContexts/Assertions.swift similarity index 98% rename from Sources/_CollectionsTestSupport/AssertionContexts/Assertions.swift rename to Tests/_CollectionsTestSupport/AssertionContexts/Assertions.swift index ff2e6e4f7..7224be07c 100644 --- a/Sources/_CollectionsTestSupport/AssertionContexts/Assertions.swift +++ b/Tests/_CollectionsTestSupport/AssertionContexts/Assertions.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2014 - 2024 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 @@ -27,7 +27,7 @@ public func expectFailure( } } -internal func _expectFailure( +public func _expectFailure( _ diagnostic: String, _ message: () -> String, trapping: Bool, @@ -402,3 +402,7 @@ public func expectThrows( errorHandler(error) } } + +public func expectType(_ actual: T, _ expectedType: Any.Type) { + expectTrue(type(of: actual) == expectedType) +} diff --git a/Sources/_CollectionsTestSupport/AssertionContexts/CollectionTestCase.swift b/Tests/_CollectionsTestSupport/AssertionContexts/CollectionTestCase.swift similarity index 58% rename from Sources/_CollectionsTestSupport/AssertionContexts/CollectionTestCase.swift rename to Tests/_CollectionsTestSupport/AssertionContexts/CollectionTestCase.swift index 044a2c08b..af5515e77 100644 --- a/Sources/_CollectionsTestSupport/AssertionContexts/CollectionTestCase.swift +++ b/Tests/_CollectionsTestSupport/AssertionContexts/CollectionTestCase.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2014 - 2024 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 @@ -17,14 +17,28 @@ open class CollectionTestCase: XCTestCase { public var context: TestContext { _context! } - public override func setUp() { + open var isAvailable: Bool { true } + + #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) + open override func invokeTest() { + guard isAvailable else { + print("\(Self.self) unavailable; skipping") + return + } + return super.invokeTest() + } + #endif + + open override func setUp() { super.setUp() _context = TestContext.pushNew() } - public override func tearDown() { - TestContext.pop(context) - _context = nil + open override func tearDown() { + if let context = _context { + TestContext.pop(context) + _context = nil + } super.tearDown() } } diff --git a/Sources/_CollectionsTestSupport/AssertionContexts/Combinatorics.swift b/Tests/_CollectionsTestSupport/AssertionContexts/Combinatorics.swift similarity index 64% rename from Sources/_CollectionsTestSupport/AssertionContexts/Combinatorics.swift rename to Tests/_CollectionsTestSupport/AssertionContexts/Combinatorics.swift index 1c3aa0a71..d0c7ac9b2 100644 --- a/Sources/_CollectionsTestSupport/AssertionContexts/Combinatorics.swift +++ b/Tests/_CollectionsTestSupport/AssertionContexts/Combinatorics.swift @@ -2,13 +2,37 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 // //===----------------------------------------------------------------------===// +/// Run the supplied closure with all values in `items` in a loop, +/// recording the current value in the current test trace stack. +public func withEvery( + _ label: String, + by generator: () -> Element?, + file: StaticString = #file, + line: UInt = #line, + run body: (Element) throws -> Void +) rethrows { + let context = TestContext.current + while let item = generator() { + let entry = context.push("\(label): \(item)", file: file, line: line) + var done = false + defer { + context.pop(entry) + if !done { + print(context.currentTrace(title: "Throwing trace")) + } + } + try body(item) + done = true + } +} + /// Run the supplied closure with all values in `items` in a loop, /// recording the current value in the current test trace stack. public func withEvery( @@ -79,48 +103,55 @@ internal func _samples(from items: C) -> [C.Element] { public func withSome( _ label: String, in items: C, + maxSamples: Int? = nil, file: StaticString = #file, line: UInt = #line, run body: (C.Element) throws -> Void ) rethrows { let context = TestContext.current - for item in _samples(from: items) { + + func check(item: C.Element) throws { let entry = context.push("\(label): \(item)", file: file, line: line) var done = false defer { - context.pop(entry) if !done { print(context.currentTrace(title: "Throwing trace")) } + context.pop(entry) } try body(item) done = true } + + let c = items.count + + if let maxSamples = maxSamples, c > maxSamples { + // FIXME: Use randomSample() from swift-algorithms to eliminate dupes + for _ in 0 ..< maxSamples { + let r = Int.random(in: 0 ..< c) + let i = items.index(items.startIndex, offsetBy: r) + try check(item: items[i]) + } + } else { + for item in items { + try check(item: item) + } + } } public func withSomeRanges( _ label: String, in bounds: Range, + maxSamples: Int? = nil, file: StaticString = #file, line: UInt = #line, run body: (Range) throws -> Void ) rethrows where T.Stride == Int { - let context = TestContext.current - for lowerBound in _samples(from: bounds) { - for upperBound in _samples(from: lowerBound ... bounds.upperBound) { - let range = lowerBound ..< upperBound - let entry = context.push("\(label): \(range)", file: file, line: line) - var done = false - defer { - context.pop(entry) - if !done { - print(context.currentTrace(title: "Throwing trace")) - } - } - try body(range) - done = true - } - } + try withSome( + label, + in: IndexRangeCollection(bounds: bounds), + maxSamples: maxSamples, + run: body) } /// Utility function for testing mutations with value semantics. @@ -181,3 +212,68 @@ public func withHiddenCopies< checker(copy) return result } + +/// Run the supplied closure with all subsets `items` in a loop, +/// recording the current subset in the test trace stack. +/// +/// The subsets are generated so that they contain elements in the same order +/// as the original collection. +public func withEverySubset( + _ label: String, + of items: C, + body: ([C.Element]) -> Void +) { + var set: [C.Element] = [] + _withEverySubset(label, of: items, extending: &set, body: body) +} + +func _withEverySubset( + _ label: String, + of items: C, + extending set: inout [C.Element], + body: ([C.Element]) -> Void +) { + guard let item = items.first else { + let entry = TestContext.current.push("\(label): \(set)") + defer { TestContext.current.pop(entry) } + body(set) + return + } + _withEverySubset(label, of: items.dropFirst(), extending: &set, body: body) + set.append(item) + _withEverySubset(label, of: items.dropFirst(), extending: &set, body: body) + set.removeLast() +} + +public func withEveryPermutation( + _ label: String, + of items: C, + body: ([C.Element]) -> Void +) { + func send(_ items: [C.Element]) { + let entry = TestContext.current.push("\(label): \(items)") + body(items) + TestContext.current.pop(entry) + } + + // Heap's algorithm. + var items = Array(items) + send(items) + var i = 1 + var c = [Int](repeating: 0, count: items.count) + while i < items.count { + if c[i] < i { + if i.isMultiple(of: 2) { + items.swapAt(0, i) + } else { + items.swapAt(c[i], i) + } + send(items) + c[i] += 1 + i = 1 + } else { + c[i] = 0 + i += 1 + } + } +} diff --git a/Sources/_CollectionsTestSupport/AssertionContexts/TestContext.swift b/Tests/_CollectionsTestSupport/AssertionContexts/TestContext.swift similarity index 99% rename from Sources/_CollectionsTestSupport/AssertionContexts/TestContext.swift rename to Tests/_CollectionsTestSupport/AssertionContexts/TestContext.swift index d12ab6dba..ad3b89af4 100644 --- a/Sources/_CollectionsTestSupport/AssertionContexts/TestContext.swift +++ b/Tests/_CollectionsTestSupport/AssertionContexts/TestContext.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 diff --git a/Sources/_CollectionsTestSupport/ConformanceCheckers/CheckBidirectionalCollection.swift b/Tests/_CollectionsTestSupport/ConformanceCheckers/CheckBidirectionalCollection.swift similarity index 57% rename from Sources/_CollectionsTestSupport/ConformanceCheckers/CheckBidirectionalCollection.swift rename to Tests/_CollectionsTestSupport/ConformanceCheckers/CheckBidirectionalCollection.swift index 5c14935d2..d7c20c106 100644 --- a/Sources/_CollectionsTestSupport/ConformanceCheckers/CheckBidirectionalCollection.swift +++ b/Tests/_CollectionsTestSupport/ConformanceCheckers/CheckBidirectionalCollection.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2014 - 2024 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 @@ -43,6 +43,7 @@ extension BidirectionalCollection { public func checkBidirectionalCollection( _ collection: C, expectedContents: S, + maxSamples: Int? = nil, file: StaticString = #file, line: UInt = #line ) where C.Element: Equatable, S.Element == C.Element { @@ -50,6 +51,7 @@ public func checkBidirectionalCollection Bool, + maxSamples: Int? = nil, file: StaticString = #file, line: UInt = #line ) where S.Element == C.Element { @@ -68,17 +71,75 @@ public func checkBidirectionalCollection( + _ collection: C, + expectedContents: [C.Element], + by areEquivalent: (C.Element, C.Element) -> Bool, + maxSamples: Int? = nil, + file: StaticString = #file, + line: UInt = #line +) { + var allIndices = collection._indicesByIndexAfter() + allIndices.append(collection.endIndex) + + withSomeRanges( + "range", + in: 0 ..< allIndices.count - (1 as Int), + maxSamples: maxSamples + ) { range in + let i = range.lowerBound + let j = range.upperBound + + // Check `index(_,offsetBy:)` with negative offsets + let a = collection.index(allIndices[j], offsetBy: i - j) + expectEqual(a, allIndices[i]) + if i < expectedContents.count { + expectEquivalent( + collection[a], expectedContents[i], + by: areEquivalent) + } + + // Check `distance(from:to:)` with decreasing indices + let d = collection.distance(from: allIndices[j], to: allIndices[i]) + expectEqual(d, i - j) + } + + // Check `index(_,offsetBy:limitedBy:)` + let limits = + Set([0, allIndices.count - 1, allIndices.count / 2]) + .sorted() + withEvery("limit", in: limits) { limit in + withEvery("i", in: 0 ..< allIndices.count) { i in + let min = -i - (limit <= i ? 2 : 0) + withEvery("delta", in: stride(from: 0, through: min, by: -1)) { delta in + let actual = collection.index( + allIndices[i], + offsetBy: delta, + limitedBy: allIndices[limit]) + let j = i + delta + let expected = i < limit || j >= limit ? allIndices[j] : nil + expectEqual(actual, expected) + } + } + } +} + public func _checkBidirectionalCollection( _ collection: C, expectedContents: S, by areEquivalent: (S.Element, S.Element) -> Bool, + maxSamples: Int? = nil, file: StaticString = #file, line: UInt = #line ) where S.Element == C.Element { @@ -106,42 +167,35 @@ public func _checkBidirectionalCollection.self { + TestContext.current.withTrace("Indices") { + checkBidirectionalCollection( + collection.indices, + expectedContents: indicesByIndexAfter, + maxSamples: maxSamples) } } } diff --git a/Sources/_CollectionsTestSupport/ConformanceCheckers/CheckCollection.swift b/Tests/_CollectionsTestSupport/ConformanceCheckers/CheckCollection.swift similarity index 60% rename from Sources/_CollectionsTestSupport/ConformanceCheckers/CheckCollection.swift rename to Tests/_CollectionsTestSupport/ConformanceCheckers/CheckCollection.swift index 8f5588b39..80e95d012 100644 --- a/Sources/_CollectionsTestSupport/ConformanceCheckers/CheckCollection.swift +++ b/Tests/_CollectionsTestSupport/ConformanceCheckers/CheckCollection.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2014 - 2024 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 @@ -67,6 +67,7 @@ extension Collection { public func checkCollection( _ collection: C, expectedContents: Expected, + maxSamples: Int? = nil, file: StaticString = #file, line: UInt = #line ) where C.Element: Equatable, Expected.Element == C.Element { @@ -76,6 +77,7 @@ public func checkCollection( collection, expectedContents: expectedContents, by: ==, + maxSamples: maxSamples, file: file, line: line) let indicesByIndexAfter = collection._indicesByIndexAfter() @@ -101,6 +103,7 @@ public func checkCollection( _ collection: C, expectedContents: Expected, by areEquivalent: (C.Element, C.Element) -> Bool, + maxSamples: Int? = nil, file: StaticString = #file, line: UInt = #line ) where Expected.Element == C.Element { @@ -113,6 +116,7 @@ public func checkCollection( collection, expectedContents: expectedContents, by: areEquivalent, + maxSamples: maxSamples, file: file, line: line) } @@ -120,6 +124,7 @@ public func _checkCollection( _ collection: C, expectedContents: Expected, by areEquivalent: (C.Element, C.Element) -> Bool, + maxSamples: Int? = nil, file: StaticString = #file, line: UInt = #line ) where Expected.Element == C.Element { @@ -156,30 +161,20 @@ public func _checkCollection( // Check the endIndex. expectEqual(collection.endIndex, collection.indices.endIndex) - // Check the Indices associated type - if C.self != C.Indices.self { - checkCollection(collection.indices, expectedContents: indicesByIndexAfter) - } else { - expectEqual(collection.indices.count, collection.count) - expectEqualElements(collection.indices, indicesByIndexAfter) - } + // Quickly check the Indices associated type + expectEqual(collection.indices.count, collection.count) + expectEqualElements(collection.indices, indicesByIndexAfter) expectEqual(collection.indices.endIndex, collection.endIndex) // The sequence of indices must be monotonically increasing. var allIndices = indicesByIndexAfter allIndices.append(collection.endIndex) - checkComparable(allIndices, oracle: { .comparing($0, $1) }) - // Check `index(_,offsetBy:)` - for (offset, start) in allIndices.enumerated() { - for distance in 0 ... indicesByIndexAfter.count - offset { - let end = collection.index(start, offsetBy: distance) - expectEqual(end, allIndices[offset + distance]) - if offset + distance < expectedContents.count { - expectEquivalent(collection[end], expectedContents[offset + distance], - by: areEquivalent) - } - } + if C.Index.self != Int.self { + checkComparable( + allIndices, + oracle: { .comparing($0, $1) }, + maxSamples: maxSamples) } // Check `index(_,offsetBy:limitedBy:)` @@ -200,71 +195,90 @@ public func _checkCollection( } } } + + withSomeRanges( + "range", in: 0 ..< allIndices.count - 1, maxSamples: maxSamples + ) { range in + let i = range.lowerBound + let j = range.upperBound - // Check `distance(from:to:)` - withEvery("i", in: allIndices.indices) { i in - withEvery("j", in: allIndices.indices[i...]) { j in - let d = collection.distance(from: allIndices[i], to: allIndices[j]) - expectEqual(d, j - i) + // Check `index(_,offsetBy:)` + let e = collection.index(allIndices[i], offsetBy: j - i) + expectEqual(e, allIndices[j]) + if j < expectedContents.count { + expectEquivalent(collection[e], expectedContents[j], by: areEquivalent) } - } - - // Check slicing. - withEvery("i", in: 0 ..< allIndices.count) { i in - withEvery("j", in: i ..< allIndices.count) { j in - let range = allIndices[i] ..< allIndices[j] - let slice = collection[range] - expectEqual(slice.count, j - i) - expectEqual(slice.isEmpty, i == j) - expectEqual(slice.startIndex, allIndices[i]) - expectEqual(slice.endIndex, allIndices[j]) - expectEqual(slice.distance(from: allIndices[i], to: allIndices[j]), j - i) - expectEqual(slice.index(allIndices[i], offsetBy: j - i), allIndices[j]) - - expectEqual(slice.index(allIndices[i], offsetBy: j - i, limitedBy: allIndices[j]), - allIndices[j]) - expectEqual(slice.index(allIndices[i], offsetBy: j - i, limitedBy: allIndices[i]), - j - i > 0 ? nil : allIndices[i]) - expectEqual(slice.index(allIndices[i], offsetBy: j - i, limitedBy: allIndices[0]), - i > 0 || j == 0 ? allIndices[j] : nil) - - expectEquivalentElements(slice, expectedContents[i ..< j], - by: areEquivalent) - - expectEquivalentElements( - slice._contentsByIterator(), expectedContents[i ..< j], - by: areEquivalent) - expectEquivalentElements( - slice._contentsByCopyContents(), expectedContents[i ..< j], - by: areEquivalent) - // Check _copyContents. - let copyContents = collection._contentsByCopyContents() - expectEquivalentElements( - copyContents, expectedContents, + // Check `distance(from:to:)` + let d = collection.distance(from: allIndices[i], to: allIndices[j]) + expectEqual(d, j - i) + + // Check slicing. + let range = allIndices[i] ..< allIndices[j] + let slice = collection[range] + expectEqual(slice.count, j - i) + expectEqual(slice.isEmpty, i == j) + expectEqual(slice.startIndex, allIndices[i]) + expectEqual(slice.endIndex, allIndices[j]) + expectEqual(slice.distance(from: allIndices[i], to: allIndices[j]), j - i) + expectEqual(slice.index(allIndices[i], offsetBy: j - i), allIndices[j]) + + expectEqual(slice.index(allIndices[i], offsetBy: j - i, limitedBy: allIndices[j]), + allIndices[j]) + expectEqual(slice.index(allIndices[i], offsetBy: j - i, limitedBy: allIndices[i]), + j - i > 0 ? nil : allIndices[i]) + expectEqual(slice.index(allIndices[i], offsetBy: j - i, limitedBy: allIndices[0]), + i > 0 || j == 0 ? allIndices[j] : nil) + + expectEquivalentElements(slice, expectedContents[i ..< j], + by: areEquivalent) + + expectEquivalentElements( + slice._contentsByIterator(), expectedContents[i ..< j], + by: areEquivalent) + expectEquivalentElements( + slice._contentsByCopyContents(), expectedContents[i ..< j], + by: areEquivalent) + + // Check _copyContents. + let copyContents = collection._contentsByCopyContents() + expectEquivalentElements( + copyContents, expectedContents, + by: areEquivalent) + + expectEqualElements(slice._indicesByIndexAfter(), allIndices[i ..< j]) + expectEqualElements(slice._indicesByFormIndexAfter(), allIndices[i ..< j]) + expectEqualElements(slice.indices, allIndices[i ..< j]) + expectEqualElements(slice.indices._indicesByIndexAfter(), allIndices[i ..< j]) + expectEqualElements(slice.indices._indicesByFormIndexAfter(), allIndices[i ..< j]) + // Check the subsequence iterator. + expectEquivalentElements( + slice, expectedContents[i ..< j], + by: areEquivalent) + // Check the subsequence subscript. + withSome("k", in: i ..< j, maxSamples: 25) { k in + expectEquivalent( + slice[allIndices[k]], + expectedContents[k], by: areEquivalent) + } + // Check _copyToContiguousArray. + expectEquivalentElements( + Array(slice), expectedContents[i ..< j], + by: areEquivalent) + // Check slicing of slices. + expectEquivalentElements( + slice[range], expectedContents[i ..< j], + by: areEquivalent) + } - expectEqualElements(slice._indicesByIndexAfter(), allIndices[i ..< j]) - expectEqualElements(slice._indicesByFormIndexAfter(), allIndices[i ..< j]) - expectEqualElements(slice.indices, allIndices[i ..< j]) - expectEqualElements(slice.indices._indicesByIndexAfter(), allIndices[i ..< j]) - expectEqualElements(slice.indices._indicesByFormIndexAfter(), allIndices[i ..< j]) - // Check the subsequence iterator. - expectEquivalentElements( - slice, expectedContents[i ..< j], - by: areEquivalent) - // Check the subsequence subscript. - expectEquivalentElements( - allIndices[i ..< j].map { slice[$0] }, expectedContents[i ..< j], - by: areEquivalent) - // Check _copyToContiguousArray. - expectEquivalentElements( - Array(slice), expectedContents[i ..< j], - by: areEquivalent) - // Check slicing of slices. - expectEquivalentElements( - slice[range], expectedContents[i ..< j], - by: areEquivalent) + if C.Indices.self != C.self && C.Indices.self != DefaultIndices.self { + // Do a more exhaustive check on Indices. + TestContext.current.withTrace("Indices") { + checkCollection( + collection.indices, + expectedContents: indicesByIndexAfter, + maxSamples: maxSamples) } } } diff --git a/Sources/_CollectionsTestSupport/ConformanceCheckers/CheckComparable.swift b/Tests/_CollectionsTestSupport/ConformanceCheckers/CheckComparable.swift similarity index 75% rename from Sources/_CollectionsTestSupport/ConformanceCheckers/CheckComparable.swift rename to Tests/_CollectionsTestSupport/ConformanceCheckers/CheckComparable.swift index 5067b35e4..8bde8a7de 100644 --- a/Sources/_CollectionsTestSupport/ConformanceCheckers/CheckComparable.swift +++ b/Tests/_CollectionsTestSupport/ConformanceCheckers/CheckComparable.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2014 - 2024 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 @@ -48,6 +48,7 @@ extension ExpectedComparisonResult: CustomStringConvertible { public func checkComparable( sortedEquivalenceClasses: [[Instance]], + maxSamples: Int? = nil, file: StaticString = #file, line: UInt = #line ) { let instances = sortedEquivalenceClasses.flatMap { $0 } @@ -60,6 +61,7 @@ public func checkComparable( if oracle[$0] > oracle[$1] { return .gt } return .eq }, + maxSamples: maxSamples, file: file, line: line) } @@ -69,16 +71,24 @@ public func checkComparable( public func checkComparable( _ instances: Instances, oracle: (Instances.Index, Instances.Index) -> ExpectedComparisonResult, + maxSamples: Int? = nil, file: StaticString = #file, line: UInt = #line -) where Instances.Element: Comparable { - checkEquatable(instances, - oracle: { oracle($0, $1) == .eq }, - file: file, line: line) - _checkComparable(instances, oracle: oracle, file: file, line: line) +) where Instances.Element: Comparable, Instances.Index == Int { + checkEquatable( + instances, + oracle: { oracle($0, $1) == .eq }, + maxSamples: maxSamples, + file: file, line: line) + _checkComparable( + instances, + oracle: oracle, + maxSamples: maxSamples, + file: file, line: line) } public func checkComparable( - expected: ExpectedComparisonResult, _ lhs: T, _ rhs: T, + expected: ExpectedComparisonResult, + _ lhs: T, _ rhs: T, file: StaticString = #file, line: UInt = #line ) { checkComparable( @@ -92,78 +102,86 @@ public func checkComparable( public func _checkComparable( _ instances: Instances, oracle: (Instances.Index, Instances.Index) -> ExpectedComparisonResult, + maxSamples: Int? = nil, file: StaticString = #file, line: UInt = #line -) where Instances.Element: Comparable { +) where Instances.Element: Comparable, Instances.Index == Int { let entry = TestContext.current.push("checkComparable", file: file, line: line) defer { TestContext.current.pop(entry) } - for i in instances.indices { - let x = instances[i] - expectFalse( - x < x, - "found 'x < x' at index \(i): \(String(reflecting: x))") + withSomeRanges( + "range", in: 0 ..< instances.count - 1, maxSamples: maxSamples + ) { range in + let i = range.lowerBound + let j = range.upperBound - expectFalse( - x > x, - "found 'x > x' at index \(i): \(String(reflecting: x))") + if i == j { + let x = instances[i] - expectTrue(x <= x, - "found 'x <= x' to be false at index \(i): \(String(reflecting: x))") + expectFalse( + x < x, + "found 'x < x' at index \(i): \(String(reflecting: x))") - expectTrue(x >= x, - "found 'x >= x' to be false at index \(i): \(String(reflecting: x))") + expectFalse( + x > x, + "found 'x > x' at index \(i): \(String(reflecting: x))") - for j in instances.indices where i != j { - let y = instances[j] + expectTrue(x <= x, + "found 'x <= x' to be false at index \(i): \(String(reflecting: x))") + expectTrue(x >= x, + "found 'x >= x' to be false at index \(i): \(String(reflecting: x))") + } else { + let x = instances[i] + let y = instances[j] + let expected = oracle(i, j) - + expectEqual( expected.flip(), oracle(j, i), - """ + """ bad oracle: missing antisymmetry: lhs (at index \(i)): \(String(reflecting: x)) rhs (at index \(j)): \(String(reflecting: y)) """) - + expectEqual( expected == .lt, x < y, - """ + """ x < y doesn't match oracle lhs (at index \(i)): \(String(reflecting: x)) rhs (at index \(j)): \(String(reflecting: y)) """) - + expectEqual( expected != .gt, x <= y, - """ + """ x <= y doesn't match oracle lhs (at index \(i)): \(String(reflecting: x)) rhs (at index \(j)): \(String(reflecting: y)) """) - + expectEqual( expected != .lt, x >= y, - """ + """ x >= y doesn't match oracle lhs (at index \(i)): \(String(reflecting: x)) rhs (at index \(j)): \(String(reflecting: y)) """) - + expectEqual( expected == .gt, x > y, - """ + """ x > y doesn't match oracle lhs (at index \(i)): \(String(reflecting: x)) rhs (at index \(j)): \(String(reflecting: y)) """) - - for k in instances.indices { + + withSome("k", in: instances.indices, maxSamples: 10) { k in let expected2 = oracle(j, k) if expected == expected2 { expectEqual( expected, oracle(i, k), - """ + """ bad oracle: transitivity violation x (at index \(i)): \(String(reflecting: x)) y (at index \(j)): \(String(reflecting: y)) diff --git a/Sources/_CollectionsTestSupport/ConformanceCheckers/CheckEquatable.swift b/Tests/_CollectionsTestSupport/ConformanceCheckers/CheckEquatable.swift similarity index 60% rename from Sources/_CollectionsTestSupport/ConformanceCheckers/CheckEquatable.swift rename to Tests/_CollectionsTestSupport/ConformanceCheckers/CheckEquatable.swift index 1b58b2e68..4bc8586fc 100644 --- a/Sources/_CollectionsTestSupport/ConformanceCheckers/CheckEquatable.swift +++ b/Tests/_CollectionsTestSupport/ConformanceCheckers/CheckEquatable.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2014 - 2024 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 @@ -14,6 +14,7 @@ public func checkEquatable( equivalenceClasses: [[Instance]], + maxSamples: Int? = nil, file: StaticString = #file, line: UInt = #line ) { @@ -25,6 +26,7 @@ public func checkEquatable( public func checkEquatable( _ instances: C, oracle: (C.Index, C.Index) -> Bool, + maxSamples: Int? = nil, file: StaticString = #file, line: UInt = #line ) where C.Element: Equatable { @@ -48,6 +50,7 @@ public func checkEquatable( public func checkEquatable( _ instances: [Instance], oracle: (Int, Int) -> Bool, + maxSamples: Int? = nil, file: StaticString = #file, line: UInt = #line ) { @@ -57,21 +60,27 @@ public func checkEquatable( // set of equal instances. var transitivityScoreboard: [Box>] = instances.map { _ in Box([]) } - for i in instances.indices { - let x = instances[i] - expectTrue(oracle(i, i), - "bad oracle: broken reflexivity at index \(i)") - - for j in instances.indices { - let y = instances[j] + withSomeRanges( + "range", in: 0 ..< instances.count - 1, maxSamples: maxSamples + ) { range in + let i = range.lowerBound + let j = range.upperBound - let expectedXY = oracle(i, j) - expectEqual(oracle(j, i), expectedXY, - "bad oracle: broken symmetry between indices \(i), \(j)") + if i == j { + expectTrue(oracle(i, i), + "bad oracle: broken reflexivity at index \(i)") + } + let x = instances[i] + let y = instances[j] - let actualXY = (x == y) - expectEqual( - actualXY, expectedXY, + let expectedXY = oracle(i, j) + expectEqual(oracle(j, i), expectedXY, + "bad oracle: broken symmetry between indices \(i), \(j)") + + let actualXY = (x == y) + let actualYX = (y == x) + expectEqual( + actualXY, expectedXY, """ Elements \((expectedXY ? "expected equal, found not equal" @@ -80,37 +89,47 @@ public func checkEquatable( rhs (at index \(j)): \(String(reflecting: y)) """) - // Not-equal is an inverse of equal. - expectNotEqual( - x != y, actualXY, + expectEqual( + actualYX, actualXY, """ - `!=` returns the same result as `==`: + Reflexivity violation: + \(actualXY + ? "lhs == rhs but !(rhs == lhs)" + : "!(lhs == rhs) but rhs == lhs") lhs (at index \(i)): \(String(reflecting: x)) rhs (at index \(j)): \(String(reflecting: y)) """) - // Check transitivity of the predicate represented by the oracle. - // If we are adding the instance `j` into an equivalence set, check that - // it is equal to every other instance in the set. - if expectedXY && i < j && transitivityScoreboard[i].value.insert(j).inserted { - if transitivityScoreboard[i].value.count == 1 { - transitivityScoreboard[i].value.insert(i) - } - for k in transitivityScoreboard[i].value { - expectTrue( - oracle(j, k), + // Not-equal must be the inverse of equal. + expectNotEqual( + x != y, actualXY, + """ + `!=` returns the same result as `==`: + lhs (at index \(i)): \(String(reflecting: x)) + rhs (at index \(j)): \(String(reflecting: y)) + """) + + // Check transitivity of the predicate represented by the oracle. + // If we are adding the instance `j` into an equivalence set, check that + // it is equal to every other instance in the set. + if expectedXY && i < j && transitivityScoreboard[i].value.insert(j).inserted { + if transitivityScoreboard[i].value.count == 1 { + transitivityScoreboard[i].value.insert(i) + } + for k in transitivityScoreboard[i].value { + expectTrue( + oracle(j, k), """ bad oracle: transitivity violation x (at index \(i)): \(String(reflecting: x)) y (at index \(j)): \(String(reflecting: y)) z (at index \(k)): \(String(reflecting: instances[k])) """) - // No need to check equality between actual values, we will check - // them with the checks above. - } - precondition(transitivityScoreboard[j].value.isEmpty) - transitivityScoreboard[j] = transitivityScoreboard[i] + // No need to check equality between actual values, we will check + // them with the checks above. } + precondition(transitivityScoreboard[j].value.isEmpty) + transitivityScoreboard[j] = transitivityScoreboard[i] } } } diff --git a/Sources/_CollectionsTestSupport/ConformanceCheckers/CheckHashable.swift b/Tests/_CollectionsTestSupport/ConformanceCheckers/CheckHashable.swift similarity index 98% rename from Sources/_CollectionsTestSupport/ConformanceCheckers/CheckHashable.swift rename to Tests/_CollectionsTestSupport/ConformanceCheckers/CheckHashable.swift index e6942c0d6..91c329a58 100644 --- a/Sources/_CollectionsTestSupport/ConformanceCheckers/CheckHashable.swift +++ b/Tests/_CollectionsTestSupport/ConformanceCheckers/CheckHashable.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2014 - 2024 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 diff --git a/Sources/_CollectionsTestSupport/ConformanceCheckers/CheckSequence.swift b/Tests/_CollectionsTestSupport/ConformanceCheckers/CheckSequence.swift similarity index 97% rename from Sources/_CollectionsTestSupport/ConformanceCheckers/CheckSequence.swift rename to Tests/_CollectionsTestSupport/ConformanceCheckers/CheckSequence.swift index b46cba711..da6869502 100644 --- a/Sources/_CollectionsTestSupport/ConformanceCheckers/CheckSequence.swift +++ b/Tests/_CollectionsTestSupport/ConformanceCheckers/CheckSequence.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2014 - 2024 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 diff --git a/Sources/_CollectionsTestSupport/MinimalTypes/MinimalBidirectionalCollection.swift b/Tests/_CollectionsTestSupport/MinimalTypes/MinimalBidirectionalCollection.swift similarity index 98% rename from Sources/_CollectionsTestSupport/MinimalTypes/MinimalBidirectionalCollection.swift rename to Tests/_CollectionsTestSupport/MinimalTypes/MinimalBidirectionalCollection.swift index f61c8bad8..02b3dbba0 100644 --- a/Sources/_CollectionsTestSupport/MinimalTypes/MinimalBidirectionalCollection.swift +++ b/Tests/_CollectionsTestSupport/MinimalTypes/MinimalBidirectionalCollection.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2014 - 2024 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 diff --git a/Sources/_CollectionsTestSupport/MinimalTypes/MinimalCollection.swift b/Tests/_CollectionsTestSupport/MinimalTypes/MinimalCollection.swift similarity index 98% rename from Sources/_CollectionsTestSupport/MinimalTypes/MinimalCollection.swift rename to Tests/_CollectionsTestSupport/MinimalTypes/MinimalCollection.swift index 499397e76..50d0d9868 100644 --- a/Sources/_CollectionsTestSupport/MinimalTypes/MinimalCollection.swift +++ b/Tests/_CollectionsTestSupport/MinimalTypes/MinimalCollection.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2014 - 2024 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 diff --git a/Sources/_CollectionsTestSupport/MinimalTypes/MinimalDecoder.swift b/Tests/_CollectionsTestSupport/MinimalTypes/MinimalDecoder.swift similarity index 99% rename from Sources/_CollectionsTestSupport/MinimalTypes/MinimalDecoder.swift rename to Tests/_CollectionsTestSupport/MinimalTypes/MinimalDecoder.swift index dd1a01b8e..a16839b51 100644 --- a/Sources/_CollectionsTestSupport/MinimalTypes/MinimalDecoder.swift +++ b/Tests/_CollectionsTestSupport/MinimalTypes/MinimalDecoder.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 @@ -179,7 +179,7 @@ extension MinimalDecoder.KeyedContainer: KeyedDecodingContainerProtocol { return input[key.stringValue] != nil } - func _decode(key: CKey) throws -> Value { + func _decode(key: K) throws -> Value { expectTrue(isValid, "Container isn't valid", trapping: true) commitPendingContainer() guard let value = input[key.stringValue] else { diff --git a/Sources/_CollectionsTestSupport/MinimalTypes/MinimalEncoder.swift b/Tests/_CollectionsTestSupport/MinimalTypes/MinimalEncoder.swift similarity index 99% rename from Sources/_CollectionsTestSupport/MinimalTypes/MinimalEncoder.swift rename to Tests/_CollectionsTestSupport/MinimalTypes/MinimalEncoder.swift index d48061a64..19d0f61b0 100644 --- a/Sources/_CollectionsTestSupport/MinimalTypes/MinimalEncoder.swift +++ b/Tests/_CollectionsTestSupport/MinimalTypes/MinimalEncoder.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 diff --git a/Sources/_CollectionsTestSupport/MinimalTypes/MinimalIndex.swift b/Tests/_CollectionsTestSupport/MinimalTypes/MinimalIndex.swift similarity index 96% rename from Sources/_CollectionsTestSupport/MinimalTypes/MinimalIndex.swift rename to Tests/_CollectionsTestSupport/MinimalTypes/MinimalIndex.swift index 032b6904b..88e558416 100644 --- a/Sources/_CollectionsTestSupport/MinimalTypes/MinimalIndex.swift +++ b/Tests/_CollectionsTestSupport/MinimalTypes/MinimalIndex.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2014 - 2024 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 diff --git a/Sources/_CollectionsTestSupport/MinimalTypes/MinimalIterator.swift b/Tests/_CollectionsTestSupport/MinimalTypes/MinimalIterator.swift similarity index 96% rename from Sources/_CollectionsTestSupport/MinimalTypes/MinimalIterator.swift rename to Tests/_CollectionsTestSupport/MinimalTypes/MinimalIterator.swift index 32a6734aa..004301503 100644 --- a/Sources/_CollectionsTestSupport/MinimalTypes/MinimalIterator.swift +++ b/Tests/_CollectionsTestSupport/MinimalTypes/MinimalIterator.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2014 - 2024 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 diff --git a/Sources/_CollectionsTestSupport/MinimalTypes/MinimalMutableRandomAccessCollection.swift b/Tests/_CollectionsTestSupport/MinimalTypes/MinimalMutableRandomAccessCollection.swift similarity index 98% rename from Sources/_CollectionsTestSupport/MinimalTypes/MinimalMutableRandomAccessCollection.swift rename to Tests/_CollectionsTestSupport/MinimalTypes/MinimalMutableRandomAccessCollection.swift index 4d2656f8f..24998e2be 100644 --- a/Sources/_CollectionsTestSupport/MinimalTypes/MinimalMutableRandomAccessCollection.swift +++ b/Tests/_CollectionsTestSupport/MinimalTypes/MinimalMutableRandomAccessCollection.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2014 - 2024 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 diff --git a/Sources/_CollectionsTestSupport/MinimalTypes/MinimalMutableRangeReplaceableRandomAccessCollection.swift b/Tests/_CollectionsTestSupport/MinimalTypes/MinimalMutableRangeReplaceableRandomAccessCollection.swift similarity index 99% rename from Sources/_CollectionsTestSupport/MinimalTypes/MinimalMutableRangeReplaceableRandomAccessCollection.swift rename to Tests/_CollectionsTestSupport/MinimalTypes/MinimalMutableRangeReplaceableRandomAccessCollection.swift index 1f70e13e9..b3eedec23 100644 --- a/Sources/_CollectionsTestSupport/MinimalTypes/MinimalMutableRangeReplaceableRandomAccessCollection.swift +++ b/Tests/_CollectionsTestSupport/MinimalTypes/MinimalMutableRangeReplaceableRandomAccessCollection.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2014 - 2024 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 diff --git a/Sources/_CollectionsTestSupport/MinimalTypes/MinimalRandomAccessCollection.swift b/Tests/_CollectionsTestSupport/MinimalTypes/MinimalRandomAccessCollection.swift similarity index 98% rename from Sources/_CollectionsTestSupport/MinimalTypes/MinimalRandomAccessCollection.swift rename to Tests/_CollectionsTestSupport/MinimalTypes/MinimalRandomAccessCollection.swift index 773011e47..bf666b8f9 100644 --- a/Sources/_CollectionsTestSupport/MinimalTypes/MinimalRandomAccessCollection.swift +++ b/Tests/_CollectionsTestSupport/MinimalTypes/MinimalRandomAccessCollection.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2014 - 2024 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 diff --git a/Sources/_CollectionsTestSupport/MinimalTypes/MinimalRangeReplaceableRandomAccessCollection.swift b/Tests/_CollectionsTestSupport/MinimalTypes/MinimalRangeReplaceableRandomAccessCollection.swift similarity index 98% rename from Sources/_CollectionsTestSupport/MinimalTypes/MinimalRangeReplaceableRandomAccessCollection.swift rename to Tests/_CollectionsTestSupport/MinimalTypes/MinimalRangeReplaceableRandomAccessCollection.swift index 88905b178..4a119be33 100644 --- a/Sources/_CollectionsTestSupport/MinimalTypes/MinimalRangeReplaceableRandomAccessCollection.swift +++ b/Tests/_CollectionsTestSupport/MinimalTypes/MinimalRangeReplaceableRandomAccessCollection.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2014 - 2024 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 diff --git a/Sources/_CollectionsTestSupport/MinimalTypes/MinimalSequence.swift b/Tests/_CollectionsTestSupport/MinimalTypes/MinimalSequence.swift similarity index 97% rename from Sources/_CollectionsTestSupport/MinimalTypes/MinimalSequence.swift rename to Tests/_CollectionsTestSupport/MinimalTypes/MinimalSequence.swift index 98c0f3eaf..66f6924b3 100644 --- a/Sources/_CollectionsTestSupport/MinimalTypes/MinimalSequence.swift +++ b/Tests/_CollectionsTestSupport/MinimalTypes/MinimalSequence.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2014 - 2024 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 diff --git a/Sources/_CollectionsTestSupport/MinimalTypes/ResettableValue.swift b/Tests/_CollectionsTestSupport/MinimalTypes/ResettableValue.swift similarity index 94% rename from Sources/_CollectionsTestSupport/MinimalTypes/ResettableValue.swift rename to Tests/_CollectionsTestSupport/MinimalTypes/ResettableValue.swift index aebb0c968..916dee7c0 100644 --- a/Sources/_CollectionsTestSupport/MinimalTypes/ResettableValue.swift +++ b/Tests/_CollectionsTestSupport/MinimalTypes/ResettableValue.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2014 - 2024 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 diff --git a/Sources/_CollectionsTestSupport/MinimalTypes/_CollectionState.swift b/Tests/_CollectionsTestSupport/MinimalTypes/_CollectionState.swift similarity index 99% rename from Sources/_CollectionsTestSupport/MinimalTypes/_CollectionState.swift rename to Tests/_CollectionsTestSupport/MinimalTypes/_CollectionState.swift index fcb05959e..40466337f 100644 --- a/Sources/_CollectionsTestSupport/MinimalTypes/_CollectionState.swift +++ b/Tests/_CollectionsTestSupport/MinimalTypes/_CollectionState.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2014 - 2024 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 diff --git a/Sources/_CollectionsTestSupport/MinimalTypes/_MinimalCollectionCore.swift b/Tests/_CollectionsTestSupport/MinimalTypes/_MinimalCollectionCore.swift similarity index 99% rename from Sources/_CollectionsTestSupport/MinimalTypes/_MinimalCollectionCore.swift rename to Tests/_CollectionsTestSupport/MinimalTypes/_MinimalCollectionCore.swift index 4bdc7c639..6e6d86668 100644 --- a/Sources/_CollectionsTestSupport/MinimalTypes/_MinimalCollectionCore.swift +++ b/Tests/_CollectionsTestSupport/MinimalTypes/_MinimalCollectionCore.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2014 - 2024 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 diff --git a/Tests/_CollectionsTestSupport/Utilities/AllOnesRandomNumberGenerator.swift b/Tests/_CollectionsTestSupport/Utilities/AllOnesRandomNumberGenerator.swift new file mode 100644 index 000000000..f54ff7b92 --- /dev/null +++ b/Tests/_CollectionsTestSupport/Utilities/AllOnesRandomNumberGenerator.swift @@ -0,0 +1,21 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +/// A terrible random number generator that always returns values with all +/// bits set to true. +public struct AllOnesRandomNumberGenerator: RandomNumberGenerator { + + public init() {} + + public mutating func next() -> UInt64 { + UInt64.max + } +} diff --git a/Sources/_CollectionsTestSupport/Utilities/Box.swift b/Tests/_CollectionsTestSupport/Utilities/Box.swift similarity index 89% rename from Sources/_CollectionsTestSupport/Utilities/Box.swift rename to Tests/_CollectionsTestSupport/Utilities/Box.swift index 15cae71fd..3141f17c6 100644 --- a/Sources/_CollectionsTestSupport/Utilities/Box.swift +++ b/Tests/_CollectionsTestSupport/Utilities/Box.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2014 - 2024 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 diff --git a/Tests/_CollectionsTestSupport/Utilities/DictionaryAPIChecker.swift b/Tests/_CollectionsTestSupport/Utilities/DictionaryAPIChecker.swift new file mode 100644 index 000000000..9d6b4d194 --- /dev/null +++ b/Tests/_CollectionsTestSupport/Utilities/DictionaryAPIChecker.swift @@ -0,0 +1,147 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +/// A test protocol for validating that dictionary-like types implement users' +/// baseline expectations. +/// +/// To ensure maximum utility, this protocol doesn't refine `Collection`, +/// although it does share some of the same requirements. +public protocol DictionaryAPIChecker { + associatedtype Key + associatedtype Value + associatedtype Index + + typealias Element = (key: Key, value: Value) + + associatedtype Keys: Collection + where Keys.Element == Key, Keys.Index == Index + + var keys: Keys { get } + + // `Values` ought to be a `MutableCollection` when possible, with `values` + // providing a setter. Unfortunately, tree-based dictionaries need to + // invalidate indices when mutating keys. + associatedtype Values: Collection + where Values.Element == Value, Values.Index == Index + + var values: Values { get } + + var isEmpty: Bool { get } + var count: Int { get } + + func index(forKey key: Key) -> Index? + + subscript(key: Key) -> Value? { get set } + subscript( + key: Key, + default defaultValue: @autoclosure () -> Value + ) -> Value { get set } + + mutating func updateValue(_ value: Value, forKey key: Key) -> Value? + mutating func removeValue(forKey key: Key) -> Value? + mutating func remove(at index: Index) -> Element + + init() + + init( + _ keysAndValues: S, + uniquingKeysWith combine: (Value, Value) throws -> Value + ) rethrows where S.Element == (Key, Value) + + mutating func merge( + _ keysAndValues: __owned S, + uniquingKeysWith combine: (Value, Value) throws -> Value + ) rethrows where S.Element == (Key, Value) + + __consuming func merging( + _ other: __owned S, + uniquingKeysWith combine: (Value, Value) throws -> Value + ) rethrows -> Self where S.Element == (Key, Value) + + func filter( + _ isIncluded: (Element) throws -> Bool + ) rethrows -> Self + +#if false + // We can't express these as protocol requirements: + func mapValues( + _ transform: (Value) throws -> T + ) rethrows -> Self + + func compactMapValues( + _ transform: (Value) throws -> T? + ) rethrows -> Self + + init( + grouping values: S, + by keyForValue: (S.Element) throws -> Key + ) rethrows where Value: RangeReplaceableCollection, Value.Element == S.Element + + public init( + grouping values: S, + by keyForValue: (S.Element) throws -> Key + ) rethrows where Value == [S.Element] +#endif +} + +extension Dictionary: DictionaryAPIChecker {} + +/// Additional entry points provided by this package that aren't provided +/// by `Dictionary` (yet?). +public protocol DictionaryAPIExtras: DictionaryAPIChecker { + // Extras (not in the Standard Library) + + init( + uniqueKeysWithValues keysAndValues: S + ) where S.Element == (Key, Value) + + init( + uniqueKeysWithValues keysAndValues: S + ) where S.Element == Element + +// init( +// uniqueKeys keys: Keys, +// values: Values +// ) where Keys.Element == Key, Values.Element == Value + + init( + _ keysAndValues: S, + uniquingKeysWith combine: (Value, Value) throws -> Value + ) rethrows where S.Element == Element + + mutating func merge( + _ keysAndValues: __owned S, + uniquingKeysWith combine: (Value, Value) throws -> Value + ) rethrows where S.Element == Element + + __consuming func merging( + _ other: __owned S, + uniquingKeysWith combine: (Value, Value) throws -> Value + ) rethrows -> Self where S.Element == Element + + mutating func updateValue( + forKey key: Key, + default defaultValue: @autoclosure () -> Value, + with body: (inout Value) throws -> R + ) rethrows -> R + +#if false + // Potential additions implemented by TreeDictionary: + + func contains(_ key: Key) -> Bool + + mutating func updateValue( + forKey key: Key, + with body: (inout Value?) throws -> R + ) rethrows -> R + +#endif +} diff --git a/Sources/_CollectionsTestSupport/Utilities/HashableBox.swift b/Tests/_CollectionsTestSupport/Utilities/HashableBox.swift similarity index 92% rename from Sources/_CollectionsTestSupport/Utilities/HashableBox.swift rename to Tests/_CollectionsTestSupport/Utilities/HashableBox.swift index ed2388e55..d7ebf2e43 100644 --- a/Sources/_CollectionsTestSupport/Utilities/HashableBox.swift +++ b/Tests/_CollectionsTestSupport/Utilities/HashableBox.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2014 - 2024 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 diff --git a/Tests/_CollectionsTestSupport/Utilities/IndexRangeCollection.swift b/Tests/_CollectionsTestSupport/Utilities/IndexRangeCollection.swift new file mode 100644 index 000000000..3d96dfca7 --- /dev/null +++ b/Tests/_CollectionsTestSupport/Utilities/IndexRangeCollection.swift @@ -0,0 +1,90 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +public struct IndexRangeCollection +where Bound.Stride == Int +{ + var _bounds: Range + + public init(bounds: Range) { + self._bounds = bounds + } +} + +extension IndexRangeCollection: RandomAccessCollection { + public typealias Element = Range + public typealias Iterator = IndexingIterator + public typealias SubSequence = Slice + public typealias Indices = DefaultIndices + + public struct Index: Comparable { + var _start: Int + var _end: Int + + internal init(_start: Int, end: Int) { + assert(_start >= 0 && _start <= end) + self._start = _start + self._end = end + } + + internal init(_offset: Int) { + assert(_offset >= 0) + let end = ((8 * _offset + 1)._squareRoot() - 1) / 2 + let base = end * (end + 1) / 2 + self._start = _offset - base + self._end = end + } + + internal var _offset: Int { + return _end * (_end + 1) / 2 + _start + } + + public static func ==(left: Self, right: Self) -> Bool { + left._start == right._start && left._end == right._end + } + + public static func <(left: Self, right: Self) -> Bool { + (left._end, left._start) < (right._end, right._start) + } + } + + public var count: Int { (_bounds.count + 1) * (_bounds.count + 2) / 2 } + + public var isEmpty: Bool { false } + public var startIndex: Index { Index(_start: 0, end: 0) } + public var endIndex: Index { Index(_start: 0, end: _bounds.count + 1) } + + public func index(after i: Index) -> Index { + guard i._start < i._end else { + return Index(_start: 0, end: i._end + 1) + } + return Index(_start: i._start + 1, end: i._end) + } + + public func index(before i: Index) -> Index { + guard i._start > 0 else { + return Index(_start: i._end - 1, end: i._end - 1) + } + return Index(_start: i._start - 1, end: i._end) + } + + public func index(_ i: Index, offsetBy distance: Int) -> Index { + Index(_offset: i._offset + distance) + } + + public subscript(position: Index) -> Range { + precondition(position._end <= _bounds.count) + return Range( + uncheckedBounds: ( + lower: _bounds.lowerBound.advanced(by: position._start), + upper: _bounds.lowerBound.advanced(by: position._end))) + } +} diff --git a/Tests/_CollectionsTestSupport/Utilities/Integer Square Root.swift b/Tests/_CollectionsTestSupport/Utilities/Integer Square Root.swift new file mode 100644 index 000000000..0acbbd66d --- /dev/null +++ b/Tests/_CollectionsTestSupport/Utilities/Integer Square Root.swift @@ -0,0 +1,26 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 FixedWidthInteger { + internal func _squareRoot() -> Self { + // Newton's method + precondition(self >= 0) + guard self != 0 else { return 0 } + var x: Self = 1 &<< ((self.bitWidth + 1) / 2) + var y: Self = 0 + while true { + y = (self / x + x) &>> 1 + if x == y || x == y - 1 { break } + x = y + } + return x + } +} diff --git a/Sources/_CollectionsTestSupport/Utilities/LifetimeTracked.swift b/Tests/_CollectionsTestSupport/Utilities/LifetimeTracked.swift similarity index 91% rename from Sources/_CollectionsTestSupport/Utilities/LifetimeTracked.swift rename to Tests/_CollectionsTestSupport/Utilities/LifetimeTracked.swift index a53b57951..b17dbef8e 100644 --- a/Sources/_CollectionsTestSupport/Utilities/LifetimeTracked.swift +++ b/Tests/_CollectionsTestSupport/Utilities/LifetimeTracked.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2014 - 2024 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 @@ -59,9 +59,17 @@ extension LifetimeTracked: Equatable where Payload: Equatable { } extension LifetimeTracked: Hashable where Payload: Hashable { + public func _rawHashValue(seed: Int) -> Int { + payload._rawHashValue(seed: seed) + } + public func hash(into hasher: inout Hasher) { hasher.combine(payload) } + + public var hashValue: Int { + payload.hashValue + } } extension LifetimeTracked: Comparable where Payload: Comparable { diff --git a/Sources/_CollectionsTestSupport/Utilities/LifetimeTracker.swift b/Tests/_CollectionsTestSupport/Utilities/LifetimeTracker.swift similarity index 88% rename from Sources/_CollectionsTestSupport/Utilities/LifetimeTracker.swift rename to Tests/_CollectionsTestSupport/Utilities/LifetimeTracker.swift index e920a6756..1728cff69 100644 --- a/Sources/_CollectionsTestSupport/Utilities/LifetimeTracker.swift +++ b/Tests/_CollectionsTestSupport/Utilities/LifetimeTracker.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2014 - 2024 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 @@ -40,6 +40,12 @@ public class LifetimeTracker { public func instances(for items: S) -> [LifetimeTracked] { return items.map { LifetimeTracked($0, for: self) } } + + public func instances( + for items: S, by transform: (S.Element) -> T + ) -> [LifetimeTracked] { + items.map { instance(for: transform($0)) } + } } @inlinable diff --git a/Tests/_CollectionsTestSupport/Utilities/RandomStableSample.swift b/Tests/_CollectionsTestSupport/Utilities/RandomStableSample.swift new file mode 100644 index 000000000..6b1746781 --- /dev/null +++ b/Tests/_CollectionsTestSupport/Utilities/RandomStableSample.swift @@ -0,0 +1,69 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 - 2024 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 +// +//===----------------------------------------------------------------------===// + +// Note: This comes from swift-algorithms. + +extension Collection { + /// Randomly selects the specified number of elements from this collection, + /// maintaining their relative order. + /// + /// - Parameters: + /// - k: The number of elements to randomly select. + /// - rng: The random number generator to use for the sampling. + /// - Returns: An array of `k` random elements. If `k` is greater than this + /// collection's count, then this method returns the full collection. + /// + /// - Complexity: O(*n*), where *n* is the length of the collection. + @inlinable + public func randomStableSample( + count k: Int, using rng: inout G + ) -> [Element] { + guard k > 0 else { return [] } + + var remainingCount = count + guard k < remainingCount else { return Array(self) } + + var result: [Element] = [] + result.reserveCapacity(k) + + var i = startIndex + var countToSelect = k + while countToSelect > 0 { + let r = Int.random(in: 0.. [Element] { + var g = SystemRandomNumberGenerator() + return randomStableSample(count: k, using: &g) + } +} diff --git a/Sources/_CollectionsTestSupport/Utilities/SeedableRandomNumberGenerator.swift b/Tests/_CollectionsTestSupport/Utilities/RepeatableRandomNumberGenerator.swift similarity index 80% rename from Sources/_CollectionsTestSupport/Utilities/SeedableRandomNumberGenerator.swift rename to Tests/_CollectionsTestSupport/Utilities/RepeatableRandomNumberGenerator.swift index f09b89ebc..ccba75d67 100644 --- a/Sources/_CollectionsTestSupport/Utilities/SeedableRandomNumberGenerator.swift +++ b/Tests/_CollectionsTestSupport/Utilities/RepeatableRandomNumberGenerator.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2021 - 2024 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 @@ -10,6 +10,14 @@ //===----------------------------------------------------------------------===// public struct RepeatableRandomNumberGenerator: RandomNumberGenerator { + public static var globalSeed: Int { +#if COLLECTIONS_RANDOMIZED_TESTING + 42.hashValue +#else + 0 +#endif + } + // This uses the same linear congruential generator as rand48. // FIXME: Replace with something better. internal static let _m: UInt64 = 1 << 48 @@ -26,7 +34,8 @@ public struct RepeatableRandomNumberGenerator: RandomNumberGenerator { // Perturb the seed a little so that the sequence doesn't start with a // zero value in the common case of seed == 0. (Using a zero seed is a // rather silly thing to do, but it's the easy thing.) - _state = seed ^ 0x536f52616e646f6d // "SoRandom" + let global = UInt64(truncatingIfNeeded: Self.globalSeed) + _state = seed ^ global ^ 0x536f52616e646f6d // "SoRandom" } private mutating func _next() -> UInt64 { diff --git a/Tests/_CollectionsTestSupport/Utilities/SetAPIChecker.swift b/Tests/_CollectionsTestSupport/Utilities/SetAPIChecker.swift new file mode 100644 index 000000000..0514c44d5 --- /dev/null +++ b/Tests/_CollectionsTestSupport/Utilities/SetAPIChecker.swift @@ -0,0 +1,81 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +/// A test protocol for validating that set-like types implement users' +/// baseline expectations. +/// +/// To ensure maximum utility, this protocol refines neither `Collection` nor +/// `SetAlgebra` although it does share some of the same requirements. +public protocol SetAPIChecker { + associatedtype Element + associatedtype Index + + var isEmpty: Bool { get } + var count: Int { get } + + init() + + mutating func remove(at index: Index) -> Element + + func filter(_ isIncluded: (Element) throws -> Bool) rethrows -> Self + + func isSubset(of other: S) -> Bool + where S.Element == Element + + func isSuperset(of other: S) -> Bool + where S.Element == Element + + func isStrictSubset(of other: S) -> Bool + where S.Element == Element + + func isStrictSuperset(of other: S) -> Bool + where S.Element == Element + + func isDisjoint(with other: S) -> Bool + where S.Element == Element + + + func intersection(_ other: S) -> Self + where S.Element == Element + + func union(_ other: __owned S) -> Self + where S.Element == Element + + __consuming func subtracting(_ other: S) -> Self + where S.Element == Element + + func symmetricDifference(_ other: __owned S) -> Self + where S.Element == Element + + mutating func formIntersection(_ other: S) + where S.Element == Element + + mutating func formUnion(_ other: __owned S) + where S.Element == Element + + mutating func subtract(_ other: S) + where S.Element == Element + + mutating func formSymmetricDifference(_ other: __owned S) + where S.Element == Element +} + +extension Set: SetAPIChecker {} + +public protocol SetAPIExtras: SetAPIChecker { + // Non-standard extensions + + mutating func update(_ member: Element, at index: Index) -> Element + + func isEqualSet(to other: Self) -> Bool + func isEqualSet(to other: S) -> Bool + where S.Element == Element +} diff --git a/Tests/_CollectionsTestSupport/Utilities/SortedCollectionAPIChecker.swift b/Tests/_CollectionsTestSupport/Utilities/SortedCollectionAPIChecker.swift new file mode 100644 index 000000000..f13a3db6c --- /dev/null +++ b/Tests/_CollectionsTestSupport/Utilities/SortedCollectionAPIChecker.swift @@ -0,0 +1,33 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2022 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if COLLECTIONS_SINGLE_MODULE +import Collections +#else +import _CollectionsUtilities +#endif + +/// This protocol simply lists Collection/Sequence extensions that ought to be +/// customized for sorted collections. +/// +/// Conforming to this protocol admittedly doesn't do much, as the default +/// implementations already exist for most of these requirements +/// (but they aren't doing the right thing). +public protocol SortedCollectionAPIChecker: Collection, _SortedCollection +where Element: Comparable { + // This one actually does not come with a default implementation. + func sorted() -> Self + + // These are also defined on `Sequence`, but the default implementation does + // a linear search, which isn't what we want. + func min() -> Element? + func max() -> Element? +} diff --git a/Sources/_CollectionsTestSupport/MinimalTypes/StringConvertibleValue.swift b/Tests/_CollectionsTestSupport/Utilities/StringConvertibleValue.swift similarity index 93% rename from Sources/_CollectionsTestSupport/MinimalTypes/StringConvertibleValue.swift rename to Tests/_CollectionsTestSupport/Utilities/StringConvertibleValue.swift index b4e524bec..7d35c1929 100644 --- a/Sources/_CollectionsTestSupport/MinimalTypes/StringConvertibleValue.swift +++ b/Tests/_CollectionsTestSupport/Utilities/StringConvertibleValue.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors +// Copyright (c) 2014 - 2024 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 diff --git a/Utils/README.md b/Utils/README.md new file mode 100644 index 000000000..58bce3bdc --- /dev/null +++ b/Utils/README.md @@ -0,0 +1,17 @@ +# Maintenance Scripts + +This directory contains scripts that are used to maintain this package. + +Beware! The contents of this directory are not source stable. They are provided as is, with no compatibility promises across package releases. Future versions of this package can arbitrarily change these files or remove them, without any advance notice. (This can include patch releases.) + +- `generate-docs.sh`: A shell scripts that automates the generation of API documentation. + +- `generate-sources.sh`: A shell script that invokes `gyb` to regenerate the contents of autogenerated/ directories. This needs to be run whenever a file with a `.swift.gyb` extension is updated. + +- `gyb`, `gyb.py`: Generate Your Boilerplate. A rudimentary source code generation utility. + +- `gyb_utils.py`: A Python module containing code generation utility definitions that are shared across multiple `.swift.gyb` files in this repository. + +- `run-full-tests.sh`: A shell script that exercises many common configurations of this package in a semi-automated way. This is used before tagging a release to avoid accidentally shipping a package version that breaks some setups. + +- `shuffle-sources.sh`: A legacy utility that randomly reorders Swift source files in a given directory. This is used to avoid reoccurrances of issue #7. (This is hopefully only relevant with compilers that the package no longer supports.) diff --git a/Utils/generate-docs.sh b/Utils/generate-docs.sh index 86506af13..f525f9b39 100755 --- a/Utils/generate-docs.sh +++ b/Utils/generate-docs.sh @@ -3,7 +3,7 @@ # # This source file is part of the Swift Collections open source project # -# Copyright (c) 2022 Apple Inc. and the Swift project authors +# Copyright (c) 2022 - 2024 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 @@ -29,7 +29,9 @@ output_dir_base=/tmp/foo rm -rf "$output_dir_base" -components="DequeModule OrderedCollections" +components="\ + BitCollections DequeModule HeapModule OrderedCollections \ + HashTreeCollections" if [ $# -eq 0 ]; then targets="Collections $components" diff --git a/Utils/generate-sources.sh b/Utils/generate-sources.sh new file mode 100755 index 000000000..1ed52f696 --- /dev/null +++ b/Utils/generate-sources.sh @@ -0,0 +1,62 @@ +#!/bin/sh +#===----------------------------------------------------------------------===// +# +# This source file is part of the Swift.org open source project +# +# Copyright (c) 2020 - 2024 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 +# +#===----------------------------------------------------------------------===// + +set -eu + +srcroot="$(dirname "$0")/.." +cd "$srcroot" + +gyb="./Utils/gyb" + +# Disable line directives in gyb output. We commit generated sources +# into the package repository, so we do not want absolute file names +# in them. +lineDirective='' + +# Uncomment the following line to enable #sourceLocation directives. +# This is useful for local development. +#lineDirective='#sourceLocation(file: "%(file)s", line: %(line)d)' + + +# Create a temporary directory; remove it on exit. +tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/$(basename "$0").XXXXXXXX")" +trap "rm -rf \"$tmpdir\"" EXIT + +# Run gyb on each gyb file in the source tree and put results in +# subdirectories named 'autogenerated'. +find ./Sources ./Tests -name "*.gyb" | while read input; do + basename="$(basename "$input")" + targetdir="$(dirname "$input")/autogenerated" + output="$targetdir/"${basename%.gyb} + tmpfile="$tmpdir/${basename%.gyb}" + + # Make sure the output directory exists. + mkdir -p "$targetdir" + + # Run gyb, making sure to only update files when they change. + "$gyb" --line-directive "$lineDirective" -o "$tmpfile" "$input" + if [ -e "$output" ] && cmp -s "$tmpfile" "$output"; then + : Ignore unchanged file + else + echo "Updated $output" + cp "$tmpfile" "$output" + fi + echo "$output" >> "$tmpdir/generated-files.txt" +done + +# Remove autogenerated files without a corresponding gyb. +find . -path '*/autogenerated/*.swift' >> "$tmpdir/generated-files.txt" +sort "$tmpdir/generated-files.txt" | uniq -u | while read obsolete; do + echo "Removing $obsolete" + rm "$obsolete" +done diff --git a/Utils/gyb b/Utils/gyb new file mode 100755 index 000000000..8206bd8a0 --- /dev/null +++ b/Utils/gyb @@ -0,0 +1,3 @@ +#!/usr/bin/env python3 +import gyb +gyb.main() diff --git a/Utils/gyb.py b/Utils/gyb.py new file mode 100755 index 000000000..e3af0b7d8 --- /dev/null +++ b/Utils/gyb.py @@ -0,0 +1,1247 @@ +#!/usr/bin/env python +# GYB: Generate Your Boilerplate (improved names welcome; at least +# this one's short). See -h output for instructions + +import os +import re +import sys +import textwrap +import tokenize +from bisect import bisect +from io import StringIO + + +def get_line_starts(s): + """Return a list containing the start index of each line in s. + + The list also contains a sentinel index for the end of the string, + so there will be one more element in the list than there are lines + in the string + """ + starts = [0] + + for line in s.split('\n'): + starts.append(starts[-1] + len(line) + 1) + + starts[-1] -= 1 + return starts + + +def strip_trailing_nl(s): + """If s ends with a newline, drop it; else return s intact""" + return s[:-1] if s.endswith('\n') else s + + +def split_lines(s): + """Split s into a list of lines, each of which has a trailing newline + + If the lines are later concatenated, the result is s, possibly + with a single appended newline. + """ + return [l + '\n' for l in s.split('\n')] + + +# text on a line up to the first '$$', '${', or '%%' +literalText = r'(?: [^$\n%] | \$(?![${]) | %(?!%) )*' + +# The part of an '%end' line that follows the '%' sign +linesClose = r'[\ \t]* end [\ \t]* (?: \# .* )? $' + +# Note: Where "# Absorb" appears below, the regexp attempts to eat up +# through the end of ${...} and %{...}% constructs. In reality we +# handle this with the Python tokenizer, which avoids mis-detections +# due to nesting, comments and strings. This extra absorption in the +# regexp facilitates testing the regexp on its own, by preventing the +# interior of some of these constructs from being treated as literal +# text. +tokenize_re = re.compile( + r''' +# %-lines and %{...}-blocks + # \n? # absorb one preceding newline + ^ + (?: + (?P + (?P<_indent> [\ \t]* % (?! [{%] ) [\ \t]* ) (?! [\ \t] | ''' + + linesClose + r''' ) .* + ( \n (?P=_indent) (?! ''' + linesClose + r''' ) .* ) * + ) + | (?P [\ \t]* % [ \t]* ''' + linesClose + r''' ) + | [\ \t]* (?P %\{ ) + (?: [^}]| \} (?!%) )* \}% # Absorb + ) + \n? # absorb one trailing newline + +# Substitutions +| (?P \$\{ ) + [^}]* \} # Absorb + +# %% and $$ are literal % and $ respectively +| (?P[$%]) (?P=symbol) + +# Literal text +| (?P ''' + literalText + r''' + (?: + # newline that doesn't precede space+% + (?: \n (?! [\ \t]* %[^%] ) ) + ''' + literalText + r''' + )* + \n? + ) +''', re.VERBOSE | re.MULTILINE) + +gyb_block_close = re.compile(r'\}%[ \t]*\n?') + + +def token_pos_to_index(token_pos, start, line_starts): + """Translate a tokenize (line, column) pair into an absolute + position in source text given the position where we started + tokenizing and a list that maps lines onto their starting + character indexes. + """ + relative_token_line_plus1, token_col = token_pos + + # line number where we started tokenizing + start_line_num = bisect(line_starts, start) - 1 + + # line number of the token in the whole text + abs_token_line = relative_token_line_plus1 - 1 + start_line_num + + # if found in the first line, adjust the end column to account + # for the extra text + if relative_token_line_plus1 == 1: + token_col += start - line_starts[start_line_num] + + # Sometimes tokenizer errors report a line beyond the last one + if abs_token_line >= len(line_starts): + return line_starts[-1] + + return line_starts[abs_token_line] + token_col + + +def tokenize_python_to_unmatched_close_curly(source_text, start, line_starts): + """Apply Python's tokenize to source_text starting at index start + while matching open and close curly braces. When an unmatched + close curly brace is found, return its index. If not found, + return len(source_text). If there's a tokenization error, return + the position of the error. + """ + stream = StringIO(source_text) + stream.seek(start) + nesting = 0 + + try: + for kind, text, token_start, token_end, line_text \ + in tokenize.generate_tokens(stream.readline): + + if text == '{': + nesting += 1 + elif text == '}': + nesting -= 1 + if nesting < 0: + return token_pos_to_index(token_start, start, line_starts) + + except tokenize.TokenError as error: + (message, error_pos) = error.args + return token_pos_to_index(error_pos, start, line_starts) + + return len(source_text) + + +def tokenize_template(template_text): + r"""Given the text of a template, returns an iterator over + (tokenType, token, match) tuples. + + **Note**: this is template syntax tokenization, not Python + tokenization. + + When a non-literal token is matched, a client may call + iter.send(pos) on the iterator to reset the position in + template_text at which scanning will resume. + + This function provides a base level of tokenization which is + then refined by ParseContext.token_generator. + + >>> from pprint import * + >>> pprint(list((kind, text) for kind, text, _ in tokenize_template( + ... '%for x in range(10):\n% print x\n%end\njuicebox'))) + [('gybLines', '%for x in range(10):\n% print x'), + ('gybLinesClose', '%end'), + ('literal', 'juicebox')] + + >>> pprint(list((kind, text) for kind, text, _ in tokenize_template( + ... '''Nothing + ... % if x: + ... % for i in range(3): + ... ${i} + ... % end + ... % else: + ... THIS SHOULD NOT APPEAR IN THE OUTPUT + ... '''))) + [('literal', 'Nothing\n'), + ('gybLines', '% if x:\n% for i in range(3):'), + ('substitutionOpen', '${'), + ('literal', '\n'), + ('gybLinesClose', '% end'), + ('gybLines', '% else:'), + ('literal', 'THIS SHOULD NOT APPEAR IN THE OUTPUT\n')] + + >>> for kind, text, _ in tokenize_template(''' + ... This is $some$ literal stuff containing a ${substitution} + ... followed by a %{...} block: + ... %{ + ... # Python code + ... }% + ... and here $${are} some %-lines: + ... % x = 1 + ... % y = 2 + ... % if z == 3: + ... % print '${hello}' + ... % end + ... % for x in zz: + ... % print x + ... % # different indentation + ... % twice + ... and some lines that literally start with a %% token + ... %% first line + ... %% second line + ... '''): + ... print((kind, text.strip().split('\n',1)[0])) + ('literal', 'This is $some$ literal stuff containing a') + ('substitutionOpen', '${') + ('literal', 'followed by a %{...} block:') + ('gybBlockOpen', '%{') + ('literal', 'and here ${are} some %-lines:') + ('gybLines', '% x = 1') + ('gybLinesClose', '% end') + ('gybLines', '% for x in zz:') + ('gybLines', '% # different indentation') + ('gybLines', '% twice') + ('literal', 'and some lines that literally start with a % token') + """ + pos = 0 + end = len(template_text) + + saved_literal = [] + literal_first_match = None + + while pos < end: + m = tokenize_re.match(template_text, pos, end) + + # pull out the one matched key (ignoring internal patterns starting + # with _) + ((kind, text), ) = ( + (kind, text) for (kind, text) in list(m.groupdict().items()) + if text is not None and kind[0] != '_') + + if kind in ('literal', 'symbol'): + if len(saved_literal) == 0: + literal_first_match = m + # literals and symbols get batched together + saved_literal.append(text) + pos = None + else: + # found a non-literal. First yield any literal we've accumulated + if saved_literal != []: + yield 'literal', ''.join(saved_literal), literal_first_match + saved_literal = [] + + # Then yield the thing we found. If we get a reply, it's + # the place to resume tokenizing + pos = yield kind, text, m + + # If we were not sent a new position by our client, resume + # tokenizing at the end of this match. + if pos is None: + pos = m.end(0) + else: + # Client is not yet ready to process next token + yield + + if saved_literal != []: + yield 'literal', ''.join(saved_literal), literal_first_match + + +def split_gyb_lines(source_lines): + r"""Return a list of lines at which to split the incoming source + + These positions represent the beginnings of python line groups that + will require a matching %end construct if they are to be closed. + + >>> src = split_lines('''\ + ... if x: + ... print x + ... if y: # trailing comment + ... print z + ... if z: # another comment\ + ... ''') + >>> s = split_gyb_lines(src) + >>> len(s) + 2 + >>> src[s[0]] + ' print z\n' + >>> s[1] - len(src) + 0 + + >>> src = split_lines('''\ + ... if x: + ... if y: print 1 + ... if z: + ... print 2 + ... pass\ + ... ''') + >>> s = split_gyb_lines(src) + >>> len(s) + 1 + >>> src[s[0]] + ' if y: print 1\n' + + >>> src = split_lines('''\ + ... if x: + ... if y: + ... print 1 + ... print 2 + ... ''') + >>> s = split_gyb_lines(src) + >>> len(s) + 2 + >>> src[s[0]] + ' if y:\n' + >>> src[s[1]] + ' print 1\n' + """ + last_token_text, last_token_kind = None, None + unmatched_indents = [] + + dedents = 0 + try: + for token_kind, token_text, token_start, \ + (token_end_line, token_end_col), line_text \ + in tokenize.generate_tokens(lambda i=iter(source_lines): + next(i)): + + if token_kind in (tokenize.COMMENT, tokenize.ENDMARKER): + continue + + if token_text == '\n' and last_token_text == ':': + unmatched_indents.append(token_end_line) + + # The tokenizer appends dedents at EOF; don't consider + # those as matching indentations. Instead just save them + # up... + if last_token_kind == tokenize.DEDENT: + dedents += 1 + # And count them later, when we see something real. + if token_kind != tokenize.DEDENT and dedents > 0: + unmatched_indents = unmatched_indents[:-dedents] + dedents = 0 + + last_token_text, last_token_kind = token_text, token_kind + + except tokenize.TokenError: + # Let the later compile() call report the error + return [] + + if last_token_text == ':': + unmatched_indents.append(len(source_lines)) + + return unmatched_indents + + +def code_starts_with_dedent_keyword(source_lines): + r"""Return True iff the incoming Python source_lines begin with "else", + "elif", "except", or "finally". + + Initial comments and whitespace are ignored. + + >>> code_starts_with_dedent_keyword(split_lines('if x in y: pass')) + False + >>> code_starts_with_dedent_keyword(split_lines('except ifSomethingElse:')) + True + >>> code_starts_with_dedent_keyword( + ... split_lines('\n# comment\nelse: # yes')) + True + """ + token_text = None + for token_kind, token_text, _, _, _ \ + in tokenize.generate_tokens(lambda i=iter(source_lines): next(i)): + + if token_kind != tokenize.COMMENT and token_text.strip() != '': + break + + return token_text in ('else', 'elif', 'except', 'finally') + + +class ParseContext(object): + + """State carried through a parse of a template""" + + filename = '' + template = '' + line_starts = [] + code_start_line = -1 + code_text = None + tokens = None # The rest of the tokens + close_lines = False + + def __init__(self, filename, template=None): + self.filename = os.path.abspath(filename) + if sys.platform == 'win32': + self.filename = self.filename.replace('\\', '/') + if template is None: + with open(filename) as f: + self.template = f.read() + else: + self.template = template + self.line_starts = get_line_starts(self.template) + self.tokens = self.token_generator(tokenize_template(self.template)) + self.next_token() + + def pos_to_line(self, pos): + return bisect(self.line_starts, pos) - 1 + + def token_generator(self, base_tokens): + r"""Given an iterator over (kind, text, match) triples (see + tokenize_template above), return a refined iterator over + token_kinds. + + Among other adjustments to the elements found by base_tokens, + this refined iterator tokenizes python code embedded in + template text to help determine its true extent. The + expression "base_tokens.send(pos)" is used to reset the index at + which base_tokens resumes scanning the underlying text. + + >>> ctx = ParseContext('dummy', ''' + ... %for x in y: + ... % print x + ... % end + ... literally + ... ''') + >>> while ctx.token_kind: + ... print((ctx.token_kind, ctx.code_text or ctx.token_text)) + ... ignored = ctx.next_token() + ('literal', '\n') + ('gybLinesOpen', 'for x in y:\n') + ('gybLines', ' print x\n') + ('gybLinesClose', '% end') + ('literal', 'literally\n') + + >>> ctx = ParseContext('dummy', + ... '''Nothing + ... % if x: + ... % for i in range(3): + ... ${i} + ... % end + ... % else: + ... THIS SHOULD NOT APPEAR IN THE OUTPUT + ... ''') + >>> while ctx.token_kind: + ... print((ctx.token_kind, ctx.code_text or ctx.token_text)) + ... ignored = ctx.next_token() + ('literal', 'Nothing\n') + ('gybLinesOpen', 'if x:\n') + ('gybLinesOpen', ' for i in range(3):\n') + ('substitutionOpen', 'i') + ('literal', '\n') + ('gybLinesClose', '% end') + ('gybLinesOpen', 'else:\n') + ('literal', 'THIS SHOULD NOT APPEAR IN THE OUTPUT\n') + + >>> ctx = ParseContext('dummy', + ... '''% for x in [1, 2, 3]: + ... % if x == 1: + ... literal1 + ... % elif x > 1: # add output line here to fix bug + ... % if x == 2: + ... literal2 + ... % end + ... % end + ... % end + ... ''') + >>> while ctx.token_kind: + ... print((ctx.token_kind, ctx.code_text or ctx.token_text)) + ... ignored = ctx.next_token() + ('gybLinesOpen', 'for x in [1, 2, 3]:\n') + ('gybLinesOpen', ' if x == 1:\n') + ('literal', 'literal1\n') + ('gybLinesOpen', 'elif x > 1: # add output line here to fix bug\n') + ('gybLinesOpen', ' if x == 2:\n') + ('literal', 'literal2\n') + ('gybLinesClose', '% end') + ('gybLinesClose', '% end') + ('gybLinesClose', '% end') + """ + for self.token_kind, self.token_text, self.token_match in base_tokens: + kind = self.token_kind + self.code_text = None + + # Do we need to close the current lines? + self.close_lines = kind == 'gybLinesClose' + + # %{...}% and ${...} constructs + if kind.endswith('Open'): + + # Tokenize text that follows as Python up to an unmatched '}' + code_start = self.token_match.end(kind) + self.code_start_line = self.pos_to_line(code_start) + + close_pos = tokenize_python_to_unmatched_close_curly( + self.template, code_start, self.line_starts) + self.code_text = self.template[code_start:close_pos] + yield kind + + if (kind == 'gybBlockOpen'): + # Absorb any '}% \n' + m2 = gyb_block_close.match(self.template, close_pos) + if not m2: + raise ValueError("Invalid block closure") + next_pos = m2.end(0) + else: + assert kind == 'substitutionOpen' + # skip past the closing '}' + next_pos = close_pos + 1 + + # Resume tokenizing after the end of the code. + base_tokens.send(next_pos) + + elif kind == 'gybLines': + + self.code_start_line = self.pos_to_line( + self.token_match.start('gybLines')) + indentation = self.token_match.group('_indent') + + # Strip off the leading indentation and %-sign + source_lines = re.split( + '^' + re.escape(indentation), + self.token_match.group('gybLines') + '\n', + flags=re.MULTILINE)[1:] + + if code_starts_with_dedent_keyword(source_lines): + self.close_lines = True + + last_split = 0 + for line in split_gyb_lines(source_lines): + self.token_kind = 'gybLinesOpen' + self.code_text = ''.join(source_lines[last_split:line]) + yield self.token_kind + last_split = line + self.code_start_line += line - last_split + self.close_lines = False + + self.code_text = ''.join(source_lines[last_split:]) + if self.code_text: + self.token_kind = 'gybLines' + yield self.token_kind + else: + yield self.token_kind + + def next_token(self): + """Move to the next token""" + for kind in self.tokens: + return self.token_kind + + self.token_kind = None + + +_default_line_directive = \ + '// ###sourceLocation(file: "%(file)s", line: %(line)d)' + + +class ExecutionContext(object): + + """State we pass around during execution of a template""" + + def __init__(self, line_directive=_default_line_directive, + **local_bindings): + self.local_bindings = local_bindings + self.line_directive = line_directive + self.local_bindings['__context__'] = self + self.result_text = [] + self.last_file_line = None + + def append_text(self, text, file, line): + # see if we need to inject a line marker + if self.line_directive: + if (file, line) != self.last_file_line: + # We can only insert the line directive at a line break + if len(self.result_text) == 0 \ + or self.result_text[-1].endswith('\n'): + substitutions = {'file': file, 'line': line + 1} + format_str = self.line_directive + '\n' + self.result_text.append(format_str % substitutions) + # But if the new text contains any line breaks, we can create + # one + elif '\n' in text: + i = text.find('\n') + self.result_text.append(text[:i + 1]) + # and try again + self.append_text(text[i + 1:], file, line + 1) + return + + self.result_text.append(text) + self.last_file_line = (file, line + text.count('\n')) + + +class ASTNode(object): + + """Abstract base class for template AST nodes""" + + def __init__(self): + raise NotImplementedError("ASTNode.__init__ is not implemented.") + + def execute(self, context): + raise NotImplementedError("ASTNode.execute is not implemented.") + + def __str__(self, indent=''): + raise NotImplementedError("ASTNode.__str__ is not implemented.") + + def format_children(self, indent): + if not self.children: + return ' []' + + return '\n'.join( + ['', indent + '['] + + [x.__str__(indent + 4 * ' ') for x in self.children] + + [indent + ']']) + + +class Block(ASTNode): + + """A sequence of other AST nodes, to be executed in order""" + + children = [] + + def __init__(self, context): + self.children = [] + + while context.token_kind and not context.close_lines: + if context.token_kind == 'literal': + node = Literal + else: + node = Code + self.children.append(node(context)) + + def execute(self, context): + for x in self.children: + x.execute(context) + + def __str__(self, indent=''): + return indent + 'Block:' + self.format_children(indent) + + +class Literal(ASTNode): + + """An AST node that generates literal text""" + + def __init__(self, context): + self.text = context.token_text + start_position = context.token_match.start(context.token_kind) + self.start_line_number = context.pos_to_line(start_position) + self.filename = context.filename + context.next_token() + + def execute(self, context): + context.append_text(self.text, self.filename, self.start_line_number) + + def __str__(self, indent=''): + return '\n'.join( + [indent + x for x in ['Literal:'] + + strip_trailing_nl(self.text).split('\n')]) + + +class Code(ASTNode): + + """An AST node that is evaluated as Python""" + + code = None + children = () + kind = None + + def __init__(self, context): + + source = '' + source_line_count = 0 + + def accumulate_code(): + s = source + (context.code_start_line - source_line_count) * '\n' \ + + textwrap.dedent(context.code_text) + line_count = context.code_start_line + \ + context.code_text.count('\n') + context.next_token() + return s, line_count + + eval_exec = 'exec' + if context.token_kind.startswith('substitution'): + eval_exec = 'eval' + source, source_line_count = accumulate_code() + source = '(' + source.strip() + ')' + + else: + while context.token_kind == 'gybLinesOpen': + source, source_line_count = accumulate_code() + source += ' __children__[%d].execute(__context__)\n' % len( + self.children) + source_line_count += 1 + + self.children += (Block(context),) + + if context.token_kind == 'gybLinesClose': + context.next_token() + + if context.token_kind == 'gybLines': + source, source_line_count = accumulate_code() + + # Only handle a substitution as part of this code block if + # we don't already have some %-lines. + elif context.token_kind == 'gybBlockOpen': + + # Opening ${...} and %{...}% constructs + source, source_line_count = accumulate_code() + + self.filename = context.filename + self.start_line_number = context.code_start_line + self.code = compile(source, context.filename, eval_exec) + self.source = source + + def execute(self, context): + # Save __children__ from the local bindings + save_children = context.local_bindings.get('__children__') + # Execute the code with our __children__ in scope + context.local_bindings['__children__'] = self.children + context.local_bindings['__file__'] = self.filename + result = eval(self.code, context.local_bindings) + + if context.local_bindings['__children__'] is not self.children: + raise ValueError("The code is not allowed to mutate __children__") + # Restore the bindings + context.local_bindings['__children__'] = save_children + + # If we got a result, the code was an expression, so append + # its value + if result is not None \ + or (isinstance(result, str) and result != ''): + from numbers import Number, Integral + result_string = None + if isinstance(result, Number) and not isinstance(result, Integral): + result_string = repr(result) + else: + result_string = str(result) + context.append_text( + result_string, self.filename, self.start_line_number) + + def __str__(self, indent=''): + source_lines = re.sub(r'^\n', '', strip_trailing_nl( + self.source), flags=re.MULTILINE).split('\n') + if len(source_lines) == 1: + s = indent + 'Code: {' + source_lines[0] + '}' + else: + s = indent + 'Code:\n' + indent + '{\n' + '\n'.join( + indent + 4 * ' ' + l for l in source_lines + ) + '\n' + indent + '}' + return s + self.format_children(indent) + + +def expand(filename, line_directive=_default_line_directive, **local_bindings): + r"""Return the contents of the given template file, executed with the given + local bindings. + + >>> from tempfile import NamedTemporaryFile + >>> # On Windows, the name of a NamedTemporaryFile cannot be used to open + >>> # the file for a second time if delete=True. Therefore, we have to + >>> # manually handle closing and deleting this file to allow us to open + >>> # the file by its name across all platforms. + >>> f = NamedTemporaryFile(delete=False) + >>> f.write( + ... r'''--- + ... % for i in range(int(x)): + ... a pox on ${i} for epoxy + ... % end + ... ${120 + + ... + ... 3} + ... abc + ... ${"w\nx\nX\ny"} + ... z + ... ''') + >>> f.flush() + >>> result = expand( + ... f.name, + ... line_directive='//#sourceLocation(file: "%(file)s", ' + \ + ... 'line: %(line)d)', + ... x=2 + ... ).replace( + ... '"%s"' % f.name.replace('\\', '/'), '"dummy.file"') + >>> print(result, end='') + //#sourceLocation(file: "dummy.file", line: 1) + --- + //#sourceLocation(file: "dummy.file", line: 3) + a pox on 0 for epoxy + //#sourceLocation(file: "dummy.file", line: 3) + a pox on 1 for epoxy + //#sourceLocation(file: "dummy.file", line: 5) + 123 + //#sourceLocation(file: "dummy.file", line: 8) + abc + w + x + X + y + //#sourceLocation(file: "dummy.file", line: 10) + z + >>> f.close() + >>> os.remove(f.name) + """ + with open(filename) as f: + t = parse_template(filename, f.read()) + d = os.getcwd() + os.chdir(os.path.dirname(os.path.abspath(filename))) + try: + return execute_template( + t, line_directive=line_directive, **local_bindings) + finally: + os.chdir(d) + + +def parse_template(filename, text=None): + r"""Return an AST corresponding to the given template file. + + If text is supplied, it is assumed to be the contents of the file, + as a string. + + >>> print(parse_template('dummy.file', text= + ... '''% for x in [1, 2, 3]: + ... % if x == 1: + ... literal1 + ... % elif x > 1: # add output line after this line to fix bug + ... % if x == 2: + ... literal2 + ... % end + ... % end + ... % end + ... ''')) + Block: + [ + Code: + { + for x in [1, 2, 3]: + __children__[0].execute(__context__) + } + [ + Block: + [ + Code: + { + if x == 1: + __children__[0].execute(__context__) + elif x > 1: # add output line after this line to fix bug + __children__[1].execute(__context__) + } + [ + Block: + [ + Literal: + literal1 + ] + Block: + [ + Code: + { + if x == 2: + __children__[0].execute(__context__) + } + [ + Block: + [ + Literal: + literal2 + ] + ] + ] + ] + ] + ] + ] + + >>> print(parse_template( + ... 'dummy.file', + ... text='%for x in range(10):\n% print(x)\n%end\njuicebox')) + Block: + [ + Code: + { + for x in range(10): + __children__[0].execute(__context__) + } + [ + Block: + [ + Code: {print(x)} [] + ] + ] + Literal: + juicebox + ] + + >>> print(parse_template('/dummy.file', text= + ... '''Nothing + ... % if x: + ... % for i in range(3): + ... ${i} + ... % end + ... % else: + ... THIS SHOULD NOT APPEAR IN THE OUTPUT + ... ''')) + Block: + [ + Literal: + Nothing + Code: + { + if x: + __children__[0].execute(__context__) + else: + __children__[1].execute(__context__) + } + [ + Block: + [ + Code: + { + for i in range(3): + __children__[0].execute(__context__) + } + [ + Block: + [ + Code: {(i)} [] + Literal: + + ] + ] + ] + Block: + [ + Literal: + THIS SHOULD NOT APPEAR IN THE OUTPUT + ] + ] + ] + + >>> print(parse_template('dummy.file', text='''% + ... %for x in y: + ... % print(y) + ... ''')) + Block: + [ + Code: + { + for x in y: + __children__[0].execute(__context__) + } + [ + Block: + [ + Code: {print(y)} [] + ] + ] + ] + + >>> print(parse_template('dummy.file', text='''% + ... %if x: + ... % print(y) + ... AAAA + ... %else: + ... BBBB + ... ''')) + Block: + [ + Code: + { + if x: + __children__[0].execute(__context__) + else: + __children__[1].execute(__context__) + } + [ + Block: + [ + Code: {print(y)} [] + Literal: + AAAA + ] + Block: + [ + Literal: + BBBB + ] + ] + ] + + >>> print(parse_template('dummy.file', text='''% + ... %if x: + ... % print(y) + ... AAAA + ... %# This is a comment + ... %else: + ... BBBB + ... ''')) + Block: + [ + Code: + { + if x: + __children__[0].execute(__context__) + # This is a comment + else: + __children__[1].execute(__context__) + } + [ + Block: + [ + Code: {print(y)} [] + Literal: + AAAA + ] + Block: + [ + Literal: + BBBB + ] + ] + ] + + >>> print(parse_template('dummy.file', text='''\ + ... %for x in y: + ... AAAA + ... %if x: + ... BBBB + ... %end + ... CCCC + ... ''')) + Block: + [ + Code: + { + for x in y: + __children__[0].execute(__context__) + } + [ + Block: + [ + Literal: + AAAA + Code: + { + if x: + __children__[0].execute(__context__) + } + [ + Block: + [ + Literal: + BBBB + ] + ] + Literal: + CCCC + ] + ] + ] + """ + return Block(ParseContext(filename, text)) + + +def execute_template( + ast, line_directive=_default_line_directive, **local_bindings): + r"""Return the text generated by executing the given template AST. + + Keyword arguments become local variable bindings in the execution context + + >>> root_directory = os.path.abspath('/') + >>> file_name = (root_directory + 'dummy.file').replace('\\', '/') + >>> ast = parse_template(file_name, text= + ... '''Nothing + ... % if x: + ... % for i in range(3): + ... ${i} + ... % end + ... % else: + ... THIS SHOULD NOT APPEAR IN THE OUTPUT + ... ''') + >>> out = execute_template(ast, + ... line_directive='//#sourceLocation(file: "%(file)s", line: %(line)d)', + ... x=1) + >>> out = out.replace(file_name, "DUMMY-FILE") + >>> print(out, end="") + //#sourceLocation(file: "DUMMY-FILE", line: 1) + Nothing + //#sourceLocation(file: "DUMMY-FILE", line: 4) + 0 + //#sourceLocation(file: "DUMMY-FILE", line: 4) + 1 + //#sourceLocation(file: "DUMMY-FILE", line: 4) + 2 + + >>> ast = parse_template(file_name, text= + ... '''Nothing + ... % a = [] + ... % for x in range(3): + ... % a.append(x) + ... % end + ... ${a} + ... ''') + >>> out = execute_template(ast, + ... line_directive='//#sourceLocation(file: "%(file)s", line: %(line)d)', + ... x=1) + >>> out = out.replace(file_name, "DUMMY-FILE") + >>> print(out, end="") + //#sourceLocation(file: "DUMMY-FILE", line: 1) + Nothing + //#sourceLocation(file: "DUMMY-FILE", line: 6) + [0, 1, 2] + + >>> ast = parse_template(file_name, text= + ... '''Nothing + ... % a = [] + ... % for x in range(3): + ... % a.append(x) + ... % end + ... ${a} + ... ''') + >>> out = execute_template(ast, + ... line_directive='#line %(line)d "%(file)s"', x=1) + >>> out = out.replace(file_name, "DUMMY-FILE") + >>> print(out, end="") + #line 1 "DUMMY-FILE" + Nothing + #line 6 "DUMMY-FILE" + [0, 1, 2] + """ + execution_context = ExecutionContext( + line_directive=line_directive, **local_bindings) + ast.execute(execution_context) + return ''.join(execution_context.result_text) + + +def main(): + import argparse + import sys + + parser = argparse.ArgumentParser( + formatter_class=argparse.RawDescriptionHelpFormatter, + description='Generate Your Boilerplate!', epilog=''' + A GYB template consists of the following elements: + + - Literal text which is inserted directly into the output + + - %% or $$ in literal text, which insert literal '%' and '$' + symbols respectively. + + - Substitutions of the form ${}. The Python + expression is converted to a string and the result is inserted + into the output. + + - Python code delimited by %{...}%. Typically used to inject + definitions (functions, classes, variable bindings) into the + evaluation context of the template. Common indentation is + stripped, so you can add as much indentation to the beginning + of this code as you like + + - Lines beginning with optional whitespace followed by a single + '%' and Python code. %-lines allow you to nest other + constructs inside them. To close a level of nesting, use the + "%end" construct. + + - Lines beginning with optional whitespace and followed by a + single '%' and the token "end", which close open constructs in + %-lines. + + Example template: + + - Hello - + %{ + x = 42 + def succ(a): + return a+1 + }% + + I can assure you that ${x} < ${succ(x)} + + % if int(y) > 7: + % for i in range(3): + y is greater than seven! + % end + % else: + y is less than or equal to seven + % end + + - The End. - + + When run with "gyb -Dy=9", the output is + + - Hello - + + I can assure you that 42 < 43 + + y is greater than seven! + y is greater than seven! + y is greater than seven! + + - The End. - +''' + ) + parser.add_argument( + '-D', action='append', dest='defines', metavar='NAME=VALUE', + default=[], + help='''Bindings to be set in the template's execution context''') + + parser.add_argument( + 'file', type=argparse.FileType(), + help='Path to GYB template file (defaults to stdin)', nargs='?', + default=sys.stdin) + parser.add_argument( + '-o', dest='target', type=argparse.FileType('w'), + help='Output file (defaults to stdout)', default=sys.stdout) + parser.add_argument( + '--test', action='store_true', + default=False, help='Run a self-test') + parser.add_argument( + '--verbose-test', action='store_true', + default=False, help='Run a verbose self-test') + parser.add_argument( + '--dump', action='store_true', + default=False, help='Dump the parsed template to stdout') + parser.add_argument( + '--line-directive', + default=_default_line_directive, + help=''' + Line directive format string, which will be + provided 2 substitutions, `%%(line)d` and `%%(file)s`. + + Example: `#sourceLocation(file: "%%(file)s", line: %%(line)d)` + + The default works automatically with the `line-directive` tool, + which see for more information. + ''') + + args = parser.parse_args(sys.argv[1:]) + + if args.test or args.verbose_test: + import doctest + selfmod = sys.modules[__name__] + if doctest.testmod(selfmod, verbose=args.verbose_test or None).failed: + sys.exit(1) + + bindings = dict(x.split('=', 1) for x in args.defines) + ast = parse_template(args.file.name, args.file.read()) + if args.dump: + print(ast) + # Allow the template to open files and import .py files relative to its own + # directory + os.chdir(os.path.dirname(os.path.abspath(args.file.name))) + sys.path = ['.'] + sys.path + + args.target.write(execute_template(ast, args.line_directive, **bindings)) + + +if __name__ == '__main__': + main() diff --git a/Utils/gyb_utils.py b/Utils/gyb_utils.py new file mode 100644 index 000000000..e73fd8c78 --- /dev/null +++ b/Utils/gyb_utils.py @@ -0,0 +1,34 @@ +#===-----------------------------------------------------------------------===// +# +# This source file is part of the Swift.org open source project +# +# Copyright (c) 2023 - 2024 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 +# +#===-----------------------------------------------------------------------===// + +def autogenerated_warning(): + return """ +// ############################################################################# +// # # +// # DO NOT EDIT THIS FILE; IT IS AUTOGENERATED. # +// # # +// ############################################################################# +""" + +visibility_levels = ["internal", "public"] +def visibility_boilerplate(part): + if part == "internal": + return """ +// In single module mode, we need these declarations to be internal, +// but in regular builds we want them to be public. Unfortunately +// the current best way to do this is to duplicate all definitions. +#if COLLECTIONS_SINGLE_MODULE""" + + if part == "public": + return "#else // !COLLECTIONS_SINGLE_MODULE" + if part == "end": + return "#endif // COLLECTIONS_SINGLE_MODULE" diff --git a/Utils/run-full-tests.sh b/Utils/run-full-tests.sh index 98df0c614..131492040 100755 --- a/Utils/run-full-tests.sh +++ b/Utils/run-full-tests.sh @@ -3,7 +3,7 @@ # # This source file is part of the Swift Collections open source project # -# Copyright (c) 2021 Apple Inc. and the Swift project authors +# Copyright (c) 2021 - 2024 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 diff --git a/Utils/swift-collections.xcworkspace/xcshareddata/IDETemplateMacros.plist b/Utils/swift-collections.xcworkspace/xcshareddata/IDETemplateMacros.plist index 26d96226b..1408edb6a 100644 --- a/Utils/swift-collections.xcworkspace/xcshareddata/IDETemplateMacros.plist +++ b/Utils/swift-collections.xcworkspace/xcshareddata/IDETemplateMacros.plist @@ -7,7 +7,7 @@ // // This source file is part of the Swift Collections open source project // -// Copyright (c) 2022 Apple Inc. and the Swift project authors +// Copyright (c) 2022 - 2024 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 diff --git a/Utils/swift-collections.xcworkspace/xcshareddata/xcschemes/swift-collections-benchmark-Package.xcscheme b/Utils/swift-collections.xcworkspace/xcshareddata/xcschemes/BitCollections.xcscheme similarity index 61% rename from Utils/swift-collections.xcworkspace/xcshareddata/xcschemes/swift-collections-benchmark-Package.xcscheme rename to Utils/swift-collections.xcworkspace/xcshareddata/xcschemes/BitCollections.xcscheme index a4191ca5a..af14ac476 100644 --- a/Utils/swift-collections.xcworkspace/xcshareddata/xcschemes/swift-collections-benchmark-Package.xcscheme +++ b/Utils/swift-collections.xcworkspace/xcshareddata/xcschemes/BitCollections.xcscheme @@ -1,10 +1,10 @@ + buildImplicitDependencies = "NO"> + BlueprintIdentifier = "BitCollections" + BuildableName = "BitCollections" + BlueprintName = "BitCollections" + ReferencedContainer = "container:.."> + buildForProfiling = "NO" + buildForArchiving = "NO" + buildForAnalyzing = "NO"> + BlueprintIdentifier = "BitCollectionsTests" + BuildableName = "BitCollectionsTests" + BlueprintName = "BitCollectionsTests" + ReferencedContainer = "container:.."> @@ -40,16 +40,17 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> + shouldUseLaunchSchemeArgsEnv = "YES" + codeCoverageEnabled = "YES"> + BlueprintIdentifier = "BitCollectionsTests" + BuildableName = "BitCollectionsTests" + BlueprintName = "BitCollectionsTests" + ReferencedContainer = "container:.."> @@ -64,12 +65,6 @@ debugDocumentVersioning = "YES" debugServiceExtension = "internal" allowLocationSimulation = "YES"> - - - - + BlueprintIdentifier = "BitCollections" + BuildableName = "BitCollections" + BlueprintName = "BitCollections" + ReferencedContainer = "container:.."> diff --git a/Utils/swift-collections.xcworkspace/xcshareddata/xcschemes/Collections.xcscheme b/Utils/swift-collections.xcworkspace/xcshareddata/xcschemes/Collections.xcscheme index 87bb74323..c95503b1a 100644 --- a/Utils/swift-collections.xcworkspace/xcshareddata/xcschemes/Collections.xcscheme +++ b/Utils/swift-collections.xcworkspace/xcshareddata/xcschemes/Collections.xcscheme @@ -4,7 +4,7 @@ version = "1.7"> + buildImplicitDependencies = "NO"> + + + + + + + + + + + + + + + + + + + + + + + + + buildImplicitDependencies = "NO"> + shouldUseLaunchSchemeArgsEnv = "YES"> diff --git a/Utils/swift-collections.xcworkspace/xcshareddata/xcschemes/CollectionsTestSupport.xcscheme b/Utils/swift-collections.xcworkspace/xcshareddata/xcschemes/CollectionsTestSupport.xcscheme index eff5e75be..c99b66933 100644 --- a/Utils/swift-collections.xcworkspace/xcshareddata/xcschemes/CollectionsTestSupport.xcscheme +++ b/Utils/swift-collections.xcworkspace/xcshareddata/xcschemes/CollectionsTestSupport.xcscheme @@ -4,7 +4,7 @@ version = "1.3"> + buildImplicitDependencies = "NO"> + buildImplicitDependencies = "NO"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Utils/swift-collections.xcworkspace/xcshareddata/xcschemes/HeapModule.xcscheme b/Utils/swift-collections.xcworkspace/xcshareddata/xcschemes/HeapModule.xcscheme new file mode 100644 index 000000000..d375f753e --- /dev/null +++ b/Utils/swift-collections.xcworkspace/xcshareddata/xcschemes/HeapModule.xcscheme @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Utils/swift-collections.xcworkspace/xcshareddata/xcschemes/OrderedCollections.xcscheme b/Utils/swift-collections.xcworkspace/xcshareddata/xcschemes/OrderedCollections.xcscheme index 035f4ee02..80ffc302d 100644 --- a/Utils/swift-collections.xcworkspace/xcshareddata/xcschemes/OrderedCollections.xcscheme +++ b/Utils/swift-collections.xcworkspace/xcshareddata/xcschemes/OrderedCollections.xcscheme @@ -4,7 +4,7 @@ version = "1.7"> + buildImplicitDependencies = "NO"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Utils/swift-collections.xcworkspace/xcshareddata/xcschemes/benchmark.xcscheme b/Utils/swift-collections.xcworkspace/xcshareddata/xcschemes/benchmark.xcscheme index 6b1436946..67ab6611d 100644 --- a/Utils/swift-collections.xcworkspace/xcshareddata/xcschemes/benchmark.xcscheme +++ b/Utils/swift-collections.xcworkspace/xcshareddata/xcschemes/benchmark.xcscheme @@ -4,7 +4,7 @@ version = "1.7"> + buildImplicitDependencies = "NO"> + shouldUseLaunchSchemeArgsEnv = "YES" + codeCoverageEnabled = "YES"> diff --git a/Utils/swift-collections.xcworkspace/xcshareddata/xcschemes/memory-benchmark.xcscheme b/Utils/swift-collections.xcworkspace/xcshareddata/xcschemes/memory-benchmark.xcscheme new file mode 100644 index 000000000..b38b37fa7 --- /dev/null +++ b/Utils/swift-collections.xcworkspace/xcshareddata/xcschemes/memory-benchmark.xcscheme @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Utils/swift-collections.xcworkspace/xcshareddata/xcschemes/swift-collections-Package.xcscheme b/Utils/swift-collections.xcworkspace/xcshareddata/xcschemes/swift-collections-Package.xcscheme index 814382903..6e50ef75b 100644 --- a/Utils/swift-collections.xcworkspace/xcshareddata/xcschemes/swift-collections-Package.xcscheme +++ b/Utils/swift-collections.xcworkspace/xcshareddata/xcschemes/swift-collections-Package.xcscheme @@ -4,7 +4,7 @@ version = "1.7"> + buildImplicitDependencies = "NO"> + + + + + + + + + + + + + + + + + + + + + + + + @@ -123,6 +207,16 @@ ReferencedContainer = "container:.."> + + + + + + + + + + + + diff --git a/Xcode/Collections.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Xcode/Collections.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/Xcode/Collections.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Xcode/Collections.xcodeproj/xcshareddata/xcschemes/Collections.xcscheme b/Xcode/Collections.xcodeproj/xcshareddata/xcschemes/Collections.xcscheme new file mode 100644 index 000000000..3266c5aac --- /dev/null +++ b/Xcode/Collections.xcodeproj/xcshareddata/xcschemes/Collections.xcscheme @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Xcode/Collections.xctestplan b/Xcode/Collections.xctestplan new file mode 100644 index 000000000..446f3f5d8 --- /dev/null +++ b/Xcode/Collections.xctestplan @@ -0,0 +1,25 @@ +{ + "configurations" : [ + { + "id" : "18C3508F-98DB-45BE-9CFC-5159D79BA600", + "name" : "Test Scheme Action", + "options" : { + + } + } + ], + "defaultOptions" : { + + }, + "testTargets" : [ + { + "parallelizable" : true, + "target" : { + "containerPath" : "container:Collections.xcodeproj", + "identifier" : "7DE91B2D29CA6721004483EB", + "name" : "CollectionsTests" + } + } + ], + "version" : 1 +} diff --git a/Xcode/CollectionsTests.xcconfig b/Xcode/CollectionsTests.xcconfig new file mode 100644 index 000000000..b32fcad5a --- /dev/null +++ b/Xcode/CollectionsTests.xcconfig @@ -0,0 +1,33 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +PRODUCT_NAME = CollectionsTests +PRODUCT_BUNDLE_IDENTIFIER = org.swift.CollectionsTests + +SUPPORTED_PLATFORMS = macosx iphoneos iphonesimulator watchos watchsimulator appletvos appletvsimulator +ARCHS = $(ARCHS_STANDARD) + +MACOSX_DEPLOYMENT_TARGET = 12.0 +IPHONEOS_DEPLOYMENT_TARGET = 15.0 +WATCHOS_DEPLOYMENT_TARGET = 8.0 +TVOS_DEPLOYMENT_TARGET = 15.0 + +CURRENT_PROJECT_VERSION = 1 +MARKETING_VERSION = 1.0 + +GENERATE_INFOPLIST_FILE = YES + +CODE_SIGN_STYLE = Automatic +CODE_SIGN_IDENTITY = - + +ENABLE_TESTABILITY = NO + +SWIFT_ACTIVE_COMPILATION_CONDITIONS = $(inherited) COLLECTIONS_RANDOMIZED_TESTING diff --git a/Xcode/README.md b/Xcode/README.md new file mode 100644 index 000000000..0b8ae3825 --- /dev/null +++ b/Xcode/README.md @@ -0,0 +1,5 @@ +# Xcode build files + +The project file here is used to build a variant of this package with Xcode. The project file is a regular Xcode project that builds the code base using the COLLECTIONS_SINGLE_MODULE configuration, producing a single framework bundle. Builds settings are entirely configured via the provided xcconfig files. + +Beware! The contents of this directory are not source stable. They are provided as is, with no compatibility promises across package releases. Future versions of this package can arbitrarily change these files or remove them, without any advance notice. (This can include patch releases.) diff --git a/Xcode/Shared.xcconfig b/Xcode/Shared.xcconfig new file mode 100644 index 000000000..acf9a32ce --- /dev/null +++ b/Xcode/Shared.xcconfig @@ -0,0 +1,82 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2023 - 2024 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 +// +//===----------------------------------------------------------------------===// + +SDKROOT = macosx + +SWIFT_VERSION = 5.5 +ONLY_ACTIVE_ARCH[config=Debug] = YES + +SWIFT_ACTIVE_COMPILATION_CONDITIONS = $(inherited) COLLECTIONS_SINGLE_MODULE +SWIFT_ACTIVE_COMPILATION_CONDITIONS[config=Debug] = $(inherited) COLLECTIONS_INTERNAL_CHECKS DEBUG + +SWIFT_COMPILATION_MODE[config=Release] = wholemodule +SWIFT_COMPILATION_MODE[config=Debug] = singlefile + +SWIFT_OPTIMIZATION_LEVEL[config=Release] = -O +SWIFT_OPTIMIZATION_LEVEL[config=Debug] = -Onone + +ENABLE_USER_SCRIPT_SANDBOXING = YES + +SWIFT_EMIT_LOC_STRINGS = NO +SWIFT_INSTALL_OBJC_HEADER = NO + +DEAD_CODE_STRIPPING = YES +COPY_PHASE_STRIP = NO + +DEBUG_INFORMATION_FORMAT[config=Release] = dwarf-with-dsym +DEBUG_INFORMATION_FORMAT[config=Debug] = dwarf + +ALWAYS_SEARCH_USER_PATHS = NO + +GCC_DYNAMIC_NO_PIC = NO +GCC_NO_COMMON_BLOCKS = YES +GCC_OPTIMIZATION_LEVEL[config=Debug] = 0 + +GCC_C_LANGUAGE_STANDARD = gnu11 +CLANG_CXX_LANGUAGE_STANDARD = gnu++20 +CLANG_ENABLE_MODULES = YES +CLANG_ENABLE_OBJC_ARC = YES +CLANG_ENABLE_OBJC_WEAK = YES +ENABLE_NS_ASSERTIONS[config=Release] = NO +ENABLE_STRICT_OBJC_MSGSEND = YES +GCC_PREPROCESSOR_DEFINITIONS[config=Release] = DEBUG=1 $(inherited) + +CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES +CLANG_WARN_BOOL_CONVERSION = YES +CLANG_WARN_COMMA = YES +CLANG_WARN_CONSTANT_CONVERSION = YES +CLANG_WARN_DOCUMENTATION_COMMENTS = YES +CLANG_WARN_EMPTY_BODY = YES +CLANG_WARN_ENUM_CONVERSION = YES +CLANG_WARN_INFINITE_RECURSION = YES +CLANG_WARN_INT_CONVERSION = YES +CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN_UNREACHABLE_CODE = YES + +GCC_WARN_64_TO_32_BIT_CONVERSION = YES +GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR +GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE +GCC_WARN_UNUSED_FUNCTION = YES +GCC_WARN_UNUSED_VARIABLE = YES +CLANG_WARN_RANGE_LOOP_ANALYSIS = YES +CLANG_WARN_SUSPICIOUS_MOVE = YES +CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES +CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR +CLANG_WARN_OBJC_LITERAL_CONVERSION = YES +CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +CLANG_ANALYZER_NONNULL = YES +CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE diff --git a/cmake/modules/CMakeLists.txt b/cmake/modules/CMakeLists.txt index e07591af3..b72749030 100644 --- a/cmake/modules/CMakeLists.txt +++ b/cmake/modules/CMakeLists.txt @@ -1,7 +1,7 @@ #[[ This source file is part of the Swift Collections Open Source Project -Copyright (c) 2021 Apple Inc. and the Swift project authors +Copyright (c) 2021 - 2024 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 diff --git a/cmake/modules/SwiftCollectionsConfig.cmake.in b/cmake/modules/SwiftCollectionsConfig.cmake.in index 3343244d9..ae3cc3412 100644 --- a/cmake/modules/SwiftCollectionsConfig.cmake.in +++ b/cmake/modules/SwiftCollectionsConfig.cmake.in @@ -1,7 +1,7 @@ #[[ This source file is part of the Swift Collections Open Source Project -Copyright (c) 2021 Apple Inc. and the Swift project authors +Copyright (c) 2021 - 2024 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 diff --git a/cmake/modules/SwiftSupport.cmake b/cmake/modules/SwiftSupport.cmake index 2a63a91c9..0ce99fb82 100644 --- a/cmake/modules/SwiftSupport.cmake +++ b/cmake/modules/SwiftSupport.cmake @@ -1,7 +1,7 @@ #[[ This source file is part of the Swift Collections Open Source Project -Copyright (c) 2021 Apple Inc. and the Swift project authors +Copyright (c) 2021 - 2024 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