diff --git a/Benchmarks/Benchmarks/DictionaryBenchmarks.swift b/Benchmarks/Benchmarks/DictionaryBenchmarks.swift index 8388fe610..aa01ed4d1 100644 --- a/Benchmarks/Benchmarks/DictionaryBenchmarks.swift +++ b/Benchmarks/Benchmarks/DictionaryBenchmarks.swift @@ -145,6 +145,26 @@ extension Benchmark { blackHole(d) } + self.add( + title: "Dictionary [COW] subscript, insert", + input: ([Int], [Int]).self + ) { input, insert in + return { timer in + let d = Dictionary(uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + let c = input.count + timer.measure { + for i in insert { + var e = d + e[c + i] = 2 * (c + i) + precondition(e.count == input.count + 1) + blackHole(e) + } + } + precondition(d.count == input.count) + blackHole(d) + } + } + self.addSimple( title: "Dictionary subscript, insert, reserving capacity", input: [Int].self @@ -174,6 +194,25 @@ extension Benchmark { } } + self.add( + title: "Dictionary [COW] subscript, remove existing", + input: ([Int], [Int]).self + ) { input, lookups in + return { timer in + let d = Dictionary(uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + timer.measure { + for i in lookups { + var e = d + e[i] = nil + precondition(e.count == input.count - 1) + blackHole(e) + } + } + precondition(d.count == input.count) + blackHole(d) + } + } + self.add( title: "Dictionary subscript, remove missing", input: ([Int], [Int]).self @@ -191,6 +230,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 diff --git a/Benchmarks/Benchmarks/OrderedDictionaryBenchmarks.swift b/Benchmarks/Benchmarks/OrderedDictionaryBenchmarks.swift index f68c2aa86..8ac1e5bd9 100644 --- a/Benchmarks/Benchmarks/OrderedDictionaryBenchmarks.swift +++ b/Benchmarks/Benchmarks/OrderedDictionaryBenchmarks.swift @@ -85,6 +85,19 @@ extension Benchmark { } } + self.add( + title: "OrderedDictionary subscript(position:)", + 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 @@ -176,6 +189,26 @@ extension Benchmark { blackHole(d) } + self.add( + title: "OrderedDictionary [COW] subscript, append", + input: ([Int], [Int]).self + ) { input, insert in + return { timer in + let d = OrderedDictionary(uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + let c = input.count + timer.measure { + for i in insert { + var e = d + e[c + i] = 2 * (c + i) + precondition(e.count == input.count + 1) + blackHole(e) + } + } + precondition(d.count == input.count) + blackHole(d) + } + } + self.addSimple( title: "OrderedDictionary subscript, append, reserving capacity", input: [Int].self @@ -206,6 +239,25 @@ extension Benchmark { } } + self.add( + title: "OrderedDictionary [COW] subscript, remove existing", + input: ([Int], [Int]).self + ) { input, lookups in + return { timer in + let d = OrderedDictionary(uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + timer.measure { + for i in lookups { + var e = d + e[i] = nil + precondition(e.count == input.count - 1) + blackHole(e) + } + } + precondition(d.count == input.count) + blackHole(d) + } + } + self.add( title: "OrderedDictionary subscript, remove missing", input: ([Int], [Int]).self diff --git a/Benchmarks/Benchmarks/PersistentDictionaryBenchmarks.swift b/Benchmarks/Benchmarks/PersistentDictionaryBenchmarks.swift new file mode 100644 index 000000000..4c1a8135a --- /dev/null +++ b/Benchmarks/Benchmarks/PersistentDictionaryBenchmarks.swift @@ -0,0 +1,382 @@ +//===----------------------------------------------------------------------===// +// +// 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 PersistentCollections + +extension Benchmark { + public mutating func addPersistentDictionaryBenchmarks() { + self.add( + title: "PersistentDictionary init(uniqueKeysWithValues:)", + input: [Int].self + ) { input in + let keysAndValues = input.map { ($0, 2 * $0) } + return { timer in + blackHole(PersistentDictionary(uniqueKeysWithValues: keysAndValues)) + } + } + + self.add( + title: "PersistentDictionary sequential iteration", + input: [Int].self + ) { input in + let d = PersistentDictionary(uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + return { timer in + for item in d { + blackHole(item) + } + } + } + + self.add( + title: "PersistentDictionary.Keys sequential iteration", + input: [Int].self + ) { input in + let d = PersistentDictionary(uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + return { timer in + for item in d.keys { + blackHole(item) + } + } + } + + self.add( + title: "PersistentDictionary.Values sequential iteration", + input: [Int].self + ) { input in + let d = PersistentDictionary(uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + return { timer in + for item in d.values { + blackHole(item) + } + } + } + + self.add( + title: "PersistentDictionary subscript(position:)", + input: ([Int], [Int]).self + ) { input, lookups in + let d = PersistentDictionary(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: "PersistentDictionary subscript, successful lookups", + input: ([Int], [Int]).self + ) { input, lookups in + let d = PersistentDictionary(uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + return { timer in + for i in lookups { + precondition(d[i] == 2 * i) + } + } + } + + self.add( + title: "PersistentDictionary subscript, unsuccessful lookups", + input: ([Int], [Int]).self + ) { input, lookups in + let d = PersistentDictionary(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: "PersistentDictionary subscript, noop setter", + input: ([Int], [Int]).self + ) { input, lookups in + return { timer in + var d = PersistentDictionary(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: "PersistentDictionary subscript, set existing", + input: ([Int], [Int]).self + ) { input, lookups in + return { timer in + var d = PersistentDictionary(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: "PersistentDictionary subscript, _modify", + input: ([Int], [Int]).self + ) { input, lookups in + return { timer in + var d = PersistentDictionary(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: "PersistentDictionary subscript, insert", + input: [Int].self + ) { input in + var d: PersistentDictionary = [:] + for i in input { + d[i] = 2 * i + } + precondition(d.count == input.count) + blackHole(d) + } + + self.add( + title: "PersistentDictionary [COW] subscript, insert", + input: ([Int], [Int]).self + ) { input, insert in + return { timer in + let d = PersistentDictionary(uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + let c = input.count + timer.measure { + for i in insert { + var e = d + e[c + i] = 2 * (c + i) + precondition(e.count == input.count + 1) + blackHole(e) + } + } + precondition(d.count == input.count) + blackHole(d) + } + } + + self.add( + title: "PersistentDictionary subscript, remove existing", + input: ([Int], [Int]).self + ) { input, lookups in + return { timer in + var d = PersistentDictionary(uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + timer.measure { + for i in lookups { + d[i] = nil + } + } + precondition(d.isEmpty) + blackHole(d) + } + } + + self.add( + title: "PersistentDictionary [COW] subscript, remove existing", + input: ([Int], [Int]).self + ) { input, lookups in + return { timer in + let d = PersistentDictionary(uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + timer.measure { + for i in lookups { + var e = d + e[i] = nil + precondition(e.count == input.count - 1) + blackHole(e) + } + } + precondition(d.count == input.count) + blackHole(d) + } + } + + self.add( + title: "PersistentDictionary subscript, remove missing", + input: ([Int], [Int]).self + ) { input, lookups in + return { timer in + var d = PersistentDictionary(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: "PersistentDictionary defaulted subscript, successful lookups", + input: ([Int], [Int]).self + ) { input, lookups in + let d = PersistentDictionary(uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + return { timer in + for i in lookups { + precondition(d[i, default: -1] != -1) + } + } + } + + self.add( + title: "PersistentDictionary defaulted subscript, unsuccessful lookups", + input: ([Int], [Int]).self + ) { input, lookups in + let d = PersistentDictionary(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: "PersistentDictionary defaulted subscript, _modify existing", + input: [Int].self + ) { input in + return { timer in + var d = PersistentDictionary(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: "PersistentDictionary defaulted subscript, _modify missing", + input: ([Int], [Int]).self + ) { input, lookups in + return { timer in + var d = PersistentDictionary(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: "PersistentDictionary successful index(forKey:)", + input: ([Int], [Int]).self + ) { input, lookups in + let d = PersistentDictionary(uniqueKeysWithValues: input.lazy.map { ($0, 2 * $0) }) + return { timer in + for i in lookups { + precondition(d.index(forKey: i) != nil) + } + } + } + + self.add( + title: "PersistentDictionary unsuccessful index(forKey:)", + input: ([Int], [Int]).self + ) { input, lookups in + let d = PersistentDictionary(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: "PersistentDictionary updateValue(_:forKey:), existing", + input: ([Int], [Int]).self + ) { input, lookups in + return { timer in + var d = PersistentDictionary(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: "PersistentDictionary updateValue(_:forKey:), insert", + input: ([Int], [Int]).self + ) { input, lookups in + return { timer in + var d = PersistentDictionary(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: "PersistentDictionary random removals (existing keys)", + input: ([Int], [Int]).self + ) { input, lookups in + return { timer in + var d = PersistentDictionary(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: "PersistentDictionary random removals (missing keys)", + input: ([Int], [Int]).self + ) { input, lookups in + return { timer in + let c = input.count + var d = PersistentDictionary(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) + } + } + + } +} diff --git a/Benchmarks/Libraries/PersistentCollections.json b/Benchmarks/Libraries/PersistentCollections.json new file mode 100644 index 000000000..9fe3eca0e --- /dev/null +++ b/Benchmarks/Libraries/PersistentCollections.json @@ -0,0 +1,160 @@ +{ + "kind": "group", + "title": "PersistentCollections Benchmarks", + "directory": "Results", + "contents": [ + { + "kind": "group", + "title": "PersistentDictionary Operations", + "contents": [ + { + "kind": "chart", + "title": "all", + "tasks": [ + "PersistentDictionary init(uniqueKeysWithValues:)", + "PersistentDictionary sequential iteration", + "PersistentDictionary.Keys sequential iteration", + "PersistentDictionary.Values sequential iteration", + "PersistentDictionary subscript, successful lookups", + "PersistentDictionary subscript, unsuccessful lookups", + "PersistentDictionary subscript, noop setter", + "PersistentDictionary subscript, set existing", + "PersistentDictionary subscript, _modify", + "PersistentDictionary subscript, insert", + "PersistentDictionary subscript, insert, reserving capacity", + "PersistentDictionary subscript, remove existing", + "PersistentDictionary subscript, remove missing", + "PersistentDictionary defaulted subscript, successful lookups", + "PersistentDictionary defaulted subscript, unsuccessful lookups", + "PersistentDictionary defaulted subscript, _modify existing", + "PersistentDictionary defaulted subscript, _modify missing", + "PersistentDictionary updateValue(_:forKey:), existing", + "PersistentDictionary updateValue(_:forKey:), insert", + "PersistentDictionary random removals (existing keys)", + "PersistentDictionary random removals (missing keys)", + "PersistentDictionary successful index(forKey:)", + "PersistentDictionary unsuccessful index(forKey:)", + ] + } + ] + }, + { + "kind": "group", + "title": "HashSet Operations", + "contents": [] + }, + { + "kind": "group", + "title": "Comparisons against reference implementations", + "directory": "versus", + "contents": [ + { + "kind": "chart", + "title": "init(uniqueKeysWithValues:)", + "tasks": [ + "PersistentDictionary init(uniqueKeysWithValues:)", + "Dictionary init(uniqueKeysWithValues:)", + "OrderedDictionary init(uniqueKeysWithValues:)", + ] + }, + { + "kind": "chart", + "title": "sequential iteration", + "tasks": [ + "PersistentDictionary sequential iteration", + "Dictionary sequential iteration", + "OrderedDictionary sequential iteration", + ] + }, + { + "kind": "chart", + "title": "sequential iteration [Keys]", + "tasks": [ + "PersistentDictionary.Keys sequential iteration", + "Dictionary.Keys sequential iteration", + "OrderedDictionary.Keys sequential iteration", + ] + }, + { + "kind": "chart", + "title": "sequential iteration [Values]", + "tasks": [ + "PersistentDictionary.Values sequential iteration", + "Dictionary.Values sequential iteration", + "OrderedDictionary.Values sequential iteration", + ] + }, + { + "kind": "chart", + "title": "subscript, insert", + "tasks": [ + "PersistentDictionary subscript, insert", + "Dictionary subscript, insert", + "OrderedDictionary subscript, append", + ] + }, + { + "kind": "chart", + "title": "[COW] subscript, insert", + "tasks": [ + "PersistentDictionary [COW] subscript, insert", + "Dictionary [COW] subscript, insert", + "OrderedDictionary [COW] subscript, append", + ] + }, + { + "kind": "chart", + "title": "subscript, remove existing", + "tasks": [ + "PersistentDictionary subscript, remove existing", + "Dictionary subscript, remove existing", + "OrderedDictionary subscript, remove existing", + ] + }, + { + "kind": "chart", + "title": "subscript(position:)", + "tasks": [ + "PersistentDictionary subscript(position:)", + "Dictionary subscript(position:)", + "OrderedDictionary subscript(position:)", + ] + }, + { + "kind": "chart", + "title": "index(forKey:), successful index(forKey:)", + "tasks": [ + "PersistentDictionary successful index(forKey:)", + "Dictionary successful index(forKey:)", + "OrderedDictionary successful index(forKey:)", + ] + }, + { + "kind": "chart", + "title": "index(forKey:), unsuccessful index(forKey:)", + "tasks": [ + "PersistentDictionary unsuccessful index(forKey:)", + "Dictionary unsuccessful index(forKey:)", + "OrderedDictionary unsuccessful index(forKey:)", + ] + }, + { + "kind": "chart", + "title": "updateValue(_:forKey:), existing", + "tasks": [ + "PersistentDictionary updateValue(_:forKey:), existing", + "Dictionary updateValue(_:forKey:), existing", + ] + }, + { + "kind": "chart", + "title": "updateValue(_:forKey:), insert", + "tasks": [ + "PersistentDictionary updateValue(_:forKey:), insert", + "Dictionary updateValue(_:forKey:), insert", + ] + }, + ] + }, + ] +} diff --git a/Benchmarks/benchmark-tool/main.swift b/Benchmarks/benchmark-tool/main.swift index d2096d384..435277129 100644 --- a/Benchmarks/benchmark-tool/main.swift +++ b/Benchmarks/benchmark-tool/main.swift @@ -16,6 +16,7 @@ var benchmark = Benchmark(title: "Collection Benchmarks") benchmark.addArrayBenchmarks() benchmark.addSetBenchmarks() benchmark.addDictionaryBenchmarks() +benchmark.addPersistentDictionaryBenchmarks() benchmark.addDequeBenchmarks() benchmark.addOrderedSetBenchmarks() benchmark.addOrderedDictionaryBenchmarks() diff --git a/Package.swift b/Package.swift index 97f17ce44..ccb80d957 100644 --- a/Package.swift +++ b/Package.swift @@ -54,6 +54,7 @@ let package = Package( .library(name: "Collections", targets: ["Collections"]), .library(name: "DequeModule", targets: ["DequeModule"]), .library(name: "OrderedCollections", targets: ["OrderedCollections"]), + .library(name: "PersistentCollections", targets: ["PersistentCollections"]), .library(name: "PriorityQueueModule", targets: ["PriorityQueueModule"]), ], targets: [ @@ -62,6 +63,7 @@ let package = Package( dependencies: [ "DequeModule", "OrderedCollections", + "PersistentCollections", "PriorityQueueModule", ], path: "Sources/Collections", @@ -104,6 +106,15 @@ let package = Package( dependencies: ["OrderedCollections", "_CollectionsTestSupport"], swiftSettings: settings), + // PersistentDictionary + .target( + name: "PersistentCollections", + swiftSettings: settings), + .testTarget( + name: "PersistentCollectionsTests", + dependencies: ["PersistentCollections", "_CollectionsTestSupport"], + swiftSettings: settings), + // PriorityQueue .target( name: "PriorityQueueModule", diff --git a/Sources/Collections/Collections.swift b/Sources/Collections/Collections.swift index beacfc238..ec989b309 100644 --- a/Sources/Collections/Collections.swift +++ b/Sources/Collections/Collections.swift @@ -11,4 +11,5 @@ @_exported import DequeModule @_exported import OrderedCollections +@_exported import PersistentCollections @_exported import PriorityQueueModule diff --git a/Sources/PersistentCollections/PersistentDictionary+CVarArg.swift b/Sources/PersistentCollections/PersistentDictionary+CVarArg.swift new file mode 100644 index 000000000..1be31a6ef --- /dev/null +++ b/Sources/PersistentCollections/PersistentDictionary+CVarArg.swift @@ -0,0 +1,16 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 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 +// +//===----------------------------------------------------------------------===// + +//extension PersistentDictionary: CVarArg { +// public var _cVarArgEncoding: [Int] { +// <#code#> +// } +//} diff --git a/Sources/PersistentCollections/PersistentDictionary+Collection.swift b/Sources/PersistentCollections/PersistentDictionary+Collection.swift new file mode 100644 index 000000000..d2aa7dbb4 --- /dev/null +++ b/Sources/PersistentCollections/PersistentDictionary+Collection.swift @@ -0,0 +1,52 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 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 +// +//===----------------------------------------------------------------------===// + +extension PersistentDictionary: Collection { + public typealias Index = PersistentDictionaryIndex + + /// + /// Manipulating Indices + /// + + public var startIndex: Self.Index { PersistentDictionaryIndex(value: 0) } + + public var endIndex: Self.Index { PersistentDictionaryIndex(value: count) } + + public func index(after i: Self.Index) -> Self.Index { + return i + 1 + } + + /// + /// Returns the index for the given key. + /// + public func index(forKey key: Key) -> Self.Index? { + return rootNode.index(key, computeHash(key), 0, 0) + } + + /// + /// Accesses the key-value pair at the specified position. + /// + public subscript(position: Self.Index) -> Self.Element { + return rootNode.get(position: position, 0, position.value) + } +} + +public struct PersistentDictionaryIndex: Comparable { + let value: Int + + public static func < (lhs: Self, rhs: Self) -> Bool { + return lhs.value < rhs.value + } + + public static func +(lhs: Self, rhs: Int) -> Self { + return PersistentDictionaryIndex(value: lhs.value + rhs) + } +} diff --git a/Sources/PersistentCollections/PersistentDictionary+CustomDebugStringConvertible.swift b/Sources/PersistentCollections/PersistentDictionary+CustomDebugStringConvertible.swift new file mode 100644 index 000000000..4e86b71e8 --- /dev/null +++ b/Sources/PersistentCollections/PersistentDictionary+CustomDebugStringConvertible.swift @@ -0,0 +1,16 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 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 +// +//===----------------------------------------------------------------------===// + +//extension PersistentDictionary: CustomDebugStringConvertible { +// public var debugDescription: String { +// <#code#> +// } +//} diff --git a/Sources/PersistentCollections/PersistentDictionary+CustomReflectible.swift b/Sources/PersistentCollections/PersistentDictionary+CustomReflectible.swift new file mode 100644 index 000000000..393908f09 --- /dev/null +++ b/Sources/PersistentCollections/PersistentDictionary+CustomReflectible.swift @@ -0,0 +1,16 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 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 +// +//===----------------------------------------------------------------------===// + +//extension PersistentDictionary: CustomReflectable { +// public var customMirror: Mirror { +// <#code#> +// } +//} diff --git a/Sources/PersistentCollections/PersistentDictionary+CustomStringConvertible.swift b/Sources/PersistentCollections/PersistentDictionary+CustomStringConvertible.swift new file mode 100644 index 000000000..7820e21b0 --- /dev/null +++ b/Sources/PersistentCollections/PersistentDictionary+CustomStringConvertible.swift @@ -0,0 +1,31 @@ +//===----------------------------------------------------------------------===// +// +// 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 PersistentDictionary: CustomStringConvertible { + public var description: String { + guard count > 0 else { + return "[:]" + } + + var result = "[" + var first = true + for (key, value) in self { + if first { + first = false + } else { + result += ", " + } + result += "\(key): \(value)" + } + result += "]" + return result + } +} diff --git a/Sources/PersistentCollections/PersistentDictionary+Decodable.swift b/Sources/PersistentCollections/PersistentDictionary+Decodable.swift new file mode 100644 index 000000000..511fb846e --- /dev/null +++ b/Sources/PersistentCollections/PersistentDictionary+Decodable.swift @@ -0,0 +1,16 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 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 +// +//===----------------------------------------------------------------------===// + +//extension PersistentDictionary: Decodable where Key: Decodable, Value: Decodable { +// public init(from decoder: Decoder) throws { +// <#code#> +// } +//} diff --git a/Sources/PersistentCollections/PersistentDictionary+Encodable.swift b/Sources/PersistentCollections/PersistentDictionary+Encodable.swift new file mode 100644 index 000000000..c1362ab72 --- /dev/null +++ b/Sources/PersistentCollections/PersistentDictionary+Encodable.swift @@ -0,0 +1,16 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 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 +// +//===----------------------------------------------------------------------===// + +//extension PersistentDictionary: Encodable where Key: Encodable, Value: Encodable { +// public func encode(to encoder: Encoder) throws { +// <#code#> +// } +//} diff --git a/Sources/PersistentCollections/PersistentDictionary+Equatable.swift b/Sources/PersistentCollections/PersistentDictionary+Equatable.swift new file mode 100644 index 000000000..9578a47f6 --- /dev/null +++ b/Sources/PersistentCollections/PersistentDictionary+Equatable.swift @@ -0,0 +1,16 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2019 - 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 PersistentDictionary: Equatable where Value: Equatable { + public static func == (lhs: PersistentDictionary, rhs: PersistentDictionary) -> Bool { + lhs.rootNode === rhs.rootNode || lhs.rootNode == rhs.rootNode + } +} diff --git a/Sources/PersistentCollections/PersistentDictionary+ExpressibleByDictionaryLiteral.swift b/Sources/PersistentCollections/PersistentDictionary+ExpressibleByDictionaryLiteral.swift new file mode 100644 index 000000000..2efbde5e3 --- /dev/null +++ b/Sources/PersistentCollections/PersistentDictionary+ExpressibleByDictionaryLiteral.swift @@ -0,0 +1,18 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2019 - 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 PersistentDictionary: ExpressibleByDictionaryLiteral { + @inlinable + @inline(__always) + public init(dictionaryLiteral elements: (Key, Value)...) { + self.init(uniqueKeysWithValues: elements) + } +} diff --git a/Sources/PersistentCollections/PersistentDictionary+Hashable.swift b/Sources/PersistentCollections/PersistentDictionary+Hashable.swift new file mode 100644 index 000000000..e7483d94c --- /dev/null +++ b/Sources/PersistentCollections/PersistentDictionary+Hashable.swift @@ -0,0 +1,23 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2019 - 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 PersistentDictionary: Hashable where Value: Hashable { + 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/PersistentCollections/PersistentDictionary+Keys.swift b/Sources/PersistentCollections/PersistentDictionary+Keys.swift new file mode 100644 index 000000000..7d014fedf --- /dev/null +++ b/Sources/PersistentCollections/PersistentDictionary+Keys.swift @@ -0,0 +1,25 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 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 +// +//===----------------------------------------------------------------------===// + +// TODO: implement a custom `Keys` view rather than relying on an array representation +extension PersistentDictionary { + /// + /// A view of a dictionary’s keys. + /// + public typealias Keys = [Key] + + /// + /// A collection containing just the keys of the dictionary. + /// + public var keys: Self.Keys /* { get } */ { + self.map { $0.key } + } +} diff --git a/Sources/PersistentCollections/PersistentDictionary+Sequence.swift b/Sources/PersistentCollections/PersistentDictionary+Sequence.swift new file mode 100644 index 000000000..5b053baf1 --- /dev/null +++ b/Sources/PersistentCollections/PersistentDictionary+Sequence.swift @@ -0,0 +1,18 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2019 - 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 PersistentDictionary: Sequence { + public typealias Element = DictionaryKeyValueTupleIterator.Element + + public __consuming func makeIterator() -> DictionaryKeyValueTupleIterator { + return DictionaryKeyValueTupleIterator(rootNode: rootNode) + } +} diff --git a/Sources/PersistentCollections/PersistentDictionary+Values.swift b/Sources/PersistentCollections/PersistentDictionary+Values.swift new file mode 100644 index 000000000..83e356556 --- /dev/null +++ b/Sources/PersistentCollections/PersistentDictionary+Values.swift @@ -0,0 +1,25 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 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 +// +//===----------------------------------------------------------------------===// + +// TODO: implement a custom `Values` view rather than relying on an array representation +extension PersistentDictionary { + /// + /// A view of a dictionary’s values. + /// + public typealias Values = [Value] + + /// + /// A collection containing just the values of the dictionary. + /// + public var values: Self.Values /* { get set } */ { + self.map { $0.value } + } +} diff --git a/Sources/PersistentCollections/PersistentDictionary.swift b/Sources/PersistentCollections/PersistentDictionary.swift new file mode 100644 index 000000000..355838ef0 --- /dev/null +++ b/Sources/PersistentCollections/PersistentDictionary.swift @@ -0,0 +1,220 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2019 - 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 +// +//===----------------------------------------------------------------------===// + +public struct PersistentDictionary where Key: Hashable { + var rootNode: BitmapIndexedDictionaryNode + + fileprivate init(_ rootNode: BitmapIndexedDictionaryNode) { + self.rootNode = rootNode + } + + public init() { + self.init(BitmapIndexedDictionaryNode()) + } + + public init(_ map: PersistentDictionary) { + self.init(map.rootNode) + } + + @inlinable + @inline(__always) + public init(uniqueKeysWithValues keysAndValues: S) where S : Sequence, S.Element == (Key, Value) { + var builder = Self() + var expectedCount = 0 + keysAndValues.forEach { key, value in + builder.updateValue(value, forKey: key) + expectedCount += 1 + + guard expectedCount == builder.count else { + preconditionFailure("Duplicate key: '\(key)'") + } + } + self.init(builder) + } + + @inlinable + @inline(__always) + public init(uniqueKeys keys: Keys, values: Values) where Keys.Element == Key, Values.Element == Value { + self.init(uniqueKeysWithValues: zip(keys, values)) + } + + /// + /// Inspecting a Dictionary + /// + + public var isEmpty: Bool { rootNode.count == 0 } + + public var count: Int { rootNode.count } + + public var underestimatedCount: Int { rootNode.count } + + public var capacity: Int { rootNode.count } + + /// + /// Accessing Keys and Values + /// + + public subscript(key: Key) -> Value? { + get { + return get(key) + } + mutating set(optionalValue) { + if let value = optionalValue { + updateValue(value, forKey: key) + } else { + removeValue(forKey: key) + } + } + } + + public subscript(key: Key, default defaultValue: @autoclosure () -> Value) -> Value { + get { + return get(key) ?? defaultValue() + } + mutating set(value) { + updateValue(value, forKey: key) + } + } + + public func contains(_ key: Key) -> Bool { + rootNode.containsKey(key, computeHash(key), 0) + } + + func get(_ key: Key) -> Value? { + rootNode.get(key, computeHash(key), 0) + } + + @discardableResult + public mutating func updateValue(_ value: Value, forKey key: Key) -> Value? { + let isStorageKnownUniquelyReferenced = isKnownUniquelyReferenced(&self.rootNode) + + var effect = DictionaryEffect() + let keyHash = computeHash(key) + let newRootNode = rootNode.updateOrUpdating(isStorageKnownUniquelyReferenced, key, value, keyHash, 0, &effect) + + if effect.modified { + self.rootNode = newRootNode + } + + // Note, always tracking discardable result negatively impacts batch use cases + return effect.previousValue + } + + // fluid/immutable API + public func updatingValue(_ value: Value, forKey key: Key) -> Self { + var effect = DictionaryEffect() + let keyHash = computeHash(key) + let newRootNode = rootNode.updateOrUpdating(false, key, value, keyHash, 0, &effect) + + if effect.modified { + return Self(newRootNode) + } else { return self } + } + + @discardableResult + public mutating func removeValue(forKey key: Key) -> Value? { + let isStorageKnownUniquelyReferenced = isKnownUniquelyReferenced(&self.rootNode) + + var effect = DictionaryEffect() + let keyHash = computeHash(key) + let newRootNode = rootNode.removeOrRemoving(isStorageKnownUniquelyReferenced, key, keyHash, 0, &effect) + + if effect.modified { + self.rootNode = newRootNode + } + + // Note, always tracking discardable result negatively impacts batch use cases + return effect.previousValue + } + + // fluid/immutable API + public func removingValue(forKey key: Key) -> Self { + var effect = DictionaryEffect() + let keyHash = computeHash(key) + let newRootNode = rootNode.removeOrRemoving(false, key, keyHash, 0, &effect) + + if effect.modified { + return Self(newRootNode) + } else { return self } + } +} + +/// +/// Fixed-stack iterator for traversing a hash-trie. The iterator performs a +/// depth-first pre-order traversal, which yields first all payload elements of the current +/// node before traversing sub-nodes (left to right). +/// +public struct DictionaryKeyValueTupleIterator: IteratorProtocol { + + private var payloadIterator: UnsafeBufferPointer<(key: Key, value: Value)>.Iterator? + + private var trieIteratorStackTop: UnsafeBufferPointer>.Iterator? + private var trieIteratorStackRemainder: [UnsafeBufferPointer>.Iterator] + + init(rootNode: BitmapIndexedDictionaryNode) { + trieIteratorStackRemainder = [] + trieIteratorStackRemainder.reserveCapacity(maxDepth) + + if rootNode.hasNodes { trieIteratorStackTop = rootNode._trieSlice.makeIterator() } + if rootNode.hasPayload { payloadIterator = rootNode._dataSlice.makeIterator() } + } + + public mutating func next() -> (key: Key, value: Value)? { + if let payload = payloadIterator?.next() { + return payload + } + + while trieIteratorStackTop != nil { + if let nextNode = trieIteratorStackTop!.next() { + if nextNode.hasNodes { + trieIteratorStackRemainder.append(trieIteratorStackTop!) + trieIteratorStackTop = nextNode._trieSlice.makeIterator() + } + if nextNode.hasPayload { + payloadIterator = nextNode._dataSlice.makeIterator() + return payloadIterator?.next() + } + } else { + trieIteratorStackTop = trieIteratorStackRemainder.popLast() + } + } + + // Clean-up state + payloadIterator = nil + + assert(payloadIterator == nil) + assert(trieIteratorStackTop == nil) + assert(trieIteratorStackRemainder.isEmpty) + + return nil + } +} + +// TODO consider reworking similar to `DictionaryKeyValueTupleIterator` +// (would require a reversed variant of `UnsafeBufferPointer<(key: Key, value: Value)>.Iterator`) +public struct DictionaryKeyValueTupleReverseIterator { + private var baseIterator: BaseReverseIterator> + + init(rootNode: BitmapIndexedDictionaryNode) { + self.baseIterator = BaseReverseIterator(rootNode: rootNode) + } +} + +extension DictionaryKeyValueTupleReverseIterator: IteratorProtocol { + public mutating func next() -> (key: Key, value: Value)? { + guard baseIterator.hasNext() else { return nil } + + let payload = baseIterator.currentValueNode!.getPayload(baseIterator.currentValueCursor) + baseIterator.currentValueCursor -= 1 + + return payload + } +} diff --git a/Sources/PersistentCollections/_BitmapIndexedDictionaryNode+CustomDebugStringConvertible.swift b/Sources/PersistentCollections/_BitmapIndexedDictionaryNode+CustomDebugStringConvertible.swift new file mode 100644 index 000000000..5ba56bba3 --- /dev/null +++ b/Sources/PersistentCollections/_BitmapIndexedDictionaryNode+CustomDebugStringConvertible.swift @@ -0,0 +1,16 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 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 +// +//===----------------------------------------------------------------------===// + +//extension BitmapIndexedDictionaryNode: CustomDebugStringConvertible { +// public var debugDescription: String { +// <#code#> +// } +//} diff --git a/Sources/PersistentCollections/_BitmapIndexedDictionaryNode+CustomStringConvertible.swift b/Sources/PersistentCollections/_BitmapIndexedDictionaryNode+CustomStringConvertible.swift new file mode 100644 index 000000000..80b97f323 --- /dev/null +++ b/Sources/PersistentCollections/_BitmapIndexedDictionaryNode+CustomStringConvertible.swift @@ -0,0 +1,39 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 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 +// +//===----------------------------------------------------------------------===// + +extension BitmapIndexedDictionaryNode: CustomStringConvertible { + public var description: String { + guard count > 0 else { + return "[:]" + } + + var result = "[" + var first = true + for (key, value) in _dataSlice { + if first { + first = false + } else { + result += ", " + } + result += "\(key): \(value)" + } + for node in _trieSlice { + if first { + first = false + } else { + result += ", " + } + result += "\(node.description)" + } + result += "]" + return result + } +} diff --git a/Sources/PersistentCollections/_BitmapIndexedDictionaryNode.swift b/Sources/PersistentCollections/_BitmapIndexedDictionaryNode.swift new file mode 100644 index 000000000..1cb49be6a --- /dev/null +++ b/Sources/PersistentCollections/_BitmapIndexedDictionaryNode.swift @@ -0,0 +1,796 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2019 - 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 +// +//===----------------------------------------------------------------------===// + +fileprivate let initialDataCapacity: Capacity = 4 +fileprivate let initialTrieCapacity: Capacity = 1 + +final class BitmapIndexedDictionaryNode: DictionaryNode where Key: Hashable { + + typealias ReturnPayload = (key: Key, value: Value) + typealias ReturnBitmapIndexedNode = BitmapIndexedDictionaryNode + + typealias DataBufferElement = ReturnPayload + typealias TrieBufferElement = ReturnBitmapIndexedNode + + var header: Header + var count: Int + + + var collisionFree: Bool { + !hashCollision + } + + var hashCollision: Bool { + header.hashCollision + } + + let dataCapacity: Capacity + let trieCapacity: Capacity + + let dataBaseAddress: UnsafeMutablePointer + let trieBaseAddress: UnsafeMutablePointer + + private var rootBaseAddress: UnsafeMutableRawPointer { UnsafeMutableRawPointer(trieBaseAddress) } + + deinit { + dataBaseAddress.deinitialize(count: header.dataCount) + trieBaseAddress.deinitialize(count: header.trieCount) + + rootBaseAddress.deallocate() + } + + @inline(__always) + var dataMap: Bitmap { + header.dataMap + } + + @inline(__always) + var trieMap: Bitmap { + header.trieMap + } + + @inlinable + static func _allocate(dataCapacity: Capacity, trieCapacity: Capacity) -> (dataBaseAddress: UnsafeMutablePointer, trieBaseAddress: UnsafeMutablePointer) { + let dataCapacityInBytes = Int(dataCapacity) * MemoryLayout.stride + let trieCapacityInBytes = Int(trieCapacity) * MemoryLayout.stride + + let memory = UnsafeMutableRawPointer.allocate( + byteCount: dataCapacityInBytes + trieCapacityInBytes, + alignment: Swift.max(MemoryLayout.alignment, MemoryLayout.alignment)) + + let dataBaseAddress = memory.advanced(by: trieCapacityInBytes).bindMemory(to: DataBufferElement.self, capacity: Int(dataCapacity)) + let trieBaseAddress = memory.bindMemory(to: TrieBufferElement.self, capacity: Int(trieCapacity)) + + return (dataBaseAddress, trieBaseAddress) + } + + func copy(withDataCapacityFactor dataCapacityFactor: Capacity = 1, + withDataCapacityShrinkFactor dataCapacityShrinkFactor: Capacity = 1, + withTrieCapacityFactor trieCapacityFactor: Capacity = 1, + withTrieCapacityShrinkFactor trieCapacityShrinkFactor: Capacity = 1) -> Self { + let src = self + let dst = Self(dataCapacity: src.dataCapacity &* dataCapacityFactor / dataCapacityShrinkFactor, trieCapacity: src.trieCapacity &* trieCapacityFactor / trieCapacityShrinkFactor) + + dst.header = src.header + dst.count = src.count + + dst.dataBaseAddress.initialize(from: src.dataBaseAddress, count: src.header.dataCount) + dst.trieBaseAddress.initialize(from: src.trieBaseAddress, count: src.header.trieCount) + + assert(src.invariant) + assert(dst.invariant) + return dst + } + + var invariant: Bool { + guard headerInvariant else { + return false + } + +// let recursiveCount = self.reduce(0, { count, _ in count + 1 }) +// +// guard recursiveCount == count else { +// return false +// } + + guard count - payloadArity >= 2 * nodeArity else { + return false + } + + if hashCollision { + let hash = computeHash(_dataSlice.first!.key) + + guard _dataSlice.allSatisfy({ computeHash($0.key) == hash }) else { + return false + } + } + + return true + } + + var headerInvariant: Bool { + (header.dataMap & header.trieMap) == 0 || (header.dataMap == header.trieMap) + } + + var _dataSlice: UnsafeBufferPointer { + UnsafeBufferPointer(start: dataBaseAddress, count: header.dataCount) + } + + var _trieSlice: UnsafeMutableBufferPointer { + UnsafeMutableBufferPointer(start: trieBaseAddress, count: header.trieCount) + } + + init(dataCapacity: Capacity, trieCapacity: Capacity) { + let (dataBaseAddress, trieBaseAddress) = Self._allocate(dataCapacity: dataCapacity, trieCapacity: trieCapacity) + + self.header = Header(dataMap: 0, trieMap: 0) + self.count = 0 + + self.dataBaseAddress = dataBaseAddress + self.trieBaseAddress = trieBaseAddress + + self.dataCapacity = dataCapacity + self.trieCapacity = trieCapacity + + assert(self.invariant) + } + + convenience init() { + self.init(dataCapacity: initialDataCapacity, trieCapacity: initialTrieCapacity) + + self.header = Header(dataMap: 0, trieMap: 0) + + assert(self.invariant) + } + + convenience init(dataMap: Bitmap, firstKey: Key, firstValue: Value) { + self.init() + + self.header = Header(dataMap: dataMap, trieMap: 0) + self.count = 1 + + self.dataBaseAddress.initialize(to: (firstKey, firstValue)) + + assert(self.invariant) + } + + convenience init(dataMap: Bitmap, firstKey: Key, firstValue: Value, secondKey: Key, secondValue: Value) { + self.init() + + self.header = Header(dataMap: dataMap, trieMap: 0) + self.count = 2 + + self.dataBaseAddress.initialize(to: (firstKey, firstValue)) + self.dataBaseAddress.successor().initialize(to: (secondKey, secondValue)) + + assert(self.invariant) + } + + convenience init(trieMap: Bitmap, firstNode: BitmapIndexedDictionaryNode) { + self.init() + + self.header = Header(dataMap: 0, trieMap: trieMap) + self.count = firstNode.count + + self.trieBaseAddress.initialize(to: firstNode) + + assert(self.invariant) + } + + convenience init(dataMap: Bitmap, trieMap: Bitmap, firstKey: Key, firstValue: Value, firstNode: BitmapIndexedDictionaryNode) { + self.init() + + self.header = Header(dataMap: dataMap, trieMap: trieMap) + self.count = 1 + firstNode.count + + self.dataBaseAddress.initialize(to: (firstKey, firstValue)) + self.trieBaseAddress.initialize(to: firstNode) + + assert(self.invariant) + } + + convenience init(collisions: [ReturnPayload]) { + self.init(dataCapacity: Capacity(collisions.count), trieCapacity: 0) + + self.header = Header(dataMap: Bitmap(collisions.count), trieMap: Bitmap(collisions.count)) + self.count = collisions.count + + self.dataBaseAddress.initialize(from: collisions, count: collisions.count) + + assert(self.invariant) + } + + func get(_ key: Key, _ keyHash: Int, _ shift: Int) -> Value? { + let mask = maskFrom(keyHash, shift) + let bitpos = bitposFrom(mask) + + guard collisionFree else { + let content: [ReturnPayload] = Array(self) + let hash = computeHash(content.first!.key) + + guard keyHash == hash else { + return nil + } + + return content.first(where: { key == $0.key }).map { $0.value } + } + + guard (dataMap & bitpos) == 0 else { + let index = indexFrom(dataMap, mask, bitpos) + let payload = self.getPayload(index) + return key == payload.key ? payload.value : nil + } + + guard (trieMap & bitpos) == 0 else { + let index = indexFrom(trieMap, mask, bitpos) + return self.getNode(index).get(key, keyHash, shift + bitPartitionSize) + } + + return nil + } + + func containsKey(_ key: Key, _ keyHash: Int, _ shift: Int) -> Bool { + let mask = maskFrom(keyHash, shift) + let bitpos = bitposFrom(mask) + + guard collisionFree else { + let content: [ReturnPayload] = Array(self) + let hash = computeHash(content.first!.key) + + guard keyHash == hash else { + return false + } + + return content.contains(where: { key == $0.key }) + } + + guard (dataMap & bitpos) == 0 else { + let index = indexFrom(dataMap, mask, bitpos) + let payload = self.getPayload(index) + return key == payload.key + } + + guard (trieMap & bitpos) == 0 else { + let index = indexFrom(trieMap, mask, bitpos) + return self.getNode(index).containsKey(key, keyHash, shift + bitPartitionSize) + } + + return false + } + + func index(_ key: Key, _ keyHash: Int, _ shift: Int, _ skippedBefore: Int) -> PersistentDictionaryIndex? { + guard collisionFree else { + let content: [ReturnPayload] = Array(self) + let hash = computeHash(content.first!.key) + + assert(keyHash == hash) + return content.firstIndex(where: { _key, _ in _key == key }).map { PersistentDictionaryIndex(value: $0) } + } + + let mask = maskFrom(keyHash, shift) + let bitpos = bitposFrom(mask) + + let skipped = self.counts.prefix(upTo: mask).reduce(0, +) + + guard (dataMap & bitpos) == 0 else { + let index = indexFrom(dataMap, mask, bitpos) + let payload = self.getPayload(index) + guard key == payload.key else { return nil } + + return PersistentDictionaryIndex(value: skippedBefore + skipped) + } + + guard (trieMap & bitpos) == 0 else { + let index = indexFrom(trieMap, mask, bitpos) + return self.getNode(index).index(key, keyHash, shift + bitPartitionSize, skippedBefore + skipped) + } + + return nil + } + + func get(position: PersistentDictionaryIndex, _ shift: Int, _ stillToSkip: Int) -> ReturnPayload { + var cumulativeCounts = self.counts + + for i in 1 ..< cumulativeCounts.count { + cumulativeCounts[i] += cumulativeCounts[i - 1] + } + + var mask = 0 + + for i in 0 ..< cumulativeCounts.count { + if cumulativeCounts[i] <= stillToSkip { + mask = i + } else { + mask = i + break + } + } + + let skipped = (mask == 0) ? 0 : cumulativeCounts[mask - 1] + + let bitpos = bitposFrom(mask) + + guard (dataMap & bitpos) == 0 else { + let index = indexFrom(dataMap, mask, bitpos) + return self.getPayload(index) + } + + guard (trieMap & bitpos) == 0 else { + let index = indexFrom(trieMap, mask, bitpos) + return self.getNode(index).get(position: position, shift + bitPartitionSize, stillToSkip - skipped) + } + + fatalError("Should not reach here.") + } + + final func updateOrUpdating(_ isStorageKnownUniquelyReferenced: Bool, _ key: Key, _ value: Value, _ keyHash: Int, _ shift: Int, _ effect: inout DictionaryEffect) -> BitmapIndexedDictionaryNode { + + guard collisionFree else { + return updateOrUpdatingCollision(isStorageKnownUniquelyReferenced, key, value, keyHash, shift, &effect) + } + + let mask = maskFrom(keyHash, shift) + let bitpos = bitposFrom(mask) + + guard (dataMap & bitpos) == 0 else { + let index = indexFrom(dataMap, mask, bitpos) + let (key0, value0) = self.getPayload(index) + + if key0 == key { + effect.setReplacedValue(previousValue: value0) + return copyAndSetValue(isStorageKnownUniquelyReferenced, bitpos, value) + } else { + let keyHash0 = computeHash(key0) + + if keyHash0 == keyHash { + let subNodeNew = Self(/* hash, */ collisions: [(key0, value0), (key, value)]) + + effect.setModified() + if self.count == 1 { + return subNodeNew + } else { + return copyAndMigrateFromInlineToNode(isStorageKnownUniquelyReferenced, bitpos, subNodeNew) + } + } else { + let subNodeNew = mergeTwoKeyValPairs(key0, value0, keyHash0, key, value, keyHash, shift + bitPartitionSize) + + effect.setModified() + return copyAndMigrateFromInlineToNode(isStorageKnownUniquelyReferenced, bitpos, subNodeNew) + } + } + } + + guard (trieMap & bitpos) == 0 else { + let index = indexFrom(trieMap, mask, bitpos) + let subNodeModifyInPlace = self.isTrieNodeKnownUniquelyReferenced(index, isStorageKnownUniquelyReferenced) + + let subNode = self.getNode(index) + + let subNodeNew = subNode.updateOrUpdating(subNodeModifyInPlace, key, value, keyHash, shift + bitPartitionSize, &effect) + guard effect.modified && subNode !== subNodeNew else { if effect.previousValue == nil { count += 1 } ; assert(self.invariant) ; return self } + + return copyAndSetTrieNode(isStorageKnownUniquelyReferenced, bitpos, index, subNodeNew, updateCount: { $0 -= subNode.count ; $0 += subNodeNew.count }) + } + + effect.setModified() + return copyAndInsertValue(isStorageKnownUniquelyReferenced, bitpos, key, value) + } + + @inline(never) + final func updateOrUpdatingCollision(_ isStorageKnownUniquelyReferenced: Bool, _ key: Key, _ value: Value, _ keyHash: Int, _ shift: Int, _ effect: inout DictionaryEffect) -> BitmapIndexedDictionaryNode { + assert(hashCollision) + + let content: [ReturnPayload] = Array(self) + let hash = computeHash(content.first!.key) + + guard keyHash == hash else { + effect.setModified() + return mergeKeyValPairAndCollisionNode(key, value, keyHash, self, hash, shift) + } + + if let index = content.firstIndex(where: { key == $0.key }) { + let updatedContent: [ReturnPayload] = content[0..) -> BitmapIndexedDictionaryNode { + + guard collisionFree else { + return removeOrRemovingCollision(isStorageKnownUniquelyReferenced, key, keyHash, shift, &effect) + } + + let mask = maskFrom(keyHash, shift) + let bitpos = bitposFrom(mask) + + guard (dataMap & bitpos) == 0 else { + let index = indexFrom(dataMap, mask, bitpos) + let (key0, value0) = self.getPayload(index) + guard key0 == key else { assert(self.invariant) ; return self } + + effect.setModified(previousValue: value0) + if self.payloadArity == 2 && self.nodeArity == 0 { + if shift == 0 { + // keep remaining pair on root level + let newDataMap = (dataMap ^ bitpos) + let (remainingKey, remainingValue) = getPayload(1 - index) + return Self(dataMap: newDataMap, firstKey: remainingKey, firstValue: remainingValue) + } else { + // create potential new root: will a) become new root, or b) inlined on another level + let newDataMap = bitposFrom(maskFrom(keyHash, 0)) + let (remainingKey, remainingValue) = getPayload(1 - index) + return Self(dataMap: newDataMap, firstKey: remainingKey, firstValue: remainingValue) + } + } else if self.payloadArity == 1 && self.nodeArity == 1, self.getNode(0).hashCollision { + // escalate hash-collision node + return getNode(0) + } else { return copyAndRemoveValue(isStorageKnownUniquelyReferenced, bitpos) } + } + + guard (trieMap & bitpos) == 0 else { + let index = indexFrom(trieMap, mask, bitpos) + let subNodeModifyInPlace = self.isTrieNodeKnownUniquelyReferenced(index, isStorageKnownUniquelyReferenced) + + let subNode = self.getNode(index) + + let subNodeNew = subNode.removeOrRemoving(subNodeModifyInPlace, key, keyHash, shift + bitPartitionSize, &effect) + guard effect.modified && subNode !== subNodeNew else { if effect.modified { count -= 1 } ; assert(self.invariant) ; return self } + + assert(subNodeNew.count > 0, "Sub-node must have at least one element.") + switch subNodeNew.count { + case 1: + if self.isCandiateForCompaction { + // escalate singleton + return subNodeNew + } else { + // inline singleton + return copyAndMigrateFromNodeToInline(isStorageKnownUniquelyReferenced, bitpos, subNodeNew.getPayload(0)) + } + + case _: + if subNodeNew.hashCollision && self.isCandiateForCompaction { + // escalate singleton + return subNodeNew + } else { + // modify current node (set replacement node) + return copyAndSetTrieNode(isStorageKnownUniquelyReferenced, bitpos, index, subNodeNew, updateCount: { $0 -= 1 }) + } + } + } + + return self + } + + @inline(never) + final func removeOrRemovingCollision(_ isStorageKnownUniquelyReferenced: Bool, _ key: Key, _ keyHash: Int, _ shift: Int, _ effect: inout DictionaryEffect) -> BitmapIndexedDictionaryNode { + assert(hashCollision) + + let content: [ReturnPayload] = Array(self) + let _ = computeHash(content.first!.key) + + if let index = content.firstIndex(where: { key == $0.key }) { + effect.setModified(previousValue: content[index].value) + var updatedContent = content; updatedContent.remove(at: index) + assert(updatedContent.count == content.count - 1) + + if updatedContent.count == 1 { + // create potential new root: will a) become new root, or b) inlined on another level + let newDataMap = bitposFrom(maskFrom(keyHash, 0)) + let (remainingKey, remainingValue) = updatedContent.first! + return Self(dataMap: newDataMap, firstKey: remainingKey, firstValue: remainingValue) + } else { + return Self(/* hash, */ collisions: updatedContent) + } + } else { + return self + } + } + + var isCandiateForCompaction: Bool { payloadArity == 0 && nodeArity == 1 } + + func mergeTwoKeyValPairs(_ key0: Key, _ value0: Value, _ keyHash0: Int, _ key1: Key, _ value1: Value, _ keyHash1: Int, _ shift: Int) -> BitmapIndexedDictionaryNode { + assert(keyHash0 != keyHash1) + + let mask0 = maskFrom(keyHash0, shift) + let mask1 = maskFrom(keyHash1, shift) + + if mask0 != mask1 { + // unique prefixes, payload fits on same level + if mask0 < mask1 { + return Self(dataMap: bitposFrom(mask0) | bitposFrom(mask1), firstKey: key0, firstValue: value0, secondKey: key1, secondValue: value1) + } else { + return Self(dataMap: bitposFrom(mask1) | bitposFrom(mask0), firstKey: key1, firstValue: value1, secondKey: key0, secondValue: value0) + } + } else { + // recurse: identical prefixes, payload must be disambiguated deeper in the trie + let node = mergeTwoKeyValPairs(key0, value0, keyHash0, key1, value1, keyHash1, shift + bitPartitionSize) + + return Self(trieMap: bitposFrom(mask0), firstNode: node) + } + } + + func mergeKeyValPairAndCollisionNode(_ key0: Key, _ value0: Value, _ keyHash0: Int, _ node1: BitmapIndexedDictionaryNode, _ nodeHash1: Int, _ shift: Int) -> BitmapIndexedDictionaryNode { + assert(keyHash0 != nodeHash1) + + let mask0 = maskFrom(keyHash0, shift) + let mask1 = maskFrom(nodeHash1, shift) + + if mask0 != mask1 { + // unique prefixes, payload and collision node fit on same level + return Self(dataMap: bitposFrom(mask0), trieMap: bitposFrom(mask1), firstKey: key0, firstValue: value0, firstNode: node1) + } else { + // recurse: identical prefixes, payload must be disambiguated deeper in the trie + let node = mergeKeyValPairAndCollisionNode(key0, value0, keyHash0, node1, nodeHash1, shift + bitPartitionSize) + + return Self(trieMap: bitposFrom(mask0), firstNode: node) + } + } + + private func isTrieNodeKnownUniquelyReferenced(_ slotIndex: Int, _ isParentNodeKnownUniquelyReferenced: Bool) -> Bool { + let isKnownUniquelyReferenced = Swift.isKnownUniquelyReferenced(&trieBaseAddress[slotIndex]) + + return isParentNodeKnownUniquelyReferenced && isKnownUniquelyReferenced + } + + var hasNodes: Bool { header.trieMap != 0 } + + var nodeArity: Int { header.trieCount } + + func getNode(_ index: Int) -> BitmapIndexedDictionaryNode { + return trieBaseAddress[index] + } + + var hasPayload: Bool { header.dataMap != 0 } + + var payloadArity: Int { header.dataCount } + + func getPayload(_ index: Int) -> (key: Key, value: Value) { + dataBaseAddress[index] + } + + private final var counts: [Int] { + var counts = Array(repeating: 0, count: Bitmap.bitWidth) + + zip(header.dataMap.nonzeroBits(), _dataSlice).forEach { (index, _) in + counts[index] = 1 + } + + zip(header.trieMap.nonzeroBits(), _trieSlice).forEach { (index, trieNode) in + counts[index] = trieNode.count + } + + return counts + } + + private final func count(upTo mask: Int) -> Int { + let bitpos = bitposFrom(mask) + + let dataIndex = indexFrom(dataMap, mask, bitpos) + let trieIndex = indexFrom(trieMap, mask, bitpos) + + let count = dataIndex + UnsafeMutableBufferPointer(start: trieBaseAddress, count: header.trieCount).prefix(upTo: trieIndex).map { $0.count }.reduce(0, +) + + assert(count == counts.prefix(upTo: mask).reduce(0, +)) + return count + } + + func dataIndex(_ bitpos: Bitmap) -> Int { (dataMap & (bitpos &- 1)).nonzeroBitCount } + + func trieIndex(_ bitpos: Bitmap) -> Int { (trieMap & (bitpos &- 1)).nonzeroBitCount } + + func copyAndSetValue(_ isStorageKnownUniquelyReferenced: Bool, _ bitpos: Bitmap, _ newValue: Value) -> BitmapIndexedDictionaryNode { + let src: ReturnBitmapIndexedNode = self + let dst: ReturnBitmapIndexedNode + + if isStorageKnownUniquelyReferenced { + dst = src + } else { + dst = src.copy() + } + + let idx = dataIndex(bitpos) + + dst.dataBaseAddress[idx].value = newValue + + assert(src.invariant) + assert(dst.invariant) + return dst + } + + private func copyAndSetTrieNode(_ isStorageKnownUniquelyReferenced: Bool, _ bitpos: Bitmap, _ idx: Int, _ newNode: TrieBufferElement, updateCount: (inout Int) -> Void) -> BitmapIndexedDictionaryNode { + let src: ReturnBitmapIndexedNode = self + let dst: ReturnBitmapIndexedNode + + if isStorageKnownUniquelyReferenced { + dst = src + } else { + dst = src.copy() + } + + dst.trieBaseAddress[idx] = newNode + + // update metadata: `dataMap, nodeMap, collMap` + updateCount(&dst.count) + + assert(src.invariant) + assert(dst.invariant) + return dst + } + + func copyAndInsertValue(_ isStorageKnownUniquelyReferenced: Bool, _ bitpos: Bitmap, _ key: Key, _ value: Value) -> BitmapIndexedDictionaryNode { + let src: ReturnBitmapIndexedNode = self + let dst: ReturnBitmapIndexedNode + + let hasRoomForData = header.dataCount < dataCapacity + + if isStorageKnownUniquelyReferenced && hasRoomForData { + dst = src + } else { + dst = src.copy(withDataCapacityFactor: hasRoomForData ? 1 : 2) + } + + let dataIdx = indexFrom(dataMap, bitpos) + rangeInsert((key, value), at: dataIdx, into: dst.dataBaseAddress, count: dst.header.dataCount) + + // update metadata: `dataMap | bitpos, nodeMap, collMap` + dst.header.dataMap |= bitpos + dst.count += 1 + + assert(src.invariant) + assert(dst.invariant) + return dst + } + + func copyAndRemoveValue(_ isStorageKnownUniquelyReferenced: Bool, _ bitpos: Bitmap) -> BitmapIndexedDictionaryNode { + let src: ReturnBitmapIndexedNode = self + let dst: ReturnBitmapIndexedNode + + if isStorageKnownUniquelyReferenced { + dst = src + } else { + dst = src.copy() + } + + let dataIdx = indexFrom(dataMap, bitpos) + rangeRemove(at: dataIdx, from: dst.dataBaseAddress, count: dst.header.dataCount) + + // update metadata: `dataMap ^ bitpos, nodeMap, collMap` + dst.header.dataMap ^= bitpos + dst.count -= 1 + + assert(src.invariant) + assert(dst.invariant) + return dst + } + + func copyAndMigrateFromInlineToNode(_ isStorageKnownUniquelyReferenced: Bool, _ bitpos: Bitmap, _ node: TrieBufferElement) -> BitmapIndexedDictionaryNode { + let src: ReturnBitmapIndexedNode = self + let dst: ReturnBitmapIndexedNode + + let hasRoomForTrie = header.trieCount < trieCapacity + + if isStorageKnownUniquelyReferenced && hasRoomForTrie { + dst = src + } else { + // TODO reconsider the details of the heuristic + // + // Since copying is necessary, check if the data section can be reduced. + // Keep at mininum the initial capacity. + // + // Notes currently can grow to a maximum size of 48 (tuple and sub-node) slots. + let tooMuchForData = Swift.max(header.dataCount * 2 - 1, 4) < dataCapacity + + dst = src.copy(withDataCapacityShrinkFactor: tooMuchForData ? 2 : 1, withTrieCapacityFactor: hasRoomForTrie ? 1 : 2) + } + + let dataIdx = indexFrom(dataMap, bitpos) + rangeRemove(at: dataIdx, from: dst.dataBaseAddress, count: dst.header.dataCount) + + let trieIdx = indexFrom(trieMap, bitpos) + rangeInsert(node, at: trieIdx, into: dst.trieBaseAddress, count: dst.header.trieCount) + + // update metadata: `dataMap ^ bitpos, nodeMap | bitpos, collMap` + dst.header.dataMap ^= bitpos + dst.header.trieMap |= bitpos + dst.count += 1 // assuming that `node.count == 2` + + assert(src.invariant) + assert(dst.invariant) + return dst + } + + func copyAndMigrateFromNodeToInline(_ isStorageKnownUniquelyReferenced: Bool, _ bitpos: Bitmap, _ tuple: (key: Key, value: Value)) -> BitmapIndexedDictionaryNode { + let src: ReturnBitmapIndexedNode = self + let dst: ReturnBitmapIndexedNode + + let hasRoomForData = header.dataCount < dataCapacity + + if isStorageKnownUniquelyReferenced && hasRoomForData { + dst = src + } else { + dst = src.copy(withDataCapacityFactor: hasRoomForData ? 1 : 2) + } + + let nodeIdx = indexFrom(trieMap, bitpos) + rangeRemove(at: nodeIdx, from: dst.trieBaseAddress, count: dst.header.trieCount) + + let dataIdx = indexFrom(dataMap, bitpos) + rangeInsert(tuple, at: dataIdx, into: dst.dataBaseAddress, count: dst.header.dataCount) + + // update metadata: `dataMap | bitpos, nodeMap ^ bitpos, collMap` + dst.header.dataMap |= bitpos + dst.header.trieMap ^= bitpos + dst.count -= 1 // assuming that updated `node.count == 1` + + assert(src.invariant) + assert(dst.invariant) + return dst + } +} + +// TODO: `Equatable` needs more test coverage, apart from hash-collision smoke test +extension BitmapIndexedDictionaryNode: Equatable where Value: Equatable { + static func == (lhs: BitmapIndexedDictionaryNode, rhs: BitmapIndexedDictionaryNode) -> Bool { + if lhs.hashCollision && rhs.hashCollision { + return Dictionary.init(uniqueKeysWithValues: Array(lhs)) == Dictionary.init(uniqueKeysWithValues: Array(rhs)) + } + + return lhs === rhs || + lhs.header == rhs.header && + lhs.count == rhs.count && + deepContentEquality(lhs, rhs) + } + + private static func deepContentEquality(_ lhs: BitmapIndexedDictionaryNode, _ rhs: BitmapIndexedDictionaryNode) -> Bool { + guard lhs.header == rhs.header else { return false } + + for index in 0.. DictionaryKeyValueTupleIterator { + return DictionaryKeyValueTupleIterator(rootNode: self) + } +} + +struct Header: Equatable { + var dataMap: Bitmap + var trieMap: Bitmap + + var dataCount: Int { hashCollision ? Int(dataMap) : dataMap.nonzeroBitCount } + var trieCount: Int { hashCollision ? 0 : trieMap.nonzeroBitCount } + + var hashCollision: Bool { + (dataMap & trieMap) != 0 + } + + static func == (lhs: Header, rhs: Header) -> Bool { + lhs.dataMap == rhs.dataMap && lhs.trieMap == rhs.trieMap + } +} diff --git a/Sources/PersistentCollections/_Common.swift b/Sources/PersistentCollections/_Common.swift new file mode 100644 index 000000000..2cc979d1a --- /dev/null +++ b/Sources/PersistentCollections/_Common.swift @@ -0,0 +1,295 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2019 - 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 +// +//===----------------------------------------------------------------------===// + +func computeHash(_ value: T) -> Int { + value.hashValue +} + +typealias Bitmap = UInt32 + +extension Bitmap { + public func nonzeroBits() -> NonzeroBits { + return NonzeroBits(from: self) + } + + public func zeroBits() -> NonzeroBits { + return NonzeroBits(from: ~self) + } +} + +public struct NonzeroBits: Sequence, IteratorProtocol, CustomStringConvertible where Bitmap: BinaryInteger { + var bitmap: Bitmap + + init(from bitmap: Bitmap) { + self.bitmap = bitmap + } + + public mutating func next() -> Int? { + guard bitmap != 0 else { return nil } + + let index = bitmap.trailingZeroBitCount + bitmap ^= 1 << index + + return index + } + + public var description: String { + "[\(self.map { $0.description }.joined(separator: ", "))]" + } +} + +let bitPartitionSize: Int = 5 + +let bitPartitionMask: Int = (1 << bitPartitionSize) - 1 + +typealias Capacity = UInt32 // TODO: restore type to `UInt8` after reworking hash-collisions to grow in depth instead of width + +let hashCodeLength: Int = Int.bitWidth + +let maxDepth = Int((Double(hashCodeLength) / Double(bitPartitionSize)).rounded(.up)) + +func maskFrom(_ hash: Int, _ shift: Int) -> Int { + (hash >> shift) & bitPartitionMask +} + +func bitposFrom(_ mask: Int) -> Bitmap { + 1 << mask +} + +func indexFrom(_ bitmap: Bitmap, _ bitpos: Bitmap) -> Int { + (bitmap & (bitpos &- 1)).nonzeroBitCount +} + +func indexFrom(_ bitmap: Bitmap, _ mask: Int, _ bitpos: Bitmap) -> Int { + (bitmap == Bitmap.max) ? mask : indexFrom(bitmap, bitpos) +} + +protocol Node: AnyObject { + associatedtype ReturnPayload + associatedtype ReturnBitmapIndexedNode: Node + + var hasNodes: Bool { get } + + var nodeArity: Int { get } + + func getNode(_ index: Int) -> Self + + var hasPayload: Bool { get } + + var payloadArity: Int { get } + + func getPayload(_ index: Int) -> ReturnPayload + + var count: Int { get } +} + +/// +/// Base class for fixed-stack iterators that traverse a hash-trie. The iterator performs a +/// depth-first pre-order traversal, which yields first all payload elements of the current +/// node before traversing sub-nodes (left to right). +/// +struct BaseIterator { + var currentValueCursor: Int = 0 + var currentValueLength: Int = 0 + var currentValueNode: T? = nil + + private var currentStackLevel: Int = -1 + private var nodeCursorsAndLengths: [Int] = Array(repeating: 0, count: maxDepth * 2) + private var nodes: [T?] = Array(repeating: nil, count: maxDepth) + + init(rootNode: T) { + if rootNode.hasNodes { pushNode(rootNode) } + if rootNode.hasPayload { setupPayloadNode(rootNode) } + } + + private mutating func setupPayloadNode(_ node: T) { + currentValueNode = node + currentValueCursor = 0 + currentValueLength = node.payloadArity + } + + private mutating func pushNode(_ node: T) { + currentStackLevel = currentStackLevel + 1 + + let cursorIndex = currentStackLevel * 2 + let lengthIndex = currentStackLevel * 2 + 1 + + nodes[currentStackLevel] = node + nodeCursorsAndLengths[cursorIndex] = 0 + nodeCursorsAndLengths[lengthIndex] = node.nodeArity + } + + private mutating func popNode() { + currentStackLevel = currentStackLevel - 1 + } + + /// + /// Searches for next node that contains payload values, + /// and pushes encountered sub-nodes on a stack for depth-first traversal. + /// + private mutating func searchNextValueNode() -> Bool { + while currentStackLevel >= 0 { + let cursorIndex = currentStackLevel * 2 + let lengthIndex = currentStackLevel * 2 + 1 + + let nodeCursor = nodeCursorsAndLengths[cursorIndex] + let nodeLength = nodeCursorsAndLengths[lengthIndex] + + if nodeCursor < nodeLength { + nodeCursorsAndLengths[cursorIndex] += 1 + + let currentNode = nodes[currentStackLevel]! + let nextNode = currentNode.getNode(nodeCursor) + + if nextNode.hasNodes { pushNode(nextNode) } + if nextNode.hasPayload { setupPayloadNode(nextNode) ; return true } + } else { + popNode() + } + } + + return false + } + + mutating func hasNext() -> Bool { + return (currentValueCursor < currentValueLength) || searchNextValueNode() + } +} + +/// +/// Base class for fixed-stack iterators that traverse a hash-trie in reverse order. The base +/// iterator performs a depth-first post-order traversal, traversing sub-nodes (right to left). +/// +struct BaseReverseIterator { + var currentValueCursor: Int = -1 + var currentValueNode: T? = nil + + private var currentStackLevel: Int = -1 + private var nodeIndex: [Int] = Array(repeating: 0, count: maxDepth + 1) + private var nodeStack: [T?] = Array(repeating: nil, count: maxDepth + 1) + + init(rootNode: T) { + pushNode(rootNode) + searchNextValueNode() + } + + private mutating func setupPayloadNode(_ node: T) { + currentValueNode = node + currentValueCursor = node.payloadArity - 1 + } + + private mutating func pushNode(_ node: T) { + currentStackLevel = currentStackLevel + 1 + + nodeStack[currentStackLevel] = node + nodeIndex[currentStackLevel] = node.nodeArity - 1 + } + + private mutating func popNode() { + currentStackLevel = currentStackLevel - 1 + } + + /// + /// Searches for rightmost node that contains payload values, + /// and pushes encountered sub-nodes on a stack for depth-first traversal. + /// + @discardableResult + private mutating func searchNextValueNode() -> Bool { + while currentStackLevel >= 0 { + let nodeCursor = nodeIndex[currentStackLevel] ; nodeIndex[currentStackLevel] = nodeCursor - 1 + + if nodeCursor >= 0 { + let currentNode = nodeStack[currentStackLevel]! + let nextNode = currentNode.getNode(nodeCursor) + pushNode(nextNode) + } else { + let currNode = nodeStack[currentStackLevel]! + popNode() + + if currNode.hasPayload { setupPayloadNode(currNode) ; return true } + } + } + + return false + } + + mutating func hasNext() -> Bool { + return (currentValueCursor >= 0) || searchNextValueNode() + } +} + +func rangeInsert(_ element: T, at index: Int, intoRange range: Range>) { + let seq = range.dropFirst(index) + + let src = seq.startIndex + let dst = src.successor() + + dst.moveInitialize(from: src, count: seq.count) + + src.initialize(to: element) +} + +// NEW +@inlinable +@inline(__always) +func rangeInsert(_ element: T, at index: Int, into baseAddress: UnsafeMutablePointer, count: Int) { + let src = baseAddress.advanced(by: index) + let dst = src.successor() + + dst.moveInitialize(from: src, count: count - index) + + src.initialize(to: element) +} + +// `index` is the logical index starting at the rear, indexing to the left +func rangeInsertReversed(_ element: T, at index: Int, intoRange range: Range>) { + let seq = range.dropLast(index) + + let src = seq.startIndex + let dst = src.predecessor() + + dst.moveInitialize(from: src, count: seq.count) + + // requires call to predecessor on "past the end" position + seq.endIndex.predecessor().initialize(to: element) +} + +func rangeRemove(at index: Int, fromRange range: Range>) { + let seq = range.dropFirst(index + 1) + + let src = seq.startIndex + let dst = src.predecessor() + + dst.deinitialize(count: 1) + dst.moveInitialize(from: src, count: seq.count) +} + +// NEW +@inlinable +@inline(__always) +func rangeRemove(at index: Int, from baseAddress: UnsafeMutablePointer, count: Int) { + let src = baseAddress.advanced(by: index + 1) + let dst = src.predecessor() + + dst.deinitialize(count: 1) + dst.moveInitialize(from: src, count: count - index - 1) +} + +// `index` is the logical index starting at the rear, indexing to the left +func rangeRemoveReversed(at index: Int, fromRange range: Range>) { + let seq = range.dropLast(index + 1) + + let src = seq.startIndex + let dst = src.successor() + + seq.endIndex.deinitialize(count: 1) + dst.moveInitialize(from: src, count: seq.count) +} diff --git a/Sources/PersistentCollections/_DictionaryEffect.swift b/Sources/PersistentCollections/_DictionaryEffect.swift new file mode 100644 index 000000000..c4f0541d9 --- /dev/null +++ b/Sources/PersistentCollections/_DictionaryEffect.swift @@ -0,0 +1,29 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2019 - 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 +// +//===----------------------------------------------------------------------===// + +struct DictionaryEffect { + var modified: Bool = false + var previousValue: Value? + + mutating func setModified() { + self.modified = true + } + + mutating func setModified(previousValue: Value) { + self.modified = true + self.previousValue = previousValue + } + + mutating func setReplacedValue(previousValue: Value) { + self.modified = true + self.previousValue = previousValue + } +} diff --git a/Sources/PersistentCollections/_DictionaryNode.swift b/Sources/PersistentCollections/_DictionaryNode.swift new file mode 100644 index 000000000..c87727020 --- /dev/null +++ b/Sources/PersistentCollections/_DictionaryNode.swift @@ -0,0 +1,25 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2019 - 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 +// +//===----------------------------------------------------------------------===// + +protocol DictionaryNode: Node { + associatedtype Key: Hashable + associatedtype Value + + func get(_ key: Key, _ hash: Int, _ shift: Int) -> Value? + + func containsKey(_ key: Key, _ hash: Int, _ shift: Int) -> Bool + + func index(_ key: Key, _ hash: Int, _ shift: Int, _ skippedBefore: Int) -> PersistentDictionaryIndex? + + func updateOrUpdating(_ isStorageKnownUniquelyReferenced: Bool, _ key: Key, _ value: Value, _ hash: Int, _ shift: Int, _ effect: inout DictionaryEffect) -> ReturnBitmapIndexedNode + + func removeOrRemoving(_ isStorageKnownUniquelyReferenced: Bool, _ key: Key, _ hash: Int, _ shift: Int, _ effect: inout DictionaryEffect) -> ReturnBitmapIndexedNode +} diff --git a/Tests/PersistentCollectionsTests/PersistentCollections Smoke Tests.swift b/Tests/PersistentCollectionsTests/PersistentCollections Smoke Tests.swift new file mode 100644 index 000000000..995a88ef2 --- /dev/null +++ b/Tests/PersistentCollectionsTests/PersistentCollections Smoke Tests.swift @@ -0,0 +1,580 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2019 - 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 _CollectionsTestSupport +@testable import PersistentCollections + +final class CapsuleSmokeTests: CollectionTestCase { + func testSubscriptAdd() { + var map: PersistentDictionary = [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: PersistentDictionary = [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: PersistentDictionary = [1: "a", 2: "b"] + + map[1] = nil + map[2] = nil + + expectEqual(map.count, 0) + expectEqual(map[1], nil) + expectEqual(map[2], nil) + } + + func testTriggerOverwrite1() { + let map: PersistentDictionary = [1: "a", 2: "b"] + + _ = map + .updatingValue("x", forKey: 1) // triggers COW + .updatingValue("y", forKey: 2) // triggers COW + + var res1: PersistentDictionary = [:] + res1.updateValue("a", forKey: 1) // in-place + res1.updateValue("b", forKey: 2) // in-place + + var res2: PersistentDictionary = [:] + res2[1] = "a" // in-place + res2[2] = "b" // in-place + + var res3: PersistentDictionary = 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: PersistentDictionary = [:] + res1.updateValue("a", forKey: CollidableInt(10, 01)) // in-place + res1.updateValue("a", forKey: CollidableInt(11, 33)) // in-place + res1.updateValue("b", forKey: CollidableInt(20, 02)) // in-place + + res1.updateValue("x", forKey: CollidableInt(10, 01)) // in-place + res1.updateValue("x", forKey: CollidableInt(11, 33)) // in-place + res1.updateValue("y", forKey: CollidableInt(20, 02)) // in-place + + var res2: PersistentDictionary = res1 + res2.updateValue("a", forKey: CollidableInt(10, 01)) // triggers COW + res2.updateValue("a", forKey: CollidableInt(11, 33)) // in-place + res2.updateValue("b", forKey: CollidableInt(20, 02)) // in-place + + expectEqual(res1[CollidableInt(10, 01)], "x") + expectEqual(res1[CollidableInt(11, 33)], "x") + expectEqual(res1[CollidableInt(20, 02)], "y") + + expectEqual(res2[CollidableInt(10, 01)], "a") + expectEqual(res2[CollidableInt(11, 33)], "a") + expectEqual(res2[CollidableInt(20, 02)], "b") + + } + + func testTriggerOverwrite3() { + let upperBound = 1_000 + + // Populating `map1` + var map1: PersistentDictionary = [:] + 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: PersistentDictionary = [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: PersistentDictionary = [:] + + var res12 = map + res12[CollidableInt(1, 1)] = CollidableInt(1, 1) + res12[CollidableInt(2, 1)] = CollidableInt(2, 1) + + var res13 = map + res13[CollidableInt(1, 1)] = CollidableInt(1, 1) + res13[CollidableInt(3, 1)] = CollidableInt(3, 1) + + var res31 = map + res31[CollidableInt(3, 1)] = CollidableInt(3, 1) + res31[CollidableInt(1, 1)] = CollidableInt(1, 1) + + expectEqual(res13, res31) + expectNotEqual(res13, res12) + expectNotEqual(res31, res12) + } + + func testCountForCopyOnWriteInsertion() { + let map = PersistentDictionary.init([CollidableInt(1): CollidableInt(1), CollidableInt(32769_1, 32769): CollidableInt(32769_1, 32769), CollidableInt(32769_2, 32769): CollidableInt(32769_2, 32769)]) + expectEqual(map.rootNode.count, 3) + expectEqual(map.rootNode.reduce(0, { count, _ in count + 1 }), 3) + expectTrue(map.rootNode.invariant) + } + + func testCountForCopyOnWriteDeletion() { + var map: PersistentDictionary = [:] + + map[CollidableInt(32769)] = CollidableInt(32769) + map[CollidableInt(11, 1)] = CollidableInt(11, 1) + map[CollidableInt(12, 1)] = CollidableInt(12, 1) + map[CollidableInt(33, 33)] = CollidableInt(33, 33) + map[CollidableInt(11, 1)] = nil + map[CollidableInt(12, 1)] = nil + expectEqual(map.rootNode.count, 2) + expectEqual(map.rootNode.reduce(0, { count, _ in count + 1 }), 2) + expectTrue(map.rootNode.invariant) + } + + func testCompactionWhenDeletingFromHashCollisionNode1() { + let map: PersistentDictionary = [:] + + + var res1 = map + res1[CollidableInt(11, 1)] = CollidableInt(11, 1) + res1[CollidableInt(12, 1)] = CollidableInt(12, 1) + + expectTrue(res1.contains(CollidableInt(11, 1))) + expectTrue(res1.contains(CollidableInt(12, 1))) + + expectEqual(res1.count, 2) + expectEqual(PersistentDictionary.init([CollidableInt(11, 1): CollidableInt(11, 1), CollidableInt(12, 1): CollidableInt(12, 1)]), res1) + + + var res2 = res1 + res2[CollidableInt(12, 1)] = nil + + expectTrue(res2.contains(CollidableInt(11, 1))) + expectFalse(res2.contains(CollidableInt(12, 1))) + + expectEqual(res2.count, 1) + expectEqual(PersistentDictionary.init([CollidableInt(11, 1): CollidableInt(11, 1)]), res2) + + + var res3 = res1 + res3[CollidableInt(11, 1)] = nil + + expectFalse(res3.contains(CollidableInt(11, 1))) + expectTrue(res3.contains(CollidableInt(12, 1))) + + expectEqual(res3.count, 1) + expectEqual(PersistentDictionary.init([CollidableInt(12, 1): CollidableInt(12, 1)]), res3) + + + var resX = res1 + resX[CollidableInt(32769)] = CollidableInt(32769) + resX[CollidableInt(12, 1)] = nil + + expectTrue(resX.contains(CollidableInt(11, 1))) + expectFalse(resX.contains(CollidableInt(12, 1))) + expectTrue(resX.contains(CollidableInt(32769))) + + expectEqual(resX.count, 2) + expectEqual(PersistentDictionary.init([CollidableInt(11, 1): CollidableInt(11, 1), CollidableInt(32769): CollidableInt(32769)]), resX) + + + var resY = res1 + resY[CollidableInt(32769)] = CollidableInt(32769) + resY[CollidableInt(32769)] = nil + + expectTrue(resY.contains(CollidableInt(11, 1))) + expectTrue(resY.contains(CollidableInt(12, 1))) + expectFalse(resY.contains(CollidableInt(32769))) + + expectEqual(resY.count, 2) + expectEqual(PersistentDictionary.init([CollidableInt(11, 1): CollidableInt(11, 1), CollidableInt(12, 1): CollidableInt(12, 1)]), resY) + } + + func testCompactionWhenDeletingFromHashCollisionNode2() { + let map: PersistentDictionary = [:] + + + var res1 = map + res1[CollidableInt(32769_1, 32769)] = CollidableInt(32769_1, 32769) + res1[CollidableInt(32769_2, 32769)] = CollidableInt(32769_2, 32769) + + expectTrue(res1.contains(CollidableInt(32769_1, 32769))) + expectTrue(res1.contains(CollidableInt(32769_2, 32769))) + + expectEqual(res1.count, 2) + expectEqual(PersistentDictionary.init([CollidableInt(32769_1, 32769): CollidableInt(32769_1, 32769), CollidableInt(32769_2, 32769): CollidableInt(32769_2, 32769)]), res1) + + + var res2 = res1 + res2[CollidableInt(1)] = CollidableInt(1) + + expectTrue(res2.contains(CollidableInt(1))) + expectTrue(res2.contains(CollidableInt(32769_1, 32769))) + expectTrue(res2.contains(CollidableInt(32769_2, 32769))) + + expectEqual(res2.count, 3) + expectEqual(PersistentDictionary.init([CollidableInt(1): CollidableInt(1), CollidableInt(32769_1, 32769): CollidableInt(32769_1, 32769), CollidableInt(32769_2, 32769): CollidableInt(32769_2, 32769)]), res2) + + + var res3 = res2 + res3[CollidableInt(32769_2, 32769)] = nil + + expectTrue(res3.contains(CollidableInt(1))) + expectTrue(res3.contains(CollidableInt(32769_1, 32769))) + + expectEqual(res3.count, 2) + expectEqual(PersistentDictionary.init([CollidableInt(1): CollidableInt(1), CollidableInt(32769_1, 32769): CollidableInt(32769_1, 32769)]), res3) + } + + func testCompactionWhenDeletingFromHashCollisionNode3() { + let map: PersistentDictionary = [:] + + + var res1 = map + res1[CollidableInt(32769_1, 32769)] = CollidableInt(32769_1, 32769) + res1[CollidableInt(32769_2, 32769)] = CollidableInt(32769_2, 32769) + + expectTrue(res1.contains(CollidableInt(32769_1, 32769))) + expectTrue(res1.contains(CollidableInt(32769_2, 32769))) + + expectEqual(res1.count, 2) + expectEqual(PersistentDictionary.init([CollidableInt(32769_1, 32769): CollidableInt(32769_1, 32769), CollidableInt(32769_2, 32769): CollidableInt(32769_2, 32769)]), res1) + + + var res2 = res1 + res2[CollidableInt(1)] = CollidableInt(1) + + expectTrue(res2.contains(CollidableInt(1))) + expectTrue(res2.contains(CollidableInt(32769_1, 32769))) + expectTrue(res2.contains(CollidableInt(32769_2, 32769))) + + expectEqual(res2.count, 3) + expectEqual(PersistentDictionary.init([CollidableInt(1): CollidableInt(1), CollidableInt(32769_1, 32769): CollidableInt(32769_1, 32769), CollidableInt(32769_2, 32769): CollidableInt(32769_2, 32769)]), res2) + + + var res3 = res2 + res3[CollidableInt(1)] = nil + + expectTrue(res3.contains(CollidableInt(32769_1, 32769))) + expectTrue(res3.contains(CollidableInt(32769_2, 32769))) + + expectEqual(res3.count, 2) + expectEqual(PersistentDictionary.init([CollidableInt(32769_1, 32769): CollidableInt(32769_1, 32769), CollidableInt(32769_2, 32769): CollidableInt(32769_2, 32769)]), res3) + + + expectEqual(res1, res3) + } + + func testCompactionWhenDeletingFromHashCollisionNode4() { + let map: PersistentDictionary = [:] + + + var res1 = map + res1[CollidableInt(32769_1, 32769)] = CollidableInt(32769_1, 32769) + res1[CollidableInt(32769_2, 32769)] = CollidableInt(32769_2, 32769) + + expectTrue(res1.contains(CollidableInt(32769_1, 32769))) + expectTrue(res1.contains(CollidableInt(32769_2, 32769))) + + expectEqual(res1.count, 2) + expectEqual(PersistentDictionary.init([CollidableInt(32769_1, 32769): CollidableInt(32769_1, 32769), CollidableInt(32769_2, 32769): CollidableInt(32769_2, 32769)]), res1) + + + var res2 = res1 + res2[CollidableInt(5)] = CollidableInt(5) + + expectTrue(res2.contains(CollidableInt(5))) + expectTrue(res2.contains(CollidableInt(32769_1, 32769))) + expectTrue(res2.contains(CollidableInt(32769_2, 32769))) + + expectEqual(res2.count, 3) + expectEqual(PersistentDictionary.init([CollidableInt(5): CollidableInt(5), CollidableInt(32769_1, 32769): CollidableInt(32769_1, 32769), CollidableInt(32769_2, 32769): CollidableInt(32769_2, 32769)]), res2) + + + var res3 = res2 + res3[CollidableInt(5)] = nil + + expectTrue(res3.contains(CollidableInt(32769_1, 32769))) + expectTrue(res3.contains(CollidableInt(32769_2, 32769))) + + expectEqual(res3.count, 2) + expectEqual(PersistentDictionary.init([CollidableInt(32769_1, 32769): CollidableInt(32769_1, 32769), CollidableInt(32769_2, 32769): CollidableInt(32769_2, 32769)]), res3) + + + expectEqual(res1, res3) + } + + func testCompactionWhenDeletingFromHashCollisionNode5() { + let map: PersistentDictionary = [:] + + + var res1 = map + res1[CollidableInt(1)] = CollidableInt(1) + res1[CollidableInt(1026)] = CollidableInt(1026) + res1[CollidableInt(32770_1, 32770)] = CollidableInt(32770_1, 32770) + res1[CollidableInt(32770_2, 32770)] = CollidableInt(32770_2, 32770) + + expectTrue(res1.contains(CollidableInt(1))) + expectTrue(res1.contains(CollidableInt(1026))) + expectTrue(res1.contains(CollidableInt(32770_1, 32770))) + expectTrue(res1.contains(CollidableInt(32770_2, 32770))) + + expectEqual(res1.count, 4) + expectEqual(PersistentDictionary.init([CollidableInt(1): CollidableInt(1), CollidableInt(1026): CollidableInt(1026), CollidableInt(32770_1, 32770): CollidableInt(32770_1, 32770), CollidableInt(32770_2, 32770): CollidableInt(32770_2, 32770)]), res1) + + + var res2 = res1 + res2[CollidableInt(1026)] = nil + + expectTrue(res2.contains(CollidableInt(1))) + expectFalse(res2.contains(CollidableInt(1026))) + expectTrue(res2.contains(CollidableInt(32770_1, 32770))) + expectTrue(res2.contains(CollidableInt(32770_2, 32770))) + + expectEqual(res2.count, 3) + expectEqual(PersistentDictionary.init([CollidableInt(1): CollidableInt(1), CollidableInt(32770_1, 32770): CollidableInt(32770_1, 32770), CollidableInt(32770_2, 32770): CollidableInt(32770_2, 32770)]), res2) + } + + func inferSize(_ map: PersistentDictionary) -> Int { + var size = 0 + + for _ in map { + size += 1 + } + + return size + } + + func testIteratorEnumeratesAllIfCollision() { + let upperBound = 1_000 + + // '+' prefixed values + var map1: PersistentDictionary = [:] + for index in 0.. = map1 + for index in 0.. = map1 + for index in 0.. = [:] + for index in 0..(_ map1: PersistentDictionary) { + var count = 0 + for _ in map1 { + count = count + 1 + } + expectEqual(map1.count, count) + } + + func testIteratorEnumeratesAll() { + let map1: PersistentDictionary = [ + CollidableInt(11, 1): "a", + CollidableInt(12, 1): "a", + CollidableInt(32769): "b" + ] + + var map2: PersistentDictionary = [:] + for (key, value) in map1 { + map2[key] = value + } + + expectEqual(map1, map2) + } + + func test_indexForKey_hashCollision() { + let map: PersistentDictionary = [ + CollidableInt(11, 1): "a", + CollidableInt(12, 1): "a", + CollidableInt(32769): "b" + ] + + expectEqual(map.index(forKey: CollidableInt(11, 1)), PersistentDictionaryIndex(value: 0)) + expectEqual(map.index(forKey: CollidableInt(12, 1)), PersistentDictionaryIndex(value: 1)) + expectEqual(map.index(forKey: CollidableInt(32769)), PersistentDictionaryIndex(value: 2)) + expectNil(map.index(forKey: CollidableInt(13, 1))) + } + + func test_indexForKey_exhaustIndices() { + var map: PersistentDictionary = [:] + + let range = 0 ..< 10_000 + + for value in range { + map[CollidableInt(value)] = value + } + + var expectedPositions = Set(range) + + for expectedValue in range { + let position = map.index(forKey: CollidableInt(expectedValue))! + let actualValue = map[position].value + + expectEqual(expectedValue, actualValue) + + expectedPositions.remove(position.value) + } + + expectTrue(expectedPositions.isEmpty) + } +} + +final class BitmapSmokeTests: CollectionTestCase { + func test_BitPartitionSize_isValid() { + expectTrue(bitPartitionSize > 0) + expectTrue((2 << (bitPartitionSize - 1)) != 0) + expectTrue((2 << (bitPartitionSize - 1)) <= Bitmap.bitWidth) + } + + func test_Capacity_isValid() { + expectTrue(Int(2 << bitPartitionSize) <= Int(Capacity.max)) + } + + func test_Bitmap_nonzeroBits() { + let bitmap: Bitmap = 0b0100_0000_0000_1011 + + expectEqual(Array(bitmap.nonzeroBits()), [0, 1, 3, 14]) + expectEqual(Array(bitmap.zeroBits()), (0 ..< Bitmap.bitWidth).filter { ![0, 1, 3, 14].contains($0) }) + } + + func test_Bitmap_nonzeroBitsToArray() { + let bitmap: Bitmap = 0b0100_0000_0000_1011 + + let counts = bitmap.nonzeroBits().reduce( + into: Array(repeating: 0, count: Bitmap.bitWidth), { counts, index in counts[index] = 1 }) + + expectEqual(counts.count, Bitmap.bitWidth) + expectEqual(counts.reduce(0, +), bitmap.nonzeroBitCount) + expectEqual(counts.reduce(0, +), 4) + expectEqual(counts[0], 1) + expectEqual(counts[1], 1) + expectEqual(counts[3], 1) + expectEqual(counts[14], 1) + } + + func test_Bitmap_enumerateCompactedArray() { + let bitmap: Bitmap = 0b0100_0000_0000_1011 + let elements: [String] = ["zero", "one", "three", "fourteen"] + + var zipIterator = zip(bitmap.nonzeroBits(), elements).makeIterator() + + expectEqual(zipIterator.next()!, (0, "zero")) + expectEqual(zipIterator.next()!, (1, "one")) + expectEqual(zipIterator.next()!, (3, "three")) + expectEqual(zipIterator.next()!, (14, "fourteen")) + expectNil(zipIterator.next()) + } +} diff --git a/Tests/PersistentCollectionsTests/PersistentCollections Tests.swift b/Tests/PersistentCollectionsTests/PersistentCollections Tests.swift new file mode 100644 index 000000000..04c7030a4 --- /dev/null +++ b/Tests/PersistentCollectionsTests/PersistentCollections Tests.swift @@ -0,0 +1,1334 @@ +//===----------------------------------------------------------------------===// +// +// 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 _CollectionsTestSupport +@testable import PersistentCollections + +class PersistentDictionaryTests: CollectionTestCase { + func test_empty() { + let d = PersistentDictionary() + expectEqualElements(d, []) + expectEqual(d.count, 0) + } + +// func test_uniqueKeysWithValues_Dictionary() { +// let items: Dictionary = [ +// "zero": 0, +// "one": 1, +// "two": 2, +// "three": 3, +// ] +// let d = PersistentDictionary(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 = PersistentDictionary(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 = PersistentDictionary(uniqueKeysWithValues: items) + expectEqualElements(d.sorted(by: <), items.sorted(by: <)) + } + + func test_uniqueKeys_values() { + let items: [(key: String, value: Int)] = [ + (key: "zero", value: 0), + (key: "one", value: 1), + (key: "two", value: 2), + (key: "three", value: 3) + ] + let d = PersistentDictionary(uniqueKeys: ["zero", "one", "two", "three"], values: [0, 1, 2, 3]) + 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 = PersistentDictionary(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 = PersistentDictionary(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 = PersistentDictionary(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 = PersistentDictionary(uniqueKeysWithValues: items) +// expectEqualElements(d, items) +// } + +// func test_uniqueKeysWithValues_unlabeled_tuples() { +// let items: [(String, Int)] = [ +// ("zero", 0), +// ("one", 1), +// ("two", 2), +// ("three", 3), +// ] +// let d = PersistentDictionary(uniqueKeysWithValues: items) +// expectEqualElements(d, items) +// } + + func test_ExpressibleByDictionaryLiteral() { + let d0: PersistentDictionary = [:] + expectTrue(d0.isEmpty) + + let d1: PersistentDictionary = [ + "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: PersistentDictionary = [ +// "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.persistentDictionary(keys: 0 ..< count) + expectEqual(d.isEmpty, count == 0) + expectEqual(d.count, count) + expectEqual(d.underestimatedCount, count) + } + } + } + + // TODO: determine how to best calculate the expected order of the hash-trie 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.persistentDictionary(keys: 0 ..< count) + withEvery("offset", in: 0 ..< count) { offset in + expectEqual(d.index(forKey: d.keys[offset]), PersistentDictionaryIndex(value: offset)) // NOTE: uses the actual order `d.keys` + } + expectNil(d.index(forKey: tracker.instance(for: -1))) + expectNil(d.index(forKey: tracker.instance(for: count))) + } + } + } + + // TODO: determine how to best calculate the expected order of the hash-trie 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.persistentDictionary(keys: 0 ..< count) + withEvery("offset", in: 0 ..< count) { offset in + let item = d[PersistentDictionaryIndex(value: offset)] + expectEqual(item.key, d.keys[offset]) // NOTE: uses the actual order `d.keys` + expectEqual(item.value, d.values[offset]) // NOTE: uses the actual order `d.values` + } + } + } + } + + func test_subscript_getter() { + withEvery("count", in: 0 ..< 30) { count in + withLifetimeTracking { tracker in + let (d, keys, values) = tracker.persistentDictionary(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.persistentDictionary(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.persistentDictionary(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: PersistentDictionary, 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.persistentDictionary(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 mutate( + _ value: inout T, + _ body: (inout T) throws -> R + ) rethrows -> R { + try body(&value) + } + +// 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.persistentDictionary(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.persistentDictionary(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: PersistentDictionary, 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.persistentDictionary(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.persistentDictionary(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.persistentDictionary(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: PersistentDictionary, 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.persistentDictionary(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: PersistentDictionary, 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.persistentDictionary(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: PersistentDictionary, 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.persistentDictionary(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: PersistentDictionary, 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.persistentDictionary(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: PersistentDictionary, 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.persistentDictionary(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: PersistentDictionary = [ +// "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: PersistentDictionary = [ +// "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: PersistentDictionary = [ +// "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: PersistentDictionary = [ +// "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 = PersistentDictionary(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 = PersistentDictionary(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 = PersistentDictionary(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_CustomStringConvertible() { + let a: PersistentDictionary = [:] + expectEqual(a.description, "[:]") + + let b: PersistentDictionary = [CollidableInt(0): 1] + expectEqual(b.description, "[0: 1]") + + let c: PersistentDictionary = [CollidableInt(0): 1, CollidableInt(2): 3, CollidableInt(4): 5] + expectEqual(c.description, "[0: 1, 2: 3, 4: 5]") + } + +// func test_CustomDebugStringConvertible() { +// let a: PersistentDictionary = [:] +// expectEqual(a.debugDescription, +// "PersistentDictionary([:])") +// +// let b: PersistentDictionary = [0: 1] +// expectEqual(b.debugDescription, +// "PersistentDictionary([0: 1])") +// +// let c: PersistentDictionary = [0: 1, 2: 3, 4: 5] +// expectEqual(c.debugDescription, +// "PersistentDictionary([0: 1, 2: 3, 4: 5])") +// } + +// func test_customReflectable() { +// do { +// let d: PersistentDictionary = [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: [[PersistentDictionary]] = [ + [[:], [:]], + [[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_Encodable() throws { +// let d1: PersistentDictionary = [:] +// let v1: MinimalEncoder.Value = .array([]) +// expectEqual(try MinimalEncoder.encode(d1), v1) +// +// let d2: PersistentDictionary = [0: 1] +// let v2: MinimalEncoder.Value = .array([.int(0), .int(1)]) +// expectEqual(try MinimalEncoder.encode(d2), v2) +// +// let d3: PersistentDictionary = [0: 1, 2: 3] +// let v3: MinimalEncoder.Value = +// .array([.int(0), .int(1), .int(2), .int(3)]) +// expectEqual(try MinimalEncoder.encode(d3), v3) +// +// let d4 = PersistentDictionary( +// uniqueKeys: 0 ..< 100, +// values: (0 ..< 100).map { 100 * $0 }) +// let v4: MinimalEncoder.Value = +// .array((0 ..< 100).flatMap { [.int($0), .int(100 * $0)] }) +// expectEqual(try MinimalEncoder.encode(d4), v4) +// } + +// func test_Decodable() throws { +// typealias OD = PersistentDictionary +// let d1: OD = [:] +// let v1: MinimalEncoder.Value = .array([]) +// expectEqual(try MinimalDecoder.decode(v1, as: OD.self), d1) +// +// let d2: OD = [0: 1] +// let v2: MinimalEncoder.Value = .array([.int(0), .int(1)]) +// expectEqual(try MinimalDecoder.decode(v2, as: OD.self), d2) +// +// let d3: OD = [0: 1, 2: 3] +// let v3: MinimalEncoder.Value = +// .array([.int(0), .int(1), .int(2), .int(3)]) +// expectEqual(try MinimalDecoder.decode(v3, as: OD.self), d3) +// +// let d4 = PersistentDictionary( +// uniqueKeys: 0 ..< 100, +// values: (0 ..< 100).map { 100 * $0 }) +// let v4: MinimalEncoder.Value = +// .array((0 ..< 100).flatMap { [.int($0), .int(100 * $0)] }) +// expectEqual(try MinimalDecoder.decode(v4, as: OD.self), d4) +// +// let v5: MinimalEncoder.Value = .array([.int(0), .int(1), .int(2)]) +// expectThrows(try MinimalDecoder.decode(v5, as: OD.self)) { error in +// guard case DecodingError.dataCorrupted(let context) = error else { +// expectFailure("Unexpected error \(error)") +// return +// } +// expectEqual(context.debugDescription, +// "Unkeyed container reached end before value in key-value pair") +// +// } +// +// let v6: MinimalEncoder.Value = .array([.int(0), .int(1), .int(0), .int(2)]) +// expectThrows(try MinimalDecoder.decode(v6, as: OD.self)) { error in +// guard case DecodingError.dataCorrupted(let context) = error else { +// expectFailure("Unexpected error \(error)") +// return +// } +// expectEqual(context.debugDescription, "Duplicate key at offset 2") +// } +// } + +// func test_swapAt() { +// withEvery("count", in: 0 ..< 20) { count in +// withEvery("i", in: 0 ..< count) { i in +// withEvery("j", in: 0 ..< count) { j in +// withEvery("isShared", in: [false, true]) { isShared in +// withLifetimeTracking { tracker in +// var (d, keys, values) = tracker.persistentDictionary(keys: 0 ..< count) +// keys.swapAt(i, j) +// values.swapAt(i, j) +// withHiddenCopies(if: isShared, of: &d) { d in +// d.swapAt(i, j) +// expectEqualElements(d.values, values) +// expectEqual(d[keys[i]], values[i]) +// expectEqual(d[keys[j]], values[j]) +// } +// } +// } +// } +// } +// } +// } + +// func test_partition() { +// withEvery("seed", in: 0 ..< 10) { seed in +// withEvery("count", in: 0 ..< 30) { count in +// withEvery("isShared", in: [false, true]) { isShared in +// withLifetimeTracking { tracker in +// var rng = RepeatableRandomNumberGenerator(seed: seed) +// var (d, keys, values) = tracker.persistentDictionary( +// keys: (0 ..< count).shuffled(using: &rng)) +// var items = Array(zip(keys, values)) +// let expectedPivot = items.partition { $0.0.payload < count / 2 } +// withHiddenCopies(if: isShared, of: &d) { d in +// let actualPivot = d.partition { $0.key.payload < count / 2 } +// expectEqual(actualPivot, expectedPivot) +// expectEqualElements(d, items) +// } +// } +// } +// } +// } +// } + +// func test_sort() { +// withEvery("seed", in: 0 ..< 10) { seed in +// withEvery("count", in: 0 ..< 30) { count in +// withEvery("isShared", in: [false, true]) { isShared in +// withLifetimeTracking { tracker in +// var rng = RepeatableRandomNumberGenerator(seed: seed) +// var (d, keys, values) = tracker.persistentDictionary( +// keys: (0 ..< count).shuffled(using: &rng)) +// var items = Array(zip(keys, values)) +// items.sort(by: { $0.0 < $1.0 }) +// withHiddenCopies(if: isShared, of: &d) { d in +// d.sort() +// expectEqualElements(d, items) +// } +// } +// } +// } +// } +// } + +// func test_sort_by() { +// withEvery("seed", in: 0 ..< 10) { seed in +// withEvery("count", in: 0 ..< 30) { count in +// withEvery("isShared", in: [false, true]) { isShared in +// withLifetimeTracking { tracker in +// var rng = RepeatableRandomNumberGenerator(seed: seed) +// var (d, keys, values) = tracker.persistentDictionary( +// keys: (0 ..< count).shuffled(using: &rng)) +// var items = Array(zip(keys, values)) +// items.sort(by: { $0.0 > $1.0 }) +// withHiddenCopies(if: isShared, of: &d) { d in +// d.sort(by: { $0.key > $1.key }) +// expectEqualElements(d, items) +// } +// } +// } +// } +// } +// } + +// func test_shuffle() { +// withEvery("count", in: 0 ..< 30) { count in +// withEvery("isShared", in: [false, true]) { isShared in +// withEvery("seed", in: 0 ..< 10) { seed in +// var d = PersistentDictionary( +// uniqueKeys: 0 ..< count, +// values: 100 ..< 100 + count) +// var items = (0 ..< count).map { (key: $0, value: 100 + $0) } +// withHiddenCopies(if: isShared, of: &d) { d in +// expectEqualElements(d, items) +// +// var rng1 = RepeatableRandomNumberGenerator(seed: seed) +// items.shuffle(using: &rng1) +// +// var rng2 = RepeatableRandomNumberGenerator(seed: seed) +// d.shuffle(using: &rng2) +// +// items.sort(by: { $0.key < $1.key }) +// d.sort() +// expectEqualElements(d, items) +// } +// } +// } +// if count >= 2 { +// // Check that shuffling with the system RNG does permute the elements. +// var d = PersistentDictionary( +// uniqueKeys: 0 ..< count, +// values: 100 ..< 100 + count) +// let original = d +// var success = false +// for _ in 0 ..< 1000 { +// d.shuffle() +// if !d.elementsEqual( +// original, +// by: { $0.key == $1.key && $0.value == $1.value} +// ) { +// success = true +// break +// } +// } +// expectTrue(success) +// } +// } +// } + +// func test_reverse() { +// withEvery("count", in: 0 ..< 30) { count in +// withEvery("isShared", in: [false, true]) { isShared in +// withLifetimeTracking { tracker in +// var (d, keys, values) = tracker.persistentDictionary(keys: 0 ..< count) +// var items = Array(zip(keys, values)) +// withHiddenCopies(if: isShared, of: &d) { d in +// items.reverse() +// d.reverse() +// expectEqualElements(d, items) +// } +// } +// } +// } +// } + +// func test_removeAll() { +// withEvery("count", in: 0 ..< 30) { count in +// withEvery("isShared", in: [false, true]) { isShared in +// withLifetimeTracking { tracker in +// var (d, _, _) = tracker.persistentDictionary(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.persistentDictionary(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.persistentDictionary(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 = PersistentDictionary(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.persistentDictionary(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.persistentDictionary(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.persistentDictionary(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.persistentDictionary(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.persistentDictionary(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.persistentDictionary(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/PersistentCollectionsTests/PersistentCollections Utils.swift b/Tests/PersistentCollectionsTests/PersistentCollections Utils.swift new file mode 100644 index 000000000..78f815a89 --- /dev/null +++ b/Tests/PersistentCollectionsTests/PersistentCollections Utils.swift @@ -0,0 +1,31 @@ +//===----------------------------------------------------------------------===// +// +// 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 _CollectionsTestSupport +import PersistentCollections + +extension LifetimeTracker { + func persistentDictionary( + keys: Keys + ) -> ( + dictionary: PersistentDictionary, 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 = PersistentDictionary(uniqueKeys: keys, values: values) + return (dictionary, keys, values) + } +} diff --git a/Tests/PersistentCollectionsTests/_CollidableInt.swift b/Tests/PersistentCollectionsTests/_CollidableInt.swift new file mode 100644 index 000000000..c5114c3e7 --- /dev/null +++ b/Tests/PersistentCollectionsTests/_CollidableInt.swift @@ -0,0 +1,45 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +final class CollidableInt: CustomStringConvertible, CustomDebugStringConvertible, Equatable, Hashable { + let value: Int + let hashValue: Int + + init(_ value: Int) { + self.value = value + self.hashValue = value + } + + init(_ value: Int, _ hashValue: Int) { + self.value = value + self.hashValue = hashValue + } + + var description: String { + return "\(value)" + } + + var debugDescription: String { + return "\(value) [hash = \(hashValue)]" + } + + func hash(into hasher: inout Hasher) { + hasher.combine(hashValue) + } + + static func == (lhs: CollidableInt, rhs: CollidableInt) -> Bool { + if lhs.value == rhs.value { + precondition(lhs.hashValue == rhs.hashValue) + return true + } + return false + } +}