From 1a90b25b1e22b5294977c83a583461f93ffdf4fc Mon Sep 17 00:00:00 2001 From: sergeych Date: Fri, 8 Aug 2025 19:19:50 +0300 Subject: [PATCH] fix #51 ref #48 flows started. bad closure-based bug fixed --- docs/parallelism.md | 52 +++++ lynglib/build.gradle.kts | 1 + .../kotlin/net/sergeych/lyng/AppliedScope.kt | 19 -- .../net/sergeych/lyng/ArgsDeclaration.kt | 3 +- .../kotlin/net/sergeych/lyng/Arguments.kt | 2 +- .../kotlin/net/sergeych/lyng/ClosureScope.kt | 16 ++ .../kotlin/net/sergeych/lyng/Compiler.kt | 22 ++- .../kotlin/net/sergeych/lyng/Scope.kt | 20 +- .../kotlin/net/sergeych/lyng/Script.kt | 15 +- .../kotlin/net/sergeych/lyng/ScriptError.kt | 2 + .../kotlin/net/sergeych/lyng/obj/Obj.kt | 16 +- .../kotlin/net/sergeych/lyng/obj/ObjClass.kt | 2 +- .../kotlin/net/sergeych/lyng/obj/ObjFlow.kt | 141 ++++++++++++++ .../net/sergeych/lyng/obj/ObjInstance.kt | 5 +- .../net/sergeych/lyng/obj/ObjIterable.kt | 20 +- .../net/sergeych/lyng/obj/ObjIterator.kt | 3 +- .../sergeych/lyng/obj/ObjKotlinIterator.kt | 22 +++ .../kotlin/net/sergeych/lyng/obj/ObjString.kt | 8 +- .../src/commonTest/kotlin/CoroutinesTest.kt | 51 +++++ lynglib/src/commonTest/kotlin/ScriptTest.kt | 179 +++++++++++++++--- 20 files changed, 533 insertions(+), 66 deletions(-) delete mode 100644 lynglib/src/commonMain/kotlin/net/sergeych/lyng/AppliedScope.kt create mode 100644 lynglib/src/commonMain/kotlin/net/sergeych/lyng/ClosureScope.kt create mode 100644 lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjFlow.kt diff --git a/docs/parallelism.md b/docs/parallelism.md index baf5cb1..c1f0c0e 100644 --- a/docs/parallelism.md +++ b/docs/parallelism.md @@ -128,3 +128,55 @@ Usage example: yield() } while(true) } + +# Data exchange for coroutines + +## Flow + +Flow is an async cold sequence; it is named after kotlin's Flow as it resembles it closely. The cold means the flow is only evaluated when iterated (collected, in Kotlin terms), before it is inactive. Sequence means that it is potentially unlimited, as in our example of glorious Fibonacci number generator: + + // Fibonacch numbers flow! + val f = flow { + println("Starting generator") + var n1 = 0 + var n2 = 1 + emit(n1) + emit(n2) + while(true) { + val n = n1 + n2 + emit(n) + n1 = n2 + n2 = n + } + } + val correctFibs = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765] + println("Generation starts") + assertEquals( correctFibs, f.take(correctFibs.size)) + >>> Generation starts + >>> Starting generator + >>> void + +Great: the generator is not executed until collected bu the `f.take()` call, which picks specified number of elements from the flow, can cancel it. + +Important difference from the channels or like, every time you collect the flow, you collect it anew: + + val f = flow { + emit("start") + (1..4).forEach { emit(it) } + } + // let's collect flow: + val result = [] + for( x in f ) result += x + println(result) + + // let's collect it once again: + println(f.toList()) + + // and again: + //assertEquals( result, f.toList() ) + + >>> ["start", 1, 2, 3, 4] + >>> ["start", 1, 2, 3, 4] + >>> void + +1 diff --git a/lynglib/build.gradle.kts b/lynglib/build.gradle.kts index 48043fb..e6f95c1 100644 --- a/lynglib/build.gradle.kts +++ b/lynglib/build.gradle.kts @@ -65,6 +65,7 @@ kotlin { languageSettings.optIn("kotlin.contracts.ExperimentalContracts") languageSettings.optIn("kotlin.ExperimentalUnsignedTypes") languageSettings.optIn("kotlin.coroutines.DelicateCoroutinesApi") + languageSettings.optIn("kotlinx.coroutines.flow.DelicateCoroutinesApi") } val commonMain by getting { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/AppliedScope.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/AppliedScope.kt deleted file mode 100644 index 8ff1abf..0000000 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/AppliedScope.kt +++ /dev/null @@ -1,19 +0,0 @@ -package net.sergeych.lyng - -import net.sergeych.lyng.obj.ObjRecord - -/** - * Special version of the [Scope] used to `apply` new this object to - * _parent context property. - * - * @param _parent context to apply to - * @param args arguments for the new context - * @param appliedScope the new context to apply, it will have lower priority except for `this` which - * will be reset by appliedContext's `this`. - */ -class AppliedScope(_parent: Scope, args: Arguments, val appliedScope: Scope) - : Scope(_parent, args, appliedScope.pos, appliedScope.thisObj) { - override fun get(name: String): ObjRecord? = - if (name == "this") thisObj.asReadonly - else super.get(name) ?: appliedScope[name] -} \ No newline at end of file diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ArgsDeclaration.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ArgsDeclaration.kt index 97ed68a..bea5ad2 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ArgsDeclaration.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ArgsDeclaration.kt @@ -34,7 +34,8 @@ data class ArgsDeclaration(val params: List, val endTokenType: Token.Type) defaultRecordType: ObjRecord.Type = ObjRecord.Type.ConstructorField ) { fun assign(a: Item, value: Obj) { - scope.addItem(a.name, (a.accessType ?: defaultAccessType).isMutable, value, + scope.addItem(a.name, (a.accessType ?: defaultAccessType).isMutable, + value.byValueCopy(), a.visibility ?: defaultVisibility, recordType = defaultRecordType) } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Arguments.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Arguments.kt index 56e3558..55cb5ca 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Arguments.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Arguments.kt @@ -36,7 +36,7 @@ data class Arguments(val list: List, val tailBlockMode: Boolean = false) : fun firstAndOnly(pos: Pos = Pos.UNKNOWN): Obj { if (list.size != 1) throw ScriptError(pos, "expected one argument, got ${list.size}") - return list.first() + return list.first().byValueCopy() } /** diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ClosureScope.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ClosureScope.kt new file mode 100644 index 0000000..848dd99 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ClosureScope.kt @@ -0,0 +1,16 @@ +package net.sergeych.lyng + +import net.sergeych.lyng.obj.ObjRecord + +/** + * Scope that adds a "closure" to caller; most often it is used to apply class instance to caller scope. + * Inherits [Scope.args] and [Scope.thisObj] from [callScope] and adds lookup for symbols + * from [closureScope] with proper precedence + */ +class ClosureScope(val callScope: Scope,val closureScope: Scope) : Scope(callScope, callScope.args, thisObj = callScope.thisObj) { + + override fun get(name: String): ObjRecord? { + // closure should be treated below callScope + return super.get(name) ?: closureScope.get(name) + } +} \ No newline at end of file diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index e234062..6bf9e72 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -470,7 +470,7 @@ class Compiler( val callStatement = statement { // and the source closure of the lambda which might have other thisObj. - val context = AppliedScope(closure!!, args, this) + val context = ClosureScope(this, closure!!) //AppliedScope(closure!!, args, this) if (argsDeclaration == null) { // no args: automatic var 'it' val l = args.list @@ -1540,7 +1540,8 @@ class Compiler( } } - private suspend fun parseFunctionDeclaration( + private suspend fun + parseFunctionDeclaration( visibility: Visibility = Visibility.Public, @Suppress("UNUSED_PARAMETER") isOpen: Boolean = false, isExtern: Boolean = false, @@ -1586,9 +1587,11 @@ class Compiler( val fnBody = statement(t.pos) { callerContext -> callerContext.pos = start + // restore closure where the function was defined, and making a copy of it // for local space (otherwise it will write local stuff to closure!) - val context = closure?.copy() ?: callerContext.raiseError("bug: closure not set") + val context = closure?.let { ClosureScope(callerContext, it) } + ?: callerContext.raiseError("bug: closure not set") // load params from caller context argsDeclaration.assignToContext(context, callerContext.args, defaultAccessType = AccessType.Val) @@ -1597,7 +1600,7 @@ class Compiler( } fnStatements.execute(context) } - val fnCreatestatement = statement(start) { context -> + val fnCreateStatement = statement(start) { context -> // we added fn in the context. now we must save closure // for the function closure = context @@ -1606,7 +1609,12 @@ class Compiler( val type = context[typeName]?.value ?: context.raiseSymbolNotFound("class $typeName not found") if (type !is ObjClass) context.raiseClassCastError("$typeName is not the class instance") type.addFn(name, isOpen = true) { - fnBody.execute(this) + // ObjInstance has a fixed instance scope, so we need to build a closure + (thisObj as? ObjInstance)?.let { i -> + fnBody.execute(ClosureScope(this, i.instanceScope)) + } + // other classes can create one-time scope for this rare case: + ?: fnBody.execute(thisObj.autoInstanceScope(this)) } } // regular function/method @@ -1616,10 +1624,10 @@ class Compiler( fnBody } return if (isStatic) { - currentInitScope += fnCreatestatement + currentInitScope += fnCreateStatement NopStatement } else - fnCreatestatement + fnCreateStatement } private suspend fun parseBlock(skipLeadingBrace: Boolean = false): Statement { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt index 49073f2..140a00b 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt @@ -16,7 +16,7 @@ import net.sergeych.lyng.pacman.ImportProvider * * There are special types of scopes: * - * - [AppliedScope] - scope used to apply a closure to some thisObj scope + * - [ClosureScope] - scope used to apply a closure to some thisObj scope */ open class Scope( val parent: Scope?, @@ -73,7 +73,7 @@ open class Scope( inline fun requiredArg(index: Int): T { if (args.list.size <= index) raiseError("Expected at least ${index + 1} argument, got ${args.list.size}") - return (args.list[index] as? T) + return (args.list[index].byValueCopy() as? T) ?: raiseClassCastError("Expected type ${T::class.simpleName}, got ${args.list[index]::class.simpleName}") } @@ -94,8 +94,15 @@ open class Scope( raiseError("This function does not accept any arguments") } - inline fun thisAs(): T = (thisObj as? T) - ?: raiseClassCastError("Cannot cast ${thisObj.objClass.className} to ${T::class.simpleName}") + inline fun thisAs(): T { + var s: Scope? = this + do { + val t = s!!.thisObj + if (t is T) return t + s = s.parent + } while(s != null) + raiseClassCastError("Cannot cast ${thisObj.objClass.className} to ${T::class.simpleName}") + } internal val objects = mutableMapOf() @@ -193,6 +200,11 @@ open class Scope( val importManager by lazy { (currentImportProvider as? ImportManager) ?: throw IllegalStateException("this scope has no manager in the chain (provided $currentImportProvider") } + override fun toString(): String { + val contents = objects.entries.joinToString { "${if( it.value.isMutable ) "var" else "val" } ${it.key}=${it.value.value}" } + return "S[this=$thisObj $contents]" + } + companion object { fun new(): Scope = diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt index 2888b7c..760938c 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt @@ -153,6 +153,16 @@ class Script( } result ?: raiseError(ObjAssertionFailedException(this,"Expected exception but nothing was thrown")) } + addFn("traceScope") { + println("trace Scope: $this") + var p = this.parent + var level = 0 + while (p != null) { + println(" parent#${++level}: $p") + p = p.parent + } + ObjVoid + } addVoidFn("delay") { delay((this.args.firstAndOnly().toDouble()/1000.0).roundToLong()) @@ -182,7 +192,7 @@ class Script( addConst("Mutex", ObjMutex.type) addFn("launch") { - val callable = args.firstAndOnly() as Statement + val callable = requireOnlyArg() ObjDeferred(globalDefer { callable.execute(this@addFn) }) @@ -193,6 +203,9 @@ class Script( ObjVoid } + addFn("flow") { + ObjFlow(requireOnlyArg()) + } val pi = ObjReal(PI) addConst("π", pi) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ScriptError.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ScriptError.kt index 67f0ffe..51ec459 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ScriptError.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ScriptError.kt @@ -14,6 +14,8 @@ open class ScriptError(val pos: Pos, val errorMessage: String, cause: Throwable? cause ) +class ScriptFlowIsNoMoreCollected: Exception() + class ExecutionError(val errorObject: ObjException) : ScriptError(errorObject.scope.pos, errorObject.message) class ImportException(pos: Pos, message: String) : ScriptError(pos, message) \ No newline at end of file diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt index 50ce923..8c705fa 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt @@ -55,7 +55,8 @@ open class Obj { scope: Scope, name: String, args: Arguments = Arguments.EMPTY - ): T = invokeInstanceMethod(scope, name, args) as T + ): T = + invokeInstanceMethod(scope, name, args) as T /** * Invoke a method of the object if exists @@ -68,7 +69,10 @@ open class Obj { args: Arguments = Arguments.EMPTY, onNotFoundResult: Obj?=null ): Obj = - objClass.getInstanceMemberOrNull(name)?.value?.invoke(scope, this, args) + objClass.getInstanceMemberOrNull(name)?.value?.invoke( + scope, + this, + args) ?: onNotFoundResult ?: scope.raiseSymbolNotFound(name) @@ -251,6 +255,14 @@ open class Obj { scope.raiseNotImplemented() } + fun autoInstanceScope(parent: Scope): Scope { + val scope = parent.copy(newThisObj = this, args = parent.args) + for( m in objClass.members) { + scope.objects[m.key] = m.value + } + return scope + } + companion object { val rootObjectType = ObjClass("Obj").apply { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt index 8e395e2..648d16d 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt @@ -36,7 +36,7 @@ open class ObjClass( /** * members: fields most often. These are called with [ObjInstance] withs ths [ObjInstance.objClass] */ - private val members = mutableMapOf() + internal val members = mutableMapOf() override fun toString(): String = className diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjFlow.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjFlow.kt new file mode 100644 index 0000000..6707214 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjFlow.kt @@ -0,0 +1,141 @@ +package net.sergeych.lyng.obj + +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.ChannelResult +import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.channels.SendChannel +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import net.sergeych.lyng.Scope +import net.sergeych.lyng.ScriptFlowIsNoMoreCollected +import net.sergeych.lyng.Statement +import net.sergeych.mp_tools.globalLaunch +import kotlin.coroutines.cancellation.CancellationException + + +class ObjFlowBuilder(val output: SendChannel) : Obj() { + + override val objClass = type + + companion object { + @OptIn(DelicateCoroutinesApi::class) + val type = object : ObjClass("FlowBuilder") {}.apply { + addFn("emit") { + val data = requireOnlyArg() + println("well well $data") + try { + println("builder ${thisAs().hashCode()}") + println("channel ${thisAs().output.hashCode()}") + val channel = thisAs().output + if( !channel.isClosedForSend ) + channel.send(data) + else + throw ScriptFlowIsNoMoreCollected() + } catch (x: Exception) { + if( x !is CancellationException ) + x.printStackTrace() + throw ScriptFlowIsNoMoreCollected() + } + ObjVoid + } + } + } +} + +private fun createLyngFlowInput(scope: Scope, producer: Statement): ReceiveChannel { + val channel = Channel(Channel.RENDEZVOUS) + val builder = ObjFlowBuilder(channel) + val builderScope = scope.copy(newThisObj = builder) + globalLaunch { + try { + producer.execute(builderScope) + } + catch(x: ScriptFlowIsNoMoreCollected) { + x.printStackTrace() + // premature flow closing, OK + } + catch(x: Exception) { + x.printStackTrace() + } + channel.close() + } + return channel +} + +class ObjFlow(val producer: Statement) : Obj() { + + override val objClass = type + + companion object { + val type = object : ObjClass("Flow", ObjIterable) { + override suspend fun callOn(scope: Scope): Obj { + scope.raiseError("Flow constructor is not available") + } + }.apply { + addFn("iterator") { + println("called iterator!") + ObjFlowIterator(thisAs().producer) + } + } + } +} + + +class ObjFlowIterator(val producer: Statement) : Obj() { + + override val objClass: ObjClass = type + + private var channel: ReceiveChannel? = null + + private var nextItem: ChannelResult? = null + + private var isCancelled = false + + private fun checkNotCancelled(scope: Scope) { + if( isCancelled ) + scope.raiseIllegalState("iteration is cancelled") + } + suspend fun hasNext(scope: Scope): ObjBool { + checkNotCancelled(scope) + // cold start: + if (channel == null) channel = createLyngFlowInput(scope, producer) + if (nextItem == null) nextItem = channel!!.receiveCatching() + return ObjBool(nextItem!!.isSuccess) + } + + suspend fun next(scope: Scope): Obj { + checkNotCancelled(scope) + if (hasNext(scope).value == false) scope.raiseIllegalState("iteration is done") + return nextItem!!.getOrThrow().also { nextItem = null } + } + + private val access = Mutex() + suspend fun cancel() { + access.withLock { + if (!isCancelled) { + isCancelled = true + channel?.cancel() + } + } + } + + companion object { + val type = object : ObjClass("FlowIterator", ObjIterator) { + + }.apply { + addFn("hasNext") { + thisAs().hasNext(this).toObj() + } + addFn("next") { + val x = thisAs() + x.next(this) + } + addFn("cancelIteration") { + val x = thisAs() + x.cancel() + ObjVoid + } + } + } +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt index 736e6d6..574f0a6 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt @@ -34,7 +34,10 @@ class ObjInstance(override val objClass: ObjClass) : Obj() { onNotFoundResult: Obj?): Obj = instanceScope[name]?.let { if (it.visibility.isPublic) - it.value.invoke(scope, this, args) + it.value.invoke( + instanceScope, + this, + args) else scope.raiseError(ObjAccessException(scope, "can't invoke non-public method $name")) } 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 8788905..653d5a6 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjIterable.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjIterable.kt @@ -53,7 +53,7 @@ val ObjIterable by lazy { addFn("toMap") { val result = ObjMap() thisObj.toFlow(this).collect { pair -> - result.map[pair.getAt(this,0)] = pair.getAt(this, 1) + result.map[pair.getAt(this, 0)] = pair.getAt(this, 1) } result } @@ -86,6 +86,24 @@ val ObjIterable by lazy { ObjList(result) } + addFn("take") { + var n = requireOnlyArg().value.toInt() + val result = mutableListOf() + if (n > 0) { + thisObj.enumerate(this) { + result += it + --n > 0 + } + } + ObjList(result) + } + +// addFn("drop" ) { +// var n = requireOnlyArg().value.toInt() +// if( n < 0 ) raiseIllegalArgument("drop($n): should be positive") +// val it = callMethod<>() +// } + addFn("isEmpty") { ObjBool( thisObj.invokeInstanceMethod(this, "iterator") diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjIterator.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjIterator.kt index f14f4db..28d0b41 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjIterator.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjIterator.kt @@ -1,3 +1,4 @@ package net.sergeych.lyng.obj -val ObjIterator by lazy { ObjClass("Iterator") } \ No newline at end of file +val ObjIterator by lazy { ObjClass("Iterator") } + diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjKotlinIterator.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjKotlinIterator.kt index 686a2db..3543580 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjKotlinIterator.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjKotlinIterator.kt @@ -55,4 +55,26 @@ fun Obj.toFlow(scope: Scope): Flow = flow { while (hasNext.invoke(scope, iterator).toBool()) { emit(next.invoke(scope, iterator)) } +} + +/** + * Call [callback] for each element of this obj considering it provides [Iterator] + * methods `hasNext` and `next`. + * + * IF callback returns false, iteration is stopped. + */ +suspend fun Obj.enumerate(scope: Scope,callback: suspend (Obj)->Boolean) { + val iterator = invokeInstanceMethod(scope, "iterator") + val hasNext = iterator.getInstanceMethod(scope, "hasNext") + val next = iterator.getInstanceMethod(scope, "next") + var closeIt = false + while (hasNext.invoke(scope, iterator).toBool()) { + val nextValue = next.invoke(scope, iterator) + if( !callback(nextValue) ) { + closeIt = true + break + } + } + if( closeIt ) + iterator.invokeInstanceMethod(scope, "cancelIteration", onNotFoundResult = ObjVoid) } \ No newline at end of file diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjString.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjString.kt index b614973..8152eef 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjString.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjString.kt @@ -93,7 +93,9 @@ data class ObjString(val value: String) : Obj() { ObjString(decoder.unpackBinaryData().decodeToString()) }.apply { addFn("toInt") { - ObjInt(thisAs().value.toLong()) + ObjInt(thisAs().value.toLongOrNull() + ?: raiseIllegalArgument("can't convert to int: $thisObj") + ) } addFn("startsWith") { ObjBool(thisAs().value.startsWith(requiredArg(0).value)) @@ -137,7 +139,9 @@ data class ObjString(val value: String) : Obj() { } addFn("encodeUtf8") { ObjBuffer(thisAs().value.encodeToByteArray().asUByteArray()) } addFn("size") { ObjInt(thisAs().value.length.toLong()) } - addFn("toReal") { ObjReal(thisAs().value.toDouble()) } + addFn("toReal") { + ObjReal(thisAs().value.toDouble()) + } } } } \ No newline at end of file diff --git a/lynglib/src/commonTest/kotlin/CoroutinesTest.kt b/lynglib/src/commonTest/kotlin/CoroutinesTest.kt index 2979fbc..d149878 100644 --- a/lynglib/src/commonTest/kotlin/CoroutinesTest.kt +++ b/lynglib/src/commonTest/kotlin/CoroutinesTest.kt @@ -63,4 +63,55 @@ class TestCoroutines { """.trimIndent() ) } + + @Test + fun testFlows() = runTest { + eval(""" + val f = flow { + println("Starting generator") + var n1 = 0 + var n2 = 1 + emit(n1) + emit(n2) + while(true) { + val n = n1 + n2 + emit(n) + n1 = n2 + n2 = n + } + } + val correctFibs = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765] + assertEquals( correctFibs, f.take(correctFibs.size)) + + """.trimIndent()) + } + + @Test + fun testFlow2() = runTest { + eval(""" + val f = flow { + println("Starting generator") + emit("start") + emit("start2") + println("Emitting") + (1..4).forEach { +// println("you hoo "+it) + emit(it) + } + println("Done emitting") + } + // let's collect flow: + val result = [] +// for( x in f ) result += x + println(result) + + // let's collect it once again: + println(f.toList()) + println(f.toList()) +// for( x in f ) println(x) +// for( x in f ) println(x) + + //assertEquals( result, f.toList() ) + """.trimIndent()) + } } \ No newline at end of file diff --git a/lynglib/src/commonTest/kotlin/ScriptTest.kt b/lynglib/src/commonTest/kotlin/ScriptTest.kt index a06084d..c9fd15a 100644 --- a/lynglib/src/commonTest/kotlin/ScriptTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptTest.kt @@ -1274,6 +1274,106 @@ class ScriptTest { ) } + @Test + fun testCaptureLocals() = runTest { + eval( + """ + + fun outer(prefix) { + val p1 = "0" + prefix + { + p1 + "2" + it + } + } + fun outer2(prefix) { + val p1 = "*" + prefix + { + p1 + "2" + it + } + } + val x = outer("1") + val y = outer2("1") + println(x("!")) + assertEquals( "0123", x("3") ) + assertEquals( "*123", y("3") ) + """.trimIndent() + ) + } + + @Test + fun testInstanceCallScopeIsCorrect() = runTest { + eval( + """ + + val prefix = ":" + + class T(text) { + fun getText() { + println(text) + prefix + text + "!" + } + } + + val text = "invalid" + + val t1 = T("foo") + val t2 = T("bar") + + // get inside the block + for( i in 1..3 ) { + assertEquals( "foo", t1.text ) + assertEquals( ":foo!", t1.getText() ) + assertEquals( "bar", t2.text ) + assertEquals( ":bar!", t2.getText() ) + } + """.trimIndent() + ) + } + + @Test + fun testAppliedScopes() = runTest { + eval( + """ + class T(text) { + fun getText() { + println(text) + text + "!" + } + } + + val prefix = ":" + val lambda = { + prefix + getText() + "!" + } + + val text = "invalid" + val t1 = T("foo") + val t2 = T("bar") + + t1.apply { + // it must take "text" from class t1: + assertEquals("foo", text) + assertEquals( "foo!", getText() ) + assertEquals( ":foo!!", lambda() ) + } + t2.apply { + assertEquals("bar", text) + assertEquals( "bar!", getText() ) + assertEquals( ":bar!!", lambda() ) + } + // worst case: names clash + fun badOne() { + val prefix = "&" + t1.apply { + assertEquals( ":foo!!", lambda() ) + } + } + badOne() + + """.trimIndent() + ) + } + @Test fun testLambdaWithArgsEllipsis() = runTest { eval( @@ -2244,10 +2344,12 @@ class ScriptTest { @Test fun testSet2() = runTest { - eval(""" + eval( + """ assertEquals( Set( ...[1,2,3]), Set(1,2,3) ) assertEquals( Set( ...[1,false,"ok"]), Set("ok", 1, false) ) - """.trimIndent()) + """.trimIndent() + ) } @Test @@ -2284,9 +2386,11 @@ class ScriptTest { eval( """ class Point(x,y) + // see the difference: apply changes this to newly created Point: val p = Point(1,2).apply { - this.x++; this.y++ + this.x++ + y++ } assertEquals(p, Point(2,3)) >>> void @@ -2305,6 +2409,10 @@ class ScriptTest { } fun Object.isInteger() { + println(this) + println(this is Int) + println(this is Real) + println(this is String) when(this) { is Int -> true is Real -> toInt() == this @@ -2319,7 +2427,7 @@ class ScriptTest { assert( 12.isInteger() == true ) assert( 12.1.isInteger() == false ) assert( "5".isInteger() ) - assert( ! "5.2".isInteger() ) + assert( !"5.2".isInteger() ) """.trimIndent() ) } @@ -2446,20 +2554,26 @@ class ScriptTest { fun testDefaultImportManager() = runTest { val scope = Scope.new() assertFails { - scope.eval(""" + scope.eval( + """ import foo foo() - """.trimIndent()) + """.trimIndent() + ) } - scope.importManager.addTextPackages(""" + scope.importManager.addTextPackages( + """ package foo fun foo() { "bar" } - """.trimIndent()) - scope.eval(""" + """.trimIndent() + ) + scope.eval( + """ import foo assertEquals( "bar", foo()) - """.trimIndent()) + """.trimIndent() + ) } @Test @@ -2478,7 +2592,8 @@ class ScriptTest { @Test fun testBuffer() = runTest { - eval(""" + eval( + """ import lyng.buffer assertEquals( 0, Buffer().size ) @@ -2497,12 +2612,14 @@ class ScriptTest { assertEquals(101, buffer[2]) assertEquals("Heelo", buffer.decodeUtf8()) - """.trimIndent()) + """.trimIndent() + ) } @Test fun testBufferCompare() = runTest { - eval(""" + eval( + """ import lyng.buffer println("Hello".characters()) @@ -2520,7 +2637,8 @@ class ScriptTest { assertEquals("foo", map[b2]) assertEquals(null, map[b3]) - """.trimIndent()) + """.trimIndent() + ) } @Test @@ -2548,6 +2666,7 @@ class ScriptTest { ) delay(1000) } + @Test fun testTimeStatics() = runTest { eval( @@ -2589,7 +2708,8 @@ class ScriptTest { println(Script.defaultImportManager.packageNames) println(s.importManager.packageNames) - s.importManager.addTextPackages(""" + s.importManager.addTextPackages( + """ package foo import lyng.time @@ -2597,8 +2717,10 @@ class ScriptTest { fun foo() { println("foo: %s"(Instant())) } - """.trimIndent()) - s.importManager.addTextPackages(""" + """.trimIndent() + ) + s.importManager.addTextPackages( + """ package bar import lyng.time @@ -2606,24 +2728,28 @@ class ScriptTest { fun bar() { println("bar: %s"(Instant())) } - """.trimIndent()) + """.trimIndent() + ) println(s.importManager.packageNames) - s.eval(""" + s.eval( + """ import foo import bar foo() bar() - """.trimIndent()) + """.trimIndent() + ) } @Test fun testIndexIntIncrements() = runTest { - eval(""" + eval( + """ val x = [1,2,3] x[1]++ ++x[0] @@ -2636,12 +2762,14 @@ class ScriptTest { assert( b == Buffer(1,3,3) ) ++b[0] assertEquals( b, Buffer(2,3,3) ) - """.trimIndent()) + """.trimIndent() + ) } @Test fun testIndexIntDecrements() = runTest { - eval(""" + eval( + """ val x = [1,2,3] x[1]-- --x[0] @@ -2654,13 +2782,14 @@ class ScriptTest { assert( b == Buffer(1,1,3) ) --b[0] assertEquals( b, Buffer(0,1,3) ) - """.trimIndent()) + """.trimIndent() + ) } @Test fun testRangeToList() = runTest { val x = eval("""(1..10).toList()""") as ObjList - assertEquals(listOf(1,2,3,4,5,6,7,8,9,10), x.list.map { it.toInt() }) + assertEquals(listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), x.list.map { it.toInt() }) val y = eval("""(-2..3).toList()""") as ObjList println(y.list) }