From 950e8301c331348c13214e52d806a6ba942348d5 Mon Sep 17 00:00:00 2001 From: sergeych Date: Wed, 2 Jul 2025 20:45:30 +0300 Subject: [PATCH] associateBy and Lyng:Iterable .toFlow() for kotlin integration simplicity --- docs/Iterable.md | 15 ++++++++++++ docs/tutorial.md | 2 +- .../kotlin/net/sergeych/lyng/Compiler.kt | 1 - .../kotlin/net/sergeych/lyng/Obj.kt | 10 ++++++++ .../kotlin/net/sergeych/lyng/ObjIterable.kt | 24 ++++++++++++------- .../net/sergeych/lyng/ObjKotlinIterator.kt | 18 ++++++++++++++ .../kotlin/net/sergeych/lyng/ObjMap.kt | 2 +- .../kotlin/net/sergeych/lyng/ObjString.kt | 3 +++ .../kotlin/net/sergeych/lyng/statements.kt | 2 ++ lynglib/src/commonTest/kotlin/ScriptTest.kt | 21 ++++++++++++++++ 10 files changed, 86 insertions(+), 12 deletions(-) diff --git a/docs/Iterable.md b/docs/Iterable.md index 46ec24c..8be8a2f 100644 --- a/docs/Iterable.md +++ b/docs/Iterable.md @@ -21,6 +21,20 @@ Having `Iterable` in base classes allows to use it in for loop. Also, each `Iter ## Instance methods +| fun/method | description | +|-----------------|---------------------------------------------------------------------------------| +| toList() | create a list from iterable | +| toSet() | create a set from iterable | +| contains(i) | check that iterable contains `i` | +| `i in iterator` | 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) | +| map(f) | create a list of values returned by `f` called for each element of the iterable | +| indexOf(i) | return index if the first encounter of i or a negative value if not found | +| associateBy(kf) | create a map where keys are returned by kf that will be called for each element | + + fun Iterable.toList(): List fun Iterable.toSet(): Set fun Iterable.indexOf(element): Int @@ -28,6 +42,7 @@ Having `Iterable` in base classes allows to use it in for loop. Also, each `Iter fun Iterable.isEmpty(element): Bool fun Iterable.forEach(block: (Any?)->Void ): Void fun Iterable.map(block: (Any?)->Void ): List + fun Iterable.associateBy( keyMaker: (Any?)->Any): Map ## Abstract methods diff --git a/docs/tutorial.md b/docs/tutorial.md index 8cc033a..6e0e328 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -1215,7 +1215,7 @@ Typical set of String functions includes: | s1 + s2 | concatenation | | s1 += s2 | self-modifying concatenation | | toReal() | attempts to parse string as a Real value | -| | | +| toInt() | parse string to Int value | | | | diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 096db37..84d0421 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -158,7 +158,6 @@ class Compiler( isCall = true val lambda = parseLambdaExpression(cc) - println(cc.current()) operand = Accessor { context -> context.pos = next.pos val v = left.getter(context).value diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Obj.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Obj.kt index 8fa30d7..1237cc2 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Obj.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Obj.kt @@ -87,6 +87,14 @@ open class Obj { // note that getInstanceMember traverses the hierarchy objClass.getInstanceMember(context.pos, name).value.invoke(context, this, args) + open suspend fun getInstanceMethod( + context: Context, + name: String, + args: Arguments = Arguments.EMPTY + ): Obj = + // note that getInstanceMember traverses the hierarchy + objClass.getInstanceMember(context.pos, name).value + fun getMemberOrNull(name: String): Obj? = objClass.getInstanceMemberOrNull(name)?.value // methods that to override @@ -210,6 +218,8 @@ open class Obj { context.raiseNotImplemented("indexing") } + suspend fun getAt(context: Context, index: Int): Obj = getAt(context, ObjInt(index.toLong())) + open suspend fun putAt(context: Context, index: Int, newValue: Obj) { context.raiseNotImplemented("indexing") } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjIterable.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjIterable.kt index c99c78b..3bdb25e 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjIterable.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjIterable.kt @@ -48,12 +48,20 @@ val ObjIterable by lazy { } addFn("toMap") { - val result = mutableListOf() - val it = thisObj.invokeInstanceMethod(this, "iterator") - while (it.invokeInstanceMethod(this, "hasNext").toBool()) { - result += it.invokeInstanceMethod(this, "next") + val result = ObjMap() + thisObj.toFlow(this).collect { pair -> + result.map[pair.getAt(this,0)] = pair.getAt(this, 1) } - ObjMap(ObjMap.listToMap(this, result)) + result + } + + addFn("associateBy") { + val association = requireOnlyArg() + val result = ObjMap() + thisObj.toFlow(this).collect { + result.map[association.call(this, it)] = it + } + result } addFn("forEach", isOpen = true) { @@ -67,12 +75,10 @@ val ObjIterable by lazy { } addFn("map", isOpen = true) { - val it = thisObj.invokeInstanceMethod(this, "iterator") val fn = requiredArg(0) val result = mutableListOf() - while (it.invokeInstanceMethod(this, "hasNext").toBool()) { - val x = it.invokeInstanceMethod(this, "next") - result += fn.execute(this.copy(Arguments(listOf(x)))) + thisObj.toFlow(this).collect { + result += fn.call(this, it) } ObjList(result) } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjKotlinIterator.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjKotlinIterator.kt index 1b300c0..bb3e301 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjKotlinIterator.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjKotlinIterator.kt @@ -2,6 +2,9 @@ package net.sergeych.lyng +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + /** * Iterator wrapper to allow Kotlin collections to be returned from Lyng objects; * each object is converted to a Lyng object. @@ -36,4 +39,19 @@ class ObjKotlinObjIterator(val iterator: Iterator) : Obj() { } } +} + +/** + * Convert Lyng's Iterable to Kotlin's Flow. + * + * As Lyng is totally asynchronous, its iterator can't be trivially converted to Kotlin's synchronous iterator. + * It is, though, trivially convertible to Kotlin's Flow. + */ +fun Obj.toFlow(context: Context): Flow = flow { + val iterator = invokeInstanceMethod(context, "iterator") + val hasNext = iterator.getInstanceMethod(context, "hasNext") + val next = iterator.getInstanceMethod(context, "next") + while (hasNext.invoke(context, iterator).toBool()) { + emit(next.invoke(context, iterator)) + } } \ No newline at end of file diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjMap.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjMap.kt index b110c82..d214af2 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjMap.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjMap.kt @@ -34,7 +34,7 @@ class ObjMapEntry(val key: Obj, val value: Obj) : Obj() { } } -class ObjMap(val map: MutableMap) : Obj() { +class ObjMap(val map: MutableMap = mutableMapOf()) : Obj() { override val objClass = type diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjString.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjString.kt index 29ae702..dfefc3b 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjString.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjString.kt @@ -76,6 +76,9 @@ data class ObjString(val value: String) : Obj() { companion object { val type = ObjClass("String").apply { + addFn("toInt") { + ObjInt(thisAs().value.toLong()) + } addFn("startsWith") { ObjBool(thisAs().value.startsWith(requiredArg(0).value)) } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/statements.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/statements.kt index 17c96e2..97d8aac 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/statements.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/statements.kt @@ -34,6 +34,8 @@ abstract class Statement( val type = ObjClass("Callable") } + suspend fun call(context: Context,vararg args: Obj) = execute(context.copy(args = Arguments(*args))) + } fun Statement.raise(text: String): Nothing { diff --git a/lynglib/src/commonTest/kotlin/ScriptTest.kt b/lynglib/src/commonTest/kotlin/ScriptTest.kt index f34f68b..2455bd9 100644 --- a/lynglib/src/commonTest/kotlin/ScriptTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptTest.kt @@ -1,4 +1,6 @@ +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.toList import kotlinx.coroutines.test.runTest import net.sergeych.lyng.* import kotlin.test.* @@ -2311,4 +2313,23 @@ class ScriptTest { assert( ! "5.2".isInteger() ) """.trimIndent()) } + + @Test + fun testToFlow() = runTest() { + val c = Context() + val arr = c.eval("[1,2,3]") + // array is iterable so we can: + assertEquals(listOf(1,2,3), arr.toFlow(c).map { it.toInt() }.toList()) + } + + @Test + fun testAssociateBy() = runTest() { + eval(""" + val m = [123, 456].associateBy { "k:%s"(it) } + println(m) + assertEquals(123, m["k:123"]) + assertEquals(456, m["k:456"]) + """) + listOf(1,2,3).associateBy { it * 10 } + } } \ No newline at end of file