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()
} 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.ExperimentalUnsignedTypes")
languageSettings.optIn("kotlin.coroutines.DelicateCoroutinesApi")
languageSettings.optIn("kotlinx.coroutines.flow.DelicateCoroutinesApi")
}
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
) {
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)
}

View File

@ -36,7 +36,7 @@ data class Arguments(val list: List<Obj>, 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()
}
/**

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 {
// 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 {

View File

@ -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 <reified T : Obj> 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 <reified T : Obj> thisAs(): T = (thisObj as? T)
?: raiseClassCastError("Cannot cast ${thisObj.objClass.className} to ${T::class.simpleName}")
inline fun <reified T : Obj> 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<String, ObjRecord>()
@ -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 =

View File

@ -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<Statement>()
ObjDeferred(globalDefer {
callable.execute(this@addFn)
})
@ -193,6 +203,9 @@ class Script(
ObjVoid
}
addFn("flow") {
ObjFlow(requireOnlyArg<Statement>())
}
val pi = ObjReal(PI)
addConst("π", pi)

View File

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

View File

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

View File

@ -36,7 +36,7 @@ open class 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

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

View File

@ -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<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") {
ObjBool(
thisObj.invokeInstanceMethod(this, "iterator")

View File

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

View File

@ -55,4 +55,26 @@ fun Obj.toFlow(scope: Scope): Flow<Obj> = 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)
}

View File

@ -93,7 +93,9 @@ data class ObjString(val value: String) : Obj() {
ObjString(decoder.unpackBinaryData().decodeToString())
}.apply {
addFn("toInt") {
ObjInt(thisAs<ObjString>().value.toLong())
ObjInt(thisAs<ObjString>().value.toLongOrNull()
?: raiseIllegalArgument("can't convert to int: $thisObj")
)
}
addFn("startsWith") {
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("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()
)
}
@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
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)
}