Skip to content
11 changes: 4 additions & 7 deletions core/commonMain/src/implementations/immutableMap/TrieNode.kt
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,6 @@ internal class TrieNode<K, V>(

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

// key is absent
return this
}

private fun mutableReplaceNode(targetNode: TrieNode<K, V>, newNode: TrieNode<K, V>?, nodeIndex: Int, positionMask: Int, owner: MutabilityOwnership) = when {
private fun mutableReplaceNode(newNode: TrieNode<K, V>?, nodeIndex: Int, positionMask: Int, owner: MutabilityOwnership) = when {
newNode == null ->
mutableRemoveNodeAtIndex(nodeIndex, positionMask, owner)
targetNode !== newNode ->
updateNodeAtIndex(nodeIndex, positionMask, newNode, owner)
else -> this
else -> updateNodeAtIndex(nodeIndex, positionMask, newNode, owner)
}

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

// key is absent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
*/

package tests.stress
package tests

import kotlinx.collections.immutable.internal.assert
import kotlin.js.JsName
Expand Down
4 changes: 2 additions & 2 deletions core/commonTest/src/contract/map/ImmutableMapTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import tests.contract.compare
import tests.contract.mapBehavior
import tests.contract.setBehavior
import tests.remove
import tests.stress.IntWrapper
import tests.stress.ObjectWrapper
import tests.IntWrapper
import tests.ObjectWrapper
import kotlin.test.*

class ImmutableHashMapTest : ImmutableMapTest() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ package tests.contract.map

import kotlinx.collections.immutable.implementations.immutableMap.PersistentHashMap
import kotlinx.collections.immutable.persistentHashMapOf
import tests.stress.IntWrapper
import tests.IntWrapper
import kotlin.collections.iterator
import kotlin.test.Test
import kotlin.test.assertEquals
Expand Down
41 changes: 41 additions & 0 deletions core/commonTest/src/contract/map/PersistentHashMapTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package tests.contract.map

import kotlinx.collections.immutable.implementations.immutableMap.PersistentHashMap
import kotlinx.collections.immutable.persistentHashMapOf
import tests.IntWrapper
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
Expand All @@ -32,4 +33,44 @@ class PersistentHashMapTest {
assertEquals(map3, map4.toMap())
assertEquals(map3, map4)
}

@Test
fun `builder should correctly handle multiple element removals in case of full collision`() {
val a = IntWrapper(0, 0)
val b = IntWrapper(1, 0)
val c = IntWrapper(2, 0)

val original: PersistentHashMap<IntWrapper, String> =
persistentHashMapOf(a to "a", b to "b", c to "c") as PersistentHashMap<IntWrapper, String>

val onlyA: PersistentHashMap<IntWrapper, String> =
persistentHashMapOf(a to "a") as PersistentHashMap<IntWrapper, String>

val builder = original.builder()
builder.remove(b)
builder.remove(c)
val removedBC = builder.build()

assertEquals(onlyA, removedBC)
}

@Test
fun `builder should correctly handle multiple element removals in case of partial collision`() {
val a = IntWrapper(0, 0)
val b = IntWrapper(1, 0)
val c = IntWrapper(2, 0)
val d = IntWrapper(3, 11)

val original: PersistentHashMap<IntWrapper, String> =
persistentHashMapOf(a to "a", b to "b", c to "c", d to "d") as PersistentHashMap<IntWrapper, String>

val afterImmutableRemoving = original.remove(b).remove(c)

val builder = original.builder()
builder.remove(b)
builder.remove(c)
val afterMutableRemoving = builder.build()

assertEquals(afterImmutableRemoving, afterMutableRemoving)
}
}
53 changes: 53 additions & 0 deletions core/commonTest/src/contract/map/PersistentOrderedMapTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright 2016-2025 JetBrains s.r.o.
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
*/

package tests.contract.map

