From 8b5e6ee993218d0f308e038512d8dbc63904398e Mon Sep 17 00:00:00 2001 From: sergeych Date: Thu, 12 Mar 2026 21:07:56 +0300 Subject: [PATCH] Add immutable collections hierarchy with runtime/compiler/docs/tests --- docs/Array.md | 5 +- docs/Collection.md | 12 +- docs/ImmutableList.md | 37 ++++ docs/ImmutableMap.md | 36 ++++ docs/ImmutableSet.md | 34 +++ docs/Iterable.md | 9 +- docs/List.md | 4 +- docs/Map.md | 2 + docs/Set.md | 4 +- docs/tutorial.md | 8 +- .../kotlin/net/sergeych/lyng/Compiler.kt | 65 +++++- .../kotlin/net/sergeych/lyng/Script.kt | 3 + .../lyng/bytecode/BytecodeCompiler.kt | 11 + .../lyng/miniast/StdlibDocsBootstrap.kt | 8 + .../net/sergeych/lyng/obj/ObjImmutableList.kt | 197 ++++++++++++++++++ .../net/sergeych/lyng/obj/ObjImmutableMap.kt | 196 +++++++++++++++++ .../net/sergeych/lyng/obj/ObjImmutableSet.kt | 172 +++++++++++++++ .../net/sergeych/lyng/obj/ObjIterable.kt | 56 +++++ .../kotlin/net/sergeych/lyng/obj/ObjList.kt | 8 + .../kotlin/net/sergeych/lyng/obj/ObjMap.kt | 37 +++- .../kotlin/net/sergeych/lyng/obj/ObjSet.kt | 37 +++- .../kotlin/ImmutableCollectionsTest.kt | 107 ++++++++++ lynglib/stdlib/lyng/root.lyng | 29 ++- 23 files changed, 1045 insertions(+), 32 deletions(-) create mode 100644 docs/ImmutableList.md create mode 100644 docs/ImmutableMap.md create mode 100644 docs/ImmutableSet.md create mode 100644 lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjImmutableList.kt create mode 100644 lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjImmutableMap.kt create mode 100644 lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjImmutableSet.kt create mode 100644 lynglib/src/commonTest/kotlin/ImmutableCollectionsTest.kt diff --git a/docs/Array.md b/docs/Array.md index d828768..f043f57 100644 --- a/docs/Array.md +++ b/docs/Array.md @@ -1,8 +1,8 @@ # Array It's an interface if the [Collection] that provides indexing access, like `array[3] = 0`. -Array therefore implements [Iterable] too. The well known implementatino of the `Array` is -[List]. +Array therefore implements [Iterable] too. Well known implementations of `Array` are +[List] and [ImmutableList]. Array adds the following methods: @@ -35,3 +35,4 @@ To pre-sort and array use `Iterable.sorted*` or in-place `List.sort*` families, [Collection]: Collection.md [Iterable]: Iterable.md [List]: List.md +[ImmutableList]: ImmutableList.md diff --git a/docs/Collection.md b/docs/Collection.md index 0fbd3d8..442c457 100644 --- a/docs/Collection.md +++ b/docs/Collection.md @@ -6,6 +6,12 @@ Is a [Iterable] with known `size`, a finite [Iterable]: val size } +`Collection` is a read/traversal contract shared by mutable and immutable collections. +Concrete collection classes: + +- Mutable: [List], [Set], [Map] +- Immutable: [ImmutableList], [ImmutableSet], [ImmutableMap] + | name | description | |------------------------|------------------------------------------------------| @@ -16,4 +22,8 @@ See [List], [Set], [Iterable] and [Efficient Iterables in Kotlin Interop](Effici [Iterable]: Iterable.md [List]: List.md -[Set]: Set.md \ No newline at end of file +[Set]: Set.md +[Map]: Map.md +[ImmutableList]: ImmutableList.md +[ImmutableSet]: ImmutableSet.md +[ImmutableMap]: ImmutableMap.md diff --git a/docs/ImmutableList.md b/docs/ImmutableList.md new file mode 100644 index 0000000..3f02f72 --- /dev/null +++ b/docs/ImmutableList.md @@ -0,0 +1,37 @@ +# ImmutableList built-in class + +`ImmutableList` is an immutable, indexable list value. +It implements [Array], therefore [Collection] and [Iterable]. + +Use it when API contracts require a list that cannot be mutated through aliases. + +## Creating + + val a = ImmutableList(1,2,3) + val b = [1,2,3].toImmutable() + val c = (1..3).toImmutableList() + >>> void + +## Converting + + val i = ImmutableList(1,2,3) + val m = i.toMutable() + m += 4 + assertEquals( ImmutableList(1,2,3), i ) + assertEquals( [1,2,3,4], m ) + >>> void + +## Members + +| name | meaning | +|---------------|-----------------------------------------| +| `size` | number of elements | +| `[index]` | element access by index | +| `[Range]` | immutable slice | +| `+` | append element(s), returns new immutable list | +| `-` | remove element(s), returns new immutable list | +| `toMutable()` | create mutable copy | + +[Array]: Array.md +[Collection]: Collection.md +[Iterable]: Iterable.md diff --git a/docs/ImmutableMap.md b/docs/ImmutableMap.md new file mode 100644 index 0000000..c0e74f2 --- /dev/null +++ b/docs/ImmutableMap.md @@ -0,0 +1,36 @@ +# ImmutableMap built-in class + +`ImmutableMap` is an immutable map of key-value pairs. +It implements [Collection] and [Iterable] of [MapEntry]. + +## Creating + + val a = ImmutableMap("a" => 1, "b" => 2) + val b = Map("a" => 1, "b" => 2).toImmutable() + val c = ["a" => 1, "b" => 2].toImmutableMap + >>> void + +## Converting + + val i = ImmutableMap("a" => 1) + val m = i.toMutable() + m["a"] = 2 + assertEquals( 1, i["a"] ) + assertEquals( 2, m["a"] ) + >>> void + +## Members + +| name | meaning | +|-----------------|------------------------------------------| +| `size` | number of entries | +| `[key]` | get value by key, or `null` if absent | +| `getOrNull(key)`| same as `[key]` | +| `keys` | list of keys | +| `values` | list of values | +| `+` | merge (rightmost wins), returns new immutable map | +| `toMutable()` | create mutable copy | + +[Collection]: Collection.md +[Iterable]: Iterable.md +[MapEntry]: Map.md diff --git a/docs/ImmutableSet.md b/docs/ImmutableSet.md new file mode 100644 index 0000000..22a7d88 --- /dev/null +++ b/docs/ImmutableSet.md @@ -0,0 +1,34 @@ +# ImmutableSet built-in class + +`ImmutableSet` is an immutable set of unique elements. +It implements [Collection] and [Iterable]. + +## Creating + + val a = ImmutableSet(1,2,3) + val b = Set(1,2,3).toImmutable() + val c = [1,2,3].toImmutableSet + >>> void + +## Converting + + val i = ImmutableSet(1,2,3) + val m = i.toMutable() + m += 4 + assertEquals( ImmutableSet(1,2,3), i ) + assertEquals( Set(1,2,3,4), m ) + >>> void + +## Members + +| name | meaning | +|---------------|-----------------------------------------------------| +| `size` | number of elements | +| `contains(x)` | membership test | +| `+`, `union` | union, returns new immutable set | +| `-`, `subtract` | subtraction, returns new immutable set | +| `*`, `intersect` | intersection, returns new immutable set | +| `toMutable()` | create mutable copy | + +[Collection]: Collection.md +[Iterable]: Iterable.md diff --git a/docs/Iterable.md b/docs/Iterable.md index 8889dd2..f84ba74 100644 --- a/docs/Iterable.md +++ b/docs/Iterable.md @@ -147,12 +147,15 @@ Search for the first element that satisfies the given predicate: | fun/method | description | |------------------------|---------------------------------------------------------------------------------| | toList() | create a list from iterable | +| toImmutableList() | create an immutable list from iterable | | toSet() | create a set from iterable | +| toImmutableSet | create an immutable set from iterable | | contains(i) | check that iterable contains `i` | | `i in iterable` | same as `contains(i)` | | isEmpty() | check iterable is empty | | forEach(f) | call f for each element | | toMap() | create a map from list of key-value pairs (arrays of 2 items or like) | +| toImmutableMap | create an immutable map from list of key-value pairs | | any(p) | true if any element matches predicate `p` | | all(p) | true if all elements match predicate `p` | | map(f) | create a list of values returned by `f` called for each element of the iterable | @@ -206,16 +209,20 @@ For high-performance Kotlin-side interop and custom iterable implementation deta ## Implemented in classes: -- [List], [Range], [Buffer](Buffer.md), [BitBuffer], [Buffer], [Set], [RingBuffer] +- [List], [ImmutableList], [Range], [Buffer](Buffer.md), [BitBuffer], [Buffer], [Set], [ImmutableSet], [Map], [ImmutableMap], [RingBuffer] [Collection]: Collection.md [List]: List.md +[ImmutableList]: ImmutableList.md [Flow]: parallelism.md#flow [Range]: Range.md [Set]: Set.md +[ImmutableSet]: ImmutableSet.md +[Map]: Map.md +[ImmutableMap]: ImmutableMap.md [RingBuffer]: RingBuffer.md diff --git a/docs/List.md b/docs/List.md index e007f30..1a0a1cc 100644 --- a/docs/List.md +++ b/docs/List.md @@ -1,6 +1,7 @@ # List built-in class Mutable list of any objects. +For immutable list values, see [ImmutableList]. It's class in Lyng is `List`: @@ -196,4 +197,5 @@ It inherits from [Iterable] too and thus all iterable methods are applicable to [Range]: Range.md -[Iterable]: Iterable.md \ No newline at end of file +[Iterable]: Iterable.md +[ImmutableList]: ImmutableList.md diff --git a/docs/Map.md b/docs/Map.md index 9e2cdc8..faa5830 100644 --- a/docs/Map.md +++ b/docs/Map.md @@ -3,6 +3,7 @@ Map is a mutable collection of key-value pairs, where keys are unique. You can create maps in two ways: - with the constructor `Map(...)` or `.toMap()` helpers; and - with map literals using braces: `{ "key": value, id: expr, id: }`. +For immutable map values, see [ImmutableMap]. When constructing from a list, each list item must be a [Collection] with exactly 2 elements, for example, a [List]. @@ -177,3 +178,4 @@ Notes: - 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) +[ImmutableMap]: ImmutableMap.md diff --git a/docs/Set.md b/docs/Set.md index d297550..22d2bf1 100644 --- a/docs/Set.md +++ b/docs/Set.md @@ -1,7 +1,8 @@ -# List built-in class +# Set built-in class Mutable set of any objects: a group of different objects, no repetitions. Sets are not ordered, order of appearance does not matter. +For immutable set values, see [ImmutableSet]. val set = Set(1,2,3, "foo") assert( 1 in set ) @@ -92,3 +93,4 @@ Also, it inherits methods from [Iterable]. [Range]: Range.md +[ImmutableSet]: ImmutableSet.md diff --git a/docs/tutorial.md b/docs/tutorial.md index 90be4e5..a16c4f5 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -14,7 +14,7 @@ __Other documents to read__ maybe after this one: - [time](time.md) and [parallelism](parallelism.md) - [parallelism] - multithreaded code, coroutines, etc. - Some class - references: [List], [Set], [Map], [Real], [Range], [Iterable], [Iterator], [time manipulation](time.md), [Array], [RingBuffer], [Buffer]. + references: [List], [ImmutableList], [Set], [ImmutableSet], [Map], [ImmutableMap], [Real], [Range], [Iterable], [Iterator], [time manipulation](time.md), [Array], [RingBuffer], [Buffer]. - Some samples: [combinatorics](samples/combinatorics.lyng.md), national vars and loops: [сумма ряда](samples/сумма_ряда.lyng.md). More at [samples folder](samples) @@ -785,6 +785,7 @@ Lyng has built-in mutable array class `List` with simple literals: [List] is an implementation of the type `Array`, and through it `Collection` and [Iterable]. Please read [Iterable], many collection based methods are implemented there. +For immutable list values, use `list.toImmutable()` and [ImmutableList]. Lists can contain any type of objects, lists too: @@ -967,6 +968,7 @@ Set are unordered collection of unique elements, see [Set]. Sets are [Iterable] >>> void Please see [Set] for detailed description. +For immutable set values, use `set.toImmutable()` and [ImmutableSet]. # Maps @@ -1027,6 +1029,7 @@ Notes: - When you need computed (expression) keys or non-string keys, use `Map(...)` constructor with entries, e.g. `Map( ("a" + "b") => 1 )`, then merge with a literal if needed: `{ base: } + (computedKey => 42)`. Please see the [Map] reference for a deeper guide. +For immutable map values, use `map.toImmutable()` and [ImmutableMap]. # Flow control operators @@ -1750,6 +1753,7 @@ Lambda avoid unnecessary execution if assertion is not failed. for example: | π | See [math](math.md) | [List]: List.md +[ImmutableList]: ImmutableList.md [Testing]: Testing.md @@ -1766,8 +1770,10 @@ Lambda avoid unnecessary execution if assertion is not failed. for example: [string formatting]: https://github.com/sergeych/mp_stools?tab=readme-ov-file#sprintf-syntax-summary [Set]: Set.md +[ImmutableSet]: ImmutableSet.md [Map]: Map.md +[ImmutableMap]: ImmutableMap.md [Buffer]: Buffer.md diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 0fcb75c..8592995 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -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 diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt index 0a3e0ab..8ba2ffb 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt @@ -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) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt index d5134de..e9cecc4 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt @@ -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 diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/StdlibDocsBootstrap.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/StdlibDocsBootstrap.kt index 0c4f0f9..035b9cf 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/StdlibDocsBootstrap.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/StdlibDocsBootstrap.kt @@ -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 diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjImmutableList.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjImmutableList.kt new file mode 100644 index 0000000..8c72372 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjImmutableList.kt @@ -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 = emptyList()) : Obj() { + private val data: List = 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.. 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(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() + 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 = 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().toMutableList()) + } + addFnDoc( + name = "toImmutable", + doc = "Return this immutable list.", + returns = type("lyng.ImmutableList"), + moduleName = "lyng.stdlib" + ) { + thisObj + } + } + } +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjImmutableMap.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjImmutableMap.kt new file mode 100644 index 0000000..6f5ef28 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjImmutableMap.kt @@ -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 = emptyMap()) : Obj() { + val map: Map = 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, 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") + } + } + + fun toMutableMapCopy(): MutableMap = 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().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().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().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().map.values.toMutableList()) } + ) + addFnDoc( + name = "iterator", + doc = "Iterator over map entries as MapEntry objects.", + moduleName = "lyng.stdlib" + ) { + ObjKotlinIterator(thisAs().map.entries.iterator()) + } + addFnDoc( + name = "toMutable", + doc = "Create a mutable copy of this immutable map.", + returns = type("lyng.Map"), + moduleName = "lyng.stdlib" + ) { + ObjMap(thisAs().toMutableMapCopy()) + } + addFnDoc( + name = "toImmutable", + doc = "Return this immutable map.", + returns = type("lyng.ImmutableMap"), + moduleName = "lyng.stdlib" + ) { thisObj } + } + } +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjImmutableSet.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjImmutableSet.kt new file mode 100644 index 0000000..31f1e14 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjImmutableSet.kt @@ -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 = emptyList()) : Obj() { + private val data: Set = 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 = 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().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().mul(requireScope(), args.firstAndOnly()) + } + addFnDoc( + name = "iterator", + doc = "Iterator over elements of this immutable set.", + moduleName = "lyng.stdlib" + ) { + ObjKotlinObjIterator(thisAs().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().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().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().toMutableSet()) + } + addFnDoc( + name = "toImmutable", + doc = "Return this immutable set.", + returns = type("lyng.ImmutableSet"), + moduleName = "lyng.stdlib" + ) { thisObj } + } + } +} 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 db68060..4b03471 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjIterable.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjIterable.kt @@ -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() + 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() + 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() + 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.", 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 3a832e0..0f43e0b 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjList.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjList.kt @@ -562,6 +562,14 @@ class ObjList(val list: MutableList = 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().list) + } } } } 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 60aecd4..f174323 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjMap.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjMap.kt @@ -107,8 +107,12 @@ class ObjMap(val map: MutableMap = 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 = 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 = mutableMapOf()) : Obj() { ) { ObjKotlinIterator(thisAs().map.entries.iterator()) } + addFnDoc( + name = "toImmutable", + doc = "Create an immutable snapshot of this map.", + returns = type("lyng.ImmutableMap"), + moduleName = "lyng.stdlib" + ) { + ObjImmutableMap(thisAs().map) + } } } @@ -334,6 +348,11 @@ class ObjMap(val map: MutableMap = mutableMapOf()) : Obj() { map[k] = v } } + is ObjImmutableMap -> { + for ((k, v) in other.map) { + map[k] = v + } + } is ObjMapEntry -> { map[other.key] = other.value } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjSet.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjSet.kt index 8cd68f0..89eb56b 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjSet.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjSet.kt @@ -40,8 +40,12 @@ class ObjSet(val set: MutableSet = 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 = 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 = 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 = 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().set) + } } } } diff --git a/lynglib/src/commonTest/kotlin/ImmutableCollectionsTest.kt b/lynglib/src/commonTest/kotlin/ImmutableCollectionsTest.kt new file mode 100644 index 0000000..0c1a4a1 --- /dev/null +++ b/lynglib/src/commonTest/kotlin/ImmutableCollectionsTest.kt @@ -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) + assert(imm is Array) + assert(imm is Collection) + assert(imm is Iterable) + + 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) + assert(imm is Collection) + assert(imm is Iterable) + 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) + assert(imm is Collection>) + assert(imm is Iterable>) + 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 + """ + ) + } + } +} diff --git a/lynglib/stdlib/lyng/root.lyng b/lynglib/stdlib/lyng/root.lyng index f44abcf..d973396 100644 --- a/lynglib/stdlib/lyng/root.lyng +++ b/lynglib/stdlib/lyng/root.lyng @@ -12,6 +12,11 @@ extern class Iterable { fun forEach(action: (T)->Void): Void fun map(transform: (T)->R): List fun toList(): List + fun toImmutableList(): ImmutableList + val toSet: Set + val toImmutableSet: ImmutableSet + val toMap: Map + val toImmutableMap: ImmutableMap } extern class Iterator { @@ -28,13 +33,19 @@ class KotlinIterator : Iterator { } extern class Collection : Iterable { + val size: Int } extern class Array : Collection { } +extern class ImmutableList : Array { + fun toMutable(): List +} + extern class List : Array { fun add(value: T, more...): Void + fun toImmutable(): ImmutableList } extern class RingBuffer : Iterable { @@ -44,12 +55,26 @@ extern class RingBuffer : Iterable { } extern class Set : Collection { + fun toImmutable(): ImmutableSet } -extern class Map { +extern class ImmutableSet : Collection { + fun toMutable(): Set } -extern class MapEntry +extern class Map : Collection> { + fun toImmutable(): ImmutableMap +} + +extern class ImmutableMap : Collection> { + fun getOrNull(key: K): V? + fun toMutable(): Map +} + +extern class MapEntry : Array { + val key: K + val value: V +} // Built-in math helpers (implemented in host runtime). extern fun abs(x: Object): Real