fix #51 ref #48 flows started. bad closure-based bug fixed

This commit is contained in:
Sergey Chernov 2025-08-08 19:19:50 +03:00
parent f7f020f4d6
commit 1a90b25b1e
20 changed files with 533 additions and 66 deletions

View File

@ -128,3 +128,55 @@ Usage example:
yield() yield()
} while(true) } 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

View File

@ -65,6 +65,7 @@ kotlin {
languageSettings.optIn("kotlin.contracts.ExperimentalContracts") languageSettings.optIn("kotlin.contracts.ExperimentalContracts")
languageSettings.optIn("kotlin.ExperimentalUnsignedTypes") languageSettings.optIn("kotlin.ExperimentalUnsignedTypes")
languageSettings.optIn("kotlin.coroutines.DelicateCoroutinesApi") languageSettings.optIn("kotlin.coroutines.DelicateCoroutinesApi")
languageSettings.optIn("kotlinx.coroutines.flow.DelicateCoroutinesApi")
} }
val commonMain by getting { val commonMain by getting {

View File

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

View File

@ -34,7 +34,8 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
defaultRecordType: ObjRecord.Type = ObjRecord.Type.ConstructorField defaultRecordType: ObjRecord.Type = ObjRecord.Type.ConstructorField
) { ) {
fun assign(a: Item, value: Obj) { 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, a.visibility ?: defaultVisibility,
recordType = defaultRecordType) recordType = defaultRecordType)
} }

View File

