Skip to content

Commit 5c3af1e

Browse files
Bug in PersistentMap equals implementation (#218)
Fixes #198
1 parent 4528d53 commit 5c3af1e

14 files changed

+110
-18
lines changed

core/commonMain/src/implementations/immutableMap/TrieNode.kt

+4-7
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,6 @@ internal class TrieNode<K, V>(
181181

182182
/** The given [newNode] must not be a part of any persistent map instance. */
183183
private fun updateNodeAtIndex(nodeIndex: Int, positionMask: Int, newNode: TrieNode<K, V>, owner: MutabilityOwnership? = null): TrieNode<K, V> {
184-
// assert(buffer[nodeIndex] !== newNode)
185184
val newNodeBuffer = newNode.buffer
186185
if (newNodeBuffer.size == 2 && newNode.nodeMap == 0) {
187186
if (buffer.size == 1) {
@@ -764,19 +763,17 @@ internal class TrieNode<K, V>(
764763
} else {
765764
targetNode.mutableRemove(keyHash, key, shift + LOG_MAX_BRANCHING_FACTOR, mutator)
766765
}
767-
return mutableReplaceNode(targetNode, newNode, nodeIndex, keyPositionMask, mutator.ownership)
766+
return mutableReplaceNode(newNode, nodeIndex, keyPositionMask, mutator.ownership)
768767
}
769768

770769
// key is absent
771770
return this
772771
}
773772

774-
private fun mutableReplaceNode(targetNode: TrieNode<K, V>, newNode: TrieNode<K, V>?, nodeIndex: Int, positionMask: Int, owner: MutabilityOwnership) = when {
773+
private fun mutableReplaceNode(newNode: TrieNode<K, V>?, nodeIndex: Int, positionMask: Int, owner: MutabilityOwnership) = when {
775774
newNode == null ->
776775
mutableRemoveNodeAtIndex(nodeIndex, positionMask, owner)
777-
targetNode !== newNode ->
778-
updateNodeAtIndex(nodeIndex, positionMask, newNode, owner)
779-
else -> this
776+
else -> updateNodeAtIndex(nodeIndex, positionMask, newNode, owner)
780777
}
781778

782779
fun remove(keyHash: Int, key: K, value: @UnsafeVariance V, shift: Int): TrieNode<K, V>? {
@@ -826,7 +823,7 @@ internal class TrieNode<K, V>(
826823
} else {
827824
targetNode.mutableRemove(keyHash, key, value, shift + LOG_MAX_BRANCHING_FACTOR, mutator)
828825
}
829-
return mutableReplaceNode(targetNode, newNode, nodeIndex, keyPositionMask, mutator.ownership)
826+
return mutableReplaceNode(newNode, nodeIndex, keyPositionMask, mutator.ownership)
830827
}
831828

832829
// key is absent

core/commonTest/src/stress/ObjectWrapper.kt renamed to core/commonTest/src/ObjectWrapper.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
44
*/
55

6-
package tests.stress
6+
package tests
77

88
import kotlinx.collections.immutable.internal.assert
99
import kotlin.js.JsName

core/commonTest/src/contract/map/ImmutableMapTest.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import tests.contract.compare
1111
import tests.contract.mapBehavior
1212
import tests.contract.setBehavior
1313
import tests.remove
14-
import tests.stress.IntWrapper
15-
import tests.stress.ObjectWrapper
14+
import tests.IntWrapper
15+
import tests.ObjectWrapper
1616
import kotlin.test.*
1717

1818
class ImmutableHashMapTest : ImmutableMapTest() {

core/commonTest/src/contract/map/PersistentHashMapBuilderTest.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ package tests.contract.map
77

88
import kotlinx.collections.immutable.implementations.immutableMap.PersistentHashMap
99
import kotlinx.collections.immutable.persistentHashMapOf
10-
import tests.stress.IntWrapper
10+
import tests.IntWrapper
1111
import kotlin.collections.iterator
1212
import kotlin.test.Test
1313
import kotlin.test.assertEquals

core/commonTest/src/contract/map/PersistentHashMapTest.kt

+41
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package tests.contract.map
77

88
import kotlinx.collections.immutable.implementations.immutableMap.PersistentHashMap
99
import kotlinx.collections.immutable.persistentHashMapOf
10+
import tests.IntWrapper
1011
import kotlin.test.Test
1112
import kotlin.test.assertEquals
1213
import kotlin.test.assertTrue
@@ -32,4 +33,44 @@ class PersistentHashMapTest {
3233
assertEquals(map3, map4.toMap())
3334
assertEquals(map3, map4)
3435
}
36+
37+
@Test
38+
fun `builder should correctly handle multiple element removals in case of full collision`() {
39+
val a = IntWrapper(0, 0)
40+
val b = IntWrapper(1, 0)
41+
val c = IntWrapper(2, 0)
42+
43+
val original: PersistentHashMap<IntWrapper, String> =
44+
persistentHashMapOf(a to "a", b to "b", c to "c") as PersistentHashMap<IntWrapper, String>
45+
46+
val onlyA: PersistentHashMap<IntWrapper, String> =
47+
persistentHashMapOf(a to "a") as PersistentHashMap<IntWrapper, String>
48+
49+
val builder = original.builder()
50+
builder.remove(b)
51+
builder.remove(c)
52+
val removedBC = builder.build()
53+
54+
assertEquals(onlyA, removedBC)
55+
}
56+
57+
@Test
58+
fun `builder should correctly handle multiple element removals in case of partial collision`() {
59+
val a = IntWrapper(0, 0)
60+
val b = IntWrapper(1, 0)
61+
val c = IntWrapper(2, 0)
62+
val d = IntWrapper(3, 11)
63+
64+
val original: PersistentHashMap<IntWrapper, String> =
65+
persistentHashMapOf(a to "a", b to "b", c to "c", d to "d") as PersistentHashMap<IntWrapper, String>
66+
67+
val afterImmutableRemoving = original.remove(b).remove(c)
68+
69+
val builder = original.builder()
70+
builder.remove(b)
71+
builder.remove(c)
72+
val afterMutableRemoving = builder.build()
73+
74+
assertEquals(afterImmutableRemoving, afterMutableRemoving)
75+
}
3576
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright 2016-2025 JetBrains s.r.o.
3+
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
4+
*/
5+
6+
package tests.contract.map
7+
8+
import kotlinx.collections.immutable.minus
9+
import kotlinx.collections.immutable.persistentMapOf
10+
import kotlin.test.Test
11+
import kotlin.test.assertEquals
12+
13+
class PersistentOrderedMapTest {
14+
15+
/**
16+
* Test from issue: https://github.com/Kotlin/kotlinx.collections.immutable/issues/198
17+
*/
18+
@Test
19+
fun `when removing multiple keys with identical hashcodes the remaining key should be correctly promoted`() {
20+
class ChosenHashCode(
21+
private val hashCode: Int,
22+
private val name: String,
23+
) {
24+
override fun equals(other: Any?): Boolean {
25+
return other is ChosenHashCode && (other.name == name)
26+
}
27+
28+
override fun hashCode(): Int {
29+
return hashCode
30+
}
31+
32+
override fun toString(): String {
33+
return name
34+
}
35+
}
36+
37+
val a = ChosenHashCode(123, "A")
38+
val b = ChosenHashCode(123, "B")
39+
val c = ChosenHashCode(123, "C")
40+
41+
val abc = persistentMapOf(
42+
a to "x",
43+
b to "y",
44+
c to "z",
45+
)
46+
47+
val minusAb = abc.minus(arrayOf(a, b))
48+
val cOnly = persistentMapOf(c to "z")
49+
50+
assertEquals(minusAb.entries, cOnly.entries)
51+
assertEquals(minusAb, cOnly)
52+
}
53+
}

core/commonTest/src/contract/set/ImmutableSetTest.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import tests.contract.compare
1010
import tests.contract.setBehavior
1111
import tests.isDigit
1212
import tests.isUpperCase
13-
import tests.stress.IntWrapper
13+
import tests.IntWrapper
1414
import kotlin.test.*
1515

1616
class ImmutableHashSetTest : ImmutableSetTestBase() {

core/commonTest/src/contract/set/PersistentHashSetBuilderTest.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ package tests.contract.set
77

88
import kotlinx.collections.immutable.implementations.immutableSet.PersistentHashSet
99
import kotlinx.collections.immutable.persistentHashSetOf
10-
import tests.stress.IntWrapper
10+
import tests.IntWrapper
1111
import kotlin.test.Test
1212
import kotlin.test.assertEquals
1313
import kotlin.test.assertFailsWith

core/commonTest/src/implementations/map/HashMapTrieNodeTest.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import kotlinx.collections.immutable.implementations.immutableMap.LOG_MAX_BRANCH
99
import kotlinx.collections.immutable.implementations.immutableMap.MAX_SHIFT
1010
import kotlinx.collections.immutable.implementations.immutableMap.PersistentHashMap
1111
import kotlinx.collections.immutable.implementations.immutableMap.TrieNode
12-
import tests.stress.IntWrapper
12+
import tests.IntWrapper
1313
import kotlin.test.*
1414

1515
class HashMapTrieNodeTest {

core/commonTest/src/stress/WrapperGenerator.kt

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
package tests.stress
77

8+
import tests.ObjectWrapper
89
import kotlin.random.Random
910

1011

core/commonTest/src/stress/map/PersistentHashMapBuilderTest.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import tests.NForAlgorithmComplexity
1111
import tests.distinctStringValues
1212
import tests.remove
1313
import tests.stress.ExecutionTimeMeasuringTest
14-
import tests.stress.IntWrapper
14+
import tests.IntWrapper
1515
import tests.stress.WrapperGenerator
1616
import kotlin.random.Random
1717
import kotlin.test.*

core/commonTest/src/stress/map/PersistentHashMapTest.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import tests.NForAlgorithmComplexity
1111
import tests.distinctStringValues
1212
import tests.remove
1313
import tests.stress.ExecutionTimeMeasuringTest
14-
import tests.stress.IntWrapper
14+
import tests.IntWrapper
1515
import tests.stress.WrapperGenerator
1616
import kotlin.random.Random
1717
import kotlin.test.*

core/commonTest/src/stress/set/PersistentHashSetBuilderTest.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import kotlinx.collections.immutable.persistentHashSetOf
99
import tests.NForAlgorithmComplexity
1010
import tests.distinctStringValues
1111
import tests.stress.ExecutionTimeMeasuringTest
12-
import tests.stress.IntWrapper
12+
import tests.IntWrapper
1313
import tests.stress.WrapperGenerator
1414
import kotlin.random.Random
1515
import kotlin.test.Test

core/commonTest/src/stress/set/PersistentHashSetTest.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import kotlinx.collections.immutable.persistentHashSetOf
99
import tests.NForAlgorithmComplexity
1010
import tests.distinctStringValues
1111
import tests.stress.ExecutionTimeMeasuringTest
12-
import tests.stress.IntWrapper
12+
import tests.IntWrapper
1313
import tests.stress.WrapperGenerator
1414
import kotlin.random.Random
1515
import kotlin.test.Test

0 commit comments

Comments
 (0)