fixed bug when some Map operations wer restricted to string keys only
This commit is contained in:
parent
fa91afa92b
commit
6f86e6ff97
@ -172,7 +172,7 @@ Maps and entries can also be merged with `+` and `+=`:
|
|||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- Map literals always use string keys (identifier keys are converted to strings).
|
- 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.
|
- Spreads inside map literals and `+`/`+=` merges allow any objects as keys.
|
||||||
- When you need computed or non-string keys, use the constructor form `Map(...)` or build entries with `=>` and then merge.
|
- 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)
|
[Collection](Collection.md)
|
||||||
@ -104,11 +104,14 @@ val ObjIterable by lazy {
|
|||||||
returns = type("lyng.Map"),
|
returns = type("lyng.Map"),
|
||||||
moduleName = "lyng.stdlib"
|
moduleName = "lyng.stdlib"
|
||||||
) {
|
) {
|
||||||
val result = ObjMap()
|
val result = mutableMapOf<Obj, Obj>()
|
||||||
thisObj.toFlow(this).collect { pair ->
|
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(
|
addFnDoc(
|
||||||
|
|||||||
@ -174,18 +174,21 @@ class ObjMap(val map: MutableMap<Obj, Obj> = mutableMapOf()) : Obj() {
|
|||||||
val map = mutableMapOf<Obj, Obj>()
|
val map = mutableMapOf<Obj, Obj>()
|
||||||
if (list.isEmpty()) return map
|
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 {
|
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
|
return map
|
||||||
}
|
}
|
||||||
@ -296,13 +299,11 @@ class ObjMap(val map: MutableMap<Obj, Obj> = mutableMapOf()) : Obj() {
|
|||||||
is ObjMap -> {
|
is ObjMap -> {
|
||||||
// Rightmost wins: copy all entries from `other` over existing ones
|
// Rightmost wins: copy all entries from `other` over existing ones
|
||||||
for ((k, v) in other.map) {
|
for ((k, v) in other.map) {
|
||||||
val key = k as? ObjString ?: scope.raiseIllegalArgument("map merge expects string keys; got $k")
|
map[k] = v
|
||||||
map[key] = v
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is ObjMapEntry -> {
|
is ObjMapEntry -> {
|
||||||
val key = other.key as? ObjString ?: scope.raiseIllegalArgument("map merge expects string keys; got ${other.key}")
|
map[other.key] = other.value
|
||||||
map[key] = other.value
|
|
||||||
}
|
}
|
||||||
is ObjList -> {
|
is ObjList -> {
|
||||||
// Treat as list of map entries
|
// Treat as list of map entries
|
||||||
@ -311,8 +312,7 @@ class ObjMap(val map: MutableMap<Obj, Obj> = mutableMapOf()) : Obj() {
|
|||||||
is ObjMapEntry -> e
|
is ObjMapEntry -> e
|
||||||
else -> scope.raiseIllegalArgument("map can only be merged with MapEntry elements; got $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[entry.key] = entry.value
|
||||||
map[key] = entry.value
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> scope.raiseIllegalArgument("map can only be merged with Map, MapEntry, or List<MapEntry>")
|
else -> scope.raiseIllegalArgument("map can only be merged with Map, MapEntry, or List<MapEntry>")
|
||||||
|
|||||||
@ -1656,10 +1656,8 @@ class MapLiteralRef(private val entries: List<MapLiteralEntry>) : ObjRef {
|
|||||||
is MapLiteralEntry.Spread -> {
|
is MapLiteralEntry.Spread -> {
|
||||||
val m = if (PerfFlags.RVAL_FASTPATH) e.ref.evalValue(scope) else e.ref.get(scope).value
|
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")
|
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) {
|
for ((k, v) in m.map) {
|
||||||
val sKey = k as? ObjString ?: scope.raiseIllegalArgument("spread map must have string keys; got $k")
|
result.map[k] = v
|
||||||
result.map[sKey] = v
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,7 +24,6 @@ import net.sergeych.lyng.ExecutionError
|
|||||||
import net.sergeych.lyng.ScriptError
|
import net.sergeych.lyng.ScriptError
|
||||||
import net.sergeych.lyng.eval
|
import net.sergeych.lyng.eval
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
|
||||||
import kotlin.test.assertFailsWith
|
import kotlin.test.assertFailsWith
|
||||||
|
|
||||||
class MapLiteralTest {
|
class MapLiteralTest {
|
||||||
@ -119,20 +118,21 @@ class MapLiteralTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun spreadNonStringKeysIsRuntimeError() = runTest {
|
fun spreadNonStringKeysIsAllowed() = runTest {
|
||||||
assertFailsWith<ExecutionError> {
|
eval("""
|
||||||
eval("""{ ...Map(1 => "x") }""")
|
val m = { ...Map(1 => "x") }
|
||||||
}
|
assertEquals("x", m[1])
|
||||||
|
""")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun mergeNonStringKeyIsRuntimeError() = runTest {
|
fun mergeNonStringKeyIsAllowed() = runTest {
|
||||||
assertFailsWith<ExecutionError> {
|
eval("""
|
||||||
eval("""
|
val e = (1 => "x")
|
||||||
val e = (1 => "x")
|
val m = { "a": 1 } + e
|
||||||
{ "a": 1 } + e
|
assertEquals(1, m["a"])
|
||||||
""")
|
assertEquals("x", m[1])
|
||||||
}
|
""")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@ -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
|
@Test
|
||||||
fun testBuffer() = runTest {
|
fun testBuffer() = runTest {
|
||||||
eval(
|
eval(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user