Make List/Set += and -= element-type aware

This commit is contained in:
Sergey Chernov 2026-03-12 19:13:17 +03:00
parent 3bfb80a7c1
commit c9021eb9cf
3 changed files with 81 additions and 8 deletions

View File

@ -500,16 +500,12 @@ open class Scope(
return null return null
} }
/** private fun declaredCollectionElementTypeForValue(value: Obj, rawName: String): TypeDecl? {
* 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? {
var s: Scope? = this var s: Scope? = this
var hops = 0 var hops = 0
while (s != null && hops++ < 1024) { while (s != null && hops++ < 1024) {
val decl = s.declaredTypeForValueInThisScope(value) 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() return decl.args.firstOrNull()
} }
s = s.parent s = s.parent
@ -517,6 +513,20 @@ open class Scope(
return null 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<String, Int>, records: Map<String, ObjRecord>) { internal fun applySlotPlanReset(plan: Map<String, Int>, records: Map<String, ObjRecord>) {
if (plan.isEmpty()) return if (plan.isEmpty()) return
slots.clear() slots.clear()

View File

@ -30,6 +30,15 @@ import net.sergeych.lynon.LynonEncoder
import net.sergeych.lynon.LynonType import net.sergeych.lynon.LynonType
class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() { class ObjList(val list: MutableList<Obj> = 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 { override suspend fun equals(scope: Scope, other: Obj): Boolean {
if (this === other) return true if (this === other) return true
@ -127,7 +136,7 @@ class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
other is ObjList -> other is ObjList ->
ObjList((list + other.list).toMutableList()) ObjList((list + other.list).toMutableList())
other.isInstanceOf(ObjIterable) && other !is ObjString && other !is ObjBuffer -> { !shouldTreatAsSingleElement(scope, other) && other.isInstanceOf(ObjIterable) -> {
val l = other.callMethod<ObjList>(scope, "toList") val l = other.callMethod<ObjList>(scope, "toList")
ObjList((list + l.list).toMutableList()) ObjList((list + l.list).toMutableList())
} }
@ -143,7 +152,7 @@ class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
override suspend fun plusAssign(scope: Scope, other: Obj): Obj { override suspend fun plusAssign(scope: Scope, other: Obj): Obj {
if (other is ObjList) { if (other is ObjList) {
list.addAll(other.list) 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 val otherList = (other.invokeInstanceMethod(scope, "toList") as ObjList).list
list.addAll(otherList) list.addAll(otherList)
} else { } else {
@ -152,6 +161,43 @@ class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
return this 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<Obj>()
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<Obj>()
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 { override suspend fun contains(scope: Scope, other: Obj): Boolean {
if (net.sergeych.lyng.PerfFlags.PRIMITIVE_FASTOPS) { if (net.sergeych.lyng.PerfFlags.PRIMITIVE_FASTOPS) {
// Fast path: int membership in a list of ints (common case in benches) // Fast path: int membership in a list of ints (common case in benches)

View File

@ -416,6 +416,23 @@ class TypesTest {
""".trimIndent()) """.trimIndent())
} }
@Test
fun testListTyped() = runTest {
eval("""
var l = List<String>()
val typed: List<String> = 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 // @Test
// fun testAliasesInGenerics1() = runTest { // fun testAliasesInGenerics1() = runTest {
// val scope = Script.newScope() // val scope = Script.newScope()