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

This commit is contained in:
Sergey Chernov 2026-03-12 21:07:56 +03:00
parent 9417f8f0cc
commit 8b5e6ee993
23 changed files with 1045 additions and 32 deletions

View File

@ -1,8 +1,8 @@
# Array # Array
It's an interface if the [Collection] that provides indexing access, like `array[3] = 0`. 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 Array therefore implements [Iterable] too. Well known implementations of `Array` are
[List]. [List] and [ImmutableList].
Array adds the following methods: 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 [Collection]: Collection.md
[Iterable]: Iterable.md [Iterable]: Iterable.md
[List]: List.md [List]: List.md
[ImmutableList]: ImmutableList.md

View File

@ -6,6 +6,12 @@ Is a [Iterable] with known `size`, a finite [Iterable]:
val size 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 | | name | description |
|------------------------|------------------------------------------------------| |------------------------|------------------------------------------------------|
@ -17,3 +23,7 @@ See [List], [Set], [Iterable] and [Efficient Iterables in Kotlin Interop](Effici
[Iterable]: Iterable.md [Iterable]: Iterable.md
[List]: List.md [List]: List.md
[Set]: Set.md [Set]: Set.md
[Map]: Map.md
[ImmutableList]: ImmutableList.md
[ImmutableSet]: ImmutableSet.md
[ImmutableMap]: ImmutableMap.md

37
docs/ImmutableList.md Normal file
View File

@ -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

36
docs/ImmutableMap.md Normal file
View File

@ -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

34
docs/ImmutableSet.md Normal file
View File

@ -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

View File

@ -147,12 +147,15 @@ Search for the first element that satisfies the given predicate:
| fun/method | description | | fun/method | description |
|------------------------|---------------------------------------------------------------------------------| |------------------------|---------------------------------------------------------------------------------|
| toList() | create a list from iterable | | toList() | create a list from iterable |
| toImmutableList() | create an immutable list from iterable |
| toSet() | create a set from iterable | | toSet() | create a set from iterable |
| toImmutableSet | create an immutable set from iterable |
| contains(i) | check that iterable contains `i` | | contains(i) | check that iterable contains `i` |
| `i in iterable` | same as `contains(i)` | | `i in iterable` | same as `contains(i)` |
| isEmpty() | check iterable is empty | | isEmpty() | check iterable is empty |
| forEach(f) | call f for each element | | forEach(f) | call f for each element |
| toMap() | create a map from list of key-value pairs (arrays of 2 items or like) | | 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` | | any(p) | true if any element matches predicate `p` |
| all(p) | true if all elements match 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 | | 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: ## 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 [Collection]: Collection.md
[List]: List.md [List]: List.md
[ImmutableList]: ImmutableList.md
[Flow]: parallelism.md#flow [Flow]: parallelism.md#flow
[Range]: Range.md [Range]: Range.md
[Set]: Set.md [Set]: Set.md
[ImmutableSet]: ImmutableSet.md
[Map]: Map.md
[ImmutableMap]: ImmutableMap.md
[RingBuffer]: RingBuffer.md [RingBuffer]: RingBuffer.md

View File

@ -1,6 +1,7 @@
# List built-in class # List built-in class
Mutable list of any objects. Mutable list of any objects.
For immutable list values, see [ImmutableList].
It's class in Lyng is `List`: It's class in Lyng is `List`:
@ -197,3 +198,4 @@ It inherits from [Iterable] too and thus all iterable methods are applicable to
[Range]: Range.md [Range]: Range.md
[Iterable]: Iterable.md [Iterable]: Iterable.md
[ImmutableList]: ImmutableList.md

View File

@ -3,6 +3,7 @@
Map is a mutable collection of key-value pairs, where keys are unique. You can create maps in two ways: 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 the constructor `Map(...)` or `.toMap()` helpers; and
- with map literals using braces: `{ "key": value, id: expr, id: }`. - 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]. 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. - When you need computed or non-string keys, use the constructor form `Map(...)`, map literals with computed keys (if supported), or build entries with `=>` and then merge.
[Collection](Collection.md) [Collection](Collection.md)
[ImmutableMap]: ImmutableMap.md

View File

@ -1,7 +1,8 @@
# List built-in class # Set built-in class
Mutable set of any objects: a group of different objects, no repetitions. Mutable set of any objects: a group of different objects, no repetitions.
Sets are not ordered, order of appearance does not matter. Sets are not ordered, order of appearance does not matter.
For immutable set values, see [ImmutableSet].
val set = Set(1,2,3, "foo") val set = Set(1,2,3, "foo")
assert( 1 in set ) assert( 1 in set )
@ -92,3 +93,4 @@ Also, it inherits methods from [Iterable].
[Range]: Range.md [Range]: Range.md
[ImmutableSet]: ImmutableSet.md

View File

@ -14,7 +14,7 @@ __Other documents to read__ maybe after this one:
- [time](time.md) and [parallelism](parallelism.md) - [time](time.md) and [parallelism](parallelism.md)
- [parallelism] - multithreaded code, coroutines, etc. - [parallelism] - multithreaded code, coroutines, etc.
- Some class - 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 - Some samples: [combinatorics](samples/combinatorics.lyng.md), national vars and
loops: [сумма ряда](samples/сумма_ряда.lyng.md). More at [samples folder](samples) 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], [List] is an implementation of the type `Array`, and through it `Collection` and [Iterable]. Please read [Iterable],
many collection based methods are implemented there. 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: 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 >>> void
Please see [Set] for detailed description. Please see [Set] for detailed description.
For immutable set values, use `set.toImmutable()` and [ImmutableSet].
# Maps # 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)`. - 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. Please see the [Map] reference for a deeper guide.
For immutable map values, use `map.toImmutable()` and [ImmutableMap].
# Flow control operators # Flow control operators
@ -1750,6 +1753,7 @@ Lambda avoid unnecessary execution if assertion is not failed. for example:
| π | See [math](math.md) | | π | See [math](math.md) |
[List]: List.md [List]: List.md
[ImmutableList]: ImmutableList.md
[Testing]: Testing.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 [string formatting]: https://github.com/sergeych/mp_stools?tab=readme-ov-file#sprintf-syntax-summary
[Set]: Set.md [Set]: Set.md
[ImmutableSet]: ImmutableSet.md
[Map]: Map.md [Map]: Map.md
[ImmutableMap]: ImmutableMap.md
[Buffer]: Buffer.md [Buffer]: Buffer.md

View File

@ -4339,7 +4339,7 @@ class Compiler(
val mapType = inferTypeDeclFromRef(entry.ref) ?: return TypeDecl.TypeAny to TypeDecl.TypeAny val mapType = inferTypeDeclFromRef(entry.ref) ?: return TypeDecl.TypeAny to TypeDecl.TypeAny
if (mapType is TypeDecl.Generic) { if (mapType is TypeDecl.Generic) {
val base = mapType.name.substringAfterLast('.') val base = mapType.name.substringAfterLast('.')
if (base == "Map") { if (base == "Map" || base == "ImmutableMap") {
val k = mapType.args.getOrNull(0) ?: TypeDecl.TypeAny val k = mapType.args.getOrNull(0) ?: TypeDecl.TypeAny
val v = mapType.args.getOrNull(1) ?: TypeDecl.TypeAny val v = mapType.args.getOrNull(1) ?: TypeDecl.TypeAny
addKey(k) addKey(k)
@ -4374,7 +4374,7 @@ class Compiler(
if (listType == TypeDecl.TypeAny || listType == TypeDecl.TypeNullableAny) return listType if (listType == TypeDecl.TypeAny || listType == TypeDecl.TypeNullableAny) return listType
if (listType is TypeDecl.Generic) { if (listType is TypeDecl.Generic) {
val base = listType.name.substringAfterLast('.') 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 return listType.args.firstOrNull() ?: TypeDecl.TypeAny
} }
} }
@ -4385,7 +4385,7 @@ class Compiler(
val generic = typeDecl as? TypeDecl.Generic ?: return null val generic = typeDecl as? TypeDecl.Generic ?: return null
val base = generic.name.substringAfterLast('.') val base = generic.name.substringAfterLast('.')
return when (base) { return when (base) {
"Set", "List", "Iterable", "Collection", "Array" -> generic.args.firstOrNull() "Set", "ImmutableSet", "List", "ImmutableList", "Iterable", "Collection", "Array" -> generic.args.firstOrNull()
else -> null else -> null
} }
} }
@ -4669,6 +4669,58 @@ class Compiler(
name == "next" && receiver is TypeDecl.Generic && base == "Iterator" -> { name == "next" && receiver is TypeDecl.Generic && base == "Iterator" -> {
receiver.args.firstOrNull() 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 else -> null
} }
} }
@ -4739,6 +4791,10 @@ class Compiler(
"matches" -> ObjBool.type "matches" -> ObjBool.type
"toInt", "toInt",
"toEpochSeconds" -> ObjInt.type "toEpochSeconds" -> ObjInt.type
"toImmutableList" -> ObjImmutableList.type
"toImmutableSet" -> ObjImmutableSet.type
"toImmutableMap" -> ObjImmutableMap.type
"toImmutable" -> Obj.rootObjectType
"toMutable" -> ObjMutableBuffer.type "toMutable" -> ObjMutableBuffer.type
"seq" -> ObjFlow.type "seq" -> ObjFlow.type
"encode" -> ObjBitBuffer.type "encode" -> ObjBitBuffer.type
@ -8248,8 +8304,11 @@ class Compiler(
"Bool" -> ObjBool.type "Bool" -> ObjBool.type
"Char" -> ObjChar.type "Char" -> ObjChar.type
"List" -> ObjList.type "List" -> ObjList.type
"ImmutableList" -> ObjImmutableList.type
"Map" -> ObjMap.type "Map" -> ObjMap.type
"ImmutableMap" -> ObjImmutableMap.type
"Set" -> ObjSet.type "Set" -> ObjSet.type
"ImmutableSet" -> ObjImmutableSet.type
"Range", "IntRange" -> ObjRange.type "Range", "IntRange" -> ObjRange.type
"Iterator" -> ObjIterator "Iterator" -> ObjIterator
"Iterable" -> ObjIterable "Iterable" -> ObjIterable

View File

@ -522,9 +522,12 @@ class Script(
addConst("Bool", ObjBool.type) addConst("Bool", ObjBool.type)
addConst("Char", ObjChar.type) addConst("Char", ObjChar.type)
addConst("List", ObjList.type) addConst("List", ObjList.type)
addConst("ImmutableList", ObjImmutableList.type)
addConst("Set", ObjSet.type) addConst("Set", ObjSet.type)
addConst("ImmutableSet", ObjImmutableSet.type)
addConst("Range", ObjRange.type) addConst("Range", ObjRange.type)
addConst("Map", ObjMap.type) addConst("Map", ObjMap.type)
addConst("ImmutableMap", ObjImmutableMap.type)
addConst("MapEntry", ObjMapEntry.type) addConst("MapEntry", ObjMapEntry.type)
@Suppress("RemoveRedundantQualifierName") @Suppress("RemoveRedundantQualifierName")
addConst("Callable", Statement.type) addConst("Callable", Statement.type)

View File

@ -4439,7 +4439,11 @@ class BytecodeCompiler(
} }
val initClass = when (localTarget?.name) { val initClass = when (localTarget?.name) {
"List" -> ObjList.type "List" -> ObjList.type
"ImmutableList" -> ObjImmutableList.type
"Map" -> ObjMap.type "Map" -> ObjMap.type
"ImmutableMap" -> ObjImmutableMap.type
"Set" -> ObjSet.type
"ImmutableSet" -> ObjImmutableSet.type
else -> null else -> null
} }
val callee = compileRefWithFallback(ref.target, null, refPosOrCurrent(ref.target)) ?: return null val callee = compileRefWithFallback(ref.target, null, refPosOrCurrent(ref.target)) ?: return null
@ -7304,8 +7308,11 @@ class BytecodeCompiler(
"Bool" -> ObjBool.type "Bool" -> ObjBool.type
"Char" -> ObjChar.type "Char" -> ObjChar.type
"List" -> ObjList.type "List" -> ObjList.type
"ImmutableList" -> ObjImmutableList.type
"Map" -> ObjMap.type "Map" -> ObjMap.type
"ImmutableMap" -> ObjImmutableMap.type
"Set" -> ObjSet.type "Set" -> ObjSet.type
"ImmutableSet" -> ObjImmutableSet.type
"Range", "IntRange" -> ObjRange.type "Range", "IntRange" -> ObjRange.type
"Iterator" -> ObjIterator "Iterator" -> ObjIterator
"Iterable" -> ObjIterable "Iterable" -> ObjIterable
@ -7379,7 +7386,9 @@ class BytecodeCompiler(
"iterator" -> ObjIterator "iterator" -> ObjIterator
"count" -> ObjInt.type "count" -> ObjInt.type
"toSet" -> ObjSet.type "toSet" -> ObjSet.type
"toImmutableSet" -> ObjImmutableSet.type
"toMap" -> ObjMap.type "toMap" -> ObjMap.type
"toImmutableMap" -> ObjImmutableMap.type
"joinToString" -> ObjString.type "joinToString" -> ObjString.type
"now", "now",
"truncateToSecond", "truncateToSecond",
@ -7406,6 +7415,8 @@ class BytecodeCompiler(
"matches" -> ObjBool.type "matches" -> ObjBool.type
"toInt", "toInt",
"toEpochSeconds" -> ObjInt.type "toEpochSeconds" -> ObjInt.type
"toImmutableList" -> ObjImmutableList.type
"toImmutable" -> Obj.rootObjectType
"toMutable" -> ObjMutableBuffer.type "toMutable" -> ObjMutableBuffer.type
"seq" -> ObjFlow.type "seq" -> ObjFlow.type
"encode" -> ObjBitBuffer.type "encode" -> ObjBitBuffer.type

View File

@ -37,8 +37,16 @@ object StdlibDocsBootstrap {
@Suppress("UNUSED_VARIABLE") @Suppress("UNUSED_VARIABLE")
val _list = net.sergeych.lyng.obj.ObjList.type val _list = net.sergeych.lyng.obj.ObjList.type
@Suppress("UNUSED_VARIABLE") @Suppress("UNUSED_VARIABLE")
val _immutableList = net.sergeych.lyng.obj.ObjImmutableList.type
@Suppress("UNUSED_VARIABLE")
val _map = net.sergeych.lyng.obj.ObjMap.type val _map = net.sergeych.lyng.obj.ObjMap.type
@Suppress("UNUSED_VARIABLE") @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 val _int = net.sergeych.lyng.obj.ObjInt.type
@Suppress("UNUSED_VARIABLE") @Suppress("UNUSED_VARIABLE")
val _real = net.sergeych.lyng.obj.ObjReal.type val _real = net.sergeych.lyng.obj.ObjReal.type

View File

@ -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
}
}
}
}

View File

@ -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 }
}
}
}

View File

@ -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 }
}
}
}