@ -36,7 +36,7 @@ data class Arguments(val list: List<Obj>, val tailBlockMode: Boolean = false) :
fun firstAndOnly(pos: Pos = Pos.UNKNOWN): Obj { fun firstAndOnly(pos: Pos = Pos.UNKNOWN): Obj {
if (list.size != 1) throw ScriptError(pos, "expected one argument, got ${list.size}") if (list.size != 1) throw ScriptError(pos, "expected one argument, got ${list.size}")
return list.first() return list.first().byValueCopy()
} }
/** /**

View File

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

View File

@ -470,7 +470,7 @@ class Compiler(
val callStatement = statement { val callStatement = statement {
// and the source closure of the lambda which might have other thisObj. // 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) { if (argsDeclaration == null) {
// no args: automatic var 'it' // no args: automatic var 'it'
val l = args.list val l = args.list
@ -1540,7 +1540,8 @@ class Compiler(
} }
} }
private suspend fun parseFunctionDeclaration( private suspend fun
parseFunctionDeclaration(
visibility: Visibility = Visibility.Public, visibility: Visibility = Visibility.Public,
@Suppress("UNUSED_PARAMETER") isOpen: Boolean = false, @Suppress("UNUSED_PARAMETER") isOpen: Boolean = false,
isExtern: Boolean = false, isExtern: Boolean = false,
@ -1586,9 +1587,11 @@ class Compiler(
val fnBody = statement(t.pos) { callerContext -> val fnBody = statement(t.pos) { callerContext ->
callerContext.pos = start callerContext.pos = start
// restore closure where the function was defined, and making a copy of it // restore closure where the function was defined, and making a copy of it
// for local space (otherwise it will write local stuff to closure!) // 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 // load params from caller context
argsDeclaration.assignToContext(context, callerContext.args, defaultAccessType = AccessType.Val) argsDeclaration.assignToContext(context, callerContext.args, defaultAccessType = AccessType.Val)
@ -1597,7 +1600,7 @@ class Compiler(
} }
fnStatements.execute(context) fnStatements.execute(context)
} }
val fnCreatestatement = statement(start) { context -> val fnCreateStatement = statement(start) { context ->
// we added fn in the context. now we must save closure // we added fn in the context. now we must save closure
// for the function // for the function
closure = context closure = context
@ -1606,7 +1609,12 @@ class Compiler(
val type = context[typeName]?.value ?: context.raiseSymbolNotFound("class $typeName not found") val type = context[typeName]?.value ?: context.raiseSymbolNotFound("class $typeName not found")
if (type !is ObjClass) context.raiseClassCastError("$typeName is not the class instance") if (type !is ObjClass) context.raiseClassCastError("$typeName is not the class instance")
type.addFn(name, isOpen = true) { 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 // regular function/method
@ -1616,10 +1624,10 @@ class Compiler(
fnBody fnBody
} }
return if (isStatic) { return if (isStatic) {
currentInitScope += fnCreatestatement currentInitScope += fnCreateStatement
NopStatement NopStatement
} else } else
fnCreatestatement fnCreateStatement
} }
private suspend fun parseBlock(skipLeadingBrace: Boolean = false): Statement { private suspend fun parseBlock(skipLeadingBrace: Boolean = false): Statement {

View File

@ -16,7 +16,7 @@ import net.sergeych.lyng.pacman.ImportProvider
* *
* There are special types of scopes: * 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( open class Scope(
val parent: Scope?, val parent: Scope?,
@ -73,7 +73,7 @@ open class Scope(
inline fun <reified T : Obj> requiredArg(index: Int): T { inline fun <reified T : Obj> requiredArg(index: Int): T {
if (args.list.size <= index) raiseError("Expected at least ${index + 1} argument, got ${args.list.size}") 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}") ?: 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") raiseError("This function does not accept any arguments")
} }
inline fun <reified T : Obj> thisAs(): T = (thisObj as? T) inline fun <reified T : Obj> thisAs(): T {
?: raiseClassCastError("Cannot cast ${thisObj.objClass.className} to ${T::class.simpleName}") 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<String, ObjRecord>() internal val objects = mutableMapOf<String, ObjRecord>()
@ -193,6 +200,11 @@ open class Scope(
val importManager by lazy { (currentImportProvider as? ImportManager) val importManager by lazy { (currentImportProvider as? ImportManager)
?: throw IllegalStateException("this scope has no manager in the chain (provided $currentImportProvider") } ?: 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 { companion object {
fun new(): Scope = fun new(): Scope =

View File

@ -153,6 +153,16 @@ class Script(
} }
result ?: raiseError(ObjAssertionFailedException(this,"Expected exception but nothing was thrown")) 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") { addVoidFn("delay") {
delay((this.args.firstAndOnly().toDouble()/1000.0).roundToLong()) delay((this.args.firstAndOnly().toDouble()/1000.0).roundToLong())
@ -182,7 +192,7 @@ class Script(
addConst("Mutex", ObjMutex.type) addConst("Mutex", ObjMutex.type)
addFn("launch") { addFn("launch") {
val callable = args.firstAndOnly() as Statement val callable = requireOnlyArg<Statement>()
ObjDeferred(globalDefer { ObjDeferred(globalDefer {
callable.execute(this@addFn) callable.execute(this@addFn)
}) })
@ -193,6 +203,9 @@ class Script(
ObjVoid ObjVoid
} }
addFn("flow") {
ObjFlow(requireOnlyArg<Statement>())
}
val pi = ObjReal(PI) val pi = ObjReal(PI)
addConst("π", pi) addConst("π", pi)

View File

@ -14,6 +14,8 @@ open class ScriptError(val pos: Pos, val errorMessage: String, cause: Throwable?
cause cause
) )
class ScriptFlowIsNoMoreCollected: Exception()
class ExecutionError(val errorObject: ObjException) : ScriptError(errorObject.scope.pos, errorObject.message) class ExecutionError(val errorObject: ObjException) : ScriptError(errorObject.scope.pos, errorObject.message)
class ImportException(pos: Pos, message: String) : ScriptError(pos, message) class ImportException(pos: Pos, message: String) : ScriptError(pos, message)

View File

@ -55,7 +55,8 @@ open class Obj {
scope: Scope, scope: Scope,
name: String, name: String,
args: Arguments = Arguments.EMPTY 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 * Invoke a method of the object if exists
@ -68,7 +69,10 @@ open class Obj {
args: Arguments = Arguments.EMPTY, args: Arguments = Arguments.EMPTY,
onNotFoundResult: Obj?=null onNotFoundResult: Obj?=null
): Obj = ): Obj =
objClass.getInstanceMemberOrNull(name)?.value?.invoke(scope, this, args) objClass.getInstanceMemberOrNull(name)?.value?.invoke(
scope,
this,
args)
?: onNotFoundResult ?: onNotFoundResult
?: scope.raiseSymbolNotFound(name) ?: scope.raiseSymbolNotFound(name)
@ -251,6 +255,14 @@ open class Obj {
scope.raiseNotImplemented() 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 { companion object {
val rootObjectType = ObjClass("Obj").apply { val rootObjectType = ObjClass("Obj").apply {

View File

@ -36,7 +36,7 @@ open class ObjClass(
/** /**
* members: fields most often. These are called with [ObjInstance] withs ths [ObjInstance.objClass] * members: fields most often. These are called with [ObjInstance] withs ths [ObjInstance.objClass]
*/ */
private val members = mutableMapOf<String, ObjRecord>() internal val members = mutableMapOf<String, ObjRecord>()
override fun toString(): String = className override fun toString(): String = className

View File