import kotlinx.collections.immutable.minus
import kotlinx.collections.immutable.persistentMapOf
import kotlin.test.Test
import kotlin.test.assertEquals

class PersistentOrderedMapTest {

/**
* Test from issue: https://github.com/Kotlin/kotlinx.collections.immutable/issues/198
*/
@Test
fun `when removing multiple keys with identical hashcodes the remaining key should be correctly promoted`() {
class ChosenHashCode(
private val hashCode: Int,
private val name: String,
) {
override fun equals(other: Any?): Boolean {
return other is ChosenHashCode && (other.name == name)
}

override fun hashCode(): Int {
return hashCode
}

override fun toString(): String {
return name
}
}

val a = ChosenHashCode(123, "A")
val b = ChosenHashCode(123, "B")
val c = ChosenHashCode(123, "C")

val abc = persistentMapOf(
a to "x",
b to "y",
c to "z",
)

val minusAb = abc.minus(arrayOf(a, b))
val cOnly = persistentMapOf(c to "z")

assertEquals(minusAb.entries, cOnly.entries)
assertEquals(minusAb, cOnly)
}
}
2 changes: 1 addition & 1 deletion core/commonTest/src/contract/set/ImmutableSetTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import tests.contract.compare
import tests.contract.setBehavior
import tests.isDigit
import tests.isUpperCase
import tests.stress.IntWrapper
import tests.IntWrapper
import kotlin.test.*

class ImmutableHashSetTest : ImmutableSetTestBase() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ package tests.contract.set

import kotlinx.collections.immutable.implementations.immutableSet.PersistentHashSet
import kotlinx.collections.immutable.persistentHashSetOf
import tests.stress.IntWrapper
import tests.IntWrapper
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import kotlinx.collections.immutable.implementations.immutableMap.LOG_MAX_BRANCH
import kotlinx.collections.immutable.implementations.immutableMap.MAX_SHIFT
import kotlinx.collections.immutable.implementations.immutableMap.PersistentHashMap
import kotlinx.collections.immutable.implementations.immutableMap.TrieNode
import tests.stress.IntWrapper
import tests.IntWrapper
import kotlin.test.*

class HashMapTrieNodeTest {
Expand Down
1 change: 1 addition & 0 deletions core/commonTest/src/stress/WrapperGenerator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

package tests.stress

import tests.ObjectWrapper
import kotlin.random.Random


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import tests.NForAlgorithmComplexity
import tests.distinctStringValues
import tests.remove
import tests.stress.ExecutionTimeMeasuringTest
import tests.stress.IntWrapper
import tests.IntWrapper
import tests.stress.WrapperGenerator
import kotlin.random.Random
import kotlin.test.*
Expand Down
2 changes: 1 addition & 1 deletion core/commonTest/src/stress/map/PersistentHashMapTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import tests.NForAlgorithmComplexity
import tests.distinctStringValues
import tests.remove
import tests.stress.ExecutionTimeMeasuringTest
import tests.stress.IntWrapper
import tests.IntWrapper
import tests.stress.WrapperGenerator
import kotlin.random.Random
import kotlin.test.*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import kotlinx.collections.immutable.persistentHashSetOf
import tests.NForAlgorithmComplexity
import tests.distinctStringValues
import tests.stress.ExecutionTimeMeasuringTest
import tests.stress.IntWrapper
import tests.IntWrapper
import tests.stress.WrapperGenerator
import kotlin.random.Random
import kotlin.test.Test
Expand Down
2 changes: 1 addition & 1 deletion core/commonTest/src/stress/set/PersistentHashSetTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import kotlinx.collections.immutable.persistentHashSetOf
import tests.NForAlgorithmComplexity
import tests.distinctStringValues
import tests.stress.ExecutionTimeMeasuringTest
import tests.stress.IntWrapper
import tests.IntWrapper
import tests.stress.WrapperGenerator
import kotlin.random.Random
import kotlin.test.Test
Expand Down