View File

@ -50,6 +50,21 @@ val ObjIterable by lazy {
ObjList(result) 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: // it is not effective, but it is open:
addFnDoc( addFnDoc(
name = "contains", 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( addPropertyDoc(
name = "toMap", name = "toMap",
doc = "Collect pairs into a map using [0] as key and [1] as value for each element.", 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( addFnDoc(
name = "associateBy", name = "associateBy",
doc = "Build a map from elements using the lambda result as key.", doc = "Build a map from elements using the lambda result as key.",

View File

@ -562,6 +562,14 @@ class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
} }
ObjInt((-1).toLong()) 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)
}
} }
} }
} }

View File

@ -107,8 +107,12 @@ class ObjMap(val map: MutableMap<Obj, Obj> = mutableMapOf()) : Obj() {
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
if (other !is ObjMap) return false val otherSize = when (other) {
if (map.size != other.map.size) return false is ObjMap -> other.map.size
is ObjImmutableMap -> other.map.size
else -> return false
}
if (map.size != otherSize) return false
for ((k, v) in map) { for ((k, v) in map) {
val otherV = other.getAt(scope, k) val otherV = other.getAt(scope, k)
if (otherV === ObjNull && !other.contains(scope, k)) return false 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 { override suspend fun compareTo(scope: Scope, other: Obj): Int {
if (other is ObjMap) { val otherMap = when (other) {
if (map == other.map) return 0 is ObjMap -> other.map
if (map.size != other.map.size) return map.size.compareTo(other.map.size) is ObjImmutableMap -> other.map
else -> 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 // for same size, if they are not equal, we don't have a stable order
// but let's try to be consistent // but let's try to be consistent
return map.toString().compareTo(other.map.toString()) return map.toString().compareTo(otherMap.toString())
}
return -1
} }
override suspend fun defaultToString(scope: Scope): ObjString { 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()) 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 map[k] = v
} }
} }
is ObjImmutableMap -> {
for ((k, v) in other.map) {
map[k] = v
}
}
is ObjMapEntry -> { is ObjMapEntry -> {
map[other.key] = other.value map[other.key] = other.value
} }

