From 6f86e6ff97e40318f2b5fe20aeca80f706080918 Mon Sep 17 00:00:00 2001 From: sergeych Date: Sat, 20 Dec 2025 23:37:56 +0100 Subject: [PATCH] fixed bug when some Map operations wer restricted to string keys only --- docs/Map.md | 4 +-- .../net/sergeych/lyng/obj/ObjIterable.kt | 9 +++-- .../kotlin/net/sergeych/lyng/obj/ObjMap.kt | 34 +++++++++---------- .../kotlin/net/sergeych/lyng/obj/ObjRef.kt | 4 +-- .../src/commonTest/kotlin/MapLiteralTest.kt | 24 ++++++------- lynglib/src/commonTest/kotlin/ScriptTest.kt | 29 ++++++++++++++++ 6 files changed, 67 insertions(+), 37 deletions(-) diff --git a/docs/Map.md b/docs/Map.md index 70ecd0c..82bb1de 100644 --- a/docs/Map.md +++ b/docs/Map.md @@ -172,7 +172,7 @@ Maps and entries can also be merged with `+` and `+=`: Notes: - Map literals always use string keys (identifier keys are converted to strings). -- Spreads inside map literals and `+`/`+=` merges require string keys on the right-hand side; this aligns with named-argument splats. -- When you need computed or non-string keys, use the constructor form `Map(...)` or build entries with `=>` and then merge. +- Spreads inside map literals and `+`/`+=` merges allow any objects as keys. +- When you need computed or non-string keys, use the constructor form `Map(...)`, map literals with computed keys (if supported), or build entries with `=>` and then merge. [Collection](Collection.md) \ No newline at end of file diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjIterable.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjIterable.kt index 13aa53f..279196d 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjIterable.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjIterable.kt @@ -104,11 +104,14 @@ val ObjIterable by lazy { returns = type("lyng.Map"), moduleName = "lyng.stdlib" ) { - val result = ObjMap() + val result = mutableMapOf() thisObj.toFlow(this).collect { pair -> - result.map[pair.getAt(this, 0)] = pair.getAt(this, 1) + when (pair) { + is ObjMapEntry -> result[pair.key] = pair.value + else -> result[pair.getAt(this, 0)] = pair.getAt(this, 1) + } } - result + ObjMap(result) } addFnDoc( diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjMap.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjMap.kt index 6b2373b..55db28e 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjMap.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjMap.kt @@ -174,18 +174,21 @@ class ObjMap(val map: MutableMap = mutableMapOf()) : Obj() { val map = mutableMapOf() if (list.isEmpty()) return map - val first = list.first() - if (first.isInstanceOf(ObjArray)) { - if (first.invokeInstanceMethod(scope, "size").toInt() != 2) - scope.raiseIllegalArgument( - "list to construct map entry should exactly be 2 element Array like [key,value], got $list" - ) - } else scope.raiseIllegalArgument("first element of map list be a Collection of 2 elements; got $first") - - - list.forEach { - map[it.getAt(scope, ObjInt.Zero)] = it.getAt(scope, ObjInt.One) + when (it) { + is ObjMapEntry -> map[it.key] = it.value + else -> { + if (it.isInstanceOf(ObjArray)) { + if (it.invokeInstanceMethod(scope, "size").toInt() != 2) + scope.raiseIllegalArgument( + "Array to construct map entry should exactly be 2 elements [key,value], got $it" + ) + map[it.getAt(scope, ObjInt.Zero)] = it.getAt(scope, ObjInt.One) + } else { + scope.raiseIllegalArgument("elements to construct map must be MapEntry or Array of 2 elements; got $it") + } + } + } } return map } @@ -296,13 +299,11 @@ class ObjMap(val map: MutableMap = mutableMapOf()) : Obj() { is ObjMap -> { // Rightmost wins: copy all entries from `other` over existing ones for ((k, v) in other.map) { - val key = k as? ObjString ?: scope.raiseIllegalArgument("map merge expects string keys; got $k") - map[key] = v + map[k] = v } } is ObjMapEntry -> { - val key = other.key as? ObjString ?: scope.raiseIllegalArgument("map merge expects string keys; got ${other.key}") - map[key] = other.value + map[other.key] = other.value } is ObjList -> { // Treat as list of map entries @@ -311,8 +312,7 @@ class ObjMap(val map: MutableMap = mutableMapOf()) : Obj() { is ObjMapEntry -> e else -> scope.raiseIllegalArgument("map can only be merged with MapEntry elements; got $e") } - val key = entry.key as? ObjString ?: scope.raiseIllegalArgument("map merge expects string keys; got ${entry.key}") - map[key] = entry.value + map[entry.key] = entry.value } } else -> scope.raiseIllegalArgument("map can only be merged with Map, MapEntry, or List") diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt index f28852f..b073080 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt @@ -1656,10 +1656,8 @@ class MapLiteralRef(private val entries: List) : ObjRef { is MapLiteralEntry.Spread -> { val m = if (PerfFlags.RVAL_FASTPATH) e.ref.evalValue(scope) else e.ref.get(scope).value if (m !is ObjMap) scope.raiseIllegalArgument("spread element in map literal must be a Map") - // Enforce string keys for map literals for ((k, v) in m.map) { - val sKey = k as? ObjString ?: scope.raiseIllegalArgument("spread map must have string keys; got $k") - result.map[sKey] = v + result.map[k] = v } } } diff --git a/lynglib/src/commonTest/kotlin/MapLiteralTest.kt b/lynglib/src/commonTest/kotlin/MapLiteralTest.kt index 7c8b629..0f72ac1 100644 --- a/lynglib/src/commonTest/kotlin/MapLiteralTest.kt +++ b/lynglib/src/commonTest/kotlin/MapLiteralTest.kt @@ -24,7 +24,6 @@ import net.sergeych.lyng.ExecutionError import net.sergeych.lyng.ScriptError import net.sergeych.lyng.eval import kotlin.test.Test -import kotlin.test.assertEquals import kotlin.test.assertFailsWith class MapLiteralTest { @@ -119,20 +118,21 @@ class MapLiteralTest { } @Test - fun spreadNonStringKeysIsRuntimeError() = runTest { - assertFailsWith { - eval("""{ ...Map(1 => "x") }""") - } + fun spreadNonStringKeysIsAllowed() = runTest { + eval(""" + val m = { ...Map(1 => "x") } + assertEquals("x", m[1]) + """) } @Test - fun mergeNonStringKeyIsRuntimeError() = runTest { - assertFailsWith { - eval(""" - val e = (1 => "x") - { "a": 1 } + e - """) - } + fun mergeNonStringKeyIsAllowed() = runTest { + eval(""" + val e = (1 => "x") + val m = { "a": 1 } + e + assertEquals(1, m["a"]) + assertEquals("x", m[1]) + """) } @Test diff --git a/lynglib/src/commonTest/kotlin/ScriptTest.kt b/lynglib/src/commonTest/kotlin/ScriptTest.kt index 7c24432..a174630 100644 --- a/lynglib/src/commonTest/kotlin/ScriptTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptTest.kt @@ -3020,6 +3020,35 @@ class ScriptTest { ) } + @Test + fun testMapWithNonStringKeys() = runTest { + eval(""" + val map = Map( 1 => "one", 2 => "two" ) + assertEquals( "one", map[1] ) + assertEquals( "two", map[2] ) + assertEquals( null, map[3] ) + map[3] = "three" + assertEquals( "three", map[3] ) + map += (4 => "four") + assertEquals( "four", map[4] ) + + // Test toMap() + val map2 = [1 => "a", 2 => "b"].toMap() + assertEquals("a", map2[1]) + assertEquals("b", map2[2]) + + // Test Map constructor with mixed entries and arrays + val map3 = Map( 1 => "a", [2, "b"] ) + assertEquals("a", map3[1]) + assertEquals("b", map3[2]) + + // Test plus + val map4 = map3 + (3 => "c") + assertEquals("c", map4[3]) + assertEquals("a", map4[1]) + """.trimIndent()) + } + @Test fun testBuffer() = runTest { eval(