@ -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>) : Obj() {
override val objClass = type
companion object {
@OptIn(DelicateCoroutinesApi::class)
val type = object : ObjClass("FlowBuilder") {}.apply {
addFn("emit") {
val data = requireOnlyArg<Obj>()
println("well well $data")
try {
println("builder ${thisAs<ObjFlowBuilder>().hashCode()}")
println("channel ${thisAs<ObjFlowBuilder>().output.hashCode()}")
val channel = thisAs<ObjFlowBuilder>().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<Obj> {
val channel = Channel<Obj>(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<ObjFlow>().producer)
}
}
}
}
class ObjFlowIterator(val producer: Statement) : Obj() {
override val objClass: ObjClass = type
private var channel: ReceiveChannel<Obj>? = null
private var nextItem: ChannelResult<Obj>? = 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<ObjFlowIterator>().hasNext(this).toObj()
}
addFn("next") {
val x = thisAs<ObjFlowIterator>()
x.next(this)
}
addFn("cancelIteration") {
val x = thisAs<ObjFlowIterator>()
x.cancel()
ObjVoid
}
}
}
}

View File

@ -34,7 +34,10 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
onNotFoundResult: Obj?): Obj = onNotFoundResult: Obj?): Obj =
instanceScope[name]?.let { instanceScope[name]?.let {
if (it.visibility.isPublic) if (it.visibility.isPublic)
it.value.invoke(scope, this, args) it.value.invoke(
instanceScope,
this,
args)
else else
scope.raiseError(ObjAccessException(scope, "can't invoke non-public method $name")) scope.raiseError(ObjAccessException(scope, "can't invoke non-public method $name"))
} }

View File

@ -86,6 +86,24 @@ val ObjIterable by lazy {
ObjList(result) ObjList(result)
} }
addFn("take") {
var n = requireOnlyArg<ObjInt>().value.toInt()
val result = mutableListOf<Obj>()
if (n > 0) {
thisObj.enumerate(this) {
result += it
--n > 0
}
}
ObjList(result)
}
// addFn("drop" ) {
// var n = requireOnlyArg<ObjInt>().value.toInt()
// if( n < 0 ) raiseIllegalArgument("drop($n): should be positive")
// val it = callMethod<>()
// }
addFn("isEmpty") { addFn("isEmpty") {
ObjBool( ObjBool(
thisObj.invokeInstanceMethod(this, "iterator") thisObj.invokeInstanceMethod(this, "iterator")

View File

@ -1,3 +1,4 @@
package net.sergeych.lyng.obj package net.sergeych.lyng.obj
val ObjIterator by lazy { ObjClass("Iterator") } val ObjIterator by lazy { ObjClass("Iterator") }

View File

@ -56,3 +56,25 @@ fun Obj.toFlow(scope: Scope): Flow<Obj> = flow {
emit(next.invoke(scope, iterator)) 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)
}

View File

@ -93,7 +93,9 @@ data class ObjString(val value: String) : Obj() {
ObjString(decoder.unpackBinaryData().decodeToString()) ObjString(decoder.unpackBinaryData().decodeToString())
}.apply { }.apply {
addFn("toInt") { addFn("toInt") {
ObjInt(thisAs<ObjString>().value.toLong()) ObjInt(thisAs<ObjString>().value.toLongOrNull()
?: raiseIllegalArgument("can't convert to int: $thisObj")
)
} }
addFn("startsWith") { addFn("startsWith") {
ObjBool(thisAs<ObjString>().value.startsWith(requiredArg<ObjString>(0).value)) ObjBool(thisAs<ObjString>().value.startsWith(requiredArg<ObjString>(0).value))
@ -137,7 +139,9 @@ data class ObjString(val value: String) : Obj() {
} }
addFn("encodeUtf8") { ObjBuffer(thisAs<ObjString>().value.encodeToByteArray().asUByteArray()) } addFn("encodeUtf8") { ObjBuffer(thisAs<ObjString>().value.encodeToByteArray().asUByteArray()) }
addFn("size") { ObjInt(thisAs<ObjString>().value.length.toLong()) } addFn("size") { ObjInt(thisAs<ObjString>().value.length.toLong()) }
addFn("toReal") { ObjReal(thisAs<ObjString>().value.toDouble()) } addFn("toReal") {
ObjReal(thisAs<ObjString>().value.toDouble())
}
} }
} }
} }

View File

@ -63,4 +63,55 @@ class TestCoroutines {
""".trimIndent() """.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())
}
} }

View File