View File

@ -40,8 +40,12 @@ class ObjSet(val set: MutableSet<Obj> = mutableSetOf()) : Obj() {
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
if (other !is ObjSet) return false val otherSet = when (other) {
if (set.size != other.set.size) return false 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 // 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 // contains() in ObjSet uses equals(scope, ...), so we need to be careful
for (e in set) { 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 { override suspend fun mul(scope: Scope, other: Obj): Obj {
return if (other is ObjSet) { return when (other) {
ObjSet(set.intersect(other.set).toMutableSet()) is ObjSet -> ObjSet(set.intersect(other.set).toMutableSet())
} else is ObjImmutableSet -> ObjSet(set.intersect(other.toMutableSet()).toMutableSet())
scope.raiseIllegalArgument("set operator * requires another set") else -> scope.raiseIllegalArgument("set operator * requires another set")
}
} }
override suspend fun minus(scope: Scope, other: Obj): Obj { 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 { override suspend fun compareTo(scope: Scope, other: Obj): Int {
if (other is ObjSet) { val otherSet = when (other) {
if (set == other.set) return 0 is ObjSet -> other.set
if (set.size != other.set.size) return set.size.compareTo(other.set.size) is ObjImmutableSet -> other.toMutableSet()
return set.toString().compareTo(other.set.toString()) 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 { override fun hashCode(): Int {
@ -233,6 +240,14 @@ class ObjSet(val set: MutableSet<Obj> = mutableSetOf()) : Obj() {
for( x in args.list ) set -= x for( x in args.list ) set -= x
if( n == set.size ) ObjFalse else ObjTrue 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)
}
} }
} }
} }

View File

@ -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
"""
)
}
}
}

View File

@ -12,6 +12,11 @@ extern class Iterable<T> {
fun forEach(action: (T)->Void): Void fun forEach(action: (T)->Void): Void
fun map<R>(transform: (T)->R): List<R> fun map<R>(transform: (T)->R): List<R>
fun toList(): List<T> fun toList(): List<T>
fun toImmutableList(): ImmutableList<T>
val toSet: Set<T>
val toImmutableSet: ImmutableSet<T>
val toMap: Map<Object,Object>
val toImmutableMap: ImmutableMap<Object,Object>
} }
extern class Iterator<T> { extern class Iterator<T> {
@ -28,13 +33,19 @@ class KotlinIterator<T> : Iterator<T> {
} }
extern class Collection<T> : Iterable<T> { extern class Collection<T> : Iterable<T> {
val size: Int
} }
extern class Array<T> : Collection<T> { extern class Array<T> : Collection<T> {
} }
extern class ImmutableList<T> : Array<T> {
fun toMutable(): List<T>
}
extern class List<T> : Array<T> { extern class List<T> : Array<T> {
fun add(value: T, more...): Void fun add(value: T, more...): Void
fun toImmutable(): ImmutableList<T>
} }
extern class RingBuffer<T> : Iterable<T> { extern class RingBuffer<T> : Iterable<T> {
@ -44,12 +55,26 @@ extern class RingBuffer<T> : Iterable<T> {
} }
extern class Set<T> : Collection<T> { extern class Set<T> : Collection<T> {
fun toImmutable(): ImmutableSet<T>
} }
extern class Map<K,V> { extern class ImmutableSet<T> : Collection<T> {
fun toMutable(): Set<T>
} }
extern class MapEntry<K,V> extern class Map<K,V> : Collection<MapEntry<K,V>> {
fun toImmutable(): ImmutableMap<K,V>
}
extern class ImmutableMap<K,V> : Collection<MapEntry<K,V>> {
fun getOrNull(key: K): V?
fun toMutable(): Map<K,V>
}
extern class MapEntry<K,V> : Array<Object> {
val key: K
val value: V
}
// Built-in math helpers (implemented in host runtime). // Built-in math helpers (implemented in host runtime).
extern fun abs(x: Object): Real extern fun abs(x: Object): Real