diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt index a4e62d0..f1688ee 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt @@ -500,16 +500,12 @@ open class Scope( return null } - /** - * Best-effort lookup of the declared Set element type for a runtime set instance. - * Returns null when type info is unavailable. - */ - fun declaredSetElementTypeForValue(value: Obj): TypeDecl? { + private fun declaredCollectionElementTypeForValue(value: Obj, rawName: String): TypeDecl? { var s: Scope? = this var hops = 0 while (s != null && hops++ < 1024) { val decl = s.declaredTypeForValueInThisScope(value) - if (decl is TypeDecl.Generic && decl.name.substringAfterLast('.') == "Set") { + if (decl is TypeDecl.Generic && decl.name.substringAfterLast('.') == rawName) { return decl.args.firstOrNull() } s = s.parent @@ -517,6 +513,20 @@ open class Scope( return null } + /** + * Best-effort lookup of the declared Set element type for a runtime set instance. + * Returns null when type info is unavailable. + */ + fun declaredSetElementTypeForValue(value: Obj): TypeDecl? = + declaredCollectionElementTypeForValue(value, "Set") + + /** + * Best-effort lookup of the declared List element type for a runtime list instance. + * Returns null when type info is unavailable. + */ + fun declaredListElementTypeForValue(value: Obj): TypeDecl? = + declaredCollectionElementTypeForValue(value, "List") + internal fun applySlotPlanReset(plan: Map, records: Map) { if (plan.isEmpty()) return slots.clear() diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjList.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjList.kt index 5db0acd..3a832e0 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjList.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjList.kt @@ -30,6 +30,15 @@ import net.sergeych.lynon.LynonEncoder import net.sergeych.lynon.LynonType class ObjList(val list: MutableList = mutableListOf()) : Obj() { + private fun shouldTreatAsSingleElement(scope: Scope, other: Obj): Boolean { + if (!other.isInstanceOf(ObjIterable)) return true + val declaredElementType = scope.declaredListElementTypeForValue(this) + if (declaredElementType != null && matchesTypeDecl(scope, other, declaredElementType)) { + return true + } + if (other is ObjString || other is ObjBuffer) return true + return false + } override suspend fun equals(scope: Scope, other: Obj): Boolean { if (this === other) return true @@ -127,7 +136,7 @@ class ObjList(val list: MutableList = mutableListOf()) : Obj() { other is ObjList -> ObjList((list + other.list).toMutableList()) - other.isInstanceOf(ObjIterable) && other !is ObjString && other !is ObjBuffer -> { + !shouldTreatAsSingleElement(scope, other) && other.isInstanceOf(ObjIterable) -> { val l = other.callMethod(scope, "toList") ObjList((list + l.list).toMutableList()) } @@ -143,7 +152,7 @@ class ObjList(val list: MutableList = mutableListOf()) : Obj() { override suspend fun plusAssign(scope: Scope, other: Obj): Obj { if (other is ObjList) { list.addAll(other.list) - } else if (other.isInstanceOf(ObjIterable) && other !is ObjString && other !is ObjBuffer) { + } else if (!shouldTreatAsSingleElement(scope, other) && other.isInstanceOf(ObjIterable)) { val otherList = (other.invokeInstanceMethod(scope, "toList") as ObjList).list list.addAll(otherList) } else { @@ -152,6 +161,43 @@ class ObjList(val list: MutableList = mutableListOf()) : Obj() { return this } + override suspend fun minus(scope: Scope, other: Obj): Obj { + val out = list.toMutableList() + if (shouldTreatAsSingleElement(scope, other)) { + out.remove(other) + return ObjList(out) + } + if (other.isInstanceOf(ObjIterable)) { + val toRemove = mutableSetOf() + other.enumerate(scope) { + toRemove += it + true + } + out.removeAll { toRemove.contains(it) } + return ObjList(out) + } + out.remove(other) + return ObjList(out) + } + + override suspend fun minusAssign(scope: Scope, other: Obj): Obj { + if (shouldTreatAsSingleElement(scope, other)) { + list.remove(other) + return this + } + if (other.isInstanceOf(ObjIterable)) { + val toRemove = mutableSetOf() + other.enumerate(scope) { + toRemove += it + true + } + list.removeAll { toRemove.contains(it) } + return this + } + list.remove(other) + return this + } + override suspend fun contains(scope: Scope, other: Obj): Boolean { if (net.sergeych.lyng.PerfFlags.PRIMITIVE_FASTOPS) { // Fast path: int membership in a list of ints (common case in benches) diff --git a/lynglib/src/commonTest/kotlin/TypesTest.kt b/lynglib/src/commonTest/kotlin/TypesTest.kt index 860c41c..d0275ab 100644 --- a/lynglib/src/commonTest/kotlin/TypesTest.kt +++ b/lynglib/src/commonTest/kotlin/TypesTest.kt @@ -416,6 +416,23 @@ class TypesTest { """.trimIndent()) } + @Test + fun testListTyped() = runTest { + eval(""" + var l = List() + val typed: List = l + assertEquals(List(), typed) + + l += "foo" + assertEquals(List("foo"), l) + l -= "foo" + assertEquals(List(), l) + + l += ["foo", "bar"] + assertEquals(List("foo", "bar"), l) + """.trimIndent()) + } + // @Test // fun testAliasesInGenerics1() = runTest { // val scope = Script.newScope()