@ -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 @Test
fun testLambdaWithArgsEllipsis() = runTest { fun testLambdaWithArgsEllipsis() = runTest {
eval( eval(
@ -2244,10 +2344,12 @@ class ScriptTest {
@Test @Test
fun testSet2() = runTest { fun testSet2() = runTest {
eval(""" eval(
"""
assertEquals( Set( ...[1,2,3]), Set(1,2,3) ) assertEquals( Set( ...[1,2,3]), Set(1,2,3) )
assertEquals( Set( ...[1,false,"ok"]), Set("ok", 1, false) ) assertEquals( Set( ...[1,false,"ok"]), Set("ok", 1, false) )
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
@ -2284,9 +2386,11 @@ class ScriptTest {
eval( eval(
""" """
class Point(x,y) class Point(x,y)
// see the difference: apply changes this to newly created Point: // see the difference: apply changes this to newly created Point:
val p = Point(1,2).apply { val p = Point(1,2).apply {
this.x++; this.y++ this.x++
y++
} }
assertEquals(p, Point(2,3)) assertEquals(p, Point(2,3))
>>> void >>> void
@ -2305,6 +2409,10 @@ class ScriptTest {
} }
fun Object.isInteger() { fun Object.isInteger() {
println(this)
println(this is Int)
println(this is Real)
println(this is String)
when(this) { when(this) {
is Int -> true is Int -> true
is Real -> toInt() == this is Real -> toInt() == this
@ -2446,20 +2554,26 @@ class ScriptTest {
fun testDefaultImportManager() = runTest { fun testDefaultImportManager() = runTest {
val scope = Scope.new() val scope = Scope.new()
assertFails { assertFails {
scope.eval(""" scope.eval(
"""
import foo import foo
foo() foo()
""".trimIndent()) """.trimIndent()
)
} }
scope.importManager.addTextPackages(""" scope.importManager.addTextPackages(
"""
package foo package foo
fun foo() { "bar" } fun foo() { "bar" }
""".trimIndent()) """.trimIndent()
scope.eval(""" )
scope.eval(
"""
import foo import foo
assertEquals( "bar", foo()) assertEquals( "bar", foo())
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
@ -2478,7 +2592,8 @@ class ScriptTest {
@Test @Test
fun testBuffer() = runTest { fun testBuffer() = runTest {
eval(""" eval(
"""
import lyng.buffer import lyng.buffer
assertEquals( 0, Buffer().size ) assertEquals( 0, Buffer().size )
@ -2497,12 +2612,14 @@ class ScriptTest {
assertEquals(101, buffer[2]) assertEquals(101, buffer[2])
assertEquals("Heelo", buffer.decodeUtf8()) assertEquals("Heelo", buffer.decodeUtf8())
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
fun testBufferCompare() = runTest { fun testBufferCompare() = runTest {
eval(""" eval(
"""
import lyng.buffer import lyng.buffer
println("Hello".characters()) println("Hello".characters())
@ -2520,7 +2637,8 @@ class ScriptTest {
assertEquals("foo", map[b2]) assertEquals("foo", map[b2])
assertEquals(null, map[b3]) assertEquals(null, map[b3])
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
@ -2548,6 +2666,7 @@ class ScriptTest {
) )
delay(1000) delay(1000)
} }
@Test @Test
fun testTimeStatics() = runTest { fun testTimeStatics() = runTest {
eval( eval(
@ -2589,7 +2708,8 @@ class ScriptTest {
println(Script.defaultImportManager.packageNames) println(Script.defaultImportManager.packageNames)
println(s.importManager.packageNames) println(s.importManager.packageNames)
s.importManager.addTextPackages(""" s.importManager.addTextPackages(
"""
package foo package foo
import lyng.time import lyng.time
@ -2597,8 +2717,10 @@ class ScriptTest {
fun foo() { fun foo() {
println("foo: %s"(Instant())) println("foo: %s"(Instant()))
} }
""".trimIndent()) """.trimIndent()
s.importManager.addTextPackages(""" )
s.importManager.addTextPackages(
"""
package bar package bar
import lyng.time import lyng.time
@ -2606,24 +2728,28 @@ class ScriptTest {
fun bar() { fun bar() {
println("bar: %s"(Instant())) println("bar: %s"(Instant()))
} }
""".trimIndent()) """.trimIndent()
)
println(s.importManager.packageNames) println(s.importManager.packageNames)
s.eval(""" s.eval(
"""
import foo import foo
import bar import bar
foo() foo()
bar() bar()
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
fun testIndexIntIncrements() = runTest { fun testIndexIntIncrements() = runTest {
eval(""" eval(
"""
val x = [1,2,3] val x = [1,2,3]
x[1]++ x[1]++
++x[0] ++x[0]
@ -2636,12 +2762,14 @@ class ScriptTest {
assert( b == Buffer(1,3,3) ) assert( b == Buffer(1,3,3) )
++b[0] ++b[0]
assertEquals( b, Buffer(2,3,3) ) assertEquals( b, Buffer(2,3,3) )
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
fun testIndexIntDecrements() = runTest { fun testIndexIntDecrements() = runTest {
eval(""" eval(
"""
val x = [1,2,3] val x = [1,2,3]
x[1]-- x[1]--
--x[0] --x[0]
@ -2654,7 +2782,8 @@ class ScriptTest {
assert( b == Buffer(1,1,3) ) assert( b == Buffer(1,1,3) )
--b[0] --b[0]
assertEquals( b, Buffer(0,1,3) ) assertEquals( b, Buffer(0,1,3) )
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test