Add immutable collections hierarchy with runtime/compiler/docs/tests

This commit is contained in:
sergeych committed 2026-03-12 21:07:56 +03:00
1 parent 9417f8f0cc
commit 8b5e6ee993
23 files changed
+1045 -32

No files matched your search

@@ -4339,7 +4339,7 @@ class Compiler(
val mapType = inferTypeDeclFromRef(entry.ref) ?: return TypeDecl.TypeAny to TypeDecl.TypeAny
if (mapType is TypeDecl.Generic) {
val base = mapType.name.substringAfterLast('.')
if (base == "Map") {
if (base == "Map" || base == "ImmutableMap") {
val k = mapType.args.getOrNull(0) ?: TypeDecl.TypeAny
val v = mapType.args.getOrNull(1) ?: TypeDecl.TypeAny
addKey(k)
@@ -4374,7 +4374,7 @@ class Compiler(
if (listType == TypeDecl.TypeAny || listType == TypeDecl.TypeNullableAny) return listType
if (listType is TypeDecl.Generic) {
val base = listType.name.substringAfterLast('.')
if (base == "List" || base == "Array" || base == "Iterable") {
if (base == "List" || base == "ImmutableList" || base == "Array" || base == "Iterable") {
return listType.args.firstOrNull() ?: TypeDecl.TypeAny
}
}
@@ -4385,7 +4385,7 @@ class Compiler(
val generic = typeDecl as? TypeDecl.Generic ?: return null
val base = generic.name.substringAfterLast('.')
return when (base) {
"Set", "List", "Iterable", "Collection", "Array" -> generic.args.firstOrNull()
"Set", "ImmutableSet", "List", "ImmutableList", "Iterable", "Collection", "Array" -> generic.args.firstOrNull()
else -> null
}
}
@@ -4669,6 +4669,58 @@ class Compiler(
name == "next" && receiver is TypeDecl.Generic && base == "Iterator" -> {
receiver.args.firstOrNull()
}
name == "toImmutableList" && receiver is TypeDecl.Generic && (base == "Iterable" || base == "Collection" || base == "Array" || base == "List" || base == "ImmutableList") -> {
val arg = receiver.args.firstOrNull() ?: TypeDecl.TypeAny
TypeDecl.Generic("ImmutableList", listOf(arg), false)
}
name == "toList" && receiver is TypeDecl.Generic && (base == "ImmutableList" || base == "List") -> {
val arg = receiver.args.firstOrNull() ?: TypeDecl.TypeAny
TypeDecl.Generic("List", listOf(arg), false)
}
name == "toMutable" && receiver is TypeDecl.Generic && base == "ImmutableList" -> {
val arg = receiver.args.firstOrNull() ?: TypeDecl.TypeAny
TypeDecl.Generic("List", listOf(arg), false)
}
name == "toImmutable" && receiver is TypeDecl.Generic && base == "List" -> {
val arg = receiver.args.firstOrNull() ?: TypeDecl.TypeAny
TypeDecl.Generic("ImmutableList", listOf(arg), false)
}
name == "toImmutableSet" && receiver is TypeDecl.Generic && (base == "Iterable" || base == "Collection" || base == "Set" || base == "ImmutableSet") -> {
val arg = receiver.args.firstOrNull() ?: TypeDecl.TypeAny
TypeDecl.Generic("ImmutableSet", listOf(arg), false)
}
name == "toSet" && receiver is TypeDecl.Generic && (base == "Iterable" || base == "Collection" || base == "Set" || base == "ImmutableSet") -> {
val arg = receiver.args.firstOrNull() ?: TypeDecl.TypeAny
TypeDecl.Generic("Set", listOf(arg), false)
}
name == "toMutable" && receiver is TypeDecl.Generic && base == "ImmutableSet" -> {
val arg = receiver.args.firstOrNull() ?: TypeDecl.TypeAny
TypeDecl.Generic("Set", listOf(arg), false)
}
name == "toImmutable" && receiver is TypeDecl.Generic && base == "Set" -> {
val arg = receiver.args.firstOrNull() ?: TypeDecl.TypeAny
TypeDecl.Generic("ImmutableSet", listOf(arg), false)
}
name == "toImmutableMap" && receiver is TypeDecl.Generic && base == "Iterable" -> {
TypeDecl.Generic("ImmutableMap", listOf(TypeDecl.TypeAny, TypeDecl.TypeAny), false)
}
name == "toMap" && receiver is TypeDecl.Generic && base == "Iterable" -> {
TypeDecl.Generic("Map", listOf(TypeDecl.TypeAny, TypeDecl.TypeAny), false)
}
name == "toMutable" && receiver is TypeDecl.Generic && base == "ImmutableMap" -> {
val args = receiver.args.ifEmpty { listOf(TypeDecl.TypeAny, TypeDecl.TypeAny) }
TypeDecl.Generic("Map", args, false)
}
name == "toImmutable" && receiver is TypeDecl.Generic && base == "Map" -> {
val args = receiver.args.ifEmpty { listOf(TypeDecl.TypeAny, TypeDecl.TypeAny) }
TypeDecl.Generic("ImmutableMap", args, false)
}
name == "toImmutable" && base == "List" -> TypeDecl.Simple("ImmutableList", false)
name == "toMutable" && base == "ImmutableList" -> TypeDecl.Simple("List", false)
name == "toImmutable" && base == "Set" -> TypeDecl.Simple("ImmutableSet", false)
name == "toMutable" && base == "ImmutableSet" -> TypeDecl.Simple("Set", false)
name == "toImmutable" && base == "Map" -> TypeDecl.Simple("ImmutableMap", false)
name == "toMutable" && base == "ImmutableMap" -> TypeDecl.Simple("Map", false)
else -> null
}
}
@@ -4739,6 +4791,10 @@ class Compiler(
"matches" -> ObjBool.type
"toInt",
"toEpochSeconds" -> ObjInt.type
"toImmutableList" -> ObjImmutableList.type
"toImmutableSet" -> ObjImmutableSet.type
"toImmutableMap" -> ObjImmutableMap.type
"toImmutable" -> Obj.rootObjectType
"toMutable" -> ObjMutableBuffer.type
"seq" -> ObjFlow.type
"encode" -> ObjBitBuffer.type
@@ -8248,8 +8304,11 @@ class Compiler(
"Bool" -> ObjBool.type
"Char" -> ObjChar.type
"List" -> ObjList.type
"ImmutableList" -> ObjImmutableList.type
"Map" -> ObjMap.type
"ImmutableMap" -> ObjImmutableMap.type
"Set" -> ObjSet.type
"ImmutableSet" -> ObjImmutableSet.type
"Range", "IntRange" -> ObjRange.type
"Iterator" -> ObjIterator
"Iterable" -> ObjIterable
@@ -522,9 +522,12 @@ class Script(
addConst("Bool", ObjBool.type)
addConst("Char", ObjChar.type)
addConst("List", ObjList.type)
addConst("ImmutableList", ObjImmutableList.type)
addConst("Set", ObjSet.type)
addConst("ImmutableSet", ObjImmutableSet.type)
addConst("Range", ObjRange.type)
addConst("Map", ObjMap.type)
addConst("ImmutableMap", ObjImmutableMap.type)
addConst("MapEntry", ObjMapEntry.type)
@Suppress("RemoveRedundantQualifierName")
addConst("Callable", Statement.type)
@@ -4439,7 +4439,11 @@ class BytecodeCompiler(
}
val initClass = when (localTarget?.name) {
"List" -> ObjList.type
"ImmutableList" -> ObjImmutableList.type
"Map" -> ObjMap.type
"ImmutableMap" -> ObjImmutableMap.type
"Set" -> ObjSet.type
"ImmutableSet" -> ObjImmutableSet.type
else -> null
}
val callee = compileRefWithFallback(ref.target, null, refPosOrCurrent(ref.target)) ?: return null
@@ -7304,8 +7308,11 @@ class BytecodeCompiler(
"Bool" -> ObjBool.type
"Char" -> ObjChar.type
"List" -> ObjList.type
"ImmutableList" -> ObjImmutableList.type
"Map" -> ObjMap.type
"ImmutableMap" -> ObjImmutableMap.type
"Set" -> ObjSet.type
"ImmutableSet" -> ObjImmutableSet.type
"Range", "IntRange" -> ObjRange.type
"Iterator" -> ObjIterator
"Iterable" -> ObjIterable
@@ -7379,7 +7386,9 @@ class BytecodeCompiler(
"iterator" -> ObjIterator
"count" -> ObjInt.type
"toSet" -> ObjSet.type
"toImmutableSet" -> ObjImmutableSet.type
"toMap" -> ObjMap.type
"toImmutableMap" -> ObjImmutableMap.type
"joinToString" -> ObjString.type
"now",
"truncateToSecond",
@@ -7406,6 +7415,8 @@ class BytecodeCompiler(
"matches" -> ObjBool.type
"toInt",
"toEpochSeconds" -> ObjInt.type
"toImmutableList" -> ObjImmutableList.type
"toImmutable" -> Obj.rootObjectType
"toMutable" -> ObjMutableBuffer.type
"seq" -> ObjFlow.type
"encode" -> ObjBitBuffer.type
@@ -37,8 +37,16 @@ object StdlibDocsBootstrap {
@Suppress("UNUSED_VARIABLE")
val _list = net.sergeych.lyng.obj.ObjList.type
@Suppress("UNUSED_VARIABLE")
val _immutableList = net.sergeych.lyng.obj.ObjImmutableList.type
@Suppress("UNUSED_VARIABLE")
val _map = net.sergeych.lyng.obj.ObjMap.type
@Suppress("UNUSED_VARIABLE")
val _immutableMap = net.sergeych.lyng.obj.ObjImmutableMap.type
@Suppress("UNUSED_VARIABLE")
val _set = net.sergeych.lyng.obj.ObjSet.type
@Suppress("UNUSED_VARIABLE")
val _immutableSet = net.sergeych.lyng.obj.ObjImmutableSet.type
@Suppress("UNUSED_VARIABLE")
val _int = net.sergeych.lyng.obj.ObjInt.type
@Suppress("UNUSED_VARIABLE")
val _real = net.sergeych.lyng.obj.ObjReal.type
@@ -0,0 +1,197 @@
/*
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package net.sergeych.lyng.obj
import net.sergeych.lyng.Scope
import net.sergeych.lyng.miniast.addFnDoc
import net.sergeych.lyng.miniast.addPropertyDoc
import net.sergeych.lyng.miniast.type
import net.sergeych.lynon.LynonDecoder
import net.sergeych.lynon.LynonEncoder
import net.sergeych.lynon.LynonType
class ObjImmutableList(items: List<Obj> = emptyList()) : Obj() {
private val data: List<Obj> = items.toList()
override val objClass: ObjClass
get() = type
override suspend fun equals(scope: Scope, other: Obj): Boolean {
if (this === other) return true
return when (other) {
is ObjImmutableList -> data.size == other.data.size && data.indices.all { i -> data[i].equals(scope, other.data[i]) }
else -> {
if (other.isInstanceOf(ObjIterable)) compareTo(scope, other) == 0 else false
}
}
}
override suspend fun compareTo(scope: Scope, other: Obj): Int {
if (other is ObjImmutableList) {
val mySize = data.size
val otherSize = other.data.size
val commonSize = minOf(mySize, otherSize)
for (i in 0..<commonSize) {
val d = data[i].compareTo(scope, other.data[i])
if (d != 0) return d
}
return mySize.compareTo(otherSize)
}
if (other.isInstanceOf(ObjIterable)) {
val it1 = data.iterator()
val it2 = other.invokeInstanceMethod(scope, "iterator")
val hasNext2 = it2.getInstanceMethod(scope, "hasNext")
val next2 = it2.getInstanceMethod(scope, "next")
while (it1.hasNext()) {
if (!hasNext2.invoke(scope, it2).toBool()) return 1
val d = it1.next().compareTo(scope, next2.invoke(scope, it2))
if (d != 0) return d
}
return if (hasNext2.invoke(scope, it2).toBool()) -1 else 0
}
return -2
}
override suspend fun getAt(scope: Scope, index: Obj): Obj {
return when (index) {
is ObjInt -> data[index.toInt()]
is ObjRange -> {
when {
index.start is ObjInt && index.end is ObjInt -> {
if (index.isEndInclusive)
ObjImmutableList(data.subList(index.start.toInt(), index.end.toInt() + 1))
else
ObjImmutableList(data.subList(index.start.toInt(), index.end.toInt()))
}
index.isOpenStart && !index.isOpenEnd -> {
if (index.isEndInclusive)
ObjImmutableList(data.subList(0, index.end!!.toInt() + 1))
else
ObjImmutableList(data.subList(0, index.end!!.toInt()))
}
index.isOpenEnd && !index.isOpenStart -> ObjImmutableList(data.subList(index.start!!.toInt(), data.size))
index.isOpenStart && index.isOpenEnd -> ObjImmutableList(data)
else -> throw RuntimeException("Can't apply range for index: $index")
}
}
else -> scope.raiseIllegalArgument("Illegal index object for immutable list: ${index.inspect(scope)}")
}
}
override suspend fun contains(scope: Scope, other: Obj): Boolean {
if (net.sergeych.lyng.PerfFlags.PRIMITIVE_FASTOPS && other is ObjInt) {
var i = 0
val sz = data.size
while (i < sz) {
val v = data[i]
if (v is ObjInt && v.value == other.value) return true
i++
}
return false
}
return data.contains(other)
}
override suspend fun enumerate(scope: Scope, callback: suspend (Obj) -> Boolean) {
for (item in data) {
if (!callback(item)) break
}
}
override suspend fun plus(scope: Scope, other: Obj): Obj {
return when {
other is ObjImmutableList -> ObjImmutableList(data + other.data)
other is ObjList -> ObjImmutableList(data + other.list)
other.isInstanceOf(ObjIterable) && other !is ObjString && other !is ObjBuffer -> {
val l = other.callMethod<ObjList>(scope, "toList")
ObjImmutableList(data + l.list)
}
else -> ObjImmutableList(data + other)
}
}
override suspend fun minus(scope: Scope, other: Obj): Obj {
if (other !is ObjString && other !is ObjBuffer && other.isInstanceOf(ObjIterable)) {
val toRemove = mutableSetOf<Obj>()
other.enumerate(scope) {
toRemove += it
true
}
return ObjImmutableList(data.filterNot { toRemove.contains(it) })
}
val out = data.toMutableList()
out.remove(other)
return ObjImmutableList(out)
}
override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) {
encoder.encodeAnyList(scope, data)
}
override suspend fun lynonType(): LynonType = LynonType.List
override suspend fun defaultToString(scope: Scope): ObjString {
return ObjString(buildString {
append("ImmutableList(")
var first = true
for (v in data) {
if (first) first = false else append(",")
append(v.toString(scope).value)
}
append(")")
})
}
fun toMutableList(): MutableList<Obj> = data.toMutableList()
companion object {
val type = object : ObjClass("ImmutableList", ObjArray) {
override suspend fun callOn(scope: Scope): Obj {
return ObjImmutableList(scope.args.list)
}
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj {
return ObjImmutableList(decoder.decodeAnyList(scope))
}
}.apply {
addPropertyDoc(
name = "size",
doc = "Number of elements in this immutable list.",
type = type("lyng.Int"),
moduleName = "lyng.stdlib",
getter = { (this.thisObj as ObjImmutableList).data.size.toObj() }
)
addFnDoc(
name = "toMutable",
doc = "Create a mutable copy of this immutable list.",
returns = type("lyng.List"),
moduleName = "lyng.stdlib"
) {
ObjList(thisAs<ObjImmutableList>().toMutableList())
}
addFnDoc(
name = "toImmutable",
doc = "Return this immutable list.",
returns = type("lyng.ImmutableList"),
moduleName = "lyng.stdlib"
) {
thisObj
}
}
}
}
@@ -0,0 +1,196 @@
/*
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package net.sergeych.lyng.obj
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import net.sergeych.lyng.Scope
import net.sergeych.lyng.miniast.ParamDoc
import net.sergeych.lyng.miniast.TypeGenericDoc
import net.sergeych.lyng.miniast.addFnDoc
import net.sergeych.lyng.miniast.addPropertyDoc
import net.sergeych.lyng.miniast.type
import net.sergeych.lynon.LynonDecoder
import net.sergeych.lynon.LynonEncoder
import net.sergeych.lynon.LynonType
class ObjImmutableMap(entries: Map<Obj, Obj> = emptyMap()) : Obj() {
val map: Map<Obj, Obj> = LinkedHashMap(entries)
override val objClass get() = type
override suspend fun equals(scope: Scope, other: Obj): Boolean {
if (this === other) return true
val otherMap = when (other) {
is ObjImmutableMap -> other.map
is ObjMap -> other.map
else -> return false
}
if (map.size != otherMap.size) return false
for ((k, v) in map) {
val ov = other.getAt(scope, k)
if (ov === ObjNull && !other.contains(scope, k)) return false
if (!v.equals(scope, ov)) return false
}
return true
}
override suspend fun compareTo(scope: Scope, other: Obj): Int {
val otherMap = when (other) {
is ObjImmutableMap -> other.map
is ObjMap -> other.map
else -> return -1
}
if (map == otherMap) return 0
if (map.size != otherMap.size) return map.size.compareTo(otherMap.size)
return map.toString().compareTo(otherMap.toString())
}
override suspend fun getAt(scope: Scope, index: Obj): Obj = map[index] ?: ObjNull
override suspend fun contains(scope: Scope, other: Obj): Boolean = other in map
override suspend fun defaultToString(scope: Scope): ObjString {
val rendered = buildString {
append("ImmutableMap(")
var first = true
for ((k, v) in map) {
if (!first) append(",")
append(k.inspect(scope))
append(" => ")
append(v.toString(scope).value)
first = false
}
append(")")
}
return ObjString(rendered)
}
override suspend fun lynonType(): LynonType = LynonType.Map
override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) {
val keys = map.keys.map { it.toObj() }
val values = map.values.map { it.toObj() }
encoder.encodeAnyList(scope, keys)
encoder.encodeAnyList(scope, values, fixedSize = true)
}
override suspend fun toJson(scope: Scope): JsonElement {
return JsonObject(map.map { it.key.toString(scope).value to it.value.toJson(scope) }.toMap())
}
override suspend fun plus(scope: Scope, other: Obj): Obj {
val out = LinkedHashMap(map)
mergeIn(scope, out, other)
return ObjImmutableMap(out)
}
private suspend fun mergeIn(scope: Scope, out: MutableMap<Obj, Obj>, other: Obj) {
when (other) {
is ObjImmutableMap -> out.putAll(other.map)
is ObjMap -> out.putAll(other.map)
is ObjMapEntry -> out[other.key] = other.value
is ObjList -> {
for (e in other.list) {
when (e) {
is ObjMapEntry -> out[e.key] = e.value
else -> {
if (e.isInstanceOf(ObjArray)) {
if (e.invokeInstanceMethod(scope, "size").toInt() != 2)
scope.raiseIllegalArgument("Array element to merge into map must have 2 elements, got $e")
out[e.getAt(scope, 0)] = e.getAt(scope, 1)
} else {
scope.raiseIllegalArgument("map can only be merged with MapEntry elements; got $e")
}
}
}
}
}
else -> scope.raiseIllegalArgument("map can only be merged with Map, ImmutableMap, MapEntry, or List<MapEntry>")
}
}
fun toMutableMapCopy(): MutableMap<Obj, Obj> = LinkedHashMap(map)
companion object {
val type = object : ObjClass("ImmutableMap", ObjCollection) {
override suspend fun callOn(scope: Scope): Obj {
return ObjImmutableMap(ObjMap.listToMap(scope, scope.args.list))
}
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj {
val keys = decoder.decodeAnyList(scope)
val values = decoder.decodeAnyList(scope, fixedSize = keys.size)
if (keys.size != values.size) scope.raiseIllegalArgument("map keys and values should be same size")
return ObjImmutableMap(keys.zip(values).toMap())
}
}.apply {
addFnDoc(
name = "getOrNull",
doc = "Get value by key or return null if the key is absent.",
params = listOf(ParamDoc("key")),
returns = type("lyng.Any", nullable = true),
moduleName = "lyng.stdlib"
) {
thisAs<ObjImmutableMap>().map[args.firstAndOnly(pos)] ?: ObjNull
}
addPropertyDoc(
name = "size",
doc = "Number of entries in the immutable map.",
type = type("lyng.Int"),
moduleName = "lyng.stdlib",
getter = { thisAs<ObjImmutableMap>().map.size.toObj() }
)
addPropertyDoc(
name = "keys",
doc = "List of keys in this immutable map.",
type = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.Any"))),
moduleName = "lyng.stdlib",
getter = { thisAs<ObjImmutableMap>().map.keys.toObj() }
)
addPropertyDoc(
name = "values",
doc = "List of values in this immutable map.",
type = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.Any"))),
moduleName = "lyng.stdlib",
getter = { ObjList(thisAs<ObjImmutableMap>().map.values.toMutableList()) }
)
addFnDoc(
name = "iterator",
doc = "Iterator over map entries as MapEntry objects.",
moduleName = "lyng.stdlib"
) {
ObjKotlinIterator(thisAs<ObjImmutableMap>().map.entries.iterator())
}
addFnDoc(
name = "toMutable",
doc = "Create a mutable copy of this immutable map.",
returns = type("lyng.Map"),
moduleName = "lyng.stdlib"
) {
ObjMap(thisAs<ObjImmutableMap>().toMutableMapCopy())
}
addFnDoc(
name = "toImmutable",
doc = "Return this immutable map.",
returns = type("lyng.ImmutableMap"),
moduleName = "lyng.stdlib"
) { thisObj }
}
}
}
@@ -0,0 +1,172 @@
/*
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package net.sergeych.lyng.obj
import net.sergeych.lyng.Scope
import net.sergeych.lyng.miniast.ParamDoc
import net.sergeych.lyng.miniast.addFnDoc
import net.sergeych.lyng.miniast.type
import net.sergeych.lynon.LynonDecoder
import net.sergeych.lynon.LynonEncoder
import net.sergeych.lynon.LynonType
class ObjImmutableSet(items: Collection<Obj> = emptyList()) : Obj() {
private val data: Set<Obj> = LinkedHashSet(items)
override val objClass get() = type
override suspend fun equals(scope: Scope, other: Obj): Boolean {
if (this === other) return true
val otherSet = when (other) {
is ObjImmutableSet -> other.data
is ObjSet -> other.set
else -> return false
}
if (data.size != otherSet.size) return false
for (e in data) {
if (!other.contains(scope, e)) return false
}
return true
}
override suspend fun compareTo(scope: Scope, other: Obj): Int {
val otherSet = when (other) {
is ObjImmutableSet -> other.data
is ObjSet -> other.set
else -> return -2
}
if (data == otherSet) return 0
if (data.size != otherSet.size) return data.size.compareTo(otherSet.size)
return data.toString().compareTo(otherSet.toString())
}
override suspend fun contains(scope: Scope, other: Obj): Boolean = data.contains(other)
override suspend fun enumerate(scope: Scope, callback: suspend (Obj) -> Boolean) {
for (item in data) {
if (!callback(item)) break
}
}
override suspend fun plus(scope: Scope, other: Obj): Obj {
val merged = LinkedHashSet(data)
when {
other is ObjImmutableSet -> merged.addAll(other.data)
other is ObjSet -> merged.addAll(other.set)
other is ObjString || other is ObjBuffer || !other.isInstanceOf(ObjIterable) -> merged.add(other)
else -> other.enumerate(scope) { merged += it; true }
}
return ObjImmutableSet(merged)
}
override suspend fun minus(scope: Scope, other: Obj): Obj {
val out = LinkedHashSet(data)
when {
other is ObjImmutableSet -> out.removeAll(other.data)
other is ObjSet -> out.removeAll(other.set)
other is ObjString || other is ObjBuffer || !other.isInstanceOf(ObjIterable) -> out.remove(other)
else -> other.enumerate(scope) { out.remove(it); true }
}
return ObjImmutableSet(out)
}
override suspend fun mul(scope: Scope, other: Obj): Obj {
val right = when (other) {
is ObjImmutableSet -> other.data
is ObjSet -> other.set
else -> scope.raiseIllegalArgument("set operator * requires another set")
}
return ObjImmutableSet(data.intersect(right))
}
override suspend fun lynonType(): LynonType = LynonType.Set
override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) {
encoder.encodeAnyList(scope, data.toList())
}
fun toMutableSet(): MutableSet<Obj> = LinkedHashSet(data)
companion object {
val type: ObjClass = object : ObjClass("ImmutableSet", ObjCollection) {
override suspend fun callOn(scope: Scope): Obj {
return ObjImmutableSet(scope.args.list)
}
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj =
ObjImmutableSet(decoder.decodeAnyList(scope))
}.apply {
addFnDoc(
name = "size",
doc = "Number of elements in this immutable set.",
returns = type("lyng.Int"),
moduleName = "lyng.stdlib"
) {
thisAs<ObjImmutableSet>().data.size.toObj()
}
addFnDoc(
name = "intersect",
doc = "Intersection with another set. Returns a new immutable set.",
params = listOf(ParamDoc("other")),
returns = type("lyng.ImmutableSet"),
moduleName = "lyng.stdlib"
) {
thisAs<ObjImmutableSet>().mul(requireScope(), args.firstAndOnly())
}
addFnDoc(
name = "iterator",
doc = "Iterator over elements of this immutable set.",
moduleName = "lyng.stdlib"
) {
ObjKotlinObjIterator(thisAs<ObjImmutableSet>().data.iterator())
}
addFnDoc(
name = "union",
doc = "Union with another set or iterable. Returns a new immutable set.",
params = listOf(ParamDoc("other")),
returns = type("lyng.ImmutableSet"),
moduleName = "lyng.stdlib"
) {
thisAs<ObjImmutableSet>().plus(requireScope(), args.firstAndOnly())
}
addFnDoc(
name = "subtract",
doc = "Subtract another set or iterable from this set. Returns a new immutable set.",
params = listOf(ParamDoc("other")),
returns = type("lyng.ImmutableSet"),
moduleName = "lyng.stdlib"
) {
thisAs<ObjImmutableSet>().minus(requireScope(), args.firstAndOnly())
}
addFnDoc(
name = "toMutable",
doc = "Create a mutable copy of this immutable set.",
returns = type("lyng.Set"),
moduleName = "lyng.stdlib"
) {
ObjSet(thisAs<ObjImmutableSet>().toMutableSet())
}
addFnDoc(
name = "toImmutable",
doc = "Return this immutable set.",
returns = type("lyng.ImmutableSet"),
moduleName = "lyng.stdlib"
) { thisObj }
}
}
}
@@ -50,6 +50,21 @@ val ObjIterable by lazy {
ObjList(result)
}
addFnDoc(
name = "toImmutableList",
doc = "Collect elements of this iterable into a new immutable list.",
returns = type("lyng.ImmutableList"),
moduleName = "lyng.stdlib"
) {
val scope = requireScope()
val result = mutableListOf<Obj>()
val it = thisObj.invokeInstanceMethod(scope, "iterator")
while (it.invokeInstanceMethod(scope, "hasNext").toBool()) {
result.add(it.invokeInstanceMethod(scope, "next"))
}
ObjImmutableList(result)
}
// it is not effective, but it is open:
addFnDoc(
name = "contains",
@@ -109,6 +124,28 @@ val ObjIterable by lazy {
}
)
addPropertyDoc(
name = "toImmutableSet",
doc = "Collect elements of this iterable into a new immutable set.",
type = type("lyng.ImmutableSet"),
moduleName = "lyng.stdlib",
getter = {
when (val self = this.thisObj) {
is ObjImmutableSet -> self
is ObjSet -> ObjImmutableSet(self.set)
else -> {
val result = mutableSetOf<Obj>()
val scope = requireScope()
val it = self.invokeInstanceMethod(scope, "iterator")
while (it.invokeInstanceMethod(scope, "hasNext").toBool()) {
result.add(it.invokeInstanceMethod(scope, "next"))
}
ObjImmutableSet(result)
}
}
}
)
addPropertyDoc(
name = "toMap",
doc = "Collect pairs into a map using [0] as key and [1] as value for each element.",
@@ -128,6 +165,25 @@ val ObjIterable by lazy {
}
)
addPropertyDoc(
name = "toImmutableMap",
doc = "Collect pairs into an immutable map using [0] as key and [1] as value for each element.",
type = type("lyng.ImmutableMap"),
moduleName = "lyng.stdlib",
getter = {
val result = linkedMapOf<Obj, Obj>()
val scope = requireScope()
this.thisObj.enumerate(scope) { pair ->
when (pair) {
is ObjMapEntry -> result[pair.key] = pair.value
else -> result[pair.getAt(scope, 0)] = pair.getAt(scope, 1)
}
true
}
ObjImmutableMap(result)
}
)
addFnDoc(
name = "associateBy",
doc = "Build a map from elements using the lambda result as key.",
@@ -562,6 +562,14 @@ class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
}
ObjInt((-1).toLong())
}
addFnDoc(
name = "toImmutable",
doc = "Create an immutable snapshot of this list.",
returns = type("lyng.ImmutableList"),
moduleName = "lyng.stdlib"
) {
ObjImmutableList(thisAs<ObjList>().list)
}
}
}
}
@@ -107,8 +107,12 @@ class ObjMap(val map: MutableMap<Obj, Obj> = mutableMapOf()) : Obj() {
override suspend fun equals(scope: Scope, other: Obj): Boolean {
if (this === other) return true
if (other !is ObjMap) return false
if (map.size != other.map.size) return false
val otherSize = when (other) {
is ObjMap -> other.map.size
is ObjImmutableMap -> other.map.size
else -> return false
}
if (map.size != otherSize) return false
for ((k, v) in map) {
val otherV = other.getAt(scope, k)
if (otherV === ObjNull && !other.contains(scope, k)) return false
@@ -131,14 +135,16 @@ class ObjMap(val map: MutableMap<Obj, Obj> = mutableMapOf()) : Obj() {
}
override suspend fun compareTo(scope: Scope, other: Obj): Int {
if (other is ObjMap) {
if (map == other.map) return 0
if (map.size != other.map.size) return map.size.compareTo(other.map.size)
// for same size, if they are not equal, we don't have a stable order
// but let's try to be consistent
return map.toString().compareTo(other.map.toString())
val otherMap = when (other) {
is ObjMap -> other.map
is ObjImmutableMap -> other.map
else -> return -1
}
return -1
if (map == otherMap) return 0
if (map.size != otherMap.size) return map.size.compareTo(otherMap.size)
// for same size, if they are not equal, we don't have a stable order
// but let's try to be consistent
return map.toString().compareTo(otherMap.toString())
}
override suspend fun defaultToString(scope: Scope): ObjString {
@@ -311,6 +317,14 @@ class ObjMap(val map: MutableMap<Obj, Obj> = mutableMapOf()) : Obj() {
) {
ObjKotlinIterator(thisAs<ObjMap>().map.entries.iterator())
}
addFnDoc(
name = "toImmutable",
doc = "Create an immutable snapshot of this map.",
returns = type("lyng.ImmutableMap"),
moduleName = "lyng.stdlib"
) {
ObjImmutableMap(thisAs<ObjMap>().map)
}
}
}
@@ -334,6 +348,11 @@ class ObjMap(val map: MutableMap<Obj, Obj> = mutableMapOf()) : Obj() {
map[k] = v
}
}
is ObjImmutableMap -> {
for ((k, v) in other.map) {
map[k] = v
}
}
is ObjMapEntry -> {
map[other.key] = other.value
}
@@ -40,8 +40,12 @@ class ObjSet(val set: MutableSet<Obj> = mutableSetOf()) : Obj() {
override suspend fun equals(scope: Scope, other: Obj): Boolean {
if (this === other) return true
if (other !is ObjSet) return false
if (set.size != other.set.size) return false
val otherSet = when (other) {
is ObjSet -> other.set
is ObjImmutableSet -> other.toMutableSet()
else -> return false
}
if (set.size != otherSet.size) return false
// Sets are equal if all my elements are in other and vice versa
// contains() in ObjSet uses equals(scope, ...), so we need to be careful
for (e in set) {
@@ -115,10 +119,11 @@ class ObjSet(val set: MutableSet<Obj> = mutableSetOf()) : Obj() {
}
override suspend fun mul(scope: Scope, other: Obj): Obj {
return if (other is ObjSet) {
ObjSet(set.intersect(other.set).toMutableSet())
} else
scope.raiseIllegalArgument("set operator * requires another set")
return when (other) {
is ObjSet -> ObjSet(set.intersect(other.set).toMutableSet())
is ObjImmutableSet -> ObjSet(set.intersect(other.toMutableSet()).toMutableSet())
else -> scope.raiseIllegalArgument("set operator * requires another set")
}
}
override suspend fun minus(scope: Scope, other: Obj): Obj {
@@ -144,12 +149,14 @@ class ObjSet(val set: MutableSet<Obj> = mutableSetOf()) : Obj() {
}
override suspend fun compareTo(scope: Scope, other: Obj): Int {
if (other is ObjSet) {
if (set == other.set) return 0
if (set.size != other.set.size) return set.size.compareTo(other.set.size)
return set.toString().compareTo(other.set.toString())
val otherSet = when (other) {
is ObjSet -> other.set
is ObjImmutableSet -> other.toMutableSet()
else -> return -2
}
return -2
if (set == otherSet) return 0
if (set.size != otherSet.size) return set.size.compareTo(otherSet.size)
return set.toString().compareTo(otherSet.toString())
}
override fun hashCode(): Int {
@@ -233,6 +240,14 @@ class ObjSet(val set: MutableSet<Obj> = mutableSetOf()) : Obj() {
for( x in args.list ) set -= x
if( n == set.size ) ObjFalse else ObjTrue
}
addFnDoc(
name = "toImmutable",
doc = "Create an immutable snapshot of this set.",
returns = type("lyng.ImmutableSet"),
moduleName = "lyng.stdlib"
) {
ObjImmutableSet(thisAs<ObjSet>().set)
}
}
}
}
@@ -0,0 +1,107 @@
/*
* Copyright 2026 Sergey S. Chernov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.eval
import kotlin.test.Test
import kotlin.test.assertFails
class ImmutableCollectionsTest {
@Test
fun immutableListSnapshotAndConversion() = runTest {
eval(
"""
val src = [1,2,3]
val imm = src.toImmutable()
assert(imm is ImmutableList<Int>)
assert(imm is Array<Int>)
assert(imm is Collection<Int>)
assert(imm is Iterable<Int>)
src += 4
assertEquals(3, imm.size)
assertEquals([1,2,3], imm.toMutable())
assertEquals([1,2,3], (1..3).toImmutableList().toMutable())
"""
)
}
@Test
fun immutableSetSnapshotAndConversion() = runTest {
eval(
"""
val src = Set(1,2,3)
val imm = src.toImmutable()
assert(imm is ImmutableSet<Int>)
assert(imm is Collection<Int>)
assert(imm is Iterable<Int>)
src += 4
assertEquals(3, imm.size)
assertEquals(Set(1,2,3), imm.toMutable())
assertEquals(Set(1,2,3), [1,2,3].toImmutableSet.toMutable())
"""
)
}
@Test
fun immutableMapSnapshotAndConversion() = runTest {
eval(
"""
val src = Map("a" => 1, "b" => 2)
val imm = src.toImmutable()
assert(imm is ImmutableMap<String,Int>)
assert(imm is Collection<MapEntry<String,Int>>)
assert(imm is Iterable<MapEntry<String,Int>>)
src["a"] = 100
assertEquals(1, imm["a"])
assertEquals(Map("a" => 1, "b" => 2), imm.toMutable())
val imm2 = ["x" => 10, "y" => 20].toImmutableMap
assertEquals(10, imm2["x"])
assertEquals(Map("x" => 10, "y" => 20), imm2.toMutable())
"""
)
}
@Test
fun immutableCollectionsRejectMutationOps() = runTest {
assertFails {
eval(
"""
val xs = ImmutableList(1,2,3)
xs += 4
"""
)
}
assertFails {
eval(
"""
val s = ImmutableSet(1,2,3)
s += 4
"""
)
}
assertFails {
eval(
"""
val m = ImmutableMap("a" => 1)
m["a"] = 10
"""
)
}
}
}