Skip to content

Commit 9ab9563

Browse files
committed
[Capsule] Implement preliminary hash-collision resolution
1 parent c9784ab commit 9ab9563

File tree

5 files changed

+266
-4
lines changed

5 files changed

+266
-4
lines changed

Sources/Capsule/HashMap+Equatable.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
//
1010
//===----------------------------------------------------------------------===//
1111

12-
extension HashMap : Equatable {
12+
// TODO check Dictionary semantics of Equatable (i.e., if it only compares keys or also values)
13+
extension HashMap : Equatable where Value : Equatable {
1314
public static func == (lhs: HashMap<Key, Value>, rhs: HashMap<Key, Value>) -> Bool {
1415
lhs.cachedSize == rhs.cachedSize &&
1516
lhs.cachedKeySetHashCode == rhs.cachedKeySetHashCode &&

Sources/Capsule/_BitmapIndexedMapNode.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ final class BitmapIndexedMapNode<Key, Value> : MapNode<Key, Value> where Key : H
156156

157157
func mergeTwoKeyValPairs(_ key0: Key, _ value0: Value, _ keyHash0: Int, _ key1: Key, _ value1: Value, _ keyHash1: Int, _ shift: Int) -> MapNode<Key, Value> {
158158
if (shift >= HashCodeLength) {
159-
preconditionFailure("Not yet implemented")
159+
return HashCollisionMapNode(keyHash0, [(key0, value0), (key1, value1)])
160160
} else {
161161
let mask0 = maskFrom(keyHash0, shift)
162162
let mask1 = maskFrom(keyHash1, shift)
@@ -279,7 +279,7 @@ final class BitmapIndexedMapNode<Key, Value> : MapNode<Key, Value> where Key : H
279279
}
280280
}
281281

282-
extension BitmapIndexedMapNode /* : Equatable */ {
282+
extension BitmapIndexedMapNode /* : Equatable where Value : Equatable */ {
283283
static func == (lhs: BitmapIndexedMapNode<Key, Value>, rhs: BitmapIndexedMapNode<Key, Value>) -> Bool {
284284
lhs === rhs ||
285285
lhs.nodeMap == rhs.nodeMap &&
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift Collections open source project
4+
//
5+
// Copyright (c) 2021 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
//
10+
//===----------------------------------------------------------------------===//
11+
12+
final class HashCollisionMapNode<Key, Value> : MapNode<Key, Value> where Key : Hashable {
13+
let hash: Int
14+
let content: Array<(Key, Value)>
15+
16+
init(_ hash: Int, _ content: Array<(Key, Value)>) {
17+
precondition(content.count >= 2)
18+
precondition(content.map { $0.0 }.allSatisfy {$0.hashValue == hash})
19+
20+
self.hash = hash
21+
self.content = content
22+
}
23+
24+
override func get(_ key: Key, _ hash: Int, _ shift: Int) -> Value? {
25+
if (self.hash == hash) {
26+
return content.first(where: { key == $0.0 }).map { $0.1 }
27+
} else { return nil }
28+
}
29+
30+
override func containsKey(_ key: Key, _ hash: Int, _ shift: Int) -> Bool {
31+
return self.hash == hash && content.contains(where: { key == $0.0 })
32+
}
33+
34+
// // TODO requires Value to be Equatable
35+
// func contains(_ key: Key, _ value: Value, _ hash: Int, _ shift: Int) -> Bool {
36+
// return self.hash == hash && content.contains(where: { key == $0.0 && value == $0.1 })
37+
// }
38+
39+
override func updated(_ key: Key, _ value: Value, _ hash: Int, _ shift: Int, _ effect: inout MapEffect) -> MapNode<Key, Value> {
40+
41+
// TODO check if key/value-pair check should be added (requires value to be Equitable)
42+
if (self.containsKey(key, hash, shift)) {
43+
let index = content.firstIndex(where: { key == $0.0 })!
44+
let updatedContent = content[0..<index] + [(key, value)] + content[index+1..<content.count]
45+
46+
effect.setReplacedValue()
47+
// TODO check (performance of) slicing and materialization of array content
48+
return HashCollisionMapNode(hash, Array(updatedContent))
49+
} else {
50+
effect.setModified()
51+
return HashCollisionMapNode(hash, content + [(key, value)])
52+
}
53+
}
54+
55+
override func removed(_ key: Key, _ hash: Int, _ shift: Int, _ effect: inout MapEffect) -> MapNode<Key, Value> {
56+
if (!self.containsKey(key, hash, shift)) {
57+
return self
58+
} else {
59+
effect.setModified()
60+
let updatedContent = content.filter { $0.0 != key }
61+
assert(updatedContent.count == content.count - 1)
62+
63+
switch updatedContent.count {
64+
case 1:
65+
let (k, v) = updatedContent[0].self
66+
return BitmapIndexedMapNode<Key, Value>(bitposFrom(maskFrom(hash, 0)), 0, Array(arrayLiteral: k, v))
67+
default:
68+
return HashCollisionMapNode(hash, updatedContent)
69+
}
70+
}
71+
}
72+
73+
override var hasNodes: Bool { false }
74+
75+
override var nodeArity: Int { 0 }
76+
77+
override func getNode(_ index: Int) -> MapNode<Key, Value> {
78+
preconditionFailure("No sub-nodes present in hash-collision leaf node")
79+
}
80+
81+
override var hasPayload: Bool { true }
82+
83+
override var payloadArity: Int { content.count }
84+
85+
override func getPayload(_ index: Int) -> (Key, Value) { content[index] }
86+
}
87+
88+
extension HashCollisionMapNode /* : Equatable where Value : Equatable */ {
89+
static func == (lhs: HashCollisionMapNode<Key, Value>, rhs: HashCollisionMapNode<Key, Value>) -> Bool {
90+
preconditionFailure("Not yet implemented")
91+
}
92+
}

Sources/Capsule/_MapNode.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ class MapNode<Key, Value> : Node where Key : Hashable {
5353
}
5454
}
5555

56-
extension MapNode : Equatable {
56+
extension MapNode : Equatable where Value : Equatable {
5757
static func == (lhs: MapNode<Key, Value>, rhs: MapNode<Key, Value>) -> Bool {
5858
preconditionFailure("Not yet implemented")
5959
}

Tests/CapsuleTests/CapsuleSmokeTests.swift

+169
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,173 @@ final class CapsuleSmokeTests: CollectionTestCase {
7373

7474
expectEqual(inoutHasher.finalize(), expectedHashValue)
7575
}
76+
77+
func testCompactionWhenDeletingFromHashCollisionNode1() {
78+
let map: HashMap<CollidableInt, CollidableInt> = [:]
79+
80+
81+
var res1 = map
82+
res1[CollidableInt(11, 1)] = CollidableInt(11, 1)
83+
res1[CollidableInt(12, 1)] = CollidableInt(12, 1)
84+
85+
expectEqual(res1.count, 2)
86+
expectTrue(res1.contains(CollidableInt(11, 1)))
87+
expectTrue(res1.contains(CollidableInt(12, 1)))
88+
89+
90+
var res2 = res1
91+
res2[CollidableInt(12, 1)] = nil
92+
expectTrue(res2.contains(CollidableInt(11, 1)))
93+
expectFalse(res2.contains(CollidableInt(12, 1)))
94+
95+
expectEqual(res2.count, 1)
96+
// expectEqual(HashMap.init([CollidableInt(11, 1) : CollidableInt(11, 1)]), res2)
97+
98+
99+
var res3 = res1
100+
res3[CollidableInt(11, 1)] = nil
101+
expectFalse(res3.contains(CollidableInt(11, 1)))
102+
expectTrue(res3.contains(CollidableInt(12, 1)))
103+
104+
expectEqual(res3.count, 1)
105+
// expectEqual(HashMap.init([CollidableInt(12, 1) : CollidableInt(12, 1)]), res3)
106+
107+
108+
var resX = res1
109+
resX[CollidableInt(32769)] = CollidableInt(32769)
110+
resX[CollidableInt(12, 1)] = nil
111+
expectTrue(resX.contains(CollidableInt(11, 1)))
112+
expectFalse(resX.contains(CollidableInt(12, 1)))
113+
expectTrue(resX.contains(CollidableInt(32769)))
114+
115+
expectEqual(resX.count, 2)
116+
// expectEqual(HashMap.init([CollidableInt(11, 1) : CollidableInt(11, 1), CollidableInt(32769) : CollidableInt(32769)]), resX)
117+
}
118+
119+
func testCompactionWhenDeletingFromHashCollisionNode2() {
120+
let map: HashMap<CollidableInt, CollidableInt> = [:]
121+
122+
123+
var res1 = map
124+
res1[CollidableInt(32769_1, 32769)] = CollidableInt(32769_1, 32769)
125+
res1[CollidableInt(32769_2, 32769)] = CollidableInt(32769_2, 32769)
126+
127+
expectEqual(res1.count, 2)
128+
expectTrue(res1.contains(CollidableInt(32769_1, 32769)))
129+
expectTrue(res1.contains(CollidableInt(32769_2, 32769)))
130+
131+
132+
var res2 = res1
133+
res2[CollidableInt(1)] = CollidableInt(1)
134+
135+
expectEqual(res2.count, 3)
136+
expectTrue(res2.contains(CollidableInt(1)))
137+
expectTrue(res2.contains(CollidableInt(32769_1, 32769)))
138+
expectTrue(res2.contains(CollidableInt(32769_2, 32769)))
139+
140+
141+
var res3 = res2
142+
res3[CollidableInt(32769_2, 32769)] = nil
143+
144+
expectEqual(res3.count, 2)
145+
expectTrue(res3.contains(CollidableInt(1)))
146+
expectTrue(res3.contains(CollidableInt(32769_1, 32769)))
147+
148+
149+
// expectEqual(HashMap.init([CollidableInt(1) : CollidableInt(1), CollidableInt(32769_2, 32769) : CollidableInt(32769_2, 32769)]), res3)
150+
}
151+
152+
func testCompactionWhenDeletingFromHashCollisionNode3() {
153+
let map: HashMap<CollidableInt, CollidableInt> = [:]
154+
155+
156+
var res1 = map
157+
res1[CollidableInt(32769_1, 32769)] = CollidableInt(32769_1, 32769)
158+
res1[CollidableInt(32769_2, 32769)] = CollidableInt(32769_2, 32769)
159+
160+
expectEqual(res1.count, 2)
161+
expectTrue(res1.contains(CollidableInt(32769_1, 32769)))
162+
expectTrue(res1.contains(CollidableInt(32769_2, 32769)))
163+
164+
165+
var res2 = res1
166+
res2[CollidableInt(1)] = CollidableInt(1)
167+
168+
expectEqual(res2.count, 3)
169+
expectTrue(res2.contains(CollidableInt(1)))
170+
expectTrue(res2.contains(CollidableInt(32769_1, 32769)))
171+
expectTrue(res2.contains(CollidableInt(32769_2, 32769)))
172+
173+
174+
var res3 = res2
175+
res3[CollidableInt(1)] = nil
176+
177+
expectEqual(res3.count, 2)
178+
expectTrue(res3.contains(CollidableInt(32769_1, 32769)))
179+
expectTrue(res3.contains(CollidableInt(32769_2, 32769)))
180+
181+
182+
// expectEqual(HashMap.init([CollidableInt(32769_1, 32769) : CollidableInt(32769_1, 32769), CollidableInt(32769_2, 32769) : CollidableInt(32769_2, 32769)]), res3)
183+
184+
}
185+
186+
func testCompactionWhenDeletingFromHashCollisionNode4() {
187+
let map: HashMap<CollidableInt, CollidableInt> = [:]
188+
189+
190+
var res1 = map
191+
res1[CollidableInt(32769_1, 32769)] = CollidableInt(32769_1, 32769)
192+
res1[CollidableInt(32769_2, 32769)] = CollidableInt(32769_2, 32769)
193+
194+
expectEqual(res1.count, 2)
195+
expectTrue(res1.contains(CollidableInt(32769_1, 32769)))
196+
expectTrue(res1.contains(CollidableInt(32769_2, 32769)))
197+
198+
199+
var res2 = res1
200+
res2[CollidableInt(5)] = CollidableInt(5)
201+
202+
expectEqual(res2.count, 3)
203+
expectTrue(res2.contains(CollidableInt(5)))
204+
expectTrue(res2.contains(CollidableInt(32769_1, 32769)))
205+
expectTrue(res2.contains(CollidableInt(32769_2, 32769)))
206+
207+
208+
var res3 = res2
209+
res3[CollidableInt(5)] = nil
210+
211+
expectEqual(res3.count, 2)
212+
expectTrue(res3.contains(CollidableInt(32769_1, 32769)))
213+
expectTrue(res3.contains(CollidableInt(32769_2, 32769)))
214+
215+
216+
// expectEqual(res1, res3)
217+
}
218+
}
219+
220+
fileprivate final class CollidableInt : CustomStringConvertible, Equatable, Hashable {
221+
let value: Int
222+
let hashValue: Int
223+
224+
fileprivate init(_ value: Int) {
225+
self.value = value
226+
self.hashValue = value
227+
}
228+
229+
fileprivate init(_ value: Int, _ hashValue: Int) {
230+
self.value = value
231+
self.hashValue = hashValue
232+
}
233+
234+
var description: String {
235+
return "\(value) [hash = \(hashValue)]"
236+
}
237+
238+
func hash(into hasher: inout Hasher) {
239+
hasher.combine(hashValue)
240+
}
241+
242+
static func == (lhs: CollidableInt, rhs: CollidableInt) -> Bool {
243+
return lhs.value == rhs.value
244+
}
76245
}

0 commit comments

Comments
 